Skip to main content

libfw_core/
storage.rs

1//! Storage abstraction for `libfw-server`.
2//!
3//! Implement [`StorageBackend`] to plug in any storage — a local
4//! filesystem, object storage, in-memory fixtures, … — behind the same
5//! streaming API. Streams are *pull/push* based so memory stays constant
6//! regardless of file size.
7
8use std::io::Read;
9
10use async_trait::async_trait;
11use serde::{Deserialize, Serialize};
12
13use crate::error::StorageError;
14use crate::metadata::{ChunkRange, FileMeta};
15use crate::range::RangeSpec;
16
17/// How an upload should open (or resume) its target stream.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum WriteMode {
20    /// Create the file; fail with `AlreadyExists` if present.
21    Create,
22    /// Create or truncate the file.
23    Overwrite,
24    /// Continue writing at `offset`; fail if the file is not exactly
25    /// `offset` bytes yet.
26    Resume { offset: u64 },
27}
28
29/// A directory listing entry.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct DirEntry {
32    /// Full virtual path (relative to the mounted root).
33    pub path: String,
34    /// Whether this entry is a directory.
35    pub is_dir: bool,
36    /// Byte size (0 for directories).
37    pub size: u64,
38    /// Last-modified unix time.
39    pub mtime: u64,
40}
41
42/// Streaming write handle returned by [`StorageBackend::write_stream`].
43///
44/// Data written goes to a temporary location (typically a temp file)
45/// until [`UploadSink::commit`] atomically renames it into place; a
46/// failed or aborted upload leaves no partial target behind.
47#[async_trait]
48pub trait UploadSink: Send {
49    /// Append `buf` at the sink's current position.
50    async fn write(&mut self, buf: &[u8]) -> Result<(), StorageError>;
51
52    /// Write `buf` at an absolute `offset` within the destination.
53    ///
54    /// Sequential-only sinks may ignore `offset` and append instead (the
55    /// default). Positional sinks (used by the concurrent "session" upload
56    /// path) seek to `offset` first, so out-of-order chunks land in the
57    /// right place.
58    async fn write_at(&mut self, _offset: u64, buf: &[u8]) -> Result<(), StorageError> {
59        self.write(buf).await
60    }
61
62    /// The contiguous byte ranges of the destination already received.
63    ///
64    /// Used by the resumable "session" upload path: after an interruption
65    /// the client probes the server for the ranges that already landed so it
66    /// can retransmit only the missing gaps (BitTorrent-style). Sinks that
67    /// do not track received ranges return `Ok(vec![])` (the default), which
68    /// makes a resume degrade to a full re-send.
69    async fn received_ranges(&mut self) -> Result<Vec<ChunkRange>, StorageError> {
70        Ok(Vec::new())
71    }
72
73    /// Current length of the destination, for size validation before commit.
74    async fn len(&self) -> Result<u64, StorageError>;
75
76    /// Finish the stream, finalize the destination and return its metadata.
77    async fn commit(self: Box<Self>) -> Result<FileMeta, StorageError>;
78
79    /// Discard the temporary data and clean up.
80    async fn abort(self: Box<Self>) -> Result<(), StorageError>;
81}
82
83/// Pluggable storage backend for `libfw-server`.
84#[async_trait]
85pub trait StorageBackend: Send + Sync + 'static {
86    /// Return metadata for `path`, or `None` when it does not exist.
87    async fn file_meta(&self, path: &str) -> Result<Option<FileMeta>, StorageError>;
88
89    /// Open a read stream for `path` restricted to `range`.
90    ///
91    /// The returned reader yields exactly `range.len()` bytes on success.
92    async fn read_stream(
93        &self,
94        path: &str,
95        range: RangeSpec,
96    ) -> Result<Box<dyn Read + Send>, StorageError>;
97
98    /// Open a write stream for `path` according to `mode`.
99    async fn write_stream(&self, path: &str, mode: WriteMode) -> Result<Box<dyn UploadSink>, StorageError>;
100
101    /// Open a positional write stream for a **concurrent** "session" upload.
102    ///
103    /// All chunk requests for one file share the same `session` id and write
104    /// into a single shared temp file at absolute offsets via
105    /// [`UploadSink::write_at`]; only the final (commit) request renames it
106    /// into place. The first request (which creates the temp) uses `mode`
107    /// for the Create/Overwrite/Resume semantics; later requests ignore it.
108    async fn write_stream_session(
109        &self,
110        path: &str,
111        _session: &str,
112        mode: WriteMode,
113    ) -> Result<Box<dyn UploadSink>, StorageError> {
114        // Default: single-request path is not concurrent; behave like a
115        // normal `write_stream` for backends that don't opt into sessions.
116        self.write_stream(path, mode).await
117    }
118
119    /// List the children of directory `path` (or the mount root when
120    /// `path` is empty).
121    async fn list_dir(&self, path: &str) -> Result<Vec<DirEntry>, StorageError>;
122
123    /// Recursively create `path` (and parents) as a directory.
124    async fn mkdir_all(&self, path: &str) -> Result<(), StorageError>;
125
126    /// Remove `path` (file, or directory recursively).
127    async fn remove(&self, path: &str) -> Result<(), StorageError>;
128
129    /// Remove stale in-progress "session" upload temps (tus `Expiration`).
130    ///
131    /// A client that vanishes mid-upload leaves its shared session temp (and
132    /// any range sidecar) behind; this sweeps the ones whose last write is
133    /// older than `max_age`, returning how many were removed. Backends that
134    /// do not maintain long-lived session temps return `Ok(0)` (the default);
135    /// the bundled filesystem backend removes `.libfw-sess-*` temps and their
136    /// `.blocks` sidecars.
137    async fn cleanup_stale_sessions(
138        &self,
139        max_age: std::time::Duration,
140    ) -> Result<usize, StorageError> {
141        let _ = max_age;
142        Ok(0)
143    }
144}