Skip to main content

dog_blob/
receipt.rs

1use serde::{Deserialize, Serialize};
2use crate::{BlobId, ByteRange, ByteStream, UploadId};
3
4/// Receipt returned after successfully storing a blob
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct BlobReceipt {
7    pub id: BlobId,
8    pub key: String,
9    pub size_bytes: u64,
10    pub content_type: Option<String>,
11    pub filename: Option<String>,
12    pub etag: Option<String>,
13    pub checksum: Option<String>,
14    pub created_at: i64,
15    pub attributes: serde_json::Value,
16    pub upload: UploadInfo,
17    pub accepts_ranges: bool,
18}
19
20/// Information about how the blob was uploaded
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub enum UploadInfo {
23    /// Single-shot upload
24    Single {
25        method: String, // "put", "signed_url", etc.
26    },
27    /// Multipart upload
28    Multipart {
29        upload_id: UploadId,
30        part_size: u64,
31        parts: u32,
32    },
33}
34
35/// Result of opening a blob for reading
36pub struct OpenedBlob {
37    pub receipt: BlobReceipt,
38    pub content: OpenedContent,
39}
40
41/// Content delivery method for opened blob
42pub enum OpenedContent {
43    /// Stream the content directly
44    Stream {
45        stream: ByteStream,
46        resolved_range: Option<ResolvedRange>,
47    },
48    /// Redirect to a signed URL
49    SignedUrl {
50        url: String,
51        expires_at: i64,
52    },
53}
54
55/// Range information for partial content
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ResolvedRange {
58    pub start: u64,
59    pub end: u64,
60    pub total_size: u64,
61}
62
63impl ResolvedRange {
64    pub fn from_request(range: &ByteRange, total_size: u64) -> Self {
65        let end = range.end.unwrap_or(total_size - 1).min(total_size - 1);
66        Self {
67            start: range.start,
68            end,
69            total_size,
70        }
71    }
72
73    pub fn content_length(&self) -> u64 {
74        self.end - self.start + 1
75    }
76
77    pub fn is_full_content(&self) -> bool {
78        self.start == 0 && self.end == self.total_size - 1
79    }
80}
81
82impl BlobReceipt {
83    /// Create a new blob receipt
84    pub fn new(id: BlobId, key: String, size_bytes: u64) -> Self {
85        let now = std::time::SystemTime::now()
86            .duration_since(std::time::UNIX_EPOCH)
87            .unwrap_or_default()
88            .as_secs() as i64;
89
90        Self {
91            id,
92            key,
93            size_bytes,
94            content_type: None,
95            filename: None,
96            etag: None,
97            checksum: None,
98            created_at: now,
99            attributes: serde_json::Value::Null,
100            upload: UploadInfo::Single {
101                method: "put".to_string(),
102            },
103            accepts_ranges: false,
104        }
105    }
106
107    /// Set content type
108    pub fn with_content_type<S: Into<String>>(mut self, content_type: S) -> Self {
109        self.content_type = Some(content_type.into());
110        self
111    }
112
113    /// Set filename
114    pub fn with_filename<S: Into<String>>(mut self, filename: S) -> Self {
115        self.filename = Some(filename.into());
116        self
117    }
118
119    /// Set etag
120    pub fn with_etag<S: Into<String>>(mut self, etag: S) -> Self {
121        self.etag = Some(etag.into());
122        self
123    }
124
125    /// Set checksum
126    pub fn with_checksum<S: Into<String>>(mut self, checksum: S) -> Self {
127        self.checksum = Some(checksum.into());
128        self
129    }
130
131    /// Set attributes
132    pub fn with_attributes(mut self, attributes: serde_json::Value) -> Self {
133        self.attributes = attributes;
134        self
135    }
136
137    /// Set upload info
138    pub fn with_upload_info(mut self, upload: UploadInfo) -> Self {
139        self.upload = upload;
140        self
141    }
142
143    /// Enable range support
144    pub fn with_range_support(mut self) -> Self {
145        self.accepts_ranges = true;
146        self
147    }
148}
149
150impl OpenedBlob {
151    /// Create with streaming content
152    pub fn stream(receipt: BlobReceipt, stream: ByteStream, resolved_range: Option<ResolvedRange>) -> Self {
153        Self {
154            receipt,
155            content: OpenedContent::Stream {
156                stream,
157                resolved_range,
158            },
159        }
160    }
161
162    /// Create with signed URL
163    pub fn signed_url(receipt: BlobReceipt, url: String, expires_at: i64) -> Self {
164        Self {
165            receipt,
166            content: OpenedContent::SignedUrl { url, expires_at },
167        }
168    }
169
170    /// Check if this is a partial content response
171    pub fn is_partial(&self) -> bool {
172        match &self.content {
173            OpenedContent::Stream { resolved_range, .. } => {
174                resolved_range.as_ref().map_or(false, |r| !r.is_full_content())
175            }
176            OpenedContent::SignedUrl { .. } => false,
177        }
178    }
179
180    /// Get content length
181    pub fn content_length(&self) -> u64 {
182        match &self.content {
183            OpenedContent::Stream { resolved_range, .. } => {
184                resolved_range
185                    .as_ref()
186                    .map_or(self.receipt.size_bytes, |r| r.content_length())
187            }
188            OpenedContent::SignedUrl { .. } => self.receipt.size_bytes,
189        }
190    }
191}