Refactor before adding NetworkOrigin

This commit is contained in:
Alexander
2026-06-29 20:33:32 +02:00
parent 13f007ebb5
commit 80dfe50aaa
12 changed files with 793 additions and 632 deletions
+12 -33
View File
@@ -1,10 +1,6 @@
use std::{fs, path::PathBuf};
use fuser::INodeNo;
use sea_orm::entity::prelude::*;
use crate::item::{FileType, Item};
use crate::virtual_dirs::parent_inode_from_path;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "items")]
@@ -40,36 +36,22 @@ impl From<&Item> for ActiveModel {
}
}
impl From<(Model, fs::Metadata)> for Item {
fn from((entity, metadata): (Model, fs::Metadata)) -> Self {
let local_path = PathBuf::from(&entity.local_path);
let parent_inode = parent_inode_from_path(&local_path);
let file_type = match entity.file_type.as_str() {
"directory" => FileType::Directory,
_ => FileType::File,
};
Item::new(
INodeNo(entity.inode as u64),
parent_inode,
entity.name,
PathBuf::from(entity.original_path),
local_path,
file_type,
metadata,
None,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::item::{FileType, Item};
use crate::origins::attrs::FileAttrs;
use fuser::INodeNo;
use std::path::PathBuf;
fn attrs_for_tempdir() -> FileAttrs {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
return FileAttrs::from(&metadata);
}
#[test]
fn item_to_active_model_fields_match() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let item = Item::new(
INodeNo(42),
INodeNo::ROOT,
@@ -77,7 +59,7 @@ mod tests {
PathBuf::from("/original/path"),
PathBuf::from("local/path"),
FileType::Directory,
metadata,
attrs_for_tempdir(),
None,
);
let am = ActiveModel::from(&item);
@@ -115,9 +97,6 @@ mod tests {
#[test]
fn file_type_string_mapping() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let item_dir = Item::new(
INodeNo(1),
INodeNo::ROOT,
@@ -125,7 +104,7 @@ mod tests {
PathBuf::from("/original/dir"),
PathBuf::from("local/dir"),
FileType::Directory,
metadata.clone(),
attrs_for_tempdir(),
None,
);
let am_dir = ActiveModel::from(&item_dir);
@@ -143,7 +122,7 @@ mod tests {
PathBuf::from("/original/file"),
PathBuf::from("local/file"),
FileType::File,
metadata,
attrs_for_tempdir(),
None,
);
let am_file = ActiveModel::from(&item_file);
+33 -33
View File
@@ -1,12 +1,16 @@
use std::{fs, hash::Hasher, path::PathBuf, time::SystemTime};
use std::{
hash::Hasher,
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use fuser::INodeNo;
use twox_hash::XxHash64;
use crate::music::metadata::MusicMetadata;
use crate::origins::attrs::FileAttrs;
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub enum FileType {
@@ -14,7 +18,7 @@ pub enum FileType {
File,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Item {
pub inode: INodeNo,
pub parent_inode: INodeNo,
@@ -22,11 +26,18 @@ pub struct Item {
pub original_path: PathBuf,
pub local_path: PathBuf,
pub file_type: FileType,
pub metadata: fs::Metadata,
pub attrs: FileAttrs,
pub music_metadata: Option<MusicMetadata>,
pub hash: u64,
}
fn secs_since_epoch(time: SystemTime) -> u64 {
return time
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
}
impl Item {
#[allow(clippy::too_many_arguments)]
pub fn new(
@@ -36,7 +47,7 @@ impl Item {
original_path: PathBuf,
local_path: PathBuf,
file_type: FileType,
metadata: fs::Metadata,
attrs: FileAttrs,
music_metadata: Option<MusicMetadata>,
) -> Item {
let mut item = Item {
@@ -46,7 +57,7 @@ impl Item {
original_path,
local_path,
file_type,
metadata,
attrs,
music_metadata,
hash: 0,
};
@@ -59,25 +70,11 @@ impl Item {
let seed = 1234;
let mut hasher = XxHash64::with_seed(seed);
let metadata = &self.metadata;
let mtime: u64 = metadata
.modified()
.unwrap_or(SystemTime::UNIX_EPOCH)
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let crtime: u64 = metadata
.created()
.unwrap_or(SystemTime::UNIX_EPOCH)
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
hasher.write_u64(self.inode.0);
hasher.write(self.original_path.as_os_str().as_bytes());
hasher.write_u64(metadata.ctime() as u64);
hasher.write_u64(mtime);
hasher.write_u64(crtime);
hasher.write_u64(secs_since_epoch(self.attrs.ctime));
hasher.write_u64(secs_since_epoch(self.attrs.mtime));
hasher.write_u64(secs_since_epoch(self.attrs.crtime));
return hasher.finish();
}
@@ -86,11 +83,11 @@ impl Item {
#[cfg(test)]
mod tests {
use super::*;
use crate::origins::attrs::FileAttrs;
#[test]
fn compute_hash_deterministic() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let attrs = file_attrs_for_tempdir();
let item1 = Item::new(
INodeNo(42),
@@ -99,11 +96,10 @@ mod tests {
PathBuf::from("/some/path"),
PathBuf::from("test"),
FileType::File,
metadata,
attrs.clone(),
None,
);
let metadata2 = std::fs::metadata(tmp.path()).unwrap();
let item2 = Item::new(
INodeNo(42),
INodeNo::ROOT,
@@ -111,7 +107,7 @@ mod tests {
PathBuf::from("/some/path"),
PathBuf::from("test"),
FileType::File,
metadata2,
attrs,
None,
);
@@ -120,8 +116,7 @@ mod tests {
#[test]
fn compute_hash_changes_on_path_change() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let attrs = file_attrs_for_tempdir();
let item1 = Item::new(
INodeNo(42),
@@ -130,11 +125,10 @@ mod tests {
PathBuf::from("/some/path"),
PathBuf::from("test"),
FileType::File,
metadata,
attrs.clone(),
None,
);
let metadata2 = std::fs::metadata(tmp.path()).unwrap();
let item2 = Item::new(
INodeNo(42),
INodeNo::ROOT,
@@ -142,10 +136,16 @@ mod tests {
PathBuf::from("/different/path"),
PathBuf::from("test"),
FileType::File,
metadata2,
attrs,
None,
);
assert_ne!(item1.hash, item2.hash);
}
fn file_attrs_for_tempdir() -> FileAttrs {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
return FileAttrs::from(&metadata);
}
}
+34 -5
View File
@@ -1,5 +1,13 @@
use std::{collections::HashMap, path::Path};
use clap::Parser;
use musicfs::origins::local::watcher::FileWatcher;
use musicfs::db::entities::{Entity, Model};
use musicfs::db::sync::sync_items_to_db;
use musicfs::music::db::restore_music_metadata_from_db;
use musicfs::origins::local::LocalOrigin;
use musicfs::origins::{FuseFs, Origin};
use musicfs::virtual_dirs::restore_virtual_paths;
use sea_orm::entity::prelude::*;
use tokio::signal::unix::{SignalKind, signal};
#[derive(Parser, Debug)]
@@ -22,11 +30,32 @@ async fn main() {
let db = sea_orm::Database::connect(&args.database).await.unwrap();
let fs = musicfs::origins::local::LocalOrigin::new(args.source.clone(), mountpoint.clone(), db)
.await
.unwrap();
let origin = LocalOrigin::new(args.source.clone(), mountpoint.clone());
fs.watch();
let mut snapshot = origin.snapshot().unwrap();
let db_items: HashMap<i64, Model> = Entity::find()
.all(&db)
.await
.unwrap()
.into_iter()
.map(|e| (e.inode, e))
.collect();
sync_items_to_db(&snapshot, &db_items, &db).await;
restore_music_metadata_from_db(&mut snapshot, &db_items, &db).await;
restore_virtual_paths(&mut snapshot, &db_items, Path::new(&args.source));
let files = std::sync::Arc::new(std::sync::Mutex::new(snapshot));
let bytes = origin.byte_source();
let watcher = origin.watcher();
watcher.watch(files.clone());
let fs = FuseFs {
files,
bytes,
client: db,
};
let cfg = fuser::Config::default();
let session =
+88
View File
@@ -0,0 +1,88 @@
use std::{
fs,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use std::os::unix::fs::{MetadataExt, PermissionsExt};
/// Owned, `Clone`-able snapshot of the file attributes that musicfs needs to
/// serve FUSE `getattr` and to compute the per-item identity hash.
///
/// Decoupled from `std::fs::Metadata` so that non-disk origins (e.g. a future
/// `NetworkOrigin` reading a server manifest) can construct equivalent
/// attributes without a real inode on disk. The on-disk origin builds this
/// via `From<&fs::Metadata>`; other origins build it from their own metadata
/// source using the same field types.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileAttrs {
pub size: u64,
pub blocks: u64,
pub atime: SystemTime,
pub mtime: SystemTime,
pub ctime: SystemTime,
pub crtime: SystemTime,
pub perm: u16,
pub nlink: u32,
pub uid: u32,
pub gid: u32,
pub rdev: u32,
pub blksize: u32,
}
impl From<&fs::Metadata> for FileAttrs {
fn from(metadata: &fs::Metadata) -> Self {
return FileAttrs {
size: metadata.size(),
blocks: metadata.blocks(),
atime: metadata.accessed().unwrap_or(UNIX_EPOCH),
mtime: metadata.modified().unwrap_or(UNIX_EPOCH),
ctime: UNIX_EPOCH + Duration::from_secs(metadata.ctime() as u64),
crtime: metadata.created().unwrap_or(UNIX_EPOCH),
perm: metadata.permissions().mode() as u16,
nlink: metadata.nlink() as u32,
uid: metadata.uid(),
gid: metadata.gid(),
rdev: metadata.rdev() as u32,
blksize: metadata.blksize() as u32,
};
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn file_attrs_from_metadata_copies_size_and_block_fields() {
let tmp = tempfile::tempdir().unwrap();
let metadata = fs::metadata(tmp.path()).unwrap();
let attrs = FileAttrs::from(&metadata);
assert_eq!(attrs.size, metadata.size());
assert_eq!(attrs.blocks, metadata.blocks());
assert_eq!(attrs.blksize, metadata.blksize() as u32);
}
#[test]
fn file_attrs_from_metadata_copies_unix_ownership() {
let tmp = tempfile::tempdir().unwrap();
let metadata = fs::metadata(tmp.path()).unwrap();
let attrs = FileAttrs::from(&metadata);
assert_eq!(attrs.uid, metadata.uid());
assert_eq!(attrs.gid, metadata.gid());
assert_eq!(attrs.perm, metadata.permissions().mode() as u16);
}
#[test]
fn file_attrs_from_metadata_preserves_mtime() {
let tmp = tempfile::tempdir().unwrap();
let metadata = fs::metadata(tmp.path()).unwrap();
let attrs = FileAttrs::from(&metadata);
assert_eq!(attrs.mtime, metadata.modified().unwrap_or(UNIX_EPOCH));
}
}
+24 -8
View File
@@ -14,8 +14,16 @@ pub fn read_bytes_at(path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>
return Ok(buf);
}
/// Reassemble a virtualized FLAC byte range by stitching together the cached
/// header, picture block headers, and on-demand audio/picture bytes fetched
/// through `reader`.
///
/// `reader(offset, len)` is transport-agnostic: for the local origin it wraps
/// `read_bytes_at(&path, ...)`; a network origin will route the same call
/// through its byte cache + HTTP range request. The FLAC assembly logic never
/// touches a `Path` directly.
pub fn assemble_flac_read(
original_path: &Path,
reader: &dyn Fn(u64, usize) -> io::Result<Vec<u8>>,
header: &[u8],
pic_hdrs: &[Vec<u8>],
pic_ranges: &[(u64, u64)],
@@ -60,7 +68,7 @@ pub fn assemble_flac_read(
let data_end = end.min(pic_end);
if data_start < data_end {
let real_off = data_real_offset + (data_start - pic_hdr_end);
let bytes = read_bytes_at(original_path, real_off, (data_end - data_start) as usize)?;
let bytes = reader(real_off, (data_end - data_start) as usize)?;
buf.extend_from_slice(&bytes);
}
@@ -70,7 +78,7 @@ pub fn assemble_flac_read(
let audio_start = offset.max(virt_pos);
if audio_start < end {
let real_off = real_audio_start + (audio_start - virt_pos);
let bytes = read_bytes_at(original_path, real_off, (end - audio_start) as usize)?;
let bytes = reader(real_off, (end - audio_start) as usize)?;
buf.extend_from_slice(&bytes);
}
@@ -81,6 +89,11 @@ pub fn assemble_flac_read(
mod tests {
use super::*;
use std::io::Write;
use std::path::PathBuf;
fn file_reader(path: PathBuf) -> impl Fn(u64, usize) -> io::Result<Vec<u8>> {
return move |offset, len| read_bytes_at(&path, offset, len);
}
#[test]
fn read_bytes_at_full_file() {
@@ -107,7 +120,8 @@ mod tests {
#[test]
fn assemble_flac_read_header_only() {
let header = b"HEADERDATA";
let result = assemble_flac_read(Path::new(""), header, &[], &[], 0, 2, 4).unwrap();
let noop = |_, _| Ok(vec![]);
let result = assemble_flac_read(&noop, header, &[], &[], 0, 2, 4).unwrap();
assert_eq!(result, b"ADER");
}
@@ -117,10 +131,11 @@ mod tests {
let content: Vec<u8> = (0..20).collect();
f.write_all(&content).unwrap();
f.flush().unwrap();
let path = f.path();
let path = f.path().to_path_buf();
let reader = file_reader(path);
let header = b"HDR";
let result = assemble_flac_read(path, header, &[], &[], 10, 5, 4).unwrap();
let result = assemble_flac_read(&reader, header, &[], &[], 10, 5, 4).unwrap();
// offset=5, header_len=3, so we read from header[3..] (0 bytes) + audio at real_off=10+(5-3)=12
// content[12..16] = [12, 13, 14, 15]
assert_eq!(result, vec![12, 13, 14, 15]);
@@ -132,10 +147,11 @@ mod tests {
let content: Vec<u8> = (0..110).collect();
f.write_all(&content).unwrap();
f.flush().unwrap();
let path = f.path();
let path = f.path().to_path_buf();
let reader = file_reader(path);
let header = b"ABCD";
let result = assemble_flac_read(path, header, &[], &[], 100, 2, 6).unwrap();
let result = assemble_flac_read(&reader, header, &[], &[], 100, 2, 6).unwrap();
// offset=2, size=6, header_len=4
// First 2 bytes from header[2..4] = "CD"
// Remaining 4 bytes from audio at real_off=100+(2+2-4)=100
+22 -528
View File
@@ -6,26 +6,16 @@ use std::{
collections::BTreeMap,
io,
path::{Path, PathBuf},
sync::{Arc, Mutex},
time::Duration,
sync::Arc,
};
use fuser::{Errno, FileAttr, Filesystem, Generation, INodeNo, Request};
use sea_orm::entity::prelude::*;
use crate::db::entities::{ActiveModel, Entity, Model};
use crate::db::sync::{run_db_blocking, sync_items_to_db};
use crate::item::{FileType, Item};
use crate::music::db::{restore_music_metadata_from_db, save_music_metadata};
use crate::virtual_dirs::{parent_inode_from_path, restore_virtual_paths};
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use fuser::INodeNo;
use crate::item::Item;
use crate::origins::{ByteSource, FileWatcher, Origin};
pub struct LocalOrigin {
pub(crate) source: PathBuf,
pub(crate) destination: PathBuf,
pub(crate) files: Arc<Mutex<BTreeMap<INodeNo, Item>>>,
client: sea_orm::DatabaseConnection,
}
impl std::fmt::Debug for LocalOrigin {
@@ -34,540 +24,44 @@ impl std::fmt::Debug for LocalOrigin {
.debug_struct("LocalOrigin")
.field("source", &self.source)
.field("destination", &self.destination)
.field("files", &self.files)
.finish();
}
}
impl LocalOrigin {
pub async fn new(
source: String,
destination: String,
client: sea_orm::DatabaseConnection,
) -> Result<LocalOrigin, io::Error> {
println!("Initializing the LocalOrigin");
let mut snapshot = snapshot::build_snapshot(Path::new(&source), Path::new(&destination))?;
let db_items: std::collections::HashMap<i64, Model> = Entity::find()
.all(&client)
.await
.unwrap()
.into_iter()
.map(|e| (e.inode, e))
.collect();
sync_items_to_db(&snapshot, &db_items, &client).await;
restore_music_metadata_from_db(&mut snapshot, &db_items, &client).await;
restore_virtual_paths(&mut snapshot, &db_items, Path::new(&source));
let local_origin = LocalOrigin {
pub fn new(source: String, destination: String) -> LocalOrigin {
return LocalOrigin {
source: source.into(),
destination: destination.into(),
files: Arc::new(Mutex::new(snapshot)),
client,
};
return Ok(local_origin);
}
fn file_to_attr(item: &Item) -> FileAttr {
let metadata = &item.metadata;
let ctime = std::time::UNIX_EPOCH + Duration::from_secs(metadata.ctime() as u64);
let atime = metadata
.accessed()
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
let mtime = metadata
.modified()
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
let crtime = metadata
.created()
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
let kind = match item.file_type {
FileType::Directory => fuser::FileType::Directory,
FileType::File => fuser::FileType::RegularFile,
};
let size = match &item.music_metadata {
Some(mm) if !mm.header.is_empty() => mm.virtual_size(metadata.size()),
_ => metadata.size(),
};
return FileAttr {
ino: item.inode,
size,
blocks: metadata.blocks(),
atime,
mtime,
ctime,
crtime,
kind,
perm: metadata.permissions().mode() as u16,
nlink: metadata.nlink() as u32,
uid: metadata.uid(),
gid: metadata.gid(),
rdev: metadata.rdev() as u32,
blksize: metadata.blksize() as u32,
flags: 0,
};
pub fn source(&self) -> &Path {
return &self.source;
}
}
impl Filesystem for LocalOrigin {
fn open(
&self,
_req: &Request,
ino: INodeNo,
_flags: fuser::OpenFlags,
reply: fuser::ReplyOpen,
) {
if self.files.lock().unwrap().contains_key(&ino) {
reply.opened(fuser::FileHandle(ino.0), fuser::FopenFlags::empty());
} else {
reply.error(Errno::ENOENT);
}
impl Origin for LocalOrigin {
fn snapshot(&self) -> io::Result<BTreeMap<INodeNo, Item>> {
return snapshot::build_snapshot(&self.source, &self.destination);
}
fn setattr(
&self,
_req: &Request,
ino: INodeNo,
_mode: Option<u32>,
_uid: Option<u32>,
_gid: Option<u32>,
_size: Option<u64>,
_atime: Option<fuser::TimeOrNow>,
_mtime: Option<fuser::TimeOrNow>,
_ctime: Option<std::time::SystemTime>,
_fh: Option<fuser::FileHandle>,
_crtime: Option<std::time::SystemTime>,
_chgtime: Option<std::time::SystemTime>,
_bkuptime: Option<std::time::SystemTime>,
_flags: Option<fuser::BsdFileFlags>,
reply: fuser::ReplyAttr,
) {
match self.files.lock().unwrap().get(&ino) {
Some(file) => reply.attr(&Duration::new(1, 0), &LocalOrigin::file_to_attr(file)),
None => reply.error(Errno::ENOENT),
}
fn byte_source(&self) -> Arc<dyn ByteSource> {
return Arc::new(LocalByteSource);
}
fn write(
&self,
_req: &Request,
ino: INodeNo,
_fh: fuser::FileHandle,
offset: u64,
data: &[u8],
_write_flags: fuser::WriteFlags,
_flags: fuser::OpenFlags,
_lock_owner: Option<fuser::LockOwner>,
reply: fuser::ReplyWrite,
) {
let written = data.len() as u32;
let write_start = offset;
let write_end = write_start + data.len() as u64;
let updated_music_metadata = {
let mut files = self.files.lock().unwrap();
let item = match files.get_mut(&ino) {
Some(item) => item,
None => {
reply.written(written);
return;
}
};
match &mut item.music_metadata {
// FLAC: intercept Vorbis comment block writes
Some(mm) if mm.vorbis_comment_length > 0 => {
let vc_data_offset = mm.vorbis_comment_offset;
let vc_hdr_offset = vc_data_offset - 4;
if write_start <= vc_hdr_offset && write_end >= vc_data_offset {
let hdr_from = (vc_hdr_offset - write_start) as usize;
let new_length = u32::from_be_bytes([
0,
data[hdr_from + 1],
data[hdr_from + 2],
data[hdr_from + 3],
]) as u64;
let vc_data_end = vc_data_offset + new_length;
if write_end >= vc_data_end {
let from = (vc_data_offset - write_start) as usize;
let to = (vc_data_end - write_start) as usize;
mm.update_from_vorbis_comment_data(&data[from..to]);
Some(mm.clone())
} else {
None
}
} else {
None
}
}
// MP3: intercept ID3v2 header writes (must start at offset 0)
Some(mm)
if !mm.header.is_empty()
&& write_start == 0
&& data.len() >= 3
&& &data[0..3] == b"ID3" =>
{
mm.update_from_id3_data(data);
Some(mm.clone())
}
// MP3: intercept ID3v1 fallback writes (128-byte "TAG" block)
Some(mm) if !mm.header.is_empty() && data.len() == 128 && &data[0..3] == b"TAG" => {
mm.update_from_id3v1_data(data);
Some(mm.clone())
}
_ => None,
}
};
if let Some(music_metadata) = updated_music_metadata {
let client = self.client.clone();
let ino_i64 = ino.0 as i64;
run_db_blocking(async move {
save_music_metadata(ino_i64, &music_metadata, &client)
.await
.unwrap();
});
}
reply.written(written);
}
fn getattr(
&self,
_req: &fuser::Request,
ino: INodeNo,
_fh: Option<fuser::FileHandle>,
reply: fuser::ReplyAttr,
) {
println!("getattr(ino={})", ino);
match self.files.lock().unwrap().get(&ino) {
Some(file) => {
let ttl = Duration::new(1, 0);
let attr = LocalOrigin::file_to_attr(file);
reply.attr(&ttl, &attr);
}
None => reply.error(Errno::ENOENT),
}
}
fn readdir(
&self,
_req: &fuser::Request,
ino: INodeNo,
fh: fuser::FileHandle,
offset: u64,
mut reply: fuser::ReplyDirectory,
) {
println!("readdir(ino={}, fh={}, offset={})", ino, fh, offset);
let files = self.files.lock().unwrap();
let parent_inode = match files.get(&ino) {
Some(dir) => dir.parent_inode,
None => {
reply.error(Errno::ENOENT);
return;
}
};
// Offsets are 1-based cursors: "." = 1, ".." = 2, real entries = 3+.
// The kernel passes back the offset of the last entry it received;
// we return entries with offset > that value.
if offset < 1 {
if reply.add(ino, 1, fuser::FileType::Directory, ".") {
reply.ok();
return;
}
}
if offset < 2 {
if reply.add(parent_inode, 2, fuser::FileType::Directory, "..") {
reply.ok();
return;
}
}
let skip_count = if offset <= 2 {
0
} else {
(offset - 2) as usize
};
for (i, (key, value)) in files
.iter()
.filter(|(_, v)| v.parent_inode == ino && v.inode != ino)
.skip(skip_count)
.enumerate()
{
let entry_offset = (skip_count + i + 3) as u64;
let file_type = match value.file_type {
FileType::Directory => fuser::FileType::Directory,
FileType::File => fuser::FileType::RegularFile,
};
if reply.add(*key, entry_offset, file_type, &value.name) {
break;
}
}
reply.ok();
}
fn rename(
&self,
_req: &Request,
parent: INodeNo,
name: &std::ffi::OsStr,
newparent: INodeNo,
newname: &std::ffi::OsStr,
_flags: fuser::RenameFlags,
reply: fuser::ReplyEmpty,
) {
let name_str = match name.to_str() {
Some(s) => s,
None => {
reply.error(Errno::EINVAL);
return;
}
};
let newname_str = match newname.to_str() {
Some(s) => s,
None => {
reply.error(Errno::EINVAL);
return;
}
};
let mut files = self.files.lock().unwrap();
let item_ino = match files
.iter()
.find(|(_, v)| v.parent_inode == parent && v.name == name_str)
{
Some((ino, _)) => *ino,
None => {
reply.error(Errno::ENOENT);
return;
}
};
let new_local_path = if newparent == INodeNo::ROOT {
PathBuf::from(newname_str)
} else {
match files.get(&newparent) {
Some(dir) => dir.local_path.join(newname_str),
None => {
reply.error(Errno::ENOENT);
return;
}
}
};
let new_parent_inode = parent_inode_from_path(&new_local_path);
let item = files.get_mut(&item_ino).unwrap();
item.name = newname_str.to_string();
item.local_path = new_local_path.clone();
item.parent_inode = new_parent_inode;
let inode_i64 = item_ino.0 as i64;
let new_name_owned = newname_str.to_string();
let new_local_path_str = new_local_path.to_string_lossy().into_owned();
let client = self.client.clone();
drop(files);
run_db_blocking(async move {
use sea_orm::ActiveValue::Set;
ActiveModel {
inode: Set(inode_i64),
name: Set(new_name_owned),
local_path: Set(new_local_path_str),
..Default::default()
}
.update(&client)
.await
.unwrap();
});
reply.ok();
}
fn read(
&self,
_req: &Request,
ino: INodeNo,
_fh: fuser::FileHandle,
offset: u64,
size: u32,
_flags: fuser::OpenFlags,
_lock_owner: Option<fuser::LockOwner>,
reply: fuser::ReplyData,
) {
let files = self.files.lock().unwrap();
let item = match files.get(&ino) {
Some(f) => f,
None => {
reply.error(Errno::ENOENT);
return;
}
};
let original_path = item.original_path.clone();
let flac = item
.music_metadata
.as_ref()
.filter(|mm| !mm.header.is_empty())
.map(|mm| {
(
mm.header.clone(),
mm.picture_block_headers.clone(),
mm.picture_data_ranges.clone(),
mm.real_audio_start,
)
});
drop(files);
let Some((header, pic_hdrs, pic_ranges, real_audio_start)) = flac else {
match file_io::read_bytes_at(&original_path, offset, size as usize) {
Ok(bytes) => reply.data(&bytes),
Err(_) => reply.error(Errno::EIO),
}
return;
};
match file_io::assemble_flac_read(
&original_path,
&header,
&pic_hdrs,
&pic_ranges,
real_audio_start,
offset,
size,
) {
Ok(bytes) => reply.data(&bytes),
Err(_) => reply.error(Errno::EIO),
}
}
fn lookup(
&self,
_req: &Request,
parent: INodeNo,
name: &std::ffi::OsStr,
reply: fuser::ReplyEntry,
) {
println!("lookup(parent={}, name={})", parent, name.display());
match self
.files
.lock()
.unwrap()
.iter()
.find(|item| item.1.parent_inode == parent && item.1.name == name.to_str().unwrap())
{
Some(item) => {
let ttl = Duration::new(1, 0);
let attr = LocalOrigin::file_to_attr(item.1);
reply.entry(&ttl, &attr, Generation(0));
}
None => reply.error(Errno::ENOENT),
}
fn watcher(&self) -> Box<dyn FileWatcher> {
return Box::new(watcher::LocalOriginFileWatcher::new(
self.source.clone(),
self.destination.clone(),
));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::item::Item;
use crate::music::metadata::MusicMetadata;
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
pub struct LocalByteSource;
#[test]
fn file_to_attr_uses_real_size_for_non_flac() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let item = Item::new(
INodeNo(42),
INodeNo::ROOT,
"test".to_string(),
tmp.path().to_path_buf(),
PathBuf::from("test"),
FileType::File,
metadata.clone(),
None,
);
let attr = LocalOrigin::file_to_attr(&item);
assert_eq!(attr.size, metadata.size());
}
#[test]
fn file_to_attr_uses_virtual_size_for_flac() {
let tmp = tempfile::tempdir().unwrap();
let test_file = tmp.path().join("test.flac");
std::fs::write(&test_file, vec![0u8; 1000]).unwrap();
let metadata = std::fs::metadata(&test_file).unwrap();
let mm = MusicMetadata {
real_audio_start: 500,
header: vec![0u8; 100],
picture_data_ranges: vec![(0, 50)],
..MusicMetadata::default()
};
let item = Item::new(
INodeNo(43),
INodeNo::ROOT,
"test_flac".to_string(),
test_file.clone(),
PathBuf::from("test_flac"),
FileType::File,
metadata.clone(),
Some(mm.clone()),
);
let attr = LocalOrigin::file_to_attr(&item);
let expected_virtual_size = mm.virtual_size(metadata.size());
assert_eq!(attr.size, expected_virtual_size);
}
#[test]
fn file_to_attr_kind_directory() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let item = Item::new(
INodeNo(44),
INodeNo::ROOT,
"test_dir".to_string(),
tmp.path().to_path_buf(),
PathBuf::from("test_dir"),
FileType::Directory,
metadata,
None,
);
let attr = LocalOrigin::file_to_attr(&item);
assert_eq!(attr.kind, fuser::FileType::Directory);
}
#[test]
fn file_to_attr_kind_file() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let item = Item::new(
INodeNo(45),
INodeNo::ROOT,
"test_file".to_string(),
tmp.path().to_path_buf(),
PathBuf::from("test_file"),
FileType::File,
metadata,
None,
);
let attr = LocalOrigin::file_to_attr(&item);
assert_eq!(attr.kind, fuser::FileType::RegularFile);
impl ByteSource for LocalByteSource {
fn read_at(&self, locator: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
return file_io::read_bytes_at(locator, offset, len);
}
}
+3 -2
View File
@@ -11,6 +11,7 @@ use std::os::unix::fs::MetadataExt;
use crate::item::{FileType, Item};
use crate::music::encoder::MusicMetadataEncoderFactory;
use crate::music::parser::MusicMetadataParserFactory;
use crate::origins::attrs::FileAttrs;
use crate::virtual_dirs::ensure_virtual_dirs;
pub fn fill_fileset(map: &Arc<Mutex<BTreeMap<INodeNo, Item>>>, source: &Path, destination: &Path) {
@@ -46,7 +47,7 @@ pub fn build_snapshot(
source.to_path_buf(),
destination.to_path_buf(),
FileType::Directory,
fs::metadata(source)?,
FileAttrs::from(&fs::metadata(source)?),
None,
);
map.insert(INodeNo::ROOT, local_root);
@@ -104,7 +105,7 @@ pub fn read_into_map(
item_path,
local_path,
FileType::File,
metadata,
FileAttrs::from(&metadata),
music_metadata,
);
+23 -7
View File
@@ -1,16 +1,32 @@
use std::{sync::mpsc, thread};
use std::{
collections::BTreeMap,
path::PathBuf,
sync::{Arc, Mutex, mpsc},
thread,
};
use fuser::INodeNo;
use notify::{Event, EventKind, RecursiveMode, Watcher};
pub trait FileWatcher {
fn watch(&self);
use crate::item::Item;
use crate::origins::FileWatcher;
pub struct LocalOriginFileWatcher {
source: PathBuf,
destination: PathBuf,
}
use super::LocalOrigin;
impl LocalOriginFileWatcher {
pub fn new(source: PathBuf, destination: PathBuf) -> Self {
return LocalOriginFileWatcher {
source,
destination,
};
}
}
impl FileWatcher for LocalOrigin {
fn watch(&self) {
let files = self.files.clone();
impl FileWatcher for LocalOriginFileWatcher {
fn watch(&self, files: Arc<Mutex<BTreeMap<INodeNo, Item>>>) {
let source = self.source.clone();
let destination = self.destination.clone();
+533
View File
@@ -1 +1,534 @@
pub mod attrs;
pub mod local;
use std::{
collections::BTreeMap,
io,
path::Path,
sync::{Arc, Mutex},
time::Duration,
};
use fuser::{Errno, FileAttr, Filesystem, Generation, INodeNo, Request};
use sea_orm::{ActiveModelTrait, DatabaseConnection};
use crate::db::sync::run_db_blocking;
use crate::item::{FileType, Item};
use crate::music::db::save_music_metadata;
use crate::origins::local::file_io;
use crate::virtual_dirs::parent_inode_from_path;
/// Transport-agnostic byte-range reader. `locator` is whatever string the
/// origin treats as a file key: a filesystem path for `LocalOrigin`, a remote
/// key for the future network origin. Both produce bytes at `(offset, len)`.
pub trait ByteSource: Send + Sync {
fn read_at(&self, locator: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>>;
}
pub trait FileWatcher: Send + Sync {
fn watch(&self, files: Arc<Mutex<BTreeMap<INodeNo, Item>>>);
}
pub trait Origin: Send + Sync {
fn snapshot(&self) -> io::Result<BTreeMap<INodeNo, Item>>;
fn byte_source(&self) -> Arc<dyn ByteSource>;
fn watcher(&self) -> Box<dyn FileWatcher>;
}
pub struct FuseFs {
pub files: Arc<Mutex<BTreeMap<INodeNo, Item>>>,
pub bytes: Arc<dyn ByteSource>,
pub client: DatabaseConnection,
}
pub(crate) fn file_to_attr(item: &Item) -> FileAttr {
let attrs = &item.attrs;
let kind = match item.file_type {
FileType::Directory => fuser::FileType::Directory,
FileType::File => fuser::FileType::RegularFile,
};
let size = match &item.music_metadata {
Some(mm) if !mm.header.is_empty() => mm.virtual_size(attrs.size),
_ => attrs.size,
};
return FileAttr {
ino: item.inode,
size,
blocks: attrs.blocks,
atime: attrs.atime,
mtime: attrs.mtime,
ctime: attrs.ctime,
crtime: attrs.crtime,
kind,
perm: attrs.perm,
nlink: attrs.nlink,
uid: attrs.uid,
gid: attrs.gid,
rdev: attrs.rdev,
blksize: attrs.blksize,
flags: 0,
};
}
impl Filesystem for FuseFs {
fn open(
&self,
_req: &Request,
ino: INodeNo,
_flags: fuser::OpenFlags,
reply: fuser::ReplyOpen,
) {
if self.files.lock().unwrap().contains_key(&ino) {
reply.opened(fuser::FileHandle(ino.0), fuser::FopenFlags::empty());
} else {
reply.error(Errno::ENOENT);
}
}
fn setattr(
&self,
_req: &Request,
ino: INodeNo,
_mode: Option<u32>,
_uid: Option<u32>,
_gid: Option<u32>,
_size: Option<u64>,
_atime: Option<fuser::TimeOrNow>,
_mtime: Option<fuser::TimeOrNow>,
_ctime: Option<std::time::SystemTime>,
_fh: Option<fuser::FileHandle>,
_crtime: Option<std::time::SystemTime>,
_chgtime: Option<std::time::SystemTime>,
_bkuptime: Option<std::time::SystemTime>,
_flags: Option<fuser::BsdFileFlags>,
reply: fuser::ReplyAttr,
) {
match self.files.lock().unwrap().get(&ino) {
Some(file) => reply.attr(&Duration::new(1, 0), &file_to_attr(file)),
None => reply.error(Errno::ENOENT),
}
}
fn write(
&self,
_req: &Request,
ino: INodeNo,
_fh: fuser::FileHandle,
offset: u64,
data: &[u8],
_write_flags: fuser::WriteFlags,
_flags: fuser::OpenFlags,
_lock_owner: Option<fuser::LockOwner>,
reply: fuser::ReplyWrite,
) {
let written = data.len() as u32;
let write_start = offset;
let write_end = write_start + data.len() as u64;
let updated_music_metadata = {
let mut files = self.files.lock().unwrap();
let item = match files.get_mut(&ino) {
Some(item) => item,
None => {
reply.written(written);
return;
}
};
match &mut item.music_metadata {
Some(mm) if mm.vorbis_comment_length > 0 => {
let vc_data_offset = mm.vorbis_comment_offset;
let vc_hdr_offset = vc_data_offset - 4;
if write_start <= vc_hdr_offset && write_end >= vc_data_offset {
let hdr_from = (vc_hdr_offset - write_start) as usize;
let new_length = u32::from_be_bytes([
0,
data[hdr_from + 1],
data[hdr_from + 2],
data[hdr_from + 3],
]) as u64;
let vc_data_end = vc_data_offset + new_length;
if write_end >= vc_data_end {
let from = (vc_data_offset - write_start) as usize;
let to = (vc_data_end - write_start) as usize;
mm.update_from_vorbis_comment_data(&data[from..to]);
Some(mm.clone())
} else {
None
}
} else {
None
}
}
Some(mm)
if !mm.header.is_empty()
&& write_start == 0
&& data.len() >= 3
&& &data[0..3] == b"ID3" =>
{
mm.update_from_id3_data(data);
Some(mm.clone())
}
Some(mm) if !mm.header.is_empty() && data.len() == 128 && &data[0..3] == b"TAG" => {
mm.update_from_id3v1_data(data);
Some(mm.clone())
}
_ => None,
}
};
if let Some(music_metadata) = updated_music_metadata {
let client = self.client.clone();
let ino_i64 = ino.0 as i64;
run_db_blocking(async move {
save_music_metadata(ino_i64, &music_metadata, &client)
.await
.unwrap();
});
}
reply.written(written);
}
fn getattr(
&self,
_req: &fuser::Request,
ino: INodeNo,
_fh: Option<fuser::FileHandle>,
reply: fuser::ReplyAttr,
) {
println!("getattr(ino={})", ino);
match self.files.lock().unwrap().get(&ino) {
Some(file) => {
let ttl = Duration::new(1, 0);
let attr = file_to_attr(file);
reply.attr(&ttl, &attr);
}
None => reply.error(Errno::ENOENT),
}
}
fn readdir(
&self,
_req: &fuser::Request,
ino: INodeNo,
fh: fuser::FileHandle,
offset: u64,
mut reply: fuser::ReplyDirectory,
) {
println!("readdir(ino={}, fh={}, offset={})", ino, fh, offset);
let files = self.files.lock().unwrap();
let parent_inode = match files.get(&ino) {
Some(dir) => dir.parent_inode,
None => {
reply.error(Errno::ENOENT);
return;
}
};
if offset < 1 {
if reply.add(ino, 1, fuser::FileType::Directory, ".") {
reply.ok();
return;
}
}
if offset < 2 {
if reply.add(parent_inode, 2, fuser::FileType::Directory, "..") {
reply.ok();
return;
}
}
let skip_count = if offset <= 2 {
0
} else {
(offset - 2) as usize
};
for (i, (key, value)) in files
.iter()
.filter(|(_, v)| v.parent_inode == ino && v.inode != ino)
.skip(skip_count)
.enumerate()
{
let entry_offset = (skip_count + i + 3) as u64;
let file_type = match value.file_type {
FileType::Directory => fuser::FileType::Directory,
FileType::File => fuser::FileType::RegularFile,
};
if reply.add(*key, entry_offset, file_type, &value.name) {
break;
}
}
reply.ok();
}
fn rename(
&self,
_req: &Request,
parent: INodeNo,
name: &std::ffi::OsStr,
newparent: INodeNo,
newname: &std::ffi::OsStr,
_flags: fuser::RenameFlags,
reply: fuser::ReplyEmpty,
) {
let name_str = match name.to_str() {
Some(s) => s,
None => {
reply.error(Errno::EINVAL);
return;
}
};
let newname_str = match newname.to_str() {
Some(s) => s,
None => {
reply.error(Errno::EINVAL);
return;
}
};
let mut files = self.files.lock().unwrap();
let item_ino = match files
.iter()
.find(|(_, v)| v.parent_inode == parent && v.name == name_str)
{
Some((ino, _)) => *ino,
None => {
reply.error(Errno::ENOENT);
return;
}
};
let new_local_path = if newparent == INodeNo::ROOT {
std::path::PathBuf::from(newname_str)
} else {
match files.get(&newparent) {
Some(dir) => dir.local_path.join(newname_str),
None => {
reply.error(Errno::ENOENT);
return;
}
}
};
let new_parent_inode = parent_inode_from_path(&new_local_path);
let item = files.get_mut(&item_ino).unwrap();
item.name = newname_str.to_string();
item.local_path = new_local_path.clone();
item.parent_inode = new_parent_inode;
let inode_i64 = item_ino.0 as i64;
let new_name_owned = newname_str.to_string();
let new_local_path_str = new_local_path.to_string_lossy().into_owned();
let client = self.client.clone();
drop(files);
run_db_blocking(async move {
use sea_orm::ActiveValue::Set;
crate::db::entities::ActiveModel {
inode: Set(inode_i64),
name: Set(new_name_owned),
local_path: Set(new_local_path_str),
..Default::default()
}
.update(&client)
.await
.unwrap();
});
reply.ok();
}
fn read(
&self,
_req: &Request,
ino: INodeNo,
_fh: fuser::FileHandle,
offset: u64,
size: u32,
_flags: fuser::OpenFlags,
_lock_owner: Option<fuser::LockOwner>,
reply: fuser::ReplyData,
) {
let (locator, flac) = {
let files = self.files.lock().unwrap();
let item = match files.get(&ino) {
Some(f) => f,
None => {
reply.error(Errno::ENOENT);
return;
}
};
let locator = item.original_path.clone();
let flac = item
.music_metadata
.as_ref()
.filter(|mm| !mm.header.is_empty())
.map(|mm| {
(
mm.header.clone(),
mm.picture_block_headers.clone(),
mm.picture_data_ranges.clone(),
mm.real_audio_start,
)
});
(locator, flac)
};
let Some((header, pic_hdrs, pic_ranges, real_audio_start)) = flac else {
match self.bytes.read_at(&locator, offset, size as usize) {
Ok(bytes) => reply.data(&bytes),
Err(_) => reply.error(Errno::EIO),
}
return;
};
let bytes = &self.bytes;
let reader = |off: u64, len: usize| bytes.read_at(&locator, off, len);
match file_io::assemble_flac_read(
&reader,
&header,
&pic_hdrs,
&pic_ranges,
real_audio_start,
offset,
size,
) {
Ok(bytes) => reply.data(&bytes),
Err(_) => reply.error(Errno::EIO),
}
}
fn lookup(
&self,
_req: &Request,
parent: INodeNo,
name: &std::ffi::OsStr,
reply: fuser::ReplyEntry,
) {
println!("lookup(parent={}, name={})", parent, name.display());
match self
.files
.lock()
.unwrap()
.iter()
.find(|item| item.1.parent_inode == parent && item.1.name == name.to_str().unwrap())
{
Some(item) => {
let ttl = Duration::new(1, 0);
let attr = file_to_attr(item.1);
reply.entry(&ttl, &attr, Generation(0));
}
None => reply.error(Errno::ENOENT),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::item::FileType;
use crate::music::metadata::MusicMetadata;
use crate::origins::attrs::FileAttrs;
use fuser::INodeNo;
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
#[test]
fn file_to_attr_uses_real_size_for_non_flac() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let attrs = FileAttrs::from(&metadata);
let item = Item::new(
INodeNo(42),
INodeNo::ROOT,
"test".to_string(),
tmp.path().to_path_buf(),
PathBuf::from("test"),
FileType::File,
attrs.clone(),
None,
);
let attr = file_to_attr(&item);
assert_eq!(attr.size, metadata.size());
}
#[test]
fn file_to_attr_uses_virtual_size_for_flac() {
let tmp = tempfile::tempdir().unwrap();
let test_file = tmp.path().join("test.flac");
std::fs::write(&test_file, vec![0u8; 1000]).unwrap();
let metadata = std::fs::metadata(&test_file).unwrap();
let attrs = FileAttrs::from(&metadata);
let mm = MusicMetadata {
real_audio_start: 500,
header: vec![0u8; 100],
picture_data_ranges: vec![(0, 50)],
..MusicMetadata::default()
};
let item = Item::new(
INodeNo(43),
INodeNo::ROOT,
"test_flac".to_string(),
test_file.clone(),
PathBuf::from("test_flac"),
FileType::File,
attrs.clone(),
Some(mm.clone()),
);
let attr = file_to_attr(&item);
let expected_virtual_size = mm.virtual_size(attrs.size);
assert_eq!(attr.size, expected_virtual_size);
}
#[test]
fn file_to_attr_kind_directory() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let item = Item::new(
INodeNo(44),
INodeNo::ROOT,
"test_dir".to_string(),
tmp.path().to_path_buf(),
PathBuf::from("test_dir"),
FileType::Directory,
FileAttrs::from(&metadata),
None,
);
let attr = file_to_attr(&item);
assert_eq!(attr.kind, fuser::FileType::Directory);
}
#[test]
fn file_to_attr_kind_file() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let item = Item::new(
INodeNo(45),
INodeNo::ROOT,
"test_file".to_string(),
tmp.path().to_path_buf(),
PathBuf::from("test_file"),
FileType::File,
FileAttrs::from(&metadata),
None,
);
let attr = file_to_attr(&item);
assert_eq!(attr.kind, fuser::FileType::RegularFile);
}
}
+3 -2
View File
@@ -10,6 +10,7 @@ use twox_hash::XxHash64;
use crate::db::entities::Model;
use crate::item::{FileType, Item};
use crate::origins::attrs::FileAttrs;
pub fn virtual_inode(path: &str) -> INodeNo {
let mut hasher = XxHash64::with_seed(5678);
@@ -71,7 +72,7 @@ pub fn ensure_virtual_dirs(
source.to_path_buf(),
PathBuf::from(&current_path),
FileType::Directory,
fs::metadata(source).unwrap(),
FileAttrs::from(&fs::metadata(source).unwrap()),
None,
);
map.insert(virt_ino, virt_item);
@@ -227,7 +228,7 @@ mod tests {
source.to_path_buf(),
PathBuf::from("real_file"),
FileType::File,
fs::metadata(source).unwrap(),
FileAttrs::from(&fs::metadata(source).unwrap()),
None,
);
snapshot.insert(real_ino, real_item);
+15 -7
View File
@@ -1,6 +1,11 @@
use std::io::Write;
use std::path::PathBuf;
use musicfs::origins::local::file_io::assemble_flac_read;
use musicfs::origins::local::file_io::{assemble_flac_read, read_bytes_at};
fn file_reader(path: PathBuf) -> impl Fn(u64, usize) -> std::io::Result<Vec<u8>> {
return move |offset, len| read_bytes_at(&path, offset, len);
}
#[test]
fn assemble_flac_read_picture_header_region() {
@@ -8,15 +13,16 @@ fn assemble_flac_read_picture_header_region() {
let data: Vec<u8> = (0..100).collect();
f.write_all(&data).unwrap();
f.flush().unwrap();
let path = f.path();
let path = f.path().to_path_buf();
let header = b"ABCD";
let pic_hdr = vec![0x86u8, 0x00, 0x00, 0x05];
let pic_ranges = [(10u64, 5u64)];
let real_audio_start = 50u64;
let reader = file_reader(path);
let result = assemble_flac_read(
path,
&reader,
header,
&[pic_hdr.clone()],
&pic_ranges,
@@ -35,15 +41,16 @@ fn assemble_flac_read_picture_data_region() {
let data: Vec<u8> = (0..100).collect();
f.write_all(&data).unwrap();
f.flush().unwrap();
let path = f.path();
let path = f.path().to_path_buf();
let header = b"ABCD";
let pic_hdr = vec![0x86u8, 0x00, 0x00, 0x05];
let pic_ranges = [(10u64, 5u64)];
let real_audio_start = 50u64;
let reader = file_reader(path);
let result = assemble_flac_read(
path,
&reader,
header,
&[pic_hdr.clone()],
&pic_ranges,
@@ -62,12 +69,13 @@ fn assemble_flac_read_full_virtual_layout() {
let data: Vec<u8> = (0..200).map(|i| i as u8).collect();
f.write_all(&data).unwrap();
f.flush().unwrap();
let path = f.path();
let path = f.path().to_path_buf();
let header = vec![0xAA; 8];
let real_audio_start = 100u64;
let result = assemble_flac_read(path, &header, &[], &[], real_audio_start, 0, 18).unwrap();
let reader = file_reader(path);
let result = assemble_flac_read(&reader, &header, &[], &[], real_audio_start, 0, 18).unwrap();
let mut expected = vec![0xAA; 8];
expected.extend_from_slice(&(100..110).map(|i| i as u8).collect::<Vec<u8>>());
+3 -7
View File
@@ -59,25 +59,21 @@ fn build_snapshot_nested_dirs() {
let dest = tempfile::tempdir().unwrap();
fs::create_dir(source.path().join("subdir")).unwrap();
let mut f = fs::File::create(source.path().join("subdir").join("file.txt")).unwrap();
let nested_path = source.path().join("subdir").join("file.txt");
let mut f = fs::File::create(&nested_path).unwrap();
f.write_all(b"nested content").unwrap();
let snapshot = build_snapshot(source.path(), dest.path()).unwrap();
assert!(snapshot.contains_key(&INodeNo::ROOT));
let subdir_entry = snapshot
.values()
.find(|item| item.name == "subdir" && item.file_type == FileType::Directory)
.expect("subdir not found");
let file_entry = snapshot
.values()
.find(|item| item.name == "file.txt" && item.file_type == FileType::File)
.expect("file.txt not found");
assert_eq!(subdir_entry.parent_inode, INodeNo::ROOT);
assert_eq!(file_entry.parent_inode, INodeNo::ROOT);
assert_eq!(file_entry.original_path, nested_path);
}
#[test]