Implement write method to support metadata update

This commit is contained in:
Alexander
2026-06-27 00:16:04 +02:00
parent 9737e26bc9
commit d49018bfc7
2 changed files with 365 additions and 5 deletions
+235
View File
@@ -161,6 +161,121 @@ impl LocalOrigin {
}
}
// 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
@@ -496,6 +611,125 @@ impl LocalOrigin {
}
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,
@@ -627,6 +861,7 @@ impl Filesystem for LocalOrigin {
tokio::runtime::Builder::new_current_thread()
.enable_time()
.enable_io()
.build()
.unwrap()
.block_on(async move {
+130 -5
View File
@@ -1,6 +1,6 @@
use std::{
fs,
io::{Read, Seek, SeekFrom},
io::{Cursor, Read, Seek, SeekFrom},
path::Path,
};
@@ -22,6 +22,8 @@ pub struct MusicMetadata {
pub picture_block_headers: Vec<[u8; 4]>,
pub picture_data_ranges: Vec<(u64, u64)>,
pub real_audio_start: u64,
pub vorbis_comment_offset: u64,
pub vorbis_comment_length: u64,
}
impl MusicMetadata {
@@ -71,11 +73,53 @@ impl MusicMetadata {
music_metadata.real_audio_start = parsed.audio_start;
music_metadata.picture_block_headers = parsed.picture_block_headers;
music_metadata.picture_data_ranges = parsed.picture_data_ranges;
music_metadata.header = build_flac_header(parsed.other_blocks, &music_metadata);
let (header, vorbis_comment_offset, vorbis_comment_length) =
build_flac_header(parsed.other_blocks, &music_metadata);
music_metadata.header = header;
music_metadata.vorbis_comment_offset = vorbis_comment_offset;
music_metadata.vorbis_comment_length = vorbis_comment_length;
}
Some(music_metadata)
}
pub fn find_vorbis_offsets(&mut self) {
let mut cursor = Cursor::new(&self.header);
let mut magic = [0u8; 4];
if cursor.read_exact(&mut magic).is_err() {
return;
}
loop {
let mut hdr = [0u8; 4];
if cursor.read_exact(&mut hdr).is_err() {
break;
}
let is_last = (hdr[0] & 0x80) != 0;
let block_type = hdr[0] & 0x7f;
let length = u32::from_be_bytes([0, hdr[1], hdr[2], hdr[3]]) as u64;
if block_type == 4 {
self.vorbis_comment_offset = cursor.position();
self.vorbis_comment_length = length;
return;
}
if cursor.seek(SeekFrom::Current(length as i64)).is_err() {
break;
}
if is_last {
break;
}
}
}
pub fn update_from_vorbis_comment_data(&mut self, data: &[u8]) {
parse_vorbis_comment_block(data, self);
let other_blocks = extract_non_vorbis_blocks(&self.header);
let (header, vorbis_comment_offset, vorbis_comment_length) =
build_flac_header(other_blocks, self);
self.header = header;
self.vorbis_comment_offset = vorbis_comment_offset;
self.vorbis_comment_length = vorbis_comment_length;
}
}
struct FlacParsed {
@@ -140,22 +184,30 @@ fn parse_flac(path: &Path) -> Option<FlacParsed> {
})
}
fn build_flac_header(blocks: Vec<(u8, Vec<u8>)>, metadata: &MusicMetadata) -> Vec<u8> {
const PADDING_SIZE: usize = 8192;
fn build_flac_header(blocks: Vec<(u8, Vec<u8>)>, metadata: &MusicMetadata) -> (Vec<u8>, u64, u64) {
let vorbis = build_vorbis_comment(metadata);
let vorbis_len = vorbis.len() as u64;
let mut patched: Vec<(u8, Vec<u8>)> = blocks;
patched.push((4, vorbis));
patched.push((1, vec![0u8; PADDING_SIZE])); // PADDING — allows metaflac in-place writes
let vorbis_idx = patched.len() - 2;
let has_pictures = !metadata.picture_data_ranges.is_empty();
let mut out = Vec::new();
out.extend_from_slice(b"fLaC");
let last = patched.len() - 1;
let mut vorbis_offset = 0u64;
for (i, (block_type, data)) in patched.iter().enumerate() {
// is_last only if this is the final block AND no picture blocks follow
let is_last_block = i == last && !has_pictures;
let flag: u8 = if is_last_block { 0x80 } else { 0x00 };
let length = data.len() as u32;
if i == vorbis_idx {
vorbis_offset = out.len() as u64 + 4; // data starts after 4-byte block header
}
out.push(flag | block_type);
out.push((length >> 16) as u8);
out.push((length >> 8) as u8);
@@ -163,7 +215,80 @@ fn build_flac_header(blocks: Vec<(u8, Vec<u8>)>, metadata: &MusicMetadata) -> Ve
out.extend_from_slice(data);
}
out
(out, vorbis_offset, vorbis_len)
}
fn parse_vorbis_comment_block(data: &[u8], out: &mut MusicMetadata) {
let mut cursor = Cursor::new(data);
let mut len_bytes = [0u8; 4];
if cursor.read_exact(&mut len_bytes).is_err() {
return;
}
let vendor_len = u32::from_le_bytes(len_bytes) as i64;
if cursor.seek(SeekFrom::Current(vendor_len)).is_err() {
return;
}
if cursor.read_exact(&mut len_bytes).is_err() {
return;
}
let count = u32::from_le_bytes(len_bytes);
out.artist.clear();
out.other_tags.clear();
for _ in 0..count {
if cursor.read_exact(&mut len_bytes).is_err() {
break;
}
let comment_len = u32::from_le_bytes(len_bytes) as usize;
let mut comment_bytes = vec![0u8; comment_len];
if cursor.read_exact(&mut comment_bytes).is_err() {
break;
}
let comment = String::from_utf8_lossy(&comment_bytes).into_owned();
if let Some((key, value)) = comment.split_once('=') {
match key.to_ascii_uppercase().as_str() {
"TITLE" => out.track_title = value.to_string(),
"ALBUM" => out.album = value.to_string(),
"TRACKNUMBER" => out.track_number = value.parse().unwrap_or(0),
"ARTIST" => out.artist.push(value.to_string()),
_ => out.other_tags.push(comment),
}
}
}
}
fn extract_non_vorbis_blocks(header: &[u8]) -> Vec<(u8, Vec<u8>)> {
let mut cursor = Cursor::new(header);
let mut blocks = vec![];
let mut magic = [0u8; 4];
if cursor.read_exact(&mut magic).is_err() {
return blocks;
}
loop {
let mut hdr = [0u8; 4];
if cursor.read_exact(&mut hdr).is_err() {
break;
}
let is_last = (hdr[0] & 0x80) != 0;
let block_type = hdr[0] & 0x7f;
let length = u32::from_be_bytes([0, hdr[1], hdr[2], hdr[3]]) as usize;
let mut data = vec![0u8; length];
if cursor.read_exact(&mut data).is_err() {
break;
}
if block_type != 4 && block_type != 1 {
blocks.push((block_type, data));
}
if is_last {
break;
}
}
blocks
}
pub mod db {