1use super::ObjectTransferAccess;
11use crate::{AbsolutePath, ContentRef, InodeId, NamespaceId, RevisionNo};
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
21#[serde(deny_unknown_fields)]
22pub struct BeginDownloadRequest {
23 pub path: AbsolutePath,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
27 #[cfg_attr(feature = "openapi", schema(nullable = false))]
28 pub revision_no: Option<RevisionNo>,
29}
30
31impl BeginDownloadRequest {
32 pub fn for_path(path: AbsolutePath) -> Self {
34 Self {
35 path,
36 revision_no: None,
37 }
38 }
39
40 pub fn for_revision(path: AbsolutePath, revision_no: RevisionNo) -> Self {
42 Self {
43 path,
44 revision_no: Some(revision_no),
45 }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
63pub struct BeginDownloadResponse {
64 pub namespace_id: NamespaceId,
66 pub path: AbsolutePath,
68 pub revision_no: RevisionNo,
70 pub content_ref: ContentRef,
75 pub access: ObjectTransferAccess,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
81#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
82#[serde(deny_unknown_fields)]
83pub struct BeginDownloadByInodeRequest {}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
88pub struct BeginDownloadByInodeResponse {
89 pub namespace_id: NamespaceId,
91 #[serde(with = "crate::public_inode_id")]
93 #[cfg_attr(
94 feature = "openapi",
95 schema(schema_with = crate::public_inode_id::schema)
96 )]
97 pub inode_id: InodeId,
98 pub revision_no: RevisionNo,
100 pub content_ref: ContentRef,
102 pub access: ObjectTransferAccess,
104}
105
106#[cfg(test)]
107mod tests {
108 use super::{
109 BeginDownloadByInodeRequest, BeginDownloadByInodeResponse, BeginDownloadRequest,
110 BeginDownloadResponse,
111 };
112 use crate::v0::ObjectTransferAccess;
113 use crate::{AbsolutePath, ContentId, ContentRef, NamespaceId, RevisionNo};
114 use std::collections::BTreeMap;
115
116 fn absolute_path() -> AbsolutePath {
117 AbsolutePath::parse("/docs/report.txt").expect("absolute path")
118 }
119
120 fn content_ref() -> ContentRef {
121 ContentRef::blob_v1(
122 ContentId::parse("con_0123456789abcdef0123456789abcdef").expect("content id"),
123 b"hello",
124 )
125 }
126
127 fn content_ref_json() -> serde_json::Value {
128 serde_json::json!({
129 "kind": "blob_v1",
130 "content_id": "con_0123456789abcdef0123456789abcdef",
131 "size_bytes": 5,
132 "checksum": {
133 "algorithm": "sha256",
134 "value": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
135 }
136 })
137 }
138
139 #[test]
140 fn a_download_request_names_only_a_path_and_a_revision() {
141 let request: BeginDownloadRequest =
142 serde_json::from_str(r#"{"path":"/docs/report.txt"}"#).expect("decode request");
143 assert_eq!(request.path, absolute_path());
144 assert_eq!(request.revision_no, None);
145
146 let pinned: BeginDownloadRequest =
147 serde_json::from_str(r#"{"path":"/docs/report.txt","revision_no":3}"#)
148 .expect("decode pinned request");
149 assert_eq!(pinned.revision_no, Some(RevisionNo(3)));
150
151 assert!(
152 serde_json::from_str::<BeginDownloadRequest>(
153 r#"{"path":"/docs/report.txt","content_id":"con_0123456789abcdef0123456789abcdef"}"#
154 )
155 .is_err(),
156 "a client must not be able to name the content object"
157 );
158 }
159
160 #[test]
161 fn a_download_grant_exposes_only_presigned_access() {
162 let response = BeginDownloadResponse {
163 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
164 path: absolute_path(),
165 revision_no: RevisionNo(7),
166 content_ref: content_ref(),
167 access: ObjectTransferAccess::PresignedUrl {
168 method: "GET".to_owned(),
169 url: "https://bucket.example/object?X-Amz-Signature=abc".to_owned(),
170 headers: BTreeMap::new(),
171 expires_at_ms: 1,
172 },
173 };
174
175 assert_eq!(
176 serde_json::to_value(&response).expect("serialize response"),
177 serde_json::json!({
178 "namespace_id": "demo",
179 "path": "/docs/report.txt",
180 "revision_no": 7,
181 "content_ref": content_ref_json(),
182 "access": {
183 "kind": "presigned_url",
184 "method": "GET",
185 "url": "https://bucket.example/object?X-Amz-Signature=abc",
186 "expires_at_ms": 1
187 }
188 })
189 );
190 }
191
192 #[test]
193 fn an_inode_download_request_is_strictly_empty_and_its_grant_is_path_free() {
194 let request: BeginDownloadByInodeRequest =
195 serde_json::from_str("{}").expect("decode empty request");
196 assert_eq!(request, BeginDownloadByInodeRequest {});
197 assert!(serde_json::from_str::<BeginDownloadByInodeRequest>(r#"{"path":"/old"}"#).is_err());
198
199 let response = BeginDownloadByInodeResponse {
200 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
201 inode_id: crate::InodeId(42),
202 revision_no: RevisionNo(7),
203 content_ref: content_ref(),
204 access: ObjectTransferAccess::PresignedUrl {
205 method: "GET".to_owned(),
206 url: "https://bucket.example/object?X-Amz-Signature=abc".to_owned(),
207 headers: BTreeMap::new(),
208 expires_at_ms: 1,
209 },
210 };
211 assert_eq!(
212 serde_json::to_value(&response).expect("serialize response"),
213 serde_json::json!({
214 "namespace_id": "demo",
215 "inode_id": "ino_42",
216 "revision_no": 7,
217 "content_ref": content_ref_json(),
218 "access": {
219 "kind": "presigned_url",
220 "method": "GET",
221 "url": "https://bucket.example/object?X-Amz-Signature=abc",
222 "expires_at_ms": 1
223 }
224 })
225 );
226 }
227}