Skip to main content

artifact_api/
stream_upload.rs

1//! Finite streaming ingestion. Bytes use the HTTP data plane; these are control messages.
2
3use serde::{Deserialize, Serialize};
4
5use crate::upload::PrepareArtifactUploadValidationError;
6use crate::{ArtifactKind, ArtifactPolicy, HostTlsIdentity, PutArtifactResponse};
7
8object_message! {
9/// Start an upload whose final length and digest need not be known yet.
10
11pub struct StartArtifactUploadRequest {
12    #[serde(default)]
13    pub identity: crate::ArtifactIdentity,
14    #[serde(default)]
15    pub retention: crate::ArtifactRetention,
16    #[serde(
17        default,
18        skip_serializing_if = "Option::is_none",
19        deserialize_with = "crate::deserialize_optional_non_null"
20    )]
21    pub creation_id: Option<String>,
22    pub kind: ArtifactKind,
23    pub mime_type: String,
24    pub max_size_bytes: u64,
25}
26}
27
28impl StartArtifactUploadRequest {
29    pub const fn policy(&self) -> ArtifactPolicy {
30        ArtifactPolicy::new(self.identity, self.retention)
31    }
32
33    pub fn validate(&self) -> Result<(), PrepareArtifactUploadValidationError> {
34        self.policy()
35            .validate_creation_id(self.creation_id.as_deref())
36            .map_err(|_| invalid("invalid artifact creation ID"))?;
37        crate::upload::validate_content_type(self.kind, &self.mime_type)?;
38        crate::validate_size_bytes(self.max_size_bytes).map_err(invalid)
39    }
40
41    /// Validate final metadata against the immutable upload declaration.
42    pub fn validate_completion(
43        &self,
44        completion: &CompleteArtifactUploadRequest,
45    ) -> Result<(), PrepareArtifactUploadValidationError> {
46        self.validate()?;
47        completion.validate()?;
48        if completion.size_bytes > self.max_size_bytes {
49            return Err(invalid("artifact upload exceeds its declared limit"));
50        }
51        crate::validate_duration_millis(self.kind, completion.duration_millis).map_err(invalid)
52    }
53}
54
55object_message! {
56pub struct StartArtifactUploadResponse {
57    #[serde(
58        default,
59        skip_serializing_if = "Option::is_none",
60        deserialize_with = "crate::deserialize_optional_non_null"
61    )]
62    pub host_identity: Option<HostTlsIdentity>,
63    pub upload_id: String,
64    pub url: String,
65    pub headers: std::collections::BTreeMap<String, String>,
66    pub max_size_bytes: u64,
67    pub expires_at_unix_ms: u64,
68}
69}
70
71impl StartArtifactUploadResponse {
72    pub fn validate(&self) -> Result<(), PrepareArtifactUploadValidationError> {
73        validate_upload_id(&self.upload_id)?;
74        crate::validate_size_bytes(self.max_size_bytes).map_err(invalid)?;
75        if self.expires_at_unix_ms == 0 || self.expires_at_unix_ms > crate::MAX_SAFE_INTEGER {
76            return Err(invalid("artifact upload expiration is invalid"));
77        }
78        if !(self.url.starts_with("https://") || self.url.starts_with("http://")) {
79            return Err(invalid("artifact upload URL must use HTTP or HTTPS"));
80        }
81        if self.headers.keys().any(|name| {
82            name.eq_ignore_ascii_case("content-length")
83                || name.eq_ignore_ascii_case("x-meow-content-sha256")
84        }) {
85            return Err(invalid(
86                "stream upload cannot require a final size or digest header",
87            ));
88        }
89        if let Some(host) = &self.host_identity {
90            let valid_key = |key: &str| {
91                key.len() == 43
92                    && key
93                        .bytes()
94                        .all(|c| c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_'))
95            };
96            if host.host_id.is_empty()
97                || !valid_key(&host.public_key.x)
98                || !valid_key(&host.public_key.y)
99            {
100                return Err(invalid("artifact upload Host identity is invalid"));
101            }
102        }
103        Ok(())
104    }
105}
106
107object_message! {
108/// Successful HTTP receipt means bytes were staged, not that an Artifact exists.
109
110pub struct ArtifactUploadReceipt {
111    pub upload_id: String,
112    pub size_bytes: u64,
113    pub sha256: String,
114}
115}
116
117impl ArtifactUploadReceipt {
118    pub fn validate(&self) -> Result<(), PrepareArtifactUploadValidationError> {
119        validate_upload_id(&self.upload_id)?;
120        crate::validate_size_bytes(self.size_bytes).map_err(invalid)?;
121        crate::upload::validate_sha256(&self.sha256)
122    }
123}
124
125object_message! {
126/// The sender supplies the digest and length of all bytes it intended to send.
127
128pub struct CompleteArtifactUploadRequest {
129    pub upload_id: String,
130    pub size_bytes: u64,
131    pub sha256: String,
132    #[serde(
133        default,
134        skip_serializing_if = "Option::is_none",
135        deserialize_with = "crate::deserialize_optional_non_null"
136    )]
137    pub duration_millis: Option<u64>,
138}
139}
140
141impl CompleteArtifactUploadRequest {
142    pub fn validate(&self) -> Result<(), PrepareArtifactUploadValidationError> {
143        validate_upload_id(&self.upload_id)?;
144        crate::validate_size_bytes(self.size_bytes).map_err(invalid)?;
145        crate::upload::validate_sha256(&self.sha256)?;
146        // The start declaration supplies the kind for the cross-request check.
147        if self
148            .duration_millis
149            .is_some_and(|duration| duration == 0 || duration > crate::MAX_SAFE_INTEGER)
150        {
151            return Err(invalid("artifact duration is invalid"));
152        }
153        Ok(())
154    }
155}
156
157pub type CompleteArtifactUploadResponse = PutArtifactResponse;
158
159object_message! {
160pub struct CancelArtifactUploadRequest {
161    pub upload_id: String,
162}
163}
164
165impl CancelArtifactUploadRequest {
166    pub fn validate(&self) -> Result<(), PrepareArtifactUploadValidationError> {
167        validate_upload_id(&self.upload_id)
168    }
169}
170
171object_message! {
172pub struct CancelArtifactUploadResponse {}
173}
174
175fn validate_upload_id(value: &str) -> Result<(), PrepareArtifactUploadValidationError> {
176    if value.is_empty()
177        || value.len() > 128
178        || !value
179            .bytes()
180            .all(|c| c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_'))
181    {
182        return Err(invalid("artifact upload ID is invalid"));
183    }
184    Ok(())
185}
186
187fn invalid(message: &'static str) -> PrepareArtifactUploadValidationError {
188    crate::upload::invalid(message)
189}