v2 / vlib / net / s3 / list.v
121 lines · 117 sloc · 3.88 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
6// list returns up to ~1000 objects matching `opts`. Use the returned
7// `next_continuation_token` to page through more.
8//
9// Speaks the ListObjectsV2 protocol.
10pub fn (c &Client) list(opts ListOptions) !ListResult {
11 bucket := pick_bucket(c.credentials, opts.bucket)!
12 creds := c.creds_for(bucket)
13 path := bucket_path(creds, bucket)
14 mut q := map[string]string{}
15 q['list-type'] = '2'
16 if opts.prefix != '' {
17 q['prefix'] = opts.prefix
18 }
19 if opts.continuation_token != '' {
20 q['continuation-token'] = opts.continuation_token
21 }
22 if opts.delimiter != '' {
23 q['delimiter'] = opts.delimiter
24 }
25 if opts.max_keys > 0 {
26 q['max-keys'] = opts.max_keys.str()
27 }
28 if opts.start_after != '' {
29 q['start-after'] = opts.start_after
30 }
31 if opts.encoding_type != '' {
32 q['encoding-type'] = opts.encoding_type
33 }
34 if opts.fetch_owner {
35 q['fetch-owner'] = 'true'
36 }
37 query := canonical_query_string(q)
38 signed := sign_request(creds, SignRequest{
39 method: 'GET'
40 path: path
41 query: query
42 payload_hash: empty_sha256
43 })!
44 resp := c.do_http(signed, '')!
45 if resp.status_code != 200 {
46 return new_http_error(resp.status_code, bucket, resp.body)
47 }
48 return parse_list_response(resp.body)
49}
50
51// parse_list_response is hand-rolled XML extraction. ListObjectsV2 uses a
52// rigid structure with no attributes we care about, so a tag scanner beats
53// pulling in the full XML decoder both in code size and predictability
54// against malformed input.
55pub fn parse_list_response(body string) !ListResult {
56 objects := parse_contents(body)
57 common := parse_common_prefixes(body)
58 max_keys := extract_xml_tag(body, 'MaxKeys').int()
59 key_count := extract_xml_tag(body, 'KeyCount').int()
60 is_truncated := extract_xml_tag(body, 'IsTruncated') == 'true'
61 return ListResult{
62 name: extract_xml_tag(body, 'Name')
63 prefix: extract_xml_tag(body, 'Prefix')
64 delimiter: extract_xml_tag(body, 'Delimiter')
65 start_after: extract_xml_tag(body, 'StartAfter')
66 max_keys: max_keys
67 key_count: key_count
68 is_truncated: is_truncated
69 continuation_token: extract_xml_tag(body, 'ContinuationToken')
70 next_continuation_token: extract_xml_tag(body, 'NextContinuationToken')
71 objects: objects
72 common_prefixes: common
73 }
74}
75
76fn parse_contents(body string) []ObjectInfo {
77 mut out := []ObjectInfo{}
78 mut start := 0
79 for {
80 open := body.index_after('<Contents>', start) or { break }
81 close := body.index_after('</Contents>', open) or { break }
82 seg := body[open + '<Contents>'.len..close]
83 mut owner := ?Owner(none)
84 owner_open := seg.index('<Owner>') or { -1 }
85 if owner_open >= 0 {
86 owner_close := seg.index('</Owner>') or { -1 }
87 if owner_close > owner_open {
88 owner_seg := seg[owner_open + '<Owner>'.len..owner_close]
89 owner = Owner{
90 id: extract_xml_tag(owner_seg, 'ID')
91 display_name: extract_xml_tag(owner_seg, 'DisplayName')
92 }
93 }
94 }
95 out << ObjectInfo{
96 key: extract_xml_tag(seg, 'Key')
97 last_modified: extract_xml_tag(seg, 'LastModified')
98 etag: extract_xml_tag(seg, 'ETag').trim('"')
99 size: extract_xml_tag(seg, 'Size').i64()
100 storage_class: extract_xml_tag(seg, 'StorageClass')
101 owner: owner
102 }
103 start = close + '</Contents>'.len
104 }
105 return out
106}
107
108fn parse_common_prefixes(body string) []CommonPrefix {
109 mut out := []CommonPrefix{}
110 mut start := 0
111 for {
112 open := body.index_after('<CommonPrefixes>', start) or { break }
113 close := body.index_after('</CommonPrefixes>', open) or { break }
114 seg := body[open + '<CommonPrefixes>'.len..close]
115 out << CommonPrefix{
116 prefix: extract_xml_tag(seg, 'Prefix')
117 }
118 start = close + '</CommonPrefixes>'.len
119 }
120 return out
121}
122