using Amazon.Runtime; using Amazon.S3; using Houston.Settings; using Microsoft.Extensions.Options; namespace Houston.Extensions; public static class DocumentsExtensions { extension(IServiceCollection services) { public IServiceCollection AddDocumentsClient() { return services.AddKeyedScoped("Documents", (services, _) => { var settings = services.GetRequiredService>(); var client = new AmazonS3Client( new BasicAWSCredentials(settings.Value.S3.Key, settings.Value.S3.Secret), new AmazonS3Config { ServiceURL = settings.Value.S3.Endpoint }); // The SDK percent-encodes the separators in x-amz-copy-source ("bucket%2Fkey%2Fdoc.pdf"). // AWS decodes that again, our S3 splits bucket and key on the first literal slash and ends up // without a key ("Invalid copy source object key"), so the separators are restored here. The // event runs before the signer, hence the corrected value is the one that gets signed. client.BeforeRequestEvent += (_, args) => { if (args is WebServiceRequestEventArgs { Headers: { } headers } && headers.TryGetValue("x-amz-copy-source", out var copySource) && copySource.Contains("%2F", StringComparison.Ordinal)) { headers["x-amz-copy-source"] = copySource.Replace("%2F", "/", StringComparison.Ordinal); } }; return client; }); } }