loonfs_api/v0/
downloads.rs1use 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 pub revision_no: Option<RevisionNo>,
28}
29
30impl BeginDownloadRequest {
31 pub fn for_path(path: AbsolutePath) -> Self {
33 Self {
34 path,
35 revision_no: None,
36 }
37 }
38
39 pub fn for_revision(path: AbsolutePath, revision_no: RevisionNo) -> Self {
41 Self {
42 path,
43 revision_no: Some(revision_no),
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
62pub struct BeginDownloadResponse {
63 pub namespace_id: NamespaceId,
65 pub path: AbsolutePath,
67 pub revision_no: RevisionNo,
69 pub content_ref: ContentRef,
74 pub access: ObjectTransferAccess,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
80#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
81#[serde(deny_unknown_fields)]
82pub struct BeginDownloadByInodeRequest {}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
87pub struct BeginDownloadByInodeResponse {
88 pub namespace_id: NamespaceId,
90 #[serde(with = "crate::public_inode_id")]
92 #[cfg_attr(
93 feature = "openapi",
94 schema(schema_with = crate::public_inode_id::schema)
95 )]
96 pub inode_id: InodeId,
97 pub revision_no: RevisionNo,
99 pub content_ref: ContentRef,
101 pub access: ObjectTransferAccess,
103}
104
105#[cfg(test)]
106mod tests {
107 use super::{
108 BeginDownloadByInodeRequest, BeginDownloadByInodeResponse, BeginDownloadRequest,
109 BeginDownloadResponse,
110 };
111 use crate::v0::ObjectTransferAccess;
112 use crate::{AbsolutePath, ContentId, ContentRef, NamespaceId, RevisionNo};
113 use std::collections::BTreeMap;
114
115 fn absolute_path() -> AbsolutePath {
116 AbsolutePath::parse("/docs/report.txt").expect("absolute path")
117 }
118
119 #[test]
122 fn a_download_request_names_only_a_path_and_a_revision() {
123 let request: BeginDownloadRequest =
124 serde_json::from_str(r#"{"path":"/docs/report.txt"}"#).expect("decode request");
125 assert_eq!(request.path, absolute_path());
126 assert_eq!(request.revision_no, None);
127
128 let pinned: BeginDownloadRequest =
129 serde_json::from_str(r#"{"path":"/docs/report.txt","revision_no":3}"#)
130 .expect("decode pinned request");
131 assert_eq!(pinned.revision_no, Some(RevisionNo(3)));
132
133 assert!(
134 serde_json::from_str::<BeginDownloadRequest>(
135 r#"{"path":"/docs/report.txt","content_id":"con_0123456789abcdef0123456789abcdef"}"#
136 )
137 .is_err(),
138 "a client must not be able to name the content object"
139 );
140 }
141
142 #[test]
143 fn a_download_grant_exposes_only_presigned_access() {
144 let response = BeginDownloadResponse {
145 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
146 path: absolute_path(),
147 revision_no: RevisionNo(7),
148 content_ref: ContentRef::blob_v1(ContentId::generate(), b"hello"),
149 access: ObjectTransferAccess::PresignedUrl {
150 method: "GET".to_owned(),
151 url: "https://bucket.example/object?X-Amz-Signature=abc".to_owned(),
152 headers: BTreeMap::new(),
153 expires_at_ms: 1,
154 },
155 };
156
157 let json = serde_json::to_string(&response).expect("serialize response");
158 assert!(json.contains(r#""kind":"presigned_url""#));
159 assert!(!json.contains("object_key"));
160 }
161
162 #[test]
163 fn an_inode_download_request_is_strictly_empty_and_its_grant_is_path_free() {
164 let request: BeginDownloadByInodeRequest =
165 serde_json::from_str("{}").expect("decode empty request");
166 assert_eq!(request, BeginDownloadByInodeRequest {});
167 assert!(serde_json::from_str::<BeginDownloadByInodeRequest>(r#"{"path":"/old"}"#).is_err());
168
169 let response = BeginDownloadByInodeResponse {
170 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
171 inode_id: crate::InodeId(42),
172 revision_no: RevisionNo(7),
173 content_ref: ContentRef::blob_v1(ContentId::generate(), b"hello"),
174 access: ObjectTransferAccess::PresignedUrl {
175 method: "GET".to_owned(),
176 url: "https://bucket.example/object?X-Amz-Signature=abc".to_owned(),
177 headers: BTreeMap::new(),
178 expires_at_ms: 1,
179 },
180 };
181 let json = serde_json::to_value(response).expect("serialize response");
182 assert_eq!(json["inode_id"], "ino_42");
183 assert!(json.get("path").is_none());
184 }
185}