Skip to main content

vantage_aws/models/s3/
object.rs

1use serde::{Deserialize, Serialize};
2use vantage_core::Result;
3use vantage_table::table::Table;
4
5use crate::types::AwsDateTime;
6use crate::{AwsAccount, eq};
7
8/// One S3 object from `ListObjectsV2`. Field names match the wire XML
9/// (`<Contents><Key/><Size/>…</Contents>`) — we surface them as-is so
10/// existing S3 docs translate directly. `Size` arrives as a numeric
11/// string in v0 (no XML-to-typed coercion).
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Object {
14    #[serde(rename = "Key")]
15    pub key: String,
16    #[serde(rename = "Size", default)]
17    pub size: String,
18    #[serde(rename = "LastModified", default)]
19    pub last_modified: String,
20    #[serde(rename = "ETag", default)]
21    pub etag: String,
22    #[serde(rename = "StorageClass", default)]
23    pub storage_class: String,
24}
25
26/// `ListObjectsV2` table. Requires `eq("Bucket", "...")` — without it
27/// the path placeholder errors out at request-build time. Optional
28/// `prefix` / `delimiter` / `max-keys` filters become query params if
29/// supplied.
30///
31/// Auto-paginates: the `@continuation-token=NextContinuationToken`
32/// cursor makes a listing walk every page (S3 returns at most 1000
33/// keys per response, or `max-keys` if set). Cap the walk with
34/// [`AwsAccount::with_max_pages`].
35///
36/// Used as the `objects` relation on [`super::bucket::Bucket`] —
37/// traversing from a single bucket fills `Bucket` automatically.
38pub fn objects_table(aws: AwsAccount) -> Table<AwsAccount, Object> {
39    Table::new(
40        "restxml/Contents@continuation-token=NextContinuationToken:s3/GET /{Bucket}?list-type=2",
41        aws,
42    )
43    .with_id_column("Key")
44    .with_title_column_of::<String>("Size")
45    .with_title_column_of::<AwsDateTime>("LastModified")
46    .with_column_of::<String>("ETag")
47    .with_column_of::<String>("StorageClass")
48}
49
50/// Fetch one object's content (`GetObject`) as text. Goes through the
51/// same REST transport as the listings — signed for a credentialed
52/// [`AwsAccount`], bare for [`AwsAccount::public`] — but skips XML
53/// parsing: the response body *is* the object.
54pub async fn get_object(aws: &AwsAccount, bucket: &str, key: &str) -> Result<String> {
55    let path = crate::restxml::transport::encode_path(&format!("/{bucket}/{key}"));
56    crate::restxml::restxml_call(aws, "s3", "GET", &path, &[]).await
57}
58
59impl Object {
60    /// Build an [`objects_table`] narrowed to the object named in
61    /// `arn`. Accepts ARNs of the shape `arn:aws:s3:::<bucket>/<key>` —
62    /// the bucket fills the path placeholder, the key narrows
63    /// `prefix` so we only fetch the matching object.
64    pub fn from_arn(arn: &str, aws: AwsAccount) -> Option<Table<AwsAccount, Object>> {
65        let rest = arn.strip_prefix("arn:aws:s3:::")?;
66        let (bucket, key) = rest.split_once('/')?;
67        if bucket.is_empty() || key.is_empty() {
68            return None;
69        }
70        let mut t = objects_table(aws);
71        t.add_condition(eq("Bucket", bucket.to_string()));
72        t.add_condition(eq("prefix", key.to_string()));
73        // The post-hoc client filter at `impls::table_source` will
74        // narrow on `Key` once the response comes back — `prefix` only
75        // narrows server-side, so we add the literal Key match too.
76        t.add_condition(eq("Key", key.to_string()));
77        Some(t)
78    }
79}