1use super::{
2 Arc, AtomicBool, AtomicU64, CONTENT_LEASE_MAGIC, CONTENT_PHYSICAL_HOLD_MAGIC,
3 ContentDescriptor, ContentId, ContentLeaseId, ContentLeaseOwnerId, ContentPhysicalHoldId,
4 ContentPhysicalHoldKind, ContentPhysicalHoldOwnerId, Db, Digest, Duration, Error, Ordering,
5 Result, Sha256, StorageDomainId, array_at, decode_chunk, decode_content_id, fmt,
6};
7
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
10pub struct ContentLifecycleMaintenanceReport {
11 pub(crate) scanned: u64,
12 pub(crate) expired_tokens_removed: u64,
13 pub(crate) expired_leases_removed: u64,
14 pub(crate) inactive_holds_removed: u64,
15}
16
17impl ContentLifecycleMaintenanceReport {
18 #[must_use]
20 pub const fn scanned(self) -> u64 {
21 self.scanned
22 }
23
24 #[must_use]
26 pub const fn expired_tokens_removed(self) -> u64 {
27 self.expired_tokens_removed
28 }
29
30 #[must_use]
32 pub const fn expired_leases_removed(self) -> u64 {
33 self.expired_leases_removed
34 }
35
36 #[must_use]
38 pub const fn inactive_holds_removed(self) -> u64 {
39 self.inactive_holds_removed
40 }
41}
42
43#[derive(Debug)]
44pub(crate) struct ContentLease {
45 id: ContentLeaseId,
46 owner_id: ContentLeaseOwnerId,
47 expires_at_unix_ms: AtomicU64,
48}
49
50impl ContentLease {
51 pub(crate) const fn new(
52 id: ContentLeaseId,
53 owner_id: ContentLeaseOwnerId,
54 expires_at_unix_ms: u64,
55 ) -> Self {
56 Self {
57 id,
58 owner_id,
59 expires_at_unix_ms: AtomicU64::new(expires_at_unix_ms),
60 }
61 }
62
63 pub(crate) const fn id(&self) -> ContentLeaseId {
64 self.id
65 }
66
67 pub(crate) const fn owner_id(&self) -> ContentLeaseOwnerId {
68 self.owner_id
69 }
70
71 pub(crate) fn expires_at_unix_ms(&self) -> u64 {
72 self.expires_at_unix_ms.load(Ordering::Acquire)
73 }
74
75 pub(crate) fn publish_expiry(&self, expires_at_unix_ms: u64) {
76 self.expires_at_unix_ms
77 .store(expires_at_unix_ms, Ordering::Release);
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub(crate) struct ContentLeaseRecord {
83 pub(crate) lease_id: ContentLeaseId,
84 pub(crate) owner_id: ContentLeaseOwnerId,
85 pub(crate) storage_domain_id: StorageDomainId,
86 pub(crate) content_id: ContentId,
87 pub(crate) expires_at_unix_ms: u64,
88}
89
90impl ContentLeaseRecord {
91 pub(crate) fn encode(self) -> Vec<u8> {
92 let mut bytes = Vec::with_capacity(8 + 16 + 16 + 16 + 33 + 8);
93 bytes.extend_from_slice(CONTENT_LEASE_MAGIC);
94 bytes.extend_from_slice(&self.lease_id.to_bytes());
95 bytes.extend_from_slice(&self.owner_id.to_bytes());
96 bytes.extend_from_slice(&self.storage_domain_id.to_bytes());
97 bytes.extend_from_slice(&self.content_id.to_bytes());
98 bytes.extend_from_slice(&self.expires_at_unix_ms.to_le_bytes());
99 bytes
100 }
101
102 pub(crate) fn decode(
103 bytes: &[u8],
104 storage_domain_id: StorageDomainId,
105 content_id: ContentId,
106 lease_id: ContentLeaseId,
107 ) -> Result<Self> {
108 const RECORD_LEN: usize = 8 + 16 + 16 + 16 + 33 + 8;
109 if bytes.len() != RECORD_LEN || bytes.get(..8) != Some(CONTENT_LEASE_MAGIC) {
110 return Err(Error::InvalidFormat {
111 message: "invalid content lease record header or length".to_owned(),
112 });
113 }
114 let stored_lease_id =
115 ContentLeaseId::from_bytes(array_at::<16>(bytes, 8, "content lease identity")?)?;
116 let owner_id =
117 ContentLeaseOwnerId::from_bytes(array_at::<16>(bytes, 24, "content lease owner")?);
118 let stored_domain =
119 StorageDomainId::from_bytes(array_at::<16>(bytes, 40, "content lease storage domain")?);
120 let stored_content = decode_content_id(bytes, 56, "content lease content identity")?;
121 let expires_at_unix_ms =
122 u64::from_le_bytes(array_at::<8>(bytes, 89, "content lease expiry")?);
123 if stored_lease_id != lease_id
124 || stored_domain != storage_domain_id
125 || stored_content != content_id
126 {
127 return Err(Error::Corruption {
128 message: "content lease record differs from its protected key".to_owned(),
129 });
130 }
131 Ok(Self {
132 lease_id,
133 owner_id,
134 storage_domain_id,
135 content_id,
136 expires_at_unix_ms,
137 })
138 }
139}
140
141pub(crate) fn content_lease_prefix(
142 storage_domain_id: StorageDomainId,
143 content_id: ContentId,
144) -> Vec<u8> {
145 let mut key = Vec::with_capacity(16 + 33);
146 key.extend_from_slice(&storage_domain_id.to_bytes());
147 key.extend_from_slice(&content_id.to_bytes());
148 key
149}
150
151pub(crate) fn content_lease_key(
152 storage_domain_id: StorageDomainId,
153 content_id: ContentId,
154 lease_id: ContentLeaseId,
155) -> Vec<u8> {
156 let mut key = content_lease_prefix(storage_domain_id, content_id);
157 key.extend_from_slice(&lease_id.to_bytes());
158 key
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub(crate) enum ContentPhysicalHoldRecordState {
163 Active,
164 Released,
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub(crate) struct ContentPhysicalHoldRecord {
169 pub(crate) hold_id: ContentPhysicalHoldId,
170 pub(crate) owner_id: ContentPhysicalHoldOwnerId,
171 pub(crate) storage_domain_id: StorageDomainId,
172 pub(crate) content_id: ContentId,
173 pub(crate) kind: ContentPhysicalHoldKind,
174 pub(crate) expires_at_unix_ms: u64,
175 pub(crate) state: ContentPhysicalHoldRecordState,
176}
177
178impl ContentPhysicalHoldRecord {
179 pub(crate) const fn is_active_at(self, now_unix_ms: u64) -> bool {
180 matches!(self.state, ContentPhysicalHoldRecordState::Active)
181 && (self.expires_at_unix_ms == 0 || now_unix_ms < self.expires_at_unix_ms)
182 }
183
184 pub(crate) const fn is_released(self) -> bool {
185 matches!(self.state, ContentPhysicalHoldRecordState::Released)
186 }
187
188 pub(crate) const fn released(mut self) -> Self {
189 self.state = ContentPhysicalHoldRecordState::Released;
190 self
191 }
192
193 pub(crate) fn encode(self) -> Vec<u8> {
194 let mut bytes = Vec::with_capacity(8 + 16 + 16 + 16 + 33 + 1 + 1 + 8);
195 bytes.extend_from_slice(CONTENT_PHYSICAL_HOLD_MAGIC);
196 bytes.extend_from_slice(&self.hold_id.to_bytes());
197 bytes.extend_from_slice(&self.owner_id.to_bytes());
198 bytes.extend_from_slice(&self.storage_domain_id.to_bytes());
199 bytes.extend_from_slice(&self.content_id.to_bytes());
200 bytes.push(self.kind.tag());
201 bytes.push(match self.state {
202 ContentPhysicalHoldRecordState::Active => 0,
203 ContentPhysicalHoldRecordState::Released => 1,
204 });
205 bytes.extend_from_slice(&self.expires_at_unix_ms.to_le_bytes());
206 bytes
207 }
208
209 pub(crate) fn decode(
210 bytes: &[u8],
211 storage_domain_id: StorageDomainId,
212 content_id: ContentId,
213 hold_id: ContentPhysicalHoldId,
214 ) -> Result<Self> {
215 const RECORD_LEN: usize = 8 + 16 + 16 + 16 + 33 + 1 + 1 + 8;
216 if bytes.len() != RECORD_LEN || bytes.get(..8) != Some(CONTENT_PHYSICAL_HOLD_MAGIC) {
217 return Err(Error::InvalidFormat {
218 message: "invalid content physical-hold record header or length".to_owned(),
219 });
220 }
221 let stored_hold_id = ContentPhysicalHoldId::from_bytes(array_at::<16>(
222 bytes,
223 8,
224 "content physical-hold identity",
225 )?)?;
226 let owner_id = ContentPhysicalHoldOwnerId::from_bytes(array_at::<16>(
227 bytes,
228 24,
229 "content physical-hold owner",
230 )?);
231 let stored_domain = StorageDomainId::from_bytes(array_at::<16>(
232 bytes,
233 40,
234 "content physical-hold storage domain",
235 )?);
236 let stored_content =
237 decode_content_id(bytes, 56, "content physical-hold content identity")?;
238 let kind = ContentPhysicalHoldKind::from_tag(bytes[89])?;
239 let state = match bytes[90] {
240 0 => ContentPhysicalHoldRecordState::Active,
241 1 => ContentPhysicalHoldRecordState::Released,
242 state => {
243 return Err(Error::UnsupportedFormat {
244 message: format!("unsupported content physical-hold state {state}"),
245 });
246 }
247 };
248 let expires_at_unix_ms =
249 u64::from_le_bytes(array_at::<8>(bytes, 91, "content physical-hold expiry")?);
250 if stored_hold_id != hold_id
251 || stored_domain != storage_domain_id
252 || stored_content != content_id
253 {
254 return Err(Error::Corruption {
255 message: "content physical-hold record differs from its protected key".to_owned(),
256 });
257 }
258 Ok(Self {
259 hold_id,
260 owner_id,
261 storage_domain_id,
262 content_id,
263 kind,
264 expires_at_unix_ms,
265 state,
266 })
267 }
268}
269
270pub(crate) fn content_physical_hold_prefix(
271 storage_domain_id: StorageDomainId,
272 content_id: ContentId,
273) -> Vec<u8> {
274 content_lease_prefix(storage_domain_id, content_id)
275}
276
277pub(crate) fn content_physical_hold_key(
278 storage_domain_id: StorageDomainId,
279 content_id: ContentId,
280 hold_id: ContentPhysicalHoldId,
281) -> Vec<u8> {
282 let mut key = content_physical_hold_prefix(storage_domain_id, content_id);
283 key.extend_from_slice(&hold_id.to_bytes());
284 key
285}
286
287pub(crate) fn current_epoch_millis() -> Result<u64> {
288 crate::platform::now_unix_millis()
289}
290
291pub(crate) fn duration_millis(duration: Duration, name: &str) -> Result<u64> {
292 let millis = u64::try_from(duration.as_millis())
293 .map_err(|_| Error::invalid_options(format!("{name} milliseconds exceed u64::MAX")))?;
294 if millis == 0 {
295 return Err(Error::invalid_options(format!(
296 "{name} must be at least one millisecond"
297 )));
298 }
299 Ok(millis)
300}
301
302#[derive(Debug)]
303struct ContentPhysicalHoldState {
304 expires_at_unix_ms: AtomicU64,
305 released: AtomicBool,
306}
307
308#[derive(Clone)]
315pub struct ContentPhysicalHold {
316 db: Db,
317 storage_domain_id: StorageDomainId,
318 content_id: ContentId,
319 hold_id: ContentPhysicalHoldId,
320 owner_id: ContentPhysicalHoldOwnerId,
321 kind: ContentPhysicalHoldKind,
322 state: Arc<ContentPhysicalHoldState>,
323}
324
325impl ContentPhysicalHold {
326 pub(crate) fn from_record(db: Db, record: ContentPhysicalHoldRecord) -> Self {
327 Self {
328 db,
329 storage_domain_id: record.storage_domain_id,
330 content_id: record.content_id,
331 hold_id: record.hold_id,
332 owner_id: record.owner_id,
333 kind: record.kind,
334 state: Arc::new(ContentPhysicalHoldState {
335 expires_at_unix_ms: AtomicU64::new(record.expires_at_unix_ms),
336 released: AtomicBool::new(false),
337 }),
338 }
339 }
340
341 #[must_use]
343 pub const fn storage_domain_id(&self) -> StorageDomainId {
344 self.storage_domain_id
345 }
346
347 #[must_use]
349 pub const fn content_id(&self) -> ContentId {
350 self.content_id
351 }
352
353 #[must_use]
355 pub const fn id(&self) -> ContentPhysicalHoldId {
356 self.hold_id
357 }
358
359 #[must_use]
361 pub const fn kind(&self) -> ContentPhysicalHoldKind {
362 self.kind
363 }
364
365 #[must_use]
369 pub fn expires_at_unix_ms(&self) -> Option<u64> {
370 match self.state.expires_at_unix_ms.load(Ordering::Acquire) {
371 0 => None,
372 expires_at_unix_ms => Some(expires_at_unix_ms),
373 }
374 }
375
376 #[must_use]
380 pub fn is_released(&self) -> bool {
381 self.state.released.load(Ordering::Acquire)
382 }
383
384 pub async fn renew(&self, ttl: Duration) -> Result<u64> {
398 self.db.renew_content_physical_hold(self, ttl).await
399 }
400
401 pub async fn release(&self) -> Result<()> {
413 self.db.release_content_physical_hold(self).await
414 }
415
416 pub(crate) const fn owner_id(&self) -> ContentPhysicalHoldOwnerId {
417 self.owner_id
418 }
419
420 pub(crate) fn publish_expiry(&self, expires_at_unix_ms: u64) {
421 self.state
422 .expires_at_unix_ms
423 .store(expires_at_unix_ms, Ordering::Release);
424 }
425
426 pub(crate) fn publish_released(&self) {
427 self.state.released.store(true, Ordering::Release);
428 }
429}
430
431impl fmt::Debug for ContentPhysicalHold {
432 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
433 formatter
434 .debug_struct("ContentPhysicalHold")
435 .field("storage_domain_id", &self.storage_domain_id)
436 .field("content_id", &self.content_id)
437 .field("hold_id", &self.hold_id)
438 .field("kind", &self.kind)
439 .field("expires_at_unix_ms", &self.expires_at_unix_ms())
440 .field("released", &self.is_released())
441 .finish_non_exhaustive()
442 }
443}
444
445#[derive(Debug, Clone)]
453pub struct ContentHandle {
454 db: Db,
455 descriptor: ContentDescriptor,
456 lease: Option<Arc<ContentLease>>,
457}
458
459impl ContentHandle {
460 pub(crate) fn new(db: Db, descriptor: ContentDescriptor) -> Self {
461 Self {
462 db,
463 descriptor,
464 lease: None,
465 }
466 }
467
468 pub(crate) fn with_lease(mut self, lease: ContentLease) -> Self {
469 self.lease = Some(Arc::new(lease));
470 self
471 }
472
473 pub(crate) fn lease(&self) -> Option<&Arc<ContentLease>> {
474 self.lease.as_ref()
475 }
476
477 #[must_use]
479 pub const fn content_id(&self) -> ContentId {
480 self.descriptor.content_id
481 }
482
483 #[must_use]
485 pub const fn storage_domain_id(&self) -> StorageDomainId {
486 self.descriptor.storage_domain_id
487 }
488
489 #[must_use]
491 pub const fn len(&self) -> u64 {
492 self.descriptor.length
493 }
494
495 #[must_use]
497 pub const fn is_empty(&self) -> bool {
498 self.descriptor.length == 0
499 }
500
501 #[must_use]
507 pub fn lease_id(&self) -> Option<ContentLeaseId> {
508 self.lease.as_deref().map(ContentLease::id)
509 }
510
511 #[must_use]
516 pub fn lease_expires_at_unix_ms(&self) -> Option<u64> {
517 self.lease.as_deref().map(ContentLease::expires_at_unix_ms)
518 }
519
520 pub async fn renew_lease(&self, ttl: Duration) -> Result<u64> {
534 self.db.renew_content_lease(self, ttl).await
535 }
536
537 fn ensure_lease_active(&self) -> Result<()> {
538 let Some(lease) = self.lease.as_deref() else {
539 return Ok(());
540 };
541 let now = current_epoch_millis()?;
542 let expires_at_unix_ms = lease.expires_at_unix_ms();
543 if now >= expires_at_unix_ms {
544 return Err(Error::ContentLeaseExpired {
545 expired_at_unix_ms: expires_at_unix_ms,
546 });
547 }
548 Ok(())
549 }
550
551 pub async fn read_range(&self, start: u64, length: u64) -> Result<Arc<[u8]>> {
564 let _activity = self.db.begin_activity()?;
565 self.ensure_lease_active()?;
566 if start > self.descriptor.length() {
567 return Err(Error::ContentRangeOutOfBounds {
568 start,
569 length: self.descriptor.length(),
570 });
571 }
572 if length == 0 || start == self.descriptor.length() {
573 return Ok(Arc::from([]));
574 }
575 let end = start.saturating_add(length).min(self.descriptor.length());
576 let result_len = usize::try_from(end - start)
577 .map_err(|_| Error::invalid_options("requested content range exceeds usize"))?;
578 let mut result = Vec::with_capacity(result_len);
579 let chunk_bytes = u64::from(self.descriptor.chunk_bytes());
580 let mut position = start;
581 while position < end {
582 self.ensure_lease_active()?;
583 let chunk_index = position / chunk_bytes;
584 let frame = self
585 .db
586 .read_content_chunk(self.descriptor.upload_id(), chunk_index)
587 .await?
588 .ok_or_else(|| Error::Corruption {
589 message: format!(
590 "content {} is missing chunk {chunk_index}",
591 self.descriptor.content_id()
592 ),
593 })?;
594 let payload = decode_chunk(&frame, self.descriptor.upload_id(), chunk_index)?;
595 let offset = usize::try_from(position % chunk_bytes)
596 .map_err(|_| Error::invalid_options("content chunk offset exceeds usize"))?;
597 let available = payload
598 .len()
599 .checked_sub(offset)
600 .ok_or_else(|| Error::Corruption {
601 message: format!("content chunk {chunk_index} is shorter than its descriptor"),
602 })?;
603 let remaining = usize::try_from(end - position)
604 .map_err(|_| Error::invalid_options("content range remainder exceeds usize"))?;
605 let take = available.min(remaining);
606 if take == 0 {
607 return Err(Error::Corruption {
608 message: format!("content chunk {chunk_index} made no read progress"),
609 });
610 }
611 result.extend_from_slice(&payload[offset..offset + take]);
612 position =
613 position
614 .checked_add(u64::try_from(take).map_err(|_| {
615 Error::invalid_options("content range progress exceeds u64")
616 })?)
617 .ok_or_else(|| Error::invalid_options("content range position overflow"))?;
618 }
619 Ok(Arc::from(result))
620 }
621
622 #[must_use]
624 pub fn stream(&self) -> ContentStream {
625 ContentStream {
626 handle: self.clone(),
627 position: 0,
628 }
629 }
630
631 pub async fn verify(&self) -> Result<()> {
640 let _activity = self.db.begin_activity()?;
641 let mut stream = self.stream();
642 let mut hasher = Sha256::new();
643 while let Some(bytes) = stream.next().await? {
644 hasher.update(&bytes);
645 }
646 let actual = ContentId::from_sha256(hasher.finalize().into());
647 if actual != self.descriptor.content_id() {
648 return Err(Error::ContentDigestMismatch {
649 expected: self.descriptor.content_id().to_string(),
650 actual: actual.to_string(),
651 });
652 }
653 Ok(())
654 }
655}
656
657#[derive(Debug, Clone)]
659pub struct ContentStream {
660 handle: ContentHandle,
661 position: u64,
662}
663
664impl ContentStream {
665 pub async fn next(&mut self) -> Result<Option<Arc<[u8]>>> {
675 let _activity = self.handle.db.begin_activity()?;
676 if self.position == self.handle.len() {
677 return Ok(None);
678 }
679 let remaining = self.handle.len() - self.position;
680 let length = remaining.min(u64::from(self.handle.descriptor.chunk_bytes()));
681 let bytes = self.handle.read_range(self.position, length).await?;
682 self.position =
683 self.position
684 .checked_add(u64::try_from(bytes.len()).map_err(|_| {
685 Error::invalid_options("content stream chunk length exceeds u64")
686 })?)
687 .ok_or_else(|| Error::invalid_options("content stream position overflow"))?;
688 Ok(Some(bytes))
689 }
690
691 #[must_use]
693 pub const fn position(&self) -> u64 {
694 self.position
695 }
696}