laterite_media/lib.rs
1//! Laterite media: content-addressed blob storage with streaming ingest.
2//!
3//! Three concepts are kept apart, which is the whole design:
4//!
5//! - a **blob** is physical bytes, named by their own BLAKE3 hash;
6//! - a **record** is a file with a stable identity, pointing at a blob;
7//! - a **link** attaches a record to something that owns it.
8//!
9//! Conflating them is what makes a media library hard to replace a file in, or
10//! to reference from two places. Here, replacing a file writes a new blob and a
11//! new hash while the record's id and every reference to it stay put.
12//!
13//! This crate currently carries the bottom layer: the storage driver, the ingest
14//! primitive, and the record table. The library browser, virtual folders,
15//! variants and the finder widget arrive with the CMS work.
16
17pub mod driver;
18pub mod folder;
19pub mod gc;
20pub mod image;
21pub mod ingest;
22pub mod kind;
23pub mod library;
24pub mod link;
25pub mod local;
26pub mod migrations;
27pub mod preset;
28pub mod record;
29#[cfg(feature = "s3")]
30pub mod s3;
31pub mod variant;
32pub mod verify;
33
34pub use driver::{BlobMeta, ByteStream, Disposition, StorageDriver, StorageError};
35pub use folder::{Folder, FolderError};
36pub use ingest::{blob_path, ingest, stream_of, IngestError, Ingested};
37pub use kind::Kind;
38pub use link::{Link, Owner};
39pub use local::LocalDisk;
40pub use preset::{Format, Mode, Preset};
41pub use record::{MediaRecord, NewMedia, NewMediaExtra, RecordError};
42#[cfg(feature = "s3")]
43pub use s3::{S3Config, S3Disk};
44pub use variant::{Variant, VariantError, Variants};
45
46use std::sync::Arc;
47
48use laterite_core::{MigrationSet, Module, ModuleId, Text};
49
50/// A named disk the application declares, for a module that stores files to
51/// find.
52///
53/// Registered once, from the application's own module:
54/// `registry.add(DiskReg::new("private", Arc::new(LocalDisk::new("storage/private"))))`.
55/// A module reads it through `RouteCtx::contributions::<DiskReg>()`, picks the
56/// disk by name and stores that name on each record it writes, so a file's
57/// location is knowable from the record alone.
58#[non_exhaustive]
59pub struct DiskReg {
60 pub name: String,
61 pub driver: Arc<dyn StorageDriver>,
62 /// What the disk is for, shown where an operator chooses one. Defaults to
63 /// the name.
64 pub label: Text,
65 /// A private disk's files are never served by a hash URL alone: a signed
66 /// URL or an app-streamed token gates them.
67 pub private: bool,
68}
69
70impl DiskReg {
71 pub fn new(name: impl Into<String>, driver: Arc<dyn StorageDriver>) -> Self {
72 let name = name.into();
73 Self {
74 label: Text::dynamic(name.clone()),
75 name,
76 driver,
77 private: false,
78 }
79 }
80
81 /// Marks the disk private (see [`DiskReg::private`]).
82 pub fn private(mut self) -> Self {
83 self.private = true;
84 self
85 }
86
87 /// What the disk is for, localized where shown.
88 pub fn label(mut self, label: impl Into<Text>) -> Self {
89 self.label = label.into();
90 self
91 }
92}
93
94/// This crate's module. An application that stores files registers it on
95/// `Bootstrap`; one that does not never compiles this crate at all.
96///
97/// Optional subsystems reach the admin the way a plugin does, through the
98/// registry, so the admin never depends on them and needs no feature flag for
99/// them. A descriptor naming something this module would have contributed, in an
100/// application that did not register it, aborts boot naming the field and the
101/// type rather than degrading silently.
102pub fn module() -> Box<dyn Module> {
103 Box::new(MediaModule)
104}
105
106/// The `laterite.media` module: the media record table.
107pub struct MediaModule;
108
109impl Module for MediaModule {
110 fn id(&self) -> ModuleId {
111 ModuleId::new(migrations::MODULE_ID)
112 }
113
114 fn migrations(&self) -> MigrationSet {
115 migrations::migrations()
116 }
117}