1136 lines
37 KiB
Rust
1136 lines
37 KiB
Rust
use std::{
|
|
collections::BTreeMap,
|
|
fs,
|
|
hash::Hasher,
|
|
io::{self, Read, Seek, SeekFrom},
|
|
os::unix::{
|
|
ffi::OsStrExt,
|
|
fs::{MetadataExt, PermissionsExt},
|
|
},
|
|
path::{Path, PathBuf},
|
|
sync::{Arc, Mutex, mpsc},
|
|
thread,
|
|
time::{Duration, SystemTime},
|
|
};
|
|
|
|
use fuser::{Errno, FileAttr, Filesystem, Generation, INodeNo, Request};
|
|
use notify::{Event, EventKind, RecursiveMode, Watcher};
|
|
use sea_orm::entity::prelude::*;
|
|
use twox_hash::XxHash64;
|
|
|
|
use crate::file_watcher::FileWatcher;
|
|
use crate::music_metadata::MusicMetadata;
|
|
use crate::music_metadata::db::{
|
|
artists, music_metadata as music_metadata_entity, other_tags, pictures,
|
|
};
|
|
|
|
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
|
enum FileType {
|
|
Directory,
|
|
File,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct LocalItem {
|
|
inode: INodeNo,
|
|
parent_inode: INodeNo,
|
|
name: String,
|
|
original_path: PathBuf,
|
|
local_path: PathBuf,
|
|
file_type: FileType,
|
|
metadata: fs::Metadata,
|
|
music_metadata: Option<MusicMetadata>,
|
|
hash: u64,
|
|
}
|
|
|
|
impl LocalItem {
|
|
fn compute_hash(&self) -> u64 {
|
|
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);
|
|
|
|
return hasher.finish();
|
|
}
|
|
}
|
|
|
|
pub struct LocalOrigin {
|
|
source: PathBuf,
|
|
destination: PathBuf,
|
|
files: Arc<Mutex<BTreeMap<INodeNo, LocalItem>>>,
|
|
|
|
client: sea_orm::DatabaseConnection,
|
|
}
|
|
|
|
impl std::fmt::Debug for LocalOrigin {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.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 =
|
|
LocalOrigin::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();
|
|
|
|
let mut to_insert: Vec<ActiveModel> = vec![];
|
|
let mut to_update: Vec<ActiveModel> = vec![];
|
|
let mut to_delete: Vec<i64> = vec![];
|
|
let mut to_save_music: Vec<i64> = vec![];
|
|
|
|
for (ino, item) in &snapshot {
|
|
let ino_i64 = ino.0 as i64;
|
|
match db_items.get(&ino_i64) {
|
|
None => {
|
|
to_insert.push(ActiveModel::from(item));
|
|
if item.music_metadata.is_some() {
|
|
to_save_music.push(ino_i64);
|
|
}
|
|
}
|
|
Some(db_item) if db_item.hash != item.hash as i64 => {
|
|
to_update.push(ActiveModel::from(item));
|
|
if item.music_metadata.is_some() {
|
|
to_save_music.push(ino_i64);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
let fresh_inodes: std::collections::HashSet<i64> =
|
|
snapshot.keys().map(|i| i.0 as i64).collect();
|
|
for ino in db_items.keys().filter(|i| !fresh_inodes.contains(i)) {
|
|
to_delete.push(*ino);
|
|
}
|
|
|
|
if !to_insert.is_empty() {
|
|
Entity::insert_many(to_insert).exec(&client).await.unwrap();
|
|
}
|
|
for model in to_update {
|
|
model.update(&client).await.unwrap();
|
|
}
|
|
for ino in to_delete {
|
|
Entity::delete_by_id(ino).exec(&client).await.unwrap();
|
|
}
|
|
|
|
for ino_i64 in &to_save_music {
|
|
let ino = INodeNo(*ino_i64 as u64);
|
|
if let Some(music_metadata) = snapshot
|
|
.get(&ino)
|
|
.and_then(|item| item.music_metadata.as_ref())
|
|
{
|
|
LocalOrigin::save_music_metadata(*ino_i64, music_metadata, &client)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
// For unchanged FLAC items, restore music_metadata from DB
|
|
// (overrides the fresh FLAC parse so that writes made in a previous session survive restarts)
|
|
let unchanged_music_inodes: Vec<i64> = db_items
|
|
.values()
|
|
.filter_map(|db_item| {
|
|
let ino = INodeNo(db_item.inode as u64);
|
|
snapshot
|
|
.get(&ino)
|
|
.filter(|item| {
|
|
item.hash as i64 == db_item.hash && item.music_metadata.is_some()
|
|
})
|
|
.map(|_| db_item.inode)
|
|
})
|
|
.collect();
|
|
|
|
if !unchanged_music_inodes.is_empty() {
|
|
use artists::Column as ArtCol;
|
|
use music_metadata_entity::Column as MmCol;
|
|
use other_tags::Column as OtCol;
|
|
use pictures::Column as PicCol;
|
|
|
|
let mm_rows: std::collections::HashMap<i64, music_metadata_entity::Model> =
|
|
music_metadata_entity::Entity::find()
|
|
.filter(MmCol::Inode.is_in(unchanged_music_inodes.clone()))
|
|
.all(&client)
|
|
.await
|
|
.unwrap()
|
|
.into_iter()
|
|
.map(|m| (m.inode, m))
|
|
.collect();
|
|
|
|
let mut artists_by_inode: std::collections::HashMap<i64, Vec<String>> =
|
|
std::collections::HashMap::new();
|
|
for row in artists::Entity::find()
|
|
.filter(ArtCol::Inode.is_in(unchanged_music_inodes.clone()))
|
|
.all(&client)
|
|
.await
|
|
.unwrap()
|
|
{
|
|
artists_by_inode
|
|
.entry(row.inode)
|
|
.or_default()
|
|
.push(row.artist);
|
|
}
|
|
|
|
let mut other_tags_by_inode: std::collections::HashMap<i64, Vec<(i32, String)>> =
|
|
std::collections::HashMap::new();
|
|
for row in other_tags::Entity::find()
|
|
.filter(OtCol::Inode.is_in(unchanged_music_inodes.clone()))
|
|
.all(&client)
|
|
.await
|
|
.unwrap()
|
|
{
|
|
other_tags_by_inode
|
|
.entry(row.inode)
|
|
.or_default()
|
|
.push((row.position, row.tag));
|
|
}
|
|
|
|
let mut pictures_by_inode: std::collections::HashMap<i64, Vec<pictures::Model>> =
|
|
std::collections::HashMap::new();
|
|
for row in pictures::Entity::find()
|
|
.filter(PicCol::Inode.is_in(unchanged_music_inodes))
|
|
.all(&client)
|
|
.await
|
|
.unwrap()
|
|
{
|
|
pictures_by_inode.entry(row.inode).or_default().push(row);
|
|
}
|
|
|
|
for (inode, mm_row) in mm_rows {
|
|
let ino = INodeNo(inode as u64);
|
|
let Some(item) = snapshot.get_mut(&ino) else {
|
|
continue;
|
|
};
|
|
|
|
let mut sorted_tags = other_tags_by_inode.remove(&inode).unwrap_or_default();
|
|
sorted_tags.sort_by_key(|(pos, _)| *pos);
|
|
|
|
let mut sorted_pics = pictures_by_inode.remove(&inode).unwrap_or_default();
|
|
sorted_pics.sort_by_key(|p| p.position);
|
|
|
|
let picture_block_headers: Vec<[u8; 4]> = sorted_pics
|
|
.iter()
|
|
.map(|p| {
|
|
let mut hdr = [0u8; 4];
|
|
let len = p.block_header.len().min(4);
|
|
hdr[..len].copy_from_slice(&p.block_header[..len]);
|
|
hdr
|
|
})
|
|
.collect();
|
|
|
|
let picture_data_ranges: Vec<(u64, u64)> = sorted_pics
|
|
.iter()
|
|
.map(|p| (p.data_offset as u64, p.data_length as u64))
|
|
.collect();
|
|
|
|
let mut music_metadata = MusicMetadata {
|
|
artist: artists_by_inode.remove(&inode).unwrap_or_default(),
|
|
album: mm_row.album,
|
|
track_number: mm_row.track_number,
|
|
track_title: mm_row.track_title,
|
|
other_tags: sorted_tags.into_iter().map(|(_, tag)| tag).collect(),
|
|
header: mm_row.header,
|
|
picture_block_headers,
|
|
picture_data_ranges,
|
|
real_audio_start: mm_row.real_audio_start as u64,
|
|
vorbis_comment_offset: 0,
|
|
vorbis_comment_length: 0,
|
|
};
|
|
music_metadata.find_vorbis_offsets();
|
|
item.music_metadata = Some(music_metadata);
|
|
}
|
|
}
|
|
|
|
// For unchanged items (hash matches), restore virtual paths from DB
|
|
// so that renames performed in a previous session are preserved.
|
|
let restorations: Vec<(INodeNo, String, PathBuf)> = db_items
|
|
.values()
|
|
.filter_map(|db_item| {
|
|
let ino = INodeNo(db_item.inode as u64);
|
|
snapshot
|
|
.get(&ino)
|
|
.filter(|item| item.hash as i64 == db_item.hash)
|
|
.map(|_| {
|
|
(
|
|
ino,
|
|
db_item.name.clone(),
|
|
PathBuf::from(&db_item.local_path),
|
|
)
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
for (_, _, local_path) in &restorations {
|
|
LocalOrigin::ensure_virtual_dirs(local_path, Path::new(&source), &mut snapshot);
|
|
}
|
|
for (ino, name, local_path) in restorations {
|
|
if let Some(item) = snapshot.get_mut(&ino) {
|
|
item.name = name;
|
|
item.parent_inode = LocalOrigin::parent_inode_from_path(&local_path);
|
|
item.local_path = local_path;
|
|
}
|
|
}
|
|
|
|
let local_origin = LocalOrigin {
|
|
source: source.into(),
|
|
destination: destination.into(),
|
|
files: Arc::new(Mutex::new(snapshot)),
|
|
client,
|
|
};
|
|
//dbg!(&local_origin);
|
|
Ok(local_origin)
|
|
}
|
|
|
|
async fn save_music_metadata(
|
|
inode: i64,
|
|
music_metadata: &MusicMetadata,
|
|
client: &sea_orm::DatabaseConnection,
|
|
) -> Result<(), sea_orm::DbErr> {
|
|
use sea_orm::ActiveValue::Set;
|
|
|
|
music_metadata_entity::Entity::delete_by_id(inode)
|
|
.exec(client)
|
|
.await?;
|
|
|
|
music_metadata_entity::Entity::insert(music_metadata_entity::ActiveModel {
|
|
inode: Set(inode),
|
|
track_title: Set(music_metadata.track_title.clone()),
|
|
album: Set(music_metadata.album.clone()),
|
|
track_number: Set(music_metadata.track_number),
|
|
header: Set(music_metadata.header.clone()),
|
|
real_audio_start: Set(music_metadata.real_audio_start as i64),
|
|
})
|
|
.exec(client)
|
|
.await?;
|
|
|
|
if !music_metadata.artist.is_empty() {
|
|
artists::Entity::insert_many(music_metadata.artist.iter().map(|a| {
|
|
artists::ActiveModel {
|
|
inode: Set(inode),
|
|
artist: Set(a.clone()),
|
|
}
|
|
}))
|
|
.exec(client)
|
|
.await?;
|
|
}
|
|
|
|
if !music_metadata.other_tags.is_empty() {
|
|
other_tags::Entity::insert_many(music_metadata.other_tags.iter().enumerate().map(
|
|
|(pos, tag)| other_tags::ActiveModel {
|
|
inode: Set(inode),
|
|
position: Set(pos as i32),
|
|
tag: Set(tag.clone()),
|
|
},
|
|
))
|
|
.exec(client)
|
|
.await?;
|
|
}
|
|
|
|
if !music_metadata.picture_block_headers.is_empty() {
|
|
pictures::Entity::insert_many(
|
|
music_metadata
|
|
.picture_block_headers
|
|
.iter()
|
|
.zip(music_metadata.picture_data_ranges.iter())
|
|
.enumerate()
|
|
.map(|(pos, (hdr, (offset, len)))| pictures::ActiveModel {
|
|
inode: Set(inode),
|
|
position: Set(pos as i32),
|
|
block_header: Set(hdr.to_vec()),
|
|
data_offset: Set(*offset as i64),
|
|
data_length: Set(*len as i64),
|
|
}),
|
|
)
|
|
.exec(client)
|
|
.await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn fill_fileset(
|
|
map: &Arc<Mutex<BTreeMap<INodeNo, LocalItem>>>,
|
|
source: &Path,
|
|
destination: &Path,
|
|
) {
|
|
match LocalOrigin::build_snapshot(source, destination) {
|
|
Ok(new_snapshot) => {
|
|
let mut files = map.lock().unwrap();
|
|
|
|
files.retain(|ino, _| new_snapshot.contains_key(ino));
|
|
|
|
for (ino, new_item) in new_snapshot {
|
|
match files.get(&ino) {
|
|
Some(existing) if existing.hash == new_item.hash => {}
|
|
_ => {
|
|
files.insert(ino, new_item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(e) => eprintln!("Error while reading source: {e}"),
|
|
}
|
|
}
|
|
|
|
fn build_snapshot(
|
|
source: &Path,
|
|
destination: &Path,
|
|
) -> Result<BTreeMap<INodeNo, LocalItem>, io::Error> {
|
|
let mut map = BTreeMap::new();
|
|
|
|
let local_root = LocalItem {
|
|
inode: INodeNo::ROOT,
|
|
parent_inode: INodeNo::ROOT,
|
|
name: "/".to_string(),
|
|
original_path: source.to_path_buf(),
|
|
local_path: destination.to_path_buf(),
|
|
file_type: FileType::Directory,
|
|
metadata: fs::metadata(source)?,
|
|
music_metadata: None,
|
|
hash: 0,
|
|
};
|
|
let local_root = LocalItem {
|
|
hash: local_root.compute_hash(),
|
|
..local_root
|
|
};
|
|
map.insert(INodeNo::ROOT, local_root);
|
|
|
|
LocalOrigin::read_into_map(source, destination, &mut map)?;
|
|
Ok(map)
|
|
}
|
|
|
|
fn read_into_map(
|
|
source: &Path,
|
|
destination: &Path,
|
|
map: &mut BTreeMap<INodeNo, LocalItem>,
|
|
) -> Result<(), io::Error> {
|
|
for item in fs::read_dir(source)? {
|
|
let entry = item?;
|
|
let name = entry.file_name().to_string_lossy().into_owned();
|
|
let item_path = entry.path();
|
|
let metadata = entry.metadata()?;
|
|
let file_type = if entry.file_type()?.is_dir() {
|
|
FileType::Directory
|
|
} else {
|
|
FileType::File
|
|
};
|
|
let music_metadata =
|
|
if item_path.extension().and_then(std::ffi::OsStr::to_str) == Some("flac") {
|
|
MusicMetadata::parse_music_metadata(&item_path)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let mut local_path = PathBuf::new();
|
|
if music_metadata.is_some() {
|
|
let music_metadata = music_metadata.clone().unwrap();
|
|
local_path.push(music_metadata.artist.join("-"));
|
|
local_path.push(music_metadata.album);
|
|
}
|
|
local_path.push(name.clone());
|
|
|
|
let inode = INodeNo(metadata.ino());
|
|
let parent_inode = LocalOrigin::ensure_virtual_dirs(&local_path, source, map);
|
|
let local_item = LocalItem {
|
|
inode,
|
|
parent_inode,
|
|
name,
|
|
original_path: item_path.clone(),
|
|
local_path,
|
|
file_type,
|
|
metadata,
|
|
music_metadata,
|
|
hash: 0,
|
|
};
|
|
let local_item = LocalItem {
|
|
hash: local_item.compute_hash(),
|
|
..local_item
|
|
};
|
|
|
|
map.insert(inode, local_item);
|
|
|
|
if file_type == FileType::Directory {
|
|
LocalOrigin::read_into_map(&item_path, destination, map)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn local_file_to_file_attr(local_file: &LocalItem) -> FileAttr {
|
|
let metadata = &local_file.metadata;
|
|
let ctime = std::time::UNIX_EPOCH + Duration::from_secs(metadata.ctime() as u64);
|
|
let atime = metadata.accessed().unwrap_or(SystemTime::UNIX_EPOCH);
|
|
let mtime = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
|
|
let crtime = metadata.created().unwrap_or(SystemTime::UNIX_EPOCH);
|
|
let kind = LocalOrigin::file_type(local_file.file_type);
|
|
|
|
let size = match &local_file.music_metadata {
|
|
Some(mm) if mm.real_audio_start > 0 => mm.virtual_size(metadata.size()),
|
|
_ => metadata.size(),
|
|
};
|
|
|
|
return FileAttr {
|
|
ino: local_file.inode,
|
|
size,
|
|
blocks: metadata.blocks(),
|
|
atime: atime,
|
|
mtime: mtime,
|
|
ctime: ctime,
|
|
crtime: crtime,
|
|
kind: 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, // TODO this is for macos only but perhaps we could get it from metadata still?
|
|
};
|
|
}
|
|
|
|
fn file_type(file_type: FileType) -> fuser::FileType {
|
|
return match file_type {
|
|
FileType::Directory => fuser::FileType::Directory,
|
|
FileType::File => fuser::FileType::RegularFile,
|
|
};
|
|
}
|
|
|
|
fn read_bytes_at(path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
|
|
let mut f = fs::File::open(path)?;
|
|
f.seek(SeekFrom::Start(offset))?;
|
|
let mut buf = vec![0u8; len];
|
|
let n = f.read(&mut buf)?;
|
|
buf.truncate(n);
|
|
Ok(buf)
|
|
}
|
|
|
|
fn parent_inode_from_path(local_path: &Path) -> INodeNo {
|
|
let components: Vec<_> = local_path.components().collect();
|
|
if components.len() <= 1 {
|
|
return INodeNo::ROOT;
|
|
}
|
|
let mut parent_path = String::new();
|
|
for (i, comp) in components[..components.len() - 1].iter().enumerate() {
|
|
if i > 0 {
|
|
parent_path.push('/');
|
|
}
|
|
parent_path.push_str(comp.as_os_str().to_str().unwrap_or_default());
|
|
}
|
|
LocalOrigin::virtual_inode(&parent_path)
|
|
}
|
|
|
|
fn virtual_inode(path: &str) -> INodeNo {
|
|
let mut hasher = XxHash64::with_seed(5678);
|
|
hasher.write(path.as_bytes());
|
|
INodeNo(hasher.finish() | (1u64 << 63))
|
|
}
|
|
|
|
fn ensure_virtual_dirs(
|
|
local_path: &Path,
|
|
source: &Path,
|
|
map: &mut BTreeMap<INodeNo, LocalItem>,
|
|
) -> INodeNo {
|
|
let components: Vec<_> = local_path.components().collect();
|
|
|
|
if components.len() <= 1 {
|
|
return INodeNo::ROOT;
|
|
}
|
|
|
|
let mut current_parent = INodeNo::ROOT;
|
|
let mut current_path = String::new();
|
|
|
|
for component in &components[..components.len() - 1] {
|
|
let comp_str = component.as_os_str().to_str().unwrap_or_default();
|
|
if !current_path.is_empty() {
|
|
current_path.push('/');
|
|
}
|
|
current_path.push_str(comp_str);
|
|
|
|
let virt_ino = LocalOrigin::virtual_inode(¤t_path);
|
|
|
|
if !map.contains_key(&virt_ino) {
|
|
let metadata = fs::metadata(source).unwrap();
|
|
let virt_item = LocalItem {
|
|
inode: virt_ino,
|
|
parent_inode: current_parent,
|
|
name: comp_str.to_string(),
|
|
original_path: source.to_path_buf(),
|
|
local_path: PathBuf::from(¤t_path),
|
|
file_type: FileType::Directory,
|
|
metadata,
|
|
music_metadata: None,
|
|
hash: 0,
|
|
};
|
|
let virt_item = LocalItem {
|
|
hash: virt_item.compute_hash(),
|
|
..virt_item
|
|
};
|
|
map.insert(virt_ino, virt_item);
|
|
}
|
|
|
|
current_parent = virt_ino;
|
|
}
|
|
|
|
return current_parent;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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::local_file_to_file_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;
|
|
}
|
|
};
|
|
let music_metadata = match &mut item.music_metadata {
|
|
Some(mm) if mm.vorbis_comment_length > 0 => mm,
|
|
_ => {
|
|
reply.written(written);
|
|
return;
|
|
}
|
|
};
|
|
|
|
let vc_data_offset = music_metadata.vorbis_comment_offset;
|
|
let vc_hdr_offset = vc_data_offset - 4;
|
|
|
|
// Must cover the 4-byte block header to read the (possibly new) data length
|
|
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;
|
|
music_metadata.update_from_vorbis_comment_data(&data[from..to]);
|
|
Some(music_metadata.clone())
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
|
|
if let Some(music_metadata) = updated_music_metadata {
|
|
let client = self.client.clone();
|
|
let ino_i64 = ino.0 as i64;
|
|
tokio::runtime::Builder::new_current_thread()
|
|
.enable_io()
|
|
.enable_time()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(async move {
|
|
LocalOrigin::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);
|
|
// TODO fix unwrap
|
|
match self.files.lock().unwrap().get(&ino) {
|
|
Some(file) => {
|
|
let ttl = Duration::new(1, 0);
|
|
let attr = LocalOrigin::local_file_to_file_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 == 0 {
|
|
let _ = reply.add(ino, 0, fuser::FileType::Directory, &Path::new("."));
|
|
let _ = reply.add(
|
|
parent_inode,
|
|
1,
|
|
fuser::FileType::Directory,
|
|
&Path::new(".."),
|
|
);
|
|
}
|
|
|
|
for (i, (key, value)) in files
|
|
.iter()
|
|
.filter(|(_, v)| v.parent_inode == ino && v.inode != ino)
|
|
.skip(offset as usize)
|
|
.enumerate()
|
|
{
|
|
let entry_offset: u64 = 2 + i as u64;
|
|
let file_type: fuser::FileType = LocalOrigin::file_type(value.file_type);
|
|
let buffer_full: bool = reply.add(*key, entry_offset, file_type, &value.name);
|
|
if buffer_full {
|
|
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 = LocalOrigin::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);
|
|
|
|
tokio::runtime::Builder::new_current_thread()
|
|
.enable_time()
|
|
.enable_io()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(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 mm = item
|
|
.music_metadata
|
|
.as_ref()
|
|
.filter(|mm| mm.real_audio_start > 0);
|
|
let is_flac = mm.is_some();
|
|
let header = mm.map(|mm| mm.header.clone()).unwrap_or_default();
|
|
let pic_hdrs = mm
|
|
.map(|mm| mm.picture_block_headers.clone())
|
|
.unwrap_or_default();
|
|
let pic_ranges = mm
|
|
.map(|mm| mm.picture_data_ranges.clone())
|
|
.unwrap_or_default();
|
|
let real_audio_start = mm.map(|mm| mm.real_audio_start).unwrap_or(0);
|
|
drop(files);
|
|
|
|
if !is_flac {
|
|
match LocalOrigin::read_bytes_at(&original_path, offset, size as usize) {
|
|
Ok(bytes) => reply.data(&bytes),
|
|
Err(_) => reply.error(Errno::EIO),
|
|
}
|
|
return;
|
|
}
|
|
|
|
let end = offset + size as u64;
|
|
let header_end = header.len() as u64;
|
|
|
|
// Pure header read — most common for tag readers
|
|
if end <= header_end {
|
|
reply.data(&header[offset as usize..end as usize]);
|
|
return;
|
|
}
|
|
|
|
// Multi-region read: walk [header][pic_hdr+pic_data]...[audio] and collect bytes
|
|
let mut buf = Vec::with_capacity(size as usize);
|
|
|
|
if offset < header_end {
|
|
buf.extend_from_slice(&header[offset as usize..]);
|
|
}
|
|
|
|
let mut virt_pos = header_end;
|
|
for (pic_hdr, (data_real_offset, data_len)) in pic_hdrs.iter().zip(pic_ranges.iter()) {
|
|
let pic_hdr_end = virt_pos + 4;
|
|
let pic_end = pic_hdr_end + data_len;
|
|
|
|
if end <= virt_pos {
|
|
break;
|
|
}
|
|
if offset >= pic_end {
|
|
virt_pos = pic_end;
|
|
continue;
|
|
}
|
|
|
|
let hdr_from = (offset.max(virt_pos) - virt_pos) as usize;
|
|
let hdr_to = ((end.min(pic_hdr_end)) - virt_pos) as usize;
|
|
if hdr_from < hdr_to {
|
|
buf.extend_from_slice(&pic_hdr[hdr_from..hdr_to.min(4)]);
|
|
}
|
|
|
|
let data_start = offset.max(pic_hdr_end);
|
|
let data_end = end.min(pic_end);
|
|
if data_start < data_end {
|
|
let real_off = data_real_offset + (data_start - pic_hdr_end);
|
|
match LocalOrigin::read_bytes_at(
|
|
&original_path,
|
|
real_off,
|
|
(data_end - data_start) as usize,
|
|
) {
|
|
Ok(bytes) => buf.extend_from_slice(&bytes),
|
|
Err(_) => {
|
|
reply.error(Errno::EIO);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
virt_pos = pic_end;
|
|
}
|
|
|
|
let audio_start = offset.max(virt_pos);
|
|
if audio_start < end {
|
|
let real_off = real_audio_start + (audio_start - virt_pos);
|
|
match LocalOrigin::read_bytes_at(&original_path, real_off, (end - audio_start) as usize)
|
|
{
|
|
Ok(bytes) => buf.extend_from_slice(&bytes),
|
|
Err(_) => {
|
|
reply.error(Errno::EIO);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
reply.data(&buf);
|
|
}
|
|
|
|
fn lookup(
|
|
&self,
|
|
_req: &Request,
|
|
parent: INodeNo,
|
|
name: &std::ffi::OsStr,
|
|
reply: fuser::ReplyEntry,
|
|
) {
|
|
println!("lookup(parent={}, name={})", parent, name.display());
|
|
|
|
return 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::local_file_to_file_attr(item.1);
|
|
|
|
reply.entry(&ttl, &attr, Generation(0));
|
|
}
|
|
None => reply.error(Errno::ENOENT),
|
|
};
|
|
}
|
|
}
|
|
|
|
impl FileWatcher for LocalOrigin {
|
|
fn watch(&self) {
|
|
let files = self.files.clone();
|
|
let source = self.source.clone();
|
|
let destination = self.destination.clone();
|
|
|
|
println!("Starting to watch the source files in another thread");
|
|
thread::spawn(move || {
|
|
let (tx, rx): (
|
|
mpsc::Sender<Result<Event, notify::Error>>,
|
|
mpsc::Receiver<Result<Event, notify::Error>>,
|
|
) = mpsc::channel();
|
|
|
|
// Use recommended_watcher() to automatically select the best implementation
|
|
// for your platform. The `EventHandler` passed to this constructor can be a
|
|
// closure, a `std::sync::mpsc::Sender`, a `crossbeam_channel::Sender`, or
|
|
// another type the trait is implemented for.
|
|
let mut watcher: notify::INotifyWatcher = notify::recommended_watcher(tx).unwrap();
|
|
|
|
// Add a path to be watched. All files and directories at that path and
|
|
// below will be monitored for changes.
|
|
watcher.watch(&source, RecursiveMode::Recursive).unwrap();
|
|
// Block forever, printing out events as they come in
|
|
for res in rx {
|
|
match res {
|
|
Ok(event) => {
|
|
println!("event: {:?}", event);
|
|
|
|
match event.kind {
|
|
EventKind::Any => {
|
|
println!("Something happened to item, ignoring");
|
|
}
|
|
EventKind::Access(_access_kind) => {
|
|
println!("Item was read");
|
|
}
|
|
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {
|
|
println!("Item was removed");
|
|
LocalOrigin::fill_fileset(&files, &source, &destination);
|
|
}
|
|
EventKind::Other => {
|
|
println!("Some other action happened to item, ignoring");
|
|
}
|
|
}
|
|
}
|
|
Err(e) => println!("watch error: {:?}", e),
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, sea_orm::DeriveEntityModel)]
|
|
#[sea_orm(table_name = "items")]
|
|
pub struct Model {
|
|
#[sea_orm(primary_key, auto_increment = false)]
|
|
pub inode: i64,
|
|
pub name: String,
|
|
pub original_path: String,
|
|
pub local_path: String,
|
|
pub file_type: String,
|
|
pub hash: i64,
|
|
}
|
|
|
|
pub type LocalItemEntity = Model;
|
|
|
|
#[derive(Copy, Clone, Debug, sea_orm::EnumIter, sea_orm::DeriveRelation)]
|
|
pub enum Relation {}
|
|
|
|
impl sea_orm::ActiveModelBehavior for ActiveModel {}
|
|
|
|
impl From<&LocalItem> for ActiveModel {
|
|
fn from(item: &LocalItem) -> Self {
|
|
use sea_orm::ActiveValue::Set;
|
|
ActiveModel {
|
|
inode: Set(item.inode.0 as i64),
|
|
name: Set(item.name.clone()),
|
|
original_path: Set(item.original_path.to_string_lossy().into_owned()),
|
|
local_path: Set(item.local_path.to_string_lossy().into_owned()),
|
|
file_type: Set(match item.file_type {
|
|
FileType::Directory => "directory".to_string(),
|
|
FileType::File => "file".to_string(),
|
|
}),
|
|
hash: Set(item.hash as i64),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<(LocalItemEntity, fs::Metadata)> for LocalItem {
|
|
fn from((entity, metadata): (LocalItemEntity, fs::Metadata)) -> Self {
|
|
let local_path = PathBuf::from(&entity.local_path);
|
|
let parent_inode = LocalOrigin::parent_inode_from_path(&local_path);
|
|
let local_item = LocalItem {
|
|
inode: INodeNo(entity.inode as u64),
|
|
parent_inode,
|
|
name: entity.name,
|
|
original_path: PathBuf::from(entity.original_path),
|
|
local_path,
|
|
file_type: match entity.file_type.as_str() {
|
|
"directory" => FileType::Directory,
|
|
_ => FileType::File,
|
|
},
|
|
metadata,
|
|
music_metadata: None,
|
|
hash: 0,
|
|
};
|
|
LocalItem {
|
|
hash: local_item.compute_hash(),
|
|
..local_item
|
|
}
|
|
}
|
|
}
|