Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dcfa7a803 | |||
| 78b4338f95 | |||
| 82be15b960 | |||
| 5f79b3c2b0 | |||
| f28091257e | |||
| 8f76dd5b39 | |||
| 8dba0c097d | |||
| d35aa2aecb | |||
| 0e50d8a2a6 | |||
| 9699da385b |
Generated
+41
-5
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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"] }
|
||||
@@ -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(¤t_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(())
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -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 ¤t_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(¤t_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,
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -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 });
|
||||
+44
-11
@@ -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(¤t_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(());
|
||||
}
|
||||
@@ -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(¤t_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(¤t_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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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},
|
||||
};
|
||||
@@ -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,
|
||||
@@ -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
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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/`:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Executable
+433
@@ -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 "$@"
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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},
|
||||
};
|
||||
@@ -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(¤t_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(¤t_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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user