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 sanitize;
32pub mod variant;
33pub mod verify;
34
35pub use driver::{BlobMeta, ByteStream, Disposition, StorageDriver, StorageError};
36pub use folder::{Folder, FolderError};
37pub use ingest::{blob_path, ingest, stream_of, IngestError, Ingested};
38pub use kind::Kind;
39pub use link::{Link, Owner};
40pub use local::LocalDisk;
41pub use preset::{Format, Mode, Preset};
42pub use record::{MediaRecord, NewMedia, NewMediaExtra, RecordError};
43#[cfg(feature = "s3")]
44pub use s3::{S3Config, S3Disk};
45pub use variant::{Variant, VariantError, Variants};
46
47use std::sync::Arc;
48
49use laterite_core::{MigrationSet, Module, ModuleId, Text};
50
51/// A named disk the application declares, for a module that stores files to
52/// find.
53///
54/// Registered once, from the application's own module:
55/// `registry.add(DiskReg::new("private", Arc::new(LocalDisk::new("storage/private"))))`.
56/// A module reads it through `RouteCtx::contributions::<DiskReg>()`, picks the
57/// disk by name and stores that name on each record it writes, so a file's
58/// location is knowable from the record alone.
59#[non_exhaustive]
60pub struct DiskReg {
61 pub name: String,
62 pub driver: Arc<dyn StorageDriver>,
63 /// What the disk is for, shown where an operator chooses one. Defaults to
64 /// the name.
65 pub label: Text,
66 /// A private disk's files are never served by a hash URL alone: a signed
67 /// URL or an app-streamed token gates them.
68 pub private: bool,
69}
70
71impl DiskReg {
72 pub fn new(name: impl Into<String>, driver: Arc<dyn StorageDriver>) -> Self {
73 let name = name.into();
74 Self {
75 label: Text::dynamic(name.clone()),
76 name,
77 driver,
78 private: false,
79 }
80 }
81
82 /// Marks the disk private (see [`DiskReg::private`]).
83 pub fn private(mut self) -> Self {
84 self.private = true;
85 self
86 }
87
88 /// What the disk is for, localized where shown.
89 pub fn label(mut self, label: impl Into<Text>) -> Self {
90 self.label = label.into();
91 self
92 }
93}
94
95/// This crate's module. An application that stores files registers it on
96/// `Bootstrap`; one that does not never compiles this crate at all.
97///
98/// Optional subsystems reach the admin the way a plugin does, through the
99/// registry, so the admin never depends on them and needs no feature flag for
100/// them. A descriptor naming something this module would have contributed, in an
101/// application that did not register it, aborts boot naming the field and the
102/// type rather than degrading silently.
103pub fn module() -> Box<dyn Module> {
104 Box::new(MediaModule)
105}
106
107/// The `laterite.media` module: the media record table.
108pub struct MediaModule;
109
110impl Module for MediaModule {
111 fn id(&self) -> ModuleId {
112 ModuleId::new(migrations::MODULE_ID)
113 }
114
115 fn migrations(&self) -> MigrationSet {
116 migrations::migrations()
117 }
118}