Reorganize docs into v1 (beetfs) and v2 (new architecture)
docs/v1/ - Original beetfs documentation:
- analysis.md, components.md, data-flow.md, drawbacks.md
- features.md, modernization.md, rust-migration.md
- benchmark-plan.md, benchmark-results.md, e2e-test-plan.md
- README.md
docs/v2/ - New MusicFS architecture:
- requirements.md: Full requirements spec (FR-1 to FR-25, NFR-1 to NFR-14)
- P0: Multi-origin, plugins, CAS, control API
- P1: Search, album art, prefetch, metadata sources
- P3: HA, 10M+ files scalability
- architecture.md: Google BlueDoc style design document
- PlantUML diagrams for all components
- Design requirements with quantitative targets
- Alternatives considered, implementation plan
This commit is contained in:
@@ -1,276 +0,0 @@
|
|||||||
# beetfs Architecture
|
|
||||||
|
|
||||||
## System Overview
|
|
||||||
|
|
||||||
beetfs implements a **metadata overlay filesystem** using FUSE. The key innovation is separating metadata storage (in beets SQLite database) from audio data storage (original files on disk).
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ USER SPACE │
|
|
||||||
│ ┌─────────────┐ ┌─────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ Application │ │ beetfs │ │
|
|
||||||
│ │ (VLC, etc) │ │ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │ │
|
|
||||||
│ │ │◄───┼──┤beetFileSystem│──│ FileHandler │──│ Interpol. │ │ │
|
|
||||||
│ │ │ │ │ (FUSE) │ │ │ │ FLAC/ID3 │ │ │
|
|
||||||
│ └─────────────┘ │ └─────────────┘ └──────────────┘ └────────────┘ │ │
|
|
||||||
│ │ │ │ │ │ │
|
|
||||||
│ │ ▼ ▼ ▼ │ │
|
|
||||||
│ │ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │ │
|
|
||||||
│ │ │ FSNode │ │ Beets │ │ Original │ │ │
|
|
||||||
│ │ │ (dir tree) │ │ Database │ │ Files │ │ │
|
|
||||||
│ │ └─────────────┘ └──────────────┘ └────────────┘ │ │
|
|
||||||
│ └─────────────────────────────────────────────────────┘ │
|
|
||||||
├─────────────────────────────────────────────────────────────────────────────┤
|
|
||||||
│ KERNEL SPACE │
|
|
||||||
│ ┌───────────────┐ │
|
|
||||||
│ │ FUSE VFS │ │
|
|
||||||
│ └───────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Component Architecture
|
|
||||||
|
|
||||||
### 1. Plugin Layer
|
|
||||||
|
|
||||||
```python
|
|
||||||
class beetFs(BeetsPlugin):
|
|
||||||
"""Beets plugin hook - registers the 'mount' subcommand"""
|
|
||||||
def commands(self):
|
|
||||||
return [beetFs_command]
|
|
||||||
|
|
||||||
beetFs_command = Subcommand('mount', help='Mount a beets filesystem')
|
|
||||||
beetFs_command.func = mount
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Initialization Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
beet mount /mountpoint
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌───────────────────────────────────────────────────────────────┐
|
|
||||||
│ mount() function │
|
|
||||||
│ 1. Parse PATH_FORMAT template │
|
|
||||||
│ 2. Create FSNode root (directory_structure) │
|
|
||||||
│ 3. Iterate all items in beets library │
|
|
||||||
│ 4. For each item: │
|
|
||||||
│ - Build template substitution map │
|
|
||||||
│ - Add directories to FSNode tree │
|
|
||||||
│ - Add file entry (filename → item.id mapping) │
|
|
||||||
│ 5. Create beetFileSystem FUSE server │
|
|
||||||
│ 6. server.main() - enter FUSE event loop │
|
|
||||||
└───────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Virtual Directory Structure
|
|
||||||
|
|
||||||
The default path template:
|
|
||||||
```python
|
|
||||||
PATH_FORMAT = "$artist/$album ($year) [$format_upper]/$track - $artist - $title.$format"
|
|
||||||
```
|
|
||||||
|
|
||||||
Results in structure like:
|
|
||||||
```
|
|
||||||
/mountpoint/
|
|
||||||
├── Pink Floyd/
|
|
||||||
│ └── The Wall (1979) [FLAC]/
|
|
||||||
│ ├── 01 - Pink Floyd - In The Flesh?.flac
|
|
||||||
│ └── 02 - Pink Floyd - The Thin Ice.flac
|
|
||||||
└── Led Zeppelin/
|
|
||||||
└── IV (1971) [FLAC]/
|
|
||||||
└── 01 - Led Zeppelin - Black Dog.flac
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. FSNode Tree Structure
|
|
||||||
|
|
||||||
```python
|
|
||||||
class FSNode:
|
|
||||||
dirs: Dict[str, FSNode] # subdirectories
|
|
||||||
files: Dict[str, int] # filename → beets item ID
|
|
||||||
|
|
||||||
# Example tree:
|
|
||||||
FSNode(
|
|
||||||
dirs={
|
|
||||||
"Pink Floyd": FSNode(
|
|
||||||
dirs={
|
|
||||||
"The Wall (1979) [FLAC]": FSNode(
|
|
||||||
dirs={},
|
|
||||||
files={
|
|
||||||
"01 - Pink Floyd - In The Flesh?.flac": 42,
|
|
||||||
"02 - Pink Floyd - The Thin Ice.flac": 43
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
files={}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
files={}
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Core Data Flow
|
|
||||||
|
|
||||||
### Read Operation
|
|
||||||
|
|
||||||
```
|
|
||||||
Application: read("/mount/Artist/Album/track.flac", offset=0, size=4096)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌───────────────────────┐
|
|
||||||
│ beetFileSystem.read() │
|
|
||||||
│ Lines 1077-1106 │
|
|
||||||
└───────────┬───────────┘
|
|
||||||
│
|
|
||||||
┌───────────────┴───────────────┐
|
|
||||||
│ Get/Create FileHandler │
|
|
||||||
│ for this path │
|
|
||||||
└───────────────┬───────────────┘
|
|
||||||
│
|
|
||||||
┌───────────┴───────────┐
|
|
||||||
│ FileHandler.read() │
|
|
||||||
│ Lines 497-517 │
|
|
||||||
└───────────┬───────────┘
|
|
||||||
│
|
|
||||||
┌───────────────┴───────────────┐
|
|
||||||
▼ ▼
|
|
||||||
┌─────────────────────┐ ┌─────────────────────┐
|
|
||||||
│ offset < bound │ │ offset >= bound │
|
|
||||||
│ (in header area) │ │ (in audio area) │
|
|
||||||
└──────────┬──────────┘ └──────────┬──────────┘
|
|
||||||
│ │
|
|
||||||
▼ ▼
|
|
||||||
┌─────────────────────┐ ┌─────────────────────┐
|
|
||||||
│ Return modified │ │ Return original │
|
|
||||||
│ header from DB │ │ audio from file │
|
|
||||||
│ │ │ │
|
|
||||||
│ self.header[...] │ │ self.music_data[...]│
|
|
||||||
└─────────────────────┘ └─────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Write Operation
|
|
||||||
|
|
||||||
```
|
|
||||||
Application: write("/mount/Artist/Album/track.flac", data, offset=100)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌───────────────────────┐
|
|
||||||
│ beetFileSystem.write()│
|
|
||||||
│ Lines 1108-1135 │
|
|
||||||
└───────────┬───────────┘
|
|
||||||
│
|
|
||||||
┌───────────┴───────────┐
|
|
||||||
│ FileHandler.write() │
|
|
||||||
│ Lines 519-565 │
|
|
||||||
└───────────┬───────────┘
|
|
||||||
│
|
|
||||||
┌───────────────┴───────────────┐
|
|
||||||
▼ ▼
|
|
||||||
┌─────────────────────┐ ┌─────────────────────┐
|
|
||||||
│ offset < bound │ │ offset >= bound │
|
|
||||||
│ (in header area) │ │ (in audio area) │
|
|
||||||
└──────────┬──────────┘ └──────────┬──────────┘
|
|
||||||
│ │
|
|
||||||
▼ ▼
|
|
||||||
┌─────────────────────┐ ┌─────────────────────┐
|
|
||||||
│ 1. Patch header │ │ DISCARD │
|
|
||||||
│ 2. Parse new tags │ │ (audio writes │
|
|
||||||
│ 3. Extract values │ │ not allowed) │
|
|
||||||
│ 4. Update beets DB │ │ │
|
|
||||||
│ 5. Regenerate header│ │ │
|
|
||||||
└─────────────────────┘ └─────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Memory Model
|
|
||||||
|
|
||||||
### FileHandler State
|
|
||||||
|
|
||||||
```python
|
|
||||||
class FileHandler:
|
|
||||||
# Paths
|
|
||||||
path: str # Virtual path in FUSE mount
|
|
||||||
real_path: str # Actual file on disk
|
|
||||||
|
|
||||||
# Beets integration
|
|
||||||
item: Item # Beets library item
|
|
||||||
lib: Library # Beets library reference
|
|
||||||
|
|
||||||
# File data
|
|
||||||
file_object: File # File handle (closed after init)
|
|
||||||
music_data: bytes # Audio data cached in memory
|
|
||||||
|
|
||||||
# Metadata
|
|
||||||
format: str # "flac" or "mp3"
|
|
||||||
inf: FLAC/ID3 # Interpolated metadata object
|
|
||||||
header: bytes # Generated header with DB metadata
|
|
||||||
bound: int # Byte offset where header ends
|
|
||||||
music_offset: int # Byte offset where audio starts in original
|
|
||||||
|
|
||||||
# Reference counting
|
|
||||||
instance_count: int # Number of open handles
|
|
||||||
```
|
|
||||||
|
|
||||||
### Memory Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
Virtual File (as seen by application):
|
|
||||||
┌────────────────────────────────────────────────────────────────┐
|
|
||||||
│ HEADER (from DB) │ AUDIO (from file) │
|
|
||||||
│ [0 ... bound) │ [bound ... EOF) │
|
|
||||||
│ │ │
|
|
||||||
│ Generated by InterpolatedFLAC │ Cached in music_data │
|
|
||||||
│ Contains: title, artist, album, │ Original audio frames │
|
|
||||||
│ genre from beets DB │ Unchanged │
|
|
||||||
└────────────────────────────────────────────────────────────────┘
|
|
||||||
▲ ▲
|
|
||||||
│ │
|
|
||||||
self.header self.music_data
|
|
||||||
|
|
||||||
|
|
||||||
Original File (on disk):
|
|
||||||
┌────────────────────────────────────────────────────────────────┐
|
|
||||||
│ ORIGINAL HEADER │ AUDIO DATA │
|
|
||||||
│ [0 ... music_offset) │ [music_offset ... EOF) │
|
|
||||||
│ │ │
|
|
||||||
│ May have different │ Same as virtual file │
|
|
||||||
│ tag values │ │
|
|
||||||
└────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Threading Model
|
|
||||||
|
|
||||||
```python
|
|
||||||
server.multithreaded = 0 # Single-threaded mode
|
|
||||||
```
|
|
||||||
|
|
||||||
beetfs runs in **single-threaded mode** to avoid concurrency issues with:
|
|
||||||
- Shared `files` dictionary
|
|
||||||
- Beets library access
|
|
||||||
- File handle reference counting
|
|
||||||
|
|
||||||
## Global State
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Module-level globals (set during mount)
|
|
||||||
structure_split: List[str] # PATH_FORMAT split by "/"
|
|
||||||
structure_depth: int # Number of path components
|
|
||||||
library: Library # Beets library instance
|
|
||||||
directory_structure: FSNode # Root of virtual directory tree
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
| Situation | Response |
|
|
||||||
|-----------|----------|
|
|
||||||
| File not found | Return `-errno.ENOENT` |
|
|
||||||
| Permission denied | Return `-errno.EACCES` |
|
|
||||||
| Operation not supported | Return `-errno.EOPNOTSUPP` |
|
|
||||||
| Parse error | Log and return `-errno.ENOENT` |
|
|
||||||
|
|
||||||
## Limitations
|
|
||||||
|
|
||||||
1. **Format Support**: Only FLAC fully implemented; MP3 support is incomplete
|
|
||||||
2. **Memory Usage**: Entire audio portion cached in memory per open file
|
|
||||||
3. **Single-threaded**: No concurrent access optimization
|
|
||||||
4. **No Streaming**: Full file must be read into memory
|
|
||||||
5. **Python 2**: Uses deprecated language features
|
|
||||||
6. **fuse-python**: Old FUSE bindings, not maintained
|
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
# beetfs Feature Set
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
beetfs is a FUSE filesystem plugin for [beets](https://beets.io/) that presents your music library as a virtual filesystem organized by metadata. Files appear with paths derived from their database metadata, and reading file headers returns metadata from the beets database rather than the actual file tags.
|
||||||
|
|
||||||
|
**Author**: Martin Eve (2010)
|
||||||
|
**License**: GPLv3
|
||||||
|
**Python**: 2.7 (uses fuse-python)
|
||||||
|
|
||||||
|
## Core Features
|
||||||
|
|
||||||
|
### 1. Virtual Metadata-Based Directory Structure
|
||||||
|
|
||||||
|
Files are presented in a configurable path format based on beets database fields:
|
||||||
|
|
||||||
|
```
|
||||||
|
$artist/$album ($year) [$format_upper]/$track - $artist - $title.$format
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```
|
||||||
|
/mnt/beetfs/
|
||||||
|
├── Metallica/
|
||||||
|
│ └── 72 Seasons (2023) [FLAC]/
|
||||||
|
│ ├── 01 - Metallica - 72 Seasons.flac
|
||||||
|
│ ├── 02 - Metallica - Shadows Follow.flac
|
||||||
|
│ └── ...
|
||||||
|
├── Pink Floyd/
|
||||||
|
│ └── The Dark Side of the Moon (1973) [FLAC]/
|
||||||
|
│ └── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available template variables**:
|
||||||
|
- `$artist`, `$album`, `$title`, `$genre`, `$composer`, `$grouping`
|
||||||
|
- `$year`, `$month`, `$day`
|
||||||
|
- `$track`, `$tracktotal`, `$disc`, `$disctotal`
|
||||||
|
- `$format`, `$format_upper` (file extension)
|
||||||
|
- `$lyrics`, `$comments`, `$bpm`, `$comp`
|
||||||
|
|
||||||
|
### 2. Metadata Overlay (Read)
|
||||||
|
|
||||||
|
When you read a file through beetfs, the **metadata header is synthesized from the beets database**, not read from the actual file on disk.
|
||||||
|
|
||||||
|
**How it works**:
|
||||||
|
1. Open file → beetfs reads the real file from disk
|
||||||
|
2. Parse the audio format header (FLAC/MP3)
|
||||||
|
3. Replace metadata fields with values from beets database
|
||||||
|
4. Return synthesized header + original audio data
|
||||||
|
|
||||||
|
**Supported fields for overlay**:
|
||||||
|
- `title`, `artist`, `album`, `genre` (FLAC only currently)
|
||||||
|
|
||||||
|
**Use case**: Your files may have inconsistent or wrong tags, but beetfs presents them with the corrected metadata from your beets library.
|
||||||
|
|
||||||
|
### 3. Metadata Passthrough (Write)
|
||||||
|
|
||||||
|
When you write to file headers through beetfs, the **changes are saved to the beets database**, not to the actual file.
|
||||||
|
|
||||||
|
**How it works**:
|
||||||
|
1. Application writes new metadata to file header region
|
||||||
|
2. beetfs intercepts the write
|
||||||
|
3. Parses the new metadata values
|
||||||
|
4. Updates the beets database (`lib.store()`, `lib.save()`)
|
||||||
|
5. Regenerates the synthesized header
|
||||||
|
|
||||||
|
**Result**: Tag editors (Picard, Kid3, etc.) can edit metadata through beetfs, and changes persist in the beets database without modifying the original files.
|
||||||
|
|
||||||
|
### 4. Format Support
|
||||||
|
|
||||||
|
| Format | Read | Metadata Overlay | Write to DB |
|
||||||
|
|--------|------|------------------|-------------|
|
||||||
|
| FLAC | ✅ | ✅ Full | ✅ |
|
||||||
|
| MP3 | ✅ | ❌ Disabled | ❌ |
|
||||||
|
| Other | ❌ | ❌ | ❌ |
|
||||||
|
|
||||||
|
**FLAC Implementation**:
|
||||||
|
- Uses `InterpolatedFLAC` class extending mutagen
|
||||||
|
- Reconstructs Vorbis comment block with DB values
|
||||||
|
- Preserves audio data and other metadata blocks
|
||||||
|
|
||||||
|
**MP3 Implementation**:
|
||||||
|
- Passthrough only (no interpolation)
|
||||||
|
- `self.bound = 0` disables header replacement
|
||||||
|
|
||||||
|
### 5. File Caching
|
||||||
|
|
||||||
|
Open files are cached in `FileHandler` objects:
|
||||||
|
|
||||||
|
- First open: Load entire file into memory, parse headers
|
||||||
|
- Subsequent opens: Reuse cached `FileHandler`
|
||||||
|
- Reference counting for multiple opens
|
||||||
|
- Release when reference count reaches zero
|
||||||
|
|
||||||
|
**Memory impact**: Each open file consumes ~filesize RAM.
|
||||||
|
|
||||||
|
## FUSE Operations
|
||||||
|
|
||||||
|
### Implemented (Functional)
|
||||||
|
|
||||||
|
| Operation | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `getattr` | File/directory stat (size, mode, timestamps) |
|
||||||
|
| `access` | Permission checking |
|
||||||
|
| `opendir` | Open directory for listing |
|
||||||
|
| `readdir` | List directory contents |
|
||||||
|
| `releasedir` | Close directory |
|
||||||
|
| `open` | Open file for reading/writing |
|
||||||
|
| `read` | Read file contents |
|
||||||
|
| `write` | Write to file (header region only) |
|
||||||
|
| `release` | Close file |
|
||||||
|
| `fgetattr` | Stat with file handle |
|
||||||
|
| `statfs` | Filesystem statistics |
|
||||||
|
|
||||||
|
### Not Implemented (Return EOPNOTSUPP)
|
||||||
|
|
||||||
|
| Operation | Reason |
|
||||||
|
|-----------|--------|
|
||||||
|
| `create` | Read-only structure |
|
||||||
|
| `mknod` | Read-only structure |
|
||||||
|
| `mkdir` | Read-only structure |
|
||||||
|
| `unlink` | Read-only structure |
|
||||||
|
| `rmdir` | Read-only structure |
|
||||||
|
| `symlink` | Not needed |
|
||||||
|
| `link` | Not needed |
|
||||||
|
| `rename` | Would break DB consistency |
|
||||||
|
| `chmod` | Metadata-only FS |
|
||||||
|
| `chown` | Metadata-only FS |
|
||||||
|
| `truncate` | Would corrupt audio |
|
||||||
|
| `utime` | Metadata-only FS |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Mount
|
||||||
|
|
||||||
|
```bash
|
||||||
|
beet mount /mnt/beetfs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unmount
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fusermount -u /mnt/beetfs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Session
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mount the filesystem
|
||||||
|
beet mount /mnt/music
|
||||||
|
|
||||||
|
# Browse by artist
|
||||||
|
ls /mnt/music/
|
||||||
|
# Metallica/ Pink Floyd/ The Beatles/ ...
|
||||||
|
|
||||||
|
# List an album
|
||||||
|
ls "/mnt/music/Metallica/72 Seasons (2023) [FLAC]/"
|
||||||
|
# 01 - Metallica - 72 Seasons.flac
|
||||||
|
# 02 - Metallica - Shadows Follow.flac
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# Play through any music player
|
||||||
|
mpv "/mnt/music/Metallica/72 Seasons (2023) [FLAC]/01 - Metallica - 72 Seasons.flac"
|
||||||
|
|
||||||
|
# Edit tags (changes go to beets DB)
|
||||||
|
kid3 "/mnt/music/Metallica/72 Seasons (2023) [FLAC]/"
|
||||||
|
|
||||||
|
# Unmount
|
||||||
|
fusermount -u /mnt/music
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ User Applications │
|
||||||
|
│ (mpv, Rhythmbox, Kid3, etc.) │
|
||||||
|
└─────────────────────────┬───────────────────────────────────┘
|
||||||
|
│ POSIX calls (open, read, write)
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Linux Kernel │
|
||||||
|
│ FUSE module │
|
||||||
|
└─────────────────────────┬───────────────────────────────────┘
|
||||||
|
│ /dev/fuse
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ beetfs │
|
||||||
|
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────────┐ │
|
||||||
|
│ │ FSNode Tree │ │ FileHandler │ │ InterpolatedFLAC │ │
|
||||||
|
│ │ (in-memory) │ │ (cache) │ │ (header synth) │ │
|
||||||
|
│ └─────────────┘ └──────────────┘ └───────────────────┘ │
|
||||||
|
└────────┬────────────────┬───────────────────┬───────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Beets DB │ │ Real Files │ │ Mutagen │
|
||||||
|
│ (SQLite) │ │ (on disk) │ │ (parsing) │
|
||||||
|
└─────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
### Current Bugs (Non-Functional)
|
||||||
|
|
||||||
|
1. **Nested Methods Bug**: Lines 758-1144 are indented inside `access()`, making FUSE operations unreachable
|
||||||
|
2. **Directory Tree Bug**: `FSNode.adddir()` crashes when building tree for non-empty library
|
||||||
|
|
||||||
|
### Design Limitations
|
||||||
|
|
||||||
|
1. **Memory Usage**: Entire file loaded into RAM on open
|
||||||
|
2. **Mount Time**: O(N) - loads all library items at mount
|
||||||
|
3. **No Lazy Loading**: Full directory tree built upfront
|
||||||
|
4. **Single Format**: Only FLAC has full metadata overlay
|
||||||
|
5. **No Real File Modification**: Writes only update DB, not actual files
|
||||||
|
6. **Python 2.7 GIL**: Single-threaded performance
|
||||||
|
|
||||||
|
### Not Supported
|
||||||
|
|
||||||
|
- Creating/deleting files or directories
|
||||||
|
- Moving/renaming files
|
||||||
|
- Modifying audio content
|
||||||
|
- Album art / embedded images
|
||||||
|
- Multi-value tags
|
||||||
|
- Non-ASCII in some edge cases
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Currently hardcoded. Potential configuration points:
|
||||||
|
|
||||||
|
| Setting | Current Value | Description |
|
||||||
|
|---------|---------------|-------------|
|
||||||
|
| `PATH_FORMAT` | `$artist/$album ($year)...` | Directory structure template |
|
||||||
|
| `METADATA_RW_FIELDS` | 17 fields | Fields available for read/write |
|
||||||
|
| Caching | Always on | FileHandler caching behavior |
|
||||||
|
| Threading | Disabled | `multithreaded = 0` |
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Python 2.7
|
||||||
|
- fuse-python
|
||||||
|
- beets 1.4.x
|
||||||
|
- mutagen (FLAC/MP3 parsing)
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [e2e-test-plan.md](e2e-test-plan.md) - Test strategy and bug documentation
|
||||||
|
- [benchmark-plan.md](benchmark-plan.md) - Performance measurement methodology
|
||||||
|
- [benchmark-results.md](benchmark-results.md) - Current benchmark status
|
||||||
@@ -0,0 +1,451 @@
|
|||||||
|
# Rust Migration Analysis for beetfs
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
Migrating beetfs from Python to Rust is **strongly recommended** based on research findings. Expected improvements:
|
||||||
|
|
||||||
|
| Metric | Python (Current) | Rust (Expected) | Improvement |
|
||||||
|
|--------|------------------|-----------------|-------------|
|
||||||
|
| **Memory per file** | ~280 bytes overhead | ~60 bytes | **4-5x reduction** |
|
||||||
|
| **File open latency** | 200-500ms | 20-50ms | **10x faster** |
|
||||||
|
| **Read latency** | 5-10ms | 0.5-2ms | **5-10x faster** |
|
||||||
|
| **Concurrent opens** | ~1,000 (threading) | ~100,000+ (Tokio) | **100x more** |
|
||||||
|
| **GC pauses** | 50-2200ms | 0ms | **Eliminated** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Rust FUSE Ecosystem
|
||||||
|
|
||||||
|
### Recommended: **fuser**
|
||||||
|
|
||||||
|
| Attribute | Value |
|
||||||
|
|-----------|-------|
|
||||||
|
| **Downloads** | 3.2M+ |
|
||||||
|
| **Maturity** | Production-ready |
|
||||||
|
| **Platforms** | Linux, macOS, FreeBSD |
|
||||||
|
| **Async** | Experimental (stable sync API) |
|
||||||
|
| **Used by** | AWS Mountpoint for S3 |
|
||||||
|
|
||||||
|
**API Example:**
|
||||||
|
```rust
|
||||||
|
use fuser::{Filesystem, Request, ReplyData};
|
||||||
|
|
||||||
|
impl Filesystem for BeetFS {
|
||||||
|
fn read(&self, _req: &Request, ino: u64, _fh: u64,
|
||||||
|
offset: i64, size: u32, _flags: i32,
|
||||||
|
_lock: Option<u64>, reply: ReplyData) {
|
||||||
|
|
||||||
|
let file = self.get_file(ino);
|
||||||
|
|
||||||
|
if offset < file.header_len {
|
||||||
|
// Return metadata from database (interpolated)
|
||||||
|
reply.data(&file.header[offset as usize..]);
|
||||||
|
} else {
|
||||||
|
// Return audio from original file (zero-copy via mmap)
|
||||||
|
let audio_offset = offset - file.header_len;
|
||||||
|
reply.data(&file.mmap[audio_offset as usize..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alternatives
|
||||||
|
|
||||||
|
| Library | Async | Maturity | Best For |
|
||||||
|
|---------|-------|----------|----------|
|
||||||
|
| **fuser** | Experimental | ⭐⭐⭐⭐⭐ | General purpose |
|
||||||
|
| **fuse3** | Native | ⭐⭐⭐⭐ | Async-heavy, Linux-only |
|
||||||
|
| **polyfuse** | Native | ⭐⭐⭐ | Custom control flow |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Rust Audio Metadata: **lofty**
|
||||||
|
|
||||||
|
Full feature parity with Python's mutagen:
|
||||||
|
|
||||||
|
| Feature | mutagen (Python) | lofty (Rust) |
|
||||||
|
|---------|------------------|--------------|
|
||||||
|
| FLAC Vorbis Comments | ✅ | ✅ |
|
||||||
|
| MP3 ID3v2 (all versions) | ✅ | ✅ |
|
||||||
|
| OGG Vorbis Comments | ✅ | ✅ |
|
||||||
|
| Opus metadata | ✅ | ✅ |
|
||||||
|
| In-memory manipulation | ✅ | ✅ |
|
||||||
|
| Header generation | ✅ | ✅ `dump_to()` |
|
||||||
|
| Picture/artwork | ✅ | ✅ |
|
||||||
|
|
||||||
|
**API Comparison:**
|
||||||
|
```python
|
||||||
|
# Python mutagen
|
||||||
|
audio = mutagen.File("song.flac")
|
||||||
|
audio['artist'] = 'New Artist'
|
||||||
|
audio['title'] = 'New Title'
|
||||||
|
audio.save()
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Rust lofty
|
||||||
|
let mut file = lofty::read_from_path("song.flac")?;
|
||||||
|
let tag = file.primary_tag_mut().unwrap();
|
||||||
|
tag.set_artist("New Artist".to_string());
|
||||||
|
tag.set_title("New Title".to_string());
|
||||||
|
tag.save_to_path("song.flac", WriteOptions::default())?;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Header Generation (Critical for beetfs):**
|
||||||
|
```rust
|
||||||
|
// Generate FLAC header with modified tags WITHOUT writing to file
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
tag.dump_to(&mut buffer, WriteOptions::default())?;
|
||||||
|
// `buffer` contains serialized metadata header
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Memory Benefits
|
||||||
|
|
||||||
|
### Python Object Overhead
|
||||||
|
|
||||||
|
| Python Type | Size | Notes |
|
||||||
|
|-------------|------|-------|
|
||||||
|
| Empty dict | 232 bytes | Base overhead |
|
||||||
|
| Dict entry | +184 bytes | Per key-value |
|
||||||
|
| Empty string | 49 bytes | Base overhead |
|
||||||
|
| Empty list | 56 bytes | Base overhead |
|
||||||
|
| Small int | 28 bytes | Even for `0` |
|
||||||
|
|
||||||
|
**Current beetfs FileHandler (Python):**
|
||||||
|
```
|
||||||
|
self.path → str → 49 + len(path) bytes
|
||||||
|
self.real_path → str → 49 + len(path) bytes
|
||||||
|
self.item → dict → 232 + entries
|
||||||
|
self.header → bytes → 33 + len(header)
|
||||||
|
self.music_data → bytes → 33 + len(audio) ← CRITICAL: full file!
|
||||||
|
self.inf → object → 100+ bytes
|
||||||
|
─────────────────────────────────────────
|
||||||
|
TOTAL: ~500 bytes + entire file in RAM
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rust Struct Efficiency
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct FileHandler {
|
||||||
|
path: PathBuf, // 24 bytes (ptr+len+cap)
|
||||||
|
real_path: PathBuf, // 24 bytes
|
||||||
|
item_id: u64, // 8 bytes
|
||||||
|
header: Vec<u8>, // 24 bytes (ptr+len+cap) + header data
|
||||||
|
mmap: Mmap, // 24 bytes (NO file data in RAM!)
|
||||||
|
header_len: u64, // 8 bytes
|
||||||
|
audio_offset: u64, // 8 bytes
|
||||||
|
}
|
||||||
|
// TOTAL: ~120 bytes + header only (audio via mmap)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory Comparison
|
||||||
|
|
||||||
|
| Scenario | Python | Rust | Savings |
|
||||||
|
|----------|--------|------|---------|
|
||||||
|
| 1 file (50MB) | ~50 MB | ~64 KB | **780x** |
|
||||||
|
| 10 files (50MB each) | ~500 MB | ~640 KB | **780x** |
|
||||||
|
| 100 files (50MB each) | ~5 GB | ~6.4 MB | **780x** |
|
||||||
|
| Library scan (1000 files) | **OOM** | ~64 MB | ∞ |
|
||||||
|
|
||||||
|
**Key insight**: Rust can use memory-mapped files (`mmap`) to serve audio data with zero copies, eliminating the need to load files into RAM.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Latency Benefits
|
||||||
|
|
||||||
|
### Python FUSE Bottlenecks
|
||||||
|
|
||||||
|
1. **Dict-to-struct conversion**: Every FUSE callback requires converting Python dicts to C structs
|
||||||
|
2. **GIL contention**: Single-threaded execution despite multi-core CPUs
|
||||||
|
3. **GC pauses**: Stop-the-world pauses of 50-2200ms under load
|
||||||
|
4. **Object allocation**: Creating Python objects for every I/O operation
|
||||||
|
|
||||||
|
### Rust FUSE Advantages
|
||||||
|
|
||||||
|
1. **Zero-cost abstractions**: No runtime overhead for type conversions
|
||||||
|
2. **No GIL**: True parallelism across all cores
|
||||||
|
3. **No GC**: Deterministic memory management, no pauses
|
||||||
|
4. **Stack allocation**: Small objects allocated on stack, not heap
|
||||||
|
|
||||||
|
### Benchmark Data
|
||||||
|
|
||||||
|
| Operation | Python FUSE | Rust FUSE | Improvement |
|
||||||
|
|-----------|-------------|-----------|-------------|
|
||||||
|
| File stat | 5-10ms | 0.5-1ms | **10x** |
|
||||||
|
| Small read | 5-10ms | 0.5-2ms | **5-10x** |
|
||||||
|
| Large read | 115 MB/s | 260+ MB/s | **2-3x** |
|
||||||
|
| Metadata lookup | 10ms | <1ms | **10x** |
|
||||||
|
|
||||||
|
### GC Pause Elimination
|
||||||
|
|
||||||
|
```
|
||||||
|
Python GC Pauses (measured):
|
||||||
|
├── P50: ~10ms
|
||||||
|
├── P95: ~50ms
|
||||||
|
├── P99: ~320ms
|
||||||
|
└── Max: ~2200ms (!)
|
||||||
|
|
||||||
|
Rust (no GC):
|
||||||
|
├── P50: ~0.5ms
|
||||||
|
├── P95: ~1ms
|
||||||
|
├── P99: ~2ms
|
||||||
|
└── Max: ~5ms (deterministic)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Concurrency Benefits
|
||||||
|
|
||||||
|
### Python Threading Limitations
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Python (current beetfs)
|
||||||
|
server.multithreaded = 0 # Single-threaded!
|
||||||
|
|
||||||
|
# Even with threading enabled:
|
||||||
|
# - GIL prevents true parallelism
|
||||||
|
# - ~8MB per thread
|
||||||
|
# - OS limits: ~1000-2000 threads max
|
||||||
|
# - Context switch: 1-10μs (kernel)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rust Async (Tokio)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Rust with Tokio
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Can handle 100K+ concurrent operations
|
||||||
|
// - ~2KB per task (4000x less than thread)
|
||||||
|
// - Work-stealing scheduler
|
||||||
|
// - Context switch: ~10ns (userspace)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Metric | Python Threading | Rust Tokio |
|
||||||
|
|--------|------------------|------------|
|
||||||
|
| Memory per task | 8 MB | 2 KB |
|
||||||
|
| Max concurrent | ~1,000 | ~100,000+ |
|
||||||
|
| Context switch | 1-10μs | ~10ns |
|
||||||
|
| Parallelism | Blocked by GIL | True multi-core |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Zero-Copy I/O
|
||||||
|
|
||||||
|
### Python (Current)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Every read copies data through Python:
|
||||||
|
self.file_object.read() # syscall → kernel buffer
|
||||||
|
# kernel buffer → Python bytes object
|
||||||
|
# Python bytes → FUSE reply buffer
|
||||||
|
# = 2-3 copies per read
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rust (Proposed)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Memory-mapped file + zero-copy reply:
|
||||||
|
let mmap = unsafe { MmapOptions::new().map(&file)? };
|
||||||
|
|
||||||
|
fn read(&self, ..., reply: ReplyData) {
|
||||||
|
// Direct slice from mmap → FUSE kernel
|
||||||
|
reply.data(&self.mmap[offset..offset+size]);
|
||||||
|
// = 0 copies (kernel reads directly from mapped pages)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### I/O Comparison
|
||||||
|
|
||||||
|
| Scenario | Python | Rust | Benefit |
|
||||||
|
|----------|--------|------|---------|
|
||||||
|
| Serve 50MB file | 50MB copied to RAM | 0 bytes copied | **50MB saved** |
|
||||||
|
| 100 concurrent reads | 5GB buffers | ~0 (shared mmap) | **5GB saved** |
|
||||||
|
| Throughput | 115 MB/s | 260+ MB/s | **2.3x faster** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Real-World Migration Results
|
||||||
|
|
||||||
|
### Case Studies
|
||||||
|
|
||||||
|
| Project | Metric | Python | Rust | Improvement |
|
||||||
|
|---------|--------|--------|------|-------------|
|
||||||
|
| API Service | Response time | 200ms | 8ms | **96% faster** |
|
||||||
|
| Data Pipeline | Processing | 3 hours | 4.5 min | **40x faster** |
|
||||||
|
| Web Backend | Memory | 1.2 GB | 180 MB | **85% less** |
|
||||||
|
| Trajectory Lib | Compute | baseline | 10x faster | **10x** |
|
||||||
|
|
||||||
|
### AWS Mountpoint for S3
|
||||||
|
|
||||||
|
- Built on **fuser** (Rust FUSE)
|
||||||
|
- Handles **terabits/sec** aggregate throughput
|
||||||
|
- Production-ready since 2024
|
||||||
|
- Validates Rust FUSE at scale
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Migration Architecture
|
||||||
|
|
||||||
|
### Proposed Rust beetfs Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
beetfs-rs/
|
||||||
|
├── Cargo.toml
|
||||||
|
├── src/
|
||||||
|
│ ├── main.rs # Entry point, mount logic
|
||||||
|
│ ├── lib.rs # Library root
|
||||||
|
│ ├── fs/
|
||||||
|
│ │ ├── mod.rs # FUSE filesystem impl
|
||||||
|
│ │ ├── tree.rs # Virtual directory tree (FSNode equivalent)
|
||||||
|
│ │ ├── file.rs # File handler with mmap
|
||||||
|
│ │ └── stat.rs # File attributes
|
||||||
|
│ ├── metadata/
|
||||||
|
│ │ ├── mod.rs # Metadata overlay logic
|
||||||
|
│ │ ├── flac.rs # FLAC header generation (using lofty)
|
||||||
|
│ │ ├── mp3.rs # MP3 ID3 header generation
|
||||||
|
│ │ └── db.rs # Database interface (SQLite or custom)
|
||||||
|
│ └── config.rs # Configuration (path templates, etc.)
|
||||||
|
└── tests/
|
||||||
|
├── fs_tests.rs
|
||||||
|
└── metadata_tests.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Virtual directory tree (equivalent to FSNode)
|
||||||
|
pub struct VirtualTree {
|
||||||
|
root: Arc<RwLock<DirNode>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DirNode {
|
||||||
|
dirs: HashMap<OsString, Arc<RwLock<DirNode>>>,
|
||||||
|
files: HashMap<OsString, FileEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FileEntry {
|
||||||
|
inode: u64,
|
||||||
|
real_path: PathBuf,
|
||||||
|
metadata_id: i64, // Database reference
|
||||||
|
}
|
||||||
|
|
||||||
|
// File handler with memory-mapped audio
|
||||||
|
pub struct OpenFile {
|
||||||
|
header: Vec<u8>, // Generated header with DB metadata
|
||||||
|
header_len: usize,
|
||||||
|
mmap: Mmap, // Memory-mapped original file
|
||||||
|
audio_offset: usize, // Where audio starts in original
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenFile {
|
||||||
|
pub fn read(&self, offset: usize, size: usize) -> &[u8] {
|
||||||
|
if offset < self.header_len {
|
||||||
|
// Return from generated header (DB metadata)
|
||||||
|
&self.header[offset..min(offset + size, self.header_len)]
|
||||||
|
} else {
|
||||||
|
// Return from mmap (original audio, zero-copy)
|
||||||
|
let audio_off = offset - self.header_len + self.audio_offset;
|
||||||
|
&self.mmap[audio_off..audio_off + size]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Migration Effort Estimate
|
||||||
|
|
||||||
|
### Timeline
|
||||||
|
|
||||||
|
| Phase | Duration | Deliverable |
|
||||||
|
|-------|----------|-------------|
|
||||||
|
| **1. Prototype** | 1-2 weeks | Basic FUSE mount, read-only |
|
||||||
|
| **2. Core features** | 2-3 weeks | Metadata overlay, FLAC support |
|
||||||
|
| **3. Full parity** | 2-3 weeks | MP3, write support, all fields |
|
||||||
|
| **4. Testing** | 1-2 weeks | Unit tests, integration tests |
|
||||||
|
| **5. Optimization** | 1-2 weeks | mmap, async, benchmarking |
|
||||||
|
|
||||||
|
**Total: 7-12 weeks**
|
||||||
|
|
||||||
|
### Skill Requirements
|
||||||
|
|
||||||
|
- Rust fundamentals (ownership, borrowing, lifetimes)
|
||||||
|
- FUSE protocol knowledge (from Python experience)
|
||||||
|
- Audio metadata formats (FLAC, ID3)
|
||||||
|
- Async Rust (Tokio) - optional for Phase 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Risk Assessment
|
||||||
|
|
||||||
|
### Low Risk ✅
|
||||||
|
|
||||||
|
| Factor | Why Low Risk |
|
||||||
|
|--------|--------------|
|
||||||
|
| FUSE library | fuser is production-proven (AWS) |
|
||||||
|
| Metadata library | lofty has full mutagen parity |
|
||||||
|
| Core algorithm | Same logic, different language |
|
||||||
|
| File format support | FLAC/MP3/OGG all supported |
|
||||||
|
|
||||||
|
### Medium Risk ⚠️
|
||||||
|
|
||||||
|
| Factor | Mitigation |
|
||||||
|
|--------|------------|
|
||||||
|
| Learning curve | Existing Rust experience helps |
|
||||||
|
| Edge cases | Port Python tests to Rust |
|
||||||
|
| Async complexity | Start with sync API, add async later |
|
||||||
|
|
||||||
|
### Benefits vs Effort
|
||||||
|
|
||||||
|
```
|
||||||
|
Current Python Issues:
|
||||||
|
├── Memory: OOM on library scan → Fixed by mmap
|
||||||
|
├── Latency: 200-500ms file open → Fixed by zero-copy
|
||||||
|
├── GC pauses: 50-2200ms → Eliminated
|
||||||
|
├── Concurrency: single-threaded → Fixed by async
|
||||||
|
└── MP3 support: disabled → Implemented properly
|
||||||
|
|
||||||
|
Migration Effort: 7-12 weeks
|
||||||
|
Expected Lifetime: 5+ years
|
||||||
|
ROI: Highly positive
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Recommendation
|
||||||
|
|
||||||
|
### ✅ **Proceed with Rust Migration**
|
||||||
|
|
||||||
|
**Justification:**
|
||||||
|
1. **10x memory reduction** via mmap (eliminates OOM)
|
||||||
|
2. **5-10x latency improvement** (eliminates blocking reads)
|
||||||
|
3. **GC pauses eliminated** (deterministic performance)
|
||||||
|
4. **100x concurrency** improvement (Tokio async)
|
||||||
|
5. **Production-proven** ecosystem (fuser + lofty)
|
||||||
|
6. **Reasonable effort** (7-12 weeks)
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
1. **Set up Rust project** with fuser and lofty dependencies
|
||||||
|
2. **Port FSNode** to Rust VirtualTree
|
||||||
|
3. **Implement basic FUSE** operations (read, getattr, readdir)
|
||||||
|
4. **Add metadata overlay** with lofty for FLAC
|
||||||
|
5. **Add mmap** for zero-copy audio serving
|
||||||
|
6. **Benchmark** against Python implementation
|
||||||
|
7. **Add MP3/OGG** support
|
||||||
|
8. **Add async** with Tokio (optional)
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
fuser = "0.17"
|
||||||
|
lofty = "0.21"
|
||||||
|
memmap2 = "0.9"
|
||||||
|
tokio = { version = "1", features = ["full"], optional = true }
|
||||||
|
rusqlite = "0.31" # For beets DB compatibility
|
||||||
|
```
|
||||||
@@ -0,0 +1,899 @@
|
|||||||
|
# MusicFS: Design Doc
|
||||||
|
|
||||||
|
**Authors:** [TBD]
|
||||||
|
**Status:** Draft
|
||||||
|
**Last Updated:** 2026-05-12
|
||||||
|
**Reviewers:** [TBD]
|
||||||
|
**Approvers:** [TBD]
|
||||||
|
**Requirements:** [requirements.md](requirements.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Abstract
|
||||||
|
|
||||||
|
MusicFS is a read-only FUSE filesystem that presents music libraries organized
|
||||||
|
by metadata (artist/album/track) rather than physical file paths. It supports
|
||||||
|
multiple origin storage backends (local, NFS, S3, SFTP), provides intelligent
|
||||||
|
caching with delta synchronization, and exposes a plugin architecture for
|
||||||
|
extensibility.
|
||||||
|
|
||||||
|
The system addresses limitations of the existing beetfs implementation:
|
||||||
|
- O(N) mount time → O(1) lazy loading
|
||||||
|
- Full file in RAM → streaming with content-addressable chunks
|
||||||
|
- Single origin → federated multi-origin with failover
|
||||||
|
- No offline support → cache-first with graceful degradation
|
||||||
|
|
||||||
|
Target users are media enthusiasts with large music collections (100K-10M+
|
||||||
|
tracks) distributed across multiple storage systems who want a unified,
|
||||||
|
metadata-organized view without modifying original files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Background
|
||||||
|
|
||||||
|
### 2.1 Current State
|
||||||
|
|
||||||
|
The existing beetfs implementation is a Python 2.7 FUSE plugin for beets that:
|
||||||
|
- Presents a virtual filesystem organized by metadata templates
|
||||||
|
- Overlays metadata from beets database onto file headers
|
||||||
|
- Supports metadata writes back to the beets database
|
||||||
|
|
||||||
|
### 2.2 Pain Points
|
||||||
|
|
||||||
|
| Problem | Impact |
|
||||||
|
|---------|--------|
|
||||||
|
| O(N) mount time (5-120s for large libraries) | Unusable for large collections |
|
||||||
|
| Loads entire file into RAM on open | OOM risk, 50-100MB per file |
|
||||||
|
| Python GIL limits concurrency | Poor performance under load |
|
||||||
|
| No caching between sessions | Repeated work on every mount |
|
||||||
|
| Single local origin only | Can't federate across storage |
|
||||||
|
| No offline support | Unusable without origin access |
|
||||||
|
| Critical bugs (nested methods, tree building) | Non-functional |
|
||||||
|
|
||||||
|
### 2.3 Related Systems
|
||||||
|
|
||||||
|
| System | Relationship |
|
||||||
|
|--------|--------------|
|
||||||
|
| [beets](https://beets.io/) | Source of inspiration; potential import source |
|
||||||
|
| [rclone mount](https://rclone.org/commands/rclone_mount/) | Similar FUSE + remote storage; no metadata organization |
|
||||||
|
| [Plex/Jellyfin](https://jellyfin.org/) | Media servers with metadata; not filesystem-based |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Goals & Non-Goals
|
||||||
|
|
||||||
|
### 3.1 Goals
|
||||||
|
|
||||||
|
| ID | Goal | Success Metric |
|
||||||
|
|----|------|----------------|
|
||||||
|
| G1 | O(1) mount time | <500ms regardless of library size |
|
||||||
|
| G2 | Minimal memory footprint | <50MB idle, <500MB peak |
|
||||||
|
| G3 | Support multiple origins | ≥2 origins with automatic failover |
|
||||||
|
| G4 | Offline-first operation | Serve cached data when origin unavailable |
|
||||||
|
| G5 | Delta synchronization | >90% bandwidth reduction vs full sync |
|
||||||
|
| G6 | Plugin extensibility | Support custom origins, formats, metadata sources |
|
||||||
|
| G7 | Full-text search | Sub-second search across 1M+ tracks |
|
||||||
|
|
||||||
|
### 3.2 Design Requirements
|
||||||
|
|
||||||
|
The following quantitative requirements drive architectural decisions. Full
|
||||||
|
specification in [requirements.md](requirements.md).
|
||||||
|
|
||||||
|
#### 3.2.1 Latency Requirements
|
||||||
|
|
||||||
|
| Operation | Target | Maximum | Requirement |
|
||||||
|
|-----------|--------|---------|-------------|
|
||||||
|
| `stat()` cached | <1ms | 5ms | NFR-1.1 |
|
||||||
|
| `readdir()` cached | <10ms | 50ms | NFR-1.2 |
|
||||||
|
| `open()` cached | <5ms | 20ms | NFR-1.3 |
|
||||||
|
| `read()` cached | <1ms | 5ms | NFR-1.4 |
|
||||||
|
| `read()` cache miss (local) | <50ms | 200ms | NFR-1.5 |
|
||||||
|
| `read()` cache miss (remote) | <200ms | 1000ms | NFR-1.6 |
|
||||||
|
| Mount completion | <100ms | 500ms | NFR-1.7 |
|
||||||
|
| Search query (1M files) | <500ms | 1000ms | FR-14 |
|
||||||
|
|
||||||
|
**Design Response:**
|
||||||
|
- Lazy loading eliminates mount-time I/O → O(1) mount
|
||||||
|
- In-memory LRU cache for hot metadata → <1ms stat
|
||||||
|
- SQLite with indexes → O(log n) lookups
|
||||||
|
- Async I/O via tokio → non-blocking operations
|
||||||
|
|
||||||
|
#### 3.2.2 Throughput Requirements
|
||||||
|
|
||||||
|
| Metric | Target | Requirement |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| Sequential read (cached) | >500 MB/s | NFR-2.1 |
|
||||||
|
| Sequential read (local origin) | >200 MB/s | NFR-2.2 |
|
||||||
|
| Metadata ops/sec | >1000 | NFR-2.3 |
|
||||||
|
| Concurrent file handles | >1000 | NFR-2.4 |
|
||||||
|
|
||||||
|
**Design Response:**
|
||||||
|
- Memory-mapped chunk files → kernel-optimized reads
|
||||||
|
- No GIL (Rust) → true parallelism
|
||||||
|
- Async FUSE ops → handle many concurrent requests
|
||||||
|
|
||||||
|
#### 3.2.3 Scalability Requirements
|
||||||
|
|
||||||
|
| Metric | Target | Stretch | Requirement |
|
||||||
|
|--------|--------|---------|-------------|
|
||||||
|
| Library size | 1M files | 10M files | NFR-3.1, NFR-3.5 |
|
||||||
|
| Directory entries | 100K | 1M | NFR-3.2 |
|
||||||
|
| Concurrent clients | 10 | 100+ | NFR-3.6 |
|
||||||
|
| Mount time scaling | O(1) | O(1) | NFR-3.3 |
|
||||||
|
|
||||||
|
**Design Response:**
|
||||||
|
- Lazy tree loading → mount time independent of size
|
||||||
|
- SQLite indexes → O(log n) regardless of scale
|
||||||
|
- Streaming readdir → handle large directories
|
||||||
|
- Connection pooling → support many clients
|
||||||
|
|
||||||
|
#### 3.2.4 Resource Requirements
|
||||||
|
|
||||||
|
| Resource | Idle | Active (1K files) | Peak | Requirement |
|
||||||
|
|----------|------|-------------------|------|-------------|
|
||||||
|
| Memory | <50 MB | <200 MB | <500 MB | NFR-4.1-4.3 |
|
||||||
|
| Per-file overhead | - | <1 KB | - | NFR-4.4 |
|
||||||
|
| Metadata cache | - | 100 MB default | configurable | NFR-5.1 |
|
||||||
|
| Content cache | - | 10 GB default | configurable | NFR-5.2 |
|
||||||
|
|
||||||
|
**Design Response:**
|
||||||
|
- Streaming reads → never load full file in memory
|
||||||
|
- Content-addressed chunks → bounded cache with LRU eviction
|
||||||
|
- Metadata in SQLite → minimal per-file RAM overhead
|
||||||
|
|
||||||
|
#### 3.2.5 Efficiency Requirements
|
||||||
|
|
||||||
|
| Metric | Target | Requirement |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| Delta sync bandwidth reduction | >90% | NFR-6.4 |
|
||||||
|
| Cache hit rate (warm) | >95% | Derived |
|
||||||
|
| Deduplication ratio | >10% typical | FR-20 |
|
||||||
|
|
||||||
|
**Design Response:**
|
||||||
|
- CDC chunking → stable boundaries, minimal re-transfer
|
||||||
|
- Content-addressable storage → automatic deduplication
|
||||||
|
- Prefetch engine → anticipate access patterns
|
||||||
|
|
||||||
|
#### 3.2.6 Reliability Requirements
|
||||||
|
|
||||||
|
| Scenario | Behavior | Requirement |
|
||||||
|
|----------|----------|-------------|
|
||||||
|
| Origin offline | Serve cached data | NFR-7.1 |
|
||||||
|
| Network failure | Graceful degradation, no crash | NFR-7.2 |
|
||||||
|
| Failed operation | Retry with backoff (100ms, 500ms, 2s) | NFR-7.3 |
|
||||||
|
| Malformed audio | Skip file, log error, don't crash | NFR-7.4 |
|
||||||
|
| Chunk corruption | Detect via checksum, re-fetch | NFR-8.1, NFR-8.4 |
|
||||||
|
| Interrupted sync | Resume from last good state | NFR-8.3 |
|
||||||
|
| Unclean unmount | Recover on next mount | NFR-8.2 |
|
||||||
|
|
||||||
|
**Design Response:**
|
||||||
|
- Cache-first architecture → offline operation by default
|
||||||
|
- Origin federation with health checks → survive single origin failure
|
||||||
|
- xxHash checksums on all chunks → detect corruption
|
||||||
|
- WAL mode SQLite → ACID transactions, crash recovery
|
||||||
|
|
||||||
|
#### 3.2.7 Concurrent Access Requirements
|
||||||
|
|
||||||
|
| Scenario | Limit | Latency Impact | Requirement |
|
||||||
|
|----------|-------|----------------|-------------|
|
||||||
|
| Simultaneous open files | >1000 handles | None | NFR-2.4 |
|
||||||
|
| Parallel read ops | >100 concurrent | <2x p99 latency | Derived |
|
||||||
|
| Multiple clients | >10 (target 100+) | Linear degradation | NFR-3.6 |
|
||||||
|
| Readdir during sync | No blocking | Serve stale if needed | FR-9.2 |
|
||||||
|
|
||||||
|
**Design Response:**
|
||||||
|
- Async I/O (tokio) → non-blocking operations
|
||||||
|
- No GIL → true parallelism across cores
|
||||||
|
- Read-write locks on cache → readers don't block readers
|
||||||
|
- Stale-while-revalidate → serve cached during refresh
|
||||||
|
|
||||||
|
### 3.3 Non-Goals
|
||||||
|
|
||||||
|
| ID | Non-Goal | Rationale |
|
||||||
|
|----|----------|-----------|
|
||||||
|
| NG1 | Write to origin files | Read-only by design; preserves originals |
|
||||||
|
| NG2 | Transcoding | Out of scope for MVP; plugin possible later |
|
||||||
|
| NG3 | Video file support | Focus on audio; deferred to future |
|
||||||
|
| NG4 | Distributed/clustered mode | Single-node for MVP; architecture supports later |
|
||||||
|
| NG5 | Mobile app | CLI/daemon only; filesystem interface |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Proposed Design
|
||||||
|
|
||||||
|
### 4.1 High-Level Architecture
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
!theme plain
|
||||||
|
skinparam componentStyle rectangle
|
||||||
|
|
||||||
|
package "User Space" {
|
||||||
|
[Media Players\n(mpv, VLC, Plex)] as Apps
|
||||||
|
|
||||||
|
package "MusicFS Daemon" {
|
||||||
|
[FUSE Interface] as FUSE
|
||||||
|
[Control API] as Control
|
||||||
|
[Metrics] as Metrics
|
||||||
|
|
||||||
|
package "Core Services" {
|
||||||
|
[Virtual Path\nResolver] as VPR
|
||||||
|
[Event Bus] as Events
|
||||||
|
[Search Engine\n(tantivy)] as Search
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Plugin Host" {
|
||||||
|
[Origin\nPlugins] as OriginPlugins
|
||||||
|
[Metadata\nPlugins] as MetaPlugins
|
||||||
|
[Format\nPlugins] as FormatPlugins
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Storage Layer" {
|
||||||
|
[Content-Addressable\nStore (CAS)] as CAS
|
||||||
|
database "SQLite\n(metadata)" as SQLite
|
||||||
|
database "sled\n(chunks)" as Sled
|
||||||
|
}
|
||||||
|
|
||||||
|
[Origin\nFederation] as Federation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Origins (Read-Only)" {
|
||||||
|
[Local FS] as Local
|
||||||
|
[NFS] as NFS
|
||||||
|
[S3] as S3
|
||||||
|
[SFTP] as SFTP
|
||||||
|
}
|
||||||
|
|
||||||
|
Apps --> FUSE : POSIX
|
||||||
|
FUSE --> VPR
|
||||||
|
VPR --> Events
|
||||||
|
VPR --> Search
|
||||||
|
VPR --> CAS
|
||||||
|
CAS --> SQLite
|
||||||
|
CAS --> Sled
|
||||||
|
VPR --> Federation
|
||||||
|
Federation --> OriginPlugins
|
||||||
|
OriginPlugins --> Local
|
||||||
|
OriginPlugins --> NFS
|
||||||
|
OriginPlugins --> S3
|
||||||
|
OriginPlugins --> SFTP
|
||||||
|
Control --> Events
|
||||||
|
Metrics --> Events
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Component Overview
|
||||||
|
|
||||||
|
| Component | Responsibility | Technology |
|
||||||
|
|-----------|---------------|------------|
|
||||||
|
| FUSE Interface | Translate POSIX ops to internal calls | fuser (Rust) |
|
||||||
|
| Virtual Path Resolver | Map virtual ↔ real paths | Custom |
|
||||||
|
| Event Bus | Decouple components, enable observability | tokio broadcast |
|
||||||
|
| Search Engine | Full-text metadata search | tantivy |
|
||||||
|
| Plugin Host | Load/manage plugins | Native + WASM |
|
||||||
|
| CAS | Content-addressed chunk storage | Custom + sled |
|
||||||
|
| Origin Federation | Multi-origin routing with failover | Custom |
|
||||||
|
|
||||||
|
### 4.3 Detailed Design
|
||||||
|
|
||||||
|
#### 4.3.1 Virtual Path Resolution
|
||||||
|
|
||||||
|
The resolver maps metadata-based virtual paths to real origin paths.
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
!theme plain
|
||||||
|
|
||||||
|
participant "FUSE" as F
|
||||||
|
participant "VirtualPathResolver" as VPR
|
||||||
|
participant "MetadataIndex" as MI
|
||||||
|
participant "TreeCache" as TC
|
||||||
|
participant "OriginFederation" as OF
|
||||||
|
|
||||||
|
F -> VPR : lookup("/Metallica/72 Seasons/01.flac")
|
||||||
|
VPR -> TC : get_cached(path)
|
||||||
|
alt cache hit
|
||||||
|
TC --> VPR : CachedEntry
|
||||||
|
else cache miss
|
||||||
|
VPR -> MI : query(artist="Metallica", album="72 Seasons", track=1)
|
||||||
|
MI --> VPR : FileRecord { origin_id, real_path, metadata }
|
||||||
|
VPR -> TC : store(path, entry)
|
||||||
|
end
|
||||||
|
VPR -> OF : resolve_origin(origin_id)
|
||||||
|
OF --> VPR : OriginHandle
|
||||||
|
VPR --> F : ResolvedPath { origin, real_path, inode }
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Path Template Grammar:**
|
||||||
|
```
|
||||||
|
template = segment ("/" segment)*
|
||||||
|
segment = (literal | variable)+
|
||||||
|
variable = "$" identifier
|
||||||
|
identifier = "artist" | "album" | "title" | "track" | "year" | "genre"
|
||||||
|
| "format" | "format_upper" | "disc"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Default Template:**
|
||||||
|
```
|
||||||
|
$artist/$album ($year) [$format_upper]/$track - $title.$format
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.3.2 Content-Addressable Store (CAS)
|
||||||
|
|
||||||
|
All file content is stored as content-addressed chunks, enabling deduplication
|
||||||
|
and efficient delta sync.
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
!theme plain
|
||||||
|
|
||||||
|
package "Content-Addressable Store" {
|
||||||
|
component "Chunk Manager" as CM
|
||||||
|
component "CDC Chunker\n(FastCDC)" as CDC
|
||||||
|
component "Hash Index\n(xxHash64)" as Hash
|
||||||
|
|
||||||
|
database "Chunk Files\n~/.cache/musicfs/chunks/" as Chunks
|
||||||
|
database "Index DB\n(sled)" as Index
|
||||||
|
|
||||||
|
CM --> CDC : chunk data
|
||||||
|
CDC --> Hash : compute hash
|
||||||
|
Hash --> Index : store hash → location
|
||||||
|
CM --> Chunks : write chunk file
|
||||||
|
}
|
||||||
|
|
||||||
|
note right of CDC
|
||||||
|
Avg chunk: 64KB
|
||||||
|
Min: 16KB, Max: 256KB
|
||||||
|
Stable boundaries for delta sync
|
||||||
|
end note
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Chunk Storage Layout:**
|
||||||
|
```
|
||||||
|
~/.cache/musicfs/
|
||||||
|
├── chunks/
|
||||||
|
│ ├── aa/
|
||||||
|
│ │ ├── aa1b2c3d4e5f6789... (64KB chunk)
|
||||||
|
│ │ └── aa9f8e7d6c5b4a32...
|
||||||
|
│ ├── ab/
|
||||||
|
│ └── ... (256 subdirs for distribution)
|
||||||
|
├── metadata.db (SQLite: file metadata, tree cache)
|
||||||
|
├── search.idx/ (tantivy: full-text index)
|
||||||
|
└── chunks.sled/ (sled: hash → chunk location)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.3.3 Origin Federation
|
||||||
|
|
||||||
|
Multiple origins are managed with priority-based routing and health tracking.
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
!theme plain
|
||||||
|
|
||||||
|
participant "VirtualPathResolver" as VPR
|
||||||
|
participant "OriginFederation" as OF
|
||||||
|
participant "HealthChecker" as HC
|
||||||
|
participant "Origin[Local]" as O1
|
||||||
|
participant "Origin[NFS]" as O2
|
||||||
|
participant "Origin[S3]" as O3
|
||||||
|
|
||||||
|
VPR -> OF : read(real_path, offset, size)
|
||||||
|
OF -> OF : select_origin(priority, health)
|
||||||
|
|
||||||
|
alt Origin[Local] healthy (pri=1)
|
||||||
|
OF -> O1 : read()
|
||||||
|
O1 --> OF : data
|
||||||
|
else Origin[Local] unhealthy, try NFS (pri=2)
|
||||||
|
OF -> O2 : read()
|
||||||
|
alt success
|
||||||
|
O2 --> OF : data
|
||||||
|
else failure
|
||||||
|
OF -> O3 : read()
|
||||||
|
O3 --> OF : data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
OF --> VPR : data
|
||||||
|
|
||||||
|
note over HC
|
||||||
|
Background health checks
|
||||||
|
every 30s per origin
|
||||||
|
end note
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Origin Configuration:**
|
||||||
|
```toml
|
||||||
|
[[origins]]
|
||||||
|
id = "local"
|
||||||
|
type = "local"
|
||||||
|
path = "/mnt/nas/music"
|
||||||
|
priority = 1
|
||||||
|
|
||||||
|
[[origins]]
|
||||||
|
id = "backup"
|
||||||
|
type = "s3"
|
||||||
|
bucket = "music-backup"
|
||||||
|
priority = 2
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.3.4 Plugin System
|
||||||
|
|
||||||
|
Plugins extend functionality without modifying core code.
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
!theme plain
|
||||||
|
|
||||||
|
interface "Plugin" {
|
||||||
|
+name(): String
|
||||||
|
+version(): Version
|
||||||
|
+init(config)
|
||||||
|
+shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface "OriginPlugin" {
|
||||||
|
+list_dir(path): Vec<DirEntry>
|
||||||
|
+read(path, offset, size): Vec<u8>
|
||||||
|
+stat(path): FileStat
|
||||||
|
+watch(path, callback): WatchHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
interface "MetadataPlugin" {
|
||||||
|
+extract(data, format): Metadata
|
||||||
|
+can_handle(format): bool
|
||||||
|
}
|
||||||
|
|
||||||
|
interface "FormatPlugin" {
|
||||||
|
+extensions(): Vec<String>
|
||||||
|
+parse_header(data): AudioHeader
|
||||||
|
+synthesize_header(metadata): Vec<u8>
|
||||||
|
}
|
||||||
|
|
||||||
|
Plugin <|-- OriginPlugin
|
||||||
|
Plugin <|-- MetadataPlugin
|
||||||
|
Plugin <|-- FormatPlugin
|
||||||
|
|
||||||
|
class "LocalFSPlugin" implements OriginPlugin
|
||||||
|
class "S3Plugin" implements OriginPlugin
|
||||||
|
class "SymphoniaPlugin" implements MetadataPlugin
|
||||||
|
class "FlacPlugin" implements FormatPlugin
|
||||||
|
class "Mp3Plugin" implements FormatPlugin
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Plugin Loading:**
|
||||||
|
1. **Built-in:** Compiled into binary (Local, S3, SFTP, symphonia)
|
||||||
|
2. **Native:** Dynamic libraries (`.so`/`.dylib`) loaded at runtime
|
||||||
|
3. **WASM:** Sandboxed plugins via wasmtime (future)
|
||||||
|
|
||||||
|
#### 4.3.5 Data Flow: Read Operation
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
!theme plain
|
||||||
|
|
||||||
|
|FUSE|
|
||||||
|
start
|
||||||
|
:receive read(path, offset, size);
|
||||||
|
|
||||||
|
|VirtualPathResolver|
|
||||||
|
:resolve virtual path to real path;
|
||||||
|
:lookup file metadata;
|
||||||
|
|
||||||
|
|CAS|
|
||||||
|
:compute chunk range for [offset, offset+size];
|
||||||
|
if (all chunks cached?) then (yes)
|
||||||
|
:read from local chunk files;
|
||||||
|
else (no)
|
||||||
|
|OriginFederation|
|
||||||
|
:select healthy origin by priority;
|
||||||
|
:fetch missing byte range;
|
||||||
|
|CAS|
|
||||||
|
:chunk fetched data (CDC);
|
||||||
|
:store chunks by hash;
|
||||||
|
:update chunk manifest;
|
||||||
|
endif
|
||||||
|
|
||||||
|
|EventBus|
|
||||||
|
:emit FileAccessed event;
|
||||||
|
|
||||||
|
|FUSE|
|
||||||
|
:return data to application;
|
||||||
|
stop
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.3.6 Data Schema
|
||||||
|
|
||||||
|
**Metadata Index (SQLite):**
|
||||||
|
```sql
|
||||||
|
CREATE TABLE files (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
origin_id TEXT NOT NULL,
|
||||||
|
real_path TEXT NOT NULL,
|
||||||
|
virtual_path TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- Metadata (see FR-6 in requirements.md)
|
||||||
|
title TEXT,
|
||||||
|
artist TEXT,
|
||||||
|
album TEXT,
|
||||||
|
album_artist TEXT,
|
||||||
|
genre TEXT,
|
||||||
|
year INTEGER,
|
||||||
|
track INTEGER,
|
||||||
|
disc INTEGER,
|
||||||
|
duration_ms INTEGER,
|
||||||
|
bitrate INTEGER,
|
||||||
|
sample_rate INTEGER,
|
||||||
|
format TEXT,
|
||||||
|
|
||||||
|
-- Sync state
|
||||||
|
origin_mtime INTEGER,
|
||||||
|
origin_size INTEGER,
|
||||||
|
content_hash TEXT,
|
||||||
|
chunk_manifest BLOB, -- msgpack: [(chunk_hash, offset, size)]
|
||||||
|
last_sync INTEGER,
|
||||||
|
|
||||||
|
UNIQUE(origin_id, real_path)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_virtual ON files(virtual_path);
|
||||||
|
CREATE INDEX idx_artist_album ON files(artist, album);
|
||||||
|
CREATE INDEX idx_content_hash ON files(content_hash);
|
||||||
|
|
||||||
|
CREATE TABLE artwork (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
file_id INTEGER REFERENCES files(id),
|
||||||
|
art_type TEXT, -- 'front', 'back'
|
||||||
|
chunk_hash TEXT, -- reference to CAS
|
||||||
|
width INTEGER,
|
||||||
|
height INTEGER,
|
||||||
|
UNIQUE(file_id, art_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE collections (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT UNIQUE,
|
||||||
|
query_json TEXT, -- smart collection query
|
||||||
|
created_at INTEGER
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.3.7 Control API
|
||||||
|
|
||||||
|
**Unix Socket Protocol (JSON-RPC 2.0):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
// Request: Get cache statistics
|
||||||
|
{"jsonrpc": "2.0", "method": "cache.stats", "id": 1}
|
||||||
|
|
||||||
|
// Response
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"result": {
|
||||||
|
"hits": 15234,
|
||||||
|
"misses": 421,
|
||||||
|
"hit_rate": 0.973,
|
||||||
|
"chunks_stored": 84521,
|
||||||
|
"chunks_unique": 71203,
|
||||||
|
"dedup_ratio": 0.157,
|
||||||
|
"size_bytes": 5368709120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request: Search
|
||||||
|
{"jsonrpc": "2.0", "method": "search", "params": {"query": "metallica"}, "id": 2}
|
||||||
|
|
||||||
|
// Request: Refresh origin
|
||||||
|
{"jsonrpc": "2.0", "method": "origin.rescan", "params": {"id": "local"}, "id": 3}
|
||||||
|
```
|
||||||
|
|
||||||
|
**CLI Interface:**
|
||||||
|
```bash
|
||||||
|
musicfs mount /mnt/music # Mount filesystem
|
||||||
|
musicfs status # Show daemon status
|
||||||
|
musicfs cache stats # Cache statistics
|
||||||
|
musicfs cache clear --origin=local # Clear cache for origin
|
||||||
|
musicfs search "metallica heavy" # Search library
|
||||||
|
musicfs origin list # List origins and health
|
||||||
|
musicfs origin rescan local # Force rescan
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Cross-Cutting Concerns
|
||||||
|
|
||||||
|
### 5.1 Security & Privacy
|
||||||
|
|
||||||
|
| Concern | Mitigation |
|
||||||
|
|---------|------------|
|
||||||
|
| Credential storage | Use system keyring (secret-service) or env vars; never in config file |
|
||||||
|
| Credential exposure | Redact from logs; exclude from `/proc/cmdline` |
|
||||||
|
| Cache at rest | Optional encryption via age/libsodium (P3 requirement) |
|
||||||
|
| Plugin sandboxing | WASM plugins run in wasmtime sandbox; native plugins require trust |
|
||||||
|
| Access control | Respect origin permissions; run as unprivileged user |
|
||||||
|
| No PII handling | Filesystem metadata only; no user data collected |
|
||||||
|
|
||||||
|
### 5.2 Observability
|
||||||
|
|
||||||
|
**Metrics (Prometheus format):**
|
||||||
|
```
|
||||||
|
musicfs_fuse_ops_total{op="read"} 152341
|
||||||
|
musicfs_fuse_ops_total{op="readdir"} 8234
|
||||||
|
musicfs_fuse_latency_seconds{op="read",quantile="0.99"} 0.004
|
||||||
|
musicfs_cache_hits_total 142107
|
||||||
|
musicfs_cache_misses_total 10234
|
||||||
|
musicfs_cache_size_bytes 5368709120
|
||||||
|
musicfs_origin_health{origin="local"} 1
|
||||||
|
musicfs_origin_health{origin="s3"} 0
|
||||||
|
musicfs_sync_files_changed{origin="local"} 15
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logging Levels:**
|
||||||
|
| Level | Content |
|
||||||
|
|-------|---------|
|
||||||
|
| ERROR | Unrecoverable failures, data corruption |
|
||||||
|
| WARN | Recoverable failures, origin timeouts |
|
||||||
|
| INFO | Mount/unmount, sync completion, config reload |
|
||||||
|
| DEBUG | Cache hits/misses, origin selection |
|
||||||
|
| TRACE | Individual FUSE operations, chunk I/O |
|
||||||
|
|
||||||
|
**Golden Signals Dashboard:**
|
||||||
|
1. **Latency:** p50/p95/p99 for read, stat, readdir
|
||||||
|
2. **Traffic:** FUSE ops/sec, bytes read/sec
|
||||||
|
3. **Errors:** Origin failures, cache corruption
|
||||||
|
4. **Saturation:** Cache fullness, open file handles
|
||||||
|
|
||||||
|
### 5.3 Scalability & Performance
|
||||||
|
|
||||||
|
**Expected Load:**
|
||||||
|
| Metric | Target | Maximum |
|
||||||
|
|--------|--------|---------|
|
||||||
|
| Library size | 1M files | 10M files |
|
||||||
|
| Concurrent clients | 10 | 100+ |
|
||||||
|
| FUSE ops/sec | 1,000 | 10,000 |
|
||||||
|
| Read throughput | 500 MB/s | 1 GB/s |
|
||||||
|
|
||||||
|
**Scaling Strategy:**
|
||||||
|
- **Horizontal:** Not supported (single daemon per mountpoint)
|
||||||
|
- **Vertical:** Increase cache size, add origins
|
||||||
|
|
||||||
|
**Resource Requirements:**
|
||||||
|
| Resource | Minimum | Recommended |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| CPU | 1 core | 4 cores |
|
||||||
|
| RAM | 256 MB | 2 GB |
|
||||||
|
| Disk (cache) | 1 GB | 50 GB |
|
||||||
|
| Network | 10 Mbps | 1 Gbps |
|
||||||
|
|
||||||
|
### 5.4 Testing Plan
|
||||||
|
|
||||||
|
| Test Type | Scope | Tools |
|
||||||
|
|-----------|-------|-------|
|
||||||
|
| Unit | Individual components | cargo test |
|
||||||
|
| Integration | Component interaction | cargo test --features integration |
|
||||||
|
| E2E | Full FUSE operations | pytest + real mount |
|
||||||
|
| Performance | Latency, throughput | criterion.rs, custom benchmarks |
|
||||||
|
| Stress | High load, large libraries | locust, custom generators |
|
||||||
|
| Chaos | Origin failures, network issues | toxiproxy |
|
||||||
|
|
||||||
|
**Test Matrix:**
|
||||||
|
```
|
||||||
|
Origins: [local, s3, sftp] × [healthy, degraded, offline]
|
||||||
|
Cache: [cold, warm, full]
|
||||||
|
Library: [100, 10K, 1M, 10M] files
|
||||||
|
Operations: [mount, readdir, stat, read, search]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Alternatives Considered
|
||||||
|
|
||||||
|
### 6.1 Alternative A: Extend beetfs (Python)
|
||||||
|
|
||||||
|
**Description:** Fix bugs in existing beetfs, add features incrementally.
|
||||||
|
|
||||||
|
**Rejected Because:**
|
||||||
|
- Python GIL fundamentally limits concurrency
|
||||||
|
- Python 2.7 EOL; migration to Python 3 substantial
|
||||||
|
- Architecture (full file in RAM) requires rewrite anyway
|
||||||
|
- No async I/O support in fuse-python
|
||||||
|
|
||||||
|
### 6.2 Alternative B: Use rclone mount
|
||||||
|
|
||||||
|
**Description:** Use rclone's FUSE mount with VFS caching.
|
||||||
|
|
||||||
|
**Rejected Because:**
|
||||||
|
- No metadata-based virtual path organization
|
||||||
|
- No metadata overlay functionality
|
||||||
|
- Limited plugin extensibility
|
||||||
|
- Would require forking and heavy modification
|
||||||
|
|
||||||
|
### 6.3 Alternative C: Build as Plex/Jellyfin Plugin
|
||||||
|
|
||||||
|
**Description:** Extend existing media server with virtual filesystem view.
|
||||||
|
|
||||||
|
**Rejected Because:**
|
||||||
|
- Tied to specific media server
|
||||||
|
- Not a true filesystem (no POSIX interface)
|
||||||
|
- Heavy runtime dependency
|
||||||
|
- Different use case (streaming vs filesystem)
|
||||||
|
|
||||||
|
### 6.4 Alternative D: Go Implementation
|
||||||
|
|
||||||
|
**Description:** Implement in Go using go-fuse.
|
||||||
|
|
||||||
|
**Considered Trade-offs:**
|
||||||
|
| Aspect | Rust | Go |
|
||||||
|
|--------|------|-----|
|
||||||
|
| Memory safety | Compile-time | GC pauses |
|
||||||
|
| Concurrency | async/await, no GC | goroutines, GC |
|
||||||
|
| FUSE library | fuser (mature) | go-fuse (mature) |
|
||||||
|
| Learning curve | Steeper | Gentler |
|
||||||
|
| Binary size | Smaller | Larger |
|
||||||
|
|
||||||
|
**Decision:** Rust chosen for zero-cost abstractions, no GC pauses during I/O,
|
||||||
|
and better fit for systems programming.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Implementation Plan
|
||||||
|
|
||||||
|
### 7.1 Phase 1: MVP (4 weeks)
|
||||||
|
|
||||||
|
**Goal:** Basic functional filesystem with single origin.
|
||||||
|
|
||||||
|
| Week | Deliverables |
|
||||||
|
|------|--------------|
|
||||||
|
| 1 | Project setup, FUSE skeleton, local origin plugin |
|
||||||
|
| 2 | Metadata extraction (symphonia), SQLite schema |
|
||||||
|
| 3 | Virtual path resolver, tree cache, basic readdir/stat/read |
|
||||||
|
| 4 | CAS implementation, chunk caching, integration tests |
|
||||||
|
|
||||||
|
**Exit Criteria:**
|
||||||
|
- Mount and browse local music library
|
||||||
|
- Play audio files through mounted filesystem
|
||||||
|
- Cache persists across restarts
|
||||||
|
|
||||||
|
### 7.2 Phase 2: Delta Sync & Multi-Origin (3 weeks)
|
||||||
|
|
||||||
|
**Goal:** Efficient synchronization and origin federation.
|
||||||
|
|
||||||
|
| Week | Deliverables |
|
||||||
|
|------|--------------|
|
||||||
|
| 5 | CDC chunking (FastCDC), delta detection |
|
||||||
|
| 6 | Origin federation, priority routing, health checks |
|
||||||
|
| 7 | S3 origin plugin, SFTP origin plugin |
|
||||||
|
|
||||||
|
**Exit Criteria:**
|
||||||
|
- Delta sync achieves >90% bandwidth reduction
|
||||||
|
- Automatic failover between origins
|
||||||
|
- Remote origins functional
|
||||||
|
|
||||||
|
### 7.3 Phase 3: Search & Smart Features (2 weeks)
|
||||||
|
|
||||||
|
**Goal:** Full-text search and intelligent caching.
|
||||||
|
|
||||||
|
| Week | Deliverables |
|
||||||
|
|------|--------------|
|
||||||
|
| 8 | tantivy integration, search indexing, `/.search/` virtual dir |
|
||||||
|
| 9 | Smart collections, prefetch engine, access pattern learning |
|
||||||
|
|
||||||
|
**Exit Criteria:**
|
||||||
|
- Search returns results in <1s for 1M tracks
|
||||||
|
- Prefetch reduces cache misses by >50%
|
||||||
|
|
||||||
|
### 7.4 Phase 4: Plugin System & Polish (2 weeks)
|
||||||
|
|
||||||
|
**Goal:** Extensibility and production readiness.
|
||||||
|
|
||||||
|
| Week | Deliverables |
|
||||||
|
|------|--------------|
|
||||||
|
| 10 | Plugin host, plugin API stabilization, example plugins |
|
||||||
|
| 11 | Control API, metrics, documentation, packaging |
|
||||||
|
|
||||||
|
**Exit Criteria:**
|
||||||
|
- Custom origin plugin loadable at runtime
|
||||||
|
- Prometheus metrics exported
|
||||||
|
- systemd service functional
|
||||||
|
|
||||||
|
### 7.5 Rollout Strategy
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml
|
||||||
|
!theme plain
|
||||||
|
|
||||||
|
[*] --> Alpha
|
||||||
|
Alpha --> Beta : Internal testing complete
|
||||||
|
Beta --> GA : Community testing complete
|
||||||
|
|
||||||
|
state Alpha {
|
||||||
|
[*] --> DevTesting
|
||||||
|
DevTesting --> DogFood : Core features work
|
||||||
|
}
|
||||||
|
|
||||||
|
state Beta {
|
||||||
|
[*] --> LimitedRelease
|
||||||
|
LimitedRelease --> PublicBeta : No critical bugs
|
||||||
|
}
|
||||||
|
|
||||||
|
state GA {
|
||||||
|
[*] --> Stable
|
||||||
|
}
|
||||||
|
|
||||||
|
note right of Alpha : 2-4 weeks\nDevelopers only
|
||||||
|
note right of Beta : 4-8 weeks\nEarly adopters
|
||||||
|
note right of GA : Stable releases
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Feature Flags:**
|
||||||
|
```toml
|
||||||
|
[features]
|
||||||
|
search_enabled = true
|
||||||
|
smart_collections = false # Beta
|
||||||
|
wasm_plugins = false # Experimental
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rollback:** Binary replacement + cache clear; no data migration needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Glossary & References
|
||||||
|
|
||||||
|
### 8.1 Glossary
|
||||||
|
|
||||||
|
| Term | Definition |
|
||||||
|
|------|------------|
|
||||||
|
| **CAS** | Content-Addressable Store; data stored/retrieved by hash |
|
||||||
|
| **CDC** | Content-Defined Chunking; chunking with stable boundaries |
|
||||||
|
| **FUSE** | Filesystem in Userspace; kernel interface for user-space filesystems |
|
||||||
|
| **Origin** | Source storage backend (local, S3, NFS, etc.) |
|
||||||
|
| **Virtual Path** | Metadata-derived path shown to users |
|
||||||
|
| **Real Path** | Actual path on origin storage |
|
||||||
|
|
||||||
|
### 8.2 References
|
||||||
|
|
||||||
|
| Document | Link |
|
||||||
|
|----------|------|
|
||||||
|
| Requirements Specification | [requirements.md](requirements.md) |
|
||||||
|
| beetfs (Original) | [beetsplug/beetFs.py](../../beetsplug/beetFs.py) |
|
||||||
|
| beetfs Features | [v1/features.md](../v1/features.md) |
|
||||||
|
| fuser (Rust FUSE) | https://github.com/cberner/fuser |
|
||||||
|
| tantivy (Search) | https://github.com/quickwit-oss/tantivy |
|
||||||
|
| symphonia (Audio) | https://github.com/pdrat/symphonia |
|
||||||
|
| FastCDC | https://github.com/nlfiedler/fastcdc-rs |
|
||||||
|
| wasmtime | https://wasmtime.dev/ |
|
||||||
|
|
||||||
|
### 8.3 Dependencies
|
||||||
|
|
||||||
|
| Crate | Version | Purpose |
|
||||||
|
|-------|---------|---------|
|
||||||
|
| fuser | 0.14+ | FUSE interface |
|
||||||
|
| tokio | 1.x | Async runtime |
|
||||||
|
| rusqlite | 0.31+ | SQLite bindings |
|
||||||
|
| sled | 0.34+ | Embedded key-value store |
|
||||||
|
| tantivy | 0.21+ | Full-text search |
|
||||||
|
| symphonia | 0.5+ | Audio metadata extraction |
|
||||||
|
| fastcdc | 3.x | Content-defined chunking |
|
||||||
|
| xxhash-rust | 0.8+ | Fast hashing |
|
||||||
|
| serde | 1.x | Serialization |
|
||||||
|
| toml | 0.8+ | Configuration |
|
||||||
|
| tracing | 0.1+ | Logging/instrumentation |
|
||||||
|
| metrics | 0.22+ | Prometheus metrics |
|
||||||
@@ -0,0 +1,649 @@
|
|||||||
|
# Music Library FUSE Filesystem - Requirements Specification
|
||||||
|
|
||||||
|
**Version**: 1.0
|
||||||
|
**Date**: 2026-05-12
|
||||||
|
**Status**: Draft
|
||||||
|
|
||||||
|
## 1. Introduction
|
||||||
|
|
||||||
|
### 1.1 Purpose
|
||||||
|
|
||||||
|
This document specifies the requirements for a FUSE-based virtual filesystem that presents a music library organized by metadata. The system overlays metadata onto audio files without modifying originals and operates as a read-only client against the origin storage.
|
||||||
|
|
||||||
|
### 1.2 Scope
|
||||||
|
|
||||||
|
The system provides:
|
||||||
|
- Virtual filesystem accessible via standard POSIX operations
|
||||||
|
- Metadata-based directory structure (artist/album/track)
|
||||||
|
- Local caching with delta synchronization
|
||||||
|
- Support for local and remote origin storage
|
||||||
|
|
||||||
|
### 1.3 Definitions
|
||||||
|
|
||||||
|
| Term | Definition |
|
||||||
|
|------|------------|
|
||||||
|
| **Origin** | The source storage containing original audio files (local FS, NFS, S3, etc.) |
|
||||||
|
| **Virtual path** | The metadata-derived path shown to users (e.g., `/Artist/Album/Track.flac`) |
|
||||||
|
| **Real path** | The actual path on origin storage |
|
||||||
|
| **Metadata overlay** | Serving synthesized file headers from cached metadata |
|
||||||
|
| **CDC** | Content-Defined Chunking - algorithm for stable file segmentation |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. System Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ User Applications │
|
||||||
|
│ (mpv, Rhythmbox, Plex, etc.) │
|
||||||
|
└─────────────────────────────┬───────────────────────────────────┘
|
||||||
|
│ POSIX (read-only)
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ FUSE Interface │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ Plugin Host │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ Origin │ │ Metadata │ │ Format │ │
|
||||||
|
│ │ Plugins │ │ Plugins │ │ Plugins │ │
|
||||||
|
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ Core Services │
|
||||||
|
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
|
||||||
|
│ │ Virtual │ │ Event │ │ Search │ │ Control │ │
|
||||||
|
│ │ Path │ │ Bus │ │ Index │ │ API │ │
|
||||||
|
│ │ Resolver │ │ │ │ │ │ │ │
|
||||||
|
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ Storage Layer │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Content-Addressable Chunk Store │ │
|
||||||
|
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
||||||
|
│ │ │ Metadata │ │ Content │ │ Tree │ │ │
|
||||||
|
│ │ │ Cache │ │ Chunks │ │ Cache │ │ │
|
||||||
|
│ │ │ (SQLite) │ │ (CAS) │ │ │ │ │
|
||||||
|
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ Origin Federation │
|
||||||
|
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||||
|
│ │ Local │ │ NFS │ │ S3 │ │ SFTP │ │
|
||||||
|
│ │ FS │ │ │ │ │ │ │ │
|
||||||
|
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│ read-only
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Origin Storage(s) │
|
||||||
|
│ (original audio files) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Functional Requirements
|
||||||
|
|
||||||
|
### 3.1 Filesystem Operations
|
||||||
|
|
||||||
|
#### FR-1: Mount/Unmount
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-1.1 | The system SHALL mount as a FUSE filesystem at a user-specified mountpoint |
|
||||||
|
| FR-1.2 | The system SHALL return control to the caller within 500ms of mount initiation |
|
||||||
|
| FR-1.3 | The system SHALL unmount cleanly via `fusermount -u` |
|
||||||
|
| FR-1.4 | The system SHALL release all resources (file handles, connections) on unmount |
|
||||||
|
|
||||||
|
#### FR-2: Directory Operations
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-2.1 | The system SHALL present files organized by metadata path format |
|
||||||
|
| FR-2.2 | The system SHALL support configurable path templates (e.g., `$artist/$album/$track - $title.$format`) |
|
||||||
|
| FR-2.3 | The system SHALL return directory listings via `readdir()` |
|
||||||
|
| FR-2.4 | The system SHALL support nested directory traversal to arbitrary depth |
|
||||||
|
| FR-2.5 | The system SHALL handle directories with 100,000+ entries |
|
||||||
|
|
||||||
|
#### FR-3: File Operations (Read)
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-3.1 | The system SHALL support `open()` for reading |
|
||||||
|
| FR-3.2 | The system SHALL support `read()` with arbitrary offset and size |
|
||||||
|
| FR-3.3 | The system SHALL support `seek()` operations for random access |
|
||||||
|
| FR-3.4 | The system SHALL return file attributes via `stat()` / `fstat()` |
|
||||||
|
| FR-3.5 | The system SHALL support concurrent reads from multiple processes |
|
||||||
|
|
||||||
|
#### FR-4: Read-Only Constraint
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-4.1 | The system SHALL NOT modify original files on the origin storage |
|
||||||
|
| FR-4.2 | The system SHALL NOT push any changes to the origin server |
|
||||||
|
| FR-4.3 | The system SHALL return `EROFS` (Read-only filesystem) for write operations |
|
||||||
|
| FR-4.4 | The system SHALL return `EROFS` for `create()`, `mkdir()`, `unlink()`, `rmdir()` |
|
||||||
|
| FR-4.5 | The system SHALL return `EROFS` for `rename()`, `chmod()`, `chown()`, `truncate()` |
|
||||||
|
|
||||||
|
### 3.2 Metadata Handling
|
||||||
|
|
||||||
|
#### FR-5: Metadata Overlay
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-5.1 | The system SHALL extract metadata from audio files on first access |
|
||||||
|
| FR-5.2 | The system SHALL cache extracted metadata in a local database |
|
||||||
|
| FR-5.3 | The system SHALL serve file headers with metadata from cache |
|
||||||
|
| FR-5.4 | The system SHALL support FLAC Vorbis comments |
|
||||||
|
| FR-5.5 | The system SHALL support MP3 ID3v2 tags |
|
||||||
|
| FR-5.6 | The system SHOULD support additional formats (OGG, M4A, OPUS) |
|
||||||
|
|
||||||
|
#### FR-6: Metadata Fields
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-6.1 | The system SHALL extract and cache: title, artist, album, genre |
|
||||||
|
| FR-6.2 | The system SHALL extract and cache: year, track number, disc number |
|
||||||
|
| FR-6.3 | The system SHALL extract and cache: duration, bitrate, sample rate |
|
||||||
|
| FR-6.4 | The system SHOULD extract: composer, album artist, lyrics |
|
||||||
|
| FR-6.5 | The system SHALL handle missing metadata gracefully with defaults |
|
||||||
|
|
||||||
|
### 3.3 Caching
|
||||||
|
|
||||||
|
#### FR-7: Metadata Cache
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-7.1 | The system SHALL persist metadata cache across restarts |
|
||||||
|
| FR-7.2 | The system SHALL store metadata in SQLite database |
|
||||||
|
| FR-7.3 | The system SHALL index by both virtual path and real path |
|
||||||
|
| FR-7.4 | The system SHALL invalidate cache entries when origin file changes |
|
||||||
|
|
||||||
|
#### FR-8: Content Cache
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-8.1 | The system SHALL cache file content in fixed-size chunks |
|
||||||
|
| FR-8.2 | The system SHALL use content-defined chunking for cache efficiency |
|
||||||
|
| FR-8.3 | The system SHALL store chunk hashes for delta detection |
|
||||||
|
| FR-8.4 | The system SHALL evict chunks under memory/disk pressure |
|
||||||
|
|
||||||
|
#### FR-9: Directory Tree Cache
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-9.1 | The system SHALL cache directory listings locally |
|
||||||
|
| FR-9.2 | The system SHALL serve `readdir()` from cache without origin access |
|
||||||
|
| FR-9.3 | The system SHALL refresh tree cache based on configurable policy |
|
||||||
|
| FR-9.4 | The system SHALL support forced refresh via signal or special file |
|
||||||
|
|
||||||
|
### 3.4 Synchronization
|
||||||
|
|
||||||
|
#### FR-10: Change Detection
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-10.1 | The system SHALL detect changes to origin files |
|
||||||
|
| FR-10.2 | The system SHALL use inotify for local filesystem origins |
|
||||||
|
| FR-10.3 | The system SHALL use polling for remote origins without push support |
|
||||||
|
| FR-10.4 | The system SHALL compare mtime and size for change detection |
|
||||||
|
| FR-10.5 | The system SHALL support content-hash verification on demand |
|
||||||
|
|
||||||
|
#### FR-11: Delta Sync
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-11.1 | The system SHALL download only changed portions of files |
|
||||||
|
| FR-11.2 | The system SHALL use CDC to identify changed chunks |
|
||||||
|
| FR-11.3 | The system SHALL preserve unchanged chunks in cache |
|
||||||
|
| FR-11.4 | The system SHALL handle file additions and deletions |
|
||||||
|
|
||||||
|
### 3.5 Origin Support
|
||||||
|
|
||||||
|
#### FR-12: Origin Types
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-12.1 | The system SHALL support local filesystem as origin |
|
||||||
|
| FR-12.2 | The system SHOULD support NFS mounted filesystems |
|
||||||
|
| FR-12.3 | The system SHOULD support SMB/CIFS shares |
|
||||||
|
| FR-12.4 | The system SHOULD support S3-compatible object storage |
|
||||||
|
| FR-12.5 | The system SHOULD support SFTP servers |
|
||||||
|
| FR-12.6 | The system SHALL provide pluggable origin interface |
|
||||||
|
|
||||||
|
#### FR-13: Multiple Origins [P0]
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-13.1 | The system SHALL support multiple simultaneous origins |
|
||||||
|
| FR-13.2 | The system SHALL present unified virtual tree across origins |
|
||||||
|
| FR-13.3 | The system SHALL support origin priority/preference ordering |
|
||||||
|
| FR-13.4 | The system SHALL handle duplicate files across origins |
|
||||||
|
| FR-13.5 | The system SHALL support per-origin configuration |
|
||||||
|
|
||||||
|
### 3.6 Search & Discovery
|
||||||
|
|
||||||
|
#### FR-14: Full-Text Search [P1]
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-14.1 | The system SHALL index metadata for full-text search |
|
||||||
|
| FR-14.2 | The system SHALL expose search via virtual directory (`/.search/query/`) |
|
||||||
|
| FR-14.3 | The system SHALL support fuzzy matching |
|
||||||
|
| FR-14.4 | The system SHOULD support search by audio fingerprint |
|
||||||
|
|
||||||
|
#### FR-15: Smart Collections [P1]
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-15.1 | The system SHALL support query-based virtual folders |
|
||||||
|
| FR-15.2 | The system SHALL support saved searches as directories |
|
||||||
|
| FR-15.3 | The system SHALL support dynamic playlists (recently played, most played) |
|
||||||
|
| FR-15.4 | The system SHOULD support user-defined metadata fields |
|
||||||
|
|
||||||
|
### 3.7 Album Art
|
||||||
|
|
||||||
|
#### FR-16: Cover Art Handling [P1]
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-16.1 | The system SHALL extract embedded album art |
|
||||||
|
| FR-16.2 | The system SHALL expose art as virtual files (`/Artist/Album/cover.jpg`) |
|
||||||
|
| FR-16.3 | The system SHALL cache artwork separately from audio |
|
||||||
|
| FR-16.4 | The system SHALL support multiple art sizes (thumbnail, medium, full) |
|
||||||
|
| FR-16.5 | The system SHOULD fetch missing art from online sources |
|
||||||
|
|
||||||
|
### 3.8 Control & API
|
||||||
|
|
||||||
|
#### FR-17: Control Interface [P0]
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-17.1 | The system SHALL expose control via Unix socket |
|
||||||
|
| FR-17.2 | The system SHOULD expose REST/gRPC API |
|
||||||
|
| FR-17.3 | The system SHALL support cache management commands (clear, refresh, stats) |
|
||||||
|
| FR-17.4 | The system SHALL support runtime configuration changes |
|
||||||
|
| FR-17.5 | The system SHALL support graceful shutdown with drain |
|
||||||
|
|
||||||
|
#### FR-18: Event System [P0]
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-18.1 | The system SHALL emit events for file access |
|
||||||
|
| FR-18.2 | The system SHALL support webhook notifications |
|
||||||
|
| FR-18.3 | The system SHOULD support event streaming (SSE/WebSocket) |
|
||||||
|
| FR-18.4 | The system SHALL log access patterns for analysis |
|
||||||
|
|
||||||
|
### 3.9 Caching Enhancements
|
||||||
|
|
||||||
|
#### FR-19: Intelligent Prefetching [P1]
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-19.1 | The system SHALL learn access patterns |
|
||||||
|
| FR-19.2 | The system SHALL support playlist-aware prefetching |
|
||||||
|
| FR-19.3 | The system SHOULD support time-based prefetching |
|
||||||
|
| FR-19.4 | The system SHALL support manual prefetch hints (`/.prefetch/path/`) |
|
||||||
|
|
||||||
|
#### FR-20: Content-Addressable Storage [P0]
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-20.1 | The system SHALL store chunks by content hash |
|
||||||
|
| FR-20.2 | The system SHALL detect identical files across library |
|
||||||
|
| FR-20.3 | The system SHALL report deduplication statistics |
|
||||||
|
| FR-20.4 | The system SHALL enable cache sharing via content addressing |
|
||||||
|
|
||||||
|
### 3.10 Integration
|
||||||
|
|
||||||
|
#### FR-21: Metadata Sources [P1]
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-21.1 | The system SHOULD integrate with MusicBrainz |
|
||||||
|
| FR-21.2 | The system SHOULD integrate with Discogs |
|
||||||
|
| FR-21.3 | The system SHOULD integrate with Last.fm |
|
||||||
|
| FR-21.4 | The system SHOULD support AcoustID fingerprinting |
|
||||||
|
| FR-21.5 | The system SHALL support custom metadata plugins |
|
||||||
|
|
||||||
|
#### FR-22: Import & Migration [P1]
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-22.1 | The system SHALL import from beets database |
|
||||||
|
| FR-22.2 | The system SHOULD import from iTunes/Apple Music library |
|
||||||
|
| FR-22.3 | The system SHALL export library metadata |
|
||||||
|
|
||||||
|
### 3.11 Extensibility
|
||||||
|
|
||||||
|
#### FR-23: Plugin System [P0]
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-23.1 | The system SHALL support loadable plugins |
|
||||||
|
| FR-23.2 | The system SHALL define stable plugin API |
|
||||||
|
| FR-23.3 | The system SHALL support plugins for: origins, metadata extractors, formats |
|
||||||
|
| FR-23.4 | The system SHOULD support WASM plugins for sandboxed execution |
|
||||||
|
| FR-23.5 | The system SHALL provide plugin lifecycle management (load, unload, reload) |
|
||||||
|
|
||||||
|
#### FR-24: Format Extensibility [P1]
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-24.1 | The system SHALL support pluggable codec modules |
|
||||||
|
| FR-24.2 | The system SHOULD support audiobook formats (M4B, chapters) |
|
||||||
|
| FR-24.3 | The system SHALL allow format plugins to register file extensions |
|
||||||
|
|
||||||
|
### 3.12 High Availability [P3]
|
||||||
|
|
||||||
|
#### FR-25: Resilience
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| FR-25.1 | The system SHOULD support active-passive failover |
|
||||||
|
| FR-25.2 | The system SHOULD support read replicas |
|
||||||
|
| FR-25.3 | The system SHALL support zero-downtime upgrades |
|
||||||
|
| FR-25.4 | The system SHALL support cache backup/restore |
|
||||||
|
| FR-25.5 | The system SHALL validate cache integrity on startup |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Non-Functional Requirements
|
||||||
|
|
||||||
|
### 4.1 Performance
|
||||||
|
|
||||||
|
#### NFR-1: Latency
|
||||||
|
|
||||||
|
| ID | Requirement | Target | Maximum |
|
||||||
|
|----|-------------|--------|---------|
|
||||||
|
| NFR-1.1 | `stat()` on cached file | <1ms | 5ms |
|
||||||
|
| NFR-1.2 | `readdir()` on cached directory | <10ms | 50ms |
|
||||||
|
| NFR-1.3 | `open()` on cached file | <5ms | 20ms |
|
||||||
|
| NFR-1.4 | `read()` from cache | <1ms | 5ms |
|
||||||
|
| NFR-1.5 | `read()` cache miss (local origin) | <50ms | 200ms |
|
||||||
|
| NFR-1.6 | `read()` cache miss (remote origin) | <200ms | 1000ms |
|
||||||
|
| NFR-1.7 | Mount completion | <100ms | 500ms |
|
||||||
|
|
||||||
|
#### NFR-2: Throughput
|
||||||
|
|
||||||
|
| ID | Requirement | Target |
|
||||||
|
|----|-------------|--------|
|
||||||
|
| NFR-2.1 | Sequential read throughput (cached) | >500 MB/s |
|
||||||
|
| NFR-2.2 | Sequential read throughput (local origin) | >200 MB/s |
|
||||||
|
| NFR-2.3 | Metadata operations per second | >1000 ops/s |
|
||||||
|
| NFR-2.4 | Concurrent file handles | >1000 |
|
||||||
|
|
||||||
|
#### NFR-3: Scalability
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| NFR-3.1 | The system SHALL handle libraries with 1,000,000+ files |
|
||||||
|
| NFR-3.2 | The system SHALL handle directories with 100,000+ entries |
|
||||||
|
| NFR-3.3 | The system SHALL maintain O(1) mount time regardless of library size |
|
||||||
|
| NFR-3.4 | The system SHALL maintain O(log n) lookup time for paths |
|
||||||
|
| NFR-3.5 | The system SHOULD handle libraries with 10,000,000+ files [P3] |
|
||||||
|
| NFR-3.6 | The system SHOULD support 100+ concurrent clients [P3] |
|
||||||
|
| NFR-3.7 | The system SHOULD achieve <100μs cached stat for high-performance use [P3] |
|
||||||
|
|
||||||
|
### 4.2 Resource Usage
|
||||||
|
|
||||||
|
#### NFR-4: Memory
|
||||||
|
|
||||||
|
| ID | Requirement | Limit |
|
||||||
|
|----|-------------|-------|
|
||||||
|
| NFR-4.1 | Idle memory usage | <50 MB |
|
||||||
|
| NFR-4.2 | Active usage (1000 files accessed) | <200 MB |
|
||||||
|
| NFR-4.3 | Peak usage under load | <500 MB |
|
||||||
|
| NFR-4.4 | Per-file metadata overhead | <1 KB |
|
||||||
|
| NFR-4.5 | The system SHALL NOT load entire files into memory |
|
||||||
|
|
||||||
|
#### NFR-5: Disk
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| NFR-5.1 | Metadata cache size SHALL be configurable (default: 100 MB) |
|
||||||
|
| NFR-5.2 | Content cache size SHALL be configurable (default: 10 GB) |
|
||||||
|
| NFR-5.3 | The system SHALL evict cache entries under disk pressure |
|
||||||
|
| NFR-5.4 | The system SHALL function with cache disabled (passthrough mode) |
|
||||||
|
|
||||||
|
#### NFR-6: Network
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| NFR-6.1 | The system SHALL minimize network round-trips via batching |
|
||||||
|
| NFR-6.2 | The system SHALL use connection pooling for remote origins |
|
||||||
|
| NFR-6.3 | The system SHALL support bandwidth limiting (configurable) |
|
||||||
|
| NFR-6.4 | Delta sync SHALL achieve >90% bandwidth reduction vs full copy |
|
||||||
|
|
||||||
|
### 4.3 Reliability
|
||||||
|
|
||||||
|
#### NFR-7: Availability
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| NFR-7.1 | The system SHALL serve cached data when origin is unavailable |
|
||||||
|
| NFR-7.2 | The system SHALL gracefully degrade with network failures |
|
||||||
|
| NFR-7.3 | The system SHALL retry failed operations with exponential backoff |
|
||||||
|
| NFR-7.4 | The system SHALL not crash on malformed audio files |
|
||||||
|
|
||||||
|
#### NFR-8: Data Integrity
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| NFR-8.1 | The system SHALL verify chunk integrity via checksums |
|
||||||
|
| NFR-8.2 | The system SHALL use ACID transactions for cache database |
|
||||||
|
| NFR-8.3 | The system SHALL recover from interrupted synchronization |
|
||||||
|
| NFR-8.4 | The system SHALL detect and report cache corruption |
|
||||||
|
|
||||||
|
### 4.4 Usability
|
||||||
|
|
||||||
|
#### NFR-9: Configuration
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| NFR-9.1 | The system SHALL support configuration via file (TOML/YAML) |
|
||||||
|
| NFR-9.2 | The system SHALL support configuration via command-line arguments |
|
||||||
|
| NFR-9.3 | The system SHALL support configuration via environment variables |
|
||||||
|
| NFR-9.4 | The system SHALL provide sensible defaults for all options |
|
||||||
|
|
||||||
|
#### NFR-10: Observability
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| NFR-10.1 | The system SHALL log operations at configurable verbosity |
|
||||||
|
| NFR-10.2 | The system SHALL expose metrics (cache hit rate, latency, etc.) |
|
||||||
|
| NFR-10.3 | The system SHALL support health check endpoint/signal |
|
||||||
|
| NFR-10.4 | The system SHOULD support integration with Prometheus/StatsD |
|
||||||
|
|
||||||
|
### 4.5 Compatibility
|
||||||
|
|
||||||
|
#### NFR-11: Platform Support
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| NFR-11.1 | The system SHALL run on Linux (kernel 4.x+) |
|
||||||
|
| NFR-11.2 | The system SHOULD run on macOS (via macFUSE) |
|
||||||
|
| NFR-11.3 | The system SHALL require FUSE kernel module |
|
||||||
|
| NFR-11.4 | The system SHALL run without root privileges (user-space FUSE) |
|
||||||
|
|
||||||
|
#### NFR-12: Application Compatibility
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| NFR-12.1 | The system SHALL work with standard media players (mpv, VLC, etc.) |
|
||||||
|
| NFR-12.2 | The system SHALL work with media servers (Plex, Jellyfin) |
|
||||||
|
| NFR-12.3 | The system SHALL work with file managers (Nautilus, Dolphin) |
|
||||||
|
| NFR-12.4 | The system SHALL correctly report file sizes and timestamps |
|
||||||
|
|
||||||
|
### 4.6 Security
|
||||||
|
|
||||||
|
#### NFR-13: Access Control
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| NFR-13.1 | The system SHALL respect origin file permissions |
|
||||||
|
| NFR-13.2 | The system SHALL run as unprivileged user |
|
||||||
|
| NFR-13.3 | The system SHALL support credential storage for remote origins |
|
||||||
|
| NFR-13.4 | The system SHALL NOT expose credentials in logs or process list |
|
||||||
|
|
||||||
|
### 4.7 Maintainability
|
||||||
|
|
||||||
|
#### NFR-14: Code Quality
|
||||||
|
|
||||||
|
| ID | Requirement |
|
||||||
|
|----|-------------|
|
||||||
|
| NFR-14.1 | The system SHALL be implemented in a memory-safe language |
|
||||||
|
| NFR-14.2 | The system SHALL have no global interpreter lock (no Python/Ruby) |
|
||||||
|
| NFR-14.3 | The system SHALL use async I/O for concurrent operations |
|
||||||
|
| NFR-14.4 | The system SHALL have modular architecture with pluggable components |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Constraints
|
||||||
|
|
||||||
|
### 5.1 Technical Constraints
|
||||||
|
|
||||||
|
| ID | Constraint |
|
||||||
|
|----|------------|
|
||||||
|
| C-1 | Must use FUSE for filesystem interface |
|
||||||
|
| C-2 | Must not require kernel module development |
|
||||||
|
| C-3 | Must work with existing audio file formats (no transcoding) |
|
||||||
|
| C-4 | Cache database must be portable (no external database server) |
|
||||||
|
|
||||||
|
### 5.2 Operational Constraints
|
||||||
|
|
||||||
|
| ID | Constraint |
|
||||||
|
|----|------------|
|
||||||
|
| C-5 | Client is read-only; no writes propagate to origin |
|
||||||
|
| C-6 | Must function offline with cached data |
|
||||||
|
| C-7 | Must not corrupt origin files under any circumstances |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Assumptions
|
||||||
|
|
||||||
|
| ID | Assumption |
|
||||||
|
|----|------------|
|
||||||
|
| A-1 | Origin storage is accessible via supported protocol |
|
||||||
|
| A-2 | Audio files contain valid metadata headers |
|
||||||
|
| A-3 | Sufficient local disk space for caching is available |
|
||||||
|
| A-4 | FUSE kernel module is installed and accessible |
|
||||||
|
| A-5 | Network connectivity is intermittent but generally available |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Dependencies
|
||||||
|
|
||||||
|
| ID | Dependency | Purpose |
|
||||||
|
|----|------------|---------|
|
||||||
|
| D-1 | FUSE library (fuser/libfuse) | Filesystem interface |
|
||||||
|
| D-2 | SQLite | Metadata and tree cache |
|
||||||
|
| D-3 | Audio parsing library (symphonia) | Metadata extraction |
|
||||||
|
| D-4 | Async runtime (tokio) | Concurrent I/O |
|
||||||
|
| D-5 | CDC library (fastcdc) | Content chunking |
|
||||||
|
| D-6 | Full-text search (tantivy) | Search index [P1] |
|
||||||
|
| D-7 | Image processing (image) | Album art thumbnails [P1] |
|
||||||
|
| D-8 | HTTP client (reqwest) | Remote origins, metadata APIs |
|
||||||
|
| D-9 | WASM runtime (wasmtime) | Plugin sandboxing [P0] |
|
||||||
|
| D-10 | Hash library (xxhash/blake3) | Content addressing [P0] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Acceptance Criteria
|
||||||
|
|
||||||
|
### 8.1 Functional Acceptance
|
||||||
|
|
||||||
|
| ID | Criterion |
|
||||||
|
|----|-----------|
|
||||||
|
| AC-1 | Mount filesystem and browse directories via `ls` |
|
||||||
|
| AC-2 | Play audio file through mounted filesystem with media player |
|
||||||
|
| AC-3 | Seek within audio file without full download |
|
||||||
|
| AC-4 | Directory listing completes without network access (when cached) |
|
||||||
|
| AC-5 | Confirm write operations return EROFS |
|
||||||
|
| AC-6 | Detect and sync changes from origin within configured interval |
|
||||||
|
|
||||||
|
### 8.2 Performance Acceptance
|
||||||
|
|
||||||
|
| ID | Criterion |
|
||||||
|
|----|-----------|
|
||||||
|
| AC-7 | Mount completes in <500ms for library of any size |
|
||||||
|
| AC-8 | Cached stat() completes in <5ms (p99) |
|
||||||
|
| AC-9 | Memory stays under 500MB with 10,000 files accessed |
|
||||||
|
| AC-10 | Tag-only change syncs <10KB of data |
|
||||||
|
|
||||||
|
### 8.3 Reliability Acceptance
|
||||||
|
|
||||||
|
| ID | Criterion |
|
||||||
|
|----|-----------|
|
||||||
|
| AC-11 | Filesystem remains accessible when origin is offline |
|
||||||
|
| AC-12 | No data corruption after unclean unmount |
|
||||||
|
| AC-13 | Recovers automatically when origin comes back online |
|
||||||
|
|
||||||
|
### 8.4 Multi-Origin Acceptance [P0]
|
||||||
|
|
||||||
|
| ID | Criterion |
|
||||||
|
|----|-----------|
|
||||||
|
| AC-14 | Configure and mount multiple origins simultaneously |
|
||||||
|
| AC-15 | Browse unified tree showing content from all origins |
|
||||||
|
| AC-16 | Access same file from preferred origin when duplicated |
|
||||||
|
|
||||||
|
### 8.5 Search & Discovery Acceptance [P1]
|
||||||
|
|
||||||
|
| ID | Criterion |
|
||||||
|
|----|-----------|
|
||||||
|
| AC-17 | Search for tracks by partial artist/album/title match |
|
||||||
|
| AC-18 | Browse smart collection (e.g., "Jazz from 1960s") |
|
||||||
|
| AC-19 | View album art via virtual cover.jpg file |
|
||||||
|
|
||||||
|
### 8.6 Plugin Acceptance [P0]
|
||||||
|
|
||||||
|
| ID | Criterion |
|
||||||
|
|----|-----------|
|
||||||
|
| AC-20 | Load custom origin plugin at runtime |
|
||||||
|
| AC-21 | Control daemon via Unix socket (cache stats, refresh) |
|
||||||
|
| AC-22 | Receive webhook on file access event |
|
||||||
|
|
||||||
|
### 8.7 Deduplication Acceptance [P0]
|
||||||
|
|
||||||
|
| ID | Criterion |
|
||||||
|
|----|-----------|
|
||||||
|
| AC-23 | Identical chunks stored once regardless of file count |
|
||||||
|
| AC-24 | Deduplication stats visible via control API |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Appendix
|
||||||
|
|
||||||
|
### 9.1 Comparison with beetfs
|
||||||
|
|
||||||
|
| Requirement Area | beetfs | This Specification |
|
||||||
|
|------------------|--------|-------------------|
|
||||||
|
| Mount time | O(N), 5-120s | O(1), <500ms (NFR-1.7) |
|
||||||
|
| Memory per file | Full file size | <1KB (NFR-4.4) |
|
||||||
|
| Write to origin | Yes (DB updates) | No (FR-4.1, FR-4.2) |
|
||||||
|
| Delta sync | None | Required (FR-11) |
|
||||||
|
| Remote origins | None | Required (FR-12) |
|
||||||
|
| Offline access | No | Required (NFR-7.1) |
|
||||||
|
| Cache persistence | No | Required (FR-7.1) |
|
||||||
|
|
||||||
|
### 9.2 Path Template Variables
|
||||||
|
|
||||||
|
| Variable | Description | Example |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `$artist` | Track artist | "Metallica" |
|
||||||
|
| `$album` | Album name | "72 Seasons" |
|
||||||
|
| `$title` | Track title | "Lux Æterna" |
|
||||||
|
| `$track` | Track number (zero-padded) | "03" |
|
||||||
|
| `$disc` | Disc number | "1" |
|
||||||
|
| `$year` | Release year | "2023" |
|
||||||
|
| `$genre` | Genre | "Metal" |
|
||||||
|
| `$format` | File extension | "flac" |
|
||||||
|
| `$format_upper` | File extension (uppercase) | "FLAC" |
|
||||||
|
|
||||||
|
### 9.3 Error Codes
|
||||||
|
|
||||||
|
| Operation | Error | Code |
|
||||||
|
|-----------|-------|------|
|
||||||
|
| Any write operation | Read-only filesystem | EROFS (30) |
|
||||||
|
| File not found | No such file | ENOENT (2) |
|
||||||
|
| Origin unavailable | I/O error | EIO (5) |
|
||||||
|
| Permission denied | Access denied | EACCES (13) |
|
||||||
Reference in New Issue
Block a user