// Copyright (c) 2019-2026 Alexander Medvednikov. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. module s3 // list returns up to ~1000 objects matching `opts`. Use the returned // `next_continuation_token` to page through more. // // Speaks the ListObjectsV2 protocol. pub fn (c &Client) list(opts ListOptions) !ListResult { bucket := pick_bucket(c.credentials, opts.bucket)! creds := c.creds_for(bucket) path := bucket_path(creds, bucket) mut q := map[string]string{} q['list-type'] = '2' if opts.prefix != '' { q['prefix'] = opts.prefix } if opts.continuation_token != '' { q['continuation-token'] = opts.continuation_token } if opts.delimiter != '' { q['delimiter'] = opts.delimiter } if opts.max_keys > 0 { q['max-keys'] = opts.max_keys.str() } if opts.start_after != '' { q['start-after'] = opts.start_after } if opts.encoding_type != '' { q['encoding-type'] = opts.encoding_type } if opts.fetch_owner { q['fetch-owner'] = 'true' } query := canonical_query_string(q) signed := sign_request(creds, SignRequest{ method: 'GET' path: path query: query payload_hash: empty_sha256 })! resp := c.do_http(signed, '')! if resp.status_code != 200 { return new_http_error(resp.status_code, bucket, resp.body) } return parse_list_response(resp.body) } // parse_list_response is hand-rolled XML extraction. ListObjectsV2 uses a // rigid structure with no attributes we care about, so a tag scanner beats // pulling in the full XML decoder both in code size and predictability // against malformed input. pub fn parse_list_response(body string) !ListResult { objects := parse_contents(body) common := parse_common_prefixes(body) max_keys := extract_xml_tag(body, 'MaxKeys').int() key_count := extract_xml_tag(body, 'KeyCount').int() is_truncated := extract_xml_tag(body, 'IsTruncated') == 'true' return ListResult{ name: extract_xml_tag(body, 'Name') prefix: extract_xml_tag(body, 'Prefix') delimiter: extract_xml_tag(body, 'Delimiter') start_after: extract_xml_tag(body, 'StartAfter') max_keys: max_keys key_count: key_count is_truncated: is_truncated continuation_token: extract_xml_tag(body, 'ContinuationToken') next_continuation_token: extract_xml_tag(body, 'NextContinuationToken') objects: objects common_prefixes: common } } fn parse_contents(body string) []ObjectInfo { mut out := []ObjectInfo{} mut start := 0 for { open := body.index_after('', start) or { break } close := body.index_after('', open) or { break } seg := body[open + ''.len..close] mut owner := ?Owner(none) owner_open := seg.index('') or { -1 } if owner_open >= 0 { owner_close := seg.index('') or { -1 } if owner_close > owner_open { owner_seg := seg[owner_open + ''.len..owner_close] owner = Owner{ id: extract_xml_tag(owner_seg, 'ID') display_name: extract_xml_tag(owner_seg, 'DisplayName') } } } out << ObjectInfo{ key: extract_xml_tag(seg, 'Key') last_modified: extract_xml_tag(seg, 'LastModified') etag: extract_xml_tag(seg, 'ETag').trim('"') size: extract_xml_tag(seg, 'Size').i64() storage_class: extract_xml_tag(seg, 'StorageClass') owner: owner } start = close + ''.len } return out } fn parse_common_prefixes(body string) []CommonPrefix { mut out := []CommonPrefix{} mut start := 0 for { open := body.index_after('', start) or { break } close := body.index_after('', open) or { break } seg := body[open + ''.len..close] out << CommonPrefix{ prefix: extract_xml_tag(seg, 'Prefix') } start = close + ''.len } return out }