1use serde::{Deserialize, Serialize};
2use crate::{BlobId, ByteRange, ByteStream, UploadId};
3
4#[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#[derive(Debug, Clone, Serialize, Deserialize)]
22pub enum UploadInfo {
23 Single {
25 method: String, },
27 Multipart {
29 upload_id: UploadId,
30 part_size: u64,
31 parts: u32,
32 },
33}
34
35pub struct OpenedBlob {
37 pub receipt: BlobReceipt,
38 pub content: OpenedContent,
39}
40
41pub enum OpenedContent {
43 Stream {
45 stream: ByteStream,
46 resolved_range: Option<ResolvedRange>,
47 },
48 SignedUrl {
50 url: String,
51 expires_at: i64,
52 },
53}
54
55#[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 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 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 pub fn with_filename<S: Into<String>>(mut self, filename: S) -> Self {
115 self.filename = Some(filename.into());
116 self
117 }
118
119 pub fn with_etag<S: Into<String>>(mut self, etag: S) -> Self {
121 self.etag = Some(etag.into());
122 self
123 }
124
125 pub fn with_checksum<S: Into<String>>(mut self, checksum: S) -> Self {
127 self.checksum = Some(checksum.into());
128 self
129 }
130
131 pub fn with_attributes(mut self, attributes: serde_json::Value) -> Self {
133 self.attributes = attributes;
134 self
135 }
136
137 pub fn with_upload_info(mut self, upload: UploadInfo) -> Self {
139 self.upload = upload;
140 self
141 }
142
143 pub fn with_range_support(mut self) -> Self {
145 self.accepts_ranges = true;
146 self
147 }
148}
149
150impl OpenedBlob {
151 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 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 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 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}