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]]
name = "musicfs"
name = "musicfs-client"
version = "0.1.0"
dependencies = [
"anyhow",
@@ -1712,24 +1712,58 @@ dependencies = [
"http",
"libc",
"log",
"musicfs-core",
"musicfs-proto",
"notify",
"prost",
"sea-orm",
"symphonia",
"tempfile",
"time",
"tokio",
"tokio-stream",
"tonic",
"tonic-health",
"tonic-prost",
"tonic-reflection",
"tracing",
"twox-hash",
]
[[package]]
name = "musicfs-core"
version = "0.1.0"
dependencies = [
"symphonia",
"tempfile",
"tracing",
"tracing-appender",
"tracing-subscriber",
"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]]
name = "nix"
version = "0.29.0"
+35 -41
View File
@@ -1,57 +1,51 @@
[package]
name = "musicfs"
[workspace]
members = [
"crates/musicfs-proto",
"crates/musicfs-core",
"crates/musicfs-server",
"crates/musicfs-client",
]
resolver = "2"
[workspace.package]
version = "0.1.0"
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]
name = "musicfs"
path = "src/lib.rs"
[workspace.dependencies]
musicfs-proto = { path = "crates/musicfs-proto" }
musicfs-core = { path = "crates/musicfs-core" }
[[bin]]
name = "musicfs"
path = "src/main.rs"
prost = "0.14"
tonic = "0.14"
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 = [
"aac", "alac", "flac", "mp3", "ogg", "vorbis", "wav"
"aac", "alac", "flac", "mp3", "ogg", "vorbis", "wav",
] }
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 = ["macros", "rt-multi-thread", "signal", "fs", "io-util", "sync"] }
tokio = { version = "1", features = [
"macros", "rt-multi-thread", "signal", "fs", "io-util", "sync",
] }
anyhow = "1"
async-trait = "0.1"
bytes = "1"
tonic = "0.14"
tonic-health = "0.14"
tonic-prost = "0.14"
tonic-reflection = "0.14"
prost = "0.14"
clap = { version = "4.6.1", features = ["derive"] }
notify = "8.2.0"
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"
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"
+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::{
hash::Hasher,
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
@@ -7,7 +6,6 @@ use std::{
use std::os::unix::ffi::OsStrExt;
use fuser::INodeNo;
use twox_hash::XxHash64;
use crate::music::metadata::MusicMetadata;
use crate::origins::attrs::FileAttrs;
@@ -38,27 +36,6 @@ fn secs_since_epoch(time: SystemTime) -> u64 {
.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 {
#[allow(clippy::too_many_arguments)]
pub fn new(
@@ -88,13 +65,13 @@ impl Item {
}
pub fn compute_hash(&self) -> u64 {
return compute_item_hash(
musicfs_core::compute_item_hash(
self.inode.0,
self.original_path.as_os_str().as_bytes(),
secs_since_epoch(self.attrs.ctime),
secs_since_epoch(self.attrs.mtime),
secs_since_epoch(self.attrs.crtime),
);
)
}
}
@@ -1,8 +1,8 @@
pub mod db;
pub mod item;
pub mod logging;
pub mod music;
pub mod origins;
pub mod proto;
pub mod server;
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::item::Item;
use crate::music::metadata::MusicMetadata;
use crate::music::metadata::db::{
artists, music_metadata as music_metadata_entity, other_tags, pictures,
};
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(
inode: i64,
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 network;
@@ -266,10 +266,8 @@ fn manifest_entry_to_item(
rdev: 0,
blksize: 4096,
};
let music_metadata: Option<MusicMetadata> = entry
.music_metadata
.clone()
.map(|mm| MusicMetadata::from(mm));
let music_metadata: Option<MusicMetadata> =
entry.music_metadata.clone().map(music_metadata_from_proto);
let mut local_path = PathBuf::new();
if let Some(mm) = &music_metadata {
@@ -303,26 +301,24 @@ fn manifest_entry_to_item(
);
}
impl From<crate::proto::MusicMetadata> for MusicMetadata {
fn from(mm: crate::proto::MusicMetadata) -> Self {
return MusicMetadata {
artist: mm.artist,
album_artist: mm.album_artist,
album: mm.album,
track_number: mm.track_number,
track_title: mm.track_title,
other_tags: mm.other_tags,
header: mm.header,
picture_block_headers: mm.picture_block_headers,
picture_data_ranges: mm
.picture_data_ranges
.into_iter()
.map(|p| (p.offset, p.length))
.collect(),
real_audio_start: mm.real_audio_start,
vorbis_comment_offset: mm.vorbis_comment_offset,
vorbis_comment_length: mm.vorbis_comment_length,
};
fn music_metadata_from_proto(mm: crate::proto::MusicMetadata) -> MusicMetadata {
MusicMetadata {
artist: mm.artist,
album_artist: mm.album_artist,
album: mm.album,
track_number: mm.track_number,
track_title: mm.track_title,
other_tags: mm.other_tags,
header: mm.header,
picture_block_headers: mm.picture_block_headers,
picture_data_ranges: mm
.picture_data_ranges
.into_iter()
.map(|p| (p.offset, p.length))
.collect(),
real_audio_start: mm.real_audio_start,
vorbis_comment_offset: mm.vorbis_comment_offset,
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 flac;
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 clap::Parser;
use musicfs::logging::{LogConfig, init};
use musicfs::server::{
use musicfs_core::logging::{LogConfig, init};
use musicfs_server::server::{
state::ServerState,
transport::{self, TransportArgs},
watcher::ServerWatcher,
+1
View File
@@ -0,0 +1 @@
pub mod server;
@@ -1,5 +1,5 @@
use crate::item::compute_item_hash;
use crate::music::metadata::MusicMetadata;
use musicfs_core::compute_item_hash;
use musicfs_core::music::metadata::MusicMetadata;
/// One row of the in-memory manifest: every field the client needs to
/// reconstruct an `Item` whose hash matches the server's hash.
@@ -8,9 +8,9 @@ use std::{
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 musicfs_core::FileAttrs;
use musicfs_core::music::parse::parse_music_metadata_for_path;
use tracing::info;
/// 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 rel_path: String,
pub attrs: FileAttrs,
pub music_metadata: Option<crate::music::metadata::MusicMetadata>,
pub music_metadata: Option<musicfs_core::music::metadata::MusicMetadata>,
}
#[derive(Clone)]
@@ -10,19 +10,19 @@ use async_trait::async_trait;
use tokio::sync::broadcast::Receiver;
use tonic::{Request, Response, Status, transport::Server};
use crate::music::metadata::MusicMetadata;
use crate::proto as proto_types;
use crate::proto::MusicFs as MusicFsTrait;
use crate::proto::{
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 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,
GetMetadataRequest, InodeHash, ManifestEntry, MusicFsServer,
MusicMetadata as ProtoMusicMetadata, PictureDataRange, ReconcileRequest, ReconcileResponse,
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};
pub struct GrpcTransport {
@@ -53,11 +53,11 @@ impl MusicTransport for GrpcTransport {
// v1alpha covers older clients; v1 is the current standard.
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)
.build_v1()?;
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)
.build_v1alpha()?;
@@ -298,31 +298,29 @@ impl From<DomainManifestEntry> for ManifestEntry {
mtime: entry.mtime,
ctime: entry.ctime,
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 from(mm: MusicMetadata) -> Self {
return ProtoMusicMetadata {
artist: mm.artist,
album_artist: mm.album_artist,
album: mm.album,
track_number: mm.track_number,
track_title: mm.track_title,
other_tags: mm.other_tags,
header: mm.header,
picture_block_headers: mm.picture_block_headers,
picture_data_ranges: mm
.picture_data_ranges
.into_iter()
.map(|(offset, length)| PictureDataRange { offset, length })
.collect(),
real_audio_start: mm.real_audio_start,
vorbis_comment_offset: mm.vorbis_comment_offset,
vorbis_comment_length: mm.vorbis_comment_length,
};
fn music_metadata_to_proto(mm: MusicMetadata) -> ProtoMusicMetadata {
ProtoMusicMetadata {
artist: mm.artist,
album_artist: mm.album_artist,
album: mm.album,
track_number: mm.track_number,
track_title: mm.track_title,
other_tags: mm.other_tags,
header: mm.header,
picture_block_headers: mm.picture_block_headers,
picture_data_ranges: mm
.picture_data_ranges
.into_iter()
.map(|(offset, length)| PictureDataRange { offset, length })
.collect(),
real_audio_start: mm.real_audio_start,
vorbis_comment_offset: mm.vorbis_comment_offset,
vorbis_comment_length: mm.vorbis_comment_length,
}
}
+4 -4
View File
@@ -42,7 +42,7 @@
processes.musicfs = {
after = [ "devenv:processes:postgres" ];
exec = lib.mkDefault ''
cargo run -- \
cargo run -p musicfs-client -- \
--source /home/fujin/Music \
--mountpoint /tmp/rust-fuse \
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
@@ -53,7 +53,7 @@
processes.musicfs = {
after = [ "devenv:processes:postgres" ];
exec = ''
cargo run -- \
cargo run -p musicfs-client -- \
--source /home/fujin/Music \
--mountpoint /tmp/rust-fuse \
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
@@ -65,7 +65,7 @@
processes.musicfs = {
after = [ "devenv:processes:postgres" ];
exec = ''
cargo run -- \
cargo run -p musicfs-client -- \
--source http://10.185.226.145:50051 \
--mountpoint /tmp/rust-fuse \
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
@@ -77,7 +77,7 @@
processes.musicfs = {
after = [ "devenv:processes:postgres" ];
exec = ''
cargo run -- \
cargo run -p musicfs-client -- \
--source http://127.0.0.1:50061 \
--mountpoint ./target/e2e/mnt \
--database "postgresql://fujin@localhost/musicfs_e2e?host=$PGHOST" \
+2 -2
View File
@@ -1,8 +1,8 @@
version: v2
plugins:
- remote: buf.build/community/neoeinstein-prost
out: ../src/proto/generated
out: ../crates/musicfs-proto/src/generated
opt:
- file_descriptor_set
- 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_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() {
log "starting VM '$VM_NAME' (port $PORT)…"
local e2e_ip; e2e_ip="$(detect_e2e_ip)"
VM_NAME="$VM_NAME" \
VM_IP="$e2e_ip" \
MUSIC_SOURCE="$MUSIC_DIR" \
LISTEN_PORT="$PORT" \
RUST_LOG=debug \
+12
View File
@@ -62,9 +62,21 @@ stop_postgres() {
# ──────────────────────── VM management ────────────────────────────────
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() {
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_IP="$e2e_ip" \
MUSIC_SOURCE="$MUSIC_DIR" \
LISTEN_PORT="$PORT" \
RUST_LOG=debug \
+1 -5
View File
@@ -179,11 +179,7 @@ wait_for_vm() {
deploy() {
local bundle; bundle="$(build_bundle)"
log "deploying bundle to $VM_NAME:$SERVER_DIR ..."
# Stop first: overwriting a running binary fails with ETXTBSY ("text file busy").
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" -- systemctl stop "$UNIT_NAME" 2>/dev/null || true
incus exec "$VM_NAME" -- mkdir -p "$SERVER_DIR/lib" "$LOG_DIR"
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).
-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},
};