Skip to main content

suno_core/
fs.rs

1//! The filesystem port: the executor's only window to disk.
2//!
3//! The download executor never touches the disk directly. It writes, renames,
4//! removes, reads, and probes files through this trait, which a CLI adapter
5//! implements with `std::fs` (an atomic temp-and-rename write, a cross-platform
6//! replace, and parent-directory creation). Tests use an in-memory double so
7//! the executor's logic is exercised without real IO.
8//!
9//! Paths are relative to an account root the adapter owns; the executor only
10//! ever passes the relative path a [`crate::Plan`] carries.
11
12/// On-disk facts about one path, as probed by [`Filesystem::metadata`].
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub struct FileStat {
15    /// Whether the file exists.
16    pub exists: bool,
17    /// Size of the file in bytes (zero when absent).
18    pub size: u64,
19}
20
21/// A filesystem failure, carrying a human-readable, secret-free reason.
22#[derive(Debug, thiserror::Error)]
23#[error("{0}")]
24pub struct FsError(pub String);
25
26impl FsError {
27    /// Build an [`FsError`] from any displayable cause.
28    pub fn new(reason: impl Into<String>) -> Self {
29        Self(reason.into())
30    }
31}
32
33/// The disk port the executor writes the plan through.
34///
35/// Methods are synchronous: disk IO is fast and the adapter can offload it if
36/// it must. Every method returns a [`Result`] so the engine never panics on an
37/// IO fault; a write failure must leave any prior file intact (atomic write).
38pub trait Filesystem {
39    /// Write `bytes` to `path` atomically, replacing any existing file.
40    ///
41    /// On failure the prior file at `path` is left untouched: the adapter
42    /// stages a temporary sibling and renames it into place only once the full
43    /// contents are written, so a partial write can never be observed.
44    fn write_atomic(&self, path: &str, bytes: &[u8]) -> Result<(), FsError>;
45
46    /// Move `from` onto `to`, replacing any existing destination.
47    fn rename(&self, from: &str, to: &str) -> Result<(), FsError>;
48
49    /// Remove `path`. Succeeds when the file is already absent (idempotent).
50    fn remove(&self, path: &str) -> Result<(), FsError>;
51
52    /// Remove empty directories under `root`, bottom-up.
53    ///
54    /// After a rename/move or a delete empties an album directory, that now-dead
55    /// directory is a ghost. This prunes it. The contract is strictly additive
56    /// and safe:
57    ///
58    /// - it removes only directories that are genuinely empty, walking
59    ///   depth-first so an emptied parent is pruned once its last child is;
60    /// - it NEVER removes a directory holding any entry, including a hidden file
61    ///   (a `.suno-manifest.json`, `.suno-lineage.json`, or `.m3u8`); and
62    /// - it NEVER removes `root` itself, only directories strictly beneath it.
63    ///
64    /// `root` is a library-relative directory, with the empty string (or `"."`)
65    /// meaning the account root. A prune failure is never fatal: the tool
66    /// re-plans and retries on the next run, so this only ever tidies.
67    fn prune_empty_dirs(&self, root: &str) -> Result<(), FsError>;
68
69    /// Read the whole file at `path`.
70    fn read(&self, path: &str) -> Result<Vec<u8>, FsError>;
71
72    /// Probe `path`, returning its [`FileStat`] or `None` when it is absent.
73    fn metadata(&self, path: &str) -> Option<FileStat>;
74}