artifact_api/
stream_upload.rs1use serde::{Deserialize, Serialize};
4
5use crate::upload::PrepareArtifactUploadValidationError;
6use crate::{ArtifactKind, ArtifactPolicy, HostTlsIdentity, PutArtifactResponse};
7
8object_message! {
9pub 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 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! {
108pub 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! {
126pub 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 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}