loonfs_api/v0/
downloads.rs1use super::ObjectTransferAccess;
11use crate::{AbsolutePath, ContentRef, 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 absolute_path: AbsolutePath,
67 pub revision_no: RevisionNo,
69 pub content_ref: ContentRef,
75 pub access: ObjectTransferAccess,
77}
78
79#[cfg(test)]
80mod tests {
81 use super::{BeginDownloadRequest, BeginDownloadResponse};
82 use crate::v0::ObjectTransferAccess;
83 use crate::{AbsolutePath, ContentId, ContentRef, NamespaceId, RevisionNo};
84 use std::collections::BTreeMap;
85
86 fn absolute_path() -> AbsolutePath {
87 AbsolutePath::parse("/docs/report.txt").expect("absolute path")
88 }
89
90 #[test]
93 fn a_download_request_names_only_a_path_and_a_revision() {
94 let request: BeginDownloadRequest =
95 serde_json::from_str(r#"{"path":"/docs/report.txt"}"#).expect("decode request");
96 assert_eq!(request.path, absolute_path());
97 assert_eq!(request.revision_no, None);
98
99 let pinned: BeginDownloadRequest =
100 serde_json::from_str(r#"{"path":"/docs/report.txt","revision_no":3}"#)
101 .expect("decode pinned request");
102 assert_eq!(pinned.revision_no, Some(RevisionNo(3)));
103
104 assert!(
105 serde_json::from_str::<BeginDownloadRequest>(
106 r#"{"path":"/docs/report.txt","content_id":"con_0123456789abcdef0123456789abcdef"}"#
107 )
108 .is_err(),
109 "a client must not be able to name the content object"
110 );
111 }
112
113 #[test]
114 fn a_download_grant_exposes_only_presigned_access() {
115 let response = BeginDownloadResponse {
116 namespace_id: NamespaceId::parse("demo").expect("namespace id"),
117 absolute_path: absolute_path(),
118 revision_no: RevisionNo(7),
119 content_ref: ContentRef::blob_v1(ContentId::generate(), b"hello"),
120 access: ObjectTransferAccess::PresignedUrl {
121 method: "GET".to_owned(),
122 url: "https://bucket.example/object?X-Amz-Signature=abc".to_owned(),
123 headers: BTreeMap::new(),
124 expires_at_ms: 1,
125 },
126 };
127
128 let json = serde_json::to_string(&response).expect("serialize response");
129 assert!(json.contains(r#""kind":"presigned_url""#));
130 assert!(!json.contains("object_key"));
131 }
132}