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}