Advertisement
sspendol

amazon_aws_s3_pkg with SSL Support

May 15th, 2014
1,708
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PL/SQL 34.48 KB | None | 0 0
  1. CREATE OR REPLACE PACKAGE BODY amazon_aws_s3_pkg
  2. AS
  3.  
  4.   /*
  5.  
  6.   Purpose:   PL/SQL wrapper package for Amazon AWS S3 API
  7.  
  8.   Remarks:   inspired by the whitepaper "Building an Amazon S3 Client with Application Express 4.0" by Jason Straub
  9.              see http://jastraub.blogspot.com/2011/01/building-amazon-s3-client-with.html
  10.  
  11.   Who     Date        Description
  12.   ------  ----------  -------------------------------------
  13.   MBR     09.01.2011  Created
  14.   SWS     05.09.2014  Added capabilities to use https and Oracle Wallet
  15.  
  16.   */
  17.  
  18.   g_orcl_wallet_path       CONSTANT VARCHAR2(255) := 'file:/app/oracle/product/11.2.0/db_1/wallet';
  19.   g_orcl_wallet_pw         CONSTANT VARCHAR2(255) := 'WALLET_PASSWORD';
  20.   g_aws_url_http           CONSTANT VARCHAR2(255) := 'http://'; -- Set to either http:// or https://
  21.   g_aws_url_s3             CONSTANT VARCHAR2(255) := g_aws_url_http || 's3.amazonaws.com/';
  22.   g_aws_host_s3            CONSTANT VARCHAR2(255) := 's3.amazonaws.com';
  23.   g_aws_namespace_s3       CONSTANT VARCHAR2(255) := 'http://s3.amazonaws.com/doc/2006-03-01/';
  24.   g_aws_namespace_s3_full  CONSTANT VARCHAR2(255) := 'xmlns="' || g_aws_namespace_s3 || '"';
  25.   g_date_format_xml        CONSTANT VARCHAR2(30)  := 'YYYY-MM-DD"T"HH24:MI:SS".000Z"';
  26.  
  27.  
  28. PROCEDURE raise_error (p_error_message IN VARCHAR2)
  29. AS
  30. BEGIN
  31.  
  32.   /*
  33.  
  34.   Purpose:   raise error
  35.  
  36.   Remarks:  
  37.  
  38.   Who     Date        Description
  39.   ------  ----------  -------------------------------------
  40.   MBR     15.01.2011  Created
  41.  
  42.   */
  43.  
  44.   raise_application_error (-20000, p_error_message);
  45.  
  46. END raise_error;
  47.  
  48.  
  49. PROCEDURE check_for_errors (p_clob IN clob)
  50. AS
  51.   l_xml xmltype;
  52. BEGIN
  53.  
  54.   /*
  55.  
  56.   Purpose:   check for errors (clob)
  57.  
  58.   Remarks:  
  59.  
  60.   Who     Date        Description
  61.   ------  ----------  -------------------------------------
  62.   MBR     15.01.2011  Created
  63.  
  64.   */
  65.  
  66.   IF (p_clob IS NOT NULL) AND (LENGTH(p_clob) > 0) THEN
  67.  
  68.     l_xml := xmltype (p_clob);
  69.  
  70.     IF l_xml.EXISTSNODE('/Error') = 1 THEN
  71.       debug_pkg.print (l_xml);
  72.       raise_error (l_xml.EXTRACT('/Error/Message/text()').getstringval());
  73.     END IF;
  74.    
  75.   END IF;
  76.  
  77. END check_for_errors;
  78.  
  79.  
  80. PROCEDURE check_for_errors (p_xml IN xmltype)
  81. AS
  82. BEGIN
  83.  
  84.   /*
  85.  
  86.   Purpose:   check for errors (XMLType)
  87.  
  88.   Remarks:  
  89.  
  90.   Who     Date        Description
  91.   ------  ----------  -------------------------------------
  92.   MBR     15.01.2011  Created
  93.  
  94.   */
  95.  
  96.   IF p_xml.EXISTSNODE('/Error') = 1 THEN
  97.     debug_pkg.print (p_xml);
  98.     raise_error (p_xml.EXTRACT('/Error/Message/text()').getstringval());
  99.   END IF;
  100.  
  101. END check_for_errors;
  102.  
  103.  
  104. FUNCTION check_for_redirect (p_clob IN clob) RETURN VARCHAR2
  105. AS
  106.   l_xml                          xmltype;
  107.   l_returnvalue                  VARCHAR2(4000);
  108. BEGIN
  109.  
  110.   /*
  111.  
  112.   Purpose:   check for redirect
  113.  
  114.   Remarks:   Used by the "delete bucket" procedure, by Jeffrey Kemp
  115.              see http://code.google.com/p/plsql-utils/issues/detail?id=14
  116.              "One thing I found when testing was that if the bucket is not in the US standard region,
  117.               Amazon seems to respond with a TemporaryRedirect error.
  118.               If the same request is re-requested to the indicated URL it works."
  119.  
  120.   Who     Date        Description
  121.   ------  ----------  -------------------------------------
  122.   MBR     16.02.2013  Created, based on code by Jeffrey Kemp
  123.  
  124.   */
  125.  
  126.   IF (p_clob IS NOT NULL) AND (LENGTH(p_clob) > 0) THEN
  127.  
  128.     l_xml := xmltype (p_clob);
  129.  
  130.     IF l_xml.EXISTSNODE('/Error') = 1 THEN
  131.  
  132.       IF l_xml.EXTRACT('/Error/Code/text()').getStringVal = 'TemporaryRedirect' THEN
  133.         l_returnvalue := l_xml.EXTRACT('/Error/Endpoint/text()').getStringVal;
  134.         debug_pkg.printf('Temporary Redirect to %1', l_returnvalue);
  135.       END IF;
  136.  
  137.     END IF;
  138.  
  139.   END IF;
  140.  
  141.   RETURN l_returnvalue;
  142.  
  143. END check_for_redirect;
  144.  
  145.  
  146. FUNCTION make_request (p_url IN VARCHAR2,
  147.                        p_http_method IN VARCHAR2,
  148.                        p_header_names IN t_str_array,
  149.                        p_header_values IN t_str_array,
  150.                        p_request_blob IN blob := NULL,
  151.                        p_request_clob IN clob := NULL) RETURN clob
  152. AS
  153.   l_http_req     UTL_HTTP.req;
  154.   l_http_resp    UTL_HTTP.resp;
  155.  
  156.   l_amount       BINARY_INTEGER := 32000;
  157.   l_offset       INTEGER := 1;
  158.   l_buffer       VARCHAR2(32000);
  159.   l_buffer_raw   RAW(32000);
  160.  
  161.   l_response     VARCHAR2(2000);
  162.   l_returnvalue  clob;
  163.  
  164. BEGIN
  165.  
  166.   /*
  167.  
  168.   Purpose:   make HTTP request
  169.  
  170.   Remarks:  
  171.  
  172.   Who     Date        Description
  173.   ------  ----------  -------------------------------------
  174.   MBR     15.01.2011  Created
  175.  
  176.   */
  177.  
  178.   debug_pkg.printf('%1 %2', p_http_method, p_url);
  179.  
  180.   -- Call to Oracle Wallet
  181.   IF g_aws_url_http = 'https://' THEN
  182.     UTL_HTTP.SET_WALLET(g_orcl_wallet_path, g_orcl_wallet_pw);
  183.   END IF;
  184.  
  185.   l_http_req := UTL_HTTP.begin_request(p_url, p_http_method);
  186.  
  187.   IF p_header_names.COUNT > 0 THEN
  188.     FOR i IN p_header_names.FIRST .. p_header_names.LAST LOOP
  189.       debug_pkg.printf('%1: %2', p_header_names(i), p_header_values(i));
  190.       UTL_HTTP.set_header(l_http_req, p_header_names(i), p_header_values(i));
  191.     END LOOP;
  192.   END IF;
  193.  
  194.   IF p_request_blob IS NOT NULL THEN
  195.  
  196.     BEGIN
  197.       LOOP
  198.         DBMS_LOB.read (p_request_blob, l_amount, l_offset, l_buffer_raw);
  199.         UTL_HTTP.write_raw (l_http_req, l_buffer_raw);
  200.         l_offset := l_offset + l_amount;
  201.         l_amount := 32000;
  202.       END LOOP;
  203.     EXCEPTION
  204.       WHEN NO_DATA_FOUND THEN
  205.         NULL;
  206.     END;
  207.  
  208.   ELSIF p_request_clob IS NOT NULL THEN
  209.  
  210.     BEGIN
  211.       LOOP
  212.         DBMS_LOB.read (p_request_clob, l_amount, l_offset, l_buffer);
  213.         UTL_HTTP.write_text (l_http_req, l_buffer);
  214.         l_offset := l_offset + l_amount;
  215.         l_amount := 32000;
  216.       END LOOP;
  217.     EXCEPTION
  218.       WHEN NO_DATA_FOUND THEN
  219.         NULL;
  220.     END;
  221.  
  222.   END IF;
  223.  
  224.   l_http_resp := UTL_HTTP.get_response(l_http_req);
  225.  
  226.   DBMS_LOB.createtemporary (l_returnvalue, FALSE);
  227.   DBMS_LOB.OPEN (l_returnvalue, DBMS_LOB.lob_readwrite);
  228.  
  229.   BEGIN
  230.     LOOP
  231.       UTL_HTTP.read_text (l_http_resp, l_buffer);
  232.       DBMS_LOB.writeappend (l_returnvalue, LENGTH(l_buffer), l_buffer);
  233.     END LOOP;
  234.   EXCEPTION
  235.     WHEN OTHERS THEN
  236.       IF SQLCODE <> -29266 THEN
  237.         RAISE;
  238.       END IF;
  239.   END;
  240.  
  241.   UTL_HTTP.end_response (l_http_resp);
  242.  
  243.   RETURN l_returnvalue;
  244.  
  245. END make_request;
  246.  
  247.  
  248. FUNCTION get_url (p_bucket_name IN VARCHAR2,
  249.                   p_key IN VARCHAR2 := NULL) RETURN VARCHAR2
  250. AS
  251.   l_returnvalue VARCHAR2(4000);
  252. BEGIN
  253.  
  254.   /*
  255.  
  256.   Purpose:   construct a valid URL
  257.  
  258.   Remarks:  
  259.  
  260.   Who     Date        Description
  261.   ------  ----------  -------------------------------------
  262.   MBR     03.03.2011  Created
  263.  
  264.   */
  265.  
  266.   l_returnvalue := g_aws_url_http || p_bucket_name || '.' || g_aws_host_s3 || '/' || p_key;
  267.  
  268.   RETURN l_returnvalue;
  269.  
  270. END get_url;
  271.  
  272.  
  273. FUNCTION get_host (p_bucket_name IN VARCHAR2) RETURN VARCHAR2
  274. AS
  275.   l_returnvalue VARCHAR2(4000);
  276. BEGIN
  277.  
  278.   /*
  279.  
  280.   Purpose:   construct a valid host string
  281.  
  282.   Remarks:  
  283.  
  284.   Who     Date        Description
  285.   ------  ----------  -------------------------------------
  286.   MBR     03.03.2011  Created
  287.  
  288.   */
  289.  
  290.   l_returnvalue := p_bucket_name || '.' || g_aws_host_s3;
  291.  
  292.   RETURN l_returnvalue;
  293.  
  294. END get_host;
  295.  
  296.  
  297. FUNCTION get_bucket_list RETURN t_bucket_list
  298. AS
  299.   l_clob                         clob;
  300.   l_xml                          xmltype;
  301.  
  302.   l_date_str                     VARCHAR2(255);
  303.   l_auth_str                     VARCHAR2(255);
  304.  
  305.   l_header_names                 t_str_array := t_str_array();
  306.   l_header_values                t_str_array := t_str_array();
  307.  
  308.   l_count                        PLS_INTEGER := 0;
  309.   l_returnvalue                  t_bucket_list;
  310.  
  311. BEGIN
  312.  
  313.   /*
  314.  
  315.   Purpose:   get buckets
  316.  
  317.   Remarks:  
  318.  
  319.   Who     Date        Description
  320.   ------  ----------  -------------------------------------
  321.   MBR     09.01.2011  Created
  322.  
  323.   */
  324.  
  325.   l_date_str := amazon_aws_auth_pkg.get_date_string;
  326.   l_auth_str := amazon_aws_auth_pkg.get_auth_string ('GET' || CHR(10) || CHR(10) || CHR(10) || l_date_str || CHR(10) || '/');
  327.  
  328.   l_header_names.extend;
  329.   l_header_names(1) := 'Host';
  330.   l_header_values.extend;
  331.   l_header_values(1) := g_aws_host_s3;
  332.  
  333.   l_header_names.extend;
  334.   l_header_names(2) := 'Date';
  335.   l_header_values.extend;
  336.   l_header_values(2) := l_date_str;
  337.  
  338.   l_header_names.extend;
  339.   l_header_names(3) := 'Authorization';
  340.   l_header_values.extend;
  341.   l_header_values(3) := l_auth_str;
  342.  
  343.   l_clob := make_request (g_aws_url_s3, 'GET', l_header_names, l_header_values, NULL);
  344.  
  345.   IF (l_clob IS NOT NULL) AND (LENGTH(l_clob) > 0) THEN
  346.  
  347.     l_xml := xmltype (l_clob);
  348.    
  349.     check_for_errors (l_xml);
  350.  
  351.     FOR l_rec IN (
  352.       SELECT EXTRACTVALUE(VALUE(t), '*/Name', g_aws_namespace_s3_full) AS bucket_name,
  353.         EXTRACTVALUE(VALUE(t), '*/CreationDate', g_aws_namespace_s3_full) AS creation_date
  354.       FROM TABLE(XMLSEQUENCE(l_xml.EXTRACT('//ListAllMyBucketsResult/Buckets/Bucket', g_aws_namespace_s3_full))) t
  355.       ) LOOP
  356.       l_count := l_count + 1;
  357.       l_returnvalue(l_count).bucket_name := l_rec.bucket_name;
  358.       l_returnvalue(l_count).creation_date := TO_DATE(l_rec.creation_date, g_date_format_xml);
  359.     END LOOP;
  360.    
  361.   END IF;
  362.  
  363.   RETURN l_returnvalue;
  364.  
  365. END get_bucket_list;
  366.  
  367.  
  368. FUNCTION get_bucket_tab RETURN t_bucket_tab pipelined
  369. AS
  370.   l_bucket_list                  t_bucket_list;
  371. BEGIN
  372.  
  373.   /*
  374.  
  375.   Purpose:   get buckets
  376.  
  377.   Remarks:  
  378.  
  379.   Who     Date        Description
  380.   ------  ----------  -------------------------------------
  381.   MBR     19.01.2011  Created
  382.  
  383.   */
  384.  
  385.   l_bucket_list := get_bucket_list;
  386.  
  387.   FOR i IN 1 .. l_bucket_list.COUNT LOOP
  388.     pipe ROW (l_bucket_list(i));
  389.   END LOOP;
  390.  
  391.   RETURN;
  392.  
  393. END get_bucket_tab;
  394.  
  395.  
  396. PROCEDURE new_bucket (p_bucket_name IN VARCHAR2,
  397.                       p_region IN VARCHAR2 := NULL)
  398. AS
  399.  
  400.   l_request_body                 clob;
  401.   l_clob                         clob;
  402.   l_xml                          xmltype;
  403.  
  404.   l_date_str                     VARCHAR2(255);
  405.   l_auth_str                     VARCHAR2(255);
  406.  
  407.   l_header_names                 t_str_array := t_str_array();
  408.   l_header_values                t_str_array := t_str_array();
  409.  
  410. BEGIN
  411.  
  412.   /*
  413.  
  414.   Purpose:   create bucket
  415.  
  416.   Remarks:   *** bucket names must be unique across all of Amazon S3 ***
  417.  
  418.              see http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTBucketPUT.html
  419.  
  420.   Who     Date        Description
  421.   ------  ----------  -------------------------------------
  422.   MBR     15.01.2011  Created
  423.  
  424.   */
  425.  
  426.   l_date_str := amazon_aws_auth_pkg.get_date_string;
  427.  
  428.   IF p_region IS NOT NULL THEN
  429.     l_auth_str := amazon_aws_auth_pkg.get_auth_string ('PUT' || CHR(10) || CHR(10) || 'text/plain' || CHR(10) || l_date_str || CHR(10) || '/' || p_bucket_name || '/');
  430.   ELSE
  431.     l_auth_str := amazon_aws_auth_pkg.get_auth_string ('PUT' || CHR(10) || CHR(10) || CHR(10) || l_date_str || CHR(10) || '/' || p_bucket_name || '/');
  432.   END IF;
  433.  
  434.   l_header_names.extend;
  435.   l_header_names(1) := 'Host';
  436.   l_header_values.extend;
  437.   l_header_values(1) := get_host(p_bucket_name);
  438.  
  439.   l_header_names.extend;
  440.   l_header_names(2) := 'Date';
  441.   l_header_values.extend;
  442.   l_header_values(2) := l_date_str;
  443.  
  444.   l_header_names.extend;
  445.   l_header_names(3) := 'Authorization';
  446.   l_header_values.extend;
  447.   l_header_values(3) := l_auth_str;
  448.  
  449.   IF p_region IS NOT NULL THEN
  450.  
  451.     l_request_body := '<CreateBucketConfiguration ' || g_aws_namespace_s3_full || '><LocationConstraint>' || p_region || '</LocationConstraint></CreateBucketConfiguration>';
  452.  
  453.     l_header_names.extend;
  454.     l_header_names(4) := 'Content-Type';
  455.     l_header_values.extend;
  456.     l_header_values(4) := 'text/plain';
  457.  
  458.     l_header_names.extend;
  459.     l_header_names(5) := 'Content-Length';
  460.     l_header_values.extend;
  461.     l_header_values(5) := LENGTH(l_request_body);
  462.  
  463.   END IF;
  464.  
  465.   l_clob := make_request (get_url (p_bucket_name), 'PUT', l_header_names, l_header_values, NULL, l_request_body);
  466.  
  467.   check_for_errors (l_clob);
  468.  
  469. END new_bucket;
  470.  
  471.  
  472. FUNCTION get_bucket_region (p_bucket_name IN VARCHAR2) RETURN VARCHAR2
  473. AS
  474.  
  475.   l_clob                         clob;
  476.   l_xml                          xmltype;
  477.  
  478.   l_date_str                     VARCHAR2(255);
  479.   l_auth_str                     VARCHAR2(255);
  480.  
  481.   l_header_names                 t_str_array := t_str_array();
  482.   l_header_values                t_str_array := t_str_array();
  483.  
  484.   l_returnvalue                  VARCHAR2(255);
  485.  
  486. BEGIN
  487.  
  488.   /*
  489.  
  490.   Purpose:   get bucket region
  491.  
  492.   Remarks:   see http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTBucketGETlocation.html
  493.  
  494.              note that the region will be NULL for buckets in the default region (US)
  495.  
  496.   Who     Date        Description
  497.   ------  ----------  -------------------------------------
  498.   MBR     03.03.2011  Created
  499.  
  500.   */
  501.  
  502.   l_date_str := amazon_aws_auth_pkg.get_date_string;
  503.   l_auth_str := amazon_aws_auth_pkg.get_auth_string ('GET' || CHR(10) || CHR(10) || CHR(10) || l_date_str || CHR(10) || '/' || p_bucket_name || '/?location');
  504.  
  505.   l_header_names.extend;
  506.   l_header_names(1) := 'Host';
  507.   l_header_values.extend;
  508.   l_header_values(1) := get_host(p_bucket_name);
  509.  
  510.   l_header_names.extend;
  511.   l_header_names(2) := 'Date';
  512.   l_header_values.extend;
  513.   l_header_values(2) := l_date_str;
  514.  
  515.   l_header_names.extend;
  516.   l_header_names(3) := 'Authorization';
  517.   l_header_values.extend;
  518.   l_header_values(3) := l_auth_str;
  519.  
  520.   l_clob := make_request (get_url(p_bucket_name) || '?location', 'GET', l_header_names, l_header_values);
  521.  
  522.   IF (l_clob IS NOT NULL) AND (LENGTH(l_clob) > 0) THEN
  523.  
  524.     l_xml := xmltype (l_clob);
  525.    
  526.     check_for_errors (l_xml);
  527.    
  528.     IF l_xml.EXISTSNODE('/LocationConstraint', g_aws_namespace_s3_full) = 1 THEN
  529.       -- see http://pbarut.blogspot.com/2006/11/ora-30625-and-xmltype.html
  530.       IF l_xml.EXTRACT('/LocationConstraint/text()', g_aws_namespace_s3_full) IS NOT NULL THEN
  531.         l_returnvalue := l_xml.EXTRACT('/LocationConstraint/text()', g_aws_namespace_s3_full).getstringval();
  532.       ELSE
  533.         l_returnvalue := NULL;
  534.       END IF;
  535.     END IF;
  536.    
  537.   END IF;
  538.  
  539.   RETURN l_returnvalue;
  540.  
  541. END get_bucket_region;
  542.  
  543.  
  544. PROCEDURE get_object_list (p_bucket_name IN VARCHAR2,
  545.                            p_prefix IN VARCHAR2,
  546.                            p_max_keys IN NUMBER,
  547.                            p_list OUT t_object_list,
  548.                            p_more_marker IN OUT VARCHAR2)
  549. AS
  550.   l_clob                         clob;
  551.   l_xml                          xmltype;
  552.  
  553.   l_date_str                     VARCHAR2(255);
  554.   l_auth_str                     VARCHAR2(255);
  555.  
  556.   l_header_names                 t_str_array := t_str_array();
  557.   l_header_values                t_str_array := t_str_array();
  558.  
  559.   l_count                        PLS_INTEGER := 0;
  560.   l_returnvalue                  t_object_list;
  561.  
  562. BEGIN
  563.  
  564.   /*
  565.  
  566.   Purpose:   get objects
  567.  
  568.   Remarks:   see http://docs.amazonwebservices.com/AmazonS3/latest/API/index.html?RESTObjectGET.html
  569.  
  570.              see http://code.google.com/p/plsql-utils/issues/detail?id=16
  571.  
  572.              "I've rewritten get_object_list as an internal procedure that uses the "marker" parameter,
  573.              so that get_object_tab can now call the Amazon API multiple times to return the complete set of objects.
  574.              The get_object_list function remains functionally unchanged in this version - it just returns one set of objects -
  575.              it could be enhanced to support the marker parameter as well, I guess,
  576.              but I'd rather not expose that sort of thing to the caller personally.
  577.              The nice thing about the pipelined function is that the subsequent calls to Amazon
  578.              will only be executed if the client actually fetches all the rows."
  579.  
  580.   Who     Date        Description
  581.   ------  ----------  -------------------------------------
  582.   MBR     15.01.2011  Created
  583.   JKEMP   14.08.2012  Rewritten as private procedure, see remarks above
  584.  
  585.   */
  586.  
  587.   l_date_str := amazon_aws_auth_pkg.get_date_string;
  588.   l_auth_str := amazon_aws_auth_pkg.get_auth_string ('GET' || CHR(10) || CHR(10) || CHR(10) || l_date_str || CHR(10) || '/' || p_bucket_name || '/');
  589.  
  590.   l_header_names.extend;
  591.   l_header_names(1) := 'Host';
  592.   l_header_values.extend;
  593.   l_header_values(1) := get_host (p_bucket_name);
  594.  
  595.   l_header_names.extend;
  596.   l_header_names(2) := 'Date';
  597.   l_header_values.extend;
  598.   l_header_values(2) := l_date_str;
  599.  
  600.   l_header_names.extend;
  601.   l_header_names(3) := 'Authorization';
  602.   l_header_values.extend;
  603.   l_header_values(3) := l_auth_str;
  604.  
  605.   IF p_more_marker IS NOT NULL THEN
  606.     l_clob := make_request (get_url(p_bucket_name) || '?marker=' || p_more_marker || '&max-keys=' || p_max_keys || '&prefix=' || UTL_URL.escape(p_prefix), 'GET', l_header_names, l_header_values, NULL);
  607.   ELSE
  608.     l_clob := make_request (get_url(p_bucket_name) || '?max-keys=' || p_max_keys || '&prefix=' || UTL_URL.escape(p_prefix), 'GET', l_header_names, l_header_values, NULL);
  609.   END IF;
  610.  
  611.   IF (l_clob IS NOT NULL) AND (LENGTH(l_clob) > 0) THEN
  612.  
  613.     l_xml := xmltype (l_clob);
  614.  
  615.     check_for_errors (l_xml);
  616.  
  617.     FOR l_rec IN (
  618.       SELECT EXTRACTVALUE(VALUE(t), '*/Key', g_aws_namespace_s3_full) AS key,
  619.         EXTRACTVALUE(VALUE(t), '*/Size', g_aws_namespace_s3_full) AS size_bytes,
  620.         EXTRACTVALUE(VALUE(t), '*/LastModified', g_aws_namespace_s3_full) AS last_modified
  621.       FROM TABLE(XMLSEQUENCE(l_xml.EXTRACT('//ListBucketResult/Contents', g_aws_namespace_s3_full))) t
  622.       ) LOOP
  623.       l_count := l_count + 1;
  624.       l_returnvalue(l_count).key := l_rec.key;
  625.       l_returnvalue(l_count).size_bytes := l_rec.size_bytes;
  626.       l_returnvalue(l_count).last_modified := TO_DATE(l_rec.last_modified, g_date_format_xml);
  627.     END LOOP;
  628.    
  629.     -- check if this is the last set of data or not
  630.  
  631.     l_xml := l_xml.EXTRACT('//ListBucketResult/IsTruncated/text()', g_aws_namespace_s3_full);
  632.    
  633.     IF l_xml IS NOT NULL AND l_xml.getStringVal = 'true' THEN
  634.       p_more_marker := l_returnvalue(l_returnvalue.LAST).key;
  635.     END IF;
  636.  
  637.   END IF;
  638.  
  639.   p_list := l_returnvalue;
  640.  
  641. END get_object_list;
  642.  
  643.  
  644. FUNCTION get_object_list (p_bucket_name IN VARCHAR2,
  645.                           p_prefix IN VARCHAR2 := NULL,
  646.                           p_max_keys IN NUMBER := NULL) RETURN t_object_list
  647. AS
  648.   l_object_list                  t_object_list;
  649.   l_more_marker                  VARCHAR2(4000);
  650. BEGIN
  651.  
  652.   /*
  653.  
  654.   Purpose:   get objects
  655.  
  656.   Remarks:   see http://docs.amazonwebservices.com/AmazonS3/latest/API/index.html?RESTObjectGET.html
  657.  
  658.   Who     Date        Description
  659.   ------  ----------  -------------------------------------
  660.   JKEMP   14.08.2012  Created
  661.  
  662.   */
  663.  
  664.   get_object_list (
  665.     p_bucket_name => p_bucket_name,
  666.     p_prefix      => p_prefix,
  667.     p_max_keys    => p_max_keys,
  668.     p_list        => l_object_list,
  669.     p_more_marker => l_more_marker --ignored by this function
  670.   );
  671.  
  672.   RETURN l_object_list;
  673.  
  674. END get_object_list;
  675.  
  676.  
  677. FUNCTION get_object_tab (p_bucket_name IN VARCHAR2,
  678.                          p_prefix IN VARCHAR2 := NULL,
  679.                          p_max_keys IN NUMBER := NULL) RETURN t_object_tab pipelined
  680. AS
  681.   l_object_list                  t_object_list;
  682.   l_more_marker                  VARCHAR2(4000);
  683. BEGIN
  684.  
  685.   /*
  686.  
  687.   Purpose:   get objects
  688.  
  689.   Remarks:
  690.  
  691.   Who     Date        Description
  692.   ------  ----------  -------------------------------------
  693.   MBR     19.01.2011  Created
  694.  
  695.   */
  696.  
  697.   LOOP
  698.  
  699.     get_object_list (
  700.       p_bucket_name => p_bucket_name,
  701.       p_prefix      => p_prefix,
  702.       p_max_keys    => p_max_keys,
  703.       p_list        => l_object_list,
  704.       p_more_marker => l_more_marker
  705.       );
  706.  
  707.     FOR i IN 1 .. l_object_list.COUNT LOOP
  708.       pipe ROW (l_object_list(i));
  709.     END LOOP;
  710.    
  711.     EXIT WHEN l_more_marker IS NULL;
  712.  
  713.   END LOOP;
  714.  
  715.   RETURN;
  716.  
  717. END get_object_tab;
  718.  
  719.  
  720. FUNCTION get_download_url (p_bucket_name IN VARCHAR2,
  721.                            p_key IN VARCHAR2,
  722.                            p_expiry_date IN DATE) RETURN VARCHAR2
  723. AS
  724.   l_returnvalue                  VARCHAR2(4000);
  725.   l_key                          VARCHAR2(4000) := UTL_URL.escape (p_key);
  726.   l_epoch                        NUMBER;
  727.   l_signature                    VARCHAR2(4000);
  728. BEGIN
  729.  
  730.   /*
  731.  
  732.   Purpose:   get download URL
  733.  
  734.   Remarks:   see http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html  
  735.  
  736.   Who     Date        Description
  737.   ------  ----------  -------------------------------------
  738.   MBR     15.01.2011  Created
  739.  
  740.   */
  741.  
  742.   l_epoch := amazon_aws_auth_pkg.get_epoch (p_expiry_date);
  743.   l_signature := amazon_aws_auth_pkg.get_signature ('GET' || CHR(10) || CHR(10) || CHR(10) || l_epoch || CHR(10) || '/' || p_bucket_name || '/' || l_key);
  744.  
  745.   l_returnvalue := get_url (p_bucket_name, l_key)
  746.     || '?AWSAccessKeyId=' || amazon_aws_auth_pkg.get_aws_id
  747.     || '&Expires=' || l_epoch
  748.     || '&Signature=' || wwv_flow_utilities.url_encode2 (l_signature);
  749.  
  750.   RETURN l_returnvalue;
  751.  
  752. END get_download_url;
  753.  
  754.  
  755. PROCEDURE new_object (p_bucket_name IN VARCHAR2,
  756.                       p_key IN VARCHAR2,
  757.                       p_object IN blob,
  758.                       p_content_type IN VARCHAR2,
  759.                       p_acl IN VARCHAR2 := NULL)
  760. AS
  761.  
  762.   l_key                          VARCHAR2(4000) := UTL_URL.escape (p_key);
  763.  
  764.   l_clob                         clob;
  765.   l_xml                          xmltype;
  766.  
  767.   l_date_str                     VARCHAR2(255);
  768.   l_auth_str                     VARCHAR2(255);
  769.  
  770.   l_header_names                 t_str_array := t_str_array();
  771.   l_header_values                t_str_array := t_str_array();
  772.  
  773. BEGIN
  774.  
  775.   /*
  776.  
  777.   Purpose:   upload new object
  778.  
  779.   Remarks:   see  http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectPUT.html
  780.  
  781.   Who     Date        Description
  782.   ------  ----------  -------------------------------------
  783.   MBR     16.01.2011  Created
  784.  
  785.   */
  786.  
  787.   l_date_str := amazon_aws_auth_pkg.get_date_string;
  788.  
  789.   IF p_acl IS NOT NULL THEN
  790.     l_auth_str := amazon_aws_auth_pkg.get_auth_string ('PUT' || CHR(10) || CHR(10) || p_content_type || CHR(10) || l_date_str || CHR(10) || 'x-amz-acl:' || p_acl || CHR(10) || '/' || p_bucket_name || '/' || l_key);
  791.   ELSE
  792.     l_auth_str := amazon_aws_auth_pkg.get_auth_string ('PUT' || CHR(10) || CHR(10) || p_content_type || CHR(10) || l_date_str || CHR(10) || '/' || p_bucket_name || '/' || l_key);
  793.   END IF;
  794.  
  795.   l_header_names.extend;
  796.   l_header_names(1) := 'Host';
  797.   l_header_values.extend;
  798.   l_header_values(1) := get_host(p_bucket_name);
  799.  
  800.   l_header_names.extend;
  801.   l_header_names(2) := 'Date';
  802.   l_header_values.extend;
  803.   l_header_values(2) := l_date_str;
  804.  
  805.   l_header_names.extend;
  806.   l_header_names(3) := 'Authorization';
  807.   l_header_values.extend;
  808.   l_header_values(3) := l_auth_str;
  809.  
  810.   l_header_names.extend;
  811.   l_header_names(4) := 'Content-Type';
  812.   l_header_values.extend;
  813.   l_header_values(4) := NVL(p_content_type, 'application/octet-stream');
  814.  
  815.   l_header_names.extend;
  816.   l_header_names(5) := 'Content-Length';
  817.   l_header_values.extend;
  818.   l_header_values(5) := DBMS_LOB.getlength(p_object);
  819.  
  820.   IF p_acl IS NOT NULL THEN
  821.     l_header_names.extend;
  822.     l_header_names(6) := 'x-amz-acl';
  823.     l_header_values.extend;
  824.     l_header_values(6) := p_acl;
  825.   END IF;
  826.  
  827.   l_clob := make_request (get_url (p_bucket_name, l_key), 'PUT', l_header_names, l_header_values, p_object);
  828.  
  829.   check_for_errors (l_clob);
  830.  
  831. END new_object;
  832.  
  833.  
  834. PROCEDURE delete_object (p_bucket_name IN VARCHAR2,
  835.                          p_key IN VARCHAR2)
  836. AS
  837.  
  838.   l_key                          VARCHAR2(4000) := UTL_URL.escape (p_key);
  839.  
  840.   l_clob                         clob;
  841.   l_xml                          xmltype;
  842.  
  843.   l_date_str                     VARCHAR2(255);
  844.   l_auth_str                     VARCHAR2(255);
  845.  
  846.   l_header_names                 t_str_array := t_str_array();
  847.   l_header_values                t_str_array := t_str_array();
  848.  
  849. BEGIN
  850.  
  851.   /*
  852.  
  853.   Purpose:   delete object
  854.  
  855.   Remarks:  
  856.  
  857.   Who     Date        Description
  858.   ------  ----------  -------------------------------------
  859.   MBR     18.01.2011  Created
  860.  
  861.   */
  862.  
  863.   l_date_str := amazon_aws_auth_pkg.get_date_string;
  864.   l_auth_str := amazon_aws_auth_pkg.get_auth_string ('DELETE' || CHR(10) || CHR(10) || CHR(10) || l_date_str || CHR(10) || '/' || p_bucket_name || '/' || l_key);
  865.  
  866.   l_header_names.extend;
  867.   l_header_names(1) := 'Host';
  868.   l_header_values.extend;
  869.   l_header_values(1) := get_host(p_bucket_name);
  870.  
  871.   l_header_names.extend;
  872.   l_header_names(2) := 'Date';
  873.   l_header_values.extend;
  874.   l_header_values(2) := l_date_str;
  875.  
  876.   l_header_names.extend;
  877.   l_header_names(3) := 'Authorization';
  878.   l_header_values.extend;
  879.   l_header_values(3) := l_auth_str;
  880.  
  881.   l_clob := make_request (get_url(p_bucket_name, l_key), 'DELETE', l_header_names, l_header_values);
  882.  
  883.   check_for_errors (l_clob);
  884.  
  885. END delete_object;
  886.  
  887.  
  888. FUNCTION get_object (p_bucket_name IN VARCHAR2,
  889.                      p_key IN VARCHAR2) RETURN blob
  890. AS
  891.   l_returnvalue blob;
  892. BEGIN
  893.  
  894.   /*
  895.  
  896.   Purpose:   get object
  897.  
  898.   Remarks:  
  899.  
  900.   Who     Date        Description
  901.   ------  ----------  -------------------------------------
  902.   MBR     20.01.2011  Created
  903.  
  904.   */
  905.  
  906.   l_returnvalue := http_util_pkg.get_blob_from_url (get_download_url (p_bucket_name, p_key, SYSDATE + 1));
  907.  
  908.   RETURN l_returnvalue;
  909.  
  910. END get_object;
  911.  
  912.  
  913. PROCEDURE delete_bucket (p_bucket_name IN VARCHAR2)
  914. AS
  915.   l_clob                         clob;
  916.   l_date_str                     VARCHAR2(255);
  917.   l_auth_str                     VARCHAR2(255);
  918.   l_header_names                 t_str_array := t_str_array();
  919.   l_header_values                t_str_array := t_str_array();
  920.   l_endpoint                     VARCHAR2(255);
  921. BEGIN
  922.  
  923.   /*
  924.  
  925.   Purpose:   delete bucket
  926.  
  927.   Remarks:
  928.  
  929.   Who     Date        Description
  930.   ------  ----------  -------------------------------------
  931.   JKEMP   09.08.2012  Created
  932.  
  933.   */
  934.  
  935.   l_date_str := amazon_aws_auth_pkg.get_date_string;
  936.   l_auth_str := amazon_aws_auth_pkg.get_auth_string ('DELETE' || CHR(10) || CHR(10) || CHR(10) || l_date_str || CHR(10) || '/' || p_bucket_name || '/');
  937.  
  938.   l_header_names.extend;
  939.   l_header_names(1) := 'Host';
  940.   l_header_values.extend;
  941.   l_header_values(1) := get_host(p_bucket_name);
  942.  
  943.   l_header_names.extend;
  944.   l_header_names(2) := 'Date';
  945.   l_header_values.extend;
  946.   l_header_values(2) := l_date_str;
  947.  
  948.   l_header_names.extend;
  949.   l_header_names(3) := 'Authorization';
  950.   l_header_values.extend;
  951.   l_header_values(3) := l_auth_str;
  952.  
  953.   l_clob := make_request (get_url(p_bucket_name), 'DELETE', l_header_names, l_header_values);
  954.  
  955.   l_endpoint := check_for_redirect (l_clob);
  956.  
  957.   IF l_endpoint IS NOT NULL THEN
  958.     l_clob := make_request (g_aws_url_http || l_endpoint || '/', 'DELETE', l_header_names, l_header_values);
  959.   END IF;
  960.  
  961.   check_for_errors (l_clob);
  962.  
  963. END delete_bucket;
  964.  
  965.  
  966. FUNCTION get_object_acl (p_bucket_name IN VARCHAR2,
  967.                          p_key IN VARCHAR2) RETURN xmltype
  968. AS
  969.                          
  970.   l_clob                         clob;
  971.   l_xml                          xmltype;
  972.  
  973.   l_date_str                     VARCHAR2(255);
  974.   l_auth_str                     VARCHAR2(255);
  975.  
  976.   l_header_names                 t_str_array := t_str_array();
  977.   l_header_values                t_str_array := t_str_array();
  978.  
  979.   l_returnvalue                  xmltype;
  980.  
  981. BEGIN
  982.  
  983.   /*
  984.  
  985.   Purpose:   get object ACL
  986.  
  987.   Remarks:  get the ACL for an object (private - used by get_object_owner, get_object_grantee_list, get_object_grantee_tab)
  988.  
  989.   Example return value:
  990.  
  991.   <AccessControlPolicy xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  992.     <Owner>
  993.       <ID>c244a7539c1fc912a06691246c90cb93629690ee4703efac8f08e6ff4cb48ef1</ID>
  994.       <DisplayName>jeffreykemp</DisplayName>
  995.     </Owner>
  996.     <AccessControlList>
  997.       <Grant>
  998.         <Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="CanonicalUser">
  999.           <ID>c244a7539c1fc912a06691246c90cb93629690ee4703efac8f08e6ff4cb48ef1</ID>
  1000.           <DisplayName>jeffreykemp</DisplayName>
  1001.         </Grantee>
  1002.         <Permission>FULL_CONTROL</Permission>
  1003.       </Grant>
  1004.       <Grant>
  1005.         <Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Group">
  1006.           <URI>http://acs.amazonaws.com/groups/global/AllUsers</URI>
  1007.         </Grantee>
  1008.         <Permission>READ</Permission>
  1009.       </Grant>
  1010.     </AccessControlList>
  1011.   </AccessControlPolicy>
  1012.  
  1013.   Who     Date        Description
  1014.   ------  ----------  -------------------------------------
  1015.   JKEMP   10.08.2012  Created
  1016.  
  1017.   */
  1018.  
  1019.   l_date_str := amazon_aws_auth_pkg.get_date_string;
  1020.   l_auth_str := amazon_aws_auth_pkg.get_auth_string ('GET' || CHR(10) || CHR(10) || CHR(10) || l_date_str || CHR(10) || '/' || p_bucket_name || '/' || p_key || '?acl');
  1021.  
  1022.   l_header_names.extend;
  1023.   l_header_names(1) := 'Host';
  1024.   l_header_values.extend;
  1025.   l_header_values(1) := g_aws_host_s3;
  1026.  
  1027.   l_header_names.extend;
  1028.   l_header_names(2) := 'Date';
  1029.   l_header_values.extend;
  1030.   l_header_values(2) := l_date_str;
  1031.  
  1032.   l_header_names.extend;
  1033.   l_header_names(3) := 'Authorization';
  1034.   l_header_values.extend;
  1035.   l_header_values(3) := l_auth_str;
  1036.  
  1037.   l_clob := make_request (get_url(p_bucket_name, p_key) || '?acl', 'GET', l_header_names, l_header_values, NULL);
  1038.  
  1039.   IF (l_clob IS NOT NULL) AND (LENGTH(l_clob) > 0) THEN
  1040.  
  1041.     l_xml := xmltype (l_clob);
  1042.     check_for_errors (l_xml);
  1043.     l_returnvalue := l_xml;
  1044.  
  1045.   END IF;
  1046.  
  1047.   RETURN l_returnvalue;
  1048.  
  1049. END get_object_acl;
  1050.  
  1051.  
  1052. FUNCTION get_object_owner (p_bucket_name IN VARCHAR2,
  1053.                            p_key IN VARCHAR2) RETURN t_owner
  1054. AS
  1055.   l_xml                          xmltype;
  1056.   l_returnvalue                  t_owner;
  1057. BEGIN
  1058.  
  1059.   /*
  1060.  
  1061.   Purpose:   get owner for an object
  1062.  
  1063.   Remarks:
  1064.  
  1065.   Who     Date        Description
  1066.   ------  ----------  -------------------------------------
  1067.   JKEMP   14.08.2012  Created
  1068.  
  1069.   */
  1070.  
  1071.   l_xml := get_object_acl (p_bucket_name, p_key);
  1072.  
  1073.   l_returnvalue.user_id := l_xml.EXTRACT('//AccessControlPolicy/Owner/ID/text()', g_aws_namespace_s3_full).getStringVal;
  1074.   l_returnvalue.user_name := l_xml.EXTRACT('//AccessControlPolicy/Owner/DisplayName/text()', g_aws_namespace_s3_full).getStringVal;
  1075.  
  1076.   RETURN l_returnvalue;
  1077.  
  1078. END get_object_owner;
  1079.  
  1080.  
  1081. FUNCTION get_object_grantee_list (p_bucket_name IN VARCHAR2,
  1082.                                   p_key IN VARCHAR2) RETURN t_grantee_list
  1083. AS
  1084.   l_xml_namespace_s3_full        CONSTANT VARCHAR2(255) := 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"';
  1085.   l_xml                          xmltype;
  1086.   l_count                        PLS_INTEGER := 0;
  1087.   l_returnvalue                  t_grantee_list;
  1088. BEGIN
  1089.  
  1090.   /*
  1091.  
  1092.   Purpose:   get grantees for an object
  1093.  
  1094.   Remarks:   Each grantee will either be a Canonical User or a Group.
  1095.              A Canonical User has an ID and a DisplayName.
  1096.              A Group has a URI.
  1097.              Permission will be FULL_CONTROL, WRITE, or READ_ACP.
  1098.  
  1099.   Who     Date        Description
  1100.   ------  ----------  -------------------------------------
  1101.   JKEMP   14.08.2012  Created
  1102.  
  1103.   */
  1104.  
  1105.   l_xml := get_object_acl (p_bucket_name, p_key);
  1106.  
  1107.   FOR l_rec IN (
  1108.     SELECT EXTRACTVALUE(VALUE(t), '*/Grantee/@xsi:type', g_aws_namespace_s3_full || ' ' || l_xml_namespace_s3_full) AS grantee_type,
  1109.       EXTRACTVALUE(VALUE(t), '*/Grantee/ID', g_aws_namespace_s3_full) AS user_id,
  1110.       EXTRACTVALUE(VALUE(t), '*/Grantee/DisplayName', g_aws_namespace_s3_full) AS user_name,
  1111.       EXTRACTVALUE(VALUE(t), '*/Grantee/URI', g_aws_namespace_s3_full) AS group_uri,
  1112.       EXTRACTVALUE(VALUE(t), '*/Permission', g_aws_namespace_s3_full) AS permission
  1113.     FROM TABLE(XMLSEQUENCE(l_xml.EXTRACT('//AccessControlPolicy/AccessControlList/Grant', g_aws_namespace_s3_full))) t
  1114.     ) LOOP
  1115.     l_count := l_count + 1;
  1116.     l_returnvalue(l_count).grantee_type := l_rec.grantee_type;
  1117.     l_returnvalue(l_count).user_id := l_rec.user_id;
  1118.     l_returnvalue(l_count).user_name := l_rec.user_name;
  1119.     l_returnvalue(l_count).group_uri := l_rec.group_uri;
  1120.     l_returnvalue(l_count).permission := l_rec.permission;
  1121.   END LOOP;
  1122.  
  1123.   RETURN l_returnvalue;
  1124.  
  1125. END get_object_grantee_list;
  1126.  
  1127.  
  1128. FUNCTION get_object_grantee_tab (p_bucket_name IN VARCHAR2,
  1129.                                  p_key IN VARCHAR2) RETURN t_grantee_tab pipelined
  1130. AS
  1131.   l_grantee_list  t_grantee_list;
  1132. BEGIN
  1133.  
  1134.   /*
  1135.  
  1136.   Purpose:   get grantees for an object
  1137.  
  1138.   Remarks:
  1139.  
  1140.   Who     Date        Description
  1141.   ------  ----------  -------------------------------------
  1142.   JKEMP   14.08.2012  Created
  1143.  
  1144.   */
  1145.  
  1146.   l_grantee_list := get_object_grantee_list (p_bucket_name, p_key);
  1147.  
  1148.   FOR i IN 1 .. l_grantee_list.COUNT LOOP
  1149.     pipe ROW (l_grantee_list(i));
  1150.   END LOOP;
  1151.  
  1152.   RETURN;
  1153.  
  1154. END get_object_grantee_tab;
  1155.  
  1156.  
  1157. PROCEDURE set_object_acl (p_bucket_name IN VARCHAR2,
  1158.                           p_key IN VARCHAR2,
  1159.                           p_acl IN VARCHAR2)
  1160. AS
  1161.   l_key                          VARCHAR2(4000) := UTL_URL.escape (p_key);
  1162.   l_clob                         clob;
  1163.   l_xml                          xmltype;
  1164.   l_date_str                     VARCHAR2(255);
  1165.   l_auth_str                     VARCHAR2(255);
  1166.   l_header_names                 t_str_array := t_str_array();
  1167.   l_header_values                t_str_array := t_str_array();
  1168. BEGIN
  1169.  
  1170.   /*
  1171.  
  1172.   Purpose:   modify the access control list (owner and grantees) for an object
  1173.  
  1174.   Remarks:   see http://code.google.com/p/plsql-utils/issues/detail?id=17
  1175.  
  1176.   Who     Date        Description
  1177.   ------  ----------  -------------------------------------
  1178.   JKEMP   22.09.2012  Created
  1179.  
  1180.   */
  1181.  
  1182.   l_date_str := amazon_aws_auth_pkg.get_date_string;
  1183.   l_auth_str := amazon_aws_auth_pkg.get_auth_string ('PUT' || CHR(10) || CHR(10) || CHR(10) || l_date_str || CHR(10) || 'x-amz-acl:' || p_acl || CHR(10) || '/' || p_bucket_name || '/' || l_key || '?acl');
  1184.  
  1185.   l_header_names.extend;
  1186.   l_header_names(1) := 'Host';
  1187.   l_header_values.extend;
  1188.   l_header_values(1) := get_host(p_bucket_name);
  1189.  
  1190.   l_header_names.extend;
  1191.   l_header_names(2) := 'Date';
  1192.   l_header_values.extend;
  1193.   l_header_values(2) := l_date_str;
  1194.  
  1195.   l_header_names.extend;
  1196.   l_header_names(3) := 'Authorization';
  1197.   l_header_values.extend;
  1198.   l_header_values(3) := l_auth_str;
  1199.  
  1200.   l_header_names.extend;
  1201.   l_header_names(4) := 'x-amz-acl';
  1202.   l_header_values.extend;
  1203.   l_header_values(4) := p_acl;
  1204.  
  1205.   l_clob := make_request (get_url(p_bucket_name, l_key) || '?acl', 'PUT', l_header_names, l_header_values);
  1206.  
  1207.   check_for_errors (l_clob);
  1208.  
  1209. END set_object_acl;
  1210.  
  1211.  
  1212. END amazon_aws_s3_pkg;
  1213. /
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement