Refactor to split client from server

This commit is contained in:
Alexander
2026-07-01 16:31:55 +02:00
parent d35aa2aecb
commit 8dba0c097d
56 changed files with 441 additions and 326 deletions
Generated
+41 -7
View File
@@ -1700,7 +1700,7 @@ dependencies = [
] ]
[[package]] [[package]]
name = "musicfs" name = "musicfs-client"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
@@ -1712,24 +1712,58 @@ dependencies = [
"http", "http",
"libc", "libc",
"log", "log",
"musicfs-core",
"musicfs-proto",
"notify", "notify",
"prost",
"sea-orm", "sea-orm",
"symphonia",
"tempfile", "tempfile",
"time",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tonic", "tonic",
"tonic-health", "tracing",
"tonic-prost", "twox-hash",
"tonic-reflection", ]
[[package]]
name = "musicfs-core"
version = "0.1.0"
dependencies = [
"symphonia",
"tempfile",
"tracing", "tracing",
"tracing-appender", "tracing-appender",
"tracing-subscriber", "tracing-subscriber",
"twox-hash", "twox-hash",
] ]
[[package]]
name = "musicfs-proto"
version = "0.1.0"
dependencies = [
"prost",
"tonic",
"tonic-prost",
]
[[package]]
name = "musicfs-server"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"clap",
"musicfs-core",
"musicfs-proto",
"notify",
"tempfile",
"tokio",
"tokio-stream",
"tonic",
"tonic-health",
"tonic-reflection",
"tracing",
]
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.29.0" version = "0.29.0"
+35 -41
View File
@@ -1,57 +1,51 @@
[package] [workspace]
name = "musicfs" members = [
"crates/musicfs-proto",
"crates/musicfs-core",
"crates/musicfs-server",
"crates/musicfs-client",
]
resolver = "2"
[workspace.package]
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
# `cargo run` (and devenv's `cargo run -- ...`) picks the FUSE binary, not
# musicfs-server, when two [[bin]] targets exist.
default-run = "musicfs"
[lib] [workspace.dependencies]
name = "musicfs" musicfs-proto = { path = "crates/musicfs-proto" }
path = "src/lib.rs" musicfs-core = { path = "crates/musicfs-core" }
[[bin]] prost = "0.14"
name = "musicfs" tonic = "0.14"
path = "src/main.rs" tonic-prost = "0.14"
tonic-health = "0.14"
tonic-reflection = "0.14"
[[bin]]
name = "musicfs-server"
path = "src/bin/musicfs-server.rs"
[dependencies]
clap = { version = "4.6.1", features = ["derive"] }
fuser = "0.17.0"
libc = "0.2.186"
notify = "8.2.0"
time = "0.3.49"
symphonia = { version = "0.5", default-features = false, features = [ symphonia = { version = "0.5", default-features = false, features = [
"aac", "alac", "flac", "mp3", "ogg", "vorbis", "wav" "aac", "alac", "flac", "mp3", "ogg", "vorbis", "wav",
] } ] }
twox-hash = "2.1.2" twox-hash = "2.1.2"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
tracing-appender = "0.2.5"
sea-orm = { version = "2.0.0-rc", features = [ "sqlx-postgres", "runtime-tokio", "macros" ] } tokio = { version = "1", features = [
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "fs", "io-util", "sync"] } "macros", "rt-multi-thread", "signal", "fs", "io-util", "sync",
] }
anyhow = "1" anyhow = "1"
async-trait = "0.1" async-trait = "0.1"
bytes = "1" clap = { version = "4.6.1", features = ["derive"] }
tonic = "0.14" notify = "8.2.0"
tonic-health = "0.14"
tonic-prost = "0.14"
tonic-reflection = "0.14"
prost = "0.14"
tokio-stream = "0.1" tokio-stream = "0.1"
fuser = "0.17.0"
libc = "0.2.186"
sea-orm = { version = "2.0.0-rc", features = [
"sqlx-postgres", "runtime-tokio", "macros",
] }
log = "0.4"
bytes = "1"
http = "1" http = "1"
chrono = { version = "0.4", default-features = false, features = ["clock"] } chrono = { version = "0.4", default-features = false, features = ["clock"] }
tracing = "0.1"
# For sea_orm's ConnectOptions::sqlx_logging_level, which takes a log::LevelFilter.
log = "0.4"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
# Pinned to 0.2.5: Builder::max_log_files (used for retention) landed in the
# 0.2.3 Builder API and is current here. An unqualified "0.2" could otherwise
# resolve to an older patch without the retention method.
tracing-appender = "0.2.5"
[dev-dependencies]
tempfile = "3" tempfile = "3"
+36
View File
@@ -0,0 +1,36 @@
[package]
name = "musicfs-client"
version.workspace = true
edition.workspace = true
default-run = "musicfs"
[lib]
name = "musicfs"
path = "src/lib.rs"
[[bin]]
name = "musicfs"
path = "src/main.rs"
[dependencies]
musicfs-core.workspace = true
musicfs-proto.workspace = true
fuser.workspace = true
libc.workspace = true
sea-orm.workspace = true
tokio.workspace = true
anyhow.workspace = true
async-trait.workspace = true
clap.workspace = true
notify.workspace = true
tokio-stream.workspace = true
tonic.workspace = true
bytes.workspace = true
http.workspace = true
tracing.workspace = true
log.workspace = true
twox-hash.workspace = true
chrono.workspace = true
[dev-dependencies]
tempfile.workspace = true
@@ -1,5 +1,4 @@
use std::{ use std::{
hash::Hasher,
path::PathBuf, path::PathBuf,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
@@ -7,7 +6,6 @@ use std::{
use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStrExt;
use fuser::INodeNo; use fuser::INodeNo;
use twox_hash::XxHash64;
use crate::music::metadata::MusicMetadata; use crate::music::metadata::MusicMetadata;
use crate::origins::attrs::FileAttrs; use crate::origins::attrs::FileAttrs;
@@ -38,27 +36,6 @@ fn secs_since_epoch(time: SystemTime) -> u64 {
.unwrap_or(0); .unwrap_or(0);
} }
/// Hash inputs shared by every Item regardless of origin. Server manifests
/// and client Items both feed these same five values through xxhash, so a
/// file's hash is identical on both sides — that's what makes `Reconcile`'s
/// diff correct.
pub fn compute_item_hash(
inode: u64,
original_path: &[u8],
ctime_secs: u64,
mtime_secs: u64,
crtime_secs: u64,
) -> u64 {
let seed = 1234;
let mut hasher = XxHash64::with_seed(seed);
hasher.write_u64(inode);
hasher.write(original_path);
hasher.write_u64(ctime_secs);
hasher.write_u64(mtime_secs);
hasher.write_u64(crtime_secs);
return hasher.finish();
}
impl Item { impl Item {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
@@ -88,13 +65,13 @@ impl Item {
} }
pub fn compute_hash(&self) -> u64 { pub fn compute_hash(&self) -> u64 {
return compute_item_hash( musicfs_core::compute_item_hash(
self.inode.0, self.inode.0,
self.original_path.as_os_str().as_bytes(), self.original_path.as_os_str().as_bytes(),
secs_since_epoch(self.attrs.ctime), secs_since_epoch(self.attrs.ctime),
secs_since_epoch(self.attrs.mtime), secs_since_epoch(self.attrs.mtime),
secs_since_epoch(self.attrs.crtime), secs_since_epoch(self.attrs.crtime),
); )
} }
} }
@@ -1,8 +1,8 @@
pub mod db; pub mod db;
pub mod item; pub mod item;
pub mod logging;
pub mod music; pub mod music;
pub mod origins; pub mod origins;
pub mod proto;
pub mod server;
pub mod virtual_dirs; pub mod virtual_dirs;
pub use musicfs_core::logging;
pub use musicfs_proto as proto;
@@ -6,11 +6,93 @@ use sea_orm::entity::prelude::*;
use crate::db::entities::Model; use crate::db::entities::Model;
use crate::item::Item; use crate::item::Item;
use crate::music::metadata::MusicMetadata; use crate::music::metadata::MusicMetadata;
use crate::music::metadata::db::{
artists, music_metadata as music_metadata_entity, other_tags, pictures,
};
use tracing::{debug, error}; use tracing::{debug, error};
mod entities {
pub mod music_metadata {
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "music_metadata")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub inode: i64,
pub track_title: String,
pub album: String,
pub track_number: i32,
#[sea_orm(column_type = "Blob")]
pub header: Vec<u8>,
pub real_audio_start: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
pub mod artists {
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "music_metadata_artists")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub inode: i64,
#[sea_orm(primary_key, auto_increment = false)]
pub artist: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
pub mod other_tags {
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "music_metadata_other_tags")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub inode: i64,
#[sea_orm(primary_key, auto_increment = false)]
pub position: i32,
pub tag: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
pub mod pictures {
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "music_metadata_pictures")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub inode: i64,
#[sea_orm(primary_key, auto_increment = false)]
pub position: i32,
#[sea_orm(column_type = "Blob")]
pub block_header: Vec<u8>,
pub data_offset: i64,
pub data_length: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
}
use entities::{artists, music_metadata as music_metadata_entity, other_tags, pictures};
pub async fn save_music_metadata( pub async fn save_music_metadata(
inode: i64, inode: i64,
music_metadata: &MusicMetadata, music_metadata: &MusicMetadata,
+2
View File
@@ -0,0 +1,2 @@
pub mod db;
pub use musicfs_core::music::{metadata, parse};
@@ -1,4 +1,4 @@
pub mod attrs; pub use musicfs_core::attrs;
pub mod local; pub mod local;
pub mod network; pub mod network;
@@ -266,10 +266,8 @@ fn manifest_entry_to_item(
rdev: 0, rdev: 0,
blksize: 4096, blksize: 4096,
}; };
let music_metadata: Option<MusicMetadata> = entry let music_metadata: Option<MusicMetadata> =
.music_metadata entry.music_metadata.clone().map(music_metadata_from_proto);
.clone()
.map(|mm| MusicMetadata::from(mm));
let mut local_path = PathBuf::new(); let mut local_path = PathBuf::new();
if let Some(mm) = &music_metadata { if let Some(mm) = &music_metadata {
@@ -303,26 +301,24 @@ fn manifest_entry_to_item(
); );
} }
impl From<crate::proto::MusicMetadata> for MusicMetadata { fn music_metadata_from_proto(mm: crate::proto::MusicMetadata) -> MusicMetadata {
fn from(mm: crate::proto::MusicMetadata) -> Self { MusicMetadata {
return MusicMetadata { artist: mm.artist,
artist: mm.artist, album_artist: mm.album_artist,
album_artist: mm.album_artist, album: mm.album,
album: mm.album, track_number: mm.track_number,
track_number: mm.track_number, track_title: mm.track_title,
track_title: mm.track_title, other_tags: mm.other_tags,
other_tags: mm.other_tags, header: mm.header,
header: mm.header, picture_block_headers: mm.picture_block_headers,
picture_block_headers: mm.picture_block_headers, picture_data_ranges: mm
picture_data_ranges: mm .picture_data_ranges
.picture_data_ranges .into_iter()
.into_iter() .map(|p| (p.offset, p.length))
.map(|p| (p.offset, p.length)) .collect(),
.collect(), real_audio_start: mm.real_audio_start,
real_audio_start: mm.real_audio_start, vorbis_comment_offset: mm.vorbis_comment_offset,
vorbis_comment_offset: mm.vorbis_comment_offset, vorbis_comment_length: mm.vorbis_comment_length,
vorbis_comment_length: mm.vorbis_comment_length,
};
} }
} }
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "musicfs-core"
version.workspace = true
edition.workspace = true
[dependencies]
symphonia.workspace = true
twox-hash.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
tracing-appender.workspace = true
[dev-dependencies]
tempfile.workspace = true
+20
View File
@@ -0,0 +1,20 @@
use std::hash::Hasher;
use twox_hash::XxHash64;
pub fn compute_item_hash(
inode: u64,
original_path: &[u8],
ctime_secs: u64,
mtime_secs: u64,
crtime_secs: u64,
) -> u64 {
let seed = 1234;
let mut hasher = XxHash64::with_seed(seed);
hasher.write_u64(inode);
hasher.write(original_path);
hasher.write_u64(ctime_secs);
hasher.write_u64(mtime_secs);
hasher.write_u64(crtime_secs);
hasher.finish()
}
+8
View File
@@ -0,0 +1,8 @@
pub mod attrs;
pub mod hash;
pub mod logging;
pub mod music;
pub use attrs::FileAttrs;
pub use hash::compute_item_hash;
pub use music::metadata::MusicMetadata;
+62
View File
@@ -0,0 +1,62 @@
use symphonia::core::meta::{MetadataRevision, StandardTagKey};
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct MusicMetadata {
pub artist: Vec<String>,
pub album_artist: Option<String>,
pub album: String,
pub track_number: i32,
pub track_title: String,
pub other_tags: Vec<String>,
pub header: Vec<u8>,
pub picture_block_headers: Vec<Vec<u8>>,
pub picture_data_ranges: Vec<(u64, u64)>,
pub real_audio_start: u64,
pub vorbis_comment_offset: u64,
pub vorbis_comment_length: u64,
}
impl MusicMetadata {
pub fn virtual_size(&self, real_file_size: u64) -> u64 {
let pictures_size: u64 = self
.picture_block_headers
.iter()
.zip(self.picture_data_ranges.iter())
.map(|(prefix, (_, len))| prefix.len() as u64 + len)
.sum();
self.header.len() as u64 + pictures_size + (real_file_size - self.real_audio_start)
}
}
pub(crate) fn extract_standard_tags(revision: &MetadataRevision, out: &mut MusicMetadata) {
for tag in revision.tags() {
let value = tag.value.to_string();
match tag.std_key {
Some(StandardTagKey::Artist) => out.artist.push(value),
Some(StandardTagKey::AlbumArtist) => out.album_artist = Some(value),
Some(StandardTagKey::Album) => out.album = value,
Some(StandardTagKey::TrackNumber) => {
out.track_number = value.parse::<i32>().unwrap_or(0)
}
Some(StandardTagKey::TrackTitle) => out.track_title = value,
_ => out.other_tags.push(format!("{}={}", tag.key, value)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn virtual_size_calculation() {
let mut metadata = MusicMetadata::default();
metadata.header = vec![0u8; 100];
metadata.picture_block_headers = vec![vec![0u8; 4]];
metadata.picture_data_ranges = vec![(0, 50)];
metadata.real_audio_start = 200;
let file_size = 1000u64;
let virtual_size = metadata.virtual_size(file_size);
assert_eq!(virtual_size, 954);
}
}
@@ -1,4 +1,3 @@
pub mod db;
pub mod encoder; pub mod encoder;
pub mod flac; pub mod flac;
pub mod metadata; pub mod metadata;
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "musicfs-proto"
version.workspace = true
edition.workspace = true
[dependencies]
prost.workspace = true
tonic.workspace = true
tonic-prost.workspace = true
+11
View File
@@ -0,0 +1,11 @@
pub mod musicfs {
include!("generated/musicfs/musicfs.rs");
}
pub use musicfs::{
ChangeEvent, FileChunk, GetFileRequest, GetManifestRequest, GetMetadataRequest, InodeHash,
ManifestEntry, MusicMetadata, PictureDataRange, ReconcileRequest, ReconcileResponse,
SubscribeEventsRequest,
music_fs_client::MusicFsClient,
music_fs_server::{MusicFs, MusicFsServer},
};
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "musicfs-server"
version.workspace = true
edition.workspace = true
default-run = "musicfs-server"
[[bin]]
name = "musicfs-server"
path = "src/bin/musicfs-server.rs"
[dependencies]
musicfs-core.workspace = true
musicfs-proto.workspace = true
tonic.workspace = true
tonic-health.workspace = true
tonic-reflection.workspace = true
tokio.workspace = true
anyhow.workspace = true
async-trait.workspace = true
clap.workspace = true
notify.workspace = true
tokio-stream.workspace = true
tracing.workspace = true
[dev-dependencies]
tempfile.workspace = true
@@ -2,8 +2,8 @@ use std::{net::SocketAddr, path::PathBuf};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use clap::Parser; use clap::Parser;
use musicfs::logging::{LogConfig, init}; use musicfs_core::logging::{LogConfig, init};
use musicfs::server::{ use musicfs_server::server::{
state::ServerState, state::ServerState,
transport::{self, TransportArgs}, transport::{self, TransportArgs},
watcher::ServerWatcher, watcher::ServerWatcher,
+1
View File
@@ -0,0 +1 @@
pub mod server;
@@ -1,5 +1,5 @@
use crate::item::compute_item_hash; use musicfs_core::compute_item_hash;
use crate::music::metadata::MusicMetadata; use musicfs_core::music::metadata::MusicMetadata;
/// One row of the in-memory manifest: every field the client needs to /// One row of the in-memory manifest: every field the client needs to
/// reconstruct an `Item` whose hash matches the server's hash. /// reconstruct an `Item` whose hash matches the server's hash.
@@ -8,9 +8,9 @@ use std::{
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use crate::music::parse::parse_music_metadata_for_path;
use crate::origins::attrs::FileAttrs;
use crate::server::manifest::ManifestEntry; use crate::server::manifest::ManifestEntry;
use musicfs_core::FileAttrs;
use musicfs_core::music::parse::parse_music_metadata_for_path;
use tracing::info; use tracing::info;
/// Server-side entry for one file. Built once on startup from a directory /// Server-side entry for one file. Built once on startup from a directory
@@ -20,7 +20,7 @@ pub struct FileEntry {
pub abs_path: PathBuf, pub abs_path: PathBuf,
pub rel_path: String, pub rel_path: String,
pub attrs: FileAttrs, pub attrs: FileAttrs,
pub music_metadata: Option<crate::music::metadata::MusicMetadata>, pub music_metadata: Option<musicfs_core::music::metadata::MusicMetadata>,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -10,19 +10,19 @@ use async_trait::async_trait;
use tokio::sync::broadcast::Receiver; use tokio::sync::broadcast::Receiver;
use tonic::{Request, Response, Status, transport::Server}; use tonic::{Request, Response, Status, transport::Server};
use crate::music::metadata::MusicMetadata; use crate::server::manifest::ManifestEntry as DomainManifestEntry;
use crate::proto as proto_types; use crate::server::state::ServerState;
use crate::proto::MusicFs as MusicFsTrait; use crate::server::transport::{MusicTransport, TransportArgs};
use crate::proto::{ use crate::server::watcher::{ChangeEvent, ChangeKind};
use musicfs_core::music::metadata::MusicMetadata;
use musicfs_proto as proto_types;
use musicfs_proto::MusicFs as MusicFsTrait;
use musicfs_proto::{
ChangeEvent as ProtoChangeEvent, FileChunk, GetFileRequest, GetManifestRequest, ChangeEvent as ProtoChangeEvent, FileChunk, GetFileRequest, GetManifestRequest,
GetMetadataRequest, InodeHash, ManifestEntry, MusicFsServer, GetMetadataRequest, InodeHash, ManifestEntry, MusicFsServer,
MusicMetadata as ProtoMusicMetadata, PictureDataRange, ReconcileRequest, ReconcileResponse, MusicMetadata as ProtoMusicMetadata, PictureDataRange, ReconcileRequest, ReconcileResponse,
SubscribeEventsRequest, SubscribeEventsRequest,
}; };
use crate::server::manifest::ManifestEntry as DomainManifestEntry;
use crate::server::state::ServerState;
use crate::server::transport::{MusicTransport, TransportArgs};
use crate::server::watcher::{ChangeEvent, ChangeKind};
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
pub struct GrpcTransport { pub struct GrpcTransport {
@@ -53,11 +53,11 @@ impl MusicTransport for GrpcTransport {
// v1alpha covers older clients; v1 is the current standard. // v1alpha covers older clients; v1 is the current standard.
let reflection_v1 = tonic_reflection::server::Builder::configure() let reflection_v1 = tonic_reflection::server::Builder::configure()
.register_encoded_file_descriptor_set(crate::proto::musicfs::FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(musicfs_proto::musicfs::FILE_DESCRIPTOR_SET)
.register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET)
.build_v1()?; .build_v1()?;
let reflection_v1alpha = tonic_reflection::server::Builder::configure() let reflection_v1alpha = tonic_reflection::server::Builder::configure()
.register_encoded_file_descriptor_set(crate::proto::musicfs::FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(musicfs_proto::musicfs::FILE_DESCRIPTOR_SET)
.register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET) .register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET)
.build_v1alpha()?; .build_v1alpha()?;
@@ -298,31 +298,29 @@ impl From<DomainManifestEntry> for ManifestEntry {
mtime: entry.mtime, mtime: entry.mtime,
ctime: entry.ctime, ctime: entry.ctime,
crtime: entry.crtime, crtime: entry.crtime,
music_metadata: entry.music_metadata.map(Into::into), music_metadata: entry.music_metadata.map(music_metadata_to_proto),
}; };
} }
} }
impl From<MusicMetadata> for ProtoMusicMetadata { fn music_metadata_to_proto(mm: MusicMetadata) -> ProtoMusicMetadata {
fn from(mm: MusicMetadata) -> Self { ProtoMusicMetadata {
return ProtoMusicMetadata { artist: mm.artist,
artist: mm.artist, album_artist: mm.album_artist,
album_artist: mm.album_artist, album: mm.album,
album: mm.album, track_number: mm.track_number,
track_number: mm.track_number, track_title: mm.track_title,
track_title: mm.track_title, other_tags: mm.other_tags,
other_tags: mm.other_tags, header: mm.header,
header: mm.header, picture_block_headers: mm.picture_block_headers,
picture_block_headers: mm.picture_block_headers, picture_data_ranges: mm
picture_data_ranges: mm .picture_data_ranges
.picture_data_ranges .into_iter()
.into_iter() .map(|(offset, length)| PictureDataRange { offset, length })
.map(|(offset, length)| PictureDataRange { offset, length }) .collect(),
.collect(), real_audio_start: mm.real_audio_start,
real_audio_start: mm.real_audio_start, vorbis_comment_offset: mm.vorbis_comment_offset,
vorbis_comment_offset: mm.vorbis_comment_offset, vorbis_comment_length: mm.vorbis_comment_length,
vorbis_comment_length: mm.vorbis_comment_length,
};
} }
} }
+4 -4
View File
@@ -42,7 +42,7 @@
processes.musicfs = { processes.musicfs = {
after = [ "devenv:processes:postgres" ]; after = [ "devenv:processes:postgres" ];
exec = lib.mkDefault '' exec = lib.mkDefault ''
cargo run -- \ cargo run -p musicfs-client -- \
--source /home/fujin/Music \ --source /home/fujin/Music \
--mountpoint /tmp/rust-fuse \ --mountpoint /tmp/rust-fuse \
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST" --database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
@@ -53,7 +53,7 @@
processes.musicfs = { processes.musicfs = {
after = [ "devenv:processes:postgres" ]; after = [ "devenv:processes:postgres" ];
exec = '' exec = ''
cargo run -- \ cargo run -p musicfs-client -- \
--source /home/fujin/Music \ --source /home/fujin/Music \
--mountpoint /tmp/rust-fuse \ --mountpoint /tmp/rust-fuse \
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST" --database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
@@ -65,7 +65,7 @@
processes.musicfs = { processes.musicfs = {
after = [ "devenv:processes:postgres" ]; after = [ "devenv:processes:postgres" ];
exec = '' exec = ''
cargo run -- \ cargo run -p musicfs-client -- \
--source http://10.185.226.145:50051 \ --source http://10.185.226.145:50051 \
--mountpoint /tmp/rust-fuse \ --mountpoint /tmp/rust-fuse \
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST" --database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
@@ -77,7 +77,7 @@
processes.musicfs = { processes.musicfs = {
after = [ "devenv:processes:postgres" ]; after = [ "devenv:processes:postgres" ];
exec = '' exec = ''
cargo run -- \ cargo run -p musicfs-client -- \
--source http://127.0.0.1:50061 \ --source http://127.0.0.1:50061 \
--mountpoint ./target/e2e/mnt \ --mountpoint ./target/e2e/mnt \
--database "postgresql://fujin@localhost/musicfs_e2e?host=$PGHOST" \ --database "postgresql://fujin@localhost/musicfs_e2e?host=$PGHOST" \
+2 -2
View File
@@ -1,8 +1,8 @@
version: v2 version: v2
plugins: plugins:
- remote: buf.build/community/neoeinstein-prost - remote: buf.build/community/neoeinstein-prost
out: ../src/proto/generated out: ../crates/musicfs-proto/src/generated
opt: opt:
- file_descriptor_set - file_descriptor_set
- remote: buf.build/community/neoeinstein-tonic - remote: buf.build/community/neoeinstein-tonic
out: ../src/proto/generated out: ../crates/musicfs-proto/src/generated
+10
View File
@@ -65,9 +65,19 @@ stop_postgres() {
# ──────────────────────── VM management ──────────────────────────────── # ──────────────────────── VM management ────────────────────────────────
vm_ip() { incus list "$VM_NAME" -f csv -c 4 2>/dev/null | head -1 | awk '{print $1}'; } vm_ip() { incus list "$VM_NAME" -f csv -c 4 2>/dev/null | head -1 | awk '{print $1}'; }
detect_e2e_ip() {
local bridge_cidr
bridge_cidr="$(incus network get incusbr0 ipv4.address 2>/dev/null || true)"
if [[ -z "$bridge_cidr" ]]; then echo ""; return; fi
local gateway="${bridge_cidr%%/*}"
echo "${gateway%.*}.146"
}
start_vm() { start_vm() {
log "starting VM '$VM_NAME' (port $PORT)…" log "starting VM '$VM_NAME' (port $PORT)…"
local e2e_ip; e2e_ip="$(detect_e2e_ip)"
VM_NAME="$VM_NAME" \ VM_NAME="$VM_NAME" \
VM_IP="$e2e_ip" \
MUSIC_SOURCE="$MUSIC_DIR" \ MUSIC_SOURCE="$MUSIC_DIR" \
LISTEN_PORT="$PORT" \ LISTEN_PORT="$PORT" \
RUST_LOG=debug \ RUST_LOG=debug \
+12
View File
@@ -62,9 +62,21 @@ stop_postgres() {
# ──────────────────────── VM management ──────────────────────────────── # ──────────────────────── VM management ────────────────────────────────
vm_ip() { incus list "$VM_NAME" -f csv -c 4 2>/dev/null | head -1 | awk '{print $1}'; } vm_ip() { incus list "$VM_NAME" -f csv -c 4 2>/dev/null | head -1 | awk '{print $1}'; }
detect_e2e_ip() {
local bridge_cidr
bridge_cidr="$(incus network get incusbr0 ipv4.address 2>/dev/null || true)"
if [[ -z "$bridge_cidr" ]]; then echo ""; return; fi
local gateway="${bridge_cidr%%/*}"
echo "${gateway%.*}.146"
}
start_vm() { start_vm() {
log "starting VM '$VM_NAME' (port $PORT)…" log "starting VM '$VM_NAME' (port $PORT)…"
# e2e VM uses .146 to avoid colliding with the dev VM (.145)
local e2e_ip
e2e_ip="$(detect_e2e_ip)"
VM_NAME="$VM_NAME" \ VM_NAME="$VM_NAME" \
VM_IP="$e2e_ip" \
MUSIC_SOURCE="$MUSIC_DIR" \ MUSIC_SOURCE="$MUSIC_DIR" \
LISTEN_PORT="$PORT" \ LISTEN_PORT="$PORT" \
RUST_LOG=debug \ RUST_LOG=debug \
+1 -5
View File
@@ -179,11 +179,7 @@ wait_for_vm() {
deploy() { deploy() {
local bundle; bundle="$(build_bundle)" local bundle; bundle="$(build_bundle)"
log "deploying bundle to $VM_NAME:$SERVER_DIR ..." log "deploying bundle to $VM_NAME:$SERVER_DIR ..."
# Stop first: overwriting a running binary fails with ETXTBSY ("text file busy"). incus exec "$VM_NAME" -- systemctl stop "$UNIT_NAME" 2>/dev/null || true
if incus exec "$VM_NAME" -- systemctl is-active --quiet "$UNIT_NAME" 2>/dev/null; then
log "stopping $UNIT_NAME (binary in use)..."
incus exec "$VM_NAME" -- systemctl stop "$UNIT_NAME"
fi
incus exec "$VM_NAME" -- mkdir -p "$SERVER_DIR/lib" "$LOG_DIR" incus exec "$VM_NAME" -- mkdir -p "$SERVER_DIR/lib" "$LOG_DIR"
incus file push "$bundle/server" "$VM_NAME$SERVER_DIR/server" --mode=0755 incus file push "$bundle/server" "$VM_NAME$SERVER_DIR/server" --mode=0755
# Ship the bundled libs one by one (robust across incus file-push versions). # Ship the bundled libs one by one (robust across incus file-push versions).
-153
View File
@@ -1,153 +0,0 @@
use symphonia::core::meta::{MetadataRevision, StandardTagKey};
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct MusicMetadata {
pub artist: Vec<String>,
pub album_artist: Option<String>,
pub album: String,
pub track_number: i32,
pub track_title: String,
pub other_tags: Vec<String>,
pub header: Vec<u8>,
/// Per-externalized-region reinsert prefix kept in memory. FLAC stores its
/// 4-byte PICTURE block header here; MP3 keeps whole frames in the original
/// file and stores an empty prefix.
pub picture_block_headers: Vec<Vec<u8>>,
pub picture_data_ranges: Vec<(u64, u64)>,
pub real_audio_start: u64,
pub vorbis_comment_offset: u64,
pub vorbis_comment_length: u64,
}
impl MusicMetadata {
/// Size of the virtual file: rebuilt header + externalized regions + the
/// original audio tail. Format-agnostic.
pub fn virtual_size(&self, real_file_size: u64) -> u64 {
let pictures_size: u64 = self
.picture_block_headers
.iter()
.zip(self.picture_data_ranges.iter())
.map(|(prefix, (_, len))| prefix.len() as u64 + len)
.sum();
self.header.len() as u64 + pictures_size + (real_file_size - self.real_audio_start)
}
}
/// Map symphonia's normalized tags onto the shared `MusicMetadata` fields.
/// Used by every format parser so the field mapping lives in one place.
pub(crate) fn extract_standard_tags(revision: &MetadataRevision, out: &mut MusicMetadata) {
for tag in revision.tags() {
let value = tag.value.to_string();
match tag.std_key {
Some(StandardTagKey::Artist) => out.artist.push(value),
Some(StandardTagKey::AlbumArtist) => out.album_artist = Some(value),
Some(StandardTagKey::Album) => out.album = value,
Some(StandardTagKey::TrackNumber) => {
out.track_number = value.parse::<i32>().unwrap_or(0)
}
Some(StandardTagKey::TrackTitle) => out.track_title = value,
_ => out.other_tags.push(format!("{}={}", tag.key, value)),
}
}
}
pub mod db {
pub mod music_metadata {
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "music_metadata")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub inode: i64,
pub track_title: String,
pub album: String,
pub track_number: i32,
#[sea_orm(column_type = "Blob")]
pub header: Vec<u8>,
pub real_audio_start: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
pub mod artists {
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "music_metadata_artists")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub inode: i64,
#[sea_orm(primary_key, auto_increment = false)]
pub artist: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
pub mod other_tags {
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "music_metadata_other_tags")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub inode: i64,
#[sea_orm(primary_key, auto_increment = false)]
pub position: i32,
pub tag: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
pub mod pictures {
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "music_metadata_pictures")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub inode: i64,
#[sea_orm(primary_key, auto_increment = false)]
pub position: i32,
#[sea_orm(column_type = "Blob")]
pub block_header: Vec<u8>,
pub data_offset: i64,
pub data_length: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn virtual_size_calculation() {
let mut metadata = MusicMetadata::default();
metadata.header = vec![0u8; 100];
metadata.picture_block_headers = vec![vec![0u8; 4]];
metadata.picture_data_ranges = vec![(0, 50)];
metadata.real_audio_start = 200;
let file_size = 1000u64;
let virtual_size = metadata.virtual_size(file_size);
// 100 (header) + 4 (prefix) + 50 (picture data) + 800 (audio tail)
assert_eq!(virtual_size, 954);
}
}
-19
View File
@@ -1,19 +0,0 @@
//! Generated protobuf + tonic bindings for the musicfs service.
//!
//! `buf generate` writes prost messages to `generated/musicfs/musicfs.rs` and
//! the tonic service to `generated/musicfs/musicfs.tonic.rs`. The prost plugin
//! appends `include!("musicfs.tonic.rs")` to the end of `musicfs.rs`, so
//! including that one file here also pulls in the service. The only glue buf
//! does not generate is the module named after the proto package (`musicfs`),
//! which we provide below before re-exporting the types callers use.
pub mod musicfs {
include!("generated/musicfs/musicfs.rs");
}
pub use musicfs::{
ChangeEvent, FileChunk, GetFileRequest, GetManifestRequest, GetMetadataRequest, InodeHash,
ManifestEntry, MusicMetadata, PictureDataRange, ReconcileRequest, ReconcileResponse,
SubscribeEventsRequest,
music_fs_client::MusicFsClient,
music_fs_server::{MusicFs, MusicFsServer},
};