Skip to main content

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}