Compare commits

..

10 Commits

Author SHA1 Message Date
Alexander 1dcfa7a803 Add readiness probe to devenv 2026-07-25 12:44:06 +02:00
Alexander 78b4338f95 Show files as {track_number} - {track_title} 2026-07-25 11:56:30 +02:00
Alexander 82be15b960 Implement list files rpc 2026-07-24 20:17:38 +02:00
Alexander 5f79b3c2b0 Configure devenv to build output 2026-07-03 22:13:16 +02:00
Alexander f28091257e Define endpoints to update metadata 2026-07-01 20:23:10 +02:00
Alexander 8f76dd5b39 Add health endpoint, add rpc to client 2026-07-01 16:56:56 +02:00
Alexander 8dba0c097d Refactor to split client from server 2026-07-01 16:31:55 +02:00
Alexander d35aa2aecb Some more fixes for reconcile with remote 2026-07-01 14:53:21 +02:00
Alexander 0e50d8a2a6 Fix reconcile with remote 2026-07-01 14:27:43 +02:00
Alexander 9699da385b Add resilience tests 2026-07-01 13:07:05 +02:00
68 changed files with 5321 additions and 1731 deletions
Generated
+41 -5
View File
@@ -1700,7 +1700,7 @@ dependencies = [
]
[[package]]
name = "musicfs"
name = "musicfs-client"
version = "0.1.0"
dependencies = [
"anyhow",
@@ -1712,24 +1712,60 @@ 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"
+116 -19
View File
@@ -1,13 +1,97 @@
#+title: musicfs
* Incus server VM
A read-only FUSE filesystem that presents a music library reorganised by
*metadata* rather than by however the files happen to sit on disk. Point it at a
messy ~~/Music~ tree (or a remote ~musicfs-server~) and it mounts a clean
=Artist/Album/Track= view, driven entirely by the tags parsed out of the audio
files themselves.
#+begin_example
mountpoint/
├── ДДТ/
│ └── Творчество в пустоте/
│ ├── 01 Intro.flac
│ └── 02 ...
└── Some Artist/
└── Some Album/
└── 01 Track.flac
#+end_example
The source files are never moved or modified. The directory hierarchy is
*virtual*: synthesised from ~album_artist~/~album~ tags, with stable synthetic
inodes so the layout survives remounts.
* How it works
Two *origins* feed the same FUSE frontend:
- *LocalOrigin* — walks a directory on the host, parses tags, builds the
snapshot. A =notify= watcher keeps it live as files change.
- *NetworkOrigin* — talks gRPC to a remote ~musicfs-server~. Metadata and file
bytes arrive over the wire; a Postgres table caches both. Reconciliation is
hash-based: the client sends the ~(inode, hash)~ pairs it has, the server
replies with only what changed or was deleted, so almost nothing crosses the
network on a steady-state refresh.
Which one is used is chosen automatically from the ~--source~ argument: an
=http(s)://= URL means network, anything else is treated as a local path.
State lives in Postgres — parsed metadata, the virtual-path layout, and (for the
network origin) a lazy byte cache populated only for files that were actually
read.
* Layout
| Crate | Responsibility |
|------------------+-----------------------------------------------------------------------|
| =musicfs-proto= | Protobuf/gRPC definitions (=proto/musicfs.proto=), generated bindings |
| =musicfs-core= | Shared logic: tag parsing (FLAC/MP3 via symphonia), hashing, logging |
| =musicfs-client= | FUSE mount, origins, Postgres cache/sync — the =musicfs= binary |
| =musicfs-server= | gRPC server sharing a library — the =musicfs-server= binary |
The gRPC surface (see =proto/musicfs.proto=): ~Reconcile~, ~GetMetadata~,
~GetManifest~, ~GetFile~ (whole-file or byte-range streaming), and
~SubscribeEvents~ (change wake-ups). The client also serves a ~ClientStatus~ RPC
for introspection.
* Running the client
Everything runs inside the =devenv= shell, which provides the Rust toolchain,
Postgres, FUSE tooling, and gRPC utilities.
#+begin_src bash
devenv shell
devenv up --profile local # starts Postgres + the FUSE mount
#+end_src
Predefined profiles in =devenv.nix=:
| Profile | Source | Purpose |
|-----------+---------------------------------+-------------------------------|
| =local= | a local directory | mount a host music folder |
| =remote= | =http://…:50051= | mount a remote musicfs-server |
| =e2e= | =http://127.0.0.1:50061= | end-to-end test harness |
Or run the binary directly:
#+begin_src bash
cargo run -p musicfs-client -- \
--source /home/you/Music \
--mountpoint /tmp/musicfs \
--database "postgresql://you@localhost/musicfs?host=$PGHOST"
#+end_src
Key flags: =--source= (local path or ~http(s)://~ server URL), =--mountpoint=,
=--database= (Postgres URL), =--log-dir= (daily-rotated logs, default =./logs=),
=--listen= (address for the client's status/health RPCs, default
=127.0.0.1:50052=).
* Running the server in an Incus VM
~musicfs-server~ (gRPC) runs inside an Incus VM, live-sharing the host's
~~/Music~ as a readonly virtiofs mount. =scripts/vm.sh= builds the binary on the
host, bundles its nix ~glibc~ so it runs unmodified in the VM, ships it in, and
runs it under systemd.
** Setup
~~/Music~ as a read-only virtiofs mount. =scripts/vm.sh= builds the binary on
the host, bundles its nix ~glibc~ so it runs unmodified in the VM, ships it in,
and runs it under systemd.
#+begin_src bash
devenv shell # cargo, incus, patchelf, grpcurl
@@ -16,17 +100,19 @@ scripts/vm.sh up # create VM, share ~/Music, build, ship, start server
Options (env): =VM_NAME=, =IMAGE=, =MUSIC_SOURCE=, =LISTEN_PORT=, =RUST_LOG=.
| Command | Action |
|--------------------------+-------------------------------|
| =scripts/vm.sh up= | create + build + ship + start |
| =scripts/vm.sh redeploy= | rebuild after a code change |
| =scripts/vm.sh logs= | tail server logs |
| =scripts/vm.sh status= | VM + service status |
| =scripts/vm.sh shell= | shell inside the VM |
| =scripts/vm.sh down= | stop the VM |
| =scripts/vm.sh destroy= | delete the VM |
| Command | Action |
|------------------------+-------------------------------|
| =scripts/vm.sh up= | create + build + ship + start |
| =scripts/vm.sh redeploy= | rebuild after a code change |
| =scripts/vm.sh logs= | tail server logs |
| =scripts/vm.sh status= | VM + service status |
| =scripts/vm.sh shell= | shell inside the VM |
| =scripts/vm.sh down= | stop the VM |
| =scripts/vm.sh destroy= | delete the VM |
** Test it (Nushell)
Reach the server at the VM's bridge IP (=scripts/vm.sh status= prints it).
** Poke at it (Nushell)
#+begin_src nushell
let VMIP = (incus list musicfs -f csv -c 4 | lines | first | split row " " | first)
@@ -46,9 +132,20 @@ incus exec musicfs -- find /music -type f | lines | length
incus exec musicfs -- cat "/music/DDT/ДДТ - Творчество в пустоте – 2 - 01 Intro.flac" | ^mpv -
#+end_src
Reach the server at the VM's bridge IP (=scripts/vm.sh status= prints it).
Install mpv with =nix profile install nixpkgs#mpv=, or use =^ffplay -= instead.
In Nushell, always put =| lines= between an external command's stdout and a
builtin (=find=, =first=, =length=, ...); external-to-external pipes (=cat |
mpv=) are byte-stable and don't need it.
builtin (=find=, =first=, =length=, …); external-to-external pipes (=cat | mpv=)
are byte-stable and don't need it.
* Development
#+begin_src bash
just build # cargo build
cargo test # unit tests
just e2e # end-to-end suite (scripts/e2e/run.sh)
just e2e-resilience # resilience suite
#+end_src
Formatting and lint (clippy, treefmt with rustfmt + nixfmt) run as git hooks via
=devenv=. Requires Rust edition 2024.
+39
View File
@@ -0,0 +1,39 @@
[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
tonic-health.workspace = true
tonic-reflection.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
sea-orm = { workspace = true, features = ["mock"] }
+248
View File
@@ -0,0 +1,248 @@
use std::{
collections::BTreeMap,
path::PathBuf,
sync::{Arc, Mutex},
};
use fuser::INodeNo;
use musicfs_core::music::encoder::MusicMetadataEncoderFactory;
use musicfs_proto::{
ClientControl, FileEntry, FileMetadata, GetMusicMetadataRequest, GetMusicMetadataResponse,
ListFilesRequest, ListFilesResponse, MusicMetadata as ProtoMusicMetadata, PictureDataRange,
UpdateMusicMetadataRequest, UpdateMusicMetadataResponse,
};
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait};
use tonic::{Request, Response, Status};
use crate::db::entities;
use crate::item::{FileType, Item};
use crate::music::db::save_music_metadata;
use crate::music::metadata::MusicMetadata;
use crate::virtual_dirs::{
compute_new_layout, ensure_virtual_dirs, find_orphaned_dirs, parent_inode_from_path,
};
pub struct ClientControlServiceImpl {
files: Arc<Mutex<BTreeMap<INodeNo, Item>>>,
db: DatabaseConnection,
}
impl ClientControlServiceImpl {
pub fn new(files: Arc<Mutex<BTreeMap<INodeNo, Item>>>, db: DatabaseConnection) -> Self {
ClientControlServiceImpl { files, db }
}
}
#[tonic::async_trait]
impl ClientControl for ClientControlServiceImpl {
async fn get_music_metadata(
&self,
request: Request<GetMusicMetadataRequest>,
) -> Result<Response<GetMusicMetadataResponse>, Status> {
let inode = INodeNo(request.into_inner().inode);
let metadata = {
let files = self.files.lock().unwrap();
let item = files
.get(&inode)
.ok_or_else(|| Status::not_found(format!("inode {} not found", inode.0)))?;
item.music_metadata.clone().ok_or_else(|| {
Status::not_found(format!("inode {} has no music metadata", inode.0))
})?
};
Ok(Response::new(GetMusicMetadataResponse {
metadata: Some(music_metadata_to_proto(metadata)),
}))
}
async fn update_music_metadata(
&self,
request: Request<UpdateMusicMetadataRequest>,
) -> Result<Response<UpdateMusicMetadataResponse>, Status> {
let req = request.into_inner();
let inode = INodeNo(req.inode);
let outcome = {
let mut files = self.files.lock().unwrap();
let (old_local_path, current_name, source_for_virtual_dirs) = {
let root = files.get(&INodeNo::ROOT);
let item = files
.get(&inode)
.ok_or_else(|| Status::not_found(format!("inode {} not found", inode.0)))?;
let source = root
.map(|r| r.original_path.clone())
.unwrap_or_else(|| PathBuf::from("/"));
(item.local_path.clone(), item.name.clone(), source)
};
// Phase 1: mutate the music metadata and re-encode the header.
// Done in its own scope so the mutable borrow on `files` is
// released before we touch `files` again for layout changes.
let (updated_mm, _encoder_extension_source) = {
let item = files
.get_mut(&inode)
.ok_or_else(|| Status::not_found(format!("inode {} not found", inode.0)))?;
let mm = item.music_metadata.as_mut().ok_or_else(|| {
Status::not_found(format!("inode {} has no music metadata", inode.0))
})?;
apply_update(mm, req);
let encoder =
MusicMetadataEncoderFactory::for_path(&item.local_path).ok_or_else(|| {
Status::failed_precondition(format!(
"no encoder for file type: {}",
item.local_path.display()
))
})?;
encoder.encode(mm);
(mm.clone(), item.local_path.clone())
};
// Phase 2: compute new layout, write it onto the item, then
// ensure new virtual dirs and prune orphans. A fresh mutable
// borrow is fine because phase 1's borrow has been released.
let (new_name, new_local_path) = compute_new_layout(&current_name, &updated_mm);
{
let item = files
.get_mut(&inode)
.ok_or_else(|| Status::not_found(format!("inode {} not found", inode.0)))?;
item.name = new_name.clone();
item.local_path = new_local_path.clone();
item.parent_inode = parent_inode_from_path(&new_local_path);
}
ensure_virtual_dirs(&new_local_path, &source_for_virtual_dirs, &mut files);
let orphaned = find_orphaned_dirs(&files, inode, &old_local_path);
for dir_inode in &orphaned {
files.remove(dir_inode);
}
UpdateOutcome {
metadata: updated_mm,
new_name,
new_local_path,
orphaned_dirs: orphaned,
}
};
save_music_metadata(inode.0 as i64, &outcome.metadata, &self.db)
.await
.map_err(|e| Status::internal(format!("DB save failed: {e}")))?;
persist_layout_changes(
&self.db,
inode.0 as i64,
&outcome.new_name,
&outcome.new_local_path,
&outcome.orphaned_dirs,
)
.await
.map_err(|e| Status::internal(format!("DB layout save failed: {e}")))?;
tracing::debug!(
ino = inode.0,
orphaned_dirs = outcome.orphaned_dirs.len(),
"control: music metadata updated, layout recomputed"
);
Ok(Response::new(UpdateMusicMetadataResponse {
metadata: Some(music_metadata_to_proto(outcome.metadata)),
}))
}
async fn list_files(
&self,
_request: Request<ListFilesRequest>,
) -> Result<Response<ListFilesResponse>, Status> {
let entries: Vec<FileEntry> = {
let files = self.files.lock().unwrap();
files
.values()
.filter(|item| item.file_type == FileType::File)
.map(|item| FileEntry {
inode: item.inode.0,
original_path: item.original_path.to_string_lossy().into_owned(),
local_path: item.local_path.to_string_lossy().into_owned(),
name: item.name.clone(),
metadata: item
.music_metadata
.as_ref()
.map(|mm| music_metadata_to_file_metadata(mm)),
})
.collect()
};
Ok(Response::new(ListFilesResponse { files: entries }))
}
}
fn music_metadata_to_file_metadata(mm: &MusicMetadata) -> FileMetadata {
FileMetadata {
artist: mm.artist.clone(),
album_artist: mm.album_artist.clone(),
album: mm.album.clone(),
track_number: mm.track_number,
track_title: mm.track_title.clone(),
other_tags: mm.other_tags.clone(),
}
}
fn music_metadata_to_proto(mm: crate::music::metadata::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,
}
}
struct UpdateOutcome {
metadata: MusicMetadata,
new_name: String,
new_local_path: PathBuf,
orphaned_dirs: Vec<INodeNo>,
}
fn apply_update(mm: &mut MusicMetadata, req: UpdateMusicMetadataRequest) {
mm.artist = req.artist;
mm.album_artist = req.album_artist;
mm.album = req.album;
mm.track_number = req.track_number;
mm.track_title = req.track_title;
mm.other_tags = req.other_tags;
}
async fn persist_layout_changes(
db: &DatabaseConnection,
inode: i64,
new_name: &str,
new_local_path: &std::path::Path,
orphaned_dirs: &[INodeNo],
) -> Result<(), sea_orm::DbErr> {
use sea_orm::ActiveValue::Set;
let new_local_path_str = new_local_path.to_string_lossy().into_owned();
let update = entities::ActiveModel {
inode: Set(inode),
name: Set(new_name.to_string()),
local_path: Set(new_local_path_str),
..Default::default()
};
update.update(db).await?;
for dir_inode in orphaned_dirs {
let _ = entities::Entity::delete_by_id(dir_inode.0 as i64)
.exec(db)
.await;
}
Ok(())
}
+165
View File
@@ -0,0 +1,165 @@
use std::{
collections::BTreeMap,
net::SocketAddr,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::{SystemTime, UNIX_EPOCH},
};
use fuser::INodeNo;
use musicfs_proto::{
ClientControlServer, ClientStatus, ClientStatusResponse, ClientStatusServer,
GetClientStatusRequest,
};
use tokio::time::Duration;
use tonic::{Request, Response, transport::Server};
#[derive(Clone)]
pub struct ServerStatus {
connected: Arc<AtomicBool>,
last_reconcile: Arc<Mutex<Option<i64>>>,
origin_type: String,
mountpoint: String,
server_endpoint: String,
}
impl ServerStatus {
pub fn new_network(mountpoint: String, server_endpoint: String) -> Self {
ServerStatus {
connected: Arc::new(AtomicBool::new(false)),
last_reconcile: Arc::new(Mutex::new(None)),
origin_type: "network".to_string(),
mountpoint,
server_endpoint,
}
}
pub fn new_local(mountpoint: String) -> Self {
ServerStatus {
connected: Arc::new(AtomicBool::new(true)),
last_reconcile: Arc::new(Mutex::new(None)),
origin_type: "local".to_string(),
mountpoint,
server_endpoint: String::new(),
}
}
pub fn set_connected(&self, connected: bool) {
self.connected.store(connected, Ordering::Relaxed);
}
pub fn mark_reconciled(&self) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
*self.last_reconcile.lock().unwrap() = Some(now);
}
pub fn is_server_connected(&self) -> bool {
self.connected.load(Ordering::Relaxed)
}
}
pub async fn spawn_grpc_server(
addr: SocketAddr,
status: ServerStatus,
files: Arc<Mutex<BTreeMap<INodeNo, crate::item::Item>>>,
db: sea_orm::DatabaseConnection,
) {
let (reporter, health_service) = tonic_health::server::health_reporter();
reporter
.set_serving::<ClientStatusServer<ClientStatusServiceImpl>>()
.await;
let reporter_clone = reporter.clone();
let status_clone = status.clone();
tokio::spawn(async move {
let mut was_connected = status_clone.is_server_connected();
loop {
tokio::time::sleep(Duration::from_secs(2)).await;
let now_connected = status_clone.is_server_connected();
if now_connected != was_connected {
if now_connected {
reporter_clone
.set_serving::<ClientStatusServer<ClientStatusServiceImpl>>()
.await;
} else {
reporter_clone
.set_not_serving::<ClientStatusServer<ClientStatusServiceImpl>>()
.await;
}
was_connected = now_connected;
}
}
});
let reflection_v1 = tonic_reflection::server::Builder::configure()
.register_encoded_file_descriptor_set(musicfs_proto::musicfs::FILE_DESCRIPTOR_SET)
.register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET)
.build_v1()
.expect("build reflection v1");
let reflection_v1alpha = tonic_reflection::server::Builder::configure()
.register_encoded_file_descriptor_set(musicfs_proto::musicfs::FILE_DESCRIPTOR_SET)
.register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET)
.build_v1alpha()
.expect("build reflection v1alpha");
let status_svc = ClientStatusServer::new(ClientStatusServiceImpl {
status,
files: files.clone(),
});
let control_svc =
ClientControlServer::new(crate::control::ClientControlServiceImpl::new(files, db));
tracing::info!(%addr, "health server listening");
tokio::spawn(async move {
if let Err(e) = Server::builder()
.add_service(health_service)
.add_service(status_svc)
.add_service(reflection_v1)
.add_service(reflection_v1alpha)
.add_service(control_svc)
.serve(addr)
.await
{
tracing::error!(error = %e, "health server ended with error");
}
});
}
struct ClientStatusServiceImpl {
status: ServerStatus,
files: Arc<Mutex<BTreeMap<INodeNo, crate::item::Item>>>,
}
#[tonic::async_trait]
impl ClientStatus for ClientStatusServiceImpl {
async fn get_client_status(
&self,
_request: Request<GetClientStatusRequest>,
) -> Result<Response<ClientStatusResponse>, tonic::Status> {
let file_count = self
.files
.lock()
.unwrap()
.values()
.filter(|i| i.file_type == crate::item::FileType::File)
.count() as u64;
let last_reconcile_unix = self.status.last_reconcile.lock().unwrap().unwrap_or(0);
Ok(Response::new(ClientStatusResponse {
origin_type: self.status.origin_type.clone(),
mountpoint: self.status.mountpoint.clone(),
file_count,
server_connected: self.status.is_server_connected(),
server_endpoint: self.status.server_endpoint.clone(),
last_reconcile_unix,
}))
}
}
@@ -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),
);
)
}
}
+10
View File
@@ -0,0 +1,10 @@
pub mod control;
pub mod db;
pub mod health;
pub mod item;
pub mod music;
pub mod origins;
pub mod virtual_dirs;
pub use musicfs_core::logging;
pub use musicfs_proto as proto;
@@ -1,9 +1,10 @@
use std::{collections::HashMap, path::Path, sync::Arc};
use std::{collections::HashMap, net::SocketAddr, path::Path, sync::Arc};
use clap::Parser;
use fuser::INodeNo;
use musicfs::db::entities::{Entity, Model};
use musicfs::db::sync::sync_items_to_db;
use musicfs::health::ServerStatus;
use musicfs::item::Item;
use musicfs::logging::{LogConfig, init};
use musicfs::music::db::restore_music_metadata_from_db;
@@ -32,6 +33,10 @@ struct Args {
/// Directory for daily-rotated log files.
#[arg(long, default_value = "./logs")]
log_dir: std::path::PathBuf,
/// gRPC server address for health, status, and control RPCs.
#[arg(long, default_value = "127.0.0.1:50052")]
listen: SocketAddr,
}
#[tokio::main]
@@ -68,21 +73,25 @@ async fn main() {
});
info!(database = %args.database, "database connected");
let (snapshot, byte_source, watcher) = if looks_like_url(&args.source) {
let (snapshot, byte_source, watcher, server_status) = if looks_like_url(&args.source) {
debug!(source = %args.source, "using NetworkOrigin");
let origin = NetworkOrigin::new(args.source.clone(), mountpoint.clone(), db.clone())
.unwrap_or_else(|e| {
error!(source = %args.source, error = %e, "network origin init failed");
panic!("network origin init: {e}");
});
// Await directly on this runtime; the sync `snapshot()` wrapper would
// try to build a nested runtime and panic (we're inside #[tokio::main]).
let server_status = origin.server_status();
let snapshot = origin.snapshot_async().await.unwrap_or_else(|e| {
error!(source = %args.source, error = %e, "network initial snapshot failed");
panic!("network initial snapshot: {e}");
});
info!(files = snapshot.len(), "network snapshot complete");
(snapshot, origin.byte_source(), origin.watcher())
(
snapshot,
origin.byte_source(),
origin.watcher(),
server_status,
)
} else {
debug!(source = %args.source, "using LocalOrigin");
let origin = LocalOrigin::new(args.source.clone(), mountpoint.clone());
@@ -106,12 +115,20 @@ async fn main() {
restore_virtual_paths(&mut snapshot, &db_items, Path::new(&args.source));
info!(files = snapshot.len(), "local snapshot complete");
(snapshot, origin.byte_source(), origin.watcher())
let server_status = ServerStatus::new_local(mountpoint.clone());
(
snapshot,
origin.byte_source(),
origin.watcher(),
server_status,
)
};
let files: Arc<std::sync::Mutex<std::collections::BTreeMap<INodeNo, Item>>> =
Arc::new(std::sync::Mutex::new(snapshot));
let watcher_handle = watcher.watch(files.clone());
let health_files = files.clone();
let grpc_db = db.clone();
let fs = FuseFs {
files,
@@ -127,6 +144,8 @@ async fn main() {
});
info!(mountpoint = %mountpoint, "FUSE mounted");
musicfs::health::spawn_grpc_server(args.listen, server_status, health_files, grpc_db).await;
let mut sigint = signal(SignalKind::interrupt()).unwrap_or_else(|e| {
error!(error = %e, "failed to register SIGINT handler");
panic!("register SIGINT handler: {e}");
@@ -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};
pub 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;
@@ -29,17 +29,11 @@ use self::transport::NetworkTransport;
pub struct NetworkOrigin {
pub(crate) endpoint: String,
pub(crate) destination: PathBuf,
/// Handle to the process-wide runtime (main's `#[tokio::main]`). Used by the
/// sync FUSE byte/watcher paths to drive async transport via `block_on`.
/// A Handle (not an owned Runtime) so dropping NetworkOrigin in async
/// context can't panic, and the runtime outlives any clones we hand out.
handle: tokio::runtime::Handle,
transport: NetworkTransport,
client: sea_orm::DatabaseConnection,
/// In-memory cache of the latest manifest, keyed by server id. Written by
/// `snapshot()`, read by the watcher to know what state to reconcile
/// against on the next pass without round-tripping through the DB.
latest_manifest: Arc<RwLock<BTreeMap<u64, ProtoManifestEntry>>>,
server_status: crate::health::ServerStatus,
}
impl std::fmt::Debug for NetworkOrigin {
@@ -58,9 +52,10 @@ impl NetworkOrigin {
destination: String,
client: sea_orm::DatabaseConnection,
) -> io::Result<Self> {
// Must be called from within the process runtime (main's #[tokio::main]).
let handle = tokio::runtime::Handle::current();
let transport = NetworkTransport::new(endpoint.clone()).map_err(io_err)?;
let server_status =
crate::health::ServerStatus::new_network(destination.clone(), endpoint.clone());
return Ok(NetworkOrigin {
endpoint,
destination: destination.into(),
@@ -68,12 +63,17 @@ impl NetworkOrigin {
transport,
client,
latest_manifest: Arc::new(RwLock::new(BTreeMap::new())),
server_status,
});
}
pub fn runtime_handle(&self) -> tokio::runtime::Handle {
return self.handle.clone();
}
pub fn server_status(&self) -> crate::health::ServerStatus {
self.server_status.clone()
}
}
impl Origin for NetworkOrigin {
@@ -96,6 +96,7 @@ impl Origin for NetworkOrigin {
self.client.clone(),
self.destination.clone(),
self.latest_manifest.clone(),
self.server_status.clone(),
));
}
}
@@ -115,6 +116,7 @@ impl NetworkOrigin {
io_err(e)
})?
.into_iter()
.filter(|m| m.file_type == "file")
.map(|m| (m.inode as u64, m.hash as u64))
.collect();
@@ -166,59 +168,9 @@ impl NetworkOrigin {
}
*self.latest_manifest.write().unwrap() = current_manifest.clone();
// 5. Convert manifest entries → Items.
let dest = self.destination.clone();
let mut snapshot = BTreeMap::new();
let root_attrs = FileAttrs {
size: 0,
blocks: 0,
atime: SystemTime::UNIX_EPOCH,
mtime: SystemTime::UNIX_EPOCH,
ctime: SystemTime::UNIX_EPOCH,
crtime: SystemTime::UNIX_EPOCH,
perm: 0o755,
nlink: 2,
uid: 0,
gid: 0,
rdev: 0,
blksize: 4096,
};
snapshot.insert(
INodeNo::ROOT,
Item::new(
INodeNo::ROOT,
INodeNo::ROOT,
"/".to_string(),
dest.clone(),
dest.clone(),
FileType::Directory,
root_attrs,
None,
),
);
let source_root = PathBuf::from("/");
for (_id, entry) in &current_manifest {
let item = manifest_entry_to_item(entry, &source_root, &mut snapshot);
snapshot.insert(item.inode, item);
}
// 6. Reconcile with DB exactly like LocalOrigin: insert/update/delete
// rows, then restore music metadata + virtual paths.
let db_items: std::collections::HashMap<i64, item_entities::Model> =
item_entities::Entity::find()
.all(&self.client)
.await
.map_err(|e| {
error!(error = %e, "network snapshot: db_items DB read failed");
io_err(e)
})?
.into_iter()
.map(|e| (e.inode, e))
.collect();
sync_items_to_db(&snapshot, &db_items, &self.client).await;
restore_music_metadata_from_db(&mut snapshot, &db_items, &self.client).await;
restore_virtual_paths(&mut snapshot, &db_items, &source_root);
let snapshot =
build_snapshot_from_manifest(&current_manifest, &self.destination, &self.client)
.await?;
info!(
changed = response.changed.len(),
@@ -230,6 +182,70 @@ impl NetworkOrigin {
}
}
/// Build a complete Items snapshot from the server manifest, sync it to the
/// DB, and restore music metadata + virtual paths from existing DB rows.
///
/// Shared between `snapshot_async` (initial mount) and the watcher's
/// `reconcile_once` (runtime updates) so both paths produce identical
/// snapshots and keep the DB in sync.
pub(crate) async fn build_snapshot_from_manifest(
manifest: &BTreeMap<u64, ProtoManifestEntry>,
destination: &Path,
client: &sea_orm::DatabaseConnection,
) -> io::Result<BTreeMap<INodeNo, Item>> {
let source_root = PathBuf::from("/");
let mut snapshot = BTreeMap::new();
let root_attrs = FileAttrs {
size: 0,
blocks: 0,
atime: SystemTime::UNIX_EPOCH,
mtime: SystemTime::UNIX_EPOCH,
ctime: SystemTime::UNIX_EPOCH,
crtime: SystemTime::UNIX_EPOCH,
perm: 0o755,
nlink: 2,
uid: 0,
gid: 0,
rdev: 0,
blksize: 4096,
};
snapshot.insert(
INodeNo::ROOT,
Item::new(
INodeNo::ROOT,
INodeNo::ROOT,
"/".to_string(),
destination.to_path_buf(),
destination.to_path_buf(),
FileType::Directory,
root_attrs,
None,
),
);
for (_id, entry) in manifest {
let item = manifest_entry_to_item(entry, &source_root, &mut snapshot);
snapshot.insert(item.inode, item);
}
let db_items: std::collections::HashMap<i64, item_entities::Model> =
item_entities::Entity::find()
.all(client)
.await
.map_err(|e| {
error!(error = %e, "build_snapshot_from_manifest: DB read failed");
io_err(e)
})?
.into_iter()
.map(|e| (e.inode, e))
.collect();
sync_items_to_db(&snapshot, &db_items, client).await;
restore_music_metadata_from_db(&mut snapshot, &db_items, client).await;
restore_virtual_paths(&mut snapshot, &db_items, &source_root);
return Ok(snapshot);
}
fn manifest_entry_to_item(
entry: &ProtoManifestEntry,
source_root: &Path,
@@ -251,10 +267,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 {
@@ -288,26 +302,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,
}
}
@@ -1,4 +1,5 @@
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result, anyhow};
use tokio_stream::StreamExt;
@@ -28,6 +29,11 @@ impl NetworkTransport {
let endpoint: tonic::transport::Endpoint = url
.try_into()
.map_err(|e| anyhow!("invalid server URL: {e}"))?;
let endpoint = endpoint
.timeout(Duration::from_secs(3))
.connect_timeout(Duration::from_secs(5))
.http2_keep_alive_interval(Duration::from_secs(10))
.keep_alive_timeout(Duration::from_secs(5));
let channel = endpoint.connect_lazy();
let client = Arc::new(tokio::sync::Mutex::new(MusicFsClient::new(channel)));
return Ok(NetworkTransport { client, endpoint });
@@ -17,7 +17,8 @@ use crate::origins::{FileWatcher, WatcherHandle};
use crate::proto::ManifestEntry as ProtoManifestEntry;
use tracing::{info, warn};
const POLL_FALLBACK_INTERVAL: Duration = Duration::from_secs(30);
const INITIAL_RETRY_DELAY: Duration = Duration::from_secs(1);
const MAX_RETRY_DELAY: Duration = Duration::from_secs(30);
pub struct NetworkOriginFileWatcher {
transport: NetworkTransport,
@@ -25,6 +26,7 @@ pub struct NetworkOriginFileWatcher {
client: DatabaseConnection,
destination: PathBuf,
latest_manifest: Arc<RwLock<BTreeMap<u64, ProtoManifestEntry>>>,
server_status: crate::health::ServerStatus,
}
impl NetworkOriginFileWatcher {
@@ -35,6 +37,7 @@ impl NetworkOriginFileWatcher {
client: DatabaseConnection,
destination: PathBuf,
latest_manifest: Arc<RwLock<BTreeMap<u64, ProtoManifestEntry>>>,
server_status: crate::health::ServerStatus,
) -> Self {
return NetworkOriginFileWatcher {
transport,
@@ -42,15 +45,11 @@ impl NetworkOriginFileWatcher {
client,
destination,
latest_manifest,
server_status,
};
}
}
/// `files` is currently unused at runtime by the network watcher — the
/// authoritative post-reconcile snapshot is written to the DB, and the next
/// FUSE `readdir`/`lookup` will reflect it because FuseFs re-locks the map.
/// The trait signature still requires it for parity with LocalOriginFileWatcher;
/// a future refactor can rebuild the map in place here.
impl FileWatcher for NetworkOriginFileWatcher {
fn watch(&self, files: Arc<std::sync::Mutex<BTreeMap<INodeNo, Item>>>) -> WatcherHandle {
let runtime_handle = self.runtime_handle.clone();
@@ -60,6 +59,7 @@ impl FileWatcher for NetworkOriginFileWatcher {
client: self.client.clone(),
destination: self.destination.clone(),
latest_manifest: self.latest_manifest.clone(),
server_status: self.server_status.clone(),
files,
};
let loop_shutdown = shutdown.clone();
@@ -73,17 +73,18 @@ impl FileWatcher for NetworkOriginFileWatcher {
}
}
#[derive(Clone)]
struct WatcherState {
transport: NetworkTransport,
client: DatabaseConnection,
destination: PathBuf,
latest_manifest: Arc<RwLock<BTreeMap<u64, ProtoManifestEntry>>>,
server_status: crate::health::ServerStatus,
files: Arc<std::sync::Mutex<BTreeMap<INodeNo, Item>>>,
}
impl WatcherState {
async fn run_loop(self, shutdown: Arc<Notify>) {
let mut retry_delay = INITIAL_RETRY_DELAY;
loop {
let subscribed = tokio::select! {
biased;
@@ -93,6 +94,8 @@ impl WatcherState {
match subscribed {
Ok(mut stream) => {
info!("network watcher: subscribed to /events");
self.server_status.set_connected(true);
retry_delay = INITIAL_RETRY_DELAY;
loop {
let item = tokio::select! {
biased;
@@ -107,21 +110,27 @@ impl WatcherState {
}
Some(Err(e)) => {
warn!(error = %e, "network watcher: stream error; reconnecting");
self.server_status.set_connected(false);
break;
}
None => {
self.server_status.set_connected(false);
break;
}
None => break,
}
}
}
Err(e) => {
self.server_status.set_connected(false);
warn!(error = %e, "network watcher: subscribe failed; will retry");
}
}
tokio::select! {
biased;
_ = shutdown.notified() => return,
_ = tokio::time::sleep(POLL_FALLBACK_INTERVAL) => {}
_ = tokio::time::sleep(retry_delay) => {}
}
retry_delay = (retry_delay * 2).min(MAX_RETRY_DELAY);
if let Err(e) = self.reconcile_once().await {
warn!(error = %e, "network watcher: poll reconcile failed");
}
@@ -133,6 +142,7 @@ impl WatcherState {
.all(&self.client)
.await?
.into_iter()
.filter(|m| m.file_type == "file")
.map(|m| (m.inode as u64, m.hash as u64))
.collect();
@@ -156,8 +166,31 @@ impl WatcherState {
}
*self.latest_manifest.write().unwrap() = current_manifest.clone();
let _ = self.destination.clone();
let _ = self.files.clone();
let new_snapshot =
super::build_snapshot_from_manifest(&current_manifest, &self.destination, &self.client)
.await?;
{
let mut files = self.files.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.clone());
}
}
}
}
self.server_status.mark_reconciled();
info!(
changed = response.changed.len(),
deleted = response.deleted.len(),
total = current_manifest.len(),
"network watcher: reconcile applied"
);
return Ok(());
}
+531
View File
@@ -0,0 +1,531 @@
use std::{
collections::BTreeMap,
fs,
hash::Hasher,
path::{Path, PathBuf},
};
use fuser::INodeNo;
use twox_hash::XxHash64;
use crate::db::entities::Model;
use crate::item::{FileType, Item};
use crate::music::metadata::MusicMetadata;
use crate::origins::attrs::FileAttrs;
pub fn virtual_inode(path: &str) -> INodeNo {
let mut hasher = XxHash64::with_seed(5678);
hasher.write(path.as_bytes());
return INodeNo(hasher.finish() | (1u64 << 63));
}
pub fn parent_inode_from_path(local_path: &Path) -> INodeNo {
let components: Vec<_> = local_path
.components()
.filter(|c| matches!(c, std::path::Component::Normal(_)))
.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());
}
return virtual_inode(&parent_path);
}
pub fn ensure_virtual_dirs(
local_path: &Path,
source: &Path,
map: &mut BTreeMap<INodeNo, Item>,
) -> INodeNo {
let components: Vec<_> = local_path
.components()
.filter(|c| matches!(c, std::path::Component::Normal(_)))
.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 = virtual_inode(&current_path);
if !map.contains_key(&virt_ino) {
let virt_item = Item::new(
virt_ino,
current_parent,
comp_str.to_string(),
source.to_path_buf(),
PathBuf::from(&current_path),
FileType::Directory,
FileAttrs::from(&fs::metadata(source).unwrap()),
None,
);
map.insert(virt_ino, virt_item);
}
current_parent = virt_ino;
}
return current_parent;
}
pub fn restore_virtual_paths(
snapshot: &mut BTreeMap<INodeNo, Item>,
db_items: &std::collections::HashMap<i64, Model>,
source: &Path,
) {
let restorations: Vec<(INodeNo, String, PathBuf)> = db_items
.values()
.filter_map(|db_item| {
let ino = INodeNo(db_item.inode as u64);
if ino == INodeNo::ROOT {
return None;
}
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 {
ensure_virtual_dirs(local_path, source, snapshot);
}
for (ino, name, local_path) in restorations {
if let Some(item) = snapshot.get_mut(&ino) {
item.name = name;
item.parent_inode = parent_inode_from_path(&local_path);
item.local_path = local_path;
}
}
}
/// Computes the new (name, local_path) for an Item based on its updated
/// `MusicMetadata`. Mirrors the layout logic in
/// `origins::local::snapshot::read_into_map`:
/// - artist dir = `album_artist` if set, else `artists.join("-")`
/// - if `track_title` is non-empty, the filename becomes
/// `{track_number:02} - {track_title}.{ext}` (zero-padded track number
/// when > 0, otherwise just `{track_title}.{ext}`), ext preserved from
/// `current_name`
/// - otherwise the filename is unchanged
/// Returns `(new_name, new_local_path)`.
pub fn compute_new_layout(current_name: &str, mm: &MusicMetadata) -> (String, PathBuf) {
let artist_dir = artist_dir(mm);
let filename = match &mm.track_title {
title if !title.is_empty() => {
let stem = if mm.track_number > 0 {
format!("{:02} - {}", mm.track_number, title)
} else {
title.clone()
};
rename_with_extension(current_name, &stem)
}
_ => current_name.to_string(),
};
let mut local_path = PathBuf::new();
local_path.push(&artist_dir);
local_path.push(&mm.album);
local_path.push(&filename);
(filename, local_path)
}
fn artist_dir(mm: &MusicMetadata) -> String {
match &mm.album_artist {
Some(a) if !a.is_empty() => a.clone(),
_ => mm.artist.join("-"),
}
}
fn rename_with_extension(current_name: &str, new_stem: &str) -> String {
let current_path = Path::new(current_name);
match current_path.extension() {
Some(ext) => format!("{new_stem}.{}", ext.to_string_lossy()),
None => new_stem.to_string(),
}
}
/// Returns the virtual inodes of directories in the `old_local_path`'s
/// parent chain that will have no remaining files after this item moves
/// away. Walks the chain from the deepest directory upward; stops at the
/// first directory that still has other files living in it.
///
/// `moving_inode` is the actual inode of the file being relocated (the
/// request's `inode`), used to exclude it from the "any other files here?"
/// check. `old_local_path` is the LOCAL (virtual FUSE) path of the file
/// before its metadata changed.
pub fn find_orphaned_dirs(
files: &BTreeMap<INodeNo, Item>,
moving_inode: INodeNo,
old_local_path: &Path,
) -> Vec<INodeNo> {
let components: Vec<_> = old_local_path
.components()
.filter(|c| matches!(c, std::path::Component::Normal(_)))
.collect();
if components.len() <= 1 {
return vec![];
}
let mut orphaned = vec![];
for depth in (1..components.len()).rev() {
let dir_path: PathBuf = components[..depth].iter().map(|c| c.as_os_str()).collect();
let dir_path_str = dir_path.to_string_lossy().into_owned();
let dir_inode = virtual_inode(&dir_path_str);
// Path-prefix match: a file at "Artist/Album2/b.flac" still keeps
// "Artist" non-orphan even though its direct parent is "Artist/Album2".
let prefix = format!("{dir_path_str}/");
let has_other_files = files.values().any(|item| {
item.inode != moving_inode
&& item.file_type == FileType::File
&& item.local_path.starts_with(&prefix)
});
if has_other_files {
break;
}
orphaned.push(dir_inode);
}
orphaned
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
// ---- compute_new_layout ----
fn mm(artist: &str, album: &str, title: &str) -> MusicMetadata {
MusicMetadata {
artist: vec![artist.to_string()],
album_artist: None,
album: album.to_string(),
track_number: 1,
track_title: title.to_string(),
other_tags: vec![],
header: vec![],
picture_block_headers: vec![],
picture_data_ranges: vec![],
real_audio_start: 0,
vorbis_comment_offset: 0,
vorbis_comment_length: 0,
}
}
fn file_item(inode: u64, name: &str, local_path: &str) -> Item {
Item {
inode: INodeNo(inode),
parent_inode: parent_inode_from_path(Path::new(local_path)),
name: name.to_string(),
original_path: format!("/torrent/{name}").into(),
local_path: local_path.into(),
file_type: FileType::File,
attrs: FileAttrs {
size: 1,
blocks: 1,
atime: std::time::SystemTime::UNIX_EPOCH,
mtime: std::time::SystemTime::UNIX_EPOCH,
ctime: std::time::SystemTime::UNIX_EPOCH,
crtime: std::time::SystemTime::UNIX_EPOCH,
perm: 0o644,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
blksize: 4096,
},
music_metadata: None,
hash: inode,
}
}
#[test]
fn compute_new_layout_renames_file_when_track_title_provided() {
let mm = mm(
"Pink Floyd",
"Wish You Were Here",
"Shine On You Crazy Diamond",
);
let (name, path) = compute_new_layout("01-track.flac", &mm);
assert_eq!(name, "01 - Shine On You Crazy Diamond.flac");
assert_eq!(
path,
PathBuf::from("Pink Floyd/Wish You Were Here/01 - Shine On You Crazy Diamond.flac")
);
}
#[test]
fn compute_new_layout_preserves_extension_through_rename() {
let mm = mm("A", "B", "New Title");
let (name, _) = compute_new_layout("old.mp3", &mm);
assert_eq!(name, "01 - New Title.mp3");
}
#[test]
fn compute_new_layout_handles_multi_dot_extensions() {
let mm = mm("A", "B", "New");
let (name, _) = compute_new_layout("old.tar.gz", &mm);
assert_eq!(name, "01 - New.gz", "only the final extension is preserved");
}
#[test]
fn compute_new_layout_keeps_original_name_when_track_title_empty() {
let mut metadata = mm("A", "B", "ignored");
metadata.track_title = String::new();
let (name, path) = compute_new_layout("original-name.flac", &metadata);
assert_eq!(name, "original-name.flac");
assert_eq!(path, PathBuf::from("A/B/original-name.flac"));
}
#[test]
fn compute_new_layout_uses_album_artist_when_present() {
let mut metadata = mm("Secondary", "Album", "Title");
metadata.album_artist = Some("Primary Artist".to_string());
let (_, path) = compute_new_layout("track.flac", &metadata);
assert_eq!(path, PathBuf::from("Primary Artist/Album/01 - Title.flac"));
}
#[test]
fn compute_new_layout_joins_multiple_artists_with_dash_when_no_album_artist() {
let mut metadata = mm("Foo", "Album", "Title");
metadata.artist = vec!["Foo".to_string(), "Bar".to_string()];
let (_, path) = compute_new_layout("track.flac", &metadata);
assert_eq!(path, PathBuf::from("Foo-Bar/Album/01 - Title.flac"));
}
// ---- find_orphaned_dirs ----
#[test]
fn find_orphaned_dirs_returns_both_album_and_artist_when_last_file_moves() {
let mut files = BTreeMap::new();
files.insert(
INodeNo(100),
file_item(100, "track.flac", "Artist/Album/track.flac"),
);
let orphaned =
find_orphaned_dirs(&files, INodeNo(100), Path::new("Artist/Album/track.flac"));
assert_eq!(orphaned.len(), 2);
assert!(orphaned.contains(&virtual_inode("Artist/Album")));
assert!(orphaned.contains(&virtual_inode("Artist")));
}
#[test]
fn find_orphaned_dirs_returns_album_only_when_other_album_keeps_artist() {
let mut files = BTreeMap::new();
files.insert(
INodeNo(100),
file_item(100, "a.flac", "Artist/Album1/a.flac"),
);
files.insert(
INodeNo(101),
file_item(101, "b.flac", "Artist/Album2/b.flac"),
);
let orphaned = find_orphaned_dirs(&files, INodeNo(100), Path::new("Artist/Album1/a.flac"));
assert_eq!(orphaned, vec![virtual_inode("Artist/Album1")]);
}
#[test]
fn find_orphaned_dirs_returns_nothing_when_other_files_remain_in_same_album() {
let mut files = BTreeMap::new();
files.insert(
INodeNo(100),
file_item(100, "a.flac", "Artist/Album/a.flac"),
);
files.insert(
INodeNo(101),
file_item(101, "b.flac", "Artist/Album/b.flac"),
);
let orphaned = find_orphaned_dirs(&files, INodeNo(100), Path::new("Artist/Album/a.flac"));
assert!(orphaned.is_empty());
}
#[test]
fn find_orphaned_dirs_returns_nothing_for_file_at_root() {
let mut files = BTreeMap::new();
files.insert(INodeNo(100), file_item(100, "track.flac", "track.flac"));
let orphaned = find_orphaned_dirs(&files, INodeNo(100), Path::new("track.flac"));
assert!(orphaned.is_empty());
}
#[test]
fn find_orphaned_dirs_excludes_moving_file_from_other_file_check() {
let mut files = BTreeMap::new();
files.insert(
INodeNo(100),
file_item(100, "only.flac", "Artist/Album/only.flac"),
);
// If we forgot to exclude the moving inode, this test would return
// empty (thinking the dir is still populated). Must return both dirs.
let orphaned =
find_orphaned_dirs(&files, INodeNo(100), Path::new("Artist/Album/only.flac"));
assert_eq!(orphaned.len(), 2);
}
#[test]
fn virtual_inode_deterministic() {
let ino1 = virtual_inode("foo");
let ino2 = virtual_inode("foo");
assert_eq!(ino1, ino2);
}
#[test]
fn virtual_inode_high_bit_set() {
let ino = virtual_inode("test");
assert_eq!(ino.0 & (1u64 << 63), 1u64 << 63);
}
#[test]
fn virtual_inode_different_inputs() {
let ino1 = virtual_inode("foo");
let ino2 = virtual_inode("bar");
assert_ne!(ino1, ino2);
}
#[test]
fn parent_inode_single_component() {
let ino = parent_inode_from_path(Path::new("file.txt"));
assert_eq!(ino, INodeNo::ROOT);
}
#[test]
fn parent_inode_multi_component() {
let ino = parent_inode_from_path(Path::new("a/b/c"));
let expected = virtual_inode("a/b");
assert_eq!(ino, expected);
}
#[test]
fn parent_inode_absolute_path_filtered() {
let ino = parent_inode_from_path(Path::new("/file"));
assert_eq!(ino, INodeNo::ROOT);
}
#[test]
fn ensure_virtual_dirs_single_component() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
let mut map = BTreeMap::new();
let ino = ensure_virtual_dirs(Path::new("file.txt"), source, &mut map);
assert_eq!(ino, INodeNo::ROOT);
assert!(map.is_empty());
}
#[test]
fn ensure_virtual_dirs_creates_hierarchy() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
let mut map = BTreeMap::new();
let ino = ensure_virtual_dirs(Path::new("a/b/file"), source, &mut map);
let a_ino = virtual_inode("a");
let ab_ino = virtual_inode("a/b");
assert_eq!(ino, ab_ino);
assert_eq!(map.len(), 2);
assert!(map.contains_key(&a_ino));
assert!(map.contains_key(&ab_ino));
let a_item = &map[&a_ino];
assert_eq!(a_item.parent_inode, INodeNo::ROOT);
let ab_item = &map[&ab_ino];
assert_eq!(ab_item.parent_inode, a_ino);
}
#[test]
fn ensure_virtual_dirs_idempotent() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
let mut map = BTreeMap::new();
ensure_virtual_dirs(Path::new("a/b/file"), source, &mut map);
let size_after_first = map.len();
ensure_virtual_dirs(Path::new("a/b/file"), source, &mut map);
let size_after_second = map.len();
assert_eq!(size_after_first, size_after_second);
}
#[test]
fn restore_virtual_paths_skips_root() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
let mut snapshot = BTreeMap::new();
let real_ino = INodeNo(42);
let real_item = Item::new(
real_ino,
INodeNo::ROOT,
"real_file".to_string(),
source.to_path_buf(),
PathBuf::from("real_file"),
FileType::File,
FileAttrs::from(&fs::metadata(source).unwrap()),
None,
);
snapshot.insert(real_ino, real_item);
let mut db_items = HashMap::new();
db_items.insert(
1i64,
Model {
inode: 1,
hash: 0,
name: "root".to_string(),
original_path: "/tmp/test".to_string(),
local_path: "/tmp/test".to_string(),
file_type: "directory".to_string(),
},
);
db_items.insert(
42i64,
Model {
inode: 42,
hash: 0,
name: "real_file".to_string(),
original_path: "real_file".to_string(),
local_path: "real_file".to_string(),
file_type: "file".to_string(),
},
);
restore_virtual_paths(&mut snapshot, &db_items, source);
for item in snapshot.values() {
assert_ne!(item.name, "/");
assert_ne!(item.name, "tmp");
}
}
}
+562
View File
@@ -0,0 +1,562 @@
use std::{
collections::BTreeMap,
sync::{Arc, Mutex},
time::SystemTime,
};
use fuser::INodeNo;
use musicfs::control::ClientControlServiceImpl;
use musicfs::db::entities;
use musicfs::item::{FileType, Item};
use musicfs::music::db::entities::{artists, music_metadata as mm_entity};
use musicfs::music::metadata::MusicMetadata;
use musicfs::origins::attrs::FileAttrs;
use musicfs_proto::{
ClientControl, GetMusicMetadataRequest, ListFilesRequest, UpdateMusicMetadataRequest,
};
use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult};
use tonic::{Code, Request};
// ── Test helpers ───────────────────────────────────────────────────────
fn make_attrs() -> FileAttrs {
FileAttrs {
size: 4096,
blocks: 8,
atime: SystemTime::UNIX_EPOCH,
mtime: SystemTime::UNIX_EPOCH,
ctime: SystemTime::UNIX_EPOCH,
crtime: SystemTime::UNIX_EPOCH,
perm: 0o644,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
blksize: 4096,
}
}
fn make_flac_item(inode: u64, name: &str, metadata: Option<MusicMetadata>) -> Item {
Item {
inode: INodeNo(inode),
parent_inode: INodeNo(1),
name: name.to_string(),
original_path: format!("/{name}").into(),
local_path: format!("Artist/Album/{name}").into(),
file_type: FileType::File,
attrs: make_attrs(),
music_metadata: metadata,
hash: inode,
}
}
fn make_flac_metadata() -> MusicMetadata {
MusicMetadata {
artist: vec!["Test Artist".to_string()],
album_artist: Some("Test Artist".to_string()),
album: "Test Album".to_string(),
track_number: 1,
track_title: "Test Track".to_string(),
other_tags: vec![],
header: vec![],
picture_block_headers: vec![],
picture_data_ranges: vec![],
real_audio_start: 0,
vorbis_comment_offset: 0,
vorbis_comment_length: 0,
}
}
/// Mock DB for get tests (no INSERT operations expected).
fn mock_db_readonly() -> sea_orm::DatabaseConnection {
MockDatabase::new(DatabaseBackend::Postgres).into_connection()
}
/// Mock DB for update with one artist.
/// save_music_metadata does: DELETE, INSERT music_metadata, INSERT artists.
/// persist_layout_changes does: UPDATE items (RETURNING), then one DELETE
/// per orphaned virtual directory (worst case = 2: artist + album dirs).
fn mock_db_update_with_artist(inode: i64) -> sea_orm::DatabaseConnection {
MockDatabase::new(DatabaseBackend::Postgres)
.append_exec_results([MockExecResult {
rows_affected: 1,
..Default::default()
}])
.append_query_results([vec![mm_entity::Model {
inode,
track_title: String::new(),
album: String::new(),
track_number: 0,
header: vec![],
real_audio_start: 0,
}]])
.append_query_results([vec![artists::Model {
inode,
artist: String::new(),
}]])
.append_query_results([vec![entities::Model {
inode,
name: String::new(),
original_path: String::new(),
local_path: String::new(),
file_type: "file".to_string(),
hash: 0,
}]])
.append_exec_results([MockExecResult {
rows_affected: 1,
..Default::default()
}])
.append_exec_results([MockExecResult {
rows_affected: 1,
..Default::default()
}])
.into_connection()
}
/// Mock DB for update with no artists.
/// save_music_metadata does: DELETE, INSERT music_metadata only.
/// persist_layout_changes does: UPDATE items (RETURNING), then up to two
/// DELETEs for orphaned virtual directories.
fn mock_db_update_no_artist(inode: i64) -> sea_orm::DatabaseConnection {
MockDatabase::new(DatabaseBackend::Postgres)
.append_exec_results([MockExecResult {
rows_affected: 1,
..Default::default()
}])
.append_query_results([vec![mm_entity::Model {
inode,
track_title: String::new(),
album: String::new(),
track_number: 0,
header: vec![],
real_audio_start: 0,
}]])
.append_query_results([vec![entities::Model {
inode,
name: String::new(),
original_path: String::new(),
local_path: String::new(),
file_type: "file".to_string(),
hash: 0,
}]])
.append_exec_results([MockExecResult {
rows_affected: 1,
..Default::default()
}])
.append_exec_results([MockExecResult {
rows_affected: 1,
..Default::default()
}])
.into_connection()
}
fn make_dir_item(inode: u64, name: &str) -> Item {
Item {
inode: INodeNo(inode),
parent_inode: INodeNo(1),
name: name.to_string(),
original_path: format!("/{name}").into(),
local_path: name.into(),
file_type: FileType::Directory,
attrs: make_attrs(),
music_metadata: None,
hash: inode,
}
}
#[tokio::test]
async fn update_removes_orphaned_virtual_dirs_when_last_file_moves_out() {
let source_dir = tempfile::tempdir().unwrap();
let source = source_dir.path();
let mut files = BTreeMap::new();
files.insert(
INodeNo(0),
Item {
inode: INodeNo(0),
parent_inode: INodeNo::ROOT,
name: "/".to_string(),
original_path: source.to_path_buf(),
local_path: "/".into(),
file_type: FileType::Directory,
attrs: make_attrs(),
music_metadata: None,
hash: 0,
},
);
files.insert(
INodeNo(100),
make_flac_item(100, "song.flac", Some(make_flac_metadata())),
);
let files = Arc::new(Mutex::new(files));
let svc = ClientControlServiceImpl::new(files.clone(), mock_db_update_with_artist(100));
// Pre-create the virtual dirs for the file's current layout so we can
// assert they get pruned after the metadata update moves the file.
{
use musicfs::virtual_dirs::{ensure_virtual_dirs, virtual_inode};
let mut guard = files.lock().unwrap();
ensure_virtual_dirs(
std::path::Path::new("Artist/Album/song.flac"),
source,
&mut guard,
);
assert!(guard.contains_key(&virtual_inode("Artist")));
assert!(guard.contains_key(&virtual_inode("Artist/Album")));
}
svc.update_music_metadata(Request::new(UpdateMusicMetadataRequest {
inode: 100,
artist: vec!["New Artist".to_string()],
album_artist: Some("New Artist".to_string()),
album: "New Album".to_string(),
track_number: 1,
track_title: "New Title".to_string(),
other_tags: vec![],
}))
.await
.expect("update should succeed");
let guard = files.lock().unwrap();
use musicfs::virtual_dirs::virtual_inode;
assert!(
!guard.contains_key(&virtual_inode("Artist")),
"old Artist dir must be removed when no files remain under it"
);
assert!(
!guard.contains_key(&virtual_inode("Artist/Album")),
"old Album dir must be removed"
);
assert!(guard.contains_key(&virtual_inode("New Artist")));
assert!(guard.contains_key(&virtual_inode("New Artist/New Album")));
let item = guard.get(&INodeNo(100)).expect("file still present");
assert_eq!(
item.local_path,
std::path::PathBuf::from("New Artist/New Album/01 - New Title.flac")
);
}
#[tokio::test]
async fn update_keeps_shared_artist_dir_when_other_album_remains() {
let source_dir = tempfile::tempdir().unwrap();
let source = source_dir.path();
let mut files = BTreeMap::new();
files.insert(
INodeNo(0),
Item {
inode: INodeNo(0),
parent_inode: INodeNo::ROOT,
name: "/".to_string(),
original_path: source.to_path_buf(),
local_path: "/".into(),
file_type: FileType::Directory,
attrs: make_attrs(),
music_metadata: None,
hash: 0,
},
);
files.insert(
INodeNo(100),
make_flac_item(100, "a.flac", Some(make_flac_metadata())),
);
files.insert(
INodeNo(101),
make_flac_item(101, "b.flac", Some(make_flac_metadata())),
);
files.get_mut(&INodeNo(101)).unwrap().local_path = "Artist/Other Album/b.flac".into();
// Pre-create virtual dirs for both files' layouts: pruning after the
// update should remove Album/ (file 100's old parent) but keep Artist/
// (still has Other Album/ with file 101 in it).
{
use musicfs::virtual_dirs::ensure_virtual_dirs;
ensure_virtual_dirs(
std::path::Path::new("Artist/Album/a.flac"),
source,
&mut files,
);
ensure_virtual_dirs(
std::path::Path::new("Artist/Other Album/b.flac"),
source,
&mut files,
);
}
let files = Arc::new(Mutex::new(files));
let svc = ClientControlServiceImpl::new(files.clone(), mock_db_update_with_artist(100));
svc.update_music_metadata(Request::new(UpdateMusicMetadataRequest {
inode: 100,
artist: vec!["New Artist".to_string()],
album_artist: Some("New Artist".to_string()),
album: "New Album".to_string(),
track_number: 1,
track_title: "New Title".to_string(),
other_tags: vec![],
}))
.await
.expect("update should succeed");
let guard = files.lock().unwrap();
use musicfs::virtual_dirs::virtual_inode;
assert!(
guard.contains_key(&virtual_inode("Artist")),
"Artist dir must remain while Other Album still has files"
);
assert!(
!guard.contains_key(&virtual_inode("Artist/Album")),
"Artist/Album dir must be removed"
);
}
// ── ListFiles ─────────────────────────────────────────────────────────
#[tokio::test]
async fn list_files_returns_all_files_with_full_field_projection() {
let mut files = BTreeMap::new();
files.insert(
INodeNo(100),
make_flac_item(100, "a.flac", Some(make_flac_metadata())),
);
files.insert(INodeNo(101), make_flac_item(101, "b.flac", None));
// Directory entries must be filtered out.
files.insert(INodeNo(2), make_dir_item(2, "Artist"));
let files = Arc::new(Mutex::new(files));
let svc = ClientControlServiceImpl::new(files, mock_db_readonly());
let resp = svc
.list_files(Request::new(ListFilesRequest {}))
.await
.expect("list should succeed");
let mut entries = resp.into_inner().files;
assert_eq!(entries.len(), 2, "directories must be filtered out");
entries.sort_by_key(|e| e.inode);
assert_eq!(entries[0].inode, 100);
assert_eq!(entries[0].name, "a.flac");
assert_eq!(entries[0].original_path, "/a.flac");
assert_eq!(entries[0].local_path, "Artist/Album/a.flac");
assert!(entries[0].metadata.is_some());
assert_eq!(entries[1].inode, 101);
assert_eq!(entries[1].name, "b.flac");
assert!(entries[1].metadata.is_none(), "no metadata on this file");
}
#[tokio::test]
async fn list_files_empty_map_returns_empty_list() {
let files = Arc::new(Mutex::new(BTreeMap::new()));
let svc = ClientControlServiceImpl::new(files, mock_db_readonly());
let resp = svc
.list_files(Request::new(ListFilesRequest {}))
.await
.expect("list should succeed");
assert!(resp.into_inner().files.is_empty());
}
#[tokio::test]
async fn list_files_skips_directories_and_root() {
let mut files = BTreeMap::new();
files.insert(INodeNo(0), make_dir_item(0, "/")); // root
files.insert(INodeNo(1), make_dir_item(1, "Artist"));
files.insert(INodeNo(2), make_dir_item(2, "Album"));
files.insert(INodeNo(100), make_flac_item(100, "track.flac", None));
let files = Arc::new(Mutex::new(files));
let svc = ClientControlServiceImpl::new(files, mock_db_readonly());
let resp = svc
.list_files(Request::new(ListFilesRequest {}))
.await
.expect("list should succeed");
let inodes: Vec<u64> = resp
.into_inner()
.files
.into_iter()
.map(|e| e.inode)
.collect();
assert_eq!(inodes, vec![100]);
}
#[tokio::test]
async fn get_returns_metadata_for_valid_inode() {
let mut files = BTreeMap::new();
files.insert(
INodeNo(100),
make_flac_item(100, "song.flac", Some(make_flac_metadata())),
);
let files = Arc::new(Mutex::new(files));
let svc = ClientControlServiceImpl::new(files, mock_db_readonly());
let resp = svc
.get_music_metadata(Request::new(GetMusicMetadataRequest { inode: 100 }))
.await
.expect("get should succeed");
let md = resp.into_inner().metadata.expect("metadata present");
assert_eq!(md.track_title, "Test Track");
assert_eq!(md.album, "Test Album");
assert_eq!(md.artist, vec!["Test Artist".to_string()]);
assert_eq!(md.track_number, 1);
}
#[tokio::test]
async fn get_returns_not_found_for_missing_inode() {
let files = Arc::new(Mutex::new(BTreeMap::new()));
let svc = ClientControlServiceImpl::new(files, mock_db_readonly());
let err = svc
.get_music_metadata(Request::new(GetMusicMetadataRequest { inode: 999 }))
.await
.expect_err("should be NOT_FOUND");
assert_eq!(err.code(), Code::NotFound);
}
#[tokio::test]
async fn get_returns_not_found_when_item_has_no_metadata() {
let mut files = BTreeMap::new();
files.insert(INodeNo(100), make_flac_item(100, "song.flac", None));
let files = Arc::new(Mutex::new(files));
let svc = ClientControlServiceImpl::new(files, mock_db_readonly());
let err = svc
.get_music_metadata(Request::new(GetMusicMetadataRequest { inode: 100 }))
.await
.expect_err("should be NOT_FOUND");
assert_eq!(err.code(), Code::NotFound);
}
// ── UpdateMusicMetadata ───────────────────────────────────────────────
#[tokio::test]
async fn update_changes_tags_and_response_reflects_new_values() {
let mut files = BTreeMap::new();
files.insert(
INodeNo(100),
make_flac_item(100, "song.flac", Some(make_flac_metadata())),
);
let files = Arc::new(Mutex::new(files));
let svc = ClientControlServiceImpl::new(files.clone(), mock_db_update_with_artist(100));
let resp = svc
.update_music_metadata(Request::new(UpdateMusicMetadataRequest {
inode: 100,
artist: vec!["Updated Artist".to_string()],
album_artist: Some("Updated Artist".to_string()),
album: "Updated Album".to_string(),
track_number: 7,
track_title: "Updated Title".to_string(),
other_tags: vec![],
}))
.await
.expect("update should succeed");
let md = resp.into_inner().metadata.expect("metadata present");
assert_eq!(md.track_title, "Updated Title");
assert_eq!(md.album, "Updated Album");
assert_eq!(md.artist, vec!["Updated Artist".to_string()]);
assert_eq!(md.track_number, 7);
let guard = files.lock().unwrap();
let item = guard.get(&INodeNo(100)).expect("item still in map");
let mm = item.music_metadata.as_ref().expect("metadata present");
assert_eq!(mm.track_title, "Updated Title");
assert_eq!(mm.album, "Updated Album");
assert_eq!(mm.artist, vec!["Updated Artist".to_string()]);
assert_eq!(mm.track_number, 7);
// Layout was recomputed from the new metadata.
assert_eq!(item.name, "07 - Updated Title.flac");
assert_eq!(
item.local_path,
std::path::PathBuf::from("Updated Artist/Updated Album/07 - Updated Title.flac")
);
}
#[tokio::test]
async fn update_returns_not_found_for_missing_inode() {
let files = Arc::new(Mutex::new(BTreeMap::new()));
let svc = ClientControlServiceImpl::new(files, mock_db_readonly());
let err = svc
.update_music_metadata(Request::new(UpdateMusicMetadataRequest {
inode: 999,
artist: vec![],
album_artist: None,
album: String::new(),
track_number: 0,
track_title: String::new(),
other_tags: vec![],
}))
.await
.expect_err("should be NOT_FOUND");
assert_eq!(err.code(), Code::NotFound);
}
#[tokio::test]
async fn update_returns_not_found_when_item_has_no_metadata() {
let mut files = BTreeMap::new();
files.insert(INodeNo(100), make_flac_item(100, "song.flac", None));
let files = Arc::new(Mutex::new(files));
let svc = ClientControlServiceImpl::new(files, mock_db_readonly());
let err = svc
.update_music_metadata(Request::new(UpdateMusicMetadataRequest {
inode: 100,
artist: vec!["X".to_string()],
album_artist: None,
album: "X".to_string(),
track_number: 1,
track_title: "X".to_string(),
other_tags: vec![],
}))
.await
.expect_err("should be NOT_FOUND");
assert_eq!(err.code(), Code::NotFound);
}
#[tokio::test]
async fn update_clears_tags_when_empty_values_sent() {
let mut files = BTreeMap::new();
files.insert(
INodeNo(100),
make_flac_item(100, "song.flac", Some(make_flac_metadata())),
);
let files = Arc::new(Mutex::new(files));
let svc = ClientControlServiceImpl::new(files.clone(), mock_db_update_no_artist(100));
let resp = svc
.update_music_metadata(Request::new(UpdateMusicMetadataRequest {
inode: 100,
artist: vec![],
album_artist: None,
album: String::new(),
track_number: 0,
track_title: String::new(),
other_tags: vec![],
}))
.await
.expect("update should succeed");
let md = resp.into_inner().metadata.expect("metadata present");
assert!(md.artist.is_empty());
assert!(md.album.is_empty());
assert!(md.track_title.is_empty());
let guard = files.lock().unwrap();
let mm = guard
.get(&INodeNo(100))
.unwrap()
.music_metadata
.as_ref()
.unwrap();
assert!(mm.artist.is_empty());
assert!(mm.album.is_empty());
}
+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
@@ -0,0 +1,835 @@
// @generated
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ReconcileRequest {
#[prost(message, repeated, tag = "1")]
pub entries: ::prost::alloc::vec::Vec<InodeHash>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ReconcileResponse {
#[prost(message, repeated, tag = "1")]
pub changed: ::prost::alloc::vec::Vec<InodeHash>,
#[prost(uint64, repeated, tag = "2")]
pub deleted: ::prost::alloc::vec::Vec<u64>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct InodeHash {
#[prost(uint64, tag = "1")]
pub inode: u64,
#[prost(uint64, tag = "2")]
pub hash: u64,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetMetadataRequest {
#[prost(uint64, repeated, tag = "1")]
pub inodes: ::prost::alloc::vec::Vec<u64>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetManifestRequest {}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetFileRequest {
#[prost(uint64, tag = "1")]
pub id: u64,
/// When both are zero the server streams the entire file.
#[prost(uint64, tag = "2")]
pub start: u64,
#[prost(uint64, tag = "3")]
pub length: u64,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct SubscribeEventsRequest {}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ManifestEntry {
#[prost(uint64, tag = "1")]
pub id: u64,
#[prost(string, tag = "2")]
pub rel_path: ::prost::alloc::string::String,
#[prost(uint64, tag = "3")]
pub size: u64,
#[prost(uint64, tag = "4")]
pub mtime: u64,
#[prost(uint64, tag = "5")]
pub ctime: u64,
#[prost(uint64, tag = "6")]
pub crtime: u64,
#[prost(message, optional, tag = "7")]
pub music_metadata: ::core::option::Option<MusicMetadata>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct MusicMetadata {
#[prost(string, repeated, tag = "1")]
pub artist: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
#[prost(string, optional, tag = "2")]
pub album_artist: ::core::option::Option<::prost::alloc::string::String>,
#[prost(string, tag = "3")]
pub album: ::prost::alloc::string::String,
#[prost(int32, tag = "4")]
pub track_number: i32,
#[prost(string, tag = "5")]
pub track_title: ::prost::alloc::string::String,
#[prost(string, repeated, tag = "6")]
pub other_tags: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
#[prost(bytes = "vec", tag = "7")]
pub header: ::prost::alloc::vec::Vec<u8>,
#[prost(bytes = "vec", repeated, tag = "8")]
pub picture_block_headers: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec<u8>>,
#[prost(message, repeated, tag = "9")]
pub picture_data_ranges: ::prost::alloc::vec::Vec<PictureDataRange>,
#[prost(uint64, tag = "10")]
pub real_audio_start: u64,
#[prost(uint64, tag = "11")]
pub vorbis_comment_offset: u64,
#[prost(uint64, tag = "12")]
pub vorbis_comment_length: u64,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct PictureDataRange {
#[prost(uint64, tag = "1")]
pub offset: u64,
#[prost(uint64, tag = "2")]
pub length: u64,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct FileChunk {
#[prost(bytes = "vec", tag = "1")]
pub data: ::prost::alloc::vec::Vec<u8>,
#[prost(uint64, tag = "2")]
pub offset: u64,
#[prost(uint64, tag = "3")]
pub total_size: u64,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ChangeEvent {
/// "create", "modify", or "remove".
#[prost(string, tag = "1")]
pub kind: ::prost::alloc::string::String,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetClientStatusRequest {}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ClientStatusResponse {
#[prost(string, tag = "1")]
pub origin_type: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub mountpoint: ::prost::alloc::string::String,
#[prost(uint64, tag = "3")]
pub file_count: u64,
#[prost(bool, tag = "4")]
pub server_connected: bool,
#[prost(string, tag = "5")]
pub server_endpoint: ::prost::alloc::string::String,
#[prost(int64, tag = "6")]
pub last_reconcile_unix: i64,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetMusicMetadataRequest {
#[prost(uint64, tag = "1")]
pub inode: u64,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetMusicMetadataResponse {
#[prost(message, optional, tag = "1")]
pub metadata: ::core::option::Option<MusicMetadata>,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct UpdateMusicMetadataRequest {
#[prost(uint64, tag = "1")]
pub inode: u64,
#[prost(string, repeated, tag = "2")]
pub artist: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
#[prost(string, optional, tag = "3")]
pub album_artist: ::core::option::Option<::prost::alloc::string::String>,
#[prost(string, tag = "4")]
pub album: ::prost::alloc::string::String,
#[prost(int32, tag = "5")]
pub track_number: i32,
#[prost(string, tag = "6")]
pub track_title: ::prost::alloc::string::String,
#[prost(string, repeated, tag = "7")]
pub other_tags: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct UpdateMusicMetadataResponse {
#[prost(message, optional, tag = "1")]
pub metadata: ::core::option::Option<MusicMetadata>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ListFilesRequest {}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ListFilesResponse {
#[prost(message, repeated, tag = "1")]
pub files: ::prost::alloc::vec::Vec<FileEntry>,
}
/// Human-readable tag fields only. Excludes the binary layout fields
/// (header, picture_block_headers, etc.) that only make sense inside
/// musicfs's FUSE read-assembly path.
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct FileMetadata {
#[prost(string, repeated, tag = "1")]
pub artist: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
#[prost(string, optional, tag = "2")]
pub album_artist: ::core::option::Option<::prost::alloc::string::String>,
#[prost(string, tag = "3")]
pub album: ::prost::alloc::string::String,
#[prost(int32, tag = "4")]
pub track_number: i32,
#[prost(string, tag = "5")]
pub track_title: ::prost::alloc::string::String,
#[prost(string, repeated, tag = "6")]
pub other_tags: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct FileEntry {
#[prost(uint64, tag = "1")]
pub inode: u64,
/// Real on-disk path (where tora/librqbit wrote the file).
#[prost(string, tag = "2")]
pub original_path: ::prost::alloc::string::String,
/// Virtual FUSE path ({artist}/{album}/{filename}); updated when metadata
/// changes via `UpdateMusicMetadata`.
#[prost(string, tag = "3")]
pub local_path: ::prost::alloc::string::String,
/// Filename component only.
#[prost(string, tag = "4")]
pub name: ::prost::alloc::string::String,
/// Empty if the file has no parsed tags.
#[prost(message, optional, tag = "5")]
pub metadata: ::core::option::Option<FileMetadata>,
}
/// Encoded file descriptor set for the `musicfs` package
pub const FILE_DESCRIPTOR_SET: &[u8] = &[
0x0a, 0xee, 0x4e, 0x0a, 0x0d, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x12, 0x07, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x22, 0x40, 0x0a, 0x10, 0x52,
0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x2c, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x12, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x49, 0x6e, 0x6f, 0x64, 0x65,
0x48, 0x61, 0x73, 0x68, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0x5b, 0x0a,
0x11, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x01, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x49, 0x6e,
0x6f, 0x64, 0x65, 0x48, 0x61, 0x73, 0x68, 0x52, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64,
0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x03, 0x28,
0x04, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x35, 0x0a, 0x09, 0x49, 0x6e,
0x6f, 0x64, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x6f, 0x64, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x69, 0x6e, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a,
0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x68, 0x61, 0x73,
0x68, 0x22, 0x2c, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x6e, 0x6f, 0x64, 0x65,
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x52, 0x06, 0x69, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x22,
0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4e, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x46, 0x69, 0x6c, 0x65,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74,
0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x16, 0x0a,
0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6c,
0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0x18, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69,
0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22,
0xd1, 0x01, 0x0a, 0x0d, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x72,
0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69,
0x64, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x50, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04,
0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65,
0x12, 0x14, 0x0a, 0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52,
0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x18,
0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06,
0x63, 0x72, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x63, 0x72,
0x74, 0x69, 0x6d, 0x65, 0x12, 0x3d, 0x0a, 0x0e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x5f, 0x6d, 0x65,
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d,
0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x4d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x52, 0x0d, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x64,
0x61, 0x74, 0x61, 0x22, 0x82, 0x04, 0x0a, 0x0d, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x4d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x18,
0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x12, 0x26, 0x0a,
0x0c, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x41, 0x72, 0x74, 0x69,
0x73, 0x74, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x18, 0x03,
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x12, 0x21, 0x0a, 0x0c, 0x74,
0x72, 0x61, 0x63, 0x6b, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28,
0x05, 0x52, 0x0b, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x1f,
0x0a, 0x0b, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x05, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x69, 0x74, 0x6c, 0x65, 0x12,
0x1d, 0x0a, 0x0a, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x06, 0x20,
0x03, 0x28, 0x09, 0x52, 0x09, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x54, 0x61, 0x67, 0x73, 0x12, 0x16,
0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06,
0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72,
0x65, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18,
0x08, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x13, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x42, 0x6c,
0x6f, 0x63, 0x6b, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x49, 0x0a, 0x13, 0x70, 0x69,
0x63, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65,
0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66,
0x73, 0x2e, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x61, 0x6e,
0x67, 0x65, 0x52, 0x11, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52,
0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x72, 0x65, 0x61, 0x6c, 0x5f, 0x61, 0x75,
0x64, 0x69, 0x6f, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52,
0x0e, 0x72, 0x65, 0x61, 0x6c, 0x41, 0x75, 0x64, 0x69, 0x6f, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12,
0x32, 0x0a, 0x15, 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e,
0x74, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x13,
0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x4f, 0x66, 0x66,
0x73, 0x65, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x5f, 0x63, 0x6f,
0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x0c, 0x20, 0x01,
0x28, 0x04, 0x52, 0x13, 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e,
0x74, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x61, 0x6c, 0x62, 0x75,
0x6d, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x22, 0x42, 0x0a, 0x10, 0x50, 0x69, 0x63, 0x74,
0x75, 0x72, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06,
0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66,
0x66, 0x73, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x02,
0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0x56, 0x0a, 0x09,
0x46, 0x69, 0x6c, 0x65, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74,
0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a,
0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f,
0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x73,
0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c,
0x53, 0x69, 0x7a, 0x65, 0x22, 0x21, 0x0a, 0x0b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x45, 0x76,
0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x22, 0x18, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x43, 0x6c,
0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x22, 0xfa, 0x01, 0x0a, 0x14, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74,
0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6f, 0x72,
0x69, 0x67, 0x69, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0a, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x6d,
0x6f, 0x75, 0x6e, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0a, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x66,
0x69, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52,
0x09, 0x66, 0x69, 0x6c, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x73, 0x65,
0x72, 0x76, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x04,
0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x6e,
0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f,
0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e,
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x2e,
0x0a, 0x13, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65,
0x5f, 0x75, 0x6e, 0x69, 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x6c, 0x61, 0x73,
0x74, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x55, 0x6e, 0x69, 0x78, 0x22, 0x2f,
0x0a, 0x17, 0x47, 0x65, 0x74, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x6f,
0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x69, 0x6e, 0x6f, 0x64, 0x65, 0x22,
0x4e, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x64,
0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x08, 0x6d,
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e,
0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x4d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22,
0xfc, 0x01, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x4d,
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14,
0x0a, 0x05, 0x69, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x69,
0x6e, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x18, 0x02,
0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0c,
0x61, 0x6c, 0x62, 0x75, 0x6d, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01,
0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x41, 0x72, 0x74, 0x69, 0x73,
0x74, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x18, 0x04, 0x20,
0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x72,
0x61, 0x63, 0x6b, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05,
0x52, 0x0b, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x1f, 0x0a,
0x0b, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x06, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0a, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x1d,
0x0a, 0x0a, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03,
0x28, 0x09, 0x52, 0x09, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x54, 0x61, 0x67, 0x73, 0x42, 0x0f, 0x0a,
0x0d, 0x5f, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x22, 0x51,
0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x4d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a,
0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x16, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x4d,
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
0x61, 0x22, 0x12, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3d, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c,
0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x66, 0x69,
0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x75, 0x73, 0x69,
0x63, 0x66, 0x73, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x66,
0x69, 0x6c, 0x65, 0x73, 0x22, 0xd8, 0x01, 0x0a, 0x0c, 0x46, 0x69, 0x6c, 0x65, 0x4d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x18,
0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x12, 0x26, 0x0a,
0x0c, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x41, 0x72, 0x74, 0x69,
0x73, 0x74, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x18, 0x03,
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x12, 0x21, 0x0a, 0x0c, 0x74,
0x72, 0x61, 0x63, 0x6b, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28,
0x05, 0x52, 0x0b, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x1f,
0x0a, 0x0b, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x05, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x69, 0x74, 0x6c, 0x65, 0x12,
0x1d, 0x0a, 0x0a, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x06, 0x20,
0x03, 0x28, 0x09, 0x52, 0x09, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x54, 0x61, 0x67, 0x73, 0x42, 0x0f,
0x0a, 0x0d, 0x5f, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x22,
0xbe, 0x01, 0x0a, 0x09, 0x46, 0x69, 0x6c, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x14, 0x0a,
0x05, 0x69, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x69, 0x6e,
0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f,
0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x72, 0x69, 0x67,
0x69, 0x6e, 0x61, 0x6c, 0x50, 0x61, 0x74, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x6f, 0x63, 0x61,
0x6c, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x6f,
0x63, 0x61, 0x6c, 0x50, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18,
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x6d,
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e,
0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x4d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
0x32, 0xdf, 0x02, 0x0a, 0x07, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x46, 0x73, 0x12, 0x42, 0x0a, 0x09,
0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x12, 0x19, 0x2e, 0x6d, 0x75, 0x73, 0x69,
0x63, 0x66, 0x73, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x52,
0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x44, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12,
0x1b, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6d,
0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x45,
0x6e, 0x74, 0x72, 0x79, 0x30, 0x01, 0x12, 0x44, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e,
0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e,
0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x4d, 0x61, 0x6e,
0x69, 0x66, 0x65, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x30, 0x01, 0x12, 0x38, 0x0a, 0x07,
0x47, 0x65, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x17, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66,
0x73, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x12, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x43,
0x68, 0x75, 0x6e, 0x6b, 0x30, 0x01, 0x12, 0x4a, 0x0a, 0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72,
0x69, 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1f, 0x2e, 0x6d, 0x75, 0x73, 0x69,
0x63, 0x66, 0x73, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, 0x65,
0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x6d, 0x75, 0x73,
0x69, 0x63, 0x66, 0x73, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74,
0x30, 0x01, 0x32, 0x61, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74,
0x75, 0x73, 0x12, 0x51, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53,
0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1f, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e,
0x47, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73,
0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x8e, 0x02, 0x0a, 0x0d, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x12, 0x57, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4d, 0x75,
0x73, 0x69, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x20, 0x2e, 0x6d, 0x75,
0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x4d, 0x65,
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e,
0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x75, 0x73, 0x69, 0x63,
0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x60, 0x0a, 0x13, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x4d,
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x23, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66,
0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x4d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6d,
0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x75, 0x73,
0x69, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x42, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12,
0x19, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69,
0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6d, 0x75, 0x73,
0x69, 0x63, 0x66, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4a, 0x80, 0x35, 0x0a, 0x07, 0x12, 0x05, 0x00, 0x00, 0xb4,
0x01, 0x01, 0x0a, 0x08, 0x0a, 0x01, 0x0c, 0x12, 0x03, 0x00, 0x00, 0x12, 0x0a, 0x08, 0x0a, 0x01,
0x02, 0x12, 0x03, 0x02, 0x00, 0x10, 0x0a, 0x62, 0x0a, 0x02, 0x06, 0x00, 0x12, 0x04, 0x06, 0x00,
0x20, 0x01, 0x1a, 0x56, 0x20, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x46, 0x73, 0x20, 0x73, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x20, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x73, 0x20, 0x61, 0x20, 0x73,
0x65, 0x72, 0x76, 0x65, 0x72, 0x2d, 0x73, 0x69, 0x64, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x69, 0x63,
0x20, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x61, 0x20, 0x72, 0x65,
0x6d, 0x6f, 0x74, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x0a, 0x20, 0x46, 0x55,
0x53, 0x45, 0x20, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x0a, 0x0a, 0x0a, 0x0a, 0x03, 0x06, 0x00,
0x01, 0x12, 0x03, 0x06, 0x08, 0x0f, 0x0a, 0xac, 0x02, 0x0a, 0x04, 0x06, 0x00, 0x02, 0x00, 0x12,
0x03, 0x0b, 0x02, 0x3e, 0x1a, 0x9e, 0x02, 0x20, 0x48, 0x61, 0x73, 0x68, 0x2d, 0x62, 0x61, 0x73,
0x65, 0x64, 0x20, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x69, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x2e, 0x20, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x73, 0x65, 0x6e, 0x64, 0x73, 0x20,
0x74, 0x68, 0x65, 0x20, 0x28, 0x69, 0x6e, 0x6f, 0x64, 0x65, 0x2c, 0x20, 0x68, 0x61, 0x73, 0x68,
0x29, 0x20, 0x70, 0x61, 0x69, 0x72, 0x73, 0x20, 0x69, 0x74, 0x0a, 0x20, 0x63, 0x75, 0x72, 0x72,
0x65, 0x6e, 0x74, 0x6c, 0x79, 0x20, 0x68, 0x61, 0x73, 0x3b, 0x20, 0x73, 0x65, 0x72, 0x76, 0x65,
0x72, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x64, 0x73, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20,
0x74, 0x68, 0x65, 0x20, 0x73, 0x75, 0x62, 0x73, 0x65, 0x74, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20,
0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x20, 0x28, 0x6e, 0x65, 0x77, 0x20, 0x6f, 0x72, 0x0a,
0x20, 0x68, 0x61, 0x73, 0x68, 0x2d, 0x64, 0x69, 0x66, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x74, 0x29,
0x20, 0x70, 0x6c, 0x75, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x69, 0x6e, 0x6f, 0x64, 0x65, 0x73,
0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x68, 0x61, 0x73, 0x20,
0x62, 0x75, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x6e,
0x6f, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x65, 0x72, 0x0a, 0x20, 0x68, 0x61, 0x73, 0x2e, 0x20, 0x43,
0x68, 0x65, 0x61, 0x70, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x77, 0x69, 0x72, 0x65,
0x20, 0xe2, 0x80, 0x94, 0x20, 0x6e, 0x6f, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x20, 0x6d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x20, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x65, 0x73, 0x20, 0x75, 0x6e,
0x74, 0x69, 0x6c, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x61,
0x73, 0x6b, 0x73, 0x2e, 0x0a, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x00, 0x01, 0x12, 0x03,
0x0b, 0x06, 0x0f, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x00, 0x02, 0x12, 0x03, 0x0b, 0x10,
0x20, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x00, 0x03, 0x12, 0x03, 0x0b, 0x2b, 0x3c, 0x0a,
0x94, 0x01, 0x0a, 0x04, 0x06, 0x00, 0x02, 0x01, 0x12, 0x03, 0x0f, 0x02, 0x45, 0x1a, 0x86, 0x01,
0x20, 0x46, 0x65, 0x74, 0x63, 0x68, 0x20, 0x66, 0x75, 0x6c, 0x6c, 0x20, 0x6d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x61, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69,
0x66, 0x69, 0x63, 0x20, 0x73, 0x65, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x69, 0x6e, 0x6f, 0x64, 0x65,
0x73, 0x2e, 0x20, 0x43, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x20, 0x61, 0x66, 0x74, 0x65, 0x72, 0x20,
0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x0a, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72,
0x6e, 0x73, 0x20, 0x61, 0x20, 0x6e, 0x6f, 0x6e, 0x2d, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x60,
0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x60, 0x20, 0x6c, 0x69, 0x73, 0x74, 0x2c, 0x20, 0x66,
0x6f, 0x72, 0x20, 0x6a, 0x75, 0x73, 0x74, 0x20, 0x74, 0x68, 0x6f, 0x73, 0x65, 0x20, 0x69, 0x6e,
0x6f, 0x64, 0x65, 0x73, 0x2e, 0x0a, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x01, 0x01, 0x12,
0x03, 0x0f, 0x06, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x01, 0x02, 0x12, 0x03, 0x0f,
0x12, 0x24, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x01, 0x06, 0x12, 0x03, 0x0f, 0x2f, 0x35,
0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x01, 0x03, 0x12, 0x03, 0x0f, 0x36, 0x43, 0x0a, 0xa8,
0x01, 0x0a, 0x04, 0x06, 0x00, 0x02, 0x02, 0x12, 0x03, 0x14, 0x02, 0x45, 0x1a, 0x9a, 0x01, 0x20,
0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x20, 0x65, 0x76, 0x65, 0x72, 0x79, 0x20, 0x66, 0x69, 0x6c,
0x65, 0x20, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6c,
0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x2e, 0x20, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65,
0x6e, 0x63, 0x65, 0x20, 0x52, 0x50, 0x43, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x66, 0x69, 0x72, 0x73,
0x74, 0x2d, 0x72, 0x75, 0x6e, 0x0a, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70,
0x70, 0x69, 0x6e, 0x67, 0x20, 0x6f, 0x72, 0x20, 0x66, 0x75, 0x6c, 0x6c, 0x2d, 0x72, 0x65, 0x66,
0x72, 0x65, 0x73, 0x68, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x64, 0x69, 0x66, 0x66, 0x2d, 0x62,
0x61, 0x73, 0x65, 0x64, 0x20, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x20, 0x70,
0x61, 0x74, 0x68, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x0a, 0x20, 0x6e, 0x6f, 0x72, 0x6d,
0x61, 0x6c, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x0a, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02,
0x02, 0x01, 0x12, 0x03, 0x14, 0x06, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x02, 0x02,
0x12, 0x03, 0x14, 0x12, 0x24, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x02, 0x06, 0x12, 0x03,
0x14, 0x2f, 0x35, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x02, 0x03, 0x12, 0x03, 0x14, 0x36,
0x43, 0x0a, 0xf8, 0x01, 0x0a, 0x04, 0x06, 0x00, 0x02, 0x03, 0x12, 0x03, 0x1a, 0x02, 0x39, 0x1a,
0xea, 0x01, 0x20, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x20, 0x62, 0x79,
0x74, 0x65, 0x73, 0x20, 0x6f, 0x66, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x2e,
0x20, 0x49, 0x66, 0x20, 0x60, 0x73, 0x74, 0x61, 0x72, 0x74, 0x60, 0x20, 0x61, 0x6e, 0x64, 0x20,
0x60, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x60, 0x20, 0x61, 0x72, 0x65, 0x20, 0x62, 0x6f, 0x74,
0x68, 0x20, 0x7a, 0x65, 0x72, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x0a, 0x20, 0x73, 0x65, 0x72, 0x76,
0x65, 0x72, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x65,
0x6e, 0x74, 0x69, 0x72, 0x65, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x3b, 0x20, 0x6f, 0x74, 0x68, 0x65,
0x72, 0x77, 0x69, 0x73, 0x65, 0x20, 0x6a, 0x75, 0x73, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x72,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x20, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x20,
0x54, 0x68, 0x65, 0x0a, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x63, 0x68, 0x75, 0x6e, 0x6b,
0x20, 0x63, 0x61, 0x72, 0x72, 0x69, 0x65, 0x73, 0x20, 0x60, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f,
0x73, 0x69, 0x7a, 0x65, 0x60, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x60, 0x6f, 0x66, 0x66, 0x73, 0x65,
0x74, 0x60, 0x3b, 0x20, 0x73, 0x75, 0x62, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x74, 0x20, 0x63,
0x68, 0x75, 0x6e, 0x6b, 0x73, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x65, 0x0a, 0x20,
0x66, 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x72, 0x65, 0x2e, 0x0a, 0x0a, 0x0c, 0x0a, 0x05,
0x06, 0x00, 0x02, 0x03, 0x01, 0x12, 0x03, 0x1a, 0x06, 0x0d, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00,
0x02, 0x03, 0x02, 0x12, 0x03, 0x1a, 0x0e, 0x1c, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x03,
0x06, 0x12, 0x03, 0x1a, 0x27, 0x2d, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x03, 0x03, 0x12,
0x03, 0x1a, 0x2e, 0x37, 0x0a, 0xc2, 0x01, 0x0a, 0x04, 0x06, 0x00, 0x02, 0x04, 0x12, 0x03, 0x1f,
0x02, 0x4b, 0x1a, 0xb4, 0x01, 0x20, 0x4c, 0x6f, 0x6e, 0x67, 0x2d, 0x6c, 0x69, 0x76, 0x65, 0x64,
0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x20, 0x6f, 0x66, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x73,
0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x20, 0x65, 0x76, 0x65,
0x6e, 0x74, 0x73, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20,
0x75, 0x73, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x73, 0x65, 0x20, 0x61, 0x73, 0x0a, 0x20, 0x77,
0x61, 0x6b, 0x65, 0x2d, 0x75, 0x70, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x72, 0x65, 0x66, 0x65, 0x74,
0x63, 0x68, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x3b,
0x20, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x63, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x20, 0x61, 0x6c, 0x77,
0x61, 0x79, 0x73, 0x20, 0x72, 0x65, 0x73, 0x74, 0x73, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65,
0x20, 0x68, 0x61, 0x73, 0x68, 0x0a, 0x20, 0x64, 0x69, 0x66, 0x66, 0x2c, 0x20, 0x6e, 0x65, 0x76,
0x65, 0x72, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x20,
0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x0a, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02,
0x04, 0x01, 0x12, 0x03, 0x1f, 0x06, 0x15, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x04, 0x02,
0x12, 0x03, 0x1f, 0x16, 0x2c, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x04, 0x06, 0x12, 0x03,
0x1f, 0x37, 0x3d, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x04, 0x03, 0x12, 0x03, 0x1f, 0x3e,
0x49, 0x0a, 0x4f, 0x0a, 0x02, 0x06, 0x01, 0x12, 0x04, 0x23, 0x00, 0x25, 0x01, 0x1a, 0x43, 0x20,
0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2d, 0x73, 0x69, 0x64, 0x65, 0x20, 0x73, 0x74, 0x61, 0x74,
0x75, 0x73, 0x20, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x28, 0x73, 0x65,
0x72, 0x76, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20, 0x74, 0x68, 0x65, 0x20, 0x46, 0x55, 0x53, 0x45,
0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x29,
0x2e, 0x0a, 0x0a, 0x0a, 0x0a, 0x03, 0x06, 0x01, 0x01, 0x12, 0x03, 0x23, 0x08, 0x14, 0x0a, 0x0b,
0x0a, 0x04, 0x06, 0x01, 0x02, 0x00, 0x12, 0x03, 0x24, 0x02, 0x4d, 0x0a, 0x0c, 0x0a, 0x05, 0x06,
0x01, 0x02, 0x00, 0x01, 0x12, 0x03, 0x24, 0x06, 0x15, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x01, 0x02,
0x00, 0x02, 0x12, 0x03, 0x24, 0x16, 0x2c, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x01, 0x02, 0x00, 0x03,
0x12, 0x03, 0x24, 0x37, 0x4b, 0x0a, 0x0a, 0x0a, 0x02, 0x06, 0x02, 0x12, 0x04, 0x27, 0x00, 0x2e,
0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x06, 0x02, 0x01, 0x12, 0x03, 0x27, 0x08, 0x15, 0x0a, 0x0b, 0x0a,
0x04, 0x06, 0x02, 0x02, 0x00, 0x12, 0x03, 0x28, 0x02, 0x53, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x02,
0x02, 0x00, 0x01, 0x12, 0x03, 0x28, 0x06, 0x16, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x02, 0x02, 0x00,
0x02, 0x12, 0x03, 0x28, 0x17, 0x2e, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x02, 0x02, 0x00, 0x03, 0x12,
0x03, 0x28, 0x39, 0x51, 0x0a, 0x0b, 0x0a, 0x04, 0x06, 0x02, 0x02, 0x01, 0x12, 0x03, 0x29, 0x02,
0x5c, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x02, 0x02, 0x01, 0x01, 0x12, 0x03, 0x29, 0x06, 0x19, 0x0a,
0x0c, 0x0a, 0x05, 0x06, 0x02, 0x02, 0x01, 0x02, 0x12, 0x03, 0x29, 0x1a, 0x34, 0x0a, 0x0c, 0x0a,
0x05, 0x06, 0x02, 0x02, 0x01, 0x03, 0x12, 0x03, 0x29, 0x3f, 0x5a, 0x0a, 0xb4, 0x01, 0x0a, 0x04,
0x06, 0x02, 0x02, 0x02, 0x12, 0x03, 0x2d, 0x02, 0x3e, 0x1a, 0xa6, 0x01, 0x20, 0x45, 0x76, 0x65,
0x72, 0x79, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x20,
0x6b, 0x6e, 0x6f, 0x77, 0x73, 0x20, 0x61, 0x62, 0x6f, 0x75, 0x74, 0x2e, 0x20, 0x43, 0x61, 0x6c,
0x6c, 0x65, 0x72, 0x20, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x20, 0x62, 0x79, 0x20, 0x60,
0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x60, 0x0a, 0x20,
0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x69, 0x6e, 0x64, 0x20, 0x74,
0x68, 0x65, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x20, 0x6f, 0x66, 0x20, 0x69, 0x6e, 0x74, 0x65,
0x72, 0x65, 0x73, 0x74, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x66,
0x69, 0x6c, 0x65, 0x73, 0x20, 0x75, 0x6e, 0x64, 0x65, 0x72, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e,
0x67, 0x6c, 0x65, 0x0a, 0x20, 0x74, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x27, 0x73, 0x20, 0x6f,
0x75, 0x74, 0x70, 0x75, 0x74, 0x20, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x29,
0x2e, 0x0a, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x02, 0x02, 0x02, 0x01, 0x12, 0x03, 0x2d, 0x06, 0x0f,
0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x02, 0x02, 0x02, 0x02, 0x12, 0x03, 0x2d, 0x10, 0x20, 0x0a, 0x0c,
0x0a, 0x05, 0x06, 0x02, 0x02, 0x02, 0x03, 0x12, 0x03, 0x2d, 0x2b, 0x3c, 0x0a, 0x0a, 0x0a, 0x02,
0x04, 0x00, 0x12, 0x04, 0x30, 0x00, 0x32, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x00, 0x01, 0x12,
0x03, 0x30, 0x08, 0x18, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x00, 0x04, 0x12, 0x03, 0x31,
0x02, 0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x00, 0x02, 0x00, 0x12, 0x03, 0x31, 0x02, 0x21, 0x0a,
0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x00, 0x06, 0x12, 0x03, 0x31, 0x0b, 0x14, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x00, 0x02, 0x00, 0x01, 0x12, 0x03, 0x31, 0x15, 0x1c, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x00, 0x02, 0x00, 0x03, 0x12, 0x03, 0x31, 0x1f, 0x20, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x01, 0x12,
0x04, 0x34, 0x00, 0x37, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x01, 0x01, 0x12, 0x03, 0x34, 0x08,
0x19, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02, 0x00, 0x04, 0x12, 0x03, 0x35, 0x02, 0x0a, 0x0a,
0x0b, 0x0a, 0x04, 0x04, 0x01, 0x02, 0x00, 0x12, 0x03, 0x35, 0x02, 0x21, 0x0a, 0x0c, 0x0a, 0x05,
0x04, 0x01, 0x02, 0x00, 0x06, 0x12, 0x03, 0x35, 0x0b, 0x14, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01,
0x02, 0x00, 0x01, 0x12, 0x03, 0x35, 0x15, 0x1c, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02, 0x00,
0x03, 0x12, 0x03, 0x35, 0x1f, 0x20, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02, 0x01, 0x04, 0x12,
0x03, 0x36, 0x02, 0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x01, 0x02, 0x01, 0x12, 0x03, 0x36, 0x02,
0x1e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02, 0x01, 0x05, 0x12, 0x03, 0x36, 0x0b, 0x11, 0x0a,
0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02, 0x01, 0x01, 0x12, 0x03, 0x36, 0x12, 0x19, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x01, 0x02, 0x01, 0x03, 0x12, 0x03, 0x36, 0x1c, 0x1d, 0x0a, 0x0a, 0x0a, 0x02, 0x04,
0x02, 0x12, 0x04, 0x39, 0x00, 0x3c, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x02, 0x01, 0x12, 0x03,
0x39, 0x08, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x02, 0x02, 0x00, 0x05, 0x12, 0x03, 0x3a, 0x02,
0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x02, 0x02, 0x00, 0x12, 0x03, 0x3a, 0x02, 0x13, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x02, 0x02, 0x00, 0x01, 0x12, 0x03, 0x3a, 0x09, 0x0e, 0x0a, 0x0c, 0x0a, 0x05,
0x04, 0x02, 0x02, 0x00, 0x03, 0x12, 0x03, 0x3a, 0x11, 0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x02,
0x02, 0x01, 0x05, 0x12, 0x03, 0x3b, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x02, 0x02, 0x01,
0x12, 0x03, 0x3b, 0x02, 0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x02, 0x02, 0x01, 0x01, 0x12, 0x03,
0x3b, 0x09, 0x0d, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x02, 0x02, 0x01, 0x03, 0x12, 0x03, 0x3b, 0x10,
0x11, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x03, 0x12, 0x04, 0x3e, 0x00, 0x40, 0x01, 0x0a, 0x0a, 0x0a,
0x03, 0x04, 0x03, 0x01, 0x12, 0x03, 0x3e, 0x08, 0x1a, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x03, 0x02,
0x00, 0x04, 0x12, 0x03, 0x3f, 0x02, 0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x03, 0x02, 0x00, 0x12,
0x03, 0x3f, 0x02, 0x1d, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x03, 0x02, 0x00, 0x05, 0x12, 0x03, 0x3f,
0x0b, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x03, 0x02, 0x00, 0x01, 0x12, 0x03, 0x3f, 0x12, 0x18,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x03, 0x02, 0x00, 0x03, 0x12, 0x03, 0x3f, 0x1b, 0x1c, 0x0a, 0x09,
0x0a, 0x02, 0x04, 0x04, 0x12, 0x03, 0x42, 0x00, 0x1d, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x04, 0x01,
0x12, 0x03, 0x42, 0x08, 0x1a, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x05, 0x12, 0x04, 0x44, 0x00, 0x49,
0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x05, 0x01, 0x12, 0x03, 0x44, 0x08, 0x16, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x05, 0x02, 0x00, 0x05, 0x12, 0x03, 0x45, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04,
0x05, 0x02, 0x00, 0x12, 0x03, 0x45, 0x02, 0x10, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x00,
0x01, 0x12, 0x03, 0x45, 0x09, 0x0b, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x00, 0x03, 0x12,
0x03, 0x45, 0x0e, 0x0f, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x01, 0x05, 0x12, 0x03, 0x47,
0x02, 0x08, 0x0a, 0x45, 0x0a, 0x04, 0x04, 0x05, 0x02, 0x01, 0x12, 0x03, 0x47, 0x02, 0x13, 0x1a,
0x38, 0x20, 0x57, 0x68, 0x65, 0x6e, 0x20, 0x62, 0x6f, 0x74, 0x68, 0x20, 0x61, 0x72, 0x65, 0x20,
0x7a, 0x65, 0x72, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20,
0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x74, 0x69,
0x72, 0x65, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x0a, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02,
0x01, 0x01, 0x12, 0x03, 0x47, 0x09, 0x0e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x01, 0x03,
0x12, 0x03, 0x47, 0x11, 0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x02, 0x05, 0x12, 0x03,
0x48, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x05, 0x02, 0x02, 0x12, 0x03, 0x48, 0x02, 0x14,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x02, 0x01, 0x12, 0x03, 0x48, 0x09, 0x0f, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x05, 0x02, 0x02, 0x03, 0x12, 0x03, 0x48, 0x12, 0x13, 0x0a, 0x09, 0x0a, 0x02,
0x04, 0x06, 0x12, 0x03, 0x4b, 0x00, 0x21, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x06, 0x01, 0x12, 0x03,
0x4b, 0x08, 0x1e, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x07, 0x12, 0x04, 0x4d, 0x00, 0x55, 0x01, 0x0a,
0x0a, 0x0a, 0x03, 0x04, 0x07, 0x01, 0x12, 0x03, 0x4d, 0x08, 0x15, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x07, 0x02, 0x00, 0x05, 0x12, 0x03, 0x4e, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x07, 0x02,
0x00, 0x12, 0x03, 0x4e, 0x02, 0x10, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x00, 0x01, 0x12,
0x03, 0x4e, 0x09, 0x0b, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x00, 0x03, 0x12, 0x03, 0x4e,
0x0e, 0x0f, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x01, 0x05, 0x12, 0x03, 0x4f, 0x02, 0x08,
0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x07, 0x02, 0x01, 0x12, 0x03, 0x4f, 0x02, 0x16, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x07, 0x02, 0x01, 0x01, 0x12, 0x03, 0x4f, 0x09, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x07, 0x02, 0x01, 0x03, 0x12, 0x03, 0x4f, 0x14, 0x15, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02,
0x02, 0x05, 0x12, 0x03, 0x50, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x07, 0x02, 0x02, 0x12,
0x03, 0x50, 0x02, 0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x02, 0x01, 0x12, 0x03, 0x50,
0x09, 0x0d, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x02, 0x03, 0x12, 0x03, 0x50, 0x10, 0x11,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x03, 0x05, 0x12, 0x03, 0x51, 0x02, 0x08, 0x0a, 0x0b,
0x0a, 0x04, 0x04, 0x07, 0x02, 0x03, 0x12, 0x03, 0x51, 0x02, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x07, 0x02, 0x03, 0x01, 0x12, 0x03, 0x51, 0x09, 0x0e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02,
0x03, 0x03, 0x12, 0x03, 0x51, 0x11, 0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x04, 0x05,
0x12, 0x03, 0x52, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x07, 0x02, 0x04, 0x12, 0x03, 0x52,
0x02, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x04, 0x01, 0x12, 0x03, 0x52, 0x09, 0x0e,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x04, 0x03, 0x12, 0x03, 0x52, 0x11, 0x12, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x07, 0x02, 0x05, 0x05, 0x12, 0x03, 0x53, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04,
0x04, 0x07, 0x02, 0x05, 0x12, 0x03, 0x53, 0x02, 0x14, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02,
0x05, 0x01, 0x12, 0x03, 0x53, 0x09, 0x0f, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x05, 0x03,
0x12, 0x03, 0x53, 0x12, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x06, 0x06, 0x12, 0x03,
0x54, 0x02, 0x0f, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x07, 0x02, 0x06, 0x12, 0x03, 0x54, 0x02, 0x23,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x06, 0x01, 0x12, 0x03, 0x54, 0x10, 0x1e, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x07, 0x02, 0x06, 0x03, 0x12, 0x03, 0x54, 0x21, 0x22, 0x0a, 0x0a, 0x0a, 0x02,
0x04, 0x08, 0x12, 0x04, 0x57, 0x00, 0x64, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x08, 0x01, 0x12,
0x03, 0x57, 0x08, 0x15, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x00, 0x04, 0x12, 0x03, 0x58,
0x02, 0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08, 0x02, 0x00, 0x12, 0x03, 0x58, 0x02, 0x1d, 0x0a,
0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x00, 0x05, 0x12, 0x03, 0x58, 0x0b, 0x11, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x08, 0x02, 0x00, 0x01, 0x12, 0x03, 0x58, 0x12, 0x18, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x08, 0x02, 0x00, 0x03, 0x12, 0x03, 0x58, 0x1b, 0x1c, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02,
0x01, 0x04, 0x12, 0x03, 0x59, 0x02, 0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08, 0x02, 0x01, 0x12,
0x03, 0x59, 0x02, 0x23, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x01, 0x05, 0x12, 0x03, 0x59,
0x0b, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x01, 0x01, 0x12, 0x03, 0x59, 0x12, 0x1e,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x01, 0x03, 0x12, 0x03, 0x59, 0x21, 0x22, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x08, 0x02, 0x02, 0x05, 0x12, 0x03, 0x5a, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04,
0x04, 0x08, 0x02, 0x02, 0x12, 0x03, 0x5a, 0x02, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02,
0x02, 0x01, 0x12, 0x03, 0x5a, 0x09, 0x0e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x02, 0x03,
0x12, 0x03, 0x5a, 0x11, 0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x03, 0x05, 0x12, 0x03,
0x5b, 0x02, 0x07, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08, 0x02, 0x03, 0x12, 0x03, 0x5b, 0x02, 0x19,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x03, 0x01, 0x12, 0x03, 0x5b, 0x08, 0x14, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x08, 0x02, 0x03, 0x03, 0x12, 0x03, 0x5b, 0x17, 0x18, 0x0a, 0x0c, 0x0a, 0x05,
0x04, 0x08, 0x02, 0x04, 0x05, 0x12, 0x03, 0x5c, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08,
0x02, 0x04, 0x12, 0x03, 0x5c, 0x02, 0x19, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x04, 0x01,
0x12, 0x03, 0x5c, 0x09, 0x14, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x04, 0x03, 0x12, 0x03,
0x5c, 0x17, 0x18, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x05, 0x04, 0x12, 0x03, 0x5d, 0x02,
0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08, 0x02, 0x05, 0x12, 0x03, 0x5d, 0x02, 0x21, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x08, 0x02, 0x05, 0x05, 0x12, 0x03, 0x5d, 0x0b, 0x11, 0x0a, 0x0c, 0x0a, 0x05,
0x04, 0x08, 0x02, 0x05, 0x01, 0x12, 0x03, 0x5d, 0x12, 0x1c, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08,
0x02, 0x05, 0x03, 0x12, 0x03, 0x5d, 0x1f, 0x20, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x06,
0x05, 0x12, 0x03, 0x5e, 0x02, 0x07, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08, 0x02, 0x06, 0x12, 0x03,
0x5e, 0x02, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x06, 0x01, 0x12, 0x03, 0x5e, 0x08,
0x0e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x06, 0x03, 0x12, 0x03, 0x5e, 0x11, 0x12, 0x0a,
0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x07, 0x04, 0x12, 0x03, 0x5f, 0x02, 0x0a, 0x0a, 0x0b, 0x0a,
0x04, 0x04, 0x08, 0x02, 0x07, 0x12, 0x03, 0x5f, 0x02, 0x2b, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08,
0x02, 0x07, 0x05, 0x12, 0x03, 0x5f, 0x0b, 0x10, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x07,
0x01, 0x12, 0x03, 0x5f, 0x11, 0x26, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x07, 0x03, 0x12,
0x03, 0x5f, 0x29, 0x2a, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x08, 0x04, 0x12, 0x03, 0x60,
0x02, 0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08, 0x02, 0x08, 0x12, 0x03, 0x60, 0x02, 0x34, 0x0a,
0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x08, 0x06, 0x12, 0x03, 0x60, 0x0b, 0x1b, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x08, 0x02, 0x08, 0x01, 0x12, 0x03, 0x60, 0x1c, 0x2f, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x08, 0x02, 0x08, 0x03, 0x12, 0x03, 0x60, 0x32, 0x33, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02,
0x09, 0x05, 0x12, 0x03, 0x61, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08, 0x02, 0x09, 0x12,
0x03, 0x61, 0x02, 0x1f, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x09, 0x01, 0x12, 0x03, 0x61,
0x09, 0x19, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x09, 0x03, 0x12, 0x03, 0x61, 0x1c, 0x1e,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x0a, 0x05, 0x12, 0x03, 0x62, 0x02, 0x08, 0x0a, 0x0b,
0x0a, 0x04, 0x04, 0x08, 0x02, 0x0a, 0x12, 0x03, 0x62, 0x02, 0x24, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x08, 0x02, 0x0a, 0x01, 0x12, 0x03, 0x62, 0x09, 0x1e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02,
0x0a, 0x03, 0x12, 0x03, 0x62, 0x21, 0x23, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x0b, 0x05,
0x12, 0x03, 0x63, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08, 0x02, 0x0b, 0x12, 0x03, 0x63,
0x02, 0x24, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x0b, 0x01, 0x12, 0x03, 0x63, 0x09, 0x1e,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x0b, 0x03, 0x12, 0x03, 0x63, 0x21, 0x23, 0x0a, 0x0a,
0x0a, 0x02, 0x04, 0x09, 0x12, 0x04, 0x66, 0x00, 0x69, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x09,
0x01, 0x12, 0x03, 0x66, 0x08, 0x18, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x09, 0x02, 0x00, 0x05, 0x12,
0x03, 0x67, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x09, 0x02, 0x00, 0x12, 0x03, 0x67, 0x02,
0x14, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x09, 0x02, 0x00, 0x01, 0x12, 0x03, 0x67, 0x09, 0x0f, 0x0a,
0x0c, 0x0a, 0x05, 0x04, 0x09, 0x02, 0x00, 0x03, 0x12, 0x03, 0x67, 0x12, 0x13, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x09, 0x02, 0x01, 0x05, 0x12, 0x03, 0x68, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04,
0x09, 0x02, 0x01, 0x12, 0x03, 0x68, 0x02, 0x14, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x09, 0x02, 0x01,
0x01, 0x12, 0x03, 0x68, 0x09, 0x0f, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x09, 0x02, 0x01, 0x03, 0x12,
0x03, 0x68, 0x12, 0x13, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x0a, 0x12, 0x04, 0x6b, 0x00, 0x6f, 0x01,
0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x0a, 0x01, 0x12, 0x03, 0x6b, 0x08, 0x11, 0x0a, 0x0c, 0x0a, 0x05,
0x04, 0x0a, 0x02, 0x00, 0x05, 0x12, 0x03, 0x6c, 0x02, 0x07, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x0a,
0x02, 0x00, 0x12, 0x03, 0x6c, 0x02, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0a, 0x02, 0x00, 0x01,
0x12, 0x03, 0x6c, 0x08, 0x0c, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0a, 0x02, 0x00, 0x03, 0x12, 0x03,
0x6c, 0x0f, 0x10, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0a, 0x02, 0x01, 0x05, 0x12, 0x03, 0x6d, 0x02,
0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x0a, 0x02, 0x01, 0x12, 0x03, 0x6d, 0x02, 0x14, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x0a, 0x02, 0x01, 0x01, 0x12, 0x03, 0x6d, 0x09, 0x0f, 0x0a, 0x0c, 0x0a, 0x05,
0x04, 0x0a, 0x02, 0x01, 0x03, 0x12, 0x03, 0x6d, 0x12, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0a,
0x02, 0x02, 0x05, 0x12, 0x03, 0x6e, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x0a, 0x02, 0x02,
0x12, 0x03, 0x6e, 0x02, 0x18, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0a, 0x02, 0x02, 0x01, 0x12, 0x03,
0x6e, 0x09, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0a, 0x02, 0x02, 0x03, 0x12, 0x03, 0x6e, 0x16,
0x17, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x0b, 0x12, 0x04, 0x71, 0x00, 0x74, 0x01, 0x0a, 0x0a, 0x0a,
0x03, 0x04, 0x0b, 0x01, 0x12, 0x03, 0x71, 0x08, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0b, 0x02,
0x00, 0x05, 0x12, 0x03, 0x73, 0x02, 0x08, 0x0a, 0x2f, 0x0a, 0x04, 0x04, 0x0b, 0x02, 0x00, 0x12,
0x03, 0x73, 0x02, 0x12, 0x1a, 0x22, 0x20, 0x22, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x22, 0x2c,
0x20, 0x22, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x22, 0x2c, 0x20, 0x6f, 0x72, 0x20, 0x22, 0x72,
0x65, 0x6d, 0x6f, 0x76, 0x65, 0x22, 0x2e, 0x0a, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0b, 0x02, 0x00,
0x01, 0x12, 0x03, 0x73, 0x09, 0x0d, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0b, 0x02, 0x00, 0x03, 0x12,
0x03, 0x73, 0x10, 0x11, 0x0a, 0x09, 0x0a, 0x02, 0x04, 0x0c, 0x12, 0x03, 0x76, 0x00, 0x21, 0x0a,
0x0a, 0x0a, 0x03, 0x04, 0x0c, 0x01, 0x12, 0x03, 0x76, 0x08, 0x1e, 0x0a, 0x0a, 0x0a, 0x02, 0x04,
0x0d, 0x12, 0x04, 0x78, 0x00, 0x7f, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x0d, 0x01, 0x12, 0x03,
0x78, 0x08, 0x1c, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0d, 0x02, 0x00, 0x05, 0x12, 0x03, 0x79, 0x02,
0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x0d, 0x02, 0x00, 0x12, 0x03, 0x79, 0x02, 0x19, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x0d, 0x02, 0x00, 0x01, 0x12, 0x03, 0x79, 0x09, 0x14, 0x0a, 0x0c, 0x0a, 0x05,
0x04, 0x0d, 0x02, 0x00, 0x03, 0x12, 0x03, 0x79, 0x17, 0x18, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0d,
0x02, 0x01, 0x05, 0x12, 0x03, 0x7a, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x0d, 0x02, 0x01,
0x12, 0x03, 0x7a, 0x02, 0x18, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0d, 0x02, 0x01, 0x01, 0x12, 0x03,
0x7a, 0x09, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0d, 0x02, 0x01, 0x03, 0x12, 0x03, 0x7a, 0x16,
0x17, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0d, 0x02, 0x02, 0x05, 0x12, 0x03, 0x7b, 0x02, 0x08, 0x0a,
0x0b, 0x0a, 0x04, 0x04, 0x0d, 0x02, 0x02, 0x12, 0x03, 0x7b, 0x02, 0x18, 0x0a, 0x0c, 0x0a, 0x05,
0x04, 0x0d, 0x02, 0x02, 0x01, 0x12, 0x03, 0x7b, 0x09, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0d,
0x02, 0x02, 0x03, 0x12, 0x03, 0x7b, 0x16, 0x17, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0d, 0x02, 0x03,
0x05, 0x12, 0x03, 0x7c, 0x02, 0x06, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x0d, 0x02, 0x03, 0x12, 0x03,
0x7c, 0x02, 0x1c, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0d, 0x02, 0x03, 0x01, 0x12, 0x03, 0x7c, 0x07,
0x17, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0d, 0x02, 0x03, 0x03, 0x12, 0x03, 0x7c, 0x1a, 0x1b, 0x0a,
0x0c, 0x0a, 0x05, 0x04, 0x0d, 0x02, 0x04, 0x05, 0x12, 0x03, 0x7d, 0x02, 0x08, 0x0a, 0x0b, 0x0a,
0x04, 0x04, 0x0d, 0x02, 0x04, 0x12, 0x03, 0x7d, 0x02, 0x1d, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0d,
0x02, 0x04, 0x01, 0x12, 0x03, 0x7d, 0x09, 0x18, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0d, 0x02, 0x04,
0x03, 0x12, 0x03, 0x7d, 0x1b, 0x1c, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0d, 0x02, 0x05, 0x05, 0x12,
0x03, 0x7e, 0x02, 0x07, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x0d, 0x02, 0x05, 0x12, 0x03, 0x7e, 0x02,
0x20, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0d, 0x02, 0x05, 0x01, 0x12, 0x03, 0x7e, 0x08, 0x1b, 0x0a,
0x0c, 0x0a, 0x05, 0x04, 0x0d, 0x02, 0x05, 0x03, 0x12, 0x03, 0x7e, 0x1e, 0x1f, 0x0a, 0x0c, 0x0a,
0x02, 0x04, 0x0e, 0x12, 0x06, 0x81, 0x01, 0x00, 0x83, 0x01, 0x01, 0x0a, 0x0b, 0x0a, 0x03, 0x04,
0x0e, 0x01, 0x12, 0x04, 0x81, 0x01, 0x08, 0x1f, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x0e, 0x02, 0x00,
0x05, 0x12, 0x04, 0x82, 0x01, 0x02, 0x08, 0x0a, 0x0c, 0x0a, 0x04, 0x04, 0x0e, 0x02, 0x00, 0x12,
0x04, 0x82, 0x01, 0x02, 0x13, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x0e, 0x02, 0x00, 0x01, 0x12, 0x04,
0x82, 0x01, 0x09, 0x0e, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x0e, 0x02, 0x00, 0x03, 0x12, 0x04, 0x82,
0x01, 0x11, 0x12, 0x0a, 0x0c, 0x0a, 0x02, 0x04, 0x0f, 0x12, 0x06, 0x85, 0x01, 0x00, 0x87, 0x01,
0x01, 0x0a, 0x0b, 0x0a, 0x03, 0x04, 0x0f, 0x01, 0x12, 0x04, 0x85, 0x01, 0x08, 0x20, 0x0a, 0x0d,
0x0a, 0x05, 0x04, 0x0f, 0x02, 0x00, 0x06, 0x12, 0x04, 0x86, 0x01, 0x02, 0x0f, 0x0a, 0x0c, 0x0a,
0x04, 0x04, 0x0f, 0x02, 0x00, 0x12, 0x04, 0x86, 0x01, 0x02, 0x1d, 0x0a, 0x0d, 0x0a, 0x05, 0x04,
0x0f, 0x02, 0x00, 0x01, 0x12, 0x04, 0x86, 0x01, 0x10, 0x18, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x0f,
0x02, 0x00, 0x03, 0x12, 0x04, 0x86, 0x01, 0x1b, 0x1c, 0x0a, 0x0c, 0x0a, 0x02, 0x04, 0x10, 0x12,
0x06, 0x89, 0x01, 0x00, 0x91, 0x01, 0x01, 0x0a, 0x0b, 0x0a, 0x03, 0x04, 0x10, 0x01, 0x12, 0x04,
0x89, 0x01, 0x08, 0x22, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x00, 0x05, 0x12, 0x04, 0x8a,
0x01, 0x02, 0x08, 0x0a, 0x0c, 0x0a, 0x04, 0x04, 0x10, 0x02, 0x00, 0x12, 0x04, 0x8a, 0x01, 0x02,
0x13, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x00, 0x01, 0x12, 0x04, 0x8a, 0x01, 0x09, 0x0e,
0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x00, 0x03, 0x12, 0x04, 0x8a, 0x01, 0x11, 0x12, 0x0a,
0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x01, 0x04, 0x12, 0x04, 0x8b, 0x01, 0x02, 0x0a, 0x0a, 0x0c,
0x0a, 0x04, 0x04, 0x10, 0x02, 0x01, 0x12, 0x04, 0x8b, 0x01, 0x02, 0x1d, 0x0a, 0x0d, 0x0a, 0x05,
0x04, 0x10, 0x02, 0x01, 0x05, 0x12, 0x04, 0x8b, 0x01, 0x0b, 0x11, 0x0a, 0x0d, 0x0a, 0x05, 0x04,
0x10, 0x02, 0x01, 0x01, 0x12, 0x04, 0x8b, 0x01, 0x12, 0x18, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10,
0x02, 0x01, 0x03, 0x12, 0x04, 0x8b, 0x01, 0x1b, 0x1c, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02,
0x02, 0x04, 0x12, 0x04, 0x8c, 0x01, 0x02, 0x0a, 0x0a, 0x0c, 0x0a, 0x04, 0x04, 0x10, 0x02, 0x02,
0x12, 0x04, 0x8c, 0x01, 0x02, 0x23, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x02, 0x05, 0x12,
0x04, 0x8c, 0x01, 0x0b, 0x11, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x02, 0x01, 0x12, 0x04,
0x8c, 0x01, 0x12, 0x1e, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x02, 0x03, 0x12, 0x04, 0x8c,
0x01, 0x21, 0x22, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x03, 0x05, 0x12, 0x04, 0x8d, 0x01,
0x02, 0x08, 0x0a, 0x0c, 0x0a, 0x04, 0x04, 0x10, 0x02, 0x03, 0x12, 0x04, 0x8d, 0x01, 0x02, 0x13,
0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x03, 0x01, 0x12, 0x04, 0x8d, 0x01, 0x09, 0x0e, 0x0a,
0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x03, 0x03, 0x12, 0x04, 0x8d, 0x01, 0x11, 0x12, 0x0a, 0x0d,
0x0a, 0x05, 0x04, 0x10, 0x02, 0x04, 0x05, 0x12, 0x04, 0x8e, 0x01, 0x02, 0x07, 0x0a, 0x0c, 0x0a,
0x04, 0x04, 0x10, 0x02, 0x04, 0x12, 0x04, 0x8e, 0x01, 0x02, 0x19, 0x0a, 0x0d, 0x0a, 0x05, 0x04,
0x10, 0x02, 0x04, 0x01, 0x12, 0x04, 0x8e, 0x01, 0x08, 0x14, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10,
0x02, 0x04, 0x03, 0x12, 0x04, 0x8e, 0x01, 0x17, 0x18, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02,
0x05, 0x05, 0x12, 0x04, 0x8f, 0x01, 0x02, 0x08, 0x0a, 0x0c, 0x0a, 0x04, 0x04, 0x10, 0x02, 0x05,
0x12, 0x04, 0x8f, 0x01, 0x02, 0x19, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x05, 0x01, 0x12,
0x04, 0x8f, 0x01, 0x09, 0x14, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x05, 0x03, 0x12, 0x04,
0x8f, 0x01, 0x17, 0x18, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x06, 0x04, 0x12, 0x04, 0x90,
0x01, 0x02, 0x0a, 0x0a, 0x0c, 0x0a, 0x04, 0x04, 0x10, 0x02, 0x06, 0x12, 0x04, 0x90, 0x01, 0x02,
0x21, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x06, 0x05, 0x12, 0x04, 0x90, 0x01, 0x0b, 0x11,
0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x06, 0x01, 0x12, 0x04, 0x90, 0x01, 0x12, 0x1c, 0x0a,
0x0d, 0x0a, 0x05, 0x04, 0x10, 0x02, 0x06, 0x03, 0x12, 0x04, 0x90, 0x01, 0x1f, 0x20, 0x0a, 0x0c,
0x0a, 0x02, 0x04, 0x11, 0x12, 0x06, 0x93, 0x01, 0x00, 0x95, 0x01, 0x01, 0x0a, 0x0b, 0x0a, 0x03,
0x04, 0x11, 0x01, 0x12, 0x04, 0x93, 0x01, 0x08, 0x23, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x11, 0x02,
0x00, 0x06, 0x12, 0x04, 0x94, 0x01, 0x02, 0x0f, 0x0a, 0x0c, 0x0a, 0x04, 0x04, 0x11, 0x02, 0x00,
0x12, 0x04, 0x94, 0x01, 0x02, 0x1d, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x11, 0x02, 0x00, 0x01, 0x12,
0x04, 0x94, 0x01, 0x10, 0x18, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x11, 0x02, 0x00, 0x03, 0x12, 0x04,
0x94, 0x01, 0x1b, 0x1c, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x12, 0x12, 0x04, 0x97, 0x01, 0x00, 0x1b,
0x0a, 0x0b, 0x0a, 0x03, 0x04, 0x12, 0x01, 0x12, 0x04, 0x97, 0x01, 0x08, 0x18, 0x0a, 0x0c, 0x0a,
0x02, 0x04, 0x13, 0x12, 0x06, 0x99, 0x01, 0x00, 0x9b, 0x01, 0x01, 0x0a, 0x0b, 0x0a, 0x03, 0x04,
0x13, 0x01, 0x12, 0x04, 0x99, 0x01, 0x08, 0x19, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x13, 0x02, 0x00,
0x04, 0x12, 0x04, 0x9a, 0x01, 0x02, 0x0a, 0x0a, 0x0c, 0x0a, 0x04, 0x04, 0x13, 0x02, 0x00, 0x12,
0x04, 0x9a, 0x01, 0x02, 0x1f, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x13, 0x02, 0x00, 0x06, 0x12, 0x04,
0x9a, 0x01, 0x0b, 0x14, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x13, 0x02, 0x00, 0x01, 0x12, 0x04, 0x9a,
0x01, 0x15, 0x1a, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x13, 0x02, 0x00, 0x03, 0x12, 0x04, 0x9a, 0x01,
0x1d, 0x1e, 0x0a, 0xb9, 0x01, 0x0a, 0x02, 0x04, 0x14, 0x12, 0x06, 0xa0, 0x01, 0x00, 0xa7, 0x01,
0x01, 0x1a, 0xaa, 0x01, 0x20, 0x48, 0x75, 0x6d, 0x61, 0x6e, 0x2d, 0x72, 0x65, 0x61, 0x64, 0x61,
0x62, 0x6c, 0x65, 0x20, 0x74, 0x61, 0x67, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x20, 0x6f,
0x6e, 0x6c, 0x79, 0x2e, 0x20, 0x45, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x20, 0x74, 0x68,
0x65, 0x20, 0x62, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x20, 0x6c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x20,
0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x0a, 0x20, 0x28, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2c,
0x20, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68,
0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x2c, 0x20, 0x65, 0x74, 0x63, 0x2e, 0x29, 0x20, 0x74, 0x68,
0x61, 0x74, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x6d, 0x61, 0x6b, 0x65, 0x20, 0x73, 0x65, 0x6e,
0x73, 0x65, 0x20, 0x69, 0x6e, 0x73, 0x69, 0x64, 0x65, 0x0a, 0x20, 0x6d, 0x75, 0x73, 0x69, 0x63,
0x66, 0x73, 0x27, 0x73, 0x20, 0x46, 0x55, 0x53, 0x45, 0x20, 0x72, 0x65, 0x61, 0x64, 0x2d, 0x61,
0x73, 0x73, 0x65, 0x6d, 0x62, 0x6c, 0x79, 0x20, 0x70, 0x61, 0x74, 0x68, 0x2e, 0x0a, 0x0a, 0x0b,
0x0a, 0x03, 0x04, 0x14, 0x01, 0x12, 0x04, 0xa0, 0x01, 0x08, 0x14, 0x0a, 0x0d, 0x0a, 0x05, 0x04,
0x14, 0x02, 0x00, 0x04, 0x12, 0x04, 0xa1, 0x01, 0x02, 0x0a, 0x0a, 0x0c, 0x0a, 0x04, 0x04, 0x14,
0x02, 0x00, 0x12, 0x04, 0xa1, 0x01, 0x02, 0x1d, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x14, 0x02, 0x00,
0x05, 0x12, 0x04, 0xa1, 0x01, 0x0b, 0x11, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x14, 0x02, 0x00, 0x01,
0x12, 0x04, 0xa1, 0x01, 0x12, 0x18, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x14, 0x02, 0x00, 0x03, 0x12,
0x04, 0xa1, 0x01, 0x1b, 0x1c, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x14, 0x02, 0x01, 0x04, 0x12, 0x04,
0xa2, 0x01, 0x02, 0x0a, 0x0a, 0x0c, 0x0a, 0x04, 0x04, 0x14, 0x02, 0x01, 0x12, 0x04, 0xa2, 0x01,
0x02, 0x23, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x14, 0x02, 0x01, 0x05, 0x12, 0x04, 0xa2, 0x01, 0x0b,
0x11, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x14, 0x02, 0x01, 0x01, 0x12, 0x04, 0xa2, 0x01, 0x12, 0x1e,
0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x14, 0x02, 0x01, 0x03, 0x12, 0x04, 0xa2, 0x01, 0x21, 0x22, 0x0a,
0x0d, 0x0a, 0x05, 0x04, 0x14, 0x02, 0x02, 0x05, 0x12, 0x04, 0xa3, 0x01, 0x02, 0x08, 0x0a, 0x0c,
0x0a, 0x04, 0x04, 0x14, 0x02, 0x02, 0x12, 0x04, 0xa3, 0x01, 0x02, 0x13, 0x0a, 0x0d, 0x0a, 0x05,
0x04, 0x14, 0x02, 0x02, 0x01, 0x12, 0x04, 0xa3, 0x01, 0x09, 0x0e, 0x0a, 0x0d, 0x0a, 0x05, 0x04,
0x14, 0x02, 0x02, 0x03, 0x12, 0x04, 0xa3, 0x01, 0x11, 0x12, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x14,
0x02, 0x03, 0x05, 0x12, 0x04, 0xa4, 0x01, 0x02, 0x07, 0x0a, 0x0c, 0x0a, 0x04, 0x04, 0x14, 0x02,
0x03, 0x12, 0x04, 0xa4, 0x01, 0x02, 0x19, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x14, 0x02, 0x03, 0x01,
0x12, 0x04, 0xa4, 0x01, 0x08, 0x14, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x14, 0x02, 0x03, 0x03, 0x12,
0x04, 0xa4, 0x01, 0x17, 0x18, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x14, 0x02, 0x04, 0x05, 0x12, 0x04,
0xa5, 0x01, 0x02, 0x08, 0x0a, 0x0c, 0x0a, 0x04, 0x04, 0x14, 0x02, 0x04, 0x12, 0x04, 0xa5, 0x01,
0x02, 0x19, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x14, 0x02, 0x04, 0x01, 0x12, 0x04, 0xa5, 0x01, 0x09,
0x14, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x14, 0x02, 0x04, 0x03, 0x12, 0x04, 0xa5, 0x01, 0x17, 0x18,
0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x14, 0x02, 0x05, 0x04, 0x12, 0x04, 0xa6, 0x01, 0x02, 0x0a, 0x0a,
0x0c, 0x0a, 0x04, 0x04, 0x14, 0x02, 0x05, 0x12, 0x04, 0xa6, 0x01, 0x02, 0x21, 0x0a, 0x0d, 0x0a,
0x05, 0x04, 0x14, 0x02, 0x05, 0x05, 0x12, 0x04, 0xa6, 0x01, 0x0b, 0x11, 0x0a, 0x0d, 0x0a, 0x05,
0x04, 0x14, 0x02, 0x05, 0x01, 0x12, 0x04, 0xa6, 0x01, 0x12, 0x1c, 0x0a, 0x0d, 0x0a, 0x05, 0x04,
0x14, 0x02, 0x05, 0x03, 0x12, 0x04, 0xa6, 0x01, 0x1f, 0x20, 0x0a, 0x0c, 0x0a, 0x02, 0x04, 0x15,
0x12, 0x06, 0xa9, 0x01, 0x00, 0xb4, 0x01, 0x01, 0x0a, 0x0b, 0x0a, 0x03, 0x04, 0x15, 0x01, 0x12,
0x04, 0xa9, 0x01, 0x08, 0x11, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x15, 0x02, 0x00, 0x05, 0x12, 0x04,
0xaa, 0x01, 0x02, 0x08, 0x0a, 0x0c, 0x0a, 0x04, 0x04, 0x15, 0x02, 0x00, 0x12, 0x04, 0xaa, 0x01,
0x02, 0x13, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x15, 0x02, 0x00, 0x01, 0x12, 0x04, 0xaa, 0x01, 0x09,
0x0e, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x15, 0x02, 0x00, 0x03, 0x12, 0x04, 0xaa, 0x01, 0x11, 0x12,
0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x15, 0x02, 0x01, 0x05, 0x12, 0x04, 0xac, 0x01, 0x02, 0x08, 0x0a,
0x47, 0x0a, 0x04, 0x04, 0x15, 0x02, 0x01, 0x12, 0x04, 0xac, 0x01, 0x02, 0x1b, 0x1a, 0x39, 0x20,
0x52, 0x65, 0x61, 0x6c, 0x20, 0x6f, 0x6e, 0x2d, 0x64, 0x69, 0x73, 0x6b, 0x20, 0x70, 0x61, 0x74,
0x68, 0x20, 0x28, 0x77, 0x68, 0x65, 0x72, 0x65, 0x20, 0x74, 0x6f, 0x72, 0x61, 0x2f, 0x6c, 0x69,
0x62, 0x72, 0x71, 0x62, 0x69, 0x74, 0x20, 0x77, 0x72, 0x6f, 0x74, 0x65, 0x20, 0x74, 0x68, 0x65,
0x20, 0x66, 0x69, 0x6c, 0x65, 0x29, 0x2e, 0x0a, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x15, 0x02, 0x01,
0x01, 0x12, 0x04, 0xac, 0x01, 0x09, 0x16, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x15, 0x02, 0x01, 0x03,
0x12, 0x04, 0xac, 0x01, 0x19, 0x1a, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x15, 0x02, 0x02, 0x05, 0x12,
0x04, 0xaf, 0x01, 0x02, 0x08, 0x0a, 0x7a, 0x0a, 0x04, 0x04, 0x15, 0x02, 0x02, 0x12, 0x04, 0xaf,
0x01, 0x02, 0x18, 0x1a, 0x6c, 0x20, 0x56, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x20, 0x46, 0x55,
0x53, 0x45, 0x20, 0x70, 0x61, 0x74, 0x68, 0x20, 0x28, 0x7b, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74,
0x7d, 0x2f, 0x7b, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x7d, 0x2f, 0x7b, 0x66, 0x69, 0x6c, 0x65, 0x6e,
0x61, 0x6d, 0x65, 0x7d, 0x29, 0x3b, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x20, 0x77,
0x68, 0x65, 0x6e, 0x20, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x0a, 0x20, 0x63, 0x68,
0x61, 0x6e, 0x67, 0x65, 0x73, 0x20, 0x76, 0x69, 0x61, 0x20, 0x60, 0x55, 0x70, 0x64, 0x61, 0x74,
0x65, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x60, 0x2e,
0x0a, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x15, 0x02, 0x02, 0x01, 0x12, 0x04, 0xaf, 0x01, 0x09, 0x13,
0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x15, 0x02, 0x02, 0x03, 0x12, 0x04, 0xaf, 0x01, 0x16, 0x17, 0x0a,
0x0d, 0x0a, 0x05, 0x04, 0x15, 0x02, 0x03, 0x05, 0x12, 0x04, 0xb1, 0x01, 0x02, 0x08, 0x0a, 0x28,
0x0a, 0x04, 0x04, 0x15, 0x02, 0x03, 0x12, 0x04, 0xb1, 0x01, 0x02, 0x12, 0x1a, 0x1a, 0x20, 0x46,
0x69, 0x6c, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e,
0x74, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x2e, 0x0a, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x15, 0x02, 0x03,
0x01, 0x12, 0x04, 0xb1, 0x01, 0x09, 0x0d, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x15, 0x02, 0x03, 0x03,
0x12, 0x04, 0xb1, 0x01, 0x10, 0x11, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x15, 0x02, 0x04, 0x04, 0x12,
0x04, 0xb3, 0x01, 0x02, 0x0a, 0x0a, 0x35, 0x0a, 0x04, 0x04, 0x15, 0x02, 0x04, 0x12, 0x04, 0xb3,
0x01, 0x02, 0x25, 0x1a, 0x27, 0x20, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x69, 0x66, 0x20, 0x74,
0x68, 0x65, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x20, 0x68, 0x61, 0x73, 0x20, 0x6e, 0x6f, 0x20, 0x70,
0x61, 0x72, 0x73, 0x65, 0x64, 0x20, 0x74, 0x61, 0x67, 0x73, 0x2e, 0x0a, 0x0a, 0x0d, 0x0a, 0x05,
0x04, 0x15, 0x02, 0x04, 0x06, 0x12, 0x04, 0xb3, 0x01, 0x0b, 0x17, 0x0a, 0x0d, 0x0a, 0x05, 0x04,
0x15, 0x02, 0x04, 0x01, 0x12, 0x04, 0xb3, 0x01, 0x18, 0x20, 0x0a, 0x0d, 0x0a, 0x05, 0x04, 0x15,
0x02, 0x04, 0x03, 0x12, 0x04, 0xb3, 0x01, 0x23, 0x24, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
];
include!("musicfs.tonic.rs");
// @@protoc_insertion_point(module)
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
pub mod musicfs {
include!("generated/musicfs/musicfs.rs");
}
pub use musicfs::{
ChangeEvent, ClientStatusResponse, FileChunk, FileEntry, FileMetadata, GetClientStatusRequest,
GetFileRequest, GetManifestRequest, GetMetadataRequest, GetMusicMetadataRequest,
GetMusicMetadataResponse, InodeHash, ListFilesRequest, ListFilesResponse, ManifestEntry,
MusicMetadata, PictureDataRange, ReconcileRequest, ReconcileResponse, SubscribeEventsRequest,
UpdateMusicMetadataRequest, UpdateMusicMetadataResponse,
client_control_server::{ClientControl, ClientControlServer},
client_status_server::{ClientStatus, ClientStatusServer},
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,19 +8,19 @@ 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
/// scan and refreshed by the watcher on inotify events.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
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)]
@@ -37,16 +37,17 @@ impl ServerState {
/// Replace the entire map with a fresh scan of `source`. Used on startup
/// and on watcher-driven reconciliations.
pub fn replace_all(&self, source: &Path) -> io::Result<()> {
pub fn replace_all(&self, source: &Path) -> io::Result<bool> {
let entries = scan_directory(source)?;
let count = entries.len();
let new_map: HashMap<u64, FileEntry> = entries.into_iter().collect();
let mut map = self.inner.lock().unwrap();
map.clear();
for (id, entry) in entries {
map.insert(id, entry);
if *map == new_map {
return Ok(false);
}
info!(count, "server state: scan complete");
return Ok(());
let count = new_map.len();
*map = new_map;
info!(count, "server state: scan complete (changed)");
return Ok(true);
}
/// Snapshot the current state into a manifest. Order is by inode ascending
@@ -179,4 +180,81 @@ mod tests {
state.replace_all(source).unwrap();
assert_eq!(state.manifest().len(), 0);
}
#[test]
fn replace_all_returns_true_on_first_scan() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
fs::write(source.join("a.txt"), b"x").unwrap();
let state = ServerState::new();
assert!(state.replace_all(source).unwrap());
}
#[test]
fn replace_all_returns_false_when_unchanged() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
fs::write(source.join("a.txt"), b"hello").unwrap();
fs::write(source.join("b.txt"), b"world").unwrap();
let state = ServerState::new();
state.replace_all(source).unwrap();
assert!(!state.replace_all(source).unwrap());
}
#[test]
fn replace_all_returns_true_after_file_added() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
fs::write(source.join("a.txt"), b"x").unwrap();
let state = ServerState::new();
state.replace_all(source).unwrap();
fs::write(source.join("b.txt"), b"y").unwrap();
assert!(state.replace_all(source).unwrap());
}
#[test]
fn replace_all_returns_true_after_file_removed() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
fs::write(source.join("a.txt"), b"x").unwrap();
fs::write(source.join("b.txt"), b"y").unwrap();
let state = ServerState::new();
state.replace_all(source).unwrap();
fs::remove_file(source.join("a.txt")).unwrap();
assert!(state.replace_all(source).unwrap());
}
#[test]
fn replace_all_returns_true_after_file_renamed() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
fs::write(source.join("old.txt"), b"x").unwrap();
let state = ServerState::new();
state.replace_all(source).unwrap();
assert!(!state.replace_all(source).unwrap());
fs::rename(source.join("old.txt"), source.join("new.txt")).unwrap();
assert!(state.replace_all(source).unwrap());
}
#[test]
fn replace_all_returns_true_after_content_modified() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
fs::write(source.join("a.txt"), b"original").unwrap();
let state = ServerState::new();
state.replace_all(source).unwrap();
assert!(!state.replace_all(source).unwrap());
fs::write(source.join("a.txt"), b"modified content").unwrap();
assert!(state.replace_all(source).unwrap());
}
}
@@ -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,
}
}
@@ -2,6 +2,7 @@ use std::{
path::PathBuf,
sync::{Arc, Mutex},
thread,
time::Duration,
};
use notify::{EventKind, RecursiveMode, Watcher};
@@ -10,6 +11,8 @@ use tokio::sync::broadcast;
use crate::server::state::ServerState;
use tracing::{error, info};
const POLL_INTERVAL: Duration = Duration::from_secs(15);
/// A change observed by the watcher. Pushed onto the broadcast channel for
/// `/events` subscribers. The client treats these as wake-ups: correctness
/// always rests on the subsequent `/manifest` hash diff.
@@ -76,25 +79,41 @@ fn run_watcher_loop(
}
info!(source = %source.display(), "server watcher: scanning for changes");
for res in rx {
match res {
Ok(event) => match event.kind {
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {
let kind = match event.kind {
EventKind::Create(_) => ChangeKind::Create,
EventKind::Modify(_) => ChangeKind::Modify,
EventKind::Remove(_) => ChangeKind::Remove,
_ => continue,
};
if let Err(e) = state.replace_all(&source) {
error!(error = %e, "server watcher: state refresh failed");
continue;
loop {
match rx.recv_timeout(POLL_INTERVAL) {
Ok(res) => match res {
Ok(event) => match event.kind {
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {
let kind = match event.kind {
EventKind::Create(_) => ChangeKind::Create,
EventKind::Modify(_) => ChangeKind::Modify,
EventKind::Remove(_) => ChangeKind::Remove,
_ => continue,
};
match state.replace_all(&source) {
Ok(true) => {
let _ = events_tx.send(ChangeEvent { kind });
}
Ok(false) => {}
Err(e) => {
error!(error = %e, "server watcher: state refresh failed");
}
}
}
let _ = events_tx.send(ChangeEvent { kind });
}
_ => {}
_ => {}
},
Err(e) => error!(error = %e, "server watcher: inotify error"),
},
Err(e) => error!(error = %e, "server watcher: inotify error"),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => match state.replace_all(&source) {
Ok(true) => {
let _ = events_tx.send(ChangeEvent {
kind: ChangeKind::Modify,
});
}
Ok(false) => {}
Err(e) => error!(error = %e, "server watcher: poll rescan failed"),
},
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
}
}
}
+38 -5
View File
@@ -6,6 +6,33 @@
...
}:
let
# devenv's `languages.rust.import` only builds single root-package crates
# (it hardcodes `cargoNix.rootCrate.build`). musicfs is a virtual workspace with
# no root package, so we drive crate2nix directly and select the `musicfs-client`
# member out of `workspaceMembers`. src = ./. gives crate2nix the whole workspace,
# so the member's path-dependencies (musicfs-core, musicfs-proto) resolve.
crate2nixTools = pkgs.callPackage "${inputs.crate2nix}/tools.nix" { };
cargoNix =
pkgs.callPackage
(crate2nixTools.generatedCargoNix {
name = "musicfs";
src = ./.;
})
{
# Build with the toolchain configured for this dev environment,
# matching what languages.rust.import does internally.
buildRustCrateForPkgs =
_:
pkgs.buildRustCrate.override {
rustc = config.languages.rust.toolchainPackage;
cargo = config.languages.rust.toolchainPackage;
};
};
# musicfs-client's binary is named `musicfs` (see [[bin]] in its Cargo.toml).
musicfs = cargoNix.workspaceMembers."musicfs-client".build;
in
{
languages.rust.enable = true;
git-hooks.hooks = {
@@ -42,18 +69,24 @@
processes.musicfs = {
after = [ "devenv:processes:postgres" ];
exec = lib.mkDefault ''
cargo run -- \
${musicfs}/bin/musicfs \
--source /home/fujin/Music \
--mountpoint /tmp/rust-fuse \
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
'';
# Probes the standard gRPC Health Checking Protocol exposed on the
# musicfs-client status server (default --listen 127.0.0.1:50052).
# SERVING implies the FUSE mount succeeded and, for NetworkOrigin, the
# upstream musicfs-server is reachable (health.rs toggles not_serving on
# upstream loss). Inherited by profiles.* since they only override `exec`.
ready.exec = "grpcurl -plaintext -max-time 2 127.0.0.1:50052 grpc.health.v1.Health/Check | grep -q SERVING";
};
profiles.local.module = {
processes.musicfs = {
after = [ "devenv:processes:postgres" ];
exec = ''
cargo run -- \
${musicfs}/bin/musicfs \
--source /home/fujin/Music \
--mountpoint /tmp/rust-fuse \
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
@@ -65,7 +98,7 @@
processes.musicfs = {
after = [ "devenv:processes:postgres" ];
exec = ''
cargo run -- \
${musicfs}/bin/musicfs \
--source http://10.185.226.145:50051 \
--mountpoint /tmp/rust-fuse \
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
@@ -77,7 +110,7 @@
processes.musicfs = {
after = [ "devenv:processes:postgres" ];
exec = ''
cargo run -- \
${musicfs}/bin/musicfs \
--source http://127.0.0.1:50061 \
--mountpoint ./target/e2e/mnt \
--database "postgresql://fujin@localhost/musicfs_e2e?host=$PGHOST" \
@@ -101,6 +134,6 @@
};
outputs = {
rust-app = config.languages.rust.import ./. { };
musicfs = musicfs;
};
}
+10 -1
View File
@@ -6,5 +6,14 @@ build:
up profile:
devenv up --profile {{profile}}
vm command:
scripts/vm.sh {{command}}
redeploy:
scripts/vm.sh redeploy
e2e:
scripts/e2e/run.sh
scripts/e2e/run.sh
e2e-resilience:
scripts/e2e/run-resilience.sh
+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
+78
View File
@@ -32,6 +32,20 @@ service MusicFs {
rpc SubscribeEvents(SubscribeEventsRequest) returns (stream ChangeEvent);
}
// Client-side status reporting (served by the FUSE client process).
service ClientStatus {
rpc GetClientStatus(GetClientStatusRequest) returns (ClientStatusResponse);
}
service ClientControl {
rpc GetMusicMetadata(GetMusicMetadataRequest) returns (GetMusicMetadataResponse);
rpc UpdateMusicMetadata(UpdateMusicMetadataRequest) returns (UpdateMusicMetadataResponse);
// Every file musicfs knows about. Caller filters by `original_path`
// prefix to find the files of interest (e.g. all files under a single
// torrent's output directory).
rpc ListFiles(ListFilesRequest) returns (ListFilesResponse);
}
message ReconcileRequest {
repeated InodeHash entries = 1;
}
@@ -101,3 +115,67 @@ message ChangeEvent {
// "create", "modify", or "remove".
string kind = 1;
}
message GetClientStatusRequest {}
message ClientStatusResponse {
string origin_type = 1;
string mountpoint = 2;
uint64 file_count = 3;
bool server_connected = 4;
string server_endpoint = 5;
int64 last_reconcile_unix = 6;
}
message GetMusicMetadataRequest {
uint64 inode = 1;
}
message GetMusicMetadataResponse {
MusicMetadata metadata = 1;
}
message UpdateMusicMetadataRequest {
uint64 inode = 1;
repeated string artist = 2;
optional string album_artist = 3;
string album = 4;
int32 track_number = 5;
string track_title = 6;
repeated string other_tags = 7;
}
message UpdateMusicMetadataResponse {
MusicMetadata metadata = 1;
}
message ListFilesRequest {}
message ListFilesResponse {
repeated FileEntry files = 1;
}
// Human-readable tag fields only. Excludes the binary layout fields
// (header, picture_block_headers, etc.) that only make sense inside
// musicfs's FUSE read-assembly path.
message FileMetadata {
repeated string artist = 1;
optional string album_artist = 2;
string album = 3;
int32 track_number = 4;
string track_title = 5;
repeated string other_tags = 6;
}
message FileEntry {
uint64 inode = 1;
// Real on-disk path (where tora/librqbit wrote the file).
string original_path = 2;
// Virtual FUSE path ({artist}/{album}/{filename}); updated when metadata
// changes via `UpdateMusicMetadata`.
string local_path = 3;
// Filename component only.
string name = 4;
// Empty if the file has no parsed tags.
optional FileMetadata metadata = 5;
}
+12
View File
@@ -14,10 +14,13 @@ against the mountpoint.
```sh
just e2e # full suite (creates VM, runs tests, destroys VM)
KEEP=1 just e2e # keep VM + DB alive after run for debugging
just e2e-resilience # resilience suite (server stop/start, network partition, outage recovery)
```
## What it tests
### Functional suite (`just e2e`)
| Phase | Description |
| ------------ | ------------------------------------------------------- |
| structural | Virtual directory tree matches fixture tags |
@@ -29,6 +32,15 @@ KEEP=1 just e2e # keep VM + DB alive after run for debugging
| chunked write| Non-metadata writes don't corrupt tag structure |
| persistence | Tags survive client restart (DB restore path) |
### Resilience suite (`just e2e-resilience`)
| Scenario | Disruption | What it verifies |
| ----------------- | ----------------------------------- | -------------------------------------------------------- |
| server stop/start | `systemctl stop/start` in VM | EIO on uncached reads, cached reads survive, watcher resubscribes within 10s |
| network partition | `incus pause/start` (silent drop) | EIO within 10s (tonic 3s timeout × kernel readahead), watcher detects via HTTP/2 keepalive within 30s |
| cache resilience | server stop, all files pre-cached | All reads/writes from cache, no server contact needed |
| long outage | `incus pause` for 15s+ | Watcher detects via keepalive PING, resubscribes within 10s of recovery |
## Fixtures
`make-fixtures.sh` generates a deterministic library under `target/e2e/music/`:
+309
View File
@@ -0,0 +1,309 @@
#!/usr/bin/env bash
#
# resilience-helpers.sh — disruption primitives and FUSE/log assertions for
# network stability and reconnection E2E tests.
#
# Requires lib.sh globals ($FAILURES, $PASSES, $FAILED_TESTS) and config
# variables from run-resilience.sh ($VM_NAME, $PORT, $VM_IP, $MNT, $LOG_DIR,
# $DB_NAME, $PGHOST, $CLIENT_PID, $ALPHA, $BETA, $GAMMA, $DELTA).
#
# Sourced by run-resilience.sh — do NOT execute directly.
# ── timeouts (seconds) ──
HEALTH_TIMEOUT=45
READ_TIMEOUT=15
READ_FAIL_TIMEOUT=10
RECOVER_TIMEOUT=30
VM_RESUME_TIMEOUT=30
# ── internal pass/fail (reuses lib.sh globals) ──
_res_pass() {
printf ' \033[1;32mPASS\033[0m %s\n' "$1"
((++PASSES))
}
_res_fail() {
printf ' \033[1;31mFAIL\033[0m %s\n' "$1"
((++FAILURES))
FAILED_TESTS+="$1; "
}
# ── client liveness ──
check_client_alive() {
if [[ -n "$CLIENT_PID" ]] && ! kill -0 "$CLIENT_PID" 2>/dev/null; then
printf '\033[1;31m[e2e error]\033[0m client process died — check %s\n' "$LOG_DIR" >&2
return 1
fi
return 0
}
# ── disruption primitives ──
server_stop() {
log "stopping server in VM…"
incus exec "$VM_NAME" -- systemctl stop musicfs-server.service 2>/dev/null || true
}
server_start() {
log "starting server in VM…"
incus exec "$VM_NAME" -- systemctl start musicfs-server.service 2>/dev/null || true
}
vm_pause() {
log "pausing VM '$VM_NAME'…"
incus pause "$VM_NAME"
}
vm_resume() {
log "resuming VM '$VM_NAME'…"
incus start "$VM_NAME"
local i
for ((i = 0; i < VM_RESUME_TIMEOUT; i++)); do
if incus exec "$VM_NAME" -- true 2>/dev/null; then return 0; fi
sleep 1
done
die "VM did not become reachable after resume"
}
# ── health check helpers ──
wait_health() {
local timeout="${1:-$HEALTH_TIMEOUT}"
local i
for ((i = 0; i < timeout; i++)); do
if timeout 5 grpcurl -plaintext "$VM_IP:$PORT" grpc.health.v1.Health/Check 2>/dev/null | grep -q SERVING; then
return 0
fi
sleep 1
done
return 1
}
assert_health_down() {
local desc="$1"
if timeout 5 grpcurl -plaintext "$VM_IP:$PORT" grpc.health.v1.Health/Check 2>/dev/null | grep -q SERVING; then
_res_fail "$desc (server still serving)"
else
_res_pass "$desc"
fi
}
assert_health_up() {
local desc="$1"
if wait_health "$HEALTH_TIMEOUT"; then
_res_pass "$desc"
else
_res_fail "$desc (health check timed out)"
fi
}
# ── cache control ──
clear_cache() {
log "clearing Postgres byte cache…"
psql -q -d "$DB_NAME" -c "DELETE FROM cached_file_bytes;" 2>/dev/null || true
}
# ── prewarm ──
prewarm_file() {
local file="$1"
timeout "$READ_TIMEOUT" dd if="$file" of=/dev/null bs=4096 2>/dev/null || true
}
prewarm_all() {
log "prewarming cache (reading all test files)…"
prewarm_file "$ALPHA"
prewarm_file "$BETA"
prewarm_file "$GAMMA"
prewarm_file "$DELTA"
}
# ── FUSE read assertions ──
assert_fuse_read_ok() {
local desc="$1" file="$2"
check_client_alive || return 1
if timeout "$READ_TIMEOUT" dd if="$file" of=/dev/null bs=4096 2>/dev/null; then
_res_pass "$desc"
else
_res_fail "$desc (read failed or timed out after ${READ_TIMEOUT}s)"
fi
}
# Assert a FUSE read fails with EIO *before* max_seconds.
# Distinguishes "fast EIO" (system returned an error) from "indefinite hang"
# (timeout killed dd). Currently FAILS during silent partitions because
# there's no tonic timeout configured — the RPC hangs until the OS gives up.
assert_fuse_eio_within() {
local desc="$1" file="$2" max_seconds="${3:-$READ_FAIL_TIMEOUT}"
check_client_alive || return 1
local start; start=$(date +%s)
local rc=0
timeout "$max_seconds" dd if="$file" of=/dev/null bs=4096 2>/dev/null || rc=$?
local end; end=$(date +%s)
local elapsed=$((end - start))
if (( rc == 0 )); then
_res_fail "$desc (read succeeded unexpectedly in ${elapsed}s)"
elif (( elapsed >= max_seconds )); then
_res_fail "$desc (hung ${elapsed}s — no EIO returned; missing tonic timeout)"
else
_res_pass "$desc (EIO in ${elapsed}s)"
fi
}
assert_fuse_metaflac_ok() {
local desc="$1" file="$2"
check_client_alive || return 1
if timeout "$READ_TIMEOUT" metaflac --show-tag=TITLE "$file" >/dev/null 2>&1; then
_res_pass "$desc"
else
_res_fail "$desc (metaflac read failed)"
fi
}
assert_fuse_id3v2_ok() {
local desc="$1" file="$2"
check_client_alive || return 1
if timeout "$READ_TIMEOUT" id3v2 --list "$file" >/dev/null 2>&1; then
_res_pass "$desc"
else
_res_fail "$desc (id3v2 read failed)"
fi
}
assert_fuse_ffprobe_ok() {
local desc="$1" file="$2"
check_client_alive || return 1
if timeout "$READ_TIMEOUT" ffprobe -hide_banner -v quiet -show_format "$file" >/dev/null 2>&1; then
_res_pass "$desc"
else
_res_fail "$desc (ffprobe failed)"
fi
}
# ── FUSE directory assertion ──
assert_dir_listing() {
local desc="$1" dir="$2" expected="$3"
check_client_alive || return 1
local result; result="$(timeout "$READ_TIMEOUT" ls "$dir" 2>/dev/null || true)"
if [[ "$result" == *"$expected"* ]]; then
_res_pass "$desc"
else
_res_fail "$desc (directory missing '$expected')"
fi
}
# ── FUSE write assertions ──
assert_metaflac_write_readback() {
local desc="$1" file="$2" tag="$3" value="$4"
check_client_alive || return 1
metaflac --remove-tag="$tag" --set-tag="$tag=$value" "$file" 2>/dev/null
local result; result="$(metaflac --show-tag="$tag" "$file" 2>/dev/null || true)"
if [[ "$result" == *"$value"* ]]; then
_res_pass "$desc"
else
_res_fail "$desc (write readback mismatch)"
fi
}
assert_mp3_write_readback() {
local desc="$1" file="$2" song="$3"
check_client_alive || return 1
id3v2 --song "$song" "$file" 2>/dev/null
local result; result="$(id3v2 --list "$file" 2>/dev/null || true)"
if [[ "$result" == *"$song"* ]]; then
_res_pass "$desc"
else
_res_fail "$desc (MP3 write readback mismatch)"
fi
}
# ── log assertions ──
_get_log_file() {
ls -t "$LOG_DIR"/musicfs.*.log 2>/dev/null | head -1
}
assert_log_contains() {
local desc="$1" pattern="$2"
local log_file; log_file="$(_get_log_file)"
if [[ -n "$log_file" ]] && grep -q "$pattern" "$log_file" 2>/dev/null; then
_res_pass "$desc"
else
_res_fail "$desc (pattern '$pattern' not in logs)"
fi
}
log_contains_since() {
local pattern="$1" from_line="$2"
local log_file; log_file="$(_get_log_file)"
[[ -n "$log_file" ]] && tail -n +"$((from_line + 1))" "$log_file" 2>/dev/null | grep -q "$pattern"
}
log_line_count() {
local log_file; log_file="$(_get_log_file)"
if [[ -n "$log_file" ]]; then
wc -l < "$log_file" 2>/dev/null || echo 0
else
echo 0
fi
}
assert_log_contains_after() {
local desc="$1" pattern="$2" marker="$3"
local log_file; log_file="$(_get_log_file)"
if [[ -z "$log_file" ]]; then
_res_fail "$desc (no log file)"
return
fi
# Find line number of the last occurrence of the marker.
local marker_line
marker_line="$(grep -n "$marker" "$log_file" 2>/dev/null | tail -1 | cut -d: -f1)"
if [[ -z "$marker_line" ]]; then
_res_fail "$desc (marker '$marker' not in log)"
return
fi
if tail -n +"$((marker_line + 1))" "$log_file" 2>/dev/null | grep -q "$pattern"; then
_res_pass "$desc"
else
_res_fail "$desc ('$pattern' not found after marker line $marker_line)"
fi
}
# ── recovery wait ──
wait_recover() {
local file="$1"
local timeout="${2:-$RECOVER_TIMEOUT}"
local i
for ((i = 0; i < timeout; i++)); do
check_client_alive || return 1
if timeout 5 dd if="$file" of=/dev/null bs=4096 2>/dev/null; then
return 0
fi
sleep 1
done
return 1
}
assert_recovered() {
local desc="$1" file="$2"
if wait_recover "$file"; then
_res_pass "$desc"
else
_res_fail "$desc (did not recover within ${RECOVER_TIMEOUT}s)"
fi
}
# ── scenario isolation ──
ensure_clean_state() {
log "ensuring clean state between scenarios…"
# Server must be up
if ! wait_health 5 2>/dev/null; then
server_start
wait_health "$HEALTH_TIMEOUT" || die "server won't come up between scenarios"
fi
# VM must be running (not paused)
local state
state="$(incus list "$VM_NAME" -f csv -c s 2>/dev/null | head -1)"
if [[ "$state" == "FROZEN" ]]; then
vm_resume
wait_health "$HEALTH_TIMEOUT" || die "server unhealthy after VM resume"
fi
check_client_alive || die "client died between scenarios"
clear_cache
}
+433
View File
@@ -0,0 +1,433 @@
#!/usr/bin/env bash
#
# run-resilience.sh — network stability and reconnection E2E suite for musicfs.
#
# Tests client behavior during server outages, network partitions (VM pause),
# cache resilience, and long-outage watcher recovery.
#
# Requires: devenv shell (for ffmpeg, metaflac, id3v2, incus, grpcurl, Postgres)
#
# Usage:
# scripts/e2e/run-resilience.sh full suite
# KEEP=1 scripts/e2e/run-resilience.sh keep VM + DB alive for debugging
#
set -euo pipefail
# ─────────────────────────── config ────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPTS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
MNT="$PROJECT_DIR/target/e2e/mnt"
MUSIC_DIR="$PROJECT_DIR/target/e2e/music"
LOG_DIR="$PROJECT_DIR/logs/e2e"
DB_NAME="musicfs_e2e"
VM_NAME="musicfs-e2e"
PORT=50061
KEEP="${KEEP:-0}"
RUST_LOG_VAL="${RUST_LOG:-musicfs=debug}"
CLIENT_PID=""
STARTED_PG=""
VM_IP=""
. "$SCRIPT_DIR/lib.sh"
. "$SCRIPT_DIR/resilience-helpers.sh"
log() { printf '\033[1;34m[e2e]\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31m[e2e error]\033[0m %s\n' "$*" >&2; exit 1; }
# ──────────────────────── postgres ─────────────────────────────────────
ensure_postgres() {
if pg_isready -h "$PGHOST" >/dev/null 2>&1; then return 0; fi
log "Postgres not running — starting via devenv…"
devenv up postgres &
STARTED_PG=$!
local i
for ((i = 0; i < 45; i++)); do
if pg_isready -h "$PGHOST" >/dev/null 2>&1; then
log "Postgres ready"
return 0
fi
sleep 1
done
die "Postgres did not become ready in 45s"
}
stop_postgres() {
[[ -n "$STARTED_PG" ]] || return 0
log "stopping Postgres (started by this script)…"
kill "$STARTED_PG" 2>/dev/null || true
wait "$STARTED_PG" 2>/dev/null || true
STARTED_PG=""
}
# ──────────────────────── 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 \
bash "$SCRIPTS_DIR/vm.sh" up
VM_IP="$(vm_ip)"
[[ -n "$VM_IP" ]] || die "could not determine VM IP"
log "VM IP: $VM_IP"
log "waiting for health check…"
local i
for ((i = 0; i < 45; i++)); do
if grpcurl -plaintext "$VM_IP:$PORT" grpc.health.v1.Health/Check 2>/dev/null | grep -q SERVING; then
log "server healthy"
return
fi
sleep 1
done
die "server health check timed out"
}
destroy_vm() {
VM_NAME="$VM_NAME" bash "$SCRIPTS_DIR/vm.sh" destroy 2>/dev/null || true
}
# ──────────────────────── DB management ────────────────────────────────
reset_db() {
log "resetting database '$DB_NAME'…"
dropdb --if-exists "$DB_NAME" 2>/dev/null || true
createdb "$DB_NAME"
psql -q -d "$DB_NAME" -f "$PROJECT_DIR/db/schema.sql"
}
drop_db() {
dropdb --if-exists "$DB_NAME" 2>/dev/null || true
}
# ────────────────────── client lifecycle ───────────────────────────────
build_client() {
log "building client…"
(cd "$PROJECT_DIR" && cargo build --bin musicfs)
}
start_client() {
mkdir -p "$MNT" "$LOG_DIR"
local dsn="postgresql://fujin@localhost/$DB_NAME?host=$PGHOST"
log "starting client (mountpoint $MNT)…"
RUST_LOG="$RUST_LOG_VAL" \
"$PROJECT_DIR/target/debug/musicfs" \
--source "http://$VM_IP:$PORT" \
--mountpoint "$MNT" \
--database "$dsn" \
--log-dir "$LOG_DIR" &
CLIENT_PID=$!
wait_for_mount
}
wait_for_mount() {
log "waiting for FUSE mount…"
local i
for ((i = 0; i < 60; i++)); do
if [[ -d "$MNT/E2E Artist A" ]]; then
log "mount ready"
return
fi
if ! kill -0 "$CLIENT_PID" 2>/dev/null; then
die "client process exited prematurely — check $LOG_DIR"
fi
sleep 1
done
die "FUSE mount not ready after 60s"
}
stop_client() {
[[ -n "$CLIENT_PID" ]] || return 0
kill "$CLIENT_PID" 2>/dev/null || true
local i
for ((i = 0; i < 10; i++)); do
kill -0 "$CLIENT_PID" 2>/dev/null || break
sleep 1
done
kill -9 "$CLIENT_PID" 2>/dev/null || true
wait "$CLIENT_PID" 2>/dev/null || true
CLIENT_PID=""
fusermount -u "$MNT" 2>/dev/null || true
sleep 1
}
# ─────────────────────────── cleanup ───────────────────────────────────
cleanup() {
local rv=$?
log "cleaning up…"
stop_client || true
if [[ "$KEEP" != "1" ]]; then
drop_db
destroy_vm
stop_postgres || true
rm -rf "$MNT"
else
log "KEEP=1 — VM, DB, and mount left intact for debugging"
log " mountpoint: $MNT"
log " VM IP: ${VM_IP:-<unknown>}"
log " logs: $LOG_DIR"
fi
exit $rv
}
trap cleanup EXIT
# ─────────────────────────── paths ─────────────────────────────────────
ALPHA="$MNT/E2E Artist A/E2E Album X/01 - Alpha.flac"
BETA="$MNT/E2E Artist A/E2E Album X/02 - Beta.flac"
GAMMA="$MNT/E2E Artist B/E2E Album Y/01 - Gamma.mp3"
DELTA="$MNT/E2E Artist B/E2E Album Y/02 - Delta.mp3"
# ════════════════════════════════════════════════════════════════════════
# RESILIENCE SCENARIOS
# ════════════════════════════════════════════════════════════════════════
# ── Scenario 1: Server Stop → Outage Window → Server Start → Recovery ──
#
# Disruption: systemctl stop musicfs-server.service
# Tests: FUSE error handling (EIO for uncached reads), cache fallback,
# tonic lazy channel auto-reconnect after clean server shutdown.
test_server_stop_start() {
printf '\n\033[1;36m── scenario 1: server stop/start ──\033[0m\n'
clear_cache
local watch_marker; watch_marker="$(log_line_count)"
# Baseline: read works
assert_fuse_read_ok "baseline — read Alpha (uncached)" "$ALPHA"
# Cache Alpha by reading it; leave Gamma uncached
# (Alpha is now cached from the read above)
# ── Disruption: stop server ──
server_stop
assert_health_down "server is down after stop"
# During outage
assert_fuse_read_ok "outage — cached read Alpha succeeds" "$ALPHA"
assert_fuse_eio_within "outage — uncached read Gamma fails (EIO)" "$GAMMA"
assert_dir_listing "outage — directory listing still works" "$MNT" "E2E Artist A"
# ── Recovery: start server ──
server_start
assert_health_up "server recovered after start"
assert_fuse_read_ok "recovery — uncached read Gamma succeeds" "$GAMMA"
assert_metaflac_write_readback "recovery — FLAC write after outage" "$BETA" "ARTIST" "Recovered Artist"
# EXPECTED FAILURE: watcher should resubscribe quickly after a clean server
# restart. Currently it sleeps a full POLL_FALLBACK_INTERVAL (30s) before
# retrying — no exponential backoff or fast-retry on recoverable errors.
local resub=0 j
for ((j = 0; j < 10; j++)); do
if log_contains_since "subscribed to /events" "$watch_marker"; then
resub=1
break
fi
sleep 1
done
if (( resub == 1 )); then
_res_pass "watcher resubscribed within 10s of server restart"
else
_res_fail "watcher resubscribed within 10s of server restart (stuck in 30s retry — no fast reconnect)"
fi
}
# ── Scenario 2: Network Partition via incus pause → Resume ──
#
# Disruption: incus pause (freezes VM — TCP connections go silent)
# Tests: Silent partition behavior. Uncached reads will hang (no tonic
# timeout/keepalive). After resume, client should recover.
#
# IMPORTANT: The FUSE daemon (fuser) dispatches requests serially on a single
# thread. An uncached read during a silent partition blocks the daemon in
# block_on(fetch_file_range()) indefinitely — queuing ALL subsequent FUSE
# operations. Therefore:
# 1. Do all non-blocking FUSE ops (dir listing) BEFORE the blocking read.
# 2. Resume the VM IMMEDIATELY after the blocking read.
# 3. Only attempt more FUSE ops after the daemon unblocks.
test_network_partition() {
printf '\n\033[1;36m── scenario 2: network partition (incus pause) ──\033[0m\n'
ensure_clean_state
# Baseline
assert_fuse_read_ok "baseline — read Alpha" "$ALPHA"
# ── Disruption: freeze VM ──
vm_pause
assert_health_down "server unreachable during pause"
# Dir listing works (in-memory, no network) — must do this BEFORE the
# blocking read below, which freezes the single-threaded FUSE daemon.
assert_dir_listing "partition — directory listing (pre-block)" "$MNT" "E2E Artist A"
# EXPECTED FAILURE: with proper tonic timeout/keepalive, this read should
# return EIO within ~10s. Instead it HANGS indefinitely because no timeout
# is configured — block_on(fetch_file_range()) never returns while the VM
# is paused. timeout kills dd at 10s, but the daemon thread stays stuck.
assert_fuse_eio_within "partition — uncached read Gamma returns EIO within 10s" "$GAMMA" 10
# ── Recovery: resume VM immediately ──
# After resume, the pending gRPC call completes (server processes the
# in-flight request), unblocking the FUSE daemon thread.
vm_resume
assert_health_up "server recovered after resume"
assert_recovered "recovery — client reads after partition" "$GAMMA"
assert_fuse_read_ok "recovery — cached read Alpha" "$ALPHA"
}
# ── Scenario 3: Cache Resilience During Full Outage ──
#
# Disruption: systemctl stop (server down for entire test)
# Tests: Postgres-backed cache provides degraded read-only mode.
# All pre-warmed files are fully readable. Writes to cached files
# work (local DB path). Writes to uncached files fail gracefully.
test_cache_resilience() {
printf '\n\033[1;36m── scenario 3: cache resilience during outage ──\033[0m\n'
ensure_clean_state
# Pre-warm: read every file so all are in Postgres cache
prewarm_all
# Verify prewarm worked (all files cached)
assert_fuse_metaflac_ok "prewarm — Alpha metaflac read" "$ALPHA"
assert_fuse_id3v2_ok "prewarm — Gamma id3v2 read" "$GAMMA"
# ── Disruption: stop server for entire scenario ──
server_stop
assert_health_down "server down for cache test"
# Cached reads — all should succeed from Postgres cache
assert_fuse_metaflac_ok "outage — cached FLAC metaflac (Alpha)" "$ALPHA"
assert_fuse_metaflac_ok "outage — cached FLAC metaflac (Beta)" "$BETA"
assert_fuse_id3v2_ok "outage — cached MP3 id3v2 (Gamma)" "$GAMMA"
assert_fuse_id3v2_ok "outage — cached MP3 id3v2 (Delta)" "$DELTA"
assert_fuse_ffprobe_ok "outage — cached ffprobe (Alpha)" "$ALPHA"
# Write to cached file — should succeed (write path is local DB only)
assert_metaflac_write_readback "outage — write to cached FLAC (Beta)" "$BETA" "ARTIST" "Cache Outage Artist"
# Directory listing — in-memory, no server needed
assert_dir_listing "outage — dir listing works" "$MNT" "E2E Artist B"
# ── Recovery ──
server_start
assert_health_up "server recovered"
assert_metaflac_write_readback "recovery — write after cache outage" "$BETA" "ARTIST" "Post Outage Artist"
}
# ── Scenario 4: Long Outage + Watcher Recovery ──
#
# Disruption: incus pause for 90s (> 2x POLL_FALLBACK_INTERVAL of 30s)
# Tests: Watcher retry loop stamina during prolonged partition.
# After resume, watcher resubscribes and reconcile succeeds.
test_long_outage_watcher() {
printf '\n\033[1;36m── scenario 4: long outage + watcher recovery ──\033[0m\n'
ensure_clean_state
assert_log_contains "baseline — watcher subscribed" "subscribed to /events"
local marker; marker="$(log_line_count)"
vm_pause
assert_health_down "server unreachable during long pause"
# EXPECTED FAILURE: watcher should detect the dead stream within ~30s
# (POLL_FALLBACK_INTERVAL). Instead it's stuck on stream.next() for ~70s
# until the HTTP/2 transport layer times out — no keepalive is configured
# to detect the silent partition sooner.
local watcher_pattern="stream error\|subscribe failed\|poll reconcile failed"
local detected=0
local i
for ((i = 0; i < 30; i++)); do
check_client_alive || break
if log_contains_since "$watcher_pattern" "$marker"; then
detected=1
break
fi
sleep 1
done
if (( detected == 1 )); then
_res_pass "watcher detected silent partition within 30s"
else
_res_fail "watcher detected silent partition within 30s (stuck on stream.next — no keepalive)"
fi
assert_dir_listing "long outage — dir listing works" "$MNT" "E2E Artist A"
check_client_alive || die "client died during long outage"
vm_resume
assert_health_up "server recovered after long pause"
assert_recovered "client reads after long outage" "$ALPHA"
local resub=0 j
for ((j = 0; j < 10; j++)); do
if log_contains_since "subscribed to /events" "$marker"; then
resub=1
break
fi
sleep 1
done
if (( resub == 1 )); then
_res_pass "watcher resubscribed within 10s of recovery"
else
_res_fail "watcher resubscribed within 10s of recovery (stuck in 30s retry — no fast reconnect)"
fi
}
# ════════════════════════════════════════════════════════════════════════
# MAIN
# ════════════════════════════════════════════════════════════════════════
main() {
[[ -n "${PGHOST:-}" ]] || die "PGHOST not set — run inside devenv shell"
ensure_postgres
command -v incus >/dev/null || die "incus not found"
command -v grpcurl >/dev/null || die "grpcurl not found"
command -v metaflac >/dev/null || die "metaflac not found"
command -v id3v2 >/dev/null || die "id3v2 not found"
command -v ffprobe >/dev/null || die "ffprobe not found"
log "=== Resilience E2E suite start ==="
# Phase 0: fixtures
bash "$SCRIPT_DIR/make-fixtures.sh" "$MUSIC_DIR"
# Phase 1: server
start_vm
# Phase 2: client
build_client
reset_db
start_client
# Phase 3: resilience scenarios
test_server_stop_start
test_network_partition
test_cache_resilience
test_long_outage_watcher
# Summary
printf '\n'
summary
local rv=$?
log "=== Resilience E2E suite done ==="
exit $rv
}
main "$@"
+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 \
+56 -13
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/usr/bin/env -S nix shell nixpkgs#bash --command bash
#
# vm.sh - Incus VM launcher for musicfs-server
#
@@ -30,6 +30,8 @@
# MUSIC_MOUNT where it appears inside the VM (default: /music)
# LISTEN_PORT gRPC port (host:vm forwarded) (default: 50051)
# RUST_LOG tracing filter for the server (default: info)
# READONLY mount music as read-only (0=writable) (default: 1)
# VM_IP static IPv4 for the VM (default: auto-detected from incusbr0, gateway.145)
#
set -euo pipefail
@@ -40,8 +42,15 @@ MUSIC_SOURCE="${MUSIC_SOURCE:-$HOME/Music}"
MUSIC_MOUNT="${MUSIC_MOUNT:-/music}"
LISTEN_PORT="${LISTEN_PORT:-50051}"
RUST_LOG="${RUST_LOG:-info}"
detect_static_ip() {
if [[ -n "${VM_IP:-}" ]]; then echo "$VM_IP"; return; fi
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%.*}.145"
}
# Fixed inside the VM; the bundle's interpreter+rpath point here.
SERVER_DIR="/opt/musicfs"
LOG_DIR="/var/log/musicfs"
BIN_NAME="musicfs-server"
@@ -60,7 +69,12 @@ vm_running() { incus list "$VM_NAME" -f csv -c n,s 2>/dev/null | grep -q "$VM_NA
device_exists() { incus config device show "$VM_NAME" 2>/dev/null | grep -q "^$1:"; }
# incus -c 4 emits "<ip> (<iface>)"; keep only the address.
vm_ip() { incus list "$VM_NAME" -f csv -c 4 2>/dev/null | head -1 | awk '{print $1}'; }
vm_ip() {
local ip
ip="$(incus list "$VM_NAME" -f csv -c 4 2>/dev/null | head -1 | awk '{print $1}')"
if [[ -n "$ip" ]]; then echo "$ip"; return; fi
detect_static_ip
}
# ──────────────────────────── build + bundle ───────────────────────────
# Emits the host path to the prepared bundle dir on stdout.
@@ -120,10 +134,24 @@ create_vm() {
log "creating VM '$VM_NAME' from $IMAGE ..."
incus init "$IMAGE" "$VM_NAME" --vm
local static_ip
static_ip="$(detect_static_ip)"
if [[ -n "$static_ip" ]]; then
log "reserving static IP: $static_ip"
incus config device add "$VM_NAME" eth0 nic \
network=incusbr0 name=eth0 ipv4.address="$static_ip"
fi
if ! device_exists music; then
log "sharing music: $MUSIC_SOURCE -> $MUSIC_MOUNT (readonly, virtiofs)"
local ro_flag="readonly=true"
local mode="read-only"
if [[ "$READONLY" == "0" ]]; then
ro_flag="readonly=false"
mode="read-write"
fi
log "sharing music: $MUSIC_SOURCE -> $MUSIC_MOUNT ($mode, virtiofs)"
incus config device add "$VM_NAME" music disk \
source="$MUSIC_SOURCE" path="$MUSIC_MOUNT" readonly=true
source="$MUSIC_SOURCE" path="$MUSIC_MOUNT" "$ro_flag"
fi
# No proxy: Incus VM proxies need NAT mode + a *static* instance IP (DHCP is
@@ -151,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).
@@ -255,7 +279,15 @@ print_status() {
printf ' ipv4: %s\n' "${ip:-<none>}"
printf ' image: %s\n' "$IMAGE"
printf ' listen: 0.0.0.0:%s (reach via VM bridge IP above)\n' "$LISTEN_PORT"
printf ' music: %s -> %s\n' "$MUSIC_SOURCE" "$MUSIC_MOUNT"
local ro_actual="read-only"
if vm_exists; then
local ro_val
ro_val="$(incus config device get "$VM_NAME" music readonly 2>/dev/null || true)"
[[ "$ro_val" == "false" ]] && ro_actual="read-write"
else
[[ "$READONLY" == "0" ]] && ro_actual="read-write"
fi
printf ' music: %s -> %s (%s)\n' "$MUSIC_SOURCE" "$MUSIC_MOUNT" "$ro_actual"
printf ' server: %s/server\n' "$SERVER_DIR"
if [ -n "$ip" ]; then
printf '\n test: grpcurl -plaintext %s:%s list\n\n' "$ip" "$LISTEN_PORT"
@@ -273,7 +305,18 @@ usage() {
}
main() {
case "${1:-up}" in
local cmd="${1:-up}"
shift || true
while (( $# )); do
case "$1" in
--writable) [[ -z "${READONLY:-}" ]] && READONLY=0 ;;
--readonly) [[ -z "${READONLY:-}" ]] && READONLY=1 ;;
*) die "unknown flag '$1'. Try: $0 --help" ;;
esac
shift
done
READONLY="${READONLY:-1}"
case "$cmd" in
up) cmd_up ;;
redeploy) cmd_redeploy ;;
restart) cmd_restart ;;
@@ -283,7 +326,7 @@ main() {
down) cmd_down ;;
destroy) cmd_destroy ;;
-h|--help|help) usage ;;
*) die "unknown command '$1'. Try: $0 --help" ;;
*) die "unknown command '$cmd'. Try: $0 --help" ;;
esac
}
-8
View File
@@ -1,8 +0,0 @@
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;
-153
View File
@@ -1,153 +0,0 @@
use symphonia::core::meta::{MetadataRevision, StandardTagKey};
#[derive(Debug, Default, Clone)]
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);
}
}
-459
View File
@@ -1,459 +0,0 @@
// @generated
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ReconcileRequest {
#[prost(message, repeated, tag = "1")]
pub entries: ::prost::alloc::vec::Vec<InodeHash>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ReconcileResponse {
#[prost(message, repeated, tag = "1")]
pub changed: ::prost::alloc::vec::Vec<InodeHash>,
#[prost(uint64, repeated, tag = "2")]
pub deleted: ::prost::alloc::vec::Vec<u64>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct InodeHash {
#[prost(uint64, tag = "1")]
pub inode: u64,
#[prost(uint64, tag = "2")]
pub hash: u64,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetMetadataRequest {
#[prost(uint64, repeated, tag = "1")]
pub inodes: ::prost::alloc::vec::Vec<u64>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetManifestRequest {}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct GetFileRequest {
#[prost(uint64, tag = "1")]
pub id: u64,
/// When both are zero the server streams the entire file.
#[prost(uint64, tag = "2")]
pub start: u64,
#[prost(uint64, tag = "3")]
pub length: u64,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct SubscribeEventsRequest {}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ManifestEntry {
#[prost(uint64, tag = "1")]
pub id: u64,
#[prost(string, tag = "2")]
pub rel_path: ::prost::alloc::string::String,
#[prost(uint64, tag = "3")]
pub size: u64,
#[prost(uint64, tag = "4")]
pub mtime: u64,
#[prost(uint64, tag = "5")]
pub ctime: u64,
#[prost(uint64, tag = "6")]
pub crtime: u64,
#[prost(message, optional, tag = "7")]
pub music_metadata: ::core::option::Option<MusicMetadata>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct MusicMetadata {
#[prost(string, repeated, tag = "1")]
pub artist: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
#[prost(string, optional, tag = "2")]
pub album_artist: ::core::option::Option<::prost::alloc::string::String>,
#[prost(string, tag = "3")]
pub album: ::prost::alloc::string::String,
#[prost(int32, tag = "4")]
pub track_number: i32,
#[prost(string, tag = "5")]
pub track_title: ::prost::alloc::string::String,
#[prost(string, repeated, tag = "6")]
pub other_tags: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
#[prost(bytes = "vec", tag = "7")]
pub header: ::prost::alloc::vec::Vec<u8>,
#[prost(bytes = "vec", repeated, tag = "8")]
pub picture_block_headers: ::prost::alloc::vec::Vec<::prost::alloc::vec::Vec<u8>>,
#[prost(message, repeated, tag = "9")]
pub picture_data_ranges: ::prost::alloc::vec::Vec<PictureDataRange>,
#[prost(uint64, tag = "10")]
pub real_audio_start: u64,
#[prost(uint64, tag = "11")]
pub vorbis_comment_offset: u64,
#[prost(uint64, tag = "12")]
pub vorbis_comment_length: u64,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct PictureDataRange {
#[prost(uint64, tag = "1")]
pub offset: u64,
#[prost(uint64, tag = "2")]
pub length: u64,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct FileChunk {
#[prost(bytes = "vec", tag = "1")]
pub data: ::prost::alloc::vec::Vec<u8>,
#[prost(uint64, tag = "2")]
pub offset: u64,
#[prost(uint64, tag = "3")]
pub total_size: u64,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ChangeEvent {
/// "create", "modify", or "remove".
#[prost(string, tag = "1")]
pub kind: ::prost::alloc::string::String,
}
/// Encoded file descriptor set for the `musicfs` package
pub const FILE_DESCRIPTOR_SET: &[u8] = &[
0x0a, 0xb0, 0x2b, 0x0a, 0x0d, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x12, 0x07, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x22, 0x40, 0x0a, 0x10, 0x52,
0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x2c, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x12, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x49, 0x6e, 0x6f, 0x64, 0x65,
0x48, 0x61, 0x73, 0x68, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0x5b, 0x0a,
0x11, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x2c, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x18, 0x01, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x49, 0x6e,
0x6f, 0x64, 0x65, 0x48, 0x61, 0x73, 0x68, 0x52, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64,
0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x03, 0x28,
0x04, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x35, 0x0a, 0x09, 0x49, 0x6e,
0x6f, 0x64, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x6f, 0x64, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x69, 0x6e, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a,
0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x68, 0x61, 0x73,
0x68, 0x22, 0x2c, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x6e, 0x6f, 0x64, 0x65,
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x52, 0x06, 0x69, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x22,
0x14, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4e, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x46, 0x69, 0x6c, 0x65,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74,
0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x16, 0x0a,
0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6c,
0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0x18, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69,
0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22,
0xd1, 0x01, 0x0a, 0x0d, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x72,
0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69,
0x64, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x50, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04,
0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65,
0x12, 0x14, 0x0a, 0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52,
0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x18,
0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06,
0x63, 0x72, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x63, 0x72,
0x74, 0x69, 0x6d, 0x65, 0x12, 0x3d, 0x0a, 0x0e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x5f, 0x6d, 0x65,
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d,
0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x4d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x52, 0x0d, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x4d, 0x65, 0x74, 0x61, 0x64,
0x61, 0x74, 0x61, 0x22, 0x82, 0x04, 0x0a, 0x0d, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x4d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x18,
0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x12, 0x26, 0x0a,
0x0c, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x41, 0x72, 0x74, 0x69,
0x73, 0x74, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x18, 0x03,
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x6c, 0x62, 0x75, 0x6d, 0x12, 0x21, 0x0a, 0x0c, 0x74,
0x72, 0x61, 0x63, 0x6b, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28,
0x05, 0x52, 0x0b, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x1f,
0x0a, 0x0b, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x05, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x54, 0x69, 0x74, 0x6c, 0x65, 0x12,
0x1d, 0x0a, 0x0a, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x06, 0x20,
0x03, 0x28, 0x09, 0x52, 0x09, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x54, 0x61, 0x67, 0x73, 0x12, 0x16,
0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06,
0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72,
0x65, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18,
0x08, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x13, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x42, 0x6c,
0x6f, 0x63, 0x6b, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x49, 0x0a, 0x13, 0x70, 0x69,
0x63, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65,
0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66,
0x73, 0x2e, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x61, 0x6e,
0x67, 0x65, 0x52, 0x11, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52,
0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x72, 0x65, 0x61, 0x6c, 0x5f, 0x61, 0x75,
0x64, 0x69, 0x6f, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52,
0x0e, 0x72, 0x65, 0x61, 0x6c, 0x41, 0x75, 0x64, 0x69, 0x6f, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12,
0x32, 0x0a, 0x15, 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e,
0x74, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x13,
0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x4f, 0x66, 0x66,
0x73, 0x65, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x5f, 0x63, 0x6f,
0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x0c, 0x20, 0x01,
0x28, 0x04, 0x52, 0x13, 0x76, 0x6f, 0x72, 0x62, 0x69, 0x73, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e,
0x74, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x61, 0x6c, 0x62, 0x75,
0x6d, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x73, 0x74, 0x22, 0x42, 0x0a, 0x10, 0x50, 0x69, 0x63, 0x74,
0x75, 0x72, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06,
0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f, 0x66,
0x66, 0x73, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x02,
0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0x56, 0x0a, 0x09,
0x46, 0x69, 0x6c, 0x65, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74,
0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a,
0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6f,
0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x73,
0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c,
0x53, 0x69, 0x7a, 0x65, 0x22, 0x21, 0x0a, 0x0b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x45, 0x76,
0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x32, 0xdf, 0x02, 0x0a, 0x07, 0x4d, 0x75, 0x73, 0x69,
0x63, 0x46, 0x73, 0x12, 0x42, 0x0a, 0x09, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65,
0x12, 0x19, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6e,
0x63, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x6d, 0x75,
0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x65,
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73,
0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x4d, 0x61,
0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x30, 0x01, 0x12, 0x44, 0x0a,
0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x2e, 0x6d,
0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65,
0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6d, 0x75, 0x73, 0x69,
0x63, 0x66, 0x73, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x72,
0x79, 0x30, 0x01, 0x12, 0x38, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x17,
0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x69, 0x6c, 0x65,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66,
0x73, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x30, 0x01, 0x12, 0x4a, 0x0a,
0x0f, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73,
0x12, 0x1f, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63,
0x72, 0x69, 0x62, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x14, 0x2e, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x2e, 0x43, 0x68, 0x61, 0x6e,
0x67, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x4a, 0x8f, 0x1e, 0x0a, 0x06, 0x12, 0x04,
0x00, 0x00, 0x66, 0x01, 0x0a, 0x08, 0x0a, 0x01, 0x0c, 0x12, 0x03, 0x00, 0x00, 0x12, 0x0a, 0x08,
0x0a, 0x01, 0x02, 0x12, 0x03, 0x02, 0x00, 0x10, 0x0a, 0x62, 0x0a, 0x02, 0x06, 0x00, 0x12, 0x04,
0x06, 0x00, 0x20, 0x01, 0x1a, 0x56, 0x20, 0x4d, 0x75, 0x73, 0x69, 0x63, 0x46, 0x73, 0x20, 0x73,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x73, 0x20, 0x61,
0x20, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2d, 0x73, 0x69, 0x64, 0x65, 0x20, 0x6d, 0x75, 0x73,
0x69, 0x63, 0x20, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x20, 0x74, 0x6f, 0x20, 0x61, 0x20,
0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x69, 0x63, 0x66, 0x73, 0x0a, 0x20,
0x46, 0x55, 0x53, 0x45, 0x20, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x0a, 0x0a, 0x0a, 0x0a, 0x03,
0x06, 0x00, 0x01, 0x12, 0x03, 0x06, 0x08, 0x0f, 0x0a, 0xac, 0x02, 0x0a, 0x04, 0x06, 0x00, 0x02,
0x00, 0x12, 0x03, 0x0b, 0x02, 0x3e, 0x1a, 0x9e, 0x02, 0x20, 0x48, 0x61, 0x73, 0x68, 0x2d, 0x62,
0x61, 0x73, 0x65, 0x64, 0x20, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x69, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x2e, 0x20, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x73, 0x65, 0x6e, 0x64,
0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x28, 0x69, 0x6e, 0x6f, 0x64, 0x65, 0x2c, 0x20, 0x68, 0x61,
0x73, 0x68, 0x29, 0x20, 0x70, 0x61, 0x69, 0x72, 0x73, 0x20, 0x69, 0x74, 0x0a, 0x20, 0x63, 0x75,
0x72, 0x72, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x20, 0x68, 0x61, 0x73, 0x3b, 0x20, 0x73, 0x65, 0x72,
0x76, 0x65, 0x72, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x64, 0x73, 0x20, 0x77, 0x69, 0x74,
0x68, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x75, 0x62, 0x73, 0x65, 0x74, 0x20, 0x74, 0x68, 0x61,
0x74, 0x20, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x20, 0x28, 0x6e, 0x65, 0x77, 0x20, 0x6f,
0x72, 0x0a, 0x20, 0x68, 0x61, 0x73, 0x68, 0x2d, 0x64, 0x69, 0x66, 0x66, 0x65, 0x72, 0x65, 0x6e,
0x74, 0x29, 0x20, 0x70, 0x6c, 0x75, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x69, 0x6e, 0x6f, 0x64,
0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x68, 0x61,
0x73, 0x20, 0x62, 0x75, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
0x20, 0x6e, 0x6f, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x65, 0x72, 0x0a, 0x20, 0x68, 0x61, 0x73, 0x2e,
0x20, 0x43, 0x68, 0x65, 0x61, 0x70, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x77, 0x69,
0x72, 0x65, 0x20, 0xe2, 0x80, 0x94, 0x20, 0x6e, 0x6f, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x20, 0x6d,
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x20, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x65, 0x73, 0x20,
0x75, 0x6e, 0x74, 0x69, 0x6c, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74,
0x20, 0x61, 0x73, 0x6b, 0x73, 0x2e, 0x0a, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x00, 0x01,
0x12, 0x03, 0x0b, 0x06, 0x0f, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x00, 0x02, 0x12, 0x03,
0x0b, 0x10, 0x20, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x00, 0x03, 0x12, 0x03, 0x0b, 0x2b,
0x3c, 0x0a, 0x94, 0x01, 0x0a, 0x04, 0x06, 0x00, 0x02, 0x01, 0x12, 0x03, 0x0f, 0x02, 0x45, 0x1a,
0x86, 0x01, 0x20, 0x46, 0x65, 0x74, 0x63, 0x68, 0x20, 0x66, 0x75, 0x6c, 0x6c, 0x20, 0x6d, 0x65,
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x61, 0x20, 0x73, 0x70, 0x65,
0x63, 0x69, 0x66, 0x69, 0x63, 0x20, 0x73, 0x65, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x69, 0x6e, 0x6f,
0x64, 0x65, 0x73, 0x2e, 0x20, 0x43, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x20, 0x61, 0x66, 0x74, 0x65,
0x72, 0x20, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x0a, 0x20, 0x72, 0x65, 0x74,
0x75, 0x72, 0x6e, 0x73, 0x20, 0x61, 0x20, 0x6e, 0x6f, 0x6e, 0x2d, 0x65, 0x6d, 0x70, 0x74, 0x79,
0x20, 0x60, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x60, 0x20, 0x6c, 0x69, 0x73, 0x74, 0x2c,
0x20, 0x66, 0x6f, 0x72, 0x20, 0x6a, 0x75, 0x73, 0x74, 0x20, 0x74, 0x68, 0x6f, 0x73, 0x65, 0x20,
0x69, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x2e, 0x0a, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x01,
0x01, 0x12, 0x03, 0x0f, 0x06, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x01, 0x02, 0x12,
0x03, 0x0f, 0x12, 0x24, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x01, 0x06, 0x12, 0x03, 0x0f,
0x2f, 0x35, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x01, 0x03, 0x12, 0x03, 0x0f, 0x36, 0x43,
0x0a, 0xa8, 0x01, 0x0a, 0x04, 0x06, 0x00, 0x02, 0x02, 0x12, 0x03, 0x14, 0x02, 0x45, 0x1a, 0x9a,
0x01, 0x20, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x20, 0x65, 0x76, 0x65, 0x72, 0x79, 0x20, 0x66,
0x69, 0x6c, 0x65, 0x20, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x20, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65,
0x20, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x2e, 0x20, 0x43, 0x6f, 0x6e, 0x76, 0x65, 0x6e,
0x69, 0x65, 0x6e, 0x63, 0x65, 0x20, 0x52, 0x50, 0x43, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x66, 0x69,
0x72, 0x73, 0x74, 0x2d, 0x72, 0x75, 0x6e, 0x0a, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72,
0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x20, 0x6f, 0x72, 0x20, 0x66, 0x75, 0x6c, 0x6c, 0x2d, 0x72,
0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x64, 0x69, 0x66, 0x66,
0x2d, 0x62, 0x61, 0x73, 0x65, 0x64, 0x20, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65,
0x20, 0x70, 0x61, 0x74, 0x68, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, 0x65, 0x0a, 0x20, 0x6e, 0x6f,
0x72, 0x6d, 0x61, 0x6c, 0x20, 0x63, 0x61, 0x73, 0x65, 0x2e, 0x0a, 0x0a, 0x0c, 0x0a, 0x05, 0x06,
0x00, 0x02, 0x02, 0x01, 0x12, 0x03, 0x14, 0x06, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02,
0x02, 0x02, 0x12, 0x03, 0x14, 0x12, 0x24, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x02, 0x06,
0x12, 0x03, 0x14, 0x2f, 0x35, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x02, 0x03, 0x12, 0x03,
0x14, 0x36, 0x43, 0x0a, 0xf8, 0x01, 0x0a, 0x04, 0x06, 0x00, 0x02, 0x03, 0x12, 0x03, 0x1a, 0x02,
0x39, 0x1a, 0xea, 0x01, 0x20, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x20,
0x62, 0x79, 0x74, 0x65, 0x73, 0x20, 0x6f, 0x66, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x66, 0x69, 0x6c,
0x65, 0x2e, 0x20, 0x49, 0x66, 0x20, 0x60, 0x73, 0x74, 0x61, 0x72, 0x74, 0x60, 0x20, 0x61, 0x6e,
0x64, 0x20, 0x60, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x60, 0x20, 0x61, 0x72, 0x65, 0x20, 0x62,
0x6f, 0x74, 0x68, 0x20, 0x7a, 0x65, 0x72, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x0a, 0x20, 0x73, 0x65,
0x72, 0x76, 0x65, 0x72, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x20, 0x74, 0x68, 0x65,
0x20, 0x65, 0x6e, 0x74, 0x69, 0x72, 0x65, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x3b, 0x20, 0x6f, 0x74,
0x68, 0x65, 0x72, 0x77, 0x69, 0x73, 0x65, 0x20, 0x6a, 0x75, 0x73, 0x74, 0x20, 0x74, 0x68, 0x65,
0x20, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x20, 0x72, 0x61, 0x6e, 0x67, 0x65,
0x2e, 0x20, 0x54, 0x68, 0x65, 0x0a, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x63, 0x68, 0x75,
0x6e, 0x6b, 0x20, 0x63, 0x61, 0x72, 0x72, 0x69, 0x65, 0x73, 0x20, 0x60, 0x74, 0x6f, 0x74, 0x61,
0x6c, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x60, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x60, 0x6f, 0x66, 0x66,
0x73, 0x65, 0x74, 0x60, 0x3b, 0x20, 0x73, 0x75, 0x62, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x74,
0x20, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x73, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x65,
0x0a, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x72, 0x65, 0x2e, 0x0a, 0x0a, 0x0c,
0x0a, 0x05, 0x06, 0x00, 0x02, 0x03, 0x01, 0x12, 0x03, 0x1a, 0x06, 0x0d, 0x0a, 0x0c, 0x0a, 0x05,
0x06, 0x00, 0x02, 0x03, 0x02, 0x12, 0x03, 0x1a, 0x0e, 0x1c, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00,
0x02, 0x03, 0x06, 0x12, 0x03, 0x1a, 0x27, 0x2d, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x03,
0x03, 0x12, 0x03, 0x1a, 0x2e, 0x37, 0x0a, 0xc2, 0x01, 0x0a, 0x04, 0x06, 0x00, 0x02, 0x04, 0x12,
0x03, 0x1f, 0x02, 0x4b, 0x1a, 0xb4, 0x01, 0x20, 0x4c, 0x6f, 0x6e, 0x67, 0x2d, 0x6c, 0x69, 0x76,
0x65, 0x64, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x20, 0x6f, 0x66, 0x20, 0x66, 0x69, 0x6c,
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x20, 0x65,
0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, 0x65, 0x6e,
0x74, 0x20, 0x75, 0x73, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x73, 0x65, 0x20, 0x61, 0x73, 0x0a,
0x20, 0x77, 0x61, 0x6b, 0x65, 0x2d, 0x75, 0x70, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x72, 0x65, 0x66,
0x65, 0x74, 0x63, 0x68, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73,
0x74, 0x3b, 0x20, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x63, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x20, 0x61,
0x6c, 0x77, 0x61, 0x79, 0x73, 0x20, 0x72, 0x65, 0x73, 0x74, 0x73, 0x20, 0x6f, 0x6e, 0x20, 0x74,
0x68, 0x65, 0x20, 0x68, 0x61, 0x73, 0x68, 0x0a, 0x20, 0x64, 0x69, 0x66, 0x66, 0x2c, 0x20, 0x6e,
0x65, 0x76, 0x65, 0x72, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x76, 0x65, 0x6e,
0x74, 0x20, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x0a, 0x0a, 0x0c, 0x0a, 0x05, 0x06,
0x00, 0x02, 0x04, 0x01, 0x12, 0x03, 0x1f, 0x06, 0x15, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02,
0x04, 0x02, 0x12, 0x03, 0x1f, 0x16, 0x2c, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x04, 0x06,
0x12, 0x03, 0x1f, 0x37, 0x3d, 0x0a, 0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x04, 0x03, 0x12, 0x03,
0x1f, 0x3e, 0x49, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x00, 0x12, 0x04, 0x22, 0x00, 0x24, 0x01, 0x0a,
0x0a, 0x0a, 0x03, 0x04, 0x00, 0x01, 0x12, 0x03, 0x22, 0x08, 0x18, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x00, 0x02, 0x00, 0x04, 0x12, 0x03, 0x23, 0x02, 0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x00, 0x02,
0x00, 0x12, 0x03, 0x23, 0x02, 0x21, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x00, 0x06, 0x12,
0x03, 0x23, 0x0b, 0x14, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x00, 0x01, 0x12, 0x03, 0x23,
0x15, 0x1c, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x00, 0x03, 0x12, 0x03, 0x23, 0x1f, 0x20,
0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x01, 0x12, 0x04, 0x26, 0x00, 0x29, 0x01, 0x0a, 0x0a, 0x0a, 0x03,
0x04, 0x01, 0x01, 0x12, 0x03, 0x26, 0x08, 0x19, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02, 0x00,
0x04, 0x12, 0x03, 0x27, 0x02, 0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x01, 0x02, 0x00, 0x12, 0x03,
0x27, 0x02, 0x21, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02, 0x00, 0x06, 0x12, 0x03, 0x27, 0x0b,
0x14, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02, 0x00, 0x01, 0x12, 0x03, 0x27, 0x15, 0x1c, 0x0a,
0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02, 0x00, 0x03, 0x12, 0x03, 0x27, 0x1f, 0x20, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x01, 0x02, 0x01, 0x04, 0x12, 0x03, 0x28, 0x02, 0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x04,
0x01, 0x02, 0x01, 0x12, 0x03, 0x28, 0x02, 0x1e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02, 0x01,
0x05, 0x12, 0x03, 0x28, 0x0b, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02, 0x01, 0x01, 0x12,
0x03, 0x28, 0x12, 0x19, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02, 0x01, 0x03, 0x12, 0x03, 0x28,
0x1c, 0x1d, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x02, 0x12, 0x04, 0x2b, 0x00, 0x2e, 0x01, 0x0a, 0x0a,
0x0a, 0x03, 0x04, 0x02, 0x01, 0x12, 0x03, 0x2b, 0x08, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x02,
0x02, 0x00, 0x05, 0x12, 0x03, 0x2c, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x02, 0x02, 0x00,
0x12, 0x03, 0x2c, 0x02, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x02, 0x02, 0x00, 0x01, 0x12, 0x03,
0x2c, 0x09, 0x0e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x02, 0x02, 0x00, 0x03, 0x12, 0x03, 0x2c, 0x11,
0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x02, 0x02, 0x01, 0x05, 0x12, 0x03, 0x2d, 0x02, 0x08, 0x0a,
0x0b, 0x0a, 0x04, 0x04, 0x02, 0x02, 0x01, 0x12, 0x03, 0x2d, 0x02, 0x12, 0x0a, 0x0c, 0x0a, 0x05,
0x04, 0x02, 0x02, 0x01, 0x01, 0x12, 0x03, 0x2d, 0x09, 0x0d, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x02,
0x02, 0x01, 0x03, 0x12, 0x03, 0x2d, 0x10, 0x11, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x03, 0x12, 0x04,
0x30, 0x00, 0x32, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x03, 0x01, 0x12, 0x03, 0x30, 0x08, 0x1a,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x03, 0x02, 0x00, 0x04, 0x12, 0x03, 0x31, 0x02, 0x0a, 0x0a, 0x0b,
0x0a, 0x04, 0x04, 0x03, 0x02, 0x00, 0x12, 0x03, 0x31, 0x02, 0x1d, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x03, 0x02, 0x00, 0x05, 0x12, 0x03, 0x31, 0x0b, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x03, 0x02,
0x00, 0x01, 0x12, 0x03, 0x31, 0x12, 0x18, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x03, 0x02, 0x00, 0x03,
0x12, 0x03, 0x31, 0x1b, 0x1c, 0x0a, 0x09, 0x0a, 0x02, 0x04, 0x04, 0x12, 0x03, 0x34, 0x00, 0x1d,
0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x04, 0x01, 0x12, 0x03, 0x34, 0x08, 0x1a, 0x0a, 0x0a, 0x0a, 0x02,
0x04, 0x05, 0x12, 0x04, 0x36, 0x00, 0x3b, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x05, 0x01, 0x12,
0x03, 0x36, 0x08, 0x16, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x00, 0x05, 0x12, 0x03, 0x37,
0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x05, 0x02, 0x00, 0x12, 0x03, 0x37, 0x02, 0x10, 0x0a,
0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x00, 0x01, 0x12, 0x03, 0x37, 0x09, 0x0b, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x05, 0x02, 0x00, 0x03, 0x12, 0x03, 0x37, 0x0e, 0x0f, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x05, 0x02, 0x01, 0x05, 0x12, 0x03, 0x39, 0x02, 0x08, 0x0a, 0x45, 0x0a, 0x04, 0x04, 0x05, 0x02,
0x01, 0x12, 0x03, 0x39, 0x02, 0x13, 0x1a, 0x38, 0x20, 0x57, 0x68, 0x65, 0x6e, 0x20, 0x62, 0x6f,
0x74, 0x68, 0x20, 0x61, 0x72, 0x65, 0x20, 0x7a, 0x65, 0x72, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20,
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x20, 0x74,
0x68, 0x65, 0x20, 0x65, 0x6e, 0x74, 0x69, 0x72, 0x65, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x0a,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x01, 0x01, 0x12, 0x03, 0x39, 0x09, 0x0e, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x05, 0x02, 0x01, 0x03, 0x12, 0x03, 0x39, 0x11, 0x12, 0x0a, 0x0c, 0x0a, 0x05,
0x04, 0x05, 0x02, 0x02, 0x05, 0x12, 0x03, 0x3a, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x05,
0x02, 0x02, 0x12, 0x03, 0x3a, 0x02, 0x14, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x02, 0x01,
0x12, 0x03, 0x3a, 0x09, 0x0f, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x05, 0x02, 0x02, 0x03, 0x12, 0x03,
0x3a, 0x12, 0x13, 0x0a, 0x09, 0x0a, 0x02, 0x04, 0x06, 0x12, 0x03, 0x3d, 0x00, 0x21, 0x0a, 0x0a,
0x0a, 0x03, 0x04, 0x06, 0x01, 0x12, 0x03, 0x3d, 0x08, 0x1e, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x07,
0x12, 0x04, 0x3f, 0x00, 0x47, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x07, 0x01, 0x12, 0x03, 0x3f,
0x08, 0x15, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x00, 0x05, 0x12, 0x03, 0x40, 0x02, 0x08,
0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x07, 0x02, 0x00, 0x12, 0x03, 0x40, 0x02, 0x10, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x07, 0x02, 0x00, 0x01, 0x12, 0x03, 0x40, 0x09, 0x0b, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x07, 0x02, 0x00, 0x03, 0x12, 0x03, 0x40, 0x0e, 0x0f, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02,
0x01, 0x05, 0x12, 0x03, 0x41, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x07, 0x02, 0x01, 0x12,
0x03, 0x41, 0x02, 0x16, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x01, 0x01, 0x12, 0x03, 0x41,
0x09, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x01, 0x03, 0x12, 0x03, 0x41, 0x14, 0x15,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x02, 0x05, 0x12, 0x03, 0x42, 0x02, 0x08, 0x0a, 0x0b,
0x0a, 0x04, 0x04, 0x07, 0x02, 0x02, 0x12, 0x03, 0x42, 0x02, 0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x07, 0x02, 0x02, 0x01, 0x12, 0x03, 0x42, 0x09, 0x0d, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02,
0x02, 0x03, 0x12, 0x03, 0x42, 0x10, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x03, 0x05,
0x12, 0x03, 0x43, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x07, 0x02, 0x03, 0x12, 0x03, 0x43,
0x02, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x03, 0x01, 0x12, 0x03, 0x43, 0x09, 0x0e,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x03, 0x03, 0x12, 0x03, 0x43, 0x11, 0x12, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x07, 0x02, 0x04, 0x05, 0x12, 0x03, 0x44, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04,
0x04, 0x07, 0x02, 0x04, 0x12, 0x03, 0x44, 0x02, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02,
0x04, 0x01, 0x12, 0x03, 0x44, 0x09, 0x0e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x04, 0x03,
0x12, 0x03, 0x44, 0x11, 0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x05, 0x05, 0x12, 0x03,
0x45, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x07, 0x02, 0x05, 0x12, 0x03, 0x45, 0x02, 0x14,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x05, 0x01, 0x12, 0x03, 0x45, 0x09, 0x0f, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x07, 0x02, 0x05, 0x03, 0x12, 0x03, 0x45, 0x12, 0x13, 0x0a, 0x0c, 0x0a, 0x05,
0x04, 0x07, 0x02, 0x06, 0x06, 0x12, 0x03, 0x46, 0x02, 0x0f, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x07,
0x02, 0x06, 0x12, 0x03, 0x46, 0x02, 0x23, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x06, 0x01,
0x12, 0x03, 0x46, 0x10, 0x1e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x07, 0x02, 0x06, 0x03, 0x12, 0x03,
0x46, 0x21, 0x22, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x08, 0x12, 0x04, 0x49, 0x00, 0x56, 0x01, 0x0a,
0x0a, 0x0a, 0x03, 0x04, 0x08, 0x01, 0x12, 0x03, 0x49, 0x08, 0x15, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x08, 0x02, 0x00, 0x04, 0x12, 0x03, 0x4a, 0x02, 0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08, 0x02,
0x00, 0x12, 0x03, 0x4a, 0x02, 0x1d, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x00, 0x05, 0x12,
0x03, 0x4a, 0x0b, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x00, 0x01, 0x12, 0x03, 0x4a,
0x12, 0x18, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x00, 0x03, 0x12, 0x03, 0x4a, 0x1b, 0x1c,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x01, 0x04, 0x12, 0x03, 0x4b, 0x02, 0x0a, 0x0a, 0x0b,
0x0a, 0x04, 0x04, 0x08, 0x02, 0x01, 0x12, 0x03, 0x4b, 0x02, 0x23, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x08, 0x02, 0x01, 0x05, 0x12, 0x03, 0x4b, 0x0b, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02,
0x01, 0x01, 0x12, 0x03, 0x4b, 0x12, 0x1e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x01, 0x03,
0x12, 0x03, 0x4b, 0x21, 0x22, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x02, 0x05, 0x12, 0x03,
0x4c, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08, 0x02, 0x02, 0x12, 0x03, 0x4c, 0x02, 0x13,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x02, 0x01, 0x12, 0x03, 0x4c, 0x09, 0x0e, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x08, 0x02, 0x02, 0x03, 0x12, 0x03, 0x4c, 0x11, 0x12, 0x0a, 0x0c, 0x0a, 0x05,
0x04, 0x08, 0x02, 0x03, 0x05, 0x12, 0x03, 0x4d, 0x02, 0x07, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08,
0x02, 0x03, 0x12, 0x03, 0x4d, 0x02, 0x19, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x03, 0x01,
0x12, 0x03, 0x4d, 0x08, 0x14, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x03, 0x03, 0x12, 0x03,
0x4d, 0x17, 0x18, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x04, 0x05, 0x12, 0x03, 0x4e, 0x02,
0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08, 0x02, 0x04, 0x12, 0x03, 0x4e, 0x02, 0x19, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x08, 0x02, 0x04, 0x01, 0x12, 0x03, 0x4e, 0x09, 0x14, 0x0a, 0x0c, 0x0a, 0x05,
0x04, 0x08, 0x02, 0x04, 0x03, 0x12, 0x03, 0x4e, 0x17, 0x18, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08,
0x02, 0x05, 0x04, 0x12, 0x03, 0x4f, 0x02, 0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08, 0x02, 0x05,
0x12, 0x03, 0x4f, 0x02, 0x21, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x05, 0x05, 0x12, 0x03,
0x4f, 0x0b, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x05, 0x01, 0x12, 0x03, 0x4f, 0x12,
0x1c, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x05, 0x03, 0x12, 0x03, 0x4f, 0x1f, 0x20, 0x0a,
0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x06, 0x05, 0x12, 0x03, 0x50, 0x02, 0x07, 0x0a, 0x0b, 0x0a,
0x04, 0x04, 0x08, 0x02, 0x06, 0x12, 0x03, 0x50, 0x02, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08,
0x02, 0x06, 0x01, 0x12, 0x03, 0x50, 0x08, 0x0e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x06,
0x03, 0x12, 0x03, 0x50, 0x11, 0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x07, 0x04, 0x12,
0x03, 0x51, 0x02, 0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08, 0x02, 0x07, 0x12, 0x03, 0x51, 0x02,
0x2b, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x07, 0x05, 0x12, 0x03, 0x51, 0x0b, 0x10, 0x0a,
0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x07, 0x01, 0x12, 0x03, 0x51, 0x11, 0x26, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x08, 0x02, 0x07, 0x03, 0x12, 0x03, 0x51, 0x29, 0x2a, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x08, 0x02, 0x08, 0x04, 0x12, 0x03, 0x52, 0x02, 0x0a, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08, 0x02,
0x08, 0x12, 0x03, 0x52, 0x02, 0x34, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x08, 0x06, 0x12,
0x03, 0x52, 0x0b, 0x1b, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x08, 0x01, 0x12, 0x03, 0x52,
0x1c, 0x2f, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x08, 0x03, 0x12, 0x03, 0x52, 0x32, 0x33,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x09, 0x05, 0x12, 0x03, 0x53, 0x02, 0x08, 0x0a, 0x0b,
0x0a, 0x04, 0x04, 0x08, 0x02, 0x09, 0x12, 0x03, 0x53, 0x02, 0x1f, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
0x08, 0x02, 0x09, 0x01, 0x12, 0x03, 0x53, 0x09, 0x19, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02,
0x09, 0x03, 0x12, 0x03, 0x53, 0x1c, 0x1e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x0a, 0x05,
0x12, 0x03, 0x54, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x08, 0x02, 0x0a, 0x12, 0x03, 0x54,
0x02, 0x24, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x0a, 0x01, 0x12, 0x03, 0x54, 0x09, 0x1e,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x0a, 0x03, 0x12, 0x03, 0x54, 0x21, 0x23, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x08, 0x02, 0x0b, 0x05, 0x12, 0x03, 0x55, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04,
0x04, 0x08, 0x02, 0x0b, 0x12, 0x03, 0x55, 0x02, 0x24, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02,
0x0b, 0x01, 0x12, 0x03, 0x55, 0x09, 0x1e, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x08, 0x02, 0x0b, 0x03,
0x12, 0x03, 0x55, 0x21, 0x23, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x09, 0x12, 0x04, 0x58, 0x00, 0x5b,
0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x09, 0x01, 0x12, 0x03, 0x58, 0x08, 0x18, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x09, 0x02, 0x00, 0x05, 0x12, 0x03, 0x59, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04,
0x09, 0x02, 0x00, 0x12, 0x03, 0x59, 0x02, 0x14, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x09, 0x02, 0x00,
0x01, 0x12, 0x03, 0x59, 0x09, 0x0f, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x09, 0x02, 0x00, 0x03, 0x12,
0x03, 0x59, 0x12, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x09, 0x02, 0x01, 0x05, 0x12, 0x03, 0x5a,
0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x09, 0x02, 0x01, 0x12, 0x03, 0x5a, 0x02, 0x14, 0x0a,
0x0c, 0x0a, 0x05, 0x04, 0x09, 0x02, 0x01, 0x01, 0x12, 0x03, 0x5a, 0x09, 0x0f, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x09, 0x02, 0x01, 0x03, 0x12, 0x03, 0x5a, 0x12, 0x13, 0x0a, 0x0a, 0x0a, 0x02, 0x04,
0x0a, 0x12, 0x04, 0x5d, 0x00, 0x61, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x0a, 0x01, 0x12, 0x03,
0x5d, 0x08, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0a, 0x02, 0x00, 0x05, 0x12, 0x03, 0x5e, 0x02,
0x07, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x0a, 0x02, 0x00, 0x12, 0x03, 0x5e, 0x02, 0x11, 0x0a, 0x0c,
0x0a, 0x05, 0x04, 0x0a, 0x02, 0x00, 0x01, 0x12, 0x03, 0x5e, 0x08, 0x0c, 0x0a, 0x0c, 0x0a, 0x05,
0x04, 0x0a, 0x02, 0x00, 0x03, 0x12, 0x03, 0x5e, 0x0f, 0x10, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0a,
0x02, 0x01, 0x05, 0x12, 0x03, 0x5f, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x0a, 0x02, 0x01,
0x12, 0x03, 0x5f, 0x02, 0x14, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0a, 0x02, 0x01, 0x01, 0x12, 0x03,
0x5f, 0x09, 0x0f, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0a, 0x02, 0x01, 0x03, 0x12, 0x03, 0x5f, 0x12,
0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0a, 0x02, 0x02, 0x05, 0x12, 0x03, 0x60, 0x02, 0x08, 0x0a,
0x0b, 0x0a, 0x04, 0x04, 0x0a, 0x02, 0x02, 0x12, 0x03, 0x60, 0x02, 0x18, 0x0a, 0x0c, 0x0a, 0x05,
0x04, 0x0a, 0x02, 0x02, 0x01, 0x12, 0x03, 0x60, 0x09, 0x13, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0a,
0x02, 0x02, 0x03, 0x12, 0x03, 0x60, 0x16, 0x17, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x0b, 0x12, 0x04,
0x63, 0x00, 0x66, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x0b, 0x01, 0x12, 0x03, 0x63, 0x08, 0x13,
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x0b, 0x02, 0x00, 0x05, 0x12, 0x03, 0x65, 0x02, 0x08, 0x0a, 0x2f,
0x0a, 0x04, 0x04, 0x0b, 0x02, 0x00, 0x12, 0x03, 0x65, 0x02, 0x12, 0x1a, 0x22, 0x20, 0x22, 0x63,
0x72, 0x65, 0x61, 0x74, 0x65, 0x22, 0x2c, 0x20, 0x22, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x22,
0x2c, 0x20, 0x6f, 0x72, 0x20, 0x22, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x22, 0x2e, 0x0a, 0x0a,
0x0c, 0x0a, 0x05, 0x04, 0x0b, 0x02, 0x00, 0x01, 0x12, 0x03, 0x65, 0x09, 0x0d, 0x0a, 0x0c, 0x0a,
0x05, 0x04, 0x0b, 0x02, 0x00, 0x03, 0x12, 0x03, 0x65, 0x10, 0x11, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
];
include!("musicfs.tonic.rs");
// @@protoc_insertion_point(module)
@@ -1,544 +0,0 @@
// @generated
/// Generated client implementations.
pub mod music_fs_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value
)]
use tonic::codegen::http::Uri;
use tonic::codegen::*;
#[derive(Debug, Clone)]
pub struct MusicFsClient<T> {
inner: tonic::client::Grpc<T>,
}
impl MusicFsClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> MusicFsClient<T>
where
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> MusicFsClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<http::Request<tonic::body::Body>>>::Error:
Into<StdError> + std::marker::Send + std::marker::Sync,
{
MusicFsClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn reconcile(
&mut self,
request: impl tonic::IntoRequest<super::ReconcileRequest>,
) -> std::result::Result<tonic::Response<super::ReconcileResponse>, tonic::Status> {
self.inner.ready().await.map_err(|e| {
tonic::Status::unknown(format!("Service was not ready: {}", e.into()))
})?;
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static("/musicfs.MusicFs/Reconcile");
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("musicfs.MusicFs", "Reconcile"));
self.inner.unary(req, path, codec).await
}
pub async fn get_metadata(
&mut self,
request: impl tonic::IntoRequest<super::GetMetadataRequest>,
) -> std::result::Result<
tonic::Response<tonic::codec::Streaming<super::ManifestEntry>>,
tonic::Status,
> {
self.inner.ready().await.map_err(|e| {
tonic::Status::unknown(format!("Service was not ready: {}", e.into()))
})?;
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static("/musicfs.MusicFs/GetMetadata");
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("musicfs.MusicFs", "GetMetadata"));
self.inner.server_streaming(req, path, codec).await
}
pub async fn get_manifest(
&mut self,
request: impl tonic::IntoRequest<super::GetManifestRequest>,
) -> std::result::Result<
tonic::Response<tonic::codec::Streaming<super::ManifestEntry>>,
tonic::Status,
> {
self.inner.ready().await.map_err(|e| {
tonic::Status::unknown(format!("Service was not ready: {}", e.into()))
})?;
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static("/musicfs.MusicFs/GetManifest");
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("musicfs.MusicFs", "GetManifest"));
self.inner.server_streaming(req, path, codec).await
}
pub async fn get_file(
&mut self,
request: impl tonic::IntoRequest<super::GetFileRequest>,
) -> std::result::Result<
tonic::Response<tonic::codec::Streaming<super::FileChunk>>,
tonic::Status,
> {
self.inner.ready().await.map_err(|e| {
tonic::Status::unknown(format!("Service was not ready: {}", e.into()))
})?;
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static("/musicfs.MusicFs/GetFile");
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("musicfs.MusicFs", "GetFile"));
self.inner.server_streaming(req, path, codec).await
}
pub async fn subscribe_events(
&mut self,
request: impl tonic::IntoRequest<super::SubscribeEventsRequest>,
) -> std::result::Result<
tonic::Response<tonic::codec::Streaming<super::ChangeEvent>>,
tonic::Status,
> {
self.inner.ready().await.map_err(|e| {
tonic::Status::unknown(format!("Service was not ready: {}", e.into()))
})?;
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static("/musicfs.MusicFs/SubscribeEvents");
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("musicfs.MusicFs", "SubscribeEvents"));
self.inner.server_streaming(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod music_fs_server {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value
)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with MusicFsServer.
#[async_trait]
pub trait MusicFs: std::marker::Send + std::marker::Sync + 'static {
async fn reconcile(
&self,
request: tonic::Request<super::ReconcileRequest>,
) -> std::result::Result<tonic::Response<super::ReconcileResponse>, tonic::Status>;
/// Server streaming response type for the GetMetadata method.
type GetMetadataStream: tonic::codegen::tokio_stream::Stream<
Item = std::result::Result<super::ManifestEntry, tonic::Status>,
> + std::marker::Send
+ 'static;
async fn get_metadata(
&self,
request: tonic::Request<super::GetMetadataRequest>,
) -> std::result::Result<tonic::Response<Self::GetMetadataStream>, tonic::Status>;
/// Server streaming response type for the GetManifest method.
type GetManifestStream: tonic::codegen::tokio_stream::Stream<
Item = std::result::Result<super::ManifestEntry, tonic::Status>,
> + std::marker::Send
+ 'static;
async fn get_manifest(
&self,
request: tonic::Request<super::GetManifestRequest>,
) -> std::result::Result<tonic::Response<Self::GetManifestStream>, tonic::Status>;
/// Server streaming response type for the GetFile method.
type GetFileStream: tonic::codegen::tokio_stream::Stream<
Item = std::result::Result<super::FileChunk, tonic::Status>,
> + std::marker::Send
+ 'static;
async fn get_file(
&self,
request: tonic::Request<super::GetFileRequest>,
) -> std::result::Result<tonic::Response<Self::GetFileStream>, tonic::Status>;
/// Server streaming response type for the SubscribeEvents method.
type SubscribeEventsStream: tonic::codegen::tokio_stream::Stream<
Item = std::result::Result<super::ChangeEvent, tonic::Status>,
> + std::marker::Send
+ 'static;
async fn subscribe_events(
&self,
request: tonic::Request<super::SubscribeEventsRequest>,
) -> std::result::Result<tonic::Response<Self::SubscribeEventsStream>, tonic::Status>;
}
#[derive(Debug)]
pub struct MusicFsServer<T> {
inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
impl<T> MusicFsServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(inner: T, interceptor: F) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for MusicFsServer<T>
where
T: MusicFs,
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/musicfs.MusicFs/Reconcile" => {
#[allow(non_camel_case_types)]
struct ReconcileSvc<T: MusicFs>(pub Arc<T>);
impl<T: MusicFs> tonic::server::UnaryService<super::ReconcileRequest> for ReconcileSvc<T> {
type Response = super::ReconcileResponse;
type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>;
fn call(
&mut self,
request: tonic::Request<super::ReconcileRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut =
async move { <T as MusicFs>::reconcile(&inner, request).await };
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = ReconcileSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/musicfs.MusicFs/GetMetadata" => {
#[allow(non_camel_case_types)]
struct GetMetadataSvc<T: MusicFs>(pub Arc<T>);
impl<T: MusicFs>
tonic::server::ServerStreamingService<super::GetMetadataRequest>
for GetMetadataSvc<T>
{
type Response = super::ManifestEntry;
type ResponseStream = T::GetMetadataStream;
type Future =
BoxFuture<tonic::Response<Self::ResponseStream>, tonic::Status>;
fn call(
&mut self,
request: tonic::Request<super::GetMetadataRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut =
async move { <T as MusicFs>::get_metadata(&inner, request).await };
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetMetadataSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.server_streaming(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/musicfs.MusicFs/GetManifest" => {
#[allow(non_camel_case_types)]
struct GetManifestSvc<T: MusicFs>(pub Arc<T>);
impl<T: MusicFs>
tonic::server::ServerStreamingService<super::GetManifestRequest>
for GetManifestSvc<T>
{
type Response = super::ManifestEntry;
type ResponseStream = T::GetManifestStream;
type Future =
BoxFuture<tonic::Response<Self::ResponseStream>, tonic::Status>;
fn call(
&mut self,
request: tonic::Request<super::GetManifestRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut =
async move { <T as MusicFs>::get_manifest(&inner, request).await };
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetManifestSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.server_streaming(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/musicfs.MusicFs/GetFile" => {
#[allow(non_camel_case_types)]
struct GetFileSvc<T: MusicFs>(pub Arc<T>);
impl<T: MusicFs> tonic::server::ServerStreamingService<super::GetFileRequest> for GetFileSvc<T> {
type Response = super::FileChunk;
type ResponseStream = T::GetFileStream;
type Future =
BoxFuture<tonic::Response<Self::ResponseStream>, tonic::Status>;
fn call(
&mut self,
request: tonic::Request<super::GetFileRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut =
async move { <T as MusicFs>::get_file(&inner, request).await };
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetFileSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.server_streaming(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/musicfs.MusicFs/SubscribeEvents" => {
#[allow(non_camel_case_types)]
struct SubscribeEventsSvc<T: MusicFs>(pub Arc<T>);
impl<T: MusicFs>
tonic::server::ServerStreamingService<super::SubscribeEventsRequest>
for SubscribeEventsSvc<T>
{
type Response = super::ChangeEvent;
type ResponseStream = T::SubscribeEventsStream;
type Future =
BoxFuture<tonic::Response<Self::ResponseStream>, tonic::Status>;
fn call(
&mut self,
request: tonic::Request<super::SubscribeEventsRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as MusicFs>::subscribe_events(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = SubscribeEventsSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.server_streaming(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => Box::pin(async move {
let mut response = http::Response::new(tonic::body::Body::default());
let headers = response.headers_mut();
headers.insert(
tonic::Status::GRPC_STATUS,
(tonic::Code::Unimplemented as i32).into(),
);
headers.insert(
http::header::CONTENT_TYPE,
tonic::metadata::GRPC_CONTENT_TYPE,
);
Ok(response)
}),
}
}
}
impl<T> Clone for MusicFsServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "musicfs.MusicFs";
impl<T> tonic::server::NamedService for MusicFsServer<T> {
const NAME: &'static str = SERVICE_NAME;
}
}
-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},
};
-267
View File
@@ -1,267 +0,0 @@
use std::{
collections::BTreeMap,
fs,
hash::Hasher,
path::{Path, PathBuf},
};
use fuser::INodeNo;
use twox_hash::XxHash64;
use crate::db::entities::Model;
use crate::item::{FileType, Item};
use crate::origins::attrs::FileAttrs;
pub fn virtual_inode(path: &str) -> INodeNo {
let mut hasher = XxHash64::with_seed(5678);
hasher.write(path.as_bytes());
return INodeNo(hasher.finish() | (1u64 << 63));
}
pub fn parent_inode_from_path(local_path: &Path) -> INodeNo {
let components: Vec<_> = local_path
.components()
.filter(|c| matches!(c, std::path::Component::Normal(_)))
.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());
}
return virtual_inode(&parent_path);
}
pub fn ensure_virtual_dirs(
local_path: &Path,
source: &Path,
map: &mut BTreeMap<INodeNo, Item>,
) -> INodeNo {
let components: Vec<_> = local_path
.components()
.filter(|c| matches!(c, std::path::Component::Normal(_)))
.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 = virtual_inode(&current_path);
if !map.contains_key(&virt_ino) {
let virt_item = Item::new(
virt_ino,
current_parent,
comp_str.to_string(),
source.to_path_buf(),
PathBuf::from(&current_path),
FileType::Directory,
FileAttrs::from(&fs::metadata(source).unwrap()),
None,
);
map.insert(virt_ino, virt_item);
}
current_parent = virt_ino;
}
return current_parent;
}
pub fn restore_virtual_paths(
snapshot: &mut BTreeMap<INodeNo, Item>,
db_items: &std::collections::HashMap<i64, Model>,
source: &Path,
) {
let restorations: Vec<(INodeNo, String, PathBuf)> = db_items
.values()
.filter_map(|db_item| {
let ino = INodeNo(db_item.inode as u64);
if ino == INodeNo::ROOT {
return None;
}
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 {
ensure_virtual_dirs(local_path, source, snapshot);
}
for (ino, name, local_path) in restorations {
if let Some(item) = snapshot.get_mut(&ino) {
item.name = name;
item.parent_inode = parent_inode_from_path(&local_path);
item.local_path = local_path;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn virtual_inode_deterministic() {
let ino1 = virtual_inode("foo");
let ino2 = virtual_inode("foo");
assert_eq!(ino1, ino2);
}
#[test]
fn virtual_inode_high_bit_set() {
let ino = virtual_inode("test");
assert_eq!(ino.0 & (1u64 << 63), 1u64 << 63);
}
#[test]
fn virtual_inode_different_inputs() {
let ino1 = virtual_inode("foo");
let ino2 = virtual_inode("bar");
assert_ne!(ino1, ino2);
}
#[test]
fn parent_inode_single_component() {
let ino = parent_inode_from_path(Path::new("file.txt"));
assert_eq!(ino, INodeNo::ROOT);
}
#[test]
fn parent_inode_multi_component() {
let ino = parent_inode_from_path(Path::new("a/b/c"));
let expected = virtual_inode("a/b");
assert_eq!(ino, expected);
}
#[test]
fn parent_inode_absolute_path_filtered() {
let ino = parent_inode_from_path(Path::new("/file"));
assert_eq!(ino, INodeNo::ROOT);
}
#[test]
fn ensure_virtual_dirs_single_component() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
let mut map = BTreeMap::new();
let ino = ensure_virtual_dirs(Path::new("file.txt"), source, &mut map);
assert_eq!(ino, INodeNo::ROOT);
assert!(map.is_empty());
}
#[test]
fn ensure_virtual_dirs_creates_hierarchy() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
let mut map = BTreeMap::new();
let ino = ensure_virtual_dirs(Path::new("a/b/file"), source, &mut map);
let a_ino = virtual_inode("a");
let ab_ino = virtual_inode("a/b");
assert_eq!(ino, ab_ino);
assert_eq!(map.len(), 2);
assert!(map.contains_key(&a_ino));
assert!(map.contains_key(&ab_ino));
let a_item = &map[&a_ino];
assert_eq!(a_item.parent_inode, INodeNo::ROOT);
let ab_item = &map[&ab_ino];
assert_eq!(ab_item.parent_inode, a_ino);
}
#[test]
fn ensure_virtual_dirs_idempotent() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
let mut map = BTreeMap::new();
ensure_virtual_dirs(Path::new("a/b/file"), source, &mut map);
let size_after_first = map.len();
ensure_virtual_dirs(Path::new("a/b/file"), source, &mut map);
let size_after_second = map.len();
assert_eq!(size_after_first, size_after_second);
}
#[test]
fn restore_virtual_paths_skips_root() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
let mut snapshot = BTreeMap::new();
let real_ino = INodeNo(42);
let real_item = Item::new(
real_ino,
INodeNo::ROOT,
"real_file".to_string(),
source.to_path_buf(),
PathBuf::from("real_file"),
FileType::File,
FileAttrs::from(&fs::metadata(source).unwrap()),
None,
);
snapshot.insert(real_ino, real_item);
let mut db_items = HashMap::new();
db_items.insert(
1i64,
Model {
inode: 1,
hash: 0,
name: "root".to_string(),
original_path: "/tmp/test".to_string(),
local_path: "/tmp/test".to_string(),
file_type: "directory".to_string(),
},
);
db_items.insert(
42i64,
Model {
inode: 42,
hash: 0,
name: "real_file".to_string(),
original_path: "real_file".to_string(),
local_path: "real_file".to_string(),
file_type: "file".to_string(),
},
);
restore_virtual_paths(&mut snapshot, &db_items, source);
for item in snapshot.values() {
assert_ne!(item.name, "/");
assert_ne!(item.name, "tmp");
}
}
}