From ef5b4ed0513067f84a6aab17283338734793aac1 Mon Sep 17 00:00:00 2001 From: Noah Pederson Date: Tue, 17 Oct 2023 22:03:49 -0500 Subject: [PATCH 01/10] Adds basic AWS S3 client Includes support for: - Bucket get/set/list/delete/exists? - Server-side object copying - Object get/set/list/delete - Bucket exists --- src/std/build-spec.ss | 4 + src/std/net/s3.ss | 5 + src/std/net/s3/api.ss | 242 ++++++++++++++++++++++++++++++++++++ src/std/net/s3/interface.ss | 22 ++++ src/std/net/s3/sigv4.ss | 106 ++++++++++++++++ 5 files changed, 379 insertions(+) create mode 100644 src/std/net/s3.ss create mode 100644 src/std/net/s3/api.ss create mode 100644 src/std/net/s3/interface.ss create mode 100644 src/std/net/s3/sigv4.ss diff --git a/src/std/build-spec.ss b/src/std/build-spec.ss index 004805b57..4d64a47db 100644 --- a/src/std/build-spec.ss +++ b/src/std/build-spec.ss @@ -217,6 +217,10 @@ "net/uri" "net/request" "net/json-rpc" + "net/s3" + "net/s3/interface" + "net/s3/api" + "net/s3/sigv4" "net/websocket/interface" "net/websocket/socket" "net/websocket/client" diff --git a/src/std/net/s3.ss b/src/std/net/s3.ss new file mode 100644 index 000000000..c0056d809 --- /dev/null +++ b/src/std/net/s3.ss @@ -0,0 +1,5 @@ +;;; -*- Gerbil -*- +;;; © vyzo, ngp +;;; AWS S3 Client +(import ./s3/api ./s3/interface) +(export (import: ./s3/api) (import: ./s3/interface)) diff --git a/src/std/net/s3/api.ss b/src/std/net/s3/api.ss new file mode 100644 index 000000000..378276cc9 --- /dev/null +++ b/src/std/net/s3/api.ss @@ -0,0 +1,242 @@ +;;; -*- Gerbil -*- +;;; (C) vyzo +;;; AWS S3 client +(import "sigv4" + :std/net/request + :std/misc/func + :std/contract + :std/net/uri + :std/crypto/digest + :std/text/hex + :std/xml + :std/error + :std/sugar + :std/srfi/19) +(export (struct-out s3-client bucket) S3ClientError) + +; precomputed empty sha256 +(def emptySHA256 #u8(227 176 196 66 152 252 28 20 154 251 244 200 153 111 185 + 36 39 174 65 228 100 155 147 76 164 149 153 27 120 82 184 85)) + +(defstruct s3-client (endpoint access-key secret-key region) + final: #t + constructor: :init!) + +(defstruct bucket (client name region) + final: #t) + +(deferror-class (S3ClientError Error) () s3-client-error?) + +(defraise/context (raise-s3-error where message irritants ...) + (S3ClientError message irritants: [irritants ...])) + +; Initializes a `s3-client`. Primarily responsible for holding onto credentials +(defmethod {:init! s3-client} + (lambda (self + (endpoint "s3.amazonaws.com") + (access-key (getenv "AWS_ACCESS_KEY_ID" #f)) + (secret-key (getenv "AWS_SECRET_ACCESS_KEY" #f)) + (region (getenv "AWS_DEFAULT_REGION" "us-east-1"))) + (using (self self : s3-client) + (set! self.endpoint endpoint) + (set! self.access-key access-key) + (set! self.secret-key secret-key) + (set! self.region region)))) + +; Retrieves buckets accessible to this client. +(defmethod {list-buckets s3-client} ; => (list : bucket) + (lambda (self) + (using (self self : s3-client) + (let* ((req (s3-request/error self verb: 'GET)) + (xml (s3-parse-xml req)) + (buckets (sxml-find xml (sxml-e? 's3:Buckets) sxml-children)) + (names (map (chain <> + (sxml-select <> (sxml-e? 's3:Name)) + (cadar <>) + (make-bucket self <> (s3-client-region self))) + buckets))) + ; buckets is #f if none are returned + (request-close req) + names)))) + +;; NOTE: all bucket operations need the correct region for the bucket or they will 400 +(defmethod {create-bucket! s3-client} + (lambda (self bucket) + (using (self self : s3-client) + (let (req (s3-request/error self verb: 'PUT bucket: bucket)) + (request-close req) + (void))))) + +; Gets a bucket struct that can be used to fetch objects. +(defmethod {get-bucket s3-client} ; => bucket + (lambda (self bucket-name) + (using (self self : s3-client) + (if {bucket-exists? self bucket-name} + (make-bucket self bucket-name self.region) + #f)))) + +; Delete a bucket by name +(defmethod {delete-bucket! s3-client} + (lambda (self bucket) + (using ((self self : s3-client) + (bucket bucket :~ string?)) + (when {bucket-exists? self bucket} + (let (req (s3-request/error self verb: 'DELETE bucket: bucket)) + (request-close req) + (void)))))) + +(defmethod {bucket-exists? s3-client} + (lambda (self bucket) + (using (self self : s3-client) + (let* ((bucket (if (bucket? bucket) (bucket-name bucket) bucket)) + (req {self.request verb: 'HEAD bucket: bucket}) + (code (request-status req))) + ; 200 and 404 are expected codes + ; we explicitly handle 404 so we get proper predicate + ; semantics and don't raise on what would otherwise be + ; #f condition. + (if (memv code [200 404]) + (begin + (request-close req) + (= code 200)) + (with-request-error req)))))) + +(defmethod {bucket s3-client} + (lambda (self name) + (using (self self : s3-client) + (if {bucket-exists? self name} + (make-bucket self name (s3-client-region self)) + #f)))) + +; Lists the objects stored within the bucket +(defmethod {list-objects bucket} + (lambda (self) + (using ((self self : bucket) + (client (bucket-client self) : s3-client)) + (let* ((name (bucket-name self)) + (req (s3-request/error client verb: 'GET bucket: name)) + (xml (s3-parse-xml req)) + (keys (sxml-select xml (sxml-e? 's3:Key) cadr))) + (request-close req) + keys)))) + +(defmethod {get bucket} + (lambda (self key) + (using ((self self : bucket) + (key :~ string?) + (client (bucket-client self) : s3-client)) + (let* ((req (s3-request/error client verb: 'GET bucket: (bucket-name self) + path: (string-append "/" key))) + (data (request-content req))) + (request-close req) + data)))) + +(defmethod {put! bucket} + (lambda (self key data content-type: (content-type "binary/octet-stream")) + (using ((self self : bucket) + (key :~ string?) + (client (bucket-client self) : s3-client)) + (let (req (s3-request/error client verb: 'PUT bucket: (bucket-name self) + path: (string-append "/" key) + body: data + content-type: content-type)) + (request-close req) + (void))))) + +(defmethod {delete! bucket} + (lambda (self key) + (using ((self : bucket) + (key :~ string?) + (client (bucket-client self) : s3-client)) + (let (req (s3-request/error client verb: 'DELETE bucket: (bucket-name self) + path: (string-append "/" key))) + (request-close req) + (void))))) + +(defmethod {copy-to! bucket} + (lambda (self src-bucket src dest) + (using ((self : bucket) + (client (bucket-client self) : s3-client) + ; a bucket instance pointing to the intended source bucket + (src-bucket : bucket) + ; the source file name + (src :~ string?) + ; the destination file name + (dest :~ string?)) + (let* ((src-ident (string-append (bucket-name src-bucket) "/" src)) + (headers [["x-amz-copy-source" :: src-ident]]) + (req (s3-request/error client + verb: 'PUT + bucket: (bucket-name self) + path: (string-append "/" dest) + extra-headers: headers))) + (request-close req) + (void))))) + + +; The core request method. Handles AWS Sig. v4, auth, and calls correct http- function based on +; `verb`. +(defmethod {request s3-client} + (lambda (self + verb: (verb 'GET) + bucket: (bucket #f) + path: (path "/") + query: (query #f) + body: (body #f) + ; optional extra headers + extra-headers: (extra-headers #f) + content-type: (content-type #f)) ; must be specified if body is specified + (using (self self : s3-client) + (let* ((now (current-date)) + (ts (date->string now "~Y~m~dT~H~M~SZ")) + (scopets (date->string now "~Y~m~d")) + (scope (string-append scopets "/" (s3-client-region self) "/s3")) + (hash (if body (sha256 body) emptySHA256)) + (host (if bucket + (string-append bucket "." (s3-client-endpoint self)) + (s3-client-endpoint self))) + (headers [["Host" :: (string-append host ":443")] + ["x-amz-date" :: ts] + ["x-amz-content-sha256" :: (hex-encode hash)] + (if body [["Content-Type" :: content-type]] []) ... + (if extra-headers extra-headers []) ...]) + (creq (aws4-canonical-request + verb: verb + uri: path + query: query + headers: headers + hash: hash)) + (headers [["Authorization" :: (aws4-auth scope creq ts headers + (s3-client-secret-key self) (s3-client-access-key self))] + :: headers]) + (url (string-append "https://" host path))) + (case verb + ((GET) + (http-get url headers: headers params: query)) + ((PUT) + (http-put url headers: headers params: query data: body)) + ((DELETE) + (http-delete url headers: headers params: query)) + ((HEAD) + (http-head url headers: headers params: query)) + (else + (error "Bad request verb" verb))))))) + +(defrule (s3-request/error self ...) + (with-request-error + {request self ...})) + +(def (s3-parse-xml req) + (read-xml (request-content req) + namespaces: '(("http://s3.amazonaws.com/doc/2006-03-01/" . "s3")))) + +(def (with-request-error req) + (using (req :~ request?) + (if (and (fx>= (request-status req) 200) + (fx< (request-status req) 300)) + req + (begin + (request-close req) + (raise-s3-error + (request-status req) + (request-status-text req)))))) diff --git a/src/std/net/s3/interface.ss b/src/std/net/s3/interface.ss new file mode 100644 index 000000000..70134f86f --- /dev/null +++ b/src/std/net/s3/interface.ss @@ -0,0 +1,22 @@ +(import :std/interface + :std/contract + :std/misc/alist) + +(export #t) + +(interface BucketMap + (get-bucket (name :~ string?)) + (create-bucket! (name :~ string?) + (opts :~ (maybe alist?) := #f)) + (delete-bucket! (name :~ string?)) + (bucket-exists? (name :~ string?)) + (list-buckets)) + +(interface ObjectMap + (get (name :~ string?)) + (put! (name :~ string?) + (data :~ u8vector?) + ; Additional options. See implementation for additional details + (opts :~ (maybe alist?) := #f)) + (delete! (name :~ string?)) + (list-objects)) diff --git a/src/std/net/s3/sigv4.ss b/src/std/net/s3/sigv4.ss new file mode 100644 index 000000000..d029ca50e --- /dev/null +++ b/src/std/net/s3/sigv4.ss @@ -0,0 +1,106 @@ +;;; -*- Gerbil -*- +;;; (C) vyzo +;;; AWS sigv4 request signatures +(import :std/misc/bytes + :std/srfi/13 + :std/crypto/digest + :std/crypto/hmac + :std/text/hex + :std/net/uri + :std/contract + :std/sort) +(export aws4-canonical-request aws4-sign aws4-auth) + +;; Reference: http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html + +;; create a canonical request string for signing +(def (aws4-canonical-request + verb: verb ; symbol -- http verb (GET PUT DELETE ...) + uri: uri ; string -- canonical request uri + query: query ; [[string . value] ...] -- query parameters + headers: headers ; [[string . value] ...] -- signed request headers + hash: hash ; bytes -- SHA256 content hash + ) + (string-append + (symbol->string verb) "\n" + uri "\n" + (if query (canonical-query-string query) "") "\n" + (canonical-headers headers) "\n" + (signed-headers headers) "\n" + (hex-encode hash))) + +;; calculate a signature for a canonical request +;; scope is the request scope: string in the form yyyymmdd/region/service +;; ts is the request timestamp string +;; request is a the canonical request string +(def (aws4-sign scope request-str ts secret-key) + (let ((key (signing-key scope secret-key)) + (str (string-to-sign scope request-str ts))) + (hmac-sha256 key (string->bytes str)))) + +;; Calcuate the authorization header +(def (aws4-auth scope request-str ts headers secret-key access-key) + (let (sig (aws4-sign scope request-str ts secret-key)) + (string-append "AWS4-HMAC-SHA256 " + "Credential=" access-key "/" scope "/aws4_request," + "SignedHeaders=" (signed-headers headers) "," + "Signature=" (hex-encode sig)))) + +;;; internal +(def (car-stringbytes + (string-append "AWS4" secret-key)) + (string->bytes date))) + (date-region-key + (hmac-sha256 date-key + (string->bytes region))) + (date-region-svc-key + (hmac-sha256 date-region-key + (string->bytes service)))) + (hmac-sha256 date-region-svc-key + (@bytes "aws4_request")))) + (else + (error "Bad request scope; expected date/region/service string" scope)))) + +(def (string-to-sign scope req ts) + (string-append "AWS4-HMAC-SHA256\n" + ts "\n" + scope "/aws4_request" "\n" + (hex-encode (sha256 req)))) From d3af8bec525c4445fdd78e8623dec3d06717c721 Mon Sep 17 00:00:00 2001 From: Noah Pederson Date: Wed, 18 Oct 2023 17:34:32 -0500 Subject: [PATCH 02/10] Address review comments --- src/std/net/s3.ss | 2 +- src/std/net/s3/api.ss | 71 ++++++++++++++++--------------------- src/std/net/s3/interface.ss | 3 ++ 3 files changed, 34 insertions(+), 42 deletions(-) diff --git a/src/std/net/s3.ss b/src/std/net/s3.ss index c0056d809..976179298 100644 --- a/src/std/net/s3.ss +++ b/src/std/net/s3.ss @@ -2,4 +2,4 @@ ;;; © vyzo, ngp ;;; AWS S3 Client (import ./s3/api ./s3/interface) -(export (import: ./s3/api) (import: ./s3/interface)) +(export (import: ./s3/api ./s3/interface)) diff --git a/src/std/net/s3/api.ss b/src/std/net/s3/api.ss index 378276cc9..8ceed2a37 100644 --- a/src/std/net/s3/api.ss +++ b/src/std/net/s3/api.ss @@ -37,7 +37,7 @@ (access-key (getenv "AWS_ACCESS_KEY_ID" #f)) (secret-key (getenv "AWS_SECRET_ACCESS_KEY" #f)) (region (getenv "AWS_DEFAULT_REGION" "us-east-1"))) - (using (self self : s3-client) + (using (self :- s3-client) (set! self.endpoint endpoint) (set! self.access-key access-key) (set! self.secret-key secret-key) @@ -46,7 +46,7 @@ ; Retrieves buckets accessible to this client. (defmethod {list-buckets s3-client} ; => (list : bucket) (lambda (self) - (using (self self : s3-client) + (using (self :- s3-client) (let* ((req (s3-request/error self verb: 'GET)) (xml (s3-parse-xml req)) (buckets (sxml-find xml (sxml-e? 's3:Buckets) sxml-children)) @@ -62,7 +62,7 @@ ;; NOTE: all bucket operations need the correct region for the bucket or they will 400 (defmethod {create-bucket! s3-client} (lambda (self bucket) - (using (self self : s3-client) + (using (self :- s3-client) (let (req (s3-request/error self verb: 'PUT bucket: bucket)) (request-close req) (void))))) @@ -70,24 +70,23 @@ ; Gets a bucket struct that can be used to fetch objects. (defmethod {get-bucket s3-client} ; => bucket (lambda (self bucket-name) - (using (self self : s3-client) - (if {bucket-exists? self bucket-name} + (using (self :- s3-client) + (if (s3-client::bucket-exists? self bucket-name) (make-bucket self bucket-name self.region) #f)))) ; Delete a bucket by name (defmethod {delete-bucket! s3-client} (lambda (self bucket) - (using ((self self : s3-client) - (bucket bucket :~ string?)) - (when {bucket-exists? self bucket} - (let (req (s3-request/error self verb: 'DELETE bucket: bucket)) - (request-close req) - (void)))))) + (using (self :- s3-client) + (when (s3-client::bucket-exists? self bucket) + (let (req (s3-request/error self verb: 'DELETE bucket: bucket)) + (request-close req) + (void)))))) (defmethod {bucket-exists? s3-client} (lambda (self bucket) - (using (self self : s3-client) + (using (self :- s3-client) (let* ((bucket (if (bucket? bucket) (bucket-name bucket) bucket)) (req {self.request verb: 'HEAD bucket: bucket}) (code (request-status req))) @@ -103,15 +102,15 @@ (defmethod {bucket s3-client} (lambda (self name) - (using (self self : s3-client) - (if {bucket-exists? self name} + (using (self self :- s3-client) + (if (s3-client::bucket-exists? self name) (make-bucket self name (s3-client-region self)) #f)))) ; Lists the objects stored within the bucket (defmethod {list-objects bucket} (lambda (self) - (using ((self self : bucket) + (using ((self self :- bucket) (client (bucket-client self) : s3-client)) (let* ((name (bucket-name self)) (req (s3-request/error client verb: 'GET bucket: name)) @@ -123,7 +122,6 @@ (defmethod {get bucket} (lambda (self key) (using ((self self : bucket) - (key :~ string?) (client (bucket-client self) : s3-client)) (let* ((req (s3-request/error client verb: 'GET bucket: (bucket-name self) path: (string-append "/" key))) @@ -134,7 +132,6 @@ (defmethod {put! bucket} (lambda (self key data content-type: (content-type "binary/octet-stream")) (using ((self self : bucket) - (key :~ string?) (client (bucket-client self) : s3-client)) (let (req (s3-request/error client verb: 'PUT bucket: (bucket-name self) path: (string-append "/" key) @@ -145,8 +142,7 @@ (defmethod {delete! bucket} (lambda (self key) - (using ((self : bucket) - (key :~ string?) + (using ((self :- bucket) (client (bucket-client self) : s3-client)) (let (req (s3-request/error client verb: 'DELETE bucket: (bucket-name self) path: (string-append "/" key))) @@ -154,17 +150,10 @@ (void))))) (defmethod {copy-to! bucket} - (lambda (self src-bucket src dest) - (using ((self : bucket) - (client (bucket-client self) : s3-client) - ; a bucket instance pointing to the intended source bucket - (src-bucket : bucket) - ; the source file name - (src :~ string?) - ; the destination file name - (dest :~ string?)) - (let* ((src-ident (string-append (bucket-name src-bucket) "/" src)) - (headers [["x-amz-copy-source" :: src-ident]]) + (lambda (self src dest) + (using ((self :- bucket) + (client (bucket-client self) : s3-client)) + (let* ((headers [["x-amz-copy-source" :: src]]) (req (s3-request/error client verb: 'PUT bucket: (bucket-name self) @@ -186,7 +175,7 @@ ; optional extra headers extra-headers: (extra-headers #f) content-type: (content-type #f)) ; must be specified if body is specified - (using (self self : s3-client) + (using (self :- s3-client) (let* ((now (current-date)) (ts (date->string now "~Y~m~dT~H~M~SZ")) (scopets (date->string now "~Y~m~d")) @@ -224,19 +213,19 @@ (defrule (s3-request/error self ...) (with-request-error - {request self ...})) + {request self ...})) (def (s3-parse-xml req) (read-xml (request-content req) - namespaces: '(("http://s3.amazonaws.com/doc/2006-03-01/" . "s3")))) + namespaces: '(("http://s3.amazonaws.com/doc/2006-03-01/" . "s3")))) (def (with-request-error req) (using (req :~ request?) - (if (and (fx>= (request-status req) 200) - (fx< (request-status req) 300)) - req - (begin - (request-close req) - (raise-s3-error - (request-status req) - (request-status-text req)))))) + (if (and (fx>= (request-status req) 200) + (fx< (request-status req) 300)) + req + (begin + (request-close req) + (raise-s3-error + (request-status req) + (request-status-text req)))))) diff --git a/src/std/net/s3/interface.ss b/src/std/net/s3/interface.ss index 70134f86f..e688146a4 100644 --- a/src/std/net/s3/interface.ss +++ b/src/std/net/s3/interface.ss @@ -19,4 +19,7 @@ ; Additional options. See implementation for additional details (opts :~ (maybe alist?) := #f)) (delete! (name :~ string?)) + ; src should follow `bucket/file/path` format. Destination should just be `file/path`. + ; Copies *from* src to *dest* in this ObjectMap. + (copy-to! (src :~ string?) (dest :~ string?)) (list-objects)) From bb6a5aa81bbac24e1ad149547eeb05c4ec205593 Mon Sep 17 00:00:00 2001 From: Noah Pederson Date: Thu, 19 Oct 2023 18:07:02 -0500 Subject: [PATCH 03/10] Handle error from s3 copy-to! in 200 response --- src/std/net/s3/api.ss | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/std/net/s3/api.ss b/src/std/net/s3/api.ss index 8ceed2a37..bf8ffd330 100644 --- a/src/std/net/s3/api.ss +++ b/src/std/net/s3/api.ss @@ -154,12 +154,20 @@ (using ((self :- bucket) (client (bucket-client self) : s3-client)) (let* ((headers [["x-amz-copy-source" :: src]]) - (req (s3-request/error client + (req (s3-client::request client verb: 'PUT bucket: (bucket-name self) path: (string-append "/" dest) - extra-headers: headers))) + extra-headers: headers)) + (error (s3-response-error? (s3-parse-xml req)))) (request-close req) + (when error + (raise-s3-error + bucket::copy-to! + "Unable to perform server-side copy" + ; when error isn't empty, it should be a parsed XML tree + (sxml-find error (sxml-e? 'Code) cadr) + (request-status-text req))) (void))))) @@ -213,12 +221,15 @@ (defrule (s3-request/error self ...) (with-request-error - {request self ...})) + (s3-client::request self ...))) (def (s3-parse-xml req) (read-xml (request-content req) namespaces: '(("http://s3.amazonaws.com/doc/2006-03-01/" . "s3")))) +(defrule (s3-response-error? xml) + (sxml-find xml (sxml-e? 'Error))) + (def (with-request-error req) (using (req :~ request?) (if (and (fx>= (request-status req) 200) From 146f75b92f7fb2529aa40a77a544374f85cfe519 Mon Sep 17 00:00:00 2001 From: Noah Pederson Date: Fri, 20 Oct 2023 22:13:35 -0500 Subject: [PATCH 04/10] Address review comments, see extra... * Raise exceptions instead of return #f * Don't export structs, create a constructor that wraps the return value in the interface * Raise error when credentials are not supplied * memv -> memq for speed * more appropriate use of (using ...) * Renames interfaces to be more clear --- src/std/net/s3/api.ss | 225 ++++++++++++++++++------------------ src/std/net/s3/interface.ss | 4 +- 2 files changed, 115 insertions(+), 114 deletions(-) diff --git a/src/std/net/s3/api.ss b/src/std/net/s3/api.ss index bf8ffd330..1242543b2 100644 --- a/src/std/net/s3/api.ss +++ b/src/std/net/s3/api.ss @@ -2,6 +2,7 @@ ;;; (C) vyzo ;;; AWS S3 client (import "sigv4" + "interface" :std/net/request :std/misc/func :std/contract @@ -12,15 +13,27 @@ :std/error :std/sugar :std/srfi/19) -(export (struct-out s3-client bucket) S3ClientError) +(export make-s3-client S3ClientError) + + +(def (make-s3-client + (endpoint "s3.amazonaws.com") + (access-key (getenv "AWS_ACCESS_KEY_ID" #f)) + (secret-key (getenv "AWS_SECRET_ACCESS_KEY" #f)) + (region (getenv "AWS_DEFAULT_REGION" "us-east-1")) + (cond + ((not access-key) + (raise-s3-error make-s3-client "Must provide access key" "access-key")) + ((not secret-key) + (raise-s3-error make-s3-client "Must provide secret key" "secret-key"))) + (S3 (make-s3-client endpoint access-key secret-key region)))) ; precomputed empty sha256 -(def emptySHA256 #u8(227 176 196 66 152 252 28 20 154 251 244 200 153 111 185 - 36 39 174 65 228 100 155 147 76 164 149 153 27 120 82 184 85)) +(def emptySHA256 (syntax-eval (sha256 #u8()))) (defstruct s3-client (endpoint access-key secret-key region) final: #t - constructor: :init!) + constructor: make-s3-client) (defstruct bucket (client name region) final: #t) @@ -30,34 +43,22 @@ (defraise/context (raise-s3-error where message irritants ...) (S3ClientError message irritants: [irritants ...])) -; Initializes a `s3-client`. Primarily responsible for holding onto credentials -(defmethod {:init! s3-client} - (lambda (self - (endpoint "s3.amazonaws.com") - (access-key (getenv "AWS_ACCESS_KEY_ID" #f)) - (secret-key (getenv "AWS_SECRET_ACCESS_KEY" #f)) - (region (getenv "AWS_DEFAULT_REGION" "us-east-1"))) - (using (self :- s3-client) - (set! self.endpoint endpoint) - (set! self.access-key access-key) - (set! self.secret-key secret-key) - (set! self.region region)))) - ; Retrieves buckets accessible to this client. (defmethod {list-buckets s3-client} ; => (list : bucket) (lambda (self) (using (self :- s3-client) - (let* ((req (s3-request/error self verb: 'GET)) - (xml (s3-parse-xml req)) - (buckets (sxml-find xml (sxml-e? 's3:Buckets) sxml-children)) - (names (map (chain <> - (sxml-select <> (sxml-e? 's3:Name)) - (cadar <>) - (make-bucket self <> (s3-client-region self))) - buckets))) - ; buckets is #f if none are returned - (request-close req) - names)))) + (let* ((req (s3-request/error self verb: 'GET)) + (xml (s3-parse-xml req)) + (buckets (sxml-find xml (sxml-e? 's3:Buckets) sxml-children)) + (names (map (chain <> + (sxml-select <> (sxml-e? 's3:Name)) + (cadar <>) + (make-bucket self <> (s3-client-region self)) + (S3Bucket <>)) + buckets))) + ; buckets is #f if none are returned + (request-close req) + names)))) ;; NOTE: all bucket operations need the correct region for the bucket or they will 400 (defmethod {create-bucket! s3-client} @@ -72,8 +73,8 @@ (lambda (self bucket-name) (using (self :- s3-client) (if (s3-client::bucket-exists? self bucket-name) - (make-bucket self bucket-name self.region) - #f)))) + (S3Bucket (make-bucket self bucket-name self.region)) + (raise-s3-error s3-client::get-bucket "Bucket does not exist" bucket-name))))) ; Delete a bucket by name (defmethod {delete-bucket! s3-client} @@ -87,52 +88,52 @@ (defmethod {bucket-exists? s3-client} (lambda (self bucket) (using (self :- s3-client) - (let* ((bucket (if (bucket? bucket) (bucket-name bucket) bucket)) - (req {self.request verb: 'HEAD bucket: bucket}) - (code (request-status req))) - ; 200 and 404 are expected codes - ; we explicitly handle 404 so we get proper predicate - ; semantics and don't raise on what would otherwise be - ; #f condition. - (if (memv code [200 404]) - (begin - (request-close req) - (= code 200)) - (with-request-error req)))))) + (let* ((bucket (if (bucket? bucket) (bucket-name bucket) bucket)) + (req {self.request verb: 'HEAD bucket: bucket}) + (code (request-status req))) + ; 200 and 404 are expected codes + ; we explicitly handle 404 so we get proper predicate + ; semantics and don't raise on what would otherwise be + ; #f condition. + (if (memq code [200 404]) + (begin + (request-close req) + (= code 200)) + (with-request-error req)))))) (defmethod {bucket s3-client} (lambda (self name) (using (self self :- s3-client) (if (s3-client::bucket-exists? self name) - (make-bucket self name (s3-client-region self)) - #f)))) + (S3Bucket (make-bucket self name (s3-client-region self))) + (raise-s3-error s3-client::bucket "bucket does not exist" name))))) ; Lists the objects stored within the bucket (defmethod {list-objects bucket} (lambda (self) (using ((self self :- bucket) - (client (bucket-client self) : s3-client)) - (let* ((name (bucket-name self)) - (req (s3-request/error client verb: 'GET bucket: name)) - (xml (s3-parse-xml req)) - (keys (sxml-select xml (sxml-e? 's3:Key) cadr))) - (request-close req) - keys)))) + (client (self.client) :- s3-client)) + (let* ((name (bucket-name self)) + (req (s3-request/error client verb: 'GET bucket: name)) + (xml (s3-parse-xml req)) + (keys (sxml-select xml (sxml-e? 's3:Key) cadr))) + (request-close req) + keys)))) (defmethod {get bucket} (lambda (self key) - (using ((self self : bucket) - (client (bucket-client self) : s3-client)) - (let* ((req (s3-request/error client verb: 'GET bucket: (bucket-name self) - path: (string-append "/" key))) - (data (request-content req))) - (request-close req) - data)))) + (using ((self :- bucket) + (client (self.client) :- s3-client)) + (let* ((req (s3-request/error client verb: 'GET bucket: (bucket-name self) + path: (string-append "/" key))) + (data (request-content req))) + (request-close req) + data)))) (defmethod {put! bucket} (lambda (self key data content-type: (content-type "binary/octet-stream")) - (using ((self self : bucket) - (client (bucket-client self) : s3-client)) + (using ((self :- bucket) + (client (self.client) :- s3-client)) (let (req (s3-request/error client verb: 'PUT bucket: (bucket-name self) path: (string-append "/" key) body: data @@ -143,7 +144,7 @@ (defmethod {delete! bucket} (lambda (self key) (using ((self :- bucket) - (client (bucket-client self) : s3-client)) + (client (self.client) :- s3-client)) (let (req (s3-request/error client verb: 'DELETE bucket: (bucket-name self) path: (string-append "/" key))) (request-close req) @@ -152,23 +153,23 @@ (defmethod {copy-to! bucket} (lambda (self src dest) (using ((self :- bucket) - (client (bucket-client self) : s3-client)) - (let* ((headers [["x-amz-copy-source" :: src]]) - (req (s3-client::request client - verb: 'PUT - bucket: (bucket-name self) - path: (string-append "/" dest) - extra-headers: headers)) - (error (s3-response-error? (s3-parse-xml req)))) - (request-close req) - (when error - (raise-s3-error - bucket::copy-to! - "Unable to perform server-side copy" - ; when error isn't empty, it should be a parsed XML tree - (sxml-find error (sxml-e? 'Code) cadr) - (request-status-text req))) - (void))))) + (client (self.client) :- s3-client)) + (let* ((headers [["x-amz-copy-source" :: src]]) + (req (s3-client::request client + verb: 'PUT + bucket: (bucket-name self) + path: (string-append "/" dest) + extra-headers: headers)) + (error (s3-response-error? (s3-parse-xml req)))) + (request-close req) + (when error + (raise-s3-error + bucket::copy-to! + "Unable to perform server-side copy" + ; when error isn't empty, it should be a parsed XML tree + (sxml-find error (sxml-e? 'Code) cadr) + (request-status-text req))) + (void))))) ; The core request method. Handles AWS Sig. v4, auth, and calls correct http- function based on @@ -184,40 +185,40 @@ extra-headers: (extra-headers #f) content-type: (content-type #f)) ; must be specified if body is specified (using (self :- s3-client) - (let* ((now (current-date)) - (ts (date->string now "~Y~m~dT~H~M~SZ")) - (scopets (date->string now "~Y~m~d")) - (scope (string-append scopets "/" (s3-client-region self) "/s3")) - (hash (if body (sha256 body) emptySHA256)) - (host (if bucket - (string-append bucket "." (s3-client-endpoint self)) - (s3-client-endpoint self))) - (headers [["Host" :: (string-append host ":443")] - ["x-amz-date" :: ts] - ["x-amz-content-sha256" :: (hex-encode hash)] - (if body [["Content-Type" :: content-type]] []) ... - (if extra-headers extra-headers []) ...]) - (creq (aws4-canonical-request - verb: verb - uri: path - query: query - headers: headers - hash: hash)) - (headers [["Authorization" :: (aws4-auth scope creq ts headers - (s3-client-secret-key self) (s3-client-access-key self))] - :: headers]) - (url (string-append "https://" host path))) - (case verb - ((GET) - (http-get url headers: headers params: query)) - ((PUT) - (http-put url headers: headers params: query data: body)) - ((DELETE) - (http-delete url headers: headers params: query)) - ((HEAD) - (http-head url headers: headers params: query)) - (else - (error "Bad request verb" verb))))))) + (let* ((now (current-date)) + (ts (date->string now "~Y~m~dT~H~M~SZ")) + (scopets (date->string now "~Y~m~d")) + (scope (string-append scopets "/" (s3-client-region self) "/s3")) + (hash (if body (sha256 body) emptySHA256)) + (host (if bucket + (string-append bucket "." (s3-client-endpoint self)) + (s3-client-endpoint self))) + (headers [["Host" :: (string-append host ":443")] + ["x-amz-date" :: ts] + ["x-amz-content-sha256" :: (hex-encode hash)] + (if body [["Content-Type" :: content-type]] []) ... + (if extra-headers extra-headers []) ...]) + (creq (aws4-canonical-request + verb: verb + uri: path + query: query + headers: headers + hash: hash)) + (headers [["Authorization" :: (aws4-auth scope creq ts headers + (s3-client-secret-key self) (s3-client-access-key self))] + :: headers]) + (url (string-append "https://" host path))) + (case verb + ((GET) + (http-get url headers: headers params: query)) + ((PUT) + (http-put url headers: headers params: query data: body)) + ((DELETE) + (http-delete url headers: headers params: query)) + ((HEAD) + (http-head url headers: headers params: query)) + (else + (error "Bad request verb" verb))))))) (defrule (s3-request/error self ...) (with-request-error diff --git a/src/std/net/s3/interface.ss b/src/std/net/s3/interface.ss index e688146a4..4175e684e 100644 --- a/src/std/net/s3/interface.ss +++ b/src/std/net/s3/interface.ss @@ -4,7 +4,7 @@ (export #t) -(interface BucketMap +(interface S3 (get-bucket (name :~ string?)) (create-bucket! (name :~ string?) (opts :~ (maybe alist?) := #f)) @@ -12,7 +12,7 @@ (bucket-exists? (name :~ string?)) (list-buckets)) -(interface ObjectMap +(interface S3Bucket (get (name :~ string?)) (put! (name :~ string?) (data :~ u8vector?) From 19f3b861de25c8e45fc29ac13c7b272747da43f8 Mon Sep 17 00:00:00 2001 From: Noah Pederson Date: Sat, 21 Oct 2023 17:51:02 -0500 Subject: [PATCH 05/10] Fixes failing build... * Renames s3-client constructor from make-s3-client to S3Client to not conflict with the one generated by `defstruct` * Calculate the empty-body sha256 hash on module import time --- src/std/net/s3/api.ss | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/std/net/s3/api.ss b/src/std/net/s3/api.ss index 1242543b2..0d91d4b26 100644 --- a/src/std/net/s3/api.ss +++ b/src/std/net/s3/api.ss @@ -13,27 +13,27 @@ :std/error :std/sugar :std/srfi/19) -(export make-s3-client S3ClientError) +(export S3Client S3ClientError) -(def (make-s3-client +; precomputed empty sha256 +(def emptySHA256 (sha256 #u8())) + +(def (S3Client (endpoint "s3.amazonaws.com") (access-key (getenv "AWS_ACCESS_KEY_ID" #f)) (secret-key (getenv "AWS_SECRET_ACCESS_KEY" #f)) - (region (getenv "AWS_DEFAULT_REGION" "us-east-1")) - (cond - ((not access-key) - (raise-s3-error make-s3-client "Must provide access key" "access-key")) - ((not secret-key) - (raise-s3-error make-s3-client "Must provide secret key" "secret-key"))) - (S3 (make-s3-client endpoint access-key secret-key region)))) - -; precomputed empty sha256 -(def emptySHA256 (syntax-eval (sha256 #u8()))) + (region (getenv "AWS_DEFAULT_REGION" "us-east-1"))) + (cond + ((not access-key) + (raise-s3-error make-s3-client "Must provide access key" "access-key")) + ((not secret-key) + (raise-s3-error make-s3-client "Must provide secret key" "secret-key"))) + (S3 (make-s3-client endpoint access-key secret-key region))) (defstruct s3-client (endpoint access-key secret-key region) final: #t - constructor: make-s3-client) + constructor: S3Client) (defstruct bucket (client name region) final: #t) From 33109cdf17b1546e137c626ab6531690da203121 Mon Sep 17 00:00:00 2001 From: Noah Pederson Date: Fri, 27 Oct 2023 19:30:34 -0500 Subject: [PATCH 06/10] Fixes incorrect syntax in interface --- src/std/net/s3/api.ss | 29 +++++++++++++++-------------- src/std/net/s3/interface.ss | 3 +-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/std/net/s3/api.ss b/src/std/net/s3/api.ss index 0d91d4b26..8001a648c 100644 --- a/src/std/net/s3/api.ss +++ b/src/std/net/s3/api.ss @@ -20,10 +20,10 @@ (def emptySHA256 (sha256 #u8())) (def (S3Client - (endpoint "s3.amazonaws.com") - (access-key (getenv "AWS_ACCESS_KEY_ID" #f)) - (secret-key (getenv "AWS_SECRET_ACCESS_KEY" #f)) - (region (getenv "AWS_DEFAULT_REGION" "us-east-1"))) + endpoint: (endpoint "s3.amazonaws.com") + access-key: (access-key (getenv "AWS_ACCESS_KEY_ID" #f)) + secret-key: (secret-key (getenv "AWS_SECRET_ACCESS_KEY" #f)) + region: (region (getenv "AWS_DEFAULT_REGION" "us-east-1"))) (cond ((not access-key) (raise-s3-error make-s3-client "Must provide access key" "access-key")) @@ -32,8 +32,7 @@ (S3 (make-s3-client endpoint access-key secret-key region))) (defstruct s3-client (endpoint access-key secret-key region) - final: #t - constructor: S3Client) + final: #t) (defstruct bucket (client name region) final: #t) @@ -103,7 +102,7 @@ (defmethod {bucket s3-client} (lambda (self name) - (using (self self :- s3-client) + (using (self :- s3-client) (if (s3-client::bucket-exists? self name) (S3Bucket (make-bucket self name (s3-client-region self))) (raise-s3-error s3-client::bucket "bucket does not exist" name))))) @@ -111,8 +110,8 @@ ; Lists the objects stored within the bucket (defmethod {list-objects bucket} (lambda (self) - (using ((self self :- bucket) - (client (self.client) :- s3-client)) + (using ((self :- bucket) + (client self.client :- s3-client)) (let* ((name (bucket-name self)) (req (s3-request/error client verb: 'GET bucket: name)) (xml (s3-parse-xml req)) @@ -123,8 +122,10 @@ (defmethod {get bucket} (lambda (self key) (using ((self :- bucket) - (client (self.client) :- s3-client)) - (let* ((req (s3-request/error client verb: 'GET bucket: (bucket-name self) + (client self.client :- s3-client)) + (let* ((req (s3-request/error client + verb: 'GET + bucket: (bucket-name self) path: (string-append "/" key))) (data (request-content req))) (request-close req) @@ -133,7 +134,7 @@ (defmethod {put! bucket} (lambda (self key data content-type: (content-type "binary/octet-stream")) (using ((self :- bucket) - (client (self.client) :- s3-client)) + (client self.client :- s3-client)) (let (req (s3-request/error client verb: 'PUT bucket: (bucket-name self) path: (string-append "/" key) body: data @@ -144,7 +145,7 @@ (defmethod {delete! bucket} (lambda (self key) (using ((self :- bucket) - (client (self.client) :- s3-client)) + (client self.client :- s3-client)) (let (req (s3-request/error client verb: 'DELETE bucket: (bucket-name self) path: (string-append "/" key))) (request-close req) @@ -153,7 +154,7 @@ (defmethod {copy-to! bucket} (lambda (self src dest) (using ((self :- bucket) - (client (self.client) :- s3-client)) + (client self.client :- s3-client)) (let* ((headers [["x-amz-copy-source" :: src]]) (req (s3-client::request client verb: 'PUT diff --git a/src/std/net/s3/interface.ss b/src/std/net/s3/interface.ss index 4175e684e..de8c59802 100644 --- a/src/std/net/s3/interface.ss +++ b/src/std/net/s3/interface.ss @@ -16,8 +16,7 @@ (get (name :~ string?)) (put! (name :~ string?) (data :~ u8vector?) - ; Additional options. See implementation for additional details - (opts :~ (maybe alist?) := #f)) + content-type: (content-type := "octet-stream" :~ string?)) (delete! (name :~ string?)) ; src should follow `bucket/file/path` format. Destination should just be `file/path`. ; Copies *from* src to *dest* in this ObjectMap. From b39b5075cb154c48113f25c5e4bc7ceea1d3b065 Mon Sep 17 00:00:00 2001 From: Noah Pederson Date: Sat, 28 Oct 2023 12:41:04 -0500 Subject: [PATCH 07/10] Fixes keyword method handling in interface macro --- src/std/interface.ss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/std/interface.ss b/src/std/interface.ss index b04998df4..1a4237e6f 100644 --- a/src/std/interface.ss +++ b/src/std/interface.ss @@ -405,7 +405,7 @@ ((kw (id default) . rest) (stx-keyword? #'kw) (lp #'rest (cons* #'(id absent-obj) #'kw args))) - ((kw (id . contract) .rest) + ((kw (id . contract) . rest) (stx-keyword? #'kw) (cond ((get-contract-default #'contract) @@ -439,7 +439,7 @@ ((kw (id default) . rest) (stx-keyword? #'kw) (lp #'rest (cons* #'(id default) #'kw args))) - ((kw (id . contract) .rest) + ((kw (id . contract) . rest) (stx-keyword? #'kw) (cond ((get-contract-default #'contract) From e2125673a2ad70caebd5c894f885c20398a5bdf8 Mon Sep 17 00:00:00 2001 From: Noah Pederson Date: Sat, 28 Oct 2023 13:07:24 -0500 Subject: [PATCH 08/10] Remove unnecessary interface args --- src/std/net/s3/interface.ss | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/std/net/s3/interface.ss b/src/std/net/s3/interface.ss index de8c59802..41eed201d 100644 --- a/src/std/net/s3/interface.ss +++ b/src/std/net/s3/interface.ss @@ -6,8 +6,7 @@ (interface S3 (get-bucket (name :~ string?)) - (create-bucket! (name :~ string?) - (opts :~ (maybe alist?) := #f)) + (create-bucket! (name :~ string?)) (delete-bucket! (name :~ string?)) (bucket-exists? (name :~ string?)) (list-buckets)) @@ -15,8 +14,7 @@ (interface S3Bucket (get (name :~ string?)) (put! (name :~ string?) - (data :~ u8vector?) - content-type: (content-type := "octet-stream" :~ string?)) + (data :~ u8vector?)) (delete! (name :~ string?)) ; src should follow `bucket/file/path` format. Destination should just be `file/path`. ; Copies *from* src to *dest* in this ObjectMap. From 9254b0f61b34242cd10f25211ee62853318fb89a Mon Sep 17 00:00:00 2001 From: Noah Pederson Date: Sun, 29 Oct 2023 15:38:30 -0500 Subject: [PATCH 09/10] Rename env var, prevent shadowing --- src/std/net/s3/api.ss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/std/net/s3/api.ss b/src/std/net/s3/api.ss index 8001a648c..832efa05a 100644 --- a/src/std/net/s3/api.ss +++ b/src/std/net/s3/api.ss @@ -22,7 +22,7 @@ (def (S3Client endpoint: (endpoint "s3.amazonaws.com") access-key: (access-key (getenv "AWS_ACCESS_KEY_ID" #f)) - secret-key: (secret-key (getenv "AWS_SECRET_ACCESS_KEY" #f)) + secret-key: (secret-key (getenv "AWS_SECRET_KEY" #f)) region: (region (getenv "AWS_DEFAULT_REGION" "us-east-1"))) (cond ((not access-key) @@ -61,11 +61,11 @@ ;; NOTE: all bucket operations need the correct region for the bucket or they will 400 (defmethod {create-bucket! s3-client} - (lambda (self bucket) + (lambda (self bucket-name) (using (self :- s3-client) - (let (req (s3-request/error self verb: 'PUT bucket: bucket)) + (let (req (s3-request/error self verb: 'PUT bucket: bucket-name)) (request-close req) - (void))))) + (S3Bucket (make-bucket self bucket-name self.region)))))) ; Gets a bucket struct that can be used to fetch objects. (defmethod {get-bucket s3-client} ; => bucket From d2304578abcf80e6f4ca7b91dcfc34f8727f08ad Mon Sep 17 00:00:00 2001 From: Noah Pederson Date: Sun, 29 Oct 2023 15:50:10 -0500 Subject: [PATCH 10/10] Adds s3 doc --- doc/.vuepress/config.js | 3 +- doc/reference/std/net/README.md | 1 + doc/reference/std/net/s3.md | 148 ++++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 doc/reference/std/net/s3.md diff --git a/doc/.vuepress/config.js b/doc/.vuepress/config.js index f0ad75af1..e1afb1696 100644 --- a/doc/.vuepress/config.js +++ b/doc/.vuepress/config.js @@ -94,7 +94,8 @@ module.exports = { "net/uri", "net/address", "net/sasl", - "net/repl" + "net/repl", + "net/s3" ] }, diff --git a/doc/reference/std/net/README.md b/doc/reference/std/net/README.md index feaa0a3c3..4969a7d8e 100644 --- a/doc/reference/std/net/README.md +++ b/doc/reference/std/net/README.md @@ -11,3 +11,4 @@ These are libraries related to network programming: - [:std/net/address](net.md) - [:std/net/sasl](sasl.md) - [:std/net/repl](repl.md) +- [:std/net/s3](s3.md) diff --git a/doc/reference/std/net/s3.md b/doc/reference/std/net/s3.md new file mode 100644 index 000000000..0aaa47942 --- /dev/null +++ b/doc/reference/std/net/s3.md @@ -0,0 +1,148 @@ +# Amazon S3 Client + +The `:std/net/s3` library provides basic support for interfacing with Amazon S3 and +compatible services. + +::: warning +Only HTTPS is currently supported +::: + +## Creating a Client + +The primary way to interact with S3 via this library is with the `S3` interface. + +To create an instance of an `S3` client, use the `S3Client` constructor: + +```scheme +(S3Client + endpoint: (endpoint "s3.amazonaws.com") + access-key: (access-key (getenv "AWS_ACCESS_KEY_ID" #f)) + secret-key: (secret-key (getenv "AWS_SECRET_KEY" #f)) + region: (region (getenv "AWS_DEFAULT_REGION" "us-east-1"))) +``` + +If `access-key` or `secret-key` aren't passed in and cannot be retrieved from the +environment (via `AWS_ACCESS_KEY_ID` and `AWS_SECRET_KEY` respectively) a `S3Error` is +raised. + +## Client + +The `S3` interface has the following signature: +```scheme +(interface S3 + (get-bucket (name :~ string?)) + (create-bucket! (name :~ string?)) + (delete-bucket! (name :~ string?)) + (bucket-exists? (name :~ string?)) + (list-buckets)) +``` + +## S3-get-bucket + +```scheme +(S3-get-bucket client bucket-name) -> S3Bucket +``` + +`S3-get-bucket` retrieves a `S3Bucket` instance by name. If the bucket does not exist, a +`S3Error` is raised. Buckets are searched for in the service and region provided to +`S3Client`'s `endpoint` and `region` arguments. + +## S3-create-bucket! + +```scheme +(S3-create-bucket! client bucket-name) -> S3Bucket +``` + +`S3-create-bucket!` creates a new bucket in the service and region the client was +instantiated with. If a bucket with the name already exists, a `S3Error` is raised +indicating there was a conflict. If the bucket is successfully created, an instance of +`S3Bucket` corresponding to the newly created bucket is returned. + +## S3-delete-bucket! + +```scheme +(S3-delete-bucket! client bucket-name) -> void +``` + +`S3-delete-bucket!` attempts to delete a bucket with the given name. `delete-bucket!` is +idempotent and will **not** error if the bucket does not exist. + +## S3-bucket-exists? + +```scheme +(S3-bucket-exists? client bucket-name) -> bool +``` + +`S3-bucket-exists?` checks if a bucket with the provided name exists in the client's +configured region and endpoint. It returns `#t` if a bucket exists and `#f` otherwise. + +## S3-list-buckets + +```scheme +(S3-list-buckets client) -> list : S3Bucket +``` + +`S3-list-buckets` returns a list of all buckets available to the client as configured. If +none are available, an empty list is returned. All buckets are instances of `S3Bucket`. + +## S3Bucket + +The `S3Bucket` interface provides a consistent way to interact with buckets. + +```scheme +(interface S3Bucket + (get (name :~ string?)) + (put! (name :~ string?) + (data :~ u8vector?)) + (delete! (name :~ string?)) + (copy-to! (src :~ string?) (dest :~ string?)) + (list-objects)) +``` +## S3Bucket-get + +```scheme +(S3Bucket-get bucket object-name) -> u8vector +``` + +`S3Bucket-get` retrieves a object by name. If the object does not exist or the client +does not have permission to retrieve the object, an `S3Error` is raised. + +## S3Bucket-put! + +```scheme +(S3Bucket-put! bucket object-name) -> void +``` + +`S3Bucket-put!` stores `data` in a object with the provided `name`. Any failures result +in a `S3Error` being raised. All data is stored with `content-type: "octet-stream"` MIME +type. + +## S3Bucket-delete! + +```scheme +(S3Bucket-delete! bucket object-name) -> void +``` + +`S3Bucket-delete!` delete's an object with the provided `name`. If the object does not +exist, an `S3Error` is raised. + +## S3Bucket-copy-to! + +```scheme +(S3Bucket-copy-to! bucket src dest) -> void +``` + +`S3Bucket-copy-to!` performs server-side copy of object described by `src`. The `src` +format is `/`. The `dest` is the same as if performing a `put!` and +will store the object in the current `S3Bucket`. Copies must be within a region and +otherwise possible to perform using the `S3` client this bucket was created with. +Any failures result in a `S3Error` being raised. + +## S3Bucket-list-objects + +```scheme +(S3Bucket-list-objects bucket) -> list : string +``` + +`S3Bucket-list-objects` enumerates all objects in the bucket, returning their names as +strings.