1374084135
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
413 lines
37 KiB
Markdown
413 lines
37 KiB
Markdown
# beetfs Data Flow
|
|
|
|
## Overview
|
|
|
|
This document details the complete data flow for read and write operations in beetfs.
|
|
|
|
---
|
|
|
|
## 1. Initialization Flow
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ beet mount /mountpoint │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ mount(lib, config, opts, args) │
|
|
│ │
|
|
│ 1. Parse PATH_FORMAT into structure_split │
|
|
│ PATH_FORMAT = "$artist/$album ($year) [$format_upper]/..." │
|
|
│ structure_split = ["$artist", "$album ($year) [$format_upper]", ...] │
|
|
│ structure_depth = 3 │
|
|
│ │
|
|
│ 2. Store global library reference │
|
|
│ library = lib │
|
|
│ │
|
|
│ 3. Create empty virtual directory tree │
|
|
│ directory_structure = FSNode({}, {}) │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ for item in lib.items(): │
|
|
│ │
|
|
│ For each item in beets library: │
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ 1. Build template mapping │ │
|
|
│ │ mapping = { │ │
|
|
│ │ 'artist': 'Pink Floyd', │ │
|
|
│ │ 'album': 'The Wall', │ │
|
|
│ │ 'year': '1979', │ │
|
|
│ │ 'format_upper': 'FLAC', │ │
|
|
│ │ 'track': '01', │ │
|
|
│ │ 'title': 'In The Flesh?', │ │
|
|
│ │ } │ │
|
|
│ │ │ │
|
|
│ │ 2. Substitute template for each level │ │
|
|
│ │ level_subbed[0] = "Pink Floyd" │ │
|
|
│ │ level_subbed[1] = "The Wall (1979) [FLAC]" │ │
|
|
│ │ level_subbed[2] = "01 - Pink Floyd - In The Flesh?.flac" │ │
|
|
│ │ │ │
|
|
│ │ 3. Add directories to tree │ │
|
|
│ │ directory_structure.adddir([], "Pink Floyd") │ │
|
|
│ │ directory_structure.adddir(["Pink Floyd"], "The Wall (1979)...") │ │
|
|
│ │ │ │
|
|
│ │ 4. Add file entry (filename → item.id) │ │
|
|
│ │ directory_structure.addfile( │ │
|
|
│ │ ["Pink Floyd", "The Wall (1979) [FLAC]"], │ │
|
|
│ │ "01 - Pink Floyd - In The Flesh?.flac", │ │
|
|
│ │ item.id # e.g., 42 │ │
|
|
│ │ ) │ │
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ beetFileSystem FUSE Server │
|
|
│ │
|
|
│ server = beetFileSystem(...) │
|
|
│ server.multithreaded = 0 │
|
|
│ server.main() ← Enters FUSE event loop │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 2. File Open Flow
|
|
|
|
```
|
|
Application: open("/mount/Pink Floyd/The Wall (1979) [FLAC]/01 - Pink Floyd - In The Flesh?.flac")
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ beetFileSystem.open(path, flags) │
|
|
│ Lines 988-1021 │
|
|
│ │
|
|
│ path = "/Pink Floyd/The Wall (1979) [FLAC]/01 - Pink Floyd - In The..." │
|
|
│ flags = os.O_RDONLY (or O_RDWR) │
|
|
│ │
|
|
│ if path in self.files: │
|
|
│ # File already open - increment reference count │
|
|
│ self.files[path].open() │
|
|
│ return self.files[path] │
|
|
│ else: │
|
|
│ # Create new FileHandler │
|
|
│ self.files[path] = FileHandler(path, self.lib) │
|
|
│ return self.files[path] │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ FileHandler.__init__(path, lib) │
|
|
│ Lines 440-483 │
|
|
│ │
|
|
│ Step 1: Resolve virtual path to beets item │
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ pathsplit = ["Pink Floyd", "The Wall (1979) [FLAC]", │ │
|
|
│ │ "01 - Pink Floyd - In The Flesh?.flac"] │ │
|
|
│ │ │ │
|
|
│ │ # Navigate to parent directory in virtual tree │ │
|
|
│ │ node = directory_structure.getnode(pathsplit[0:2]) │ │
|
|
│ │ # node.files = {"01 - Pink Floyd - In The Flesh?.flac": 42, ...} │ │
|
|
│ │ │ │
|
|
│ │ # Get beets item by ID │ │
|
|
│ │ item_id = node.files[pathsplit[2]] # 42 │ │
|
|
│ │ self.item = lib.get_item(id=42) │ │
|
|
│ │ self.real_path = self.item.path │ │
|
|
│ │ # e.g., "/mnt/music/torrents/pink_floyd_wall.flac" │ │
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ Step 2: Open real file and detect format │
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ self.file_object = open(self.real_path, 'r+') │ │
|
|
│ │ self.format = "flac" # from file extension │ │
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ Step 3: Create InterpolatedFLAC with database metadata │
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ self.inf = InterpolatedFLAC(self.file_object.read()) │ │
|
|
│ │ │ │
|
|
│ │ # INJECT DATABASE METADATA (this is the key operation!) │ │
|
|
│ │ self.inf["title"] = self.item.title # "In The Flesh?" │ │
|
|
│ │ self.inf["album"] = self.item.album # "The Wall" │ │
|
|
│ │ self.inf["artist"] = self.item.artist # "Pink Floyd" │ │
|
|
│ │ self.inf["genre"] = self.item.genre # "Progressive Rock" │ │
|
|
│ │ │ │
|
|
│ │ # Generate header with injected metadata │ │
|
|
│ │ self.header = self.inf.get_header(self.real_path) │ │
|
|
│ │ self.bound = len(self.header) # e.g., 8192 bytes │ │
|
|
│ │ self.music_offset = self.inf.offset() │ │
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ Step 4: Cache audio data │
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ self.file_object.seek(self.music_offset) │ │
|
|
│ │ self.music_data = self.file_object.read() # All audio data │ │
|
|
│ │ self.file_object.close() │ │
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 3. File Read Flow
|
|
|
|
```
|
|
Application: read(fd, buffer, 4096) # offset managed by kernel
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ beetFileSystem.read(path, size, offset, fh) │
|
|
│ Lines 1077-1106 │
|
|
│ │
|
|
│ path = "/Pink Floyd/The Wall (1979) [FLAC]/01 - ..." │
|
|
│ size = 4096 │
|
|
│ offset = 0 (first read) or previous offset + bytes_read │
|
|
│ fh = FileHandler instance │
|
|
│ │
|
|
│ return self.files[path].read(size, offset) │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ FileHandler.read(size, offset) │
|
|
│ Lines 497-517 │
|
|
│ │
|
|
│ Variables: │
|
|
│ self.bound = 8192 (header size) │
|
|
│ self.header = bytes (generated FLAC header with DB metadata) │
|
|
│ self.music_data = bytes (original audio frames) │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
┌───────────────────────┼───────────────────────┐
|
|
│ │ │
|
|
▼ ▼ ▼
|
|
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
|
|
│ Case 1: Header Only │ │ Case 2: Span Both │ │ Case 3: Audio Only │
|
|
│ offset < bound │ │ offset < bound │ │ offset >= bound │
|
|
│ offset+size < bound │ │ offset+size >= bound│ │ │
|
|
├─────────────────────┤ ├─────────────────────┤ ├─────────────────────┤
|
|
│ Example: │ │ Example: │ │ Example: │
|
|
│ offset=0 │ │ offset=8000 │ │ offset=10000 │
|
|
│ size=4096 │ │ size=4096 │ │ size=4096 │
|
|
│ bound=8192 │ │ bound=8192 │ │ bound=8192 │
|
|
├─────────────────────┤ ├─────────────────────┤ ├─────────────────────┤
|
|
│ Return: │ │ Return: │ │ Return: │
|
|
│ header[0:4096] │ │ header[8000:8192] │ │ music_data[ │
|
|
│ │ │ + music_data[0:3904]│ │ 1808:5904] │
|
|
│ (DB metadata!) │ │ │ │ │
|
|
│ │ │ (mixed) │ │ (original audio) │
|
|
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
|
|
|
|
|
|
Visual representation of virtual file:
|
|
|
|
0 bound (8192) EOF
|
|
│ │ │
|
|
▼ ▼ ▼
|
|
┌───────────────────────┬────────────────────────────────────────────┐
|
|
│ HEADER │ AUDIO DATA │
|
|
│ (self.header) │ (self.music_data) │
|
|
│ │ │
|
|
│ Contains: │ Contains: │
|
|
│ - "fLaC" magic │ - Original FLAC frames │
|
|
│ - STREAMINFO block │ - Unchanged from disk │
|
|
│ - VORBIS_COMMENT │ │
|
|
│ with DB values: │ │
|
|
│ title, artist, │ │
|
|
│ album, genre │ │
|
|
│ - PADDING block │ │
|
|
└───────────────────────┴────────────────────────────────────────────┘
|
|
▲ ▲
|
|
│ │
|
|
From InterpolatedFLAC From original file
|
|
with injected DB tags (passed through)
|
|
```
|
|
|
|
---
|
|
|
|
## 4. File Write Flow
|
|
|
|
```
|
|
Application: write(fd, "TITLE=New Title\0", 16) # Hypothetical tag edit
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ beetFileSystem.write(path, buf, offset, fh) │
|
|
│ Lines 1108-1135 │
|
|
│ │
|
|
│ return self.files[path].write(offset, buf) │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ FileHandler.write(offset, buf) │
|
|
│ Lines 519-565 │
|
|
│ │
|
|
│ if offset >= self.bound: │
|
|
│ # Write is in audio area - DISCARD │
|
|
│ return # Do nothing, audio is read-only │
|
|
│ │
|
|
│ # Write is in header area - process tag update │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ Step 1: Reconstruct full virtual file in memory │
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ filedata = self.header + self.music_data │ │
|
|
│ │ │ │
|
|
│ │ # Patch in new data │ │
|
|
│ │ filedata = filedata[0:offset] + buf + filedata[offset + len(buf):] │ │
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ Step 2: Parse patched data as FLAC │
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ self.inf = InterpolatedFLAC(filedata) │ │
|
|
│ │ # This parses the FLAC structure and extracts Vorbis comments │ │
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ Step 3: Extract tag values from parsed FLAC │
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ self.item.title = str(self.inf["title"][0]).encode('utf-8') │ │
|
|
│ │ self.item.album = str(self.inf["album"][0]).encode('utf-8') │ │
|
|
│ │ self.item.artist = str(self.inf["artist"][0]).encode('utf-8') │ │
|
|
│ │ self.item.genre = str(self.inf["genre"][0]).encode('utf-8') │ │
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ Step 4: Save to beets database │
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ self.lib.store(self.item) # Update item in library │ │
|
|
│ │ self.lib.save() # Persist to SQLite │ │
|
|
│ │ │ │
|
|
│ │ # NOTE: Original file on disk is NEVER touched! │ │
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ Step 5: Regenerate header for subsequent reads │
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ self.inf["title"] = self.item.title │ │
|
|
│ │ self.inf["album"] = self.item.album │ │
|
|
│ │ self.inf["artist"] = self.item.artist │ │
|
|
│ │ self.inf["genre"] = self.item.genre │ │
|
|
│ │ │ │
|
|
│ │ self.header = self.inf.get_header(self.real_path) │ │
|
|
│ │ self.bound = len(self.header) │ │
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ return len(buf) # Success │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
|
|
|
|
Write data flow summary:
|
|
|
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
│ Application │ │ beetfs │ │ Beets │ │ Original │
|
|
│ writes │────▶│ parses │────▶│ database │ │ file │
|
|
│ new tags │ │ extracts │ │ updated │ │ UNTOUCHED │
|
|
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 5. File Release Flow
|
|
|
|
```
|
|
Application: close(fd)
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ beetFileSystem.release(path, flags, fh) │
|
|
│ Lines 1049-1059 │
|
|
│ │
|
|
│ if self.files[path].release(): │
|
|
│ # Reference count reached 0, clean up │
|
|
│ del self.files[path] │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ FileHandler.release() │
|
|
│ Lines 489-495 │
|
|
│ │
|
|
│ self.instance_count -= 1 │
|
|
│ │
|
|
│ if self.instance_count == 0: │
|
|
│ return True # OK to delete │
|
|
│ else: │
|
|
│ return False # Still in use │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Directory Listing Flow
|
|
|
|
```
|
|
Application: ls /mount/Pink\ Floyd/
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ beetFileSystem.readdir(path, offset, dh) │
|
|
│ Lines 931-975 │
|
|
│ │
|
|
│ path = "/Pink Floyd" │
|
|
│ pathsplit = ["Pink Floyd"] │
|
|
│ │
|
|
│ yield fuse.Direntry(".") │
|
|
│ yield fuse.Direntry("..") │
|
|
│ │
|
|
│ # len(pathsplit) == 1, structure_depth - 1 == 2 │
|
|
│ # So we're listing directories (albums), not files │
|
|
│ │
|
|
│ for dirname in directory_structure.listdir(pathsplit, True): │
|
|
│ yield fuse.Direntry(dirname.encode('utf-8')) │
|
|
│ # "The Wall (1979) [FLAC]" │
|
|
│ # "Animals (1977) [FLAC]" │
|
|
│ # etc. │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Complete Request Lifecycle
|
|
|
|
```
|
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
|
│ COMPLETE LIFECYCLE │
|
|
│ │
|
|
│ 1. User mounts: beet mount /mnt/music │
|
|
│ ├─ Build virtual tree from beets library │
|
|
│ └─ Start FUSE event loop │
|
|
│ │
|
|
│ 2. Application opens file: open("/mnt/music/Artist/Album/track.flac") │
|
|
│ ├─ Resolve virtual path to beets item ID │
|
|
│ ├─ Load original file into memory │
|
|
│ ├─ Inject database metadata into FLAC structure │
|
|
│ ├─ Generate new header with DB tags │
|
|
│ └─ Cache audio data │
|
|
│ │
|
|
│ 3. Application reads file: read(fd, buf, 4096) │
|
|
│ ├─ If reading header region → return header (DB metadata) │
|
|
│ ├─ If reading audio region → return cached audio (original) │
|
|
│ └─ If spanning both → return combined data │
|
|
│ │
|
|
│ 4. Application writes tags: write(fd, new_tags, offset) │
|
|
│ ├─ If audio region → discard (read-only) │
|
|
│ ├─ If header region: │
|
|
│ │ ├─ Parse new tag values │
|
|
│ │ ├─ Update beets database │
|
|
│ │ └─ Regenerate header │
|
|
│ └─ Original file NEVER modified │
|
|
│ │
|
|
│ 5. Application closes file: close(fd) │
|
|
│ ├─ Decrement reference count │
|
|
│ └─ Clean up if count == 0 │
|
|
│ │
|
|
│ 6. User unmounts: fusermount -u /mnt/music │
|
|
│ └─ fsdestroy() called, cleanup │
|
|
│ │
|
|
└──────────────────────────────────────────────────────────────────────────────┘
|
|
```
|