use std::path::{Component, Path, PathBuf};
use suno_core::{FileStat, Filesystem, FsError};
use crate::download::{replace, write_atomic};
pub struct FsAdapter {
root: PathBuf,
}
impl FsAdapter {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
fn resolve(&self, path: &str) -> Result<PathBuf, FsError> {
let contained = !path.is_empty()
&& Path::new(path)
.components()
.all(|c| matches!(c, Component::Normal(_) | Component::CurDir));
if contained {
Ok(self.root.join(path))
} else {
Err(FsError::new(format!(
"refusing path outside the library root: {path}"
)))
}
}
fn ensure_parent(full: &Path) -> Result<(), FsError> {
if let Some(parent) = full.parent() {
std::fs::create_dir_all(parent).map_err(|err| {
FsError::new(format!("could not create {}: {err}", parent.display()))
})?;
}
Ok(())
}
}
impl Filesystem for FsAdapter {
fn write_atomic(&self, path: &str, bytes: &[u8]) -> Result<(), FsError> {
let full = self.resolve(path)?;
Self::ensure_parent(&full)?;
write_atomic(&full, bytes).map_err(|err| FsError::new(err.to_string()))
}
fn rename(&self, from: &str, to: &str) -> Result<(), FsError> {
let from_full = self.resolve(from)?;
let to_full = self.resolve(to)?;
Self::ensure_parent(&to_full)?;
replace(&from_full, &to_full).map_err(|err| FsError::new(err.to_string()))
}
fn remove(&self, path: &str) -> Result<(), FsError> {
match std::fs::remove_file(self.resolve(path)?) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(FsError::new(err.to_string())),
}
}
fn prune_empty_dirs(&self, root: &str) -> Result<(), FsError> {
let base = if root.is_empty() || root == "." {
self.root.clone()
} else {
self.resolve(root)?
};
prune_dir(&base, true);
Ok(())
}
fn read(&self, path: &str) -> Result<Vec<u8>, FsError> {
std::fs::read(self.resolve(path)?).map_err(|err| FsError::new(err.to_string()))
}
fn metadata(&self, path: &str) -> Option<FileStat> {
std::fs::metadata(self.resolve(path).ok()?)
.ok()
.map(|meta| FileStat {
exists: true,
size: meta.len(),
})
}
}
fn prune_dir(dir: &Path, is_root: bool) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
prune_dir(&path, false);
}
}
if !is_root {
let _ = std::fs::remove_dir(dir);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_root() -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = Path::new("target").join(format!("fs-adapter-{}-{nanos}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn accepts_a_nested_relative_path() {
let root = temp_root();
let adapter = FsAdapter::new(root.clone());
adapter
.write_atomic("artist/album/song.flac", b"x")
.unwrap();
assert_eq!(adapter.read("artist/album/song.flac").unwrap(), b"x");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn rejects_an_absolute_path() {
let root = temp_root();
let adapter = FsAdapter::new(root.clone());
assert!(adapter.write_atomic("/etc/passwd", b"x").is_err());
assert!(adapter.read("/etc/passwd").is_err());
assert!(adapter.remove("/etc/passwd").is_err());
assert!(adapter.metadata("/etc/passwd").is_none());
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn rejects_a_parent_traversal() {
let root = temp_root();
let adapter = FsAdapter::new(root.clone());
assert!(adapter.write_atomic("../escape.flac", b"x").is_err());
assert!(adapter.rename("ok.flac", "../escape.flac").is_err());
assert!(adapter.remove("../../etc/passwd").is_err());
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn rejects_an_empty_path() {
let root = temp_root();
let adapter = FsAdapter::new(root.clone());
assert!(adapter.write_atomic("", b"x").is_err());
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn prune_removes_only_empty_dirs_and_keeps_the_root() {
let root = temp_root();
let adapter = FsAdapter::new(root.clone());
adapter.write_atomic("keep/full/song.flac", b"x").unwrap();
std::fs::create_dir_all(root.join("empty/leaf/deeper")).unwrap();
std::fs::create_dir_all(root.join("hidden")).unwrap();
std::fs::write(root.join("hidden/.suno-manifest.json"), b"{}").unwrap();
adapter.prune_empty_dirs("").unwrap();
assert!(!root.join("empty").exists());
assert!(!root.join("empty/leaf").exists());
assert!(root.join("keep/full/song.flac").exists());
assert!(root.join("hidden/.suno-manifest.json").exists());
assert!(root.exists());
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn prune_never_removes_the_named_root_even_when_empty() {
let root = temp_root();
let adapter = FsAdapter::new(root.clone());
std::fs::create_dir_all(root.join("album/leaf")).unwrap();
adapter.prune_empty_dirs("album").unwrap();
assert!(root.join("album").exists());
assert!(!root.join("album/leaf").exists());
let _ = std::fs::remove_dir_all(&root);
}
}