v / vlib / net / s3 / fetch.v
162 lines · 154 sloc · 4.32 KB · 4142432483c4e8de44ab7b0d6ac944f3251e03c8
Raw
1// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module s3
5
6import net.http
7
8// FetchOptions overlays an S3 endpoint over the `fetch` call. All fields
9// are optional; bucket and key are taken from the URL.
10@[params]
11pub struct FetchOptions {
12pub:
13 method http.Method = .get
14 body []u8
15 credentials Credentials
16 // content_type, acl, etc. are forwarded as-is when method is PUT/POST.
17 content_type string
18 content_disposition string
19 content_encoding string
20 cache_control string
21 acl Acl
22 storage_class StorageClass
23 request_payer bool
24 range string
25 hash_payload bool
26}
27
28// FetchResponse is the simplified return type of `fetch`. It's intentionally
29// flat (no streaming yet) — easier to consume than V's `http.Response` and
30// surfaces the most useful fields.
31pub struct FetchResponse {
32pub:
33 status_code int
34 body []u8
35 headers map[string]string
36 etag string
37 content_type string
38 content_length i64
39}
40
41// fetch is a `fetch('s3://bucket/key', { ... })`-style helper.
42//
43// Examples:
44//
45// resp := s3.fetch('s3://my-bucket/path/to/file.txt')!
46// resp := s3.fetch('s3://my-bucket/key', method: .put, body: 'hello'.bytes())!
47// resp := s3.fetch('s3://key', method: .get, credentials: s3.Credentials{ bucket: 'b', ... })!
48//
49// The URL must use the `s3://` scheme. Anything else is rejected outright
50// (avoids accidentally calling a real HTTP endpoint with S3 credentials).
51pub fn fetch(url string, opts FetchOptions) !FetchResponse {
52 if !url.starts_with('s3://') {
53 return new_error('InvalidURL', 's3.fetch only accepts s3:// URLs (got: ${redact_url(url)})')
54 }
55 bucket, key := parse_s3_url(url)!
56 mut creds := if opts.credentials.access_key_id == '' {
57 Credentials.from_env()
58 } else {
59 opts.credentials
60 }
61 if bucket != '' {
62 creds = Credentials{
63 ...creds
64 bucket: bucket
65 }
66 }
67 creds.validate()!
68
69 c := Client{
70 credentials: creds
71 }
72 match opts.method {
73 .get {
74 data := c.get(key,
75 bucket: bucket
76 range: opts.range
77 request_payer: opts.request_payer
78 )!
79 return FetchResponse{
80 status_code: 200
81 body: data
82 content_length: i64(data.len)
83 }
84 }
85 .head {
86 st := c.stat(key,
87 bucket: bucket
88 request_payer: opts.request_payer
89 )!
90 return FetchResponse{
91 status_code: 200
92 etag: st.etag
93 content_type: st.content_type
94 content_length: st.size
95 }
96 }
97 .put {
98 c.put(key, opts.body,
99 bucket: bucket
100 content_type: opts.content_type
101 content_disposition: opts.content_disposition
102 content_encoding: opts.content_encoding
103 cache_control: opts.cache_control
104 acl: opts.acl
105 storage_class: opts.storage_class
106 request_payer: opts.request_payer
107 hash_payload: opts.hash_payload
108 )!
109 return FetchResponse{
110 status_code: 200
111 }
112 }
113 .delete {
114 c.delete(key,
115 bucket: bucket
116 request_payer: opts.request_payer
117 )!
118 return FetchResponse{
119 status_code: 204
120 }
121 }
122 else {
123 return new_error('InvalidMethod', 's3.fetch: unsupported method ${opts.method}')
124 }
125 }
126
127 return new_error('InvalidMethod', 's3.fetch: unreachable')
128}
129
130// parse_s3_url splits `s3://bucket/key/with/slashes` into (bucket, key).
131//
132// Special case: `s3://key` (no second path component) returns ('', 'key') —
133// the caller is then expected to provide the bucket via credentials.
134pub fn parse_s3_url(url string) !(string, string) {
135 if !url.starts_with('s3://') {
136 return new_error('InvalidURL', 'Not an s3:// URL')
137 }
138 rest := url[5..] // strip 's3://'
139 if rest == '' {
140 return new_error('InvalidURL', 'Empty s3:// URL')
141 }
142 if i := rest.index('/') {
143 bucket := rest[..i]
144 key := rest[i + 1..]
145 if key == '' {
146 return new_error('InvalidURL', 'Empty key in s3:// URL')
147 }
148 return bucket, key
149 }
150 // no '/' separator — treat the whole rest as the key
151 return '', rest
152}
153
154// redact_url strips query strings before logging. Used in error messages so
155// presigned-URL-style query params (which can contain credentials) aren't
156// leaked into logs.
157pub fn redact_url(url string) string {
158 if i := url.index('?') {
159 return url[..i] + '?<redacted>'
160 }
161 return url
162}
163