feat(fuse): implement mkdir and mv with persistence
Add mkdir and mv (rename) FUSE operations to the virtual filesystem: - mkdir: Create directories that persist across remounts via SQLite - mv: Move/rename files and directories with database persistence Changes: - Add directories table to schema for user-created empty dirs - Add tree operations: mkdir, rename_file, rename_directory - Add DB methods for path updates and directory CRUD - Remove MountOption::RO to allow write syscalls - Load stored virtual_path from DB instead of regenerating - Restore user directories on mount from directories table - Upsert files to DB during origin scan POSIX compliant: mv fails with ENOENT if parent doesn't exist (use mkdir first, shell handles -p flag and brace expansion)
This commit is contained in:
@@ -284,6 +284,146 @@ impl Database {
|
||||
.optional()
|
||||
.map_err(|e| Error::Database(format!("query mtime failed: {}", e)))
|
||||
}
|
||||
|
||||
pub fn path_exists(&self, path: &VirtualPath) -> Result<bool> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM files WHERE virtual_path = ?1",
|
||||
params![path.as_str()],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|e| Error::Database(format!("path_exists query failed: {}", e)))?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
pub fn update_virtual_path(&self, id: FileId, new_path: &VirtualPath) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let rows = conn
|
||||
.execute(
|
||||
"UPDATE files SET virtual_path = ?1 WHERE id = ?2",
|
||||
params![new_path.as_str(), id.0],
|
||||
)
|
||||
.map_err(|e| Error::Database(format!("update_virtual_path failed: {}", e)))?;
|
||||
|
||||
if rows == 0 {
|
||||
return Err(Error::FileNotFound(format!("file id {} not found", id.0)));
|
||||
}
|
||||
debug!(
|
||||
id = id.0,
|
||||
new_path = new_path.as_str(),
|
||||
"updated virtual path"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rename_directory(&self, old_prefix: &str, new_prefix: &str) -> Result<u64> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let pattern = format!("{}%", old_prefix);
|
||||
let old_len = old_prefix.len();
|
||||
|
||||
let rows = conn
|
||||
.execute(
|
||||
"UPDATE files SET virtual_path = ?1 || substr(virtual_path, ?2) WHERE virtual_path LIKE ?3",
|
||||
params![new_prefix, old_len as i64 + 1, pattern],
|
||||
)
|
||||
.map_err(|e| Error::Database(format!("rename_directory failed: {}", e)))?;
|
||||
|
||||
debug!(old_prefix, new_prefix, rows, "renamed directory paths");
|
||||
Ok(rows as u64)
|
||||
}
|
||||
|
||||
pub fn get_files_by_prefix(&self, prefix: &str) -> Result<Vec<(FileId, VirtualPath)>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let pattern = format!("{}%", prefix);
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id, virtual_path FROM files WHERE virtual_path LIKE ?1")
|
||||
.map_err(|e| Error::Database(format!("prepare failed: {}", e)))?;
|
||||
|
||||
let files: Vec<(FileId, VirtualPath)> = stmt
|
||||
.query_map(params![pattern], |row| {
|
||||
Ok((
|
||||
FileId(row.get(0)?),
|
||||
VirtualPath::new(row.get::<_, String>(1)?),
|
||||
))
|
||||
})
|
||||
.map_err(|e| Error::Database(format!("query failed: {}", e)))?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
pub fn insert_directory(&self, path: &VirtualPath) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO directories (path) VALUES (?1)",
|
||||
params![path.as_str()],
|
||||
)
|
||||
.map_err(|e| Error::Database(format!("insert_directory failed: {}", e)))?;
|
||||
debug!(path = path.as_str(), "inserted directory");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_directory(&self, path: &VirtualPath) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"DELETE FROM directories WHERE path = ?1",
|
||||
params![path.as_str()],
|
||||
)
|
||||
.map_err(|e| Error::Database(format!("delete_directory failed: {}", e)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rename_directories(&self, old_prefix: &str, new_prefix: &str) -> Result<u64> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let pattern = format!("{}%", old_prefix);
|
||||
let old_len = old_prefix.len();
|
||||
|
||||
let rows = conn
|
||||
.execute(
|
||||
"UPDATE directories SET path = ?1 || substr(path, ?2) WHERE path LIKE ?3",
|
||||
params![new_prefix, old_len as i64 + 1, pattern],
|
||||
)
|
||||
.map_err(|e| Error::Database(format!("rename_directories failed: {}", e)))?;
|
||||
|
||||
debug!(old_prefix, new_prefix, rows, "renamed directory paths");
|
||||
Ok(rows as u64)
|
||||
}
|
||||
|
||||
pub fn list_directories(&self) -> Result<Vec<VirtualPath>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT path FROM directories ORDER BY path")
|
||||
.map_err(|e| Error::Database(format!("prepare failed: {}", e)))?;
|
||||
|
||||
let dirs: Vec<VirtualPath> = stmt
|
||||
.query_map([], |row| Ok(VirtualPath::new(row.get::<_, String>(0)?)))
|
||||
.map_err(|e| Error::Database(format!("query failed: {}", e)))?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
|
||||
Ok(dirs)
|
||||
}
|
||||
|
||||
pub fn get_file_by_real_path(
|
||||
&self,
|
||||
origin_id: &OriginId,
|
||||
real_path: &Path,
|
||||
) -> Result<Option<VirtualPath>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
conn.query_row(
|
||||
"SELECT virtual_path FROM files WHERE origin_id = ?1 AND real_path = ?2",
|
||||
params![&origin_id.0, real_path.to_string_lossy()],
|
||||
|row| Ok(VirtualPath::new(row.get::<_, String>(0)?)),
|
||||
)
|
||||
.optional()
|
||||
.map_err(|e| Error::Database(format!("query failed: {}", e)))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_audio_format(s: &str) -> AudioFormat {
|
||||
@@ -501,4 +641,145 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(retrieved.content_hash.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_exists() {
|
||||
let db = Database::open_memory().unwrap();
|
||||
|
||||
let path = VirtualPath::new("/Artist/Album/Track.flac");
|
||||
assert!(!db.path_exists(&path).unwrap());
|
||||
|
||||
db.upsert_file(
|
||||
&OriginId::from("local"),
|
||||
Path::new("/test.flac"),
|
||||
&path,
|
||||
&AudioMeta::default(),
|
||||
UNIX_EPOCH,
|
||||
100,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(db.path_exists(&path).unwrap());
|
||||
assert!(!db
|
||||
.path_exists(&VirtualPath::new("/Other/Path.flac"))
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_virtual_path() {
|
||||
let db = Database::open_memory().unwrap();
|
||||
|
||||
let old_path = VirtualPath::new("/Old/Path/Track.flac");
|
||||
let new_path = VirtualPath::new("/New/Path/Track.flac");
|
||||
|
||||
let id = db
|
||||
.upsert_file(
|
||||
&OriginId::from("local"),
|
||||
Path::new("/test.flac"),
|
||||
&old_path,
|
||||
&AudioMeta::default(),
|
||||
UNIX_EPOCH,
|
||||
100,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
db.update_virtual_path(id, &new_path).unwrap();
|
||||
|
||||
assert!(db.get_file_by_virtual_path(&old_path).unwrap().is_none());
|
||||
assert!(db.get_file_by_virtual_path(&new_path).unwrap().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_directory() {
|
||||
let db = Database::open_memory().unwrap();
|
||||
let origin = OriginId::from("local");
|
||||
|
||||
db.upsert_file(
|
||||
&origin,
|
||||
Path::new("/a.flac"),
|
||||
&VirtualPath::new("/Artist/Album/Track1.flac"),
|
||||
&AudioMeta::default(),
|
||||
UNIX_EPOCH,
|
||||
100,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
db.upsert_file(
|
||||
&origin,
|
||||
Path::new("/b.flac"),
|
||||
&VirtualPath::new("/Artist/Album/Track2.flac"),
|
||||
&AudioMeta::default(),
|
||||
UNIX_EPOCH,
|
||||
100,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
db.upsert_file(
|
||||
&origin,
|
||||
Path::new("/c.flac"),
|
||||
&VirtualPath::new("/Other/Track.flac"),
|
||||
&AudioMeta::default(),
|
||||
UNIX_EPOCH,
|
||||
100,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let count = db.rename_directory("/Artist/", "/Renamed Artist/").unwrap();
|
||||
assert_eq!(count, 2);
|
||||
|
||||
assert!(db
|
||||
.path_exists(&VirtualPath::new("/Renamed Artist/Album/Track1.flac"))
|
||||
.unwrap());
|
||||
assert!(db
|
||||
.path_exists(&VirtualPath::new("/Renamed Artist/Album/Track2.flac"))
|
||||
.unwrap());
|
||||
assert!(db
|
||||
.path_exists(&VirtualPath::new("/Other/Track.flac"))
|
||||
.unwrap());
|
||||
assert!(!db
|
||||
.path_exists(&VirtualPath::new("/Artist/Album/Track1.flac"))
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_files_by_prefix() {
|
||||
let db = Database::open_memory().unwrap();
|
||||
let origin = OriginId::from("local");
|
||||
|
||||
db.upsert_file(
|
||||
&origin,
|
||||
Path::new("/a.flac"),
|
||||
&VirtualPath::new("/Artist/Album/Track1.flac"),
|
||||
&AudioMeta::default(),
|
||||
UNIX_EPOCH,
|
||||
100,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
db.upsert_file(
|
||||
&origin,
|
||||
Path::new("/b.flac"),
|
||||
&VirtualPath::new("/Artist/Album/Track2.flac"),
|
||||
&AudioMeta::default(),
|
||||
UNIX_EPOCH,
|
||||
100,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
db.upsert_file(
|
||||
&origin,
|
||||
Path::new("/c.flac"),
|
||||
&VirtualPath::new("/Other/Track.flac"),
|
||||
&AudioMeta::default(),
|
||||
UNIX_EPOCH,
|
||||
100,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let files = db.get_files_by_prefix("/Artist/").unwrap();
|
||||
assert_eq!(files.len(), 2);
|
||||
|
||||
let files = db.get_files_by_prefix("/Other/").unwrap();
|
||||
assert_eq!(files.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@ pub use metadata::MetadataCache;
|
||||
pub use patterns::{AccessContext, AccessPattern, PatternError, PatternStore};
|
||||
pub use prefetch::{PrefetchConfig, PrefetchEngine, PrefetchHandle};
|
||||
pub use tree::{
|
||||
DirNode, FileNode, Inode, RefreshPolicy, TreeBuilder, VirtualNode, VirtualTree, ROOT_INODE,
|
||||
DirNode, FileNode, Inode, RefreshPolicy, RenameError, TreeBuilder, VirtualNode, VirtualTree,
|
||||
ROOT_INODE,
|
||||
};
|
||||
|
||||
@@ -56,3 +56,11 @@ CREATE INDEX IF NOT EXISTS idx_files_real ON files(origin_id, real_path);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_origin ON files(origin_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_last_sync ON files(last_sync);
|
||||
CREATE INDEX IF NOT EXISTS idx_artwork_file ON artwork(file_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS directories (
|
||||
id INTEGER PRIMARY KEY,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_directories_path ON directories(path);
|
||||
|
||||
@@ -310,6 +310,276 @@ impl VirtualTree {
|
||||
pub fn refresh_policy(&self) -> &RefreshPolicy {
|
||||
&self.refresh_policy
|
||||
}
|
||||
|
||||
pub fn path_to_inode_iter(&self) -> impl Iterator<Item = (&VirtualPath, &Inode)> {
|
||||
self.path_to_inode.iter()
|
||||
}
|
||||
|
||||
pub fn mkdir(&mut self, path: &VirtualPath) -> std::result::Result<Inode, RenameError> {
|
||||
if self.path_to_inode.contains_key(path) {
|
||||
return Err(RenameError::TargetExists);
|
||||
}
|
||||
|
||||
let parent_path = std::path::Path::new(path.as_str())
|
||||
.parent()
|
||||
.map(|p| {
|
||||
let s = p.to_string_lossy();
|
||||
if s.is_empty() {
|
||||
VirtualPath::new("/")
|
||||
} else {
|
||||
VirtualPath::new(s.into_owned())
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| VirtualPath::new("/"));
|
||||
|
||||
let parent_inode = self
|
||||
.path_to_inode
|
||||
.get(&parent_path)
|
||||
.copied()
|
||||
.ok_or(RenameError::ParentNotFound)?;
|
||||
|
||||
if !self
|
||||
.nodes
|
||||
.get(&parent_inode)
|
||||
.map(|n| n.is_dir())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(RenameError::ParentNotFound);
|
||||
}
|
||||
|
||||
let inode = self.alloc_inode();
|
||||
let name = std::path::Path::new(path.as_str())
|
||||
.file_name()
|
||||
.map(|n| n.to_os_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let dir_node = DirNode {
|
||||
inode,
|
||||
parent: parent_inode,
|
||||
name: name.clone(),
|
||||
children: BTreeMap::new(),
|
||||
mtime: SystemTime::now(),
|
||||
};
|
||||
|
||||
self.nodes.insert(inode, VirtualNode::Directory(dir_node));
|
||||
self.path_to_inode.insert(path.clone(), inode);
|
||||
|
||||
if let Some(VirtualNode::Directory(parent)) = self.nodes.get_mut(&parent_inode) {
|
||||
parent.children.insert(name, inode);
|
||||
}
|
||||
|
||||
debug!(path = path.as_str(), inode, "created directory");
|
||||
Ok(inode)
|
||||
}
|
||||
|
||||
pub fn rename_file(
|
||||
&mut self,
|
||||
old_path: &VirtualPath,
|
||||
new_path: &VirtualPath,
|
||||
) -> std::result::Result<(), RenameError> {
|
||||
let old_inode = self
|
||||
.path_to_inode
|
||||
.get(old_path)
|
||||
.copied()
|
||||
.ok_or(RenameError::SourceNotFound)?;
|
||||
|
||||
if self.path_to_inode.contains_key(new_path) {
|
||||
return Err(RenameError::TargetExists);
|
||||
}
|
||||
|
||||
let node = self
|
||||
.nodes
|
||||
.get(&old_inode)
|
||||
.ok_or(RenameError::SourceNotFound)?;
|
||||
|
||||
if node.is_dir() {
|
||||
return Err(RenameError::IsDirectory);
|
||||
}
|
||||
|
||||
let new_parent_path = std::path::Path::new(new_path.as_str())
|
||||
.parent()
|
||||
.map(|p| {
|
||||
let s = p.to_string_lossy();
|
||||
if s.is_empty() {
|
||||
VirtualPath::new("/")
|
||||
} else {
|
||||
VirtualPath::new(s.into_owned())
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| VirtualPath::new("/"));
|
||||
|
||||
let new_parent_inode = self
|
||||
.path_to_inode
|
||||
.get(&new_parent_path)
|
||||
.copied()
|
||||
.ok_or(RenameError::ParentNotFound)?;
|
||||
|
||||
if !self
|
||||
.nodes
|
||||
.get(&new_parent_inode)
|
||||
.map(|n| n.is_dir())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(RenameError::ParentNotFound);
|
||||
}
|
||||
|
||||
self.path_to_inode.remove(old_path);
|
||||
|
||||
let old_parent_path = std::path::Path::new(old_path.as_str())
|
||||
.parent()
|
||||
.map(|p| VirtualPath::new(p.to_string_lossy().into_owned()))
|
||||
.unwrap_or_else(|| VirtualPath::new("/"));
|
||||
|
||||
if let Some(&old_parent_inode) = self.path_to_inode.get(&old_parent_path) {
|
||||
if let Some(VirtualNode::Directory(dir)) = self.nodes.get_mut(&old_parent_inode) {
|
||||
let old_name = std::path::Path::new(old_path.as_str())
|
||||
.file_name()
|
||||
.map(|n| n.to_os_string())
|
||||
.unwrap_or_default();
|
||||
dir.children.remove(&old_name);
|
||||
}
|
||||
}
|
||||
|
||||
let new_name = std::path::Path::new(new_path.as_str())
|
||||
.file_name()
|
||||
.map(|n| n.to_os_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(VirtualNode::File(file)) = self.nodes.get_mut(&old_inode) {
|
||||
file.name = new_name.clone();
|
||||
}
|
||||
|
||||
if let Some(VirtualNode::Directory(dir)) = self.nodes.get_mut(&new_parent_inode) {
|
||||
dir.children.insert(new_name, old_inode);
|
||||
}
|
||||
|
||||
self.path_to_inode.insert(new_path.clone(), old_inode);
|
||||
|
||||
debug!(
|
||||
old = old_path.as_str(),
|
||||
new = new_path.as_str(),
|
||||
inode = old_inode,
|
||||
"renamed file"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rename_directory(
|
||||
&mut self,
|
||||
old_path: &VirtualPath,
|
||||
new_path: &VirtualPath,
|
||||
) -> std::result::Result<u64, RenameError> {
|
||||
let old_inode = self
|
||||
.path_to_inode
|
||||
.get(old_path)
|
||||
.copied()
|
||||
.ok_or(RenameError::SourceNotFound)?;
|
||||
|
||||
if !self
|
||||
.nodes
|
||||
.get(&old_inode)
|
||||
.map(|n| n.is_dir())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(RenameError::NotDirectory);
|
||||
}
|
||||
|
||||
if self.path_to_inode.contains_key(new_path) {
|
||||
return Err(RenameError::TargetExists);
|
||||
}
|
||||
|
||||
let new_parent_path = std::path::Path::new(new_path.as_str())
|
||||
.parent()
|
||||
.map(|p| {
|
||||
let s = p.to_string_lossy();
|
||||
if s.is_empty() {
|
||||
VirtualPath::new("/")
|
||||
} else {
|
||||
VirtualPath::new(s.into_owned())
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| VirtualPath::new("/"));
|
||||
|
||||
let new_parent_inode = self
|
||||
.path_to_inode
|
||||
.get(&new_parent_path)
|
||||
.copied()
|
||||
.ok_or(RenameError::ParentNotFound)?;
|
||||
|
||||
if !self
|
||||
.nodes
|
||||
.get(&new_parent_inode)
|
||||
.map(|n| n.is_dir())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(RenameError::ParentNotFound);
|
||||
}
|
||||
|
||||
let old_prefix = old_path.as_str();
|
||||
let new_prefix = new_path.as_str();
|
||||
|
||||
let paths_to_update: Vec<(VirtualPath, Inode)> = self
|
||||
.path_to_inode
|
||||
.iter()
|
||||
.filter(|(p, _)| p.as_str().starts_with(old_prefix))
|
||||
.map(|(p, &i)| (p.clone(), i))
|
||||
.collect();
|
||||
|
||||
let count = paths_to_update.len() as u64;
|
||||
|
||||
for (old_p, inode) in paths_to_update {
|
||||
self.path_to_inode.remove(&old_p);
|
||||
let new_p_str = format!("{}{}", new_prefix, &old_p.as_str()[old_prefix.len()..]);
|
||||
let new_p = VirtualPath::new(&new_p_str);
|
||||
self.path_to_inode.insert(new_p, inode);
|
||||
}
|
||||
|
||||
let old_parent_path = std::path::Path::new(old_path.as_str())
|
||||
.parent()
|
||||
.map(|p| VirtualPath::new(p.to_string_lossy().into_owned()))
|
||||
.unwrap_or_else(|| VirtualPath::new("/"));
|
||||
|
||||
if let Some(&old_parent_inode) = self.path_to_inode.get(&old_parent_path) {
|
||||
if let Some(VirtualNode::Directory(dir)) = self.nodes.get_mut(&old_parent_inode) {
|
||||
let old_name = std::path::Path::new(old_path.as_str())
|
||||
.file_name()
|
||||
.map(|n| n.to_os_string())
|
||||
.unwrap_or_default();
|
||||
dir.children.remove(&old_name);
|
||||
}
|
||||
}
|
||||
|
||||
let new_name = std::path::Path::new(new_path.as_str())
|
||||
.file_name()
|
||||
.map(|n| n.to_os_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(VirtualNode::Directory(dir)) = self.nodes.get_mut(&old_inode) {
|
||||
dir.name = new_name.clone();
|
||||
dir.parent = new_parent_inode;
|
||||
}
|
||||
|
||||
if let Some(VirtualNode::Directory(dir)) = self.nodes.get_mut(&new_parent_inode) {
|
||||
dir.children.insert(new_name, old_inode);
|
||||
}
|
||||
|
||||
debug!(
|
||||
old = old_path.as_str(),
|
||||
new = new_path.as_str(),
|
||||
count,
|
||||
"renamed directory"
|
||||
);
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RenameError {
|
||||
SourceNotFound,
|
||||
TargetExists,
|
||||
ParentNotFound,
|
||||
IsDirectory,
|
||||
NotDirectory,
|
||||
}
|
||||
|
||||
impl Default for VirtualTree {
|
||||
@@ -445,4 +715,178 @@ mod tests {
|
||||
let tree = builder.build();
|
||||
assert_eq!(tree.file_count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_file() {
|
||||
let mut tree = VirtualTree::new();
|
||||
let old_path = VirtualPath::new("/Artist/Album/Track.flac");
|
||||
let new_path = VirtualPath::new("/Artist/Album/Renamed.flac");
|
||||
|
||||
tree.insert_file(&make_file_meta(1, old_path.as_str()));
|
||||
|
||||
assert!(tree.get_by_path(&old_path).is_some());
|
||||
|
||||
tree.rename_file(&old_path, &new_path).unwrap();
|
||||
|
||||
assert!(tree.get_by_path(&old_path).is_none());
|
||||
assert!(tree.get_by_path(&new_path).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_file_to_new_dir() {
|
||||
let mut tree = VirtualTree::new();
|
||||
tree.insert_file(&make_file_meta(1, "/Artist/Album/Track.flac"));
|
||||
|
||||
tree.mkdir(&VirtualPath::new("/New Artist")).unwrap();
|
||||
tree.mkdir(&VirtualPath::new("/New Artist/New Album"))
|
||||
.unwrap();
|
||||
|
||||
let result = tree.rename_file(
|
||||
&VirtualPath::new("/Artist/Album/Track.flac"),
|
||||
&VirtualPath::new("/New Artist/New Album/Track.flac"),
|
||||
);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert!(tree
|
||||
.get_by_path(&VirtualPath::new("/New Artist/New Album/Track.flac"))
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_file_parent_not_found() {
|
||||
let mut tree = VirtualTree::new();
|
||||
tree.insert_file(&make_file_meta(1, "/Artist/Album/Track.flac"));
|
||||
|
||||
let result = tree.rename_file(
|
||||
&VirtualPath::new("/Artist/Album/Track.flac"),
|
||||
&VirtualPath::new("/NonExistent/Album/Track.flac"),
|
||||
);
|
||||
|
||||
assert_eq!(result, Err(RenameError::ParentNotFound));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_file_target_exists() {
|
||||
let mut tree = VirtualTree::new();
|
||||
tree.insert_file(&make_file_meta(1, "/A/Track1.flac"));
|
||||
tree.insert_file(&make_file_meta(2, "/A/Track2.flac"));
|
||||
|
||||
let result = tree.rename_file(
|
||||
&VirtualPath::new("/A/Track1.flac"),
|
||||
&VirtualPath::new("/A/Track2.flac"),
|
||||
);
|
||||
|
||||
assert_eq!(result, Err(RenameError::TargetExists));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_file_source_not_found() {
|
||||
let mut tree = VirtualTree::new();
|
||||
|
||||
let result = tree.rename_file(
|
||||
&VirtualPath::new("/Nonexistent.flac"),
|
||||
&VirtualPath::new("/New.flac"),
|
||||
);
|
||||
|
||||
assert_eq!(result, Err(RenameError::SourceNotFound));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_directory() {
|
||||
let mut tree = VirtualTree::new();
|
||||
tree.insert_file(&make_file_meta(1, "/Artist/Album/Track1.flac"));
|
||||
tree.insert_file(&make_file_meta(2, "/Artist/Album/Track2.flac"));
|
||||
tree.insert_file(&make_file_meta(3, "/Artist/Other/Track3.flac"));
|
||||
|
||||
let count = tree
|
||||
.rename_directory(
|
||||
&VirtualPath::new("/Artist"),
|
||||
&VirtualPath::new("/Renamed Artist"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(count, 6);
|
||||
|
||||
assert!(tree.get_by_path(&VirtualPath::new("/Artist")).is_none());
|
||||
assert!(tree
|
||||
.get_by_path(&VirtualPath::new("/Renamed Artist"))
|
||||
.is_some());
|
||||
assert!(tree
|
||||
.get_by_path(&VirtualPath::new("/Renamed Artist/Album/Track1.flac"))
|
||||
.is_some());
|
||||
assert!(tree
|
||||
.get_by_path(&VirtualPath::new("/Renamed Artist/Album/Track2.flac"))
|
||||
.is_some());
|
||||
assert!(tree
|
||||
.get_by_path(&VirtualPath::new("/Renamed Artist/Other/Track3.flac"))
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_directory_parent_not_found() {
|
||||
let mut tree = VirtualTree::new();
|
||||
tree.insert_file(&make_file_meta(1, "/Artist/Album/Track.flac"));
|
||||
|
||||
let result = tree.rename_directory(
|
||||
&VirtualPath::new("/Artist"),
|
||||
&VirtualPath::new("/NonExistent/Renamed"),
|
||||
);
|
||||
|
||||
assert_eq!(result, Err(RenameError::ParentNotFound));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_directory_not_directory() {
|
||||
let mut tree = VirtualTree::new();
|
||||
tree.insert_file(&make_file_meta(1, "/Artist/Track.flac"));
|
||||
|
||||
let result = tree.rename_directory(
|
||||
&VirtualPath::new("/Artist/Track.flac"),
|
||||
&VirtualPath::new("/New"),
|
||||
);
|
||||
|
||||
assert_eq!(result, Err(RenameError::NotDirectory));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mkdir() {
|
||||
let mut tree = VirtualTree::new();
|
||||
|
||||
let inode = tree.mkdir(&VirtualPath::new("/NewDir")).unwrap();
|
||||
assert!(inode > ROOT_INODE);
|
||||
assert!(tree.get_by_path(&VirtualPath::new("/NewDir")).is_some());
|
||||
assert!(tree
|
||||
.get_by_path(&VirtualPath::new("/NewDir"))
|
||||
.unwrap()
|
||||
.is_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mkdir_nested() {
|
||||
let mut tree = VirtualTree::new();
|
||||
|
||||
tree.mkdir(&VirtualPath::new("/A")).unwrap();
|
||||
tree.mkdir(&VirtualPath::new("/A/B")).unwrap();
|
||||
tree.mkdir(&VirtualPath::new("/A/B/C")).unwrap();
|
||||
|
||||
assert!(tree.get_by_path(&VirtualPath::new("/A/B/C")).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mkdir_parent_not_found() {
|
||||
let mut tree = VirtualTree::new();
|
||||
|
||||
let result = tree.mkdir(&VirtualPath::new("/A/B/C"));
|
||||
assert_eq!(result, Err(RenameError::ParentNotFound));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mkdir_already_exists() {
|
||||
let mut tree = VirtualTree::new();
|
||||
|
||||
tree.mkdir(&VirtualPath::new("/Existing")).unwrap();
|
||||
let result = tree.mkdir(&VirtualPath::new("/Existing"));
|
||||
|
||||
assert_eq!(result, Err(RenameError::TargetExists));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user