如何将压缩文件从 Apex 发送到 S3 存储桶?

How to send a zipped file to S3 bucket from Apex?

各位,

我正在尝试使用 apex class 将数据从 Salesforce 移动到 s3。数据管理员告诉我将 zip/gzip 格式的数据发送到 S3 存储桶以节省存储成本。

我只是尝试做一个 request.setCompressed(true); 因为我读过它在将它发送到端点之前压缩 body .代码如下:

 HttpRequest request = new HttpRequest();
    request.setEndpoint('callout:'+DATA_NAMED_CRED+'/'+URL+'/'+generateUniqueTimeStampforSuffix());
    request.setMethod('PUT');
    request.setBody(JSON.serialize(data));
    request.setCompressed(true);
    request.setHeader('Content-Type','application/json');

但不管怎样我总是收到这个:

<Error><Code>XAmzContentSHA256Mismatch</Code><Message>The provided 'x-amz-content-sha256' header does not match what was computed.</Message><ClientComputedContentSHA256>fd31b2b9115ef77e8076b896cb336d21d8f66947210ffcc9c4d1971b2be3bbbc</ClientComputedContentSHA256><S3ComputedContentSHA256>1e7f2115e60132afed9e61132aa41c3224c6e305ad9f820e6893364d7257ab8d</S3ComputedContentSHA256>

我也尝试了多个headers,比如将内容类型设置为gzip/zip等

任何正确方向的指示都将不胜感激。

尝试做类似的事情时我很头疼。我感觉到你的痛苦。

以下代码使用 lambda 函数对我们有效;您可以尝试修改它,看看会发生什么。

public class AwsApiGateway {

    //  Things we need to know about the service. Set these values in init()
    String host, payloadSha256;
    String resource;
    String service = 'execute-api';
    String region;

    public Url endpoint;
    String accessKey;
    String stage;
    string secretKey;
    HttpMethod method = HttpMethod.XGET;
    //  Remember to set "payload" here if you need to specify a body
    //  payload = Blob.valueOf('some-text-i-want-to-send');
    //  This method helps prevent leaking secret key, 
    //  as it is never serialized
    // Url endpoint;
    // HttpMethod method;
    Blob payload;
    //  Not used externally, so we hide these values
    Blob signingKey;
    DateTime requestTime;
    Map<String, String> queryParams = new map<string,string>(), headerParams = new map<string,string>();

    void init(){
        if (payload == null) payload = Blob.valueOf('');
        requestTime = DateTime.now();
        createSigningKey(secretKey);
    }

    public AwsApiGateway(String resource){

        this.stage = AWS_LAMBDA_STAGE          
        this.resource = '/' + stage + '/' + resource;
        this.region = AWS_REGION;
        this.endpoint = new Url(AWS_ENDPOINT);
        this.accessKey = AWS_ACCESS_KEY;
        this.secretKey = AWS_SECRET_KEY;

    }

    //  Make sure we can't misspell methods
    public enum HttpMethod { XGET, XPUT, XHEAD, XOPTIONS, XDELETE, XPOST }

    public void setMethod (HttpMethod method){
        this.method = method;
    }

    public void setPayload (string payload){
        this.payload = Blob.valueOf(payload);
    }


    //  Add a header
    public void setHeader(String key, String value) {
        headerParams.put(key.toLowerCase(), value);
    }

    //  Add a query param
    public void setQueryParam(String key, String value) {
        queryParams.put(key.toLowerCase(), uriEncode(value));
    }


    //  Create a canonical query string (used during signing)
    String createCanonicalQueryString() {
        String[] results = new String[0], keys = new List<String>(queryParams.keySet());
        keys.sort();
        for(String key: keys) {
            results.add(key+'='+queryParams.get(key));
        }
        return String.join(results, '&');
    }

    //  Create the canonical headers (used for signing)
    String createCanonicalHeaders(String[] keys) {
        keys.addAll(headerParams.keySet());
        keys.sort();
        String[] results = new String[0];
        for(String key: keys) {
            results.add(key+':'+headerParams.get(key));
        }
        return String.join(results, '\n')+'\n';
    }

    //  Create the entire canonical request
    String createCanonicalRequest(String[] headerKeys) {
        return String.join(
            new String[] {
                method.name().removeStart('X'),         //  METHOD
                new Url(endPoint, resource).getPath(),  //  RESOURCE
                createCanonicalQueryString(),           //  CANONICAL QUERY STRING
                createCanonicalHeaders(headerKeys),     //  CANONICAL HEADERS
                String.join(headerKeys, ';'),           //  SIGNED HEADERS
                payloadSha256                           //  SHA256 PAYLOAD
            },
            '\n'
        );
    }

    //  We have to replace ~ and " " correctly, or we'll break AWS on those two characters
    string uriEncode(String value) {
        return value==null? null: EncodingUtil.urlEncode(value, 'utf-8').replaceAll('%7E','~').replaceAll('\+','%20');
    }

    //  Create the entire string to sign
    String createStringToSign(String[] signedHeaders) {
        String result = createCanonicalRequest(signedHeaders);
        return String.join(
            new String[] {
                'AWS4-HMAC-SHA256',
                headerParams.get('date'),
                String.join(new String[] { requestTime.formatGMT('yyyyMMdd'), region, service, 'aws4_request' },'/'),
                EncodingUtil.convertToHex(Crypto.generateDigest('sha256', Blob.valueof(result)))
            },
            '\n'
        );
    }

    //  Create our signing key
    void createSigningKey(String secretKey) {
        signingKey = Crypto.generateMac('hmacSHA256', Blob.valueOf('aws4_request'),
            Crypto.generateMac('hmacSHA256', Blob.valueOf(service),
                Crypto.generateMac('hmacSHA256', Blob.valueOf(region),
                    Crypto.generateMac('hmacSHA256', Blob.valueOf(requestTime.formatGMT('yyyyMMdd')), Blob.valueOf('AWS4'+secretKey))
                )
            )
        );
    }

    //  Create all of the bits and pieces using all utility functions above
    public HttpRequest createRequest() {
        init();
        payloadSha256 = EncodingUtil.convertToHex(Crypto.generateDigest('sha-256', payload));
        setHeader('date', requestTime.formatGMT('yyyyMMdd\'T\'HHmmss\'Z\''));
        if(host == null) {
            host = endpoint.getHost();
        }
        setHeader('host', host);
        HttpRequest request = new HttpRequest();
        request.setMethod(method.name().removeStart('X'));
        if(payload.size() > 0) {
            setHeader('Content-Length', String.valueOf(payload.size()));
            request.setBodyAsBlob(payload);
        }
        String finalEndpoint = new Url(endpoint, resource).toExternalForm(), 
            queryString = createCanonicalQueryString();
        if(queryString != '') {
            finalEndpoint += '?'+queryString;
        }
        request.setEndpoint(finalEndpoint);
        for(String key: headerParams.keySet()) {
            request.setHeader(key, headerParams.get(key));
        }
        String[] headerKeys = new String[0];
        String stringToSign = createStringToSign(headerKeys);
        request.setHeader(
            'Authorization', 
            String.format(
                'AWS4-HMAC-SHA256 Credential={0}, SignedHeaders={1},Signature={2}',
                new String[] {
                    String.join(new String[] { accessKey, requestTime.formatGMT('yyyyMMdd'), region, service, 'aws4_request' },'/'),
                    String.join(headerKeys,';'), EncodingUtil.convertToHex(Crypto.generateMac('hmacSHA256', Blob.valueOf(stringToSign), signingKey))}
            ));
        system.debug(json.serializePretty(request.getEndpoint()));
        return request;
    }

    //  Actually perform the request, and throw exception if response code is not valid
    public HttpResponse sendRequest(Set<Integer> validCodes) {
        HttpResponse response = new Http().send(createRequest());
        if(!validCodes.contains(response.getStatusCode())) {
            system.debug(json.deserializeUntyped(response.getBody()));
        }
        return response;
    }

    //  Same as above, but assume that only 200 is valid
    //  This method exists because most of the time, 200 is what we expect
    public HttpResponse sendRequest() {
        return sendRequest(new Set<Integer> { 200 });
    }


    // TEST METHODS
    public static string getEndpoint(string attribute){ 
        AwsApiGateway api = new AwsApiGateway(attribute);
        return api.createRequest().getEndpoint();
    }
    public static string getEndpoint(string attribute, map<string, string> params){ 
        AwsApiGateway api = new AwsApiGateway(attribute);
        for (string key: params.keySet()){ 
            api.setQueryParam(key, params.get(key));
        }
        return api.createRequest().getEndpoint();
    }

    public class EndpointConfig { 
        string resource;
        string attribute;
        list<object> items;
        map<string,string> params;

        public EndpointConfig(string resource, string attribute, list<object> items){ 
            this.items = items;
            this.resource = resource;
            this.attribute = attribute;
        }

        public EndpointConfig setQueryParams(map<string,string> parameters){ 
            params = parameters;
            return this;
        }
        public string endpoint(){ 
            if (params == null){ 
                return getEndpoint(resource);
            } else return getEndpoint(resource + '/' + attribute, params);
        }
        public SingleRequestMock mockResponse(){ 
            return new SingleRequestMock(200, 'OK', json.serialize(items), null);
        }
    }
}