// LICENSE BSD-2-Clause-FreeBSD // Copyright (c) 2018, Rohan Verma package simples3 import ( "bytes" "crypto/md5" "crypto/sha256" "encoding/base64" "encoding/xml" "fmt" "io" "mime/multipart" "net/http" "net/url" "strings" "time" ) type DownloadInput struct { Bucket string ObjectKey string VersionId string // Optional: Version ID of the object to download } // DetailsInput is passed to FileDetails as a parameter. type DetailsInput struct { Bucket string ObjectKey string VersionId string // Optional: Version ID of the object to retrieve details for } // DetailsResponse is returned by FileDetails. type DetailsResponse struct { ContentType string ContentLength string AcceptRanges string Date string Etag string LastModified string Server string AmzID2 string AmzRequestID string // Server-side encryption (e.g. "AES256" or "aws:kms") ServerSideEncryption string // KMS Key ID (ARN or ID) when ServerSideEncryption is "aws:kms" SSEKMSKeyId string AmzMeta map[string]string ExtraHeaders map[string]string } // UploadInput is passed to FileUpload as a parameter. type UploadInput struct { // essential fields Bucket string ObjectKey string FileName string ContentType string // optional fields ContentDisposition string ACL string // Setting key/value pairs adds user-defined metadata // keys to the object, prefixed with AMZMetaPrefix. CustomMetadata map[string]string // Setting key/value pairs adds tags to the object (max 10 tags) Tags map[string]string // Server-side encryption (e.g. "AES256" or "aws:kms") ServerSideEncryption string // KMS Key ID (ARN or ID) when ServerSideEncryption is "aws:kms" SSEKMSKeyId string Body io.ReadSeeker } // UploadResponse receives the following XML // in case of success, since we set a 201 response from S3. // Sample response: // // // https://s3.amazonaws.com/link-to-the-file // s3-bucket // development/8614bd40-691b-4668-9241-3b342c6cf429/image.jpg // "32-bit-tag" // type UploadResponse struct { Location string `xml:"Location"` Bucket string `xml:"Bucket"` Key string `xml:"Key"` ETag string `xml:"ETag"` } // PutResponse is returned when the action is successful, // and the service sends back an HTTP 200 response. The response // returns ETag along with HTTP headers. type PutResponse struct { ETag string Headers http.Header } // DeleteInput is passed to FileDelete as a parameter. type DeleteInput struct { Bucket string ObjectKey string VersionId string // Optional: Version ID of the object to delete } // FileDownload makes a GET call and returns a io.ReadCloser. // After reading the response body, ensure closing the response. func (s3 *S3) FileDownload(u DownloadInput) (io.ReadCloser, error) { urlStr := s3.getURL(u.Bucket, u.ObjectKey) if u.VersionId != "" { parsed, err := url.Parse(urlStr) if err != nil { return nil, err } q := parsed.Query() q.Set("versionId", u.VersionId) parsed.RawQuery = q.Encode() urlStr = parsed.String() } req, err := http.NewRequest( http.MethodGet, urlStr, nil, ) if err != nil { return nil, err } if err := s3.renewIAMToken(); err != nil { return nil, err } if err := s3.signRequest(req); err != nil { return nil, err } res, err := s3.getClient().Do(req) if err != nil { return nil, err } if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("status code: %s", res.Status) } return res.Body, nil } // FilePut makes a PUT call to S3. func (s3 *S3) FilePut(u UploadInput) (PutResponse, error) { fSize, err := detectFileSize(u.Body) if err != nil { return PutResponse{}, err } content := make([]byte, fSize) _, err = u.Body.Read(content) if err != nil { return PutResponse{}, err } u.Body.Seek(0, 0) req, er := http.NewRequest(http.MethodPut, s3.getURL(u.Bucket, u.ObjectKey), u.Body) if er != nil { return PutResponse{}, err } if u.ContentType == "" { u.ContentType = "application/octet-stream" } h := sha256.New() h.Write(content) req.Header.Set("x-amz-content-sha256", fmt.Sprintf("%x", h.Sum(nil))) req.Header.Set("Content-Type", u.ContentType) req.Header.Set("Content-Length", fmt.Sprintf("%d", fSize)) req.Header.Set("Host", req.URL.Host) for k, v := range u.CustomMetadata { req.Header.Set("x-amz-meta-"+k, v) } if u.ContentDisposition != "" { req.Header.Set("Content-Disposition", u.ContentDisposition) } if u.ACL != "" { req.Header.Set("x-amz-acl", u.ACL) } if len(u.Tags) > 0 { req.Header.Set("x-amz-tagging", encodeTagsHeader(u.Tags)) } if u.ServerSideEncryption != "" { req.Header.Set("x-amz-server-side-encryption", u.ServerSideEncryption) } if u.SSEKMSKeyId != "" { req.Header.Set("x-amz-server-side-encryption-aws-kms-key-id", u.SSEKMSKeyId) } req.ContentLength = fSize if err := s3.renewIAMToken(); err != nil { return PutResponse{}, err } if err := s3.signRequest(req); err != nil { return PutResponse{}, err } // debug(httputil.DumpRequest(req, true)) // Submit the request client := s3.getClient() res, err := client.Do(req) if err != nil { return PutResponse{}, err } defer func() { res.Body.Close() io.Copy(io.Discard, res.Body) }() data, err := io.ReadAll(res.Body) if err != nil { return PutResponse{}, err } // Check the response if res.StatusCode != http.StatusOK { return PutResponse{}, fmt.Errorf("status code: %s: %q", res.Status, data) } return PutResponse{ ETag: res.Header.Get("ETag"), Headers: res.Header.Clone(), }, nil } // FileUpload makes a POST call with the file written as multipart // and on successful upload, checks for 200 OK. func (s3 *S3) FileUpload(u UploadInput) (UploadResponse, error) { fSize, err := detectFileSize(u.Body) if err != nil { return UploadResponse{}, err } uc := UploadConfig{ UploadURL: s3.getURL(u.Bucket), BucketName: u.Bucket, ObjectKey: u.ObjectKey, ContentType: u.ContentType, ContentDisposition: u.ContentDisposition, ACL: u.ACL, FileSize: fSize, MetaData: map[string]string{ "success_action_status": "201", // returns XML doc on success }, } // Set custom metadata. for k, v := range u.CustomMetadata { if !strings.HasPrefix(k, AMZMetaPrefix) { k = AMZMetaPrefix + k } uc.MetaData[k] = v } // Set tags if provided. if len(u.Tags) > 0 { uc.MetaData["x-amz-tagging"] = encodeTagsHeader(u.Tags) } // Set server-side encryption if u.ServerSideEncryption != "" { uc.MetaData["x-amz-server-side-encryption"] = u.ServerSideEncryption } if u.SSEKMSKeyId != "" { uc.MetaData["x-amz-server-side-encryption-aws-kms-key-id"] = u.SSEKMSKeyId } if err := s3.renewIAMToken(); err != nil { return UploadResponse{}, err } policies, err := s3.CreateUploadPolicies(uc) if err != nil { return UploadResponse{}, err } var b bytes.Buffer w := multipart.NewWriter(&b) for k, v := range policies.Form { if err = w.WriteField(k, v); err != nil { return UploadResponse{}, err } } fw, err := w.CreateFormFile("file", u.FileName) if err != nil { return UploadResponse{}, err } if _, err = io.Copy(fw, u.Body); err != nil { return UploadResponse{}, err } // Don't forget to close the multipart writer. // If you don't close it, your request will be missing the terminating boundary. if err := w.Close(); err != nil { return UploadResponse{}, err } // Now that you have a form, you can submit it to your handler. req, err := http.NewRequest(http.MethodPost, policies.URL, &b) if err != nil { return UploadResponse{}, err } // Don't forget to set the content type, this will contain the boundary. req.Header.Set("Content-Type", w.FormDataContentType()) // Submit the request client := s3.getClient() res, err := client.Do(req) if err != nil { return UploadResponse{}, err } defer func() { res.Body.Close() io.Copy(io.Discard, res.Body) }() data, err := io.ReadAll(res.Body) if err != nil { return UploadResponse{}, err } // Check the response if res.StatusCode != http.StatusCreated { return UploadResponse{}, fmt.Errorf("status code: %s: %q", res.Status, data) } var ur UploadResponse _ = xml.Unmarshal(data, &ur) return ur, nil } // FileDelete makes a DELETE call with the file written as multipart // and on successful upload, checks for 204 No Content. func (s3 *S3) FileDelete(u DeleteInput) error { urlStr := s3.getURL(u.Bucket, u.ObjectKey) if u.VersionId != "" { parsed, err := url.Parse(urlStr) if err != nil { return err } q := parsed.Query() q.Set("versionId", u.VersionId) parsed.RawQuery = q.Encode() urlStr = parsed.String() } req, err := http.NewRequest( http.MethodDelete, urlStr, nil, ) if err != nil { return err } if err := s3.renewIAMToken(); err != nil { return err } if err := s3.signRequest(req); err != nil { return err } // Submit the request client := s3.getClient() res, err := client.Do(req) if err != nil { return err } defer func() { res.Body.Close() io.Copy(io.Discard, res.Body) }() // Check the response if res.StatusCode != http.StatusNoContent { return fmt.Errorf("status code: %s", res.Status) } return nil } func (s3 *S3) FileDetails(u DetailsInput) (DetailsResponse, error) { urlStr := s3.getURL(u.Bucket, u.ObjectKey) if u.VersionId != "" { parsed, err := url.Parse(urlStr) if err != nil { return DetailsResponse{}, err } q := parsed.Query() q.Set("versionId", u.VersionId) parsed.RawQuery = q.Encode() urlStr = parsed.String() } req, err := http.NewRequest( http.MethodHead, urlStr, nil, ) if err != nil { return DetailsResponse{}, err } if err := s3.renewIAMToken(); err != nil { return DetailsResponse{}, err } if err := s3.signRequest(req); err != nil { return DetailsResponse{}, err } res, err := s3.getClient().Do(req) if err != nil { return DetailsResponse{}, err } defer func() { res.Body.Close() io.Copy(io.Discard, res.Body) }() if res.StatusCode != http.StatusOK { return DetailsResponse{}, fmt.Errorf("status code: %s", res.Status) } var out DetailsResponse for k, v := range res.Header { lk := strings.ToLower(k) switch lk { case "content-type": out.ContentType = getFirstString(v) case "content-length": out.ContentLength = getFirstString(v) case "accept-ranges": out.AcceptRanges = getFirstString(v) case "date": out.Date = getFirstString(v) case "etag": out.Etag = getFirstString(v) case "last-modified": out.LastModified = getFirstString(v) case "server": out.Server = getFirstString(v) case "x-amz-id-2": out.AmzID2 = getFirstString(v) case "x-amz-request-id": out.AmzRequestID = getFirstString(v) case "x-amz-server-side-encryption": out.ServerSideEncryption = getFirstString(v) case "x-amz-server-side-encryption-aws-kms-key-id": out.SSEKMSKeyId = getFirstString(v) default: if strings.HasPrefix(lk, AMZMetaPrefix) { if out.AmzMeta == nil { out.AmzMeta = map[string]string{} } out.AmzMeta[k] = getFirstString(v) } else { if out.ExtraHeaders == nil { out.ExtraHeaders = map[string]string{} } out.ExtraHeaders[k] = getFirstString(v) } } } return out, nil } // CopyObjectInput is passed to CopyObject as a parameter. type CopyObjectInput struct { // Required: Source bucket name SourceBucket string // Required: Source object key SourceKey string // Required: Destination bucket name DestBucket string // Required: Destination object key DestKey string // Optional: COPY (default) or REPLACE // COPY - copies metadata from source // REPLACE - replaces metadata with values specified in this input MetadataDirective string // Optional: Content type (only used when MetadataDirective = REPLACE) ContentType string // Optional: Custom metadata (only used when MetadataDirective = REPLACE) CustomMetadata map[string]string // Optional: Tags to set on the destination object (max 10 tags) // When set, uses x-amz-tagging-directive: REPLACE Tags map[string]string // Optional: Server-side encryption (e.g. "AES256" or "aws:kms") ServerSideEncryption string // Optional: KMS Key ID (ARN or ID) when ServerSideEncryption is "aws:kms" SSEKMSKeyId string } // CopyObjectOutput is returned by CopyObject. type CopyObjectOutput struct { ETag string LastModified time.Time } // copyObjectResult is the internal type for XML parsing. type copyObjectResult struct { ETag string `xml:"ETag"` LastModified time.Time `xml:"LastModified"` } // CopyObject copies an object from source to destination. // Can copy within the same bucket or across buckets. // This operation is server-side, avoiding download/upload cycle. func (s3 *S3) CopyObject(input CopyObjectInput) (CopyObjectOutput, error) { // Validate required fields if input.SourceBucket == "" || input.SourceKey == "" { return CopyObjectOutput{}, fmt.Errorf("source bucket and key are required") } if input.DestBucket == "" || input.DestKey == "" { return CopyObjectOutput{}, fmt.Errorf("destination bucket and key are required") } // Renew IAM token if needed if err := s3.renewIAMToken(); err != nil { return CopyObjectOutput{}, err } // Build destination URL url := s3.getURL(input.DestBucket, input.DestKey) // Create PUT request req, err := http.NewRequest(http.MethodPut, url, nil) if err != nil { return CopyObjectOutput{}, err } // Set x-amz-copy-source header (URL-encoded) copySource := "/" + input.SourceBucket + "/" + encodePath(input.SourceKey) req.Header.Set("x-amz-copy-source", copySource) // Optional metadata directive if input.MetadataDirective != "" { req.Header.Set("x-amz-metadata-directive", input.MetadataDirective) } // If REPLACE, set content-type and custom metadata if input.MetadataDirective == "REPLACE" { if input.ContentType != "" { req.Header.Set("Content-Type", input.ContentType) } for k, v := range input.CustomMetadata { req.Header.Set("x-amz-meta-"+k, v) } } // Set tags if provided (with REPLACE directive to override source tags) // Note: x-amz-tagging-directive causes signature errors in MinIO despite being supported in code // See: https://github.com/minio/minio/pull/9711, https://github.com/minio/minio/pull/9478 // Without directive: tags are copied from source (default S3 behavior) // With directive: signature mismatch error in MinIO (works on AWS S3) // // WORKAROUND: To support both MinIO and AWS without signature errors, we do not set // the tagging headers here. Instead, we apply the tags using PutObjectTagging // after the copy operation is complete. // if len(input.Tags) > 0 { // req.Header.Set("x-amz-tagging-directive", "REPLACE") // req.Header.Set("x-amz-tagging", encodeTagsHeader(input.Tags)) // } if input.ServerSideEncryption != "" { req.Header.Set("x-amz-server-side-encryption", input.ServerSideEncryption) } if input.SSEKMSKeyId != "" { req.Header.Set("x-amz-server-side-encryption-aws-kms-key-id", input.SSEKMSKeyId) } // Sign the request if err := s3.signRequest(req); err != nil { return CopyObjectOutput{}, err } // Execute request client := s3.getClient() res, err := client.Do(req) if err != nil { return CopyObjectOutput{}, err } defer func() { res.Body.Close() io.Copy(io.Discard, res.Body) }() // Read response body body, err := io.ReadAll(res.Body) if err != nil { return CopyObjectOutput{}, err } // Handle non-OK status codes if res.StatusCode != http.StatusOK { return CopyObjectOutput{}, fmt.Errorf("status code: %s: %s", res.Status, string(body)) } // Parse XML response var result copyObjectResult if err := xml.Unmarshal(body, &result); err != nil { return CopyObjectOutput{}, err } // Apply tags if provided (2-step workaround) if len(input.Tags) > 0 { err := s3.PutObjectTagging(PutObjectTaggingInput{ Bucket: input.DestBucket, ObjectKey: input.DestKey, Tags: input.Tags, }) if err != nil { return CopyObjectOutput{ ETag: result.ETag, LastModified: result.LastModified, }, fmt.Errorf("copy succeeded but tagging failed: %v", err) } } return CopyObjectOutput{ ETag: result.ETag, LastModified: result.LastModified, }, nil } // DeleteObjectsInput is passed to DeleteObjects as a parameter. type DeleteObjectsInput struct { // Required: The name of the bucket Bucket string // Required: List of object keys to delete (max 1000) Objects []string // Optional: Quiet mode - only return errors, not successes Quiet bool } // DeleteObjectsOutput is returned by DeleteObjects. type DeleteObjectsOutput struct { Deleted []DeletedObject Errors []DeleteError } // DeletedObject represents a successfully deleted object. type DeletedObject struct { Key string `xml:"Key"` } // DeleteError represents a deletion error. type DeleteError struct { Key string `xml:"Key"` Code string `xml:"Code"` Message string `xml:"Message"` } // deleteRequest is the internal type for XML marshaling of the request. type deleteRequest struct { XMLName xml.Name `xml:"Delete"` XMLNS string `xml:"xmlns,attr"` Quiet bool `xml:"Quiet"` Objects []deleteObject `xml:"Object"` } // deleteObject represents an object to delete in the XML request. type deleteObject struct { Key string `xml:"Key"` } // deleteResult is the internal type for XML parsing of the response. type deleteResult struct { Deleted []DeletedObject `xml:"Deleted"` Errors []DeleteError `xml:"Error"` } // DeleteObjects deletes multiple objects in a single request (max 1000). // This is more efficient than calling FileDelete multiple times. // Returns both successful deletions and errors. func (s3 *S3) DeleteObjects(input DeleteObjectsInput) (DeleteObjectsOutput, error) { // Validate required fields if input.Bucket == "" { return DeleteObjectsOutput{}, fmt.Errorf("bucket name is required") } if len(input.Objects) == 0 { return DeleteObjectsOutput{}, fmt.Errorf("at least one object key is required") } if len(input.Objects) > 1000 { return DeleteObjectsOutput{}, fmt.Errorf("cannot delete more than 1000 objects per request") } // Renew IAM token if needed if err := s3.renewIAMToken(); err != nil { return DeleteObjectsOutput{}, err } // Build XML request body deleteReq := deleteRequest{ XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/", Quiet: input.Quiet, Objects: make([]deleteObject, len(input.Objects)), } for i, key := range input.Objects { deleteReq.Objects[i] = deleteObject{Key: key} } xmlBody, err := xml.Marshal(deleteReq) if err != nil { return DeleteObjectsOutput{}, err } // Calculate Content-MD5 (required by S3 for this operation) md5Hash := md5.Sum(xmlBody) contentMD5 := base64.StdEncoding.EncodeToString(md5Hash[:]) // Build URL with ?delete query parameter baseURL := s3.getURL(input.Bucket) parsedURL, err := url.Parse(baseURL) if err != nil { return DeleteObjectsOutput{}, err } // Add delete query parameter (no value, just "?delete") parsedURL.RawQuery = "delete" // Create POST request req, err := http.NewRequest(http.MethodPost, parsedURL.String(), bytes.NewReader(xmlBody)) if err != nil { return DeleteObjectsOutput{}, err } // Set ContentLength BEFORE other headers req.ContentLength = int64(len(xmlBody)) // Calculate content SHA256 for x-amz-content-sha256 header h := sha256.New() h.Write(xmlBody) req.Header.Set("x-amz-content-sha256", fmt.Sprintf("%x", h.Sum(nil))) req.Header.Set("Content-Type", "application/xml") req.Header.Set("Content-MD5", contentMD5) req.Header.Set("Content-Length", fmt.Sprintf("%d", len(xmlBody))) req.Header.Set("Host", req.URL.Host) // Sign the request if err := s3.signRequest(req); err != nil { return DeleteObjectsOutput{}, err } // Execute request client := s3.getClient() res, err := client.Do(req) if err != nil { return DeleteObjectsOutput{}, err } defer func() { res.Body.Close() io.Copy(io.Discard, res.Body) }() // Read response body body, err := io.ReadAll(res.Body) if err != nil { return DeleteObjectsOutput{}, err } // Handle non-OK status codes if res.StatusCode != http.StatusOK { return DeleteObjectsOutput{}, fmt.Errorf("status code: %s: %s", res.Status, string(body)) } // Parse XML response var result deleteResult if err := xml.Unmarshal(body, &result); err != nil { return DeleteObjectsOutput{}, err } return DeleteObjectsOutput{ Deleted: result.Deleted, Errors: result.Errors, }, nil } // --- ACL Operations --- // PutObjectAclInput is passed to PutObjectAcl. type PutObjectAclInput struct { // Required: The name of the bucket Bucket string // Required: The object key ObjectKey string // Optional: Version ID of the object VersionId string // Optional: Canned ACL (private, public-read, public-read-write, authenticated-read) // If CannedACL is set, AccessControlPolicy is ignored. CannedACL string // Optional: Full Access Control Policy AccessControlPolicy *AccessControlPolicy } // PutObjectAcl sets the Access Control List (ACL) for an object. // You can either use a CannedACL OR provide a full AccessControlPolicy. func (s3 *S3) PutObjectAcl(input PutObjectAclInput) error { // Validate input if input.Bucket == "" { return fmt.Errorf("bucket name is required") } if input.ObjectKey == "" { return fmt.Errorf("object key is required") } if input.CannedACL == "" && input.AccessControlPolicy == nil { return fmt.Errorf("either CannedACL or AccessControlPolicy must be provided") } // Renew IAM token if needed if err := s3.renewIAMToken(); err != nil { return err } // Build URL with ?acl query parameter baseURL := s3.getURL(input.Bucket, input.ObjectKey) parsedURL, err := url.Parse(baseURL) if err != nil { return err } q := parsedURL.Query() q.Set("acl", "") if input.VersionId != "" { q.Set("versionId", input.VersionId) } parsedURL.RawQuery = q.Encode() var req *http.Request var bodyReader io.Reader var contentMD5 string var sha256Hash string if input.AccessControlPolicy != nil { // Use custom ACL body input.AccessControlPolicy.XMLNS = "http://s3.amazonaws.com/doc/2006-03-01/" // Ensure namespace is set on Grantees if needed for i := range input.AccessControlPolicy.AccessControlList { input.AccessControlPolicy.AccessControlList[i].Grantee.XMLNS = "http://www.w3.org/2001/XMLSchema-instance" } xmlBody, err := xml.Marshal(input.AccessControlPolicy) if err != nil { return err } bodyReader = bytes.NewReader(xmlBody) // Calculate MD5 md5Sum := md5.Sum(xmlBody) contentMD5 = base64.StdEncoding.EncodeToString(md5Sum[:]) // Calculate SHA256 h := sha256.New() h.Write(xmlBody) sha256Hash = fmt.Sprintf("%x", h.Sum(nil)) req, err = http.NewRequest(http.MethodPut, parsedURL.String(), bodyReader) if err != nil { return err } req.ContentLength = int64(len(xmlBody)) req.Header.Set("Content-Type", "application/xml") req.Header.Set("Content-MD5", contentMD5) req.Header.Set("x-amz-content-sha256", sha256Hash) } else { // Use Canned ACL via header req, err = http.NewRequest(http.MethodPut, parsedURL.String(), nil) if err != nil { return err } req.Header.Set("x-amz-acl", input.CannedACL) // Empty body SHA256 h := sha256.New() sha256Hash = fmt.Sprintf("%x", h.Sum(nil)) req.Header.Set("x-amz-content-sha256", sha256Hash) } req.Header.Set("Host", req.URL.Host) // Sign the request if err := s3.signRequest(req); err != nil { return err } // Execute request client := s3.getClient() res, err := client.Do(req) if err != nil { return err } defer func() { res.Body.Close() io.Copy(io.Discard, res.Body) }() // Read response body body, err := io.ReadAll(res.Body) if err != nil { return err } // Handle non-OK status codes if res.StatusCode != http.StatusOK { return fmt.Errorf("status code: %s: %s", res.Status, string(body)) } return nil } // GetObjectAclInput is passed to GetObjectAcl. type GetObjectAclInput struct { // Required: The name of the bucket Bucket string // Required: The object key ObjectKey string // Optional: Version ID of the object VersionId string } // GetObjectAcl gets the Access Control List (ACL) for an object. func (s3 *S3) GetObjectAcl(input GetObjectAclInput) (AccessControlPolicy, error) { // Validate input if input.Bucket == "" { return AccessControlPolicy{}, fmt.Errorf("bucket name is required") } if input.ObjectKey == "" { return AccessControlPolicy{}, fmt.Errorf("object key is required") } // Renew IAM token if needed if err := s3.renewIAMToken(); err != nil { return AccessControlPolicy{}, err } // Build URL with ?acl query parameter baseURL := s3.getURL(input.Bucket, input.ObjectKey) parsedURL, err := url.Parse(baseURL) if err != nil { return AccessControlPolicy{}, err } q := parsedURL.Query() q.Set("acl", "") if input.VersionId != "" { q.Set("versionId", input.VersionId) } parsedURL.RawQuery = q.Encode() // Create GET request req, err := http.NewRequest(http.MethodGet, parsedURL.String(), nil) if err != nil { return AccessControlPolicy{}, err } // Sign the request if err := s3.signRequest(req); err != nil { return AccessControlPolicy{}, err } // Execute request client := s3.getClient() res, err := client.Do(req) if err != nil { return AccessControlPolicy{}, err } defer func() { res.Body.Close() io.Copy(io.Discard, res.Body) }() // Read response body body, err := io.ReadAll(res.Body) if err != nil { return AccessControlPolicy{}, err } // Handle non-OK status codes if res.StatusCode != http.StatusOK { return AccessControlPolicy{}, fmt.Errorf("status code: %s: %s", res.Status, string(body)) } // Parse XML response var policy AccessControlPolicy if err := xml.Unmarshal(body, &policy); err != nil { return AccessControlPolicy{}, err } return policy, nil }