From 305d027c8b5def9dcdec7b201e9234de5b6c4aad Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 13 May 2026 20:34:14 +0200 Subject: [PATCH] Move the files around --- .gitignore | 33 + musicfs/Cargo.lock => Cargo.lock | 0 musicfs/Cargo.toml => Cargo.toml | 0 README.rst | 7 - beetsplug/__init__.py | 2 - beetsplug/beetFs.py | 1144 ----------------- .../musicfs-cache/Cargo.toml | 0 .../musicfs-cache/src/artwork.rs | 13 +- .../crates => crates}/musicfs-cache/src/db.rs | 25 +- .../musicfs-cache/src/eviction.rs | 0 .../musicfs-cache/src/lib.rs | 0 .../musicfs-cache/src/metadata.rs | 9 +- .../musicfs-cache/src/patterns.rs | 14 +- .../musicfs-cache/src/prefetch.rs | 9 +- .../musicfs-cache/src/schema.sql | 0 .../musicfs-cache/src/tree.rs | 15 +- .../crates => crates}/musicfs-cas/Cargo.toml | 0 .../musicfs-cas/src/chunks.rs | 0 .../musicfs-cas/src/fetcher.rs | 6 +- .../crates => crates}/musicfs-cas/src/lib.rs | 0 .../musicfs-cas/src/reader.rs | 19 +- .../musicfs-cas/src/store.rs | 7 +- .../musicfs-cas/tests/integration.rs | 10 +- .../crates => crates}/musicfs-cli/Cargo.toml | 0 .../crates => crates}/musicfs-cli/src/lib.rs | 0 .../crates => crates}/musicfs-cli/src/main.rs | 25 +- .../crates => crates}/musicfs-core/Cargo.toml | 0 .../musicfs-core/src/config.rs | 0 .../musicfs-core/src/credentials.rs | 0 .../musicfs-core/src/error.rs | 0 .../musicfs-core/src/events.rs | 5 +- .../crates => crates}/musicfs-core/src/lib.rs | 0 .../musicfs-core/src/metrics.rs | 23 +- .../musicfs-core/src/resolver.rs | 0 .../musicfs-core/src/supervisor.rs | 0 .../musicfs-core/src/types.rs | 0 .../crates => crates}/musicfs-fuse/Cargo.toml | 0 .../musicfs-fuse/src/filesystem.rs | 27 +- .../crates => crates}/musicfs-fuse/src/lib.rs | 0 .../musicfs-fuse/src/ops/mod.rs | 0 .../musicfs-fuse/src/ops/prefetch.rs | 21 +- .../musicfs-fuse/src/ops/search.rs | 13 +- .../crates => crates}/musicfs-grpc/Cargo.toml | 0 .../crates => crates}/musicfs-grpc/build.rs | 0 .../musicfs-grpc/proto/musicfs.proto | 0 .../crates => crates}/musicfs-grpc/src/lib.rs | 0 .../musicfs-grpc/src/search_service.rs | 4 +- .../musicfs-grpc/src/server.rs | 23 +- .../musicfs-grpc/src/webhook.rs | 0 .../musicfs-metadata/Cargo.toml | 0 .../musicfs-metadata/src/artwork.rs | 0 .../musicfs-metadata/src/lib.rs | 0 .../musicfs-metadata/src/parser.rs | 8 +- .../musicfs-origins/Cargo.toml | 0 .../musicfs-origins/src/failover.rs | 20 +- .../musicfs-origins/src/health.rs | 14 +- .../musicfs-origins/src/lib.rs | 0 .../musicfs-origins/src/local.rs | 0 .../musicfs-origins/src/nfs.rs | 0 .../musicfs-origins/src/registry.rs | 0 .../musicfs-origins/src/router.rs | 8 +- .../musicfs-origins/src/s3.rs | 2 - .../musicfs-origins/src/sftp.rs | 0 .../musicfs-origins/src/smb.rs | 6 +- .../musicfs-origins/src/traits.rs | 0 .../musicfs-plugins/Cargo.toml | 0 .../musicfs-plugins/src/error.rs | 0 .../musicfs-plugins/src/lib.rs | 0 .../musicfs-plugins/src/manager.rs | 0 .../musicfs-plugins/src/native.rs | 20 +- .../musicfs-plugins/src/traits.rs | 6 +- .../musicfs-plugins/src/wasm.rs | 0 .../musicfs-search/Cargo.toml | 0 .../musicfs-search/src/collections.rs | 16 +- .../musicfs-search/src/index.rs | 77 +- .../musicfs-search/src/indexer.rs | 6 +- .../musicfs-search/src/lib.rs | 3 +- .../musicfs-search/src/query.rs | 0 .../crates => crates}/musicfs-sync/Cargo.toml | 0 .../crates => crates}/musicfs-sync/src/cdc.rs | 11 +- .../musicfs-sync/src/delta.rs | 8 +- .../crates => crates}/musicfs-sync/src/lib.rs | 0 .../musicfs-sync/src/watcher.rs | 15 +- .../musicfs-test-utils/Cargo.toml | 0 .../musicfs-test-utils/src/assertions.rs | 14 +- .../musicfs-test-utils/src/faulty_cas.rs | 0 .../musicfs-test-utils/src/faulty_origin.rs | 0 .../musicfs-test-utils/src/fixtures.rs | 4 +- .../musicfs-test-utils/src/lib.rs | 0 .../tests/docker_network.rs | 23 +- .../musicfs-test-utils/tests/resilience.rs | 230 +++- flake.lock | 87 +- flake.nix | 165 +-- musicfs/.cargo/config.toml | 10 - musicfs/.gitignore | 1 - musicfs/dist/PKGBUILD | 23 - musicfs/dist/config.example.toml | 30 - musicfs/dist/logrotate.d/musicfs | 9 - musicfs/dist/musicfs.service | 29 - musicfs/dist/musicfs.spec | 39 - musicfs/flake.lock | 96 -- musicfs/flake.nix | 46 - tests/__init__.py | 0 tests/conftest.py | 580 --------- {musicfs/tests => tests}/e2e/e2e_players.rs | 0 .../integration/docker-compose.yml | 0 tests/test_error_handling.py | 161 --- tests/test_nested_bug.py | 141 -- tests/test_read.py | 296 ----- tests/test_readdir.py | 179 --- tests/test_smoke.py | 83 -- tests/test_stat.py | 138 -- tests/test_write.py | 161 --- 113 files changed, 650 insertions(+), 3569 deletions(-) rename musicfs/Cargo.lock => Cargo.lock (100%) rename musicfs/Cargo.toml => Cargo.toml (100%) delete mode 100644 README.rst delete mode 100644 beetsplug/__init__.py delete mode 100644 beetsplug/beetFs.py rename {musicfs/crates => crates}/musicfs-cache/Cargo.toml (100%) rename {musicfs/crates => crates}/musicfs-cache/src/artwork.rs (94%) rename {musicfs/crates => crates}/musicfs-cache/src/db.rs (96%) rename {musicfs/crates => crates}/musicfs-cache/src/eviction.rs (100%) rename {musicfs/crates => crates}/musicfs-cache/src/lib.rs (100%) rename {musicfs/crates => crates}/musicfs-cache/src/metadata.rs (95%) rename {musicfs/crates => crates}/musicfs-cache/src/patterns.rs (96%) rename {musicfs/crates => crates}/musicfs-cache/src/prefetch.rs (94%) rename {musicfs/crates => crates}/musicfs-cache/src/schema.sql (100%) rename {musicfs/crates => crates}/musicfs-cache/src/tree.rs (96%) rename {musicfs/crates => crates}/musicfs-cas/Cargo.toml (100%) rename {musicfs/crates => crates}/musicfs-cas/src/chunks.rs (100%) rename {musicfs/crates => crates}/musicfs-cas/src/fetcher.rs (98%) rename {musicfs/crates => crates}/musicfs-cas/src/lib.rs (100%) rename {musicfs/crates => crates}/musicfs-cas/src/reader.rs (96%) rename {musicfs/crates => crates}/musicfs-cas/src/store.rs (98%) rename {musicfs/crates => crates}/musicfs-cas/tests/integration.rs (96%) rename {musicfs/crates => crates}/musicfs-cli/Cargo.toml (100%) rename {musicfs/crates => crates}/musicfs-cli/src/lib.rs (100%) rename {musicfs/crates => crates}/musicfs-cli/src/main.rs (96%) rename {musicfs/crates => crates}/musicfs-core/Cargo.toml (100%) rename {musicfs/crates => crates}/musicfs-core/src/config.rs (100%) rename {musicfs/crates => crates}/musicfs-core/src/credentials.rs (100%) rename {musicfs/crates => crates}/musicfs-core/src/error.rs (100%) rename {musicfs/crates => crates}/musicfs-core/src/events.rs (94%) rename {musicfs/crates => crates}/musicfs-core/src/lib.rs (100%) rename {musicfs/crates => crates}/musicfs-core/src/metrics.rs (95%) rename {musicfs/crates => crates}/musicfs-core/src/resolver.rs (100%) rename {musicfs/crates => crates}/musicfs-core/src/supervisor.rs (100%) rename {musicfs/crates => crates}/musicfs-core/src/types.rs (100%) rename {musicfs/crates => crates}/musicfs-fuse/Cargo.toml (100%) rename {musicfs/crates => crates}/musicfs-fuse/src/filesystem.rs (96%) rename {musicfs/crates => crates}/musicfs-fuse/src/lib.rs (100%) rename {musicfs/crates => crates}/musicfs-fuse/src/ops/mod.rs (100%) rename {musicfs/crates => crates}/musicfs-fuse/src/ops/prefetch.rs (93%) rename {musicfs/crates => crates}/musicfs-fuse/src/ops/search.rs (97%) rename {musicfs/crates => crates}/musicfs-grpc/Cargo.toml (100%) rename {musicfs/crates => crates}/musicfs-grpc/build.rs (100%) rename {musicfs/crates => crates}/musicfs-grpc/proto/musicfs.proto (100%) rename {musicfs/crates => crates}/musicfs-grpc/src/lib.rs (100%) rename {musicfs/crates => crates}/musicfs-grpc/src/search_service.rs (98%) rename {musicfs/crates => crates}/musicfs-grpc/src/server.rs (97%) rename {musicfs/crates => crates}/musicfs-grpc/src/webhook.rs (100%) rename {musicfs/crates => crates}/musicfs-metadata/Cargo.toml (100%) rename {musicfs/crates => crates}/musicfs-metadata/src/artwork.rs (100%) rename {musicfs/crates => crates}/musicfs-metadata/src/lib.rs (100%) rename {musicfs/crates => crates}/musicfs-metadata/src/parser.rs (93%) rename {musicfs/crates => crates}/musicfs-origins/Cargo.toml (100%) rename {musicfs/crates => crates}/musicfs-origins/src/failover.rs (93%) rename {musicfs/crates => crates}/musicfs-origins/src/health.rs (96%) rename {musicfs/crates => crates}/musicfs-origins/src/lib.rs (100%) rename {musicfs/crates => crates}/musicfs-origins/src/local.rs (100%) rename {musicfs/crates => crates}/musicfs-origins/src/nfs.rs (100%) rename {musicfs/crates => crates}/musicfs-origins/src/registry.rs (100%) rename {musicfs/crates => crates}/musicfs-origins/src/router.rs (99%) rename {musicfs/crates => crates}/musicfs-origins/src/s3.rs (99%) rename {musicfs/crates => crates}/musicfs-origins/src/sftp.rs (100%) rename {musicfs/crates => crates}/musicfs-origins/src/smb.rs (97%) rename {musicfs/crates => crates}/musicfs-origins/src/traits.rs (100%) rename {musicfs/crates => crates}/musicfs-plugins/Cargo.toml (100%) rename {musicfs/crates => crates}/musicfs-plugins/src/error.rs (100%) rename {musicfs/crates => crates}/musicfs-plugins/src/lib.rs (100%) rename {musicfs/crates => crates}/musicfs-plugins/src/manager.rs (100%) rename {musicfs/crates => crates}/musicfs-plugins/src/native.rs (94%) rename {musicfs/crates => crates}/musicfs-plugins/src/traits.rs (98%) rename {musicfs/crates => crates}/musicfs-plugins/src/wasm.rs (100%) rename {musicfs/crates => crates}/musicfs-search/Cargo.toml (100%) rename {musicfs/crates => crates}/musicfs-search/src/collections.rs (95%) rename {musicfs/crates => crates}/musicfs-search/src/index.rs (83%) rename {musicfs/crates => crates}/musicfs-search/src/indexer.rs (98%) rename {musicfs/crates => crates}/musicfs-search/src/lib.rs (87%) rename {musicfs/crates => crates}/musicfs-search/src/query.rs (100%) rename {musicfs/crates => crates}/musicfs-sync/Cargo.toml (100%) rename {musicfs/crates => crates}/musicfs-sync/src/cdc.rs (95%) rename {musicfs/crates => crates}/musicfs-sync/src/delta.rs (98%) rename {musicfs/crates => crates}/musicfs-sync/src/lib.rs (100%) rename {musicfs/crates => crates}/musicfs-sync/src/watcher.rs (93%) rename {musicfs/crates => crates}/musicfs-test-utils/Cargo.toml (100%) rename {musicfs/crates => crates}/musicfs-test-utils/src/assertions.rs (94%) rename {musicfs/crates => crates}/musicfs-test-utils/src/faulty_cas.rs (100%) rename {musicfs/crates => crates}/musicfs-test-utils/src/faulty_origin.rs (100%) rename {musicfs/crates => crates}/musicfs-test-utils/src/fixtures.rs (98%) rename {musicfs/crates => crates}/musicfs-test-utils/src/lib.rs (100%) rename {musicfs/crates => crates}/musicfs-test-utils/tests/docker_network.rs (90%) rename {musicfs/crates => crates}/musicfs-test-utils/tests/resilience.rs (80%) delete mode 100644 musicfs/.cargo/config.toml delete mode 100644 musicfs/.gitignore delete mode 100644 musicfs/dist/PKGBUILD delete mode 100644 musicfs/dist/config.example.toml delete mode 100644 musicfs/dist/logrotate.d/musicfs delete mode 100644 musicfs/dist/musicfs.service delete mode 100644 musicfs/dist/musicfs.spec delete mode 100644 musicfs/flake.lock delete mode 100644 musicfs/flake.nix delete mode 100644 tests/__init__.py delete mode 100644 tests/conftest.py rename {musicfs/tests => tests}/e2e/e2e_players.rs (100%) rename {musicfs/tests => tests}/integration/docker-compose.yml (100%) delete mode 100644 tests/test_error_handling.py delete mode 100644 tests/test_nested_bug.py delete mode 100644 tests/test_read.py delete mode 100644 tests/test_readdir.py delete mode 100644 tests/test_smoke.py delete mode 100644 tests/test_stat.py delete mode 100644 tests/test_write.py diff --git a/.gitignore b/.gitignore index 9ea5ba6..aa2568a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,37 @@ tests/*.log # Nix result + +.cargo/ +.direnv/ +.pre-commit-config.yaml +dist/ + +### +# Rust +### result-* +# Generated by Cargo +# will have compiled files and executables +debug +target + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ + +# rustc will dump stack traces when hitting an internal compiler error to PWD +rustc-ice-*.txt + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/musicfs/Cargo.lock b/Cargo.lock similarity index 100% rename from musicfs/Cargo.lock rename to Cargo.lock diff --git a/musicfs/Cargo.toml b/Cargo.toml similarity index 100% rename from musicfs/Cargo.toml rename to Cargo.toml diff --git a/README.rst b/README.rst deleted file mode 100644 index a708a6e..0000000 --- a/README.rst +++ /dev/null @@ -1,7 +0,0 @@ -Organising a music library can be a hassle. With the wealth of online stores all providing music tagged in various formats, it can be a nightmare to unify them all. - -This is where beetFs comes in. Derived from beets, beetFs presents a FUSE filesystem that is based on your tags. - -Modifying the tags within the beetFs mountpoint will not change the data on the hard disk, merely update the beet database. When an application requests a music file from within the beetFs mountpoint, beetFs provides tag information from its own database, instead of from the original file, but music data from the on-disk location. - -This enables completely transparent modification of tags within an audio file with no change to the underlying on-disk data. diff --git a/beetsplug/__init__.py b/beetsplug/__init__.py deleted file mode 100644 index 3ad9513..0000000 --- a/beetsplug/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from pkgutil import extend_path -__path__ = extend_path(__path__, __name__) diff --git a/beetsplug/beetFs.py b/beetsplug/beetFs.py deleted file mode 100644 index 3415dce..0000000 --- a/beetsplug/beetFs.py +++ /dev/null @@ -1,1144 +0,0 @@ -""" -beetFs -Copyright 2010 Martin Eve - -This file is part of beetFs. - -beetFs is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -beetFs is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with beetFs. If not, see . - -""" - -import calendar -import datetime -import errno -import fuse -import logging -import operator -import os -import re -import stat -import struct -from errno import EINVAL -from io import BytesIO -from string import Template - -import beets -from beets.plugins import BeetsPlugin -from beets.ui import Subcommand -from mutagen.flac import (FLAC, Padding, MetadataBlock, VCFLACDict, CueSheet, - SeekTable, FLACNoHeaderError, FLACVorbisError) -from mutagen.id3 import ID3, BitPaddedInt, MakeID3v1 -from mutagen._util import insert_bytes - -PATH_FORMAT = ("$artist/$album ($year) [$format_upper]/" - "$track - $artist - $title.$format") - -beetFs_command = Subcommand('mount', help='Mount a beets filesystem') -log = logging.getLogger('beets') - - -# FUSE version at the time of writing. Be compatible with this version. -fuse.fuse_python_api = (0, 2) - -""" This is duplicated from Library. Ideally should be exposed from there.""" -METADATA_RW_FIELDS = [ - ('title', 'text'), - ('artist', 'text'), - ('album', 'text'), - ('genre', 'text'), - ('composer', 'text'), - ('grouping', 'text'), - ('year', 'int'), - ('month', 'int'), - ('day', 'int'), - ('track', 'int'), - ('tracktotal', 'int'), - ('disc', 'int'), - ('disctotal', 'int'), - ('lyrics', 'text'), - ('comments', 'text'), - ('bpm', 'int'), - ('comp', 'bool'), -] -METADATA_FIELDS = [ - ('length', 'real'), - ('bitrate', 'int'), -] + METADATA_RW_FIELDS - -METADATA_KEYS = map(operator.itemgetter(0), METADATA_FIELDS) - - -def template_mapping(lib, item): - """ Builds a template substitution map. Taken from library.py.""" - mapping = {} - for key in METADATA_KEYS: - value = getattr(item, key) - # sanitize the value for inclusion in a path: - # replace / and leading . with _ - if isinstance(value, basestring): - value.replace(os.sep, '_') - value = re.sub(r'[\\/:]|^\.', '_', value) - elif key in ('track', 'tracktotal', 'disc', 'disctotal'): - # pad with zeros - value = '%02i' % value - else: - value = str(value) - mapping[key] = value - - format_ = os.path.splitext(item.path)[1][1:] - mapping['format'] = re.sub(r'[\\/:]|^\.', '_', format_) - mapping['format_upper'] = re.sub(r'[\\/:]|^\.', '_', format_).upper() - - # fix dud entries - if mapping['artist'] == '': - mapping['artist'] = 'Unknown Artist' - - if mapping['album'] == '': - mapping['album'] = 'Unknown Album' - - if mapping['year'] == '0': - mapping['year'] = 'Unknown Year' - - if mapping['title'] == '': - mapping['title'] = 'Unknown Track' - - return mapping - - -def mount(lib, config, opts, args): - # check we have a command line argument - if not args: - raise beets.ui.UserError('no mountpoint specified') - - # build the in-memory folder structure - global structure_split - structure_split = PATH_FORMAT.split("/") - global structure_depth - structure_depth = len(structure_split) - templates = {} - global library - library = lib - - # establish a blank dictionary at each level of depth - # also create templates for each level - for level in range(0, structure_depth): - #log.debug("Creating template %s" % structure_split[level]) - templates[level] = Template(structure_split[level]) - - global directory_structure - directory_structure = FSNode({}, {}) - - # iterate over items in library - for item in lib.items(): - # build the template map - mapping = template_mapping(lib, item) - - # do the substitutions - level_subbed = {} - for level in range(0, structure_depth): - level_subbed[level] = templates[level].substitute(mapping) - - # build a directory structure - sub_elements = [] - for level in range(0, structure_depth - 1): - # append the path up to the current depth, minus one, to elements - # this means that sub_elements contains the current path, except - # for the folder to be added - if level-1 in level_subbed: - sub_elements.append(level_subbed[level-1]) - # add this directory to the master structure - directory_structure.adddir(sub_elements, level_subbed[level]) - - # add this item as a file - - # First, compensate for 2 directories depth taken off above - # ie. sub_elements now contains the full folder path - sub_elements.append(level_subbed[structure_depth-3]) - sub_elements.append(level_subbed[structure_depth-2]) - - # do the actual add - directory_structure.addfile(sub_elements, - level_subbed[structure_depth-1], item.id) - - server = beetFileSystem(version="%prog " + fuse.__version__, - usage="", dash_s_do='setsingle') - server.parse(errex=1) - - server.multithreaded = 0 - try: - server.main() - except fuse.FuseError, e: - log.error(str(e)) - - -beetFs_command.func = mount - - -class beetFs(BeetsPlugin): - """ The beets plugin hook.""" - def commands(self): - return [beetFs_command] - - -def to_int_be(string): - """Convert an arbitrarily-long string to a long using big-endian - byte order.""" - return reduce(lambda a, b: (a << 8) + ord(b), string, 0L) - - -class InterpolatedID3 (ID3): - def save(self, filename=None, v1=0): - """Save changes to a file. - If no filename is given, the one most recently loaded is used. - Keyword arguments: - v1 -- if 0, ID3v1 tags will be removed - if 1, ID3v1 tags will be updated but not added - if 2, ID3v1 tags will be created and/or updated - The lack of a way to update only an ID3v1 tag is intentional. - """ - # Sort frames by 'importance' - order = ["TIT2", "TPE1", "TRCK", "TALB", "TPOS", "TDRC", "TCON"] - order = dict(zip(order, range(len(order)))) - last = len(order) - frames = self.items() - frames.sort(lambda a, b: cmp(order.get(a[0][:4], last), - order.get(b[0][:4], last))) - - framedata = [self.__save_frame(frame) for (key, frame) in frames] - framedata.extend([data for data in self.unknown_frames - if len(data) > 10]) - - framedata = ''.join(framedata) - framesize = len(framedata) - - if filename is None: - filename = self.filename - f = open(filename, 'rb+') - try: - idata = f.read(10) - try: - id3, vmaj, vrev, flags, insize = struct.unpack('>3sBBB4s', - idata) - except struct.error: - id3, insize = '', 0 - insize = BitPaddedInt(insize) - if id3 != 'ID3': - insize = -10 - if insize >= framesize: - outsize = insize - else: - outsize = (framesize + 1023) & ~0x3FF - framedata += '\x00' * (outsize - framesize) - - framesize = BitPaddedInt.to_str(outsize, width=4) - flags = 0 - header = struct.pack('>3sBBB4s', 'ID3', 4, 0, flags, framesize) - data = header + framedata - - if (insize < outsize): - insert_bytes(f, outsize-insize, insize+10) - f.seek(0) - - try: - f.seek(-128, 2) - except IOError, err: - if err.errno != EINVAL: - raise - f.seek(0, 2) # ensure read won't get "TAG" - - if f.read(3) == "TAG": - f.seek(-128, 2) - if v1 > 0: - f.write(MakeID3v1(self)) - else: - f.truncate() - elif v1 == 2: - f.seek(0, 2) - f.write(MakeID3v1(self)) - - finally: - f.close() - - -class InterpolatedFLAC (FLAC): - def load(self, filedata): - self.metadata_blocks = [] - self.tags = None - self.cuesheet = None - self.seektable = None - #self.filename = filename - self.filedata = filedata - self.fileobj = BytesIO(filedata) - self.__check_header(self.fileobj) - - while self.__read_metadata_block(self.fileobj): - pass - if self.fileobj.read(2) not in ["\xff\xf8", "\xff\xf9"]: - raise FLACNoHeaderError("End of metadata did not start audio") - - try: - self.metadata_blocks[0].length - except (AttributeError, IndexError): - raise FLACNoHeaderError("Stream info block not found") - - logging.info("Loaded INF") - - def __read_metadata_block(self, file): - byte = ord(file.read(1)) - size = to_int_be(file.read(3)) - try: - data = file.read(size) - if len(data) != size: - raise Exception("file said %d bytes, read %d bytes" - % (size, len(data))) - block = self.METADATA_BLOCKS[byte & 0x7F](data) - except (IndexError, TypeError): - block = MetadataBlock(data) - block.code = byte & 0x7F - self.metadata_blocks.append(block) - else: - self.metadata_blocks.append(block) - if block.code == VCFLACDict.code: - if self.tags is None: - self.tags = block - else: - raise FLACVorbisError("> 1 Vorbis comment block found") - elif block.code == CueSheet.code: - if self.cuesheet is None: - self.cuesheet = block - else: - raise Exception("> 1 CueSheet block found") - elif block.code == SeekTable.code: - if self.seektable is None: - self.seektable = block - else: - raise Exception("> 1 SeekTable block found") - return (byte >> 7) ^ 1 - - def get_header(self, filename=None): - - f = self.fileobj - f.seek(0) - - # Ensure we've got padding at the end, and only at the end. - # If adding makes it too large, we'll scale it down later. - self.metadata_blocks.append(Padding('\x00' * 1020)) - MetadataBlock.group_padding(self.metadata_blocks) - - header = self.__check_header(f) - # "fLaC" and maybe ID3 - available = self.__find_audio_offset(f) - header - data = MetadataBlock.writeblocks(self.metadata_blocks) - - if len(data) > available: - # If we have too much data, see if we can reduce padding. - padding = self.metadata_blocks[-1] - newlength = padding.length - (len(data) - available) - if newlength > 0: - padding.length = newlength - data = MetadataBlock.writeblocks(self.metadata_blocks) - assert len(data) == available - - elif len(data) < available: - # If we have too little data, increase padding. - self.metadata_blocks[-1].length += (available - len(data)) - data = MetadataBlock.writeblocks(self.metadata_blocks) - assert len(data) == available - - self.__offset = len("fLaC" + data) - - return("fLaC" + data) - - def offset(self): - return self.__offset - - def __find_audio_offset(self, fileobj): - byte = 0x00 - while not (byte >> 7) & 1: - byte = ord(fileobj.read(1)) - size = to_int_be(fileobj.read(3)) - fileobj.read(size) - return fileobj.tell() - - def __check_header(self, fileobj): - size = 4 - header = fileobj.read(4) - if header != "fLaC": - size = None - if header[:3] == "ID3": - size = 14 + BitPaddedInt(fileobj.read(6)[2:]) - fileobj.seek(size - 4) - if fileobj.read(4) != "fLaC": - size = None - if size is None: - raise FLACNoHeaderError("%r is not a valid FLAC file" - % fileobj.name) - return size - - -class FSNode(object): - """ A directory node. Contains directories (as a dictionary keyed - by directory name) and files (dictionary keyed by filename to id). - """ - def __init__(self, dirs, files): - self.dirs = dirs - self.files = files - - def getnode(self, elements, root=None): - if root is None: - root = self - if elements: - topdir = elements.pop(0) - return self.getnode(elements, root=root.dirs[topdir]) - else: - # base case - return root - - def adddir(self, elements, directory, root=None): - if root is None: - root = self - - if len(elements) == 1 and elements[0] == '': - elements = [] - node = self.getnode(elements, root=root) - if not directory in node.dirs: - node.dirs[directory] = FSNode({}, {}) - - def addfile(self, elements, filename, id, root=None): - if root is None: - root = self - - if len(elements) == 1 and elements[0] == '': - elements = [] - node = self.getnode(elements, root=root) - node.files[filename] = id - - def listdir(self, elements, directories, root=None): - if root is None: - root = self - if len(elements) == 1 and elements[0] == '': - elements = [] - node = self.getnode(elements, root=root) - if directories: - return node.dirs.keys() - else: - return node.files.keys() - - -class FileHandler(object): - def __init__(self, path, lib): - self.path = path - self.lib = lib - - pathsplit = path[1:].split('/') - - # determine the item and real path - self.item = self.lib.get_item(id=directory_structure - .getnode(pathsplit[0:structure_depth-1]) - .files[pathsplit[structure_depth-1]]) - self.real_path = self.item.path - - # open the on-disk file for reading - self.file_object = open(self.real_path, 'r+') - self.instance_count = 1 - - # now get the bounds of the file_class - #TODO: This needs to handle other file formats; use mutagen's - # detection procedure - self.format = os.path.splitext(path)[1][1:].lower() - #logging.info(self.real_path) - if self.format == "flac": - #self.inf = InterpolatedFLAC(self.real_path) - self.inf = InterpolatedFLAC(self.file_object.read()) - - # get values from database - 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) - self.music_offset = self.inf.offset() - - elif self.format == "mp3": - self.bound = 0 # disable interpolation for now - self.music_offset = 0 # disable interpolation for now - - # read in the contents of the file - self.file_object.seek(self.music_offset) - self.music_data = self.file_object.read() - # close the file - self.file_object.close() - - def open(self): - # as init() handles actual opening, just increment instance count here - self.instance_count = self.instance_count + 1 - - def release(self): - # decrement the instance count - if self.instance_count > 0: - self.instance_count = self.instance_count - 1 - return False - else: - return True - - def read(self, size, offset): - # check if read is within header boundary - if offset < self.bound: - if offset + size < len(self.header): - # can just read from header - logging.info("JUST HEADER") - ret = self.header[offset:offset+size] - return ret - else: - # get the header + some data from file - logging.info("HEADER + DATA") - ret = self.header[offset:len(self.header)] - ret = ret + self.music_data[0:size - ((len(self.header) - - offset))] - return ret - - # otherwise, pass read call to underlying file system - #self.file_object.seek(offset) - logging.info("JUST MUSIC") - return self.music_data[offset - len(self.header):offset - - len(self.header) + size] - - def write(self, offset, buf): - # determine if offset is within header; if not, discard write - - if offset < self.bound: - # load interpolated file into memory - - filedata = self.header + self.music_data - - # patch in the new data - filedata2 = filedata[0:offset] + buf + filedata[offset + len(buf):] - filedata = filedata2 - - # create a new normal mutagen object - if self.format == "flac": - try: - # now obtain a new Interpolated FLAC from the temporary - # file - self.inf = InterpolatedFLAC(filedata) - - # instead of putting the values into the FLAC, extract the - # values - 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')) - - self.lib.store(self.item) - self.lib.save() - - # get values from database - 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) - self.music_offset = self.inf.offset() - - return len(buf) - except IOError: - logging.error("Couldn't update tag.") - pass - - -class Stat(fuse.Stat): - DIRSIZE = 4096 - - def __init__(self, st_mode, st_size, st_nlink=1, st_uid=None, st_gid=None, - dt_atime=None, dt_mtime=None, dt_ctime=None): - - self.st_mode = st_mode - self.st_ino = 0 - self.st_dev = 0 - self.st_nlink = st_nlink - if st_uid is None: - st_uid = os.getuid() - self.st_uid = st_uid - if st_gid is None: - st_gid = os.getgid() - self.st_gid = st_gid - self.st_size = st_size - now = datetime.datetime.utcnow() - self.dt_atime = dt_atime or now - self.dt_mtime = dt_mtime or now - self.dt_ctime = dt_ctime or now - - def _get_dt_atime(self): - return self.epoch_datetime(self.st_atime) - - def _set_dt_atime(self, value): - self.st_atime = self.datetime_epoch(value) - - dt_atime = property(_get_dt_atime, _set_dt_atime) - - def _get_dt_mtime(self): - return self.epoch_datetime(self.st_mtime) - - def _set_dt_mtime(self, value): - self.st_mtime = self.datetime_epoch(value) - dt_mtime = property(_get_dt_mtime, _set_dt_mtime) - - def _get_dt_ctime(self): - return self.epoch_datetime(self.st_ctime) - - def _set_dt_ctime(self, value): - self.st_ctime = self.datetime_epoch(value) - - dt_ctime = property(_get_dt_ctime, _set_dt_ctime) - - @staticmethod - def datetime_epoch(dt): - return calendar.timegm(dt.timetuple()) - - @staticmethod - def epoch_datetime(seconds): - return datetime.datetime.utcfromtimestamp(seconds) - - -class beetFileSystem(fuse.Fuse): - def __init__(self, *args, **kwargs): - LOG_FILENAME = "LOG" - logging.basicConfig(filename=LOG_FILENAME, level=logging.INFO,) - - logging.info("Preparing to mount file system") - super(beetFileSystem, self).__init__(*args, **kwargs) - - def fsinit(self): - # called after filesystem is mounted - #self.lib = self.cmdline[1][0] - self.lib = library - self.files = {} - - logging.info("Filesystem mounted") - - def fsdestroy(self): - logging.info("Unmounting file system") - - def statfs(self): - logging.info("statfs") - - # have no way of knowing where the music is stored - # (disparate locations), so using homedir to fill this in - return os.statvfs(os.path.expanduser("~")) - - def getattr(self, path): - logging.info("getattr: %s" % path) - - try: - if path == "/": - logging.info("Returning /") - mode = stat.S_IFDIR | 0755 - st = Stat(st_mode=mode, st_size=Stat.DIRSIZE, st_nlink=2) - return st - else: - # determine if it's a directory or a file list - # Split path into components - pathsplit = path[1:].split('/') - - if len(pathsplit) == structure_depth: - # it's a file - item = self.lib.get_item(directory_structure - .getnode(pathsplit[0:structure_depth-1]) - .files[pathsplit[structure_depth-1]]).path - - if not item: - # file not found - logging.error("Returning ENOENT") - return -errno.ENOENT - elif item == '': - logging.error("Returning ENOENT") - return -errno.ENOENT - statinfo = os.stat(item) - st = Stat(st_mode=statinfo.st_mode, - st_size=statinfo.st_size, - st_uid=statinfo.st_uid, - st_gid=statinfo.st_gid, - st_nlink=statinfo.st_nlink, - dt_atime=(datetime.datetime - .fromtimestamp(statinfo.st_atime)), - dt_mtime=(datetime.datetime - .fromtimestamp(statinfo.st_mtime)), - dt_ctime=(datetime.datetime - .fromtimestamp(statinfo.st_ctime)) - ) - return st - else: - logging.info("dir") - # it's a directory - if not (pathsplit[len(pathsplit)-1] - in (directory_structure - .getnode(pathsplit[0:len(pathsplit)-1]).dirs)): - # directory not found - logging.error("Returning ENOENT") - return -errno.ENOENT - else: - logging.info("gotdir") - mode = stat.S_IFDIR | 0544 - st = Stat(st_mode=mode, st_size=Stat.DIRSIZE, - st_nlink=2) - return st - - except Exception as e: - logging.error(e) - return -errno.ENOENT - - # Note: utime is deprecated in favour of utimens. - # utimens takes precedence over utime, so having this here does nothing - # unless you delete utimens. - def utime(self, path, times): - atime, mtime = times - logging.info("utime: %s (atime %s, mtime %s)" % (path, atime, mtime)) - return -errno.EOPNOTSUPP - - def utimens(self, path, atime, mtime): - logging.info("utime: %s (atime %s:%s, mtime %s:%s)" - % (path, atime.tv_sec, atime.tv_nsec, mtime.tv_sec, - mtime.tv_nsec)) - return -errno.EOPNOTSUPP - - def access(self, path, flags): - logging.info("access: %s (flags %s)" % (path, oct(flags))) - pathsplit = path[1:].split('/') - if path == "/": - return 0 - else: - is_dir = not len(pathsplit) == structure_depth - - # check for existence - if is_dir: - logging.info("dir") - if not (pathsplit[len(pathsplit) - 1] in directory_structure - .getnode(pathsplit[0:len(pathsplit)-1]).dirs): - return -errno.EACCES - else: - # if exists, always return allowed for directories - return 0 - else: - item = self.lib.get_item(id=directory_structure - .getnode(pathsplit[0:structure_depth-1]) - .files[pathsplit[structure_depth-1]]).path - if not item: - return -errno.EACCES - else: - # TODO: actually check the file permissions - # NB. existence is already tested (os.F_OK) - if flags | os.R_OK: - pass - if flags | os.W_OK: - pass - if flags | os.X_OK: - pass - - return 0 - - def readlink(self, path): - """ - Get the target of a symlink. - Returns a bytestring with the contents of a symlink (its target). - May also return an int error code. - """ - logging.info("readlink: %s" % path) - return -errno.EOPNOTSUPP - - def mknod(self, path, mode, rdev): - """ - Creates a non-directory file (or a device node). - mode: Unix file mode flags for the file being created. - rdev: Special properties for creation of character or block special - devices (I've never gotten this to work). - Always 0 for regular files or FIFO buffers. - """ - # Note: mode & 0770000 gives you the non-permission bits. - # Common ones: - # S_IFREG: 0100000 (A regular file) - # S_IFIFO: 010000 (A fifo buffer, created with mkfifo) - - # Potential ones (I have never seen them): - # Note that these could be made by copying special devices or - # sockets or using mknod, but I've never gotten FUSE to pass such - # a request along. - # S_IFCHR: 020000 (character special device, created with mknod) - # S_IFBLK: 060000 (block special device, created with mknod) - # S_IFSOCK: 0140000 (socket, created with mkfifo) - - # Also note: You can use self.GetContext() to get a dictionary - # {'uid': ?, 'gid': ?}, which tells you the uid/gid of - # the user executing the current syscall. This should be - # handy when creating new files and directories, because - # they should be owned by this user/group. - logging.info("mknod: %s (mode %s, rdev %s)" % (path, oct(mode), - rdev)) - return -errno.EOPNOTSUPP - - def mkdir(self, path, mode): - """ - Creates a directory. - mode: Unix file mode flags for the directory being created. - """ - # Note: mode & 0770000 gives you the non-permission bits. - # Should be S_IDIR (040000); I guess you can assume this. - # Also see note about self.GetContext() in mknod. - logging.info("mkdir: %s (mode %s)" % (path, oct(mode))) - return -errno.EOPNOTSUPP - - def unlink(self, path): - """ Deletes a file.""" - logging.info("unlink: %s" % path) - return -errno.EOPNOTSUPP - - def rmdir(self, path): - """ Deletes a directory.""" - logging.info("rmdir: %s" % path) - return -errno.EOPNOTSUPP - - def symlink(self, target, name): - """ - Creates a symbolic link from path to target. - - The 'name' is a regular path like any other method (absolute, but - relative to the filesystem root). - The 'target' is special - it works just like any symlink target. It - may be absolute, in which case it is absolute on the user's system, - NOT the mounted filesystem, or it may be relative. It should be - treated as an opaque string - the filesystem implementation should - not ever need to follow it (that is handled by the OS). - - Hence, if the operating system creates a link FROM this system TO - another system, it will call this method with a target pointing - outside the filesystem. - If the operating system creates a link FROM some other system TO - this system, it will not touch this system at all (symlinks do not - depend on the target system unless followed). - """ - logging.info("symlink: target %s, name: %s" % (target, name)) - return -errno.EOPNOTSUPP - - def link(self, target, name): - """ - Creates a hard link from name to target. Note that both paths are - relative to the mounted file system. Hard-links across systems are - not supported. - """ - logging.info("link: target %s, name: %s" % (target, name)) - return -errno.EOPNOTSUPP - - def rename(self, old, new): - """ - Moves a file from old to new. (old and new are both full paths, and - may not be in the same directory). - - Note that both paths are relative to the mounted file system. - If the operating system needs to move files across systems, it will - manually copy and delete the file, and this method will not be - called. - """ - logging.info("rename: target %s, name: %s" % (old, new)) - return -errno.EOPNOTSUPP - - def chmod(self, path, mode): - """ Changes the mode of a file or directory.""" - logging.info("chmod: %s (mode %s)" % (path, oct(mode))) - return -errno.EOPNOTSUPP - - def chown(self, path, uid, gid): - """ Changes the owner of a file or directory.""" - logging.info("chown: %s (uid %s, gid %s)" % (path, uid, gid)) - return -errno.EOPNOTSUPP - - def truncate(self, path, size): - """ - Shrink or expand a file to a given size. - If 'size' is smaller than the existing file size, truncate it from - the end. - If 'size' if larger than the existing file size, extend it with - null bytes. - """ - logging.info("truncate: %s (size %s)" % (path, size)) - return -errno.EOPNOTSUPP - - ### DIRECTORY OPERATION METHODS ### - # Methods in this section are operations for opening directories and - # working on open directories. - # "opendir" is the method for opening directories. It *may* return an - # arbitrary Python object (not None or int), which is used as a dir - # handle by the methods for working on directories. - # All the other methods (readdir, fsyncdir, releasedir) are methods for - # working on directories. They should all be prepared to accept an - # optional dir-handle argument, which is whatever object "opendir" - # returned. - - def opendir(self, path): - """ - Checks permissions for listing a directory. - This should check the 'r' (read) permission on the directory. - - On success, *may* return an arbitrary Python object, which will be - used as the "fh" argument to all the directory operation methods on - the directory. Or, may just return None on success. - On failure, should return a negative errno code. - Should return -errno.EACCES if disallowed. - """ - logging.info("opendir: %s" % path) - pathsplit = path[1:].split('/') - if path == "/": - return directory_structure - else: - if not (pathsplit[len(pathsplit) - 1] - in directory_structure - .getnode(pathsplit[0:len(pathsplit)-1]).dirs): - return -errno.EACCES - else: - return (directory_structure - .getnode(pathsplit[0:len(pathsplit)-1]) - .dirs[pathsplit[len(pathsplit) - 1]]) - - def releasedir(self, path, dh=None): - """ Closes an open directory. Allows filesystem to clean up.""" - logging.info("releasedir: %s (dh %s)" % (path, dh)) - - def fsyncdir(self, path, datasync, dh=None): - """ - Synchronises an open directory. - datasync: If True, only flush user data, not metadata. - """ - logging.info("fsyncdir: %s (datasync %s, dh %s)" - % (path, datasync, dh)) - - def readdir(self, path, offset, dh=None): - """ - Generator function. Produces a directory listing. - Yields individual fuse.Direntry objects, one per file in the - directory. Should always yield at least "." and "..". - Should yield nothing if the file is not a directory or does not - exist. (Does not need to raise an error). - - offset: I don't know what this does, but I think it allows the OS - to request starting the listing partway through (which I clearly - don't yet support). Seems to always be 0 anyway. - """ - logging.info("readdir: %s (offset %s, dh %s)" % (path, offset, dh)) - - yield fuse.Direntry(".") - yield fuse.Direntry("..") - - try: - pathsplit = path[1:].split('/') - - if dh is None: - if path == "/": - logging.info("dh assigned as root") - dh = directory_structure - else: - dh = (directory_structure - .getnode(pathsplit[0:len(pathsplit)-1]) - .dirs[pathsplit[len(pathsplit)]]) - - if len(pathsplit) == structure_depth - 1: - # files - logging.info("Yielding files: %s" % path) - for files in directory_structure.listdir(pathsplit, False): - logging.info("Yielding file: %s" - % files.encode('utf-8')) - yield fuse.Direntry(files.encode('utf-8')) - else: - # directories - for files in directory_structure.listdir(pathsplit, True): - #logging.info("Yielding dir: %s" - # % files.encode('utf-8')) - yield fuse.Direntry(files.encode('utf-8')) - - except Exception as e: - logging.error(e) - - ### FILE OPERATION METHODS ### - # Methods in this section are operations for opening files and working - # on open files. - # "open" and "create" are methods for opening files. They *may* return - # an arbitrary Python object (not None or int), which is used as a file - # handle by the methods for working on files. - # All the other methods (fgetattr, release, read, write, fsync, flush, - # ftruncate and lock) are methods for working on files. They should all - # be prepared to accept an optional file-handle argument, which is - # whatever object "open" or "create" returned. - - def open(self, path, flags): - """ - Open a file for reading/writing, and check permissions. - flags: As described in man 2 open (Linux Programmer's Manual). - ORing of several access flags, including one of - os.O_RDONLY, os.O_WRONLY or os.O_RDWR. All other flags are - in os as well. - - On success, *may* return an arbitrary Python object, which will be - used as the "fh" argument to all the file operation methods on the - file. Or, may just return None on success. - On failure, should return a negative errno code. - Should return -errno.EACCES if disallowed. - """ - logging.info("open: %s (flags %s)" % (path, oct(flags))) - - try: - if self.files is None: - self.files = {"x": "y"} - - if path in self.files: - # get a file object - logging.info("Retrieving an existing File Handler for: %s" - % path) - self.files[path].open() - else: - # create a file open - logging.info("Creating a File Handler for: %s" % path) - self.files[path] = FileHandler(path, self.lib) - - return self.files[path] - except Exception as e: - logging.info("Error creating a File Handler: %s" % e) - return -errno.EACCES - - def create(self, path, mode, rdev): - """ - Creates a file and opens it for writing. - Will be called in favour of mknod+open, but it's optional (OS will - fall back on that sequence). - mode: Unix file mode flags for the file being created. - rdev: Special properties for creation of character or block special - devices (I've never gotten this to work). - Always 0 for regular files or FIFO buffers. - See "open" for return value. - """ - logging.info("create: %s (mode %s, rdev %s)" - % (path, oct(mode), rdev)) - return -errno.EOPNOTSUPP - - def fgetattr(self, path, fh=None): - """ - Retrieves information about a file (the "stat" of a file). - Same as Fuse.getattr, but may be given a file handle to an open - file, so it can use that instead of having to look up the path. - """ - logging.info("fgetattr: %s (fh %s)" % (path, fh)) - # We could use fh for a more efficient lookup. Here we just call - # the non-file-handle version, getattr. - return self.getattr(path) - - def release(self, path, flags, fh=None): - """ - Closes an open file. Allows filesystem to clean up. - flags: The same flags the file was opened with (see open). - """ - logging.info("release: %s (flags %s, fh %s)" % (path, oct(flags), - fh)) - if self.files[path].release(): - logging.info("Complete release: %s (flags %s, fh %s)" - % (path, oct(flags), fh)) - del self.files[path] - - def fsync(self, path, datasync, fh=None): - """ - Synchronises an open file. - datasync: If True, only flush user data, not metadata. - """ - logging.info("fsync: %s (datasync %s, fh %s)" - % (path, datasync, fh)) - - def flush(self, path, fh=None): - """ - Flush cached data to the file system. - This is NOT an fsync (I think the difference is fsync goes both - ways, while flush is just one-way). - """ - logging.info("flush: %s (fh %s)" % (path, fh)) - - def read(self, path, size, offset, fh=None): - """ - Get all or part of the contents of a file. - size: Size in bytes to read. - offset: Offset in bytes from the start of the file to read from. - Does not need to check access rights (operating system will always - call access or open first). - Returns a byte string with the contents of the file, with a length - no greater than 'size'. May also return an int error code. - - If the length of the returned string is 0, it indicates the end of - the file, and the OS will not request any more. If the length is - nonzero, the OS may request more bytes later. - To signal that it is NOT the end of file, but no bytes are - presently available (and it is a non-blocking read), return - -errno.EAGAIN. - If it is a blocking read, just block until ready. - """ - logging.info("read: %s (size %s, offset %s, fh %s)" - % (path, size, offset, fh)) - - if fh is None: - try: - if self.files is None: - self.files = {"x": "y"} - self.files[path] = FileHandler(path, self.lib) - except: - return -errno.EPERM - - return self.files[path].read(size, offset) - - def write(self, path, buf, offset, fh=None): - """ - Write over part of a file. - buf: Byte string containing the text to write. - offset: Offset in bytes from the start of the file to write to. - Does not need to check access rights (operating system will always - call access or open first). - Should only overwrite the part of the file from offset to - offset+len(buf). - - Must return an int: the number of bytes successfully written - (should be equal to len(buf) unless an error occured). May also be - a negative int, which is an errno code. - """ - logging.info("write: %s (offset %s, fh %s)" % (path, offset, fh)) - - if fh is None: - try: - if self.files is None: - self.files = {"x": "y"} - self.files[path] = FileHandler(path, self.lib) - except: - return -errno.EPERM - - try: - return self.files[path].write(offset, buf) - except Exception as ex: - logging.info(ex) - - def ftruncate(self, path, size, fh=None): - """ - Shrink or expand a file to a given size. - Same as Fuse.truncate, but may be given a file handle to an open - file, so it can use that instead of having to look up the path. - """ - logging.info("ftruncate: %s (size %s, fh %s)" % (path, size, fh)) - return -errno.EOPNOTSUPP diff --git a/musicfs/crates/musicfs-cache/Cargo.toml b/crates/musicfs-cache/Cargo.toml similarity index 100% rename from musicfs/crates/musicfs-cache/Cargo.toml rename to crates/musicfs-cache/Cargo.toml diff --git a/musicfs/crates/musicfs-cache/src/artwork.rs b/crates/musicfs-cache/src/artwork.rs similarity index 94% rename from musicfs/crates/musicfs-cache/src/artwork.rs rename to crates/musicfs-cache/src/artwork.rs index 809805c..e684448 100644 --- a/musicfs/crates/musicfs-cache/src/artwork.rs +++ b/crates/musicfs-cache/src/artwork.rs @@ -48,9 +48,18 @@ impl ArtworkCache { } pub async fn store(&self, file_id: i64, artwork: &Artwork) -> Result { - trace!(file_id = file_id, size_bytes = artwork.data.len(), "Storing artwork"); + trace!( + file_id = file_id, + size_bytes = artwork.data.len(), + "Storing artwork" + ); if artwork.data.len() > MAX_ARTWORK_INPUT_SIZE { - warn!(file_id = file_id, size = artwork.data.len(), max = MAX_ARTWORK_INPUT_SIZE, "Artwork too large"); + warn!( + file_id = file_id, + size = artwork.data.len(), + max = MAX_ARTWORK_INPUT_SIZE, + "Artwork too large" + ); return Err(ArtworkError::ImageTooLarge(artwork.data.len())); } diff --git a/musicfs/crates/musicfs-cache/src/db.rs b/crates/musicfs-cache/src/db.rs similarity index 96% rename from musicfs/crates/musicfs-cache/src/db.rs rename to crates/musicfs-cache/src/db.rs index b2f759e..b2ccb9e 100644 --- a/musicfs/crates/musicfs-cache/src/db.rs +++ b/crates/musicfs-cache/src/db.rs @@ -35,8 +35,8 @@ impl Database { pub fn open_with_integrity_check(path: &Path) -> Result { debug!(?path, "Opening database with integrity check"); - let conn = Connection::open(path) - .map_err(|e| Error::Database(format!("open failed: {}", e)))?; + let conn = + Connection::open(path).map_err(|e| Error::Database(format!("open failed: {}", e)))?; let integrity: String = conn .query_row("PRAGMA integrity_check(1)", [], |row| row.get(0)) @@ -45,7 +45,8 @@ impl Database { if integrity != "ok" { warn!(path = ?path, result = %integrity, "Database integrity check failed"); return Err(Error::DatabaseCorrupted(format!( - "integrity check failed: {}", integrity + "integrity check failed: {}", + integrity ))); } @@ -250,11 +251,9 @@ impl Database { pub fn file_count(&self) -> Result { let conn = self.conn.lock().unwrap(); - conn.query_row("SELECT COUNT(*) FROM files", [], |row| { - row.get::<_, i64>(0) - }) - .map(|c| c as u64) - .map_err(|e| Error::Database(format!("count failed: {}", e))) + conn.query_row("SELECT COUNT(*) FROM files", [], |row| row.get::<_, i64>(0)) + .map(|c| c as u64) + .map_err(|e| Error::Database(format!("count failed: {}", e))) } pub fn update_content_hash(&self, id: FileId, hash: &ContentHash) -> Result<()> { @@ -352,10 +351,7 @@ mod tests { ) .unwrap(); - let retrieved = db - .get_file_by_virtual_path(&virtual_path) - .unwrap() - .unwrap(); + let retrieved = db.get_file_by_virtual_path(&virtual_path).unwrap().unwrap(); assert_eq!(retrieved.id, id); assert_eq!( retrieved.audio.as_ref().unwrap().title, @@ -401,10 +397,7 @@ mod tests { assert_eq!(db.file_count().unwrap(), 1); - let retrieved = db - .get_file_by_virtual_path(&virtual_path) - .unwrap() - .unwrap(); + let retrieved = db.get_file_by_virtual_path(&virtual_path).unwrap().unwrap(); assert_eq!( retrieved.audio.as_ref().unwrap().title, Some("Updated".to_string()) diff --git a/musicfs/crates/musicfs-cache/src/eviction.rs b/crates/musicfs-cache/src/eviction.rs similarity index 100% rename from musicfs/crates/musicfs-cache/src/eviction.rs rename to crates/musicfs-cache/src/eviction.rs diff --git a/musicfs/crates/musicfs-cache/src/lib.rs b/crates/musicfs-cache/src/lib.rs similarity index 100% rename from musicfs/crates/musicfs-cache/src/lib.rs rename to crates/musicfs-cache/src/lib.rs diff --git a/musicfs/crates/musicfs-cache/src/metadata.rs b/crates/musicfs-cache/src/metadata.rs similarity index 95% rename from musicfs/crates/musicfs-cache/src/metadata.rs rename to crates/musicfs-cache/src/metadata.rs index bcc3859..f1960e7 100644 --- a/musicfs/crates/musicfs-cache/src/metadata.rs +++ b/crates/musicfs-cache/src/metadata.rs @@ -94,7 +94,14 @@ mod tests { }; cache - .store(&origin_id, real_path, &virtual_path, &meta, UNIX_EPOCH, 5000) + .store( + &origin_id, + real_path, + &virtual_path, + &meta, + UNIX_EPOCH, + 5000, + ) .unwrap(); let retrieved = cache.lookup(&virtual_path).unwrap().unwrap(); diff --git a/musicfs/crates/musicfs-cache/src/patterns.rs b/crates/musicfs-cache/src/patterns.rs similarity index 96% rename from musicfs/crates/musicfs-cache/src/patterns.rs rename to crates/musicfs-cache/src/patterns.rs index 6a7f44c..2a2b8c6 100644 --- a/musicfs/crates/musicfs-cache/src/patterns.rs +++ b/crates/musicfs-cache/src/patterns.rs @@ -63,13 +63,11 @@ impl PatternStore { let sequence_counts = { let mut map = HashMap::new(); - let mut stmt = db.prepare("SELECT from_file_id, to_file_id, count FROM sequence_counts")?; + let mut stmt = + db.prepare("SELECT from_file_id, to_file_id, count FROM sequence_counts")?; let rows = stmt.query_map([], |row| { Ok(( - ( - FileId(row.get::<_, i64>(0)?), - FileId(row.get::<_, i64>(1)?), - ), + (FileId(row.get::<_, i64>(0)?), FileId(row.get::<_, i64>(1)?)), row.get::<_, u32>(2)?, )) })?; @@ -154,7 +152,11 @@ impl PatternStore { .take(limit) .map(|(id, _)| id) .collect(); - debug!(file_id = current.0, predictions = result.len(), "Predicted next files"); + debug!( + file_id = current.0, + predictions = result.len(), + "Predicted next files" + ); result } diff --git a/musicfs/crates/musicfs-cache/src/prefetch.rs b/crates/musicfs-cache/src/prefetch.rs similarity index 94% rename from musicfs/crates/musicfs-cache/src/prefetch.rs rename to crates/musicfs-cache/src/prefetch.rs index 5462959..b937685 100644 --- a/musicfs/crates/musicfs-cache/src/prefetch.rs +++ b/crates/musicfs-cache/src/prefetch.rs @@ -102,13 +102,8 @@ impl PrefetchEngine { pattern_store.predict_next(file_id, config.lookahead); for predicted_id in predictions { - prefetch_file( - predicted_id, - &fetcher, - &in_flight, - &semaphore, - ) - .await; + prefetch_file(predicted_id, &fetcher, &in_flight, &semaphore) + .await; } tokio::time::sleep(config.cooldown).await; diff --git a/musicfs/crates/musicfs-cache/src/schema.sql b/crates/musicfs-cache/src/schema.sql similarity index 100% rename from musicfs/crates/musicfs-cache/src/schema.sql rename to crates/musicfs-cache/src/schema.sql diff --git a/musicfs/crates/musicfs-cache/src/tree.rs b/crates/musicfs-cache/src/tree.rs similarity index 96% rename from musicfs/crates/musicfs-cache/src/tree.rs rename to crates/musicfs-cache/src/tree.rs index 8903a26..6d22cc9 100644 --- a/musicfs/crates/musicfs-cache/src/tree.rs +++ b/crates/musicfs-cache/src/tree.rs @@ -102,8 +102,7 @@ impl VirtualTree { mtime: SystemTime::now(), }), ); - tree.path_to_inode - .insert(VirtualPath::new("/"), ROOT_INODE); + tree.path_to_inode.insert(VirtualPath::new("/"), ROOT_INODE); tree } @@ -161,13 +160,11 @@ impl VirtualTree { fn find_parent_by_path_lookup(&self, inode: Inode) -> Option { for (path, &ino) in &self.path_to_inode { if ino == inode { - return std::path::Path::new(path.as_str()) - .parent() - .and_then(|p| { - self.path_to_inode - .get(&VirtualPath::new(p.to_string_lossy().into_owned())) - .copied() - }); + return std::path::Path::new(path.as_str()).parent().and_then(|p| { + self.path_to_inode + .get(&VirtualPath::new(p.to_string_lossy().into_owned())) + .copied() + }); } } None diff --git a/musicfs/crates/musicfs-cas/Cargo.toml b/crates/musicfs-cas/Cargo.toml similarity index 100% rename from musicfs/crates/musicfs-cas/Cargo.toml rename to crates/musicfs-cas/Cargo.toml diff --git a/musicfs/crates/musicfs-cas/src/chunks.rs b/crates/musicfs-cas/src/chunks.rs similarity index 100% rename from musicfs/crates/musicfs-cas/src/chunks.rs rename to crates/musicfs-cas/src/chunks.rs diff --git a/musicfs/crates/musicfs-cas/src/fetcher.rs b/crates/musicfs-cas/src/fetcher.rs similarity index 98% rename from musicfs/crates/musicfs-cas/src/fetcher.rs rename to crates/musicfs-cas/src/fetcher.rs index c0e9bac..d3e94ee 100644 --- a/musicfs/crates/musicfs-cas/src/fetcher.rs +++ b/crates/musicfs-cas/src/fetcher.rs @@ -69,11 +69,7 @@ impl ContentFetcher { .ok_or_else(|| FetchError::OriginNotFound(meta.real_path.origin_id.clone()))? }; - info!( - "Fetching file {:?} from origin {}", - file_id, - origin.id() - ); + info!("Fetching file {:?} from origin {}", file_id, origin.id()); let data = origin .read_full(&meta.real_path.path) diff --git a/musicfs/crates/musicfs-cas/src/lib.rs b/crates/musicfs-cas/src/lib.rs similarity index 100% rename from musicfs/crates/musicfs-cas/src/lib.rs rename to crates/musicfs-cas/src/lib.rs diff --git a/musicfs/crates/musicfs-cas/src/reader.rs b/crates/musicfs-cas/src/reader.rs similarity index 96% rename from musicfs/crates/musicfs-cas/src/reader.rs rename to crates/musicfs-cas/src/reader.rs index a386a6f..31bc97c 100644 --- a/musicfs/crates/musicfs-cas/src/reader.rs +++ b/crates/musicfs-cas/src/reader.rs @@ -26,7 +26,12 @@ impl ChunkManifest { rmp_serde::from_slice(data).ok() } - pub fn from_db(file_id: FileId, total_size: u64, mtime: i64, chunk_blob: &[u8]) -> Option { + pub fn from_db( + file_id: FileId, + total_size: u64, + mtime: i64, + chunk_blob: &[u8], + ) -> Option { let chunks = Self::chunks_from_bytes(chunk_blob)?; Some(Self { file_id, @@ -80,9 +85,7 @@ impl FileReader { }; let manifest = fetcher.ensure_cached(file_id).await?; - self.manifests - .write() - .insert(file_id, manifest.clone()); + self.manifests.write().insert(file_id, manifest.clone()); Ok(manifest) } @@ -126,7 +129,9 @@ impl FileReader { self.manifests.write().insert(file_id, new_manifest); self.store.get(&chunk_ref.hash).await? } else { - return Err(ReaderError::Cas(CasError::NotFound(chunk_ref.hash.as_hex()))); + return Err(ReaderError::Cas(CasError::NotFound( + chunk_ref.hash.as_hex(), + ))); } } Err(CasError::NotFound(_)) => { @@ -136,7 +141,9 @@ impl FileReader { self.manifests.write().insert(file_id, new_manifest); self.store.get(&chunk_ref.hash).await? } else { - return Err(ReaderError::Cas(CasError::NotFound(chunk_ref.hash.as_hex()))); + return Err(ReaderError::Cas(CasError::NotFound( + chunk_ref.hash.as_hex(), + ))); } } Err(e) => return Err(ReaderError::Cas(e)), diff --git a/musicfs/crates/musicfs-cas/src/store.rs b/crates/musicfs-cas/src/store.rs similarity index 98% rename from musicfs/crates/musicfs-cas/src/store.rs rename to crates/musicfs-cas/src/store.rs index c8f9d1f..5d804ab 100644 --- a/musicfs/crates/musicfs-cas/src/store.rs +++ b/crates/musicfs-cas/src/store.rs @@ -58,8 +58,7 @@ impl CasStore { Err(repair_err) => { warn!(error = %repair_err, "sled repair failed, recreating index"); if index_path.exists() { - std::fs::remove_dir_all(&index_path) - .map_err(CasError::Io)?; + std::fs::remove_dir_all(&index_path).map_err(CasError::Io)?; } sled::open(&index_path)? } @@ -80,7 +79,9 @@ impl CasStore { Self::calculate_size_recursive(dir).await } - fn calculate_size_recursive(dir: &Path) -> std::pin::Pin + Send + '_>> { + fn calculate_size_recursive( + dir: &Path, + ) -> std::pin::Pin + Send + '_>> { Box::pin(async move { let mut size = 0u64; if let Ok(mut entries) = fs::read_dir(dir).await { diff --git a/musicfs/crates/musicfs-cas/tests/integration.rs b/crates/musicfs-cas/tests/integration.rs similarity index 96% rename from musicfs/crates/musicfs-cas/tests/integration.rs rename to crates/musicfs-cas/tests/integration.rs index 0d7753a..923fb40 100644 --- a/musicfs/crates/musicfs-cas/tests/integration.rs +++ b/crates/musicfs-cas/tests/integration.rs @@ -117,7 +117,10 @@ async fn test_fetcher_cache_miss_flow() { let store = Arc::new(CasStore::open(config).await.unwrap()); let origin_id = OriginId::from("test-origin"); - let origin = Arc::new(LocalOrigin::new(origin_id.clone(), origin_dir.path().to_path_buf())); + let origin = Arc::new(LocalOrigin::new( + origin_id.clone(), + origin_dir.path().to_path_buf(), + )); let fetcher = ContentFetcher::new(store.clone()); fetcher.register_origin(origin); @@ -163,7 +166,10 @@ async fn test_reader_with_fetcher_integration() { let store = Arc::new(CasStore::open(config).await.unwrap()); let origin_id = OriginId::from("local"); - let origin = Arc::new(LocalOrigin::new(origin_id.clone(), origin_dir.path().to_path_buf())); + let origin = Arc::new(LocalOrigin::new( + origin_id.clone(), + origin_dir.path().to_path_buf(), + )); let fetcher = ContentFetcher::new(store.clone()); fetcher.register_origin(origin); diff --git a/musicfs/crates/musicfs-cli/Cargo.toml b/crates/musicfs-cli/Cargo.toml similarity index 100% rename from musicfs/crates/musicfs-cli/Cargo.toml rename to crates/musicfs-cli/Cargo.toml diff --git a/musicfs/crates/musicfs-cli/src/lib.rs b/crates/musicfs-cli/src/lib.rs similarity index 100% rename from musicfs/crates/musicfs-cli/src/lib.rs rename to crates/musicfs-cli/src/lib.rs diff --git a/musicfs/crates/musicfs-cli/src/main.rs b/crates/musicfs-cli/src/main.rs similarity index 96% rename from musicfs/crates/musicfs-cli/src/main.rs rename to crates/musicfs-cli/src/main.rs index 10a5479..d7d327f 100644 --- a/musicfs/crates/musicfs-cli/src/main.rs +++ b/crates/musicfs-cli/src/main.rs @@ -82,12 +82,8 @@ enum CacheCommands { #[derive(Subcommand)] enum OriginCommands { List, - Health { - origin_id: String, - }, - Rescan { - origin_id: String, - }, + Health { origin_id: String }, + Rescan { origin_id: String }, } struct LockFile { @@ -245,8 +241,7 @@ fn run_mount( runtime.block_on(async { let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?; - let mut sigint = - tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())?; + let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())?; tokio::select! { _ = sigterm.recv() => { @@ -290,10 +285,7 @@ fn run_cache(command: CacheCommands) -> Result<()> { println!("Cache stats: gRPC client integration pending"); } CacheCommands::Clear { origin } => { - println!( - "Clearing cache for: {}", - origin.as_deref().unwrap_or("all") - ); + println!("Clearing cache for: {}", origin.as_deref().unwrap_or("all")); println!("gRPC client integration pending"); } CacheCommands::Prefetch { paths } => { @@ -364,8 +356,8 @@ fn init_logging(config: &LoggingConfig) -> Result { let stderr_layer = fmt::layer().with_writer(std::io::stderr).compact(); - let filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new(&config.level)); + let filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.level)); let subscriber = tracing_subscriber::registry() .with(filter) @@ -488,10 +480,7 @@ fn build_virtual_path(path: &Path, audio: Option<&musicfs_core::AudioMeta>) -> V if let Some(meta) = audio { let artist = meta.artist.as_deref().unwrap_or("Unknown Artist"); let album = meta.album.as_deref().unwrap_or("Unknown Album"); - let filename = path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("track"); + let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("track"); VirtualPath::new(&format!( "/{}/{}/{}", diff --git a/musicfs/crates/musicfs-core/Cargo.toml b/crates/musicfs-core/Cargo.toml similarity index 100% rename from musicfs/crates/musicfs-core/Cargo.toml rename to crates/musicfs-core/Cargo.toml diff --git a/musicfs/crates/musicfs-core/src/config.rs b/crates/musicfs-core/src/config.rs similarity index 100% rename from musicfs/crates/musicfs-core/src/config.rs rename to crates/musicfs-core/src/config.rs diff --git a/musicfs/crates/musicfs-core/src/credentials.rs b/crates/musicfs-core/src/credentials.rs similarity index 100% rename from musicfs/crates/musicfs-core/src/credentials.rs rename to crates/musicfs-core/src/credentials.rs diff --git a/musicfs/crates/musicfs-core/src/error.rs b/crates/musicfs-core/src/error.rs similarity index 100% rename from musicfs/crates/musicfs-core/src/error.rs rename to crates/musicfs-core/src/error.rs diff --git a/musicfs/crates/musicfs-core/src/events.rs b/crates/musicfs-core/src/events.rs similarity index 94% rename from musicfs/crates/musicfs-core/src/events.rs rename to crates/musicfs-core/src/events.rs index b9f49a7..2aea6d6 100644 --- a/musicfs/crates/musicfs-core/src/events.rs +++ b/crates/musicfs-core/src/events.rs @@ -16,7 +16,10 @@ impl EventBus { trace!(event = ?event, "Publishing event"); let receiver_count = self.sender.receiver_count(); if self.sender.send(event).is_err() && receiver_count > 0 { - debug!(receiver_count = receiver_count, "Event dropped, no active receivers"); + debug!( + receiver_count = receiver_count, + "Event dropped, no active receivers" + ); } } diff --git a/musicfs/crates/musicfs-core/src/lib.rs b/crates/musicfs-core/src/lib.rs similarity index 100% rename from musicfs/crates/musicfs-core/src/lib.rs rename to crates/musicfs-core/src/lib.rs diff --git a/musicfs/crates/musicfs-core/src/metrics.rs b/crates/musicfs-core/src/metrics.rs similarity index 95% rename from musicfs/crates/musicfs-core/src/metrics.rs rename to crates/musicfs-core/src/metrics.rs index 452eec8..d1491c8 100644 --- a/musicfs/crates/musicfs-core/src/metrics.rs +++ b/crates/musicfs-core/src/metrics.rs @@ -22,9 +22,7 @@ impl Metrics { } pub fn uptime_secs(&self) -> u64 { - self.start_time - .map(|t| t.elapsed().as_secs()) - .unwrap_or(0) + self.start_time.map(|t| t.elapsed().as_secs()).unwrap_or(0) } pub fn to_prometheus(&self) -> String { @@ -55,11 +53,16 @@ impl Metrics { musicfs_fuse_latency_seconds{{op=\"{}\",quantile=\"0.99\"}} {:.6}\n\ musicfs_fuse_latency_seconds_sum{{op=\"{}\"}} {:.6}\n\ musicfs_fuse_latency_seconds_count{{op=\"{}\"}} {}\n", - op, quantiles.p50, - op, quantiles.p95, - op, quantiles.p99, - op, histogram.sum_secs(), - op, histogram.count(), + op, + quantiles.p50, + op, + quantiles.p95, + op, + quantiles.p99, + op, + histogram.sum_secs(), + op, + histogram.count(), )); } @@ -266,9 +269,7 @@ pub struct OriginHealthMetrics { impl OriginHealthMetrics { pub fn set_health(&self, origin_id: &str, healthy: bool) { - self.status - .write() - .insert(origin_id.to_string(), healthy); + self.status.write().insert(origin_id.to_string(), healthy); } } diff --git a/musicfs/crates/musicfs-core/src/resolver.rs b/crates/musicfs-core/src/resolver.rs similarity index 100% rename from musicfs/crates/musicfs-core/src/resolver.rs rename to crates/musicfs-core/src/resolver.rs diff --git a/musicfs/crates/musicfs-core/src/supervisor.rs b/crates/musicfs-core/src/supervisor.rs similarity index 100% rename from musicfs/crates/musicfs-core/src/supervisor.rs rename to crates/musicfs-core/src/supervisor.rs diff --git a/musicfs/crates/musicfs-core/src/types.rs b/crates/musicfs-core/src/types.rs similarity index 100% rename from musicfs/crates/musicfs-core/src/types.rs rename to crates/musicfs-core/src/types.rs diff --git a/musicfs/crates/musicfs-fuse/Cargo.toml b/crates/musicfs-fuse/Cargo.toml similarity index 100% rename from musicfs/crates/musicfs-fuse/Cargo.toml rename to crates/musicfs-fuse/Cargo.toml diff --git a/musicfs/crates/musicfs-fuse/src/filesystem.rs b/crates/musicfs-fuse/src/filesystem.rs similarity index 96% rename from musicfs/crates/musicfs-fuse/src/filesystem.rs rename to crates/musicfs-fuse/src/filesystem.rs index 57f57ab..39daf31 100644 --- a/musicfs/crates/musicfs-fuse/src/filesystem.rs +++ b/crates/musicfs-fuse/src/filesystem.rs @@ -46,7 +46,11 @@ impl MusicFs { } } - pub fn with_reader(tree: Arc>, reader: Arc, runtime_handle: Handle) -> Self { + pub fn with_reader( + tree: Arc>, + reader: Arc, + runtime_handle: Handle, + ) -> Self { Self { tree, reader: Some(reader), @@ -287,7 +291,12 @@ impl Filesystem for MusicFs { let tree = self.tree.read(); if let Some(children) = tree.readdir(ino) { - trace!(ino, offset, children_count = children.len(), "directory found"); + trace!( + ino, + offset, + children_count = children.len(), + "directory found" + ); let parent_ino = tree.get_parent(ino).unwrap_or(ROOT_INODE); let entries: Vec<(u64, FileType, &str)> = vec![ @@ -396,7 +405,13 @@ impl Filesystem for MusicFs { match result { Ok(Ok(data)) => { - trace!(ino, offset, size_bytes = size, bytes_read = data.len(), "read successful"); + trace!( + ino, + offset, + size_bytes = size, + bytes_read = data.len(), + "read successful" + ); reply.data(&data); } Ok(Err(e)) => { @@ -582,7 +597,7 @@ mod tests { fn test_tree_integration() { let runtime = tokio::runtime::Runtime::new().unwrap(); let handle = runtime.handle().clone(); - + let mut builder = TreeBuilder::new(); builder.add_file(&make_file_meta(1, "/Artist/Album/Track.flac", 30_000_000)); let tree = Arc::new(RwLock::new(builder.build())); @@ -591,6 +606,8 @@ mod tests { let tree_read = tree.read(); assert!(tree_read.get(ROOT_INODE).is_some()); - assert!(tree_read.get_by_path(&VirtualPath::new("/Artist")).is_some()); + assert!(tree_read + .get_by_path(&VirtualPath::new("/Artist")) + .is_some()); } } diff --git a/musicfs/crates/musicfs-fuse/src/lib.rs b/crates/musicfs-fuse/src/lib.rs similarity index 100% rename from musicfs/crates/musicfs-fuse/src/lib.rs rename to crates/musicfs-fuse/src/lib.rs diff --git a/musicfs/crates/musicfs-fuse/src/ops/mod.rs b/crates/musicfs-fuse/src/ops/mod.rs similarity index 100% rename from musicfs/crates/musicfs-fuse/src/ops/mod.rs rename to crates/musicfs-fuse/src/ops/mod.rs diff --git a/musicfs/crates/musicfs-fuse/src/ops/prefetch.rs b/crates/musicfs-fuse/src/ops/prefetch.rs similarity index 93% rename from musicfs/crates/musicfs-fuse/src/ops/prefetch.rs rename to crates/musicfs-fuse/src/ops/prefetch.rs index a1eb551..4c84448 100644 --- a/musicfs/crates/musicfs-fuse/src/ops/prefetch.rs +++ b/crates/musicfs-fuse/src/ops/prefetch.rs @@ -43,10 +43,7 @@ impl PrefetchOps { } } - pub fn start_engine( - &self, - event_bus: Arc, - ) -> Option { + pub fn start_engine(&self, event_bus: Arc) -> Option { self.engine .as_ref() .map(|e| e.clone().start(event_bus, self.pattern_store.clone())) @@ -266,7 +263,8 @@ mod tests { #[test] fn test_prefetch_ops_new() { let dir = TempDir::new().unwrap(); - let pattern_store = Arc::new(PatternStore::new(&dir.path().join("patterns.db"), 30).unwrap()); + let pattern_store = + Arc::new(PatternStore::new(&dir.path().join("patterns.db"), 30).unwrap()); let _ops = PrefetchOps::new(pattern_store, 1000, 1000); } @@ -283,11 +281,18 @@ mod tests { #[test] fn test_hint_name_to_inode() { let dir = TempDir::new().unwrap(); - let pattern_store = Arc::new(PatternStore::new(&dir.path().join("patterns.db"), 30).unwrap()); + let pattern_store = + Arc::new(PatternStore::new(&dir.path().join("patterns.db"), 30).unwrap()); let ops = PrefetchOps::new(pattern_store, 1000, 1000); - assert_eq!(ops.hint_name_to_inode("hint_0001"), Some(PREFETCH_HINTS_BASE + 1)); - assert_eq!(ops.hint_name_to_inode("hint_9999"), Some(PREFETCH_HINTS_BASE + 9999)); + assert_eq!( + ops.hint_name_to_inode("hint_0001"), + Some(PREFETCH_HINTS_BASE + 1) + ); + assert_eq!( + ops.hint_name_to_inode("hint_9999"), + Some(PREFETCH_HINTS_BASE + 9999) + ); assert_eq!(ops.hint_name_to_inode("invalid"), None); } } diff --git a/musicfs/crates/musicfs-fuse/src/ops/search.rs b/crates/musicfs-fuse/src/ops/search.rs similarity index 97% rename from musicfs/crates/musicfs-fuse/src/ops/search.rs rename to crates/musicfs-fuse/src/ops/search.rs index 8eaa9ba..92a5f5b 100644 --- a/musicfs/crates/musicfs-fuse/src/ops/search.rs +++ b/crates/musicfs-fuse/src/ops/search.rs @@ -160,16 +160,17 @@ impl SearchOps { } fn safe_symlink_target(&self, virtual_path: &str) -> Option { - let normalized = Path::new(virtual_path) - .components() - .fold(std::path::PathBuf::new(), |mut acc, comp| { + let normalized = Path::new(virtual_path).components().fold( + std::path::PathBuf::new(), + |mut acc, comp| { match comp { std::path::Component::Normal(s) => acc.push(s), std::path::Component::RootDir => acc.push("/"), _ => {} } acc - }); + }, + ); let path_str = normalized.to_string_lossy(); if path_str.contains("..") { @@ -198,7 +199,9 @@ impl SearchOps { fn result_filename(&self, hit: &SearchHit, index: usize) -> String { let artist = hit.artist.as_deref().unwrap_or("Unknown"); let title = hit.title.as_deref().unwrap_or("Unknown"); - let ext = hit.virtual_path.as_str() + let ext = hit + .virtual_path + .as_str() .rsplit('.') .next() .unwrap_or("flac"); diff --git a/musicfs/crates/musicfs-grpc/Cargo.toml b/crates/musicfs-grpc/Cargo.toml similarity index 100% rename from musicfs/crates/musicfs-grpc/Cargo.toml rename to crates/musicfs-grpc/Cargo.toml diff --git a/musicfs/crates/musicfs-grpc/build.rs b/crates/musicfs-grpc/build.rs similarity index 100% rename from musicfs/crates/musicfs-grpc/build.rs rename to crates/musicfs-grpc/build.rs diff --git a/musicfs/crates/musicfs-grpc/proto/musicfs.proto b/crates/musicfs-grpc/proto/musicfs.proto similarity index 100% rename from musicfs/crates/musicfs-grpc/proto/musicfs.proto rename to crates/musicfs-grpc/proto/musicfs.proto diff --git a/musicfs/crates/musicfs-grpc/src/lib.rs b/crates/musicfs-grpc/src/lib.rs similarity index 100% rename from musicfs/crates/musicfs-grpc/src/lib.rs rename to crates/musicfs-grpc/src/lib.rs diff --git a/musicfs/crates/musicfs-grpc/src/search_service.rs b/crates/musicfs-grpc/src/search_service.rs similarity index 98% rename from musicfs/crates/musicfs-grpc/src/search_service.rs rename to crates/musicfs-grpc/src/search_service.rs index a40c9fa..39f9d37 100644 --- a/musicfs/crates/musicfs-grpc/src/search_service.rs +++ b/crates/musicfs-grpc/src/search_service.rs @@ -35,7 +35,9 @@ impl MusicFs for SearchService { } if req.query.len() > 256 { - return Err(Status::invalid_argument("Query exceeds maximum length (256)")); + return Err(Status::invalid_argument( + "Query exceeds maximum length (256)", + )); } let limit = req.limit.unwrap_or(100).min(10000) as usize; diff --git a/musicfs/crates/musicfs-grpc/src/server.rs b/crates/musicfs-grpc/src/server.rs similarity index 97% rename from musicfs/crates/musicfs-grpc/src/server.rs rename to crates/musicfs-grpc/src/server.rs index 729e177..f8bde00 100644 --- a/musicfs/crates/musicfs-grpc/src/server.rs +++ b/crates/musicfs-grpc/src/server.rs @@ -228,10 +228,7 @@ impl MusicFs for MusicFsServer { } #[instrument(level = "info", skip(self, request), fields(method = "shutdown"))] - async fn shutdown( - &self, - request: Request, - ) -> Result, Status> { + async fn shutdown(&self, request: Request) -> Result, Status> { let req = request.into_inner(); info!( graceful = req.graceful, @@ -242,7 +239,11 @@ impl MusicFs for MusicFsServer { Ok(Response::new(Empty {})) } - #[instrument(level = "debug", skip(self, _request), fields(method = "get_cache_stats"))] + #[instrument( + level = "debug", + skip(self, _request), + fields(method = "get_cache_stats") + )] async fn get_cache_stats( &self, _request: Request, @@ -339,7 +340,11 @@ impl MusicFs for MusicFsServer { Ok(Response::new(OriginsResponse { origins: vec![] })) } - #[instrument(level = "debug", skip(self, request), fields(method = "get_origin_health"))] + #[instrument( + level = "debug", + skip(self, request), + fields(method = "get_origin_health") + )] async fn get_origin_health( &self, request: Request, @@ -389,7 +394,11 @@ impl MusicFs for MusicFsServer { type SubscribeEventsStream = ReceiverStream>; - #[instrument(level = "info", skip(self, request), fields(method = "subscribe_events"))] + #[instrument( + level = "info", + skip(self, request), + fields(method = "subscribe_events") + )] async fn subscribe_events( &self, request: Request, diff --git a/musicfs/crates/musicfs-grpc/src/webhook.rs b/crates/musicfs-grpc/src/webhook.rs similarity index 100% rename from musicfs/crates/musicfs-grpc/src/webhook.rs rename to crates/musicfs-grpc/src/webhook.rs diff --git a/musicfs/crates/musicfs-metadata/Cargo.toml b/crates/musicfs-metadata/Cargo.toml similarity index 100% rename from musicfs/crates/musicfs-metadata/Cargo.toml rename to crates/musicfs-metadata/Cargo.toml diff --git a/musicfs/crates/musicfs-metadata/src/artwork.rs b/crates/musicfs-metadata/src/artwork.rs similarity index 100% rename from musicfs/crates/musicfs-metadata/src/artwork.rs rename to crates/musicfs-metadata/src/artwork.rs diff --git a/musicfs/crates/musicfs-metadata/src/lib.rs b/crates/musicfs-metadata/src/lib.rs similarity index 100% rename from musicfs/crates/musicfs-metadata/src/lib.rs rename to crates/musicfs-metadata/src/lib.rs diff --git a/musicfs/crates/musicfs-metadata/src/parser.rs b/crates/musicfs-metadata/src/parser.rs similarity index 93% rename from musicfs/crates/musicfs-metadata/src/parser.rs rename to crates/musicfs-metadata/src/parser.rs index 4c0d8e1..3e81a1d 100644 --- a/musicfs/crates/musicfs-metadata/src/parser.rs +++ b/crates/musicfs-metadata/src/parser.rs @@ -52,8 +52,7 @@ impl MetadataParser { if let Some(n_frames) = params.n_frames { if let Some(sample_rate) = params.sample_rate { - audio_meta.duration_ms = - Some((n_frames as u64 * 1000) / sample_rate as u64); + audio_meta.duration_ms = Some((n_frames as u64 * 1000) / sample_rate as u64); audio_meta.sample_rate = Some(sample_rate); } } @@ -61,9 +60,8 @@ impl MetadataParser { if let Some(bits_per_sample) = params.bits_per_sample { if let Some(sample_rate) = params.sample_rate { if let Some(channels) = params.channels { - audio_meta.bitrate = Some( - bits_per_sample * sample_rate * channels.count() as u32 / 1000, - ); + audio_meta.bitrate = + Some(bits_per_sample * sample_rate * channels.count() as u32 / 1000); } } } diff --git a/musicfs/crates/musicfs-origins/Cargo.toml b/crates/musicfs-origins/Cargo.toml similarity index 100% rename from musicfs/crates/musicfs-origins/Cargo.toml rename to crates/musicfs-origins/Cargo.toml diff --git a/musicfs/crates/musicfs-origins/src/failover.rs b/crates/musicfs-origins/src/failover.rs similarity index 93% rename from musicfs/crates/musicfs-origins/src/failover.rs rename to crates/musicfs-origins/src/failover.rs index 9cf5489..0a3a2d7 100644 --- a/musicfs/crates/musicfs-origins/src/failover.rs +++ b/crates/musicfs-origins/src/failover.rs @@ -67,11 +67,10 @@ impl FailoverExecutor { if origins.is_empty() { if let Some(origin) = self.registry.route_with_fallback(path) { - warn!( - "No healthy origins, using fallback origin {}", - origin.id() - ); - return self.read_with_retry(&origin, &path.path, offset, size).await; + warn!("No healthy origins, using fallback origin {}", origin.id()); + return self + .read_with_retry(&origin, &path.path, offset, size) + .await; } return Err(Error::NoOriginAvailable); } @@ -81,7 +80,10 @@ impl FailoverExecutor { for origin in origins { trace!(origin_id = %origin.id(), "Attempting read from origin"); let start = std::time::Instant::now(); - match self.read_with_retry(&origin, &path.path, offset, size).await { + match self + .read_with_retry(&origin, &path.path, offset, size) + .await + { Ok(data) => { let latency = start.elapsed().as_millis() as u64; self.registry.record_latency(origin.id(), latency); @@ -214,10 +216,8 @@ mod tests { #[test] fn test_custom_delays() { - let config = RetryConfig::with_delays(vec![ - Duration::from_millis(50), - Duration::from_millis(100), - ]); + let config = + RetryConfig::with_delays(vec![Duration::from_millis(50), Duration::from_millis(100)]); assert_eq!(config.max_attempts, 2); assert_eq!(config.delay_for_attempt(0), Duration::from_millis(50)); diff --git a/musicfs/crates/musicfs-origins/src/health.rs b/crates/musicfs-origins/src/health.rs similarity index 96% rename from musicfs/crates/musicfs-origins/src/health.rs rename to crates/musicfs-origins/src/health.rs index e2da71f..2e6b23e 100644 --- a/musicfs/crates/musicfs-origins/src/health.rs +++ b/crates/musicfs-origins/src/health.rs @@ -349,10 +349,13 @@ mod tests { let mut thresholds = HashMap::new(); thresholds.insert(OriginType::Local, 3); - let monitor = HealthMonitor::new(Duration::from_secs(30)) - .with_per_type_thresholds(thresholds); + let monitor = + HealthMonitor::new(Duration::from_secs(30)).with_per_type_thresholds(thresholds); - let origin = Arc::new(LocalOrigin::new("missing", std::path::Path::new("/nonexistent"))); + let origin = Arc::new(LocalOrigin::new( + "missing", + std::path::Path::new("/nonexistent"), + )); monitor.add_origin(origin); monitor.check_now(&OriginId::from("missing")).await; @@ -372,7 +375,10 @@ mod tests { async fn test_local_origin_threshold_is_one() { let monitor = HealthMonitor::new(Duration::from_secs(30)); - let origin = Arc::new(LocalOrigin::new("missing", std::path::Path::new("/nonexistent"))); + let origin = Arc::new(LocalOrigin::new( + "missing", + std::path::Path::new("/nonexistent"), + )); monitor.add_origin(origin); monitor.check_now(&OriginId::from("missing")).await; diff --git a/musicfs/crates/musicfs-origins/src/lib.rs b/crates/musicfs-origins/src/lib.rs similarity index 100% rename from musicfs/crates/musicfs-origins/src/lib.rs rename to crates/musicfs-origins/src/lib.rs diff --git a/musicfs/crates/musicfs-origins/src/local.rs b/crates/musicfs-origins/src/local.rs similarity index 100% rename from musicfs/crates/musicfs-origins/src/local.rs rename to crates/musicfs-origins/src/local.rs diff --git a/musicfs/crates/musicfs-origins/src/nfs.rs b/crates/musicfs-origins/src/nfs.rs similarity index 100% rename from musicfs/crates/musicfs-origins/src/nfs.rs rename to crates/musicfs-origins/src/nfs.rs diff --git a/musicfs/crates/musicfs-origins/src/registry.rs b/crates/musicfs-origins/src/registry.rs similarity index 100% rename from musicfs/crates/musicfs-origins/src/registry.rs rename to crates/musicfs-origins/src/registry.rs diff --git a/musicfs/crates/musicfs-origins/src/router.rs b/crates/musicfs-origins/src/router.rs similarity index 99% rename from musicfs/crates/musicfs-origins/src/router.rs rename to crates/musicfs-origins/src/router.rs index f2d0d55..091c009 100644 --- a/musicfs/crates/musicfs-origins/src/router.rs +++ b/crates/musicfs-origins/src/router.rs @@ -86,7 +86,7 @@ impl Router { (priority, latency) }) .cloned(); - + if let Some(ref id) = selected { let priority = self.get_priority(id); let latency = self.latency_stats.get(id).map(|s| s.p50_ms).unwrap_or(0); @@ -97,7 +97,7 @@ impl Router { "Selected healthy origin" ); } - + selected } @@ -141,7 +141,7 @@ impl Router { (failures, priority) }) .cloned(); - + if let Some(ref id) = selected { let failures = health.failure_count(id).unwrap_or(u32::MAX); trace!( @@ -151,7 +151,7 @@ impl Router { "Selected least-bad unhealthy origin" ); } - + selected } } diff --git a/musicfs/crates/musicfs-origins/src/s3.rs b/crates/musicfs-origins/src/s3.rs similarity index 99% rename from musicfs/crates/musicfs-origins/src/s3.rs rename to crates/musicfs-origins/src/s3.rs index bb1a21b..f1d8d5e 100644 --- a/musicfs/crates/musicfs-origins/src/s3.rs +++ b/crates/musicfs-origins/src/s3.rs @@ -47,5 +47,3 @@ mod implementation { // Full S3 implementation would go here when aws-sdk-s3 is enabled } - - diff --git a/musicfs/crates/musicfs-origins/src/sftp.rs b/crates/musicfs-origins/src/sftp.rs similarity index 100% rename from musicfs/crates/musicfs-origins/src/sftp.rs rename to crates/musicfs-origins/src/sftp.rs diff --git a/musicfs/crates/musicfs-origins/src/smb.rs b/crates/musicfs-origins/src/smb.rs similarity index 97% rename from musicfs/crates/musicfs-origins/src/smb.rs rename to crates/musicfs-origins/src/smb.rs index ee515f9..b9e18a8 100644 --- a/musicfs/crates/musicfs-origins/src/smb.rs +++ b/crates/musicfs-origins/src/smb.rs @@ -91,11 +91,13 @@ impl Origin for SmbOrigin { } async fn read(&self, path: &Path, offset: u64, size: u32) -> Result> { - self.retry_on_disconnect(|| self.inner.read(path, offset, size)).await + self.retry_on_disconnect(|| self.inner.read(path, offset, size)) + .await } async fn read_full(&self, path: &Path) -> Result> { - self.retry_on_disconnect(|| self.inner.read_full(path)).await + self.retry_on_disconnect(|| self.inner.read_full(path)) + .await } async fn exists(&self, path: &Path) -> Result { diff --git a/musicfs/crates/musicfs-origins/src/traits.rs b/crates/musicfs-origins/src/traits.rs similarity index 100% rename from musicfs/crates/musicfs-origins/src/traits.rs rename to crates/musicfs-origins/src/traits.rs diff --git a/musicfs/crates/musicfs-plugins/Cargo.toml b/crates/musicfs-plugins/Cargo.toml similarity index 100% rename from musicfs/crates/musicfs-plugins/Cargo.toml rename to crates/musicfs-plugins/Cargo.toml diff --git a/musicfs/crates/musicfs-plugins/src/error.rs b/crates/musicfs-plugins/src/error.rs similarity index 100% rename from musicfs/crates/musicfs-plugins/src/error.rs rename to crates/musicfs-plugins/src/error.rs diff --git a/musicfs/crates/musicfs-plugins/src/lib.rs b/crates/musicfs-plugins/src/lib.rs similarity index 100% rename from musicfs/crates/musicfs-plugins/src/lib.rs rename to crates/musicfs-plugins/src/lib.rs diff --git a/musicfs/crates/musicfs-plugins/src/manager.rs b/crates/musicfs-plugins/src/manager.rs similarity index 100% rename from musicfs/crates/musicfs-plugins/src/manager.rs rename to crates/musicfs-plugins/src/manager.rs diff --git a/musicfs/crates/musicfs-plugins/src/native.rs b/crates/musicfs-plugins/src/native.rs similarity index 94% rename from musicfs/crates/musicfs-plugins/src/native.rs rename to crates/musicfs-plugins/src/native.rs index f2e78ce..f3b0796 100644 --- a/musicfs/crates/musicfs-plugins/src/native.rs +++ b/crates/musicfs-plugins/src/native.rs @@ -55,9 +55,8 @@ impl NativePluginHost { info!("Loading native plugin from {:?}", canonical); let library = unsafe { - Library::new(&canonical).map_err(|e| { - PluginError::LoadFailed(format!("Failed to load library: {}", e)) - })? + Library::new(&canonical) + .map_err(|e| PluginError::LoadFailed(format!("Failed to load library: {}", e)))? }; self.verify_api_version(&library)?; @@ -190,9 +189,9 @@ impl NativePluginHost { fn verify_api_version(&self, library: &Library) -> Result<()> { let version_fn: Symbol *const std::ffi::c_char> = unsafe { - library - .get(b"musicfs_plugin_api_version") - .map_err(|_| PluginError::SymbolNotFound("musicfs_plugin_api_version".to_string()))? + library.get(b"musicfs_plugin_api_version").map_err(|_| { + PluginError::SymbolNotFound("musicfs_plugin_api_version".to_string()) + })? }; let version_ptr = unsafe { version_fn() }; @@ -203,10 +202,11 @@ impl NativePluginHost { actual: "".to_string(), })?; - let plugin_version = Version::parse(version_str).map_err(|_| PluginError::VersionMismatch { - expected: PLUGIN_API_VERSION.to_string(), - actual: version_str.to_string(), - })?; + let plugin_version = + Version::parse(version_str).map_err(|_| PluginError::VersionMismatch { + expected: PLUGIN_API_VERSION.to_string(), + actual: version_str.to_string(), + })?; let expected_version = Version::parse(PLUGIN_API_VERSION).unwrap(); diff --git a/musicfs/crates/musicfs-plugins/src/traits.rs b/crates/musicfs-plugins/src/traits.rs similarity index 98% rename from musicfs/crates/musicfs-plugins/src/traits.rs rename to crates/musicfs-plugins/src/traits.rs index 2fd6c63..ca81c15 100644 --- a/musicfs/crates/musicfs-plugins/src/traits.rs +++ b/crates/musicfs-plugins/src/traits.rs @@ -95,11 +95,7 @@ pub trait OriginPlugin: Plugin { /// /// The config contains origin-specific settings (credentials, paths, etc). /// Returns a boxed Origin that can be used by the OriginRouter. - async fn create_origin( - &self, - id: &str, - config: Value, - ) -> Result>; + async fn create_origin(&self, id: &str, config: Value) -> Result>; } /// Instance created by OriginPlugin diff --git a/musicfs/crates/musicfs-plugins/src/wasm.rs b/crates/musicfs-plugins/src/wasm.rs similarity index 100% rename from musicfs/crates/musicfs-plugins/src/wasm.rs rename to crates/musicfs-plugins/src/wasm.rs diff --git a/musicfs/crates/musicfs-search/Cargo.toml b/crates/musicfs-search/Cargo.toml similarity index 100% rename from musicfs/crates/musicfs-search/Cargo.toml rename to crates/musicfs-search/Cargo.toml diff --git a/musicfs/crates/musicfs-search/src/collections.rs b/crates/musicfs-search/src/collections.rs similarity index 95% rename from musicfs/crates/musicfs-search/src/collections.rs rename to crates/musicfs-search/src/collections.rs index 149a7dd..a86c9ab 100644 --- a/musicfs/crates/musicfs-search/src/collections.rs +++ b/crates/musicfs-search/src/collections.rs @@ -261,7 +261,12 @@ mod tests { let store = CollectionStore::new(&db_path).unwrap(); let collection = store - .create("Jazz", CollectionQuery::Genre { genre: "Jazz".to_string() }) + .create( + "Jazz", + CollectionQuery::Genre { + genre: "Jazz".to_string(), + }, + ) .unwrap(); assert_eq!(collection.name, "Jazz"); @@ -279,7 +284,9 @@ mod tests { let query = CollectionQuery::Compound { op: BoolOp::And, children: vec![ - CollectionQuery::Genre { genre: "Metal".to_string() }, + CollectionQuery::Genre { + genre: "Metal".to_string(), + }, CollectionQuery::DateRange { field: "year".to_string(), start: 1980, @@ -306,6 +313,9 @@ mod tests { assert!(CollectionQuery::RecentlyAdded { days: 30 }.is_dynamic()); assert!(CollectionQuery::RecentlyPlayed { days: 7 }.is_dynamic()); assert!(CollectionQuery::MostPlayed { limit: 100 }.is_dynamic()); - assert!(!CollectionQuery::Genre { genre: "Rock".to_string() }.is_dynamic()); + assert!(!CollectionQuery::Genre { + genre: "Rock".to_string() + } + .is_dynamic()); } } diff --git a/musicfs/crates/musicfs-search/src/index.rs b/crates/musicfs-search/src/index.rs similarity index 83% rename from musicfs/crates/musicfs-search/src/index.rs rename to crates/musicfs-search/src/index.rs index 196a9ba..f7fa1fd 100644 --- a/musicfs/crates/musicfs-search/src/index.rs +++ b/crates/musicfs-search/src/index.rs @@ -4,7 +4,7 @@ use std::path::Path; use std::sync::Arc; use tantivy::collector::TopDocs; use tantivy::query::{BooleanQuery, FuzzyTermQuery, Occur, Query, QueryParser}; -use tantivy::schema::{Field, Schema, Value, STORED, TEXT, INDEXED}; +use tantivy::schema::{Field, Schema, Value, INDEXED, STORED, TEXT}; use tantivy::{Index, IndexReader, IndexWriter, ReloadPolicy, TantivyDocument, Term}; use tracing::{debug, info, warn}; @@ -109,8 +109,7 @@ impl SearchIndex { "Search index corrupted, rebuilding from scratch" ); if index_path.exists() { - std::fs::remove_dir_all(index_path) - .map_err(SearchError::Io)?; + std::fs::remove_dir_all(index_path).map_err(SearchError::Io)?; } Self::open(index_path) } @@ -205,20 +204,21 @@ impl SearchIndex { self.schema.composer, ]; - let query: Box = if let Some((term, distance)) = Self::parse_fuzzy_query(query_str) { - let subqueries: Vec<(Occur, Box)> = default_fields - .iter() - .map(|&field| { - let term = Term::from_field_text(field, &term); - let fuzzy = FuzzyTermQuery::new(term, distance, true); - (Occur::Should, Box::new(fuzzy) as Box) - }) - .collect(); - Box::new(BooleanQuery::new(subqueries)) - } else { - let query_parser = QueryParser::for_index(&self.index, default_fields); - query_parser.parse_query(query_str)? - }; + let query: Box = + if let Some((term, distance)) = Self::parse_fuzzy_query(query_str) { + let subqueries: Vec<(Occur, Box)> = default_fields + .iter() + .map(|&field| { + let term = Term::from_field_text(field, &term); + let fuzzy = FuzzyTermQuery::new(term, distance, true); + (Occur::Should, Box::new(fuzzy) as Box) + }) + .collect(); + Box::new(BooleanQuery::new(subqueries)) + } else { + let query_parser = QueryParser::for_index(&self.index, default_fields); + query_parser.parse_query(query_str)? + }; let top_docs = searcher.search(&*query, &TopDocs::with_limit(limit))?; @@ -241,9 +241,18 @@ impl SearchIndex { results.push(SearchHit { file_id, virtual_path, - artist: doc.get_first(self.schema.artist).and_then(|v| v.as_str()).map(String::from), - album: doc.get_first(self.schema.album).and_then(|v| v.as_str()).map(String::from), - title: doc.get_first(self.schema.title).and_then(|v| v.as_str()).map(String::from), + artist: doc + .get_first(self.schema.artist) + .and_then(|v| v.as_str()) + .map(String::from), + album: doc + .get_first(self.schema.album) + .and_then(|v| v.as_str()) + .map(String::from), + title: doc + .get_first(self.schema.title) + .and_then(|v| v.as_str()) + .map(String::from), score, }); } @@ -322,9 +331,15 @@ mod tests { let dir = TempDir::new().unwrap(); let index = SearchIndex::open(dir.path()).unwrap(); - index.index_file(&make_file(1, "Metallica", "Black Album", "Enter Sandman")).unwrap(); - index.index_file(&make_file(2, "Metallica", "Master of Puppets", "Battery")).unwrap(); - index.index_file(&make_file(3, "Iron Maiden", "Powerslave", "Aces High")).unwrap(); + index + .index_file(&make_file(1, "Metallica", "Black Album", "Enter Sandman")) + .unwrap(); + index + .index_file(&make_file(2, "Metallica", "Master of Puppets", "Battery")) + .unwrap(); + index + .index_file(&make_file(3, "Iron Maiden", "Powerslave", "Aces High")) + .unwrap(); index.commit().unwrap(); let results = index.search("metallica", 10).unwrap(); @@ -340,7 +355,9 @@ mod tests { let dir = TempDir::new().unwrap(); let index = SearchIndex::open(dir.path()).unwrap(); - index.index_file(&make_file(1, "Metallica", "Black Album", "Enter Sandman")).unwrap(); + index + .index_file(&make_file(1, "Metallica", "Black Album", "Enter Sandman")) + .unwrap(); index.commit().unwrap(); let results = index.search("metalica~1", 10).unwrap(); @@ -352,7 +369,9 @@ mod tests { let dir = TempDir::new().unwrap(); let index = SearchIndex::open(dir.path()).unwrap(); - index.index_file(&make_file(1, "Metallica", "Black Album", "Enter Sandman")).unwrap(); + index + .index_file(&make_file(1, "Metallica", "Black Album", "Enter Sandman")) + .unwrap(); index.commit().unwrap(); let results = index.search("genre:Metal", 10).unwrap(); @@ -364,7 +383,9 @@ mod tests { let dir = TempDir::new().unwrap(); let index = SearchIndex::open(dir.path()).unwrap(); - index.index_file(&make_file(1, "Test", "Album", "Song")).unwrap(); + index + .index_file(&make_file(1, "Test", "Album", "Song")) + .unwrap(); index.commit().unwrap(); assert_eq!(index.search("test", 10).unwrap().len(), 1); @@ -381,7 +402,9 @@ mod tests { { let index = SearchIndex::open(dir.path()).unwrap(); - index.index_file(&make_file(1, "Artist", "Album", "Track")).unwrap(); + index + .index_file(&make_file(1, "Artist", "Album", "Track")) + .unwrap(); index.commit().unwrap(); } diff --git a/musicfs/crates/musicfs-search/src/indexer.rs b/crates/musicfs-search/src/indexer.rs similarity index 98% rename from musicfs/crates/musicfs-search/src/indexer.rs rename to crates/musicfs-search/src/indexer.rs index 0958df1..75a7345 100644 --- a/musicfs/crates/musicfs-search/src/indexer.rs +++ b/crates/musicfs-search/src/indexer.rs @@ -15,11 +15,7 @@ pub struct Indexer { } impl Indexer { - pub fn new( - index: Arc, - event_bus: Arc, - metadata_lookup: Arc, - ) -> Self { + pub fn new(index: Arc, event_bus: Arc, metadata_lookup: Arc) -> Self { Self { index, event_bus, diff --git a/musicfs/crates/musicfs-search/src/lib.rs b/crates/musicfs-search/src/lib.rs similarity index 87% rename from musicfs/crates/musicfs-search/src/lib.rs rename to crates/musicfs-search/src/lib.rs index ec4a217..d86f608 100644 --- a/musicfs/crates/musicfs-search/src/lib.rs +++ b/crates/musicfs-search/src/lib.rs @@ -4,8 +4,7 @@ mod indexer; mod query; pub use collections::{ - builtin_collections, BoolOp, CollectionError, CollectionQuery, CollectionStore, - SmartCollection, + builtin_collections, BoolOp, CollectionError, CollectionQuery, CollectionStore, SmartCollection, }; pub use index::{SearchError, SearchHit, SearchIndex}; pub use indexer::{Indexer, IndexerHandle, MetadataLookup}; diff --git a/musicfs/crates/musicfs-search/src/query.rs b/crates/musicfs-search/src/query.rs similarity index 100% rename from musicfs/crates/musicfs-search/src/query.rs rename to crates/musicfs-search/src/query.rs diff --git a/musicfs/crates/musicfs-sync/Cargo.toml b/crates/musicfs-sync/Cargo.toml similarity index 100% rename from musicfs/crates/musicfs-sync/Cargo.toml rename to crates/musicfs-sync/Cargo.toml diff --git a/musicfs/crates/musicfs-sync/src/cdc.rs b/crates/musicfs-sync/src/cdc.rs similarity index 95% rename from musicfs/crates/musicfs-sync/src/cdc.rs rename to crates/musicfs-sync/src/cdc.rs index 9553d09..d269e96 100644 --- a/musicfs/crates/musicfs-sync/src/cdc.rs +++ b/crates/musicfs-sync/src/cdc.rs @@ -138,14 +138,21 @@ mod tests { let shared = hashes1.intersection(&hashes2).count(); - assert!(shared > 0, "CDC should produce stable boundaries, got {} chunks in original, {} after prepend", chunks1.len(), chunks2.len()); + assert!( + shared > 0, + "CDC should produce stable boundaries, got {} chunks in original, {} after prepend", + chunks1.len(), + chunks2.len() + ); } #[test] fn test_cdc_chunk_sizes() { let chunker = CdcChunker::default(); - let data: Vec = (0..1024 * 1024).map(|i| ((i * 17 + 31) % 256) as u8).collect(); + let data: Vec = (0..1024 * 1024) + .map(|i| ((i * 17 + 31) % 256) as u8) + .collect(); let chunks = chunker.chunk(&data); diff --git a/musicfs/crates/musicfs-sync/src/delta.rs b/crates/musicfs-sync/src/delta.rs similarity index 98% rename from musicfs/crates/musicfs-sync/src/delta.rs rename to crates/musicfs-sync/src/delta.rs index 92b9c43..fdaa708 100644 --- a/musicfs/crates/musicfs-sync/src/delta.rs +++ b/crates/musicfs-sync/src/delta.rs @@ -68,7 +68,7 @@ impl DeltaDetector { ) -> Result { let origin_id = origin.id().clone(); info!(origin_id = %origin_id, "Starting delta detection"); - + let mut changes = ChangeSet::default(); let origin_files = self.scan_origin(origin).await?; @@ -187,7 +187,11 @@ impl DeltaDetector { .collect()) } - fn compute_diff(&self, old_chunks: &[ManifestChunk], new_chunks: &[ManifestChunk]) -> ManifestDiff { + fn compute_diff( + &self, + old_chunks: &[ManifestChunk], + new_chunks: &[ManifestChunk], + ) -> ManifestDiff { let old_hashes: HashSet<_> = old_chunks.iter().map(|c| c.hash).collect(); let new_hashes: HashSet<_> = new_chunks.iter().map(|c| c.hash).collect(); diff --git a/musicfs/crates/musicfs-sync/src/lib.rs b/crates/musicfs-sync/src/lib.rs similarity index 100% rename from musicfs/crates/musicfs-sync/src/lib.rs rename to crates/musicfs-sync/src/lib.rs diff --git a/musicfs/crates/musicfs-sync/src/watcher.rs b/crates/musicfs-sync/src/watcher.rs similarity index 93% rename from musicfs/crates/musicfs-sync/src/watcher.rs rename to crates/musicfs-sync/src/watcher.rs index 9c9252b..a1d476d 100644 --- a/musicfs/crates/musicfs-sync/src/watcher.rs +++ b/crates/musicfs-sync/src/watcher.rs @@ -34,7 +34,8 @@ impl OriginWatcher { let origin_id_str = origin_id.to_string(); tokio::spawn( async move { - if let Err(e) = Self::watch_loop(&origin_id, &root, &event_bus, &mut stop_rx).await { + if let Err(e) = Self::watch_loop(&origin_id, &root, &event_bus, &mut stop_rx).await + { error!("Watcher error: {}", e); } } @@ -126,7 +127,10 @@ impl OriginWatcher { } EventKind::Remove(_) => { trace!(origin_id = %origin_id, path = ?relative, "File removed"); - event_bus.publish(Event::FileRemoved { path: vpath, file_id: None }); + event_bus.publish(Event::FileRemoved { + path: vpath, + file_id: None, + }); } EventKind::Modify(_) => { trace!(origin_id = %origin_id, path = ?relative, "File modified"); @@ -186,7 +190,8 @@ mod tests { let event_bus = Arc::new(EventBus::default()); let mut rx = event_bus.subscribe(); - let watcher = OriginWatcher::new(OriginId::from("test"), dir.path().to_path_buf(), event_bus); + let watcher = + OriginWatcher::new(OriginId::from("test"), dir.path().to_path_buf(), event_bus); let handle = watcher.start(); tokio::time::sleep(Duration::from_millis(100)).await; @@ -206,6 +211,8 @@ mod tests { assert!(OriginWatcher::is_audio_file(Path::new("/music/song.flac"))); assert!(OriginWatcher::is_audio_file(Path::new("/music/song.MP3"))); assert!(!OriginWatcher::is_audio_file(Path::new("/music/cover.jpg"))); - assert!(!OriginWatcher::is_audio_file(Path::new("/music/readme.txt"))); + assert!(!OriginWatcher::is_audio_file(Path::new( + "/music/readme.txt" + ))); } } diff --git a/musicfs/crates/musicfs-test-utils/Cargo.toml b/crates/musicfs-test-utils/Cargo.toml similarity index 100% rename from musicfs/crates/musicfs-test-utils/Cargo.toml rename to crates/musicfs-test-utils/Cargo.toml diff --git a/musicfs/crates/musicfs-test-utils/src/assertions.rs b/crates/musicfs-test-utils/src/assertions.rs similarity index 94% rename from musicfs/crates/musicfs-test-utils/src/assertions.rs rename to crates/musicfs-test-utils/src/assertions.rs index 16a4da4..e2c1e74 100644 --- a/musicfs/crates/musicfs-test-utils/src/assertions.rs +++ b/crates/musicfs-test-utils/src/assertions.rs @@ -133,10 +133,7 @@ where { tokio::time::timeout(timeout, future) .await - .expect(&format!( - "Operation did not complete within {:?}", - timeout - )) + .expect(&format!("Operation did not complete within {:?}", timeout)) } pub async fn assert_times_out(future: F, timeout: Duration) @@ -168,8 +165,10 @@ mod tests { #[test] fn test_assert_io_error() { - let result: Result<(), Error> = - Err(Error::Io(std::io::Error::new(std::io::ErrorKind::Other, "test"))); + let result: Result<(), Error> = Err(Error::Io(std::io::Error::new( + std::io::ErrorKind::Other, + "test", + ))); assert_io_error(result); } @@ -188,8 +187,7 @@ mod tests { #[tokio::test] async fn test_assert_completes_within() { - let result = - assert_completes_within(async { 42 }, Duration::from_millis(100)).await; + let result = assert_completes_within(async { 42 }, Duration::from_millis(100)).await; assert_eq!(result, 42); } diff --git a/musicfs/crates/musicfs-test-utils/src/faulty_cas.rs b/crates/musicfs-test-utils/src/faulty_cas.rs similarity index 100% rename from musicfs/crates/musicfs-test-utils/src/faulty_cas.rs rename to crates/musicfs-test-utils/src/faulty_cas.rs diff --git a/musicfs/crates/musicfs-test-utils/src/faulty_origin.rs b/crates/musicfs-test-utils/src/faulty_origin.rs similarity index 100% rename from musicfs/crates/musicfs-test-utils/src/faulty_origin.rs rename to crates/musicfs-test-utils/src/faulty_origin.rs diff --git a/musicfs/crates/musicfs-test-utils/src/fixtures.rs b/crates/musicfs-test-utils/src/fixtures.rs similarity index 98% rename from musicfs/crates/musicfs-test-utils/src/fixtures.rs rename to crates/musicfs-test-utils/src/fixtures.rs index 6ed3257..a41504e 100644 --- a/musicfs/crates/musicfs-test-utils/src/fixtures.rs +++ b/crates/musicfs-test-utils/src/fixtures.rs @@ -1,8 +1,6 @@ use musicfs_cache::TreeBuilder; use musicfs_cas::{CasConfig, CasStore}; -use musicfs_core::{ - AudioFormat, AudioMeta, FileId, FileMeta, OriginId, RealPath, VirtualPath, -}; +use musicfs_core::{AudioFormat, AudioMeta, FileId, FileMeta, OriginId, RealPath, VirtualPath}; use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; use std::time::SystemTime; diff --git a/musicfs/crates/musicfs-test-utils/src/lib.rs b/crates/musicfs-test-utils/src/lib.rs similarity index 100% rename from musicfs/crates/musicfs-test-utils/src/lib.rs rename to crates/musicfs-test-utils/src/lib.rs diff --git a/musicfs/crates/musicfs-test-utils/tests/docker_network.rs b/crates/musicfs-test-utils/tests/docker_network.rs similarity index 90% rename from musicfs/crates/musicfs-test-utils/tests/docker_network.rs rename to crates/musicfs-test-utils/tests/docker_network.rs index b0a9bd5..6e8df32 100644 --- a/musicfs/crates/musicfs-test-utils/tests/docker_network.rs +++ b/crates/musicfs-test-utils/tests/docker_network.rs @@ -17,7 +17,11 @@ async fn require_toxiproxy() { Ok(resp) => resp.status().is_success(), Err(_) => false, }; - assert!(available, "Toxiproxy not available at {}. Run: cd tests/integration && docker-compose up -d", TOXIPROXY_API); + assert!( + available, + "Toxiproxy not available at {}. Run: cd tests/integration && docker-compose up -d", + TOXIPROXY_API + ); } #[tokio::test] @@ -41,10 +45,7 @@ async fn test_toxiproxy_latency_injection() { toxicity: 1.0, }; - proxy - .add_toxic(&toxic) - .await - .expect("Failed to add toxic"); + proxy.add_toxic(&toxic).await.expect("Failed to add toxic"); let start = std::time::Instant::now(); let _ = reqwest::get(format!("http://{}/minio/health/live", TOXIPROXY_LISTEN)).await; @@ -80,10 +81,7 @@ async fn test_toxiproxy_timeout_simulates_network_partition() { toxicity: 1.0, }; - proxy - .add_toxic(&toxic) - .await - .expect("Failed to add toxic"); + proxy.add_toxic(&toxic).await.expect("Failed to add toxic"); let result = tokio::time::timeout( Duration::from_secs(2), @@ -127,10 +125,7 @@ async fn test_toxiproxy_slow_close_throttles_responses() { toxicity: 1.0, }; - proxy - .add_toxic(&toxic) - .await - .expect("Failed to add toxic"); + proxy.add_toxic(&toxic).await.expect("Failed to add toxic"); let start = std::time::Instant::now(); let _ = reqwest::get(format!("http://{}/minio/health/live", TOXIPROXY_LISTEN)).await; @@ -144,5 +139,3 @@ async fn test_toxiproxy_slow_close_throttles_responses() { proxy.delete().await.expect("Failed to cleanup proxy"); } - - diff --git a/musicfs/crates/musicfs-test-utils/tests/resilience.rs b/crates/musicfs-test-utils/tests/resilience.rs similarity index 80% rename from musicfs/crates/musicfs-test-utils/tests/resilience.rs rename to crates/musicfs-test-utils/tests/resilience.rs index 42e8e39..610198c 100644 --- a/musicfs/crates/musicfs-test-utils/tests/resilience.rs +++ b/crates/musicfs-test-utils/tests/resilience.rs @@ -33,7 +33,10 @@ async fn setup_cas(dir: &Path) -> CasStore { } fn create_faulty_origin(id: &str, dir: &TempDir, mode: FailMode) -> Arc { - let inner = Arc::new(LocalOrigin::new(OriginId::from(id), dir.path().to_path_buf())); + let inner = Arc::new(LocalOrigin::new( + OriginId::from(id), + dir.path().to_path_buf(), + )); Arc::new(FaultyOrigin::new(inner, mode)) } @@ -94,7 +97,9 @@ async fn test_tantivy_corruption_triggers_rebuild() { { let index = SearchIndex::open(&index_path).unwrap(); - index.index_file(&make_file_meta(1, "/a.flac", 1000)).unwrap(); + index + .index_file(&make_file_meta(1, "/a.flac", 1000)) + .unwrap(); index.commit().unwrap(); } @@ -147,8 +152,11 @@ async fn test_cas_put_handles_enospc() { let large_data = vec![0u8; 1000]; let result = store.put(&large_data).await; - - assert!(result.is_err(), "Issue 2.8: CasStore should pre-check space and reject oversized write"); + + assert!( + result.is_err(), + "Issue 2.8: CasStore should pre-check space and reject oversized write" + ); } /// Demonstrates the PROBLEM with std::sync::RwLock: after a writer panic, @@ -190,7 +198,10 @@ fn test_parking_lot_rwlock_survives_panic() { let _ = handle.join(); - assert!(tree.read().get(ROOT_INODE).is_some(), "parking_lot RwLock should survive writer panic"); + assert!( + tree.read().get(ROOT_INODE).is_some(), + "parking_lot RwLock should survive writer panic" + ); } #[tokio::test] @@ -200,12 +211,17 @@ async fn test_failover_on_primary_death() { setup_test_file(&primary_dir, "test.txt", b"primary"); setup_test_file(&backup_dir, "test.txt", b"backup"); - let primary = create_faulty_origin("primary", &primary_dir, FailMode::ReturnError(ErrorKind::ConnectionRefused)); + let primary = create_faulty_origin( + "primary", + &primary_dir, + FailMode::ReturnError(ErrorKind::ConnectionRefused), + ); let backup = create_faulty_origin("backup", &backup_dir, FailMode::Healthy); let mut thresholds = HashMap::new(); thresholds.insert(OriginType::Local, 1); - let monitor = Arc::new(HealthMonitor::new(Duration::from_secs(30)).with_per_type_thresholds(thresholds)); + let monitor = + Arc::new(HealthMonitor::new(Duration::from_secs(30)).with_per_type_thresholds(thresholds)); let registry = Arc::new(OriginRegistry::new(monitor.clone())); registry.register(primary.clone(), 1); @@ -231,21 +247,44 @@ async fn test_origin_recovery_resumes_routing() { let dir = TempDir::new().unwrap(); setup_test_file(&dir, "test.txt", b"content"); - let faulty = create_faulty_origin("recovering", &dir, FailMode::ReturnError(ErrorKind::ConnectionRefused)); + let faulty = create_faulty_origin( + "recovering", + &dir, + FailMode::ReturnError(ErrorKind::ConnectionRefused), + ); let mut thresholds = HashMap::new(); thresholds.insert(OriginType::Local, 1); - let monitor = Arc::new(HealthMonitor::new(Duration::from_secs(30)).with_per_type_thresholds(thresholds)); + let monitor = + Arc::new(HealthMonitor::new(Duration::from_secs(30)).with_per_type_thresholds(thresholds)); monitor.add_origin(faulty.clone()); monitor.check_now(&OriginId::from("recovering")).await; - assert_eq!(monitor.get_state(&OriginId::from("recovering")).unwrap().status, HealthStatus::Unhealthy); + assert_eq!( + monitor + .get_state(&OriginId::from("recovering")) + .unwrap() + .status, + HealthStatus::Unhealthy + ); faulty.set_mode(FailMode::Healthy); monitor.check_now(&OriginId::from("recovering")).await; - assert_eq!(monitor.get_state(&OriginId::from("recovering")).unwrap().status, HealthStatus::Healthy); - assert_eq!(monitor.get_state(&OriginId::from("recovering")).unwrap().consecutive_failures, 0); + assert_eq!( + monitor + .get_state(&OriginId::from("recovering")) + .unwrap() + .status, + HealthStatus::Healthy + ); + assert_eq!( + monitor + .get_state(&OriginId::from("recovering")) + .unwrap() + .consecutive_failures, + 0 + ); } #[tokio::test] @@ -262,9 +301,12 @@ async fn test_local_origin_health_check_has_timeout() { monitor.check_now(&OriginId::from("slow")).await; let elapsed = start.elapsed(); - assert!(elapsed < Duration::from_secs(2), - "Issue 4.2.1: Health check should timeout in <2s, took {:?}", elapsed); - + assert!( + elapsed < Duration::from_secs(2), + "Issue 4.2.1: Health check should timeout in <2s, took {:?}", + elapsed + ); + let state = monitor.get_state(&OriginId::from("slow")).unwrap(); assert_eq!(state.status, HealthStatus::Unhealthy); } @@ -288,7 +330,11 @@ async fn test_health_checks_run_in_parallel() { monitor.check_all().await; let elapsed = start.elapsed(); - assert!(elapsed < Duration::from_millis(350), "Issue 4.2.2: check_all() should run in parallel (sequential would take ~600ms), took {:?}", elapsed); + assert!( + elapsed < Duration::from_millis(350), + "Issue 4.2.2: check_all() should run in parallel (sequential would take ~600ms), took {:?}", + elapsed + ); } #[test] @@ -298,9 +344,13 @@ fn test_tantivy_survives_uncommitted_crash() { { let index = SearchIndex::open(&index_path).unwrap(); - index.index_file(&make_file_meta(1, "/a.flac", 1000)).unwrap(); + index + .index_file(&make_file_meta(1, "/a.flac", 1000)) + .unwrap(); index.commit().unwrap(); - index.index_file(&make_file_meta(2, "/b.flac", 1000)).unwrap(); + index + .index_file(&make_file_meta(2, "/b.flac", 1000)) + .unwrap(); } let index = SearchIndex::open(&index_path).unwrap(); @@ -329,10 +379,7 @@ async fn test_fd_exhaustion_handling() { Ok(_store) => {} Err(e) => { let msg = format!("{}", e); - assert!( - !msg.contains("panic"), - "Should not panic on fd exhaustion" - ); + assert!(!msg.contains("panic"), "Should not panic on fd exhaustion"); } } @@ -356,8 +403,11 @@ async fn test_corrupt_chunk_auto_refetched() { setup_test_file(&origin_dir, "test.flac", test_content); let store = Arc::new(setup_cas(dir.path()).await); - - let origin = Arc::new(LocalOrigin::new(OriginId::from("local"), origin_dir.path().to_path_buf())); + + let origin = Arc::new(LocalOrigin::new( + OriginId::from("local"), + origin_dir.path().to_path_buf(), + )); let fetcher = Arc::new(ContentFetcher::new(store.clone())); fetcher.register_origin(origin); @@ -378,8 +428,13 @@ async fn test_corrupt_chunk_auto_refetched() { let manifest = fetcher.fetch_file(FileId(1)).await.unwrap(); let chunk_hash = manifest.chunks[0].hash; let hex = chunk_hash.as_hex(); - let chunk_path = dir.path().join("chunks").join(&hex[0..2]).join(&hex[2..4]).join(&hex); - + let chunk_path = dir + .path() + .join("chunks") + .join(&hex[0..2]) + .join(&hex[2..4]) + .join(&hex); + let mut corrupted = std::fs::read(&chunk_path).unwrap(); corrupted[0] = corrupted[0].wrapping_add(1); std::fs::write(&chunk_path, &corrupted).unwrap(); @@ -388,9 +443,16 @@ async fn test_corrupt_chunk_auto_refetched() { reader.register_manifest(manifest); let result = reader.read(FileId(1), 0, test_content.len() as u32).await; - - assert!(result.is_ok(), "Issue 6.4: Corrupted chunk should be auto-refetched from origin"); - assert_eq!(&result.unwrap()[..], test_content, "Data should match original after re-fetch"); + + assert!( + result.is_ok(), + "Issue 6.4: Corrupted chunk should be auto-refetched from origin" + ); + assert_eq!( + &result.unwrap()[..], + test_content, + "Data should match original after re-fetch" + ); } #[tokio::test] @@ -404,8 +466,11 @@ async fn test_missing_chunk_triggers_origin_fetch() { setup_test_file(&origin_dir, "test.flac", test_content); let store = Arc::new(setup_cas(dir.path()).await); - - let origin = Arc::new(LocalOrigin::new(OriginId::from("local"), origin_dir.path().to_path_buf())); + + let origin = Arc::new(LocalOrigin::new( + OriginId::from("local"), + origin_dir.path().to_path_buf(), + )); let fetcher = Arc::new(ContentFetcher::new(store.clone())); fetcher.register_origin(origin); @@ -426,17 +491,29 @@ async fn test_missing_chunk_triggers_origin_fetch() { let manifest = fetcher.fetch_file(FileId(1)).await.unwrap(); let chunk_hash = manifest.chunks[0].hash; let hex = chunk_hash.as_hex(); - let chunk_path = dir.path().join("chunks").join(&hex[0..2]).join(&hex[2..4]).join(&hex); - + let chunk_path = dir + .path() + .join("chunks") + .join(&hex[0..2]) + .join(&hex[2..4]) + .join(&hex); + std::fs::remove_file(&chunk_path).unwrap(); let reader = FileReader::with_fetcher(store, fetcher); reader.register_manifest(manifest); let result = reader.read(FileId(1), 0, test_content.len() as u32).await; - - assert!(result.is_ok(), "Issue 6.4: Missing chunk should be re-fetched from origin"); - assert_eq!(&result.unwrap()[..], test_content, "Data should match original after re-fetch"); + + assert!( + result.is_ok(), + "Issue 6.4: Missing chunk should be re-fetched from origin" + ); + assert_eq!( + &result.unwrap()[..], + test_content, + "Data should match original after re-fetch" + ); } #[tokio::test] @@ -449,15 +526,20 @@ async fn test_passthrough_mode_when_cache_disk_dead() { let test_content = b"passthrough test data"; setup_test_file(&origin_dir, "test.flac", test_content); - let store = Arc::new(CasStore::open(CasConfig { - chunks_dir: dir.path().join("chunks"), - max_size: 10, - shard_levels: 2, - }) - .await - .unwrap()); - - let origin = Arc::new(LocalOrigin::new(OriginId::from("local"), origin_dir.path().to_path_buf())); + let store = Arc::new( + CasStore::open(CasConfig { + chunks_dir: dir.path().join("chunks"), + max_size: 10, + shard_levels: 2, + }) + .await + .unwrap(), + ); + + let origin = Arc::new(LocalOrigin::new( + OriginId::from("local"), + origin_dir.path().to_path_buf(), + )); let fetcher = Arc::new(ContentFetcher::new(store.clone())); fetcher.register_origin(origin); @@ -477,7 +559,10 @@ async fn test_passthrough_mode_when_cache_disk_dead() { let manifest = fetcher.fetch_file(FileId(1)).await.unwrap(); - assert!(!manifest.chunks.is_empty(), "Issue 6.6: Fetch should complete even when CAS write fails (passthrough mode)"); + assert!( + !manifest.chunks.is_empty(), + "Issue 6.6: Fetch should complete even when CAS write fails (passthrough mode)" + ); } #[tokio::test] @@ -522,12 +607,18 @@ fn test_pid_file_prevents_concurrent_mount() { assert!(lock1.is_ok(), "Issue C9: First lock should succeed"); let lock2 = try_lock(&lock_path); - assert!(lock2.is_err(), "Issue C9: Second lock should fail (already held)"); + assert!( + lock2.is_err(), + "Issue C9: Second lock should fail (already held)" + ); drop(lock1); let lock3 = try_lock(&lock_path); - assert!(lock3.is_ok(), "Issue C9: Third lock should succeed after first released"); + assert!( + lock3.is_ok(), + "Issue C9: Third lock should succeed after first released" + ); } #[test] @@ -556,7 +647,10 @@ fn test_stale_mount_check_function_exists() { fn test_systemd_service_has_execstoppost() { let service_path = std::path::Path::new("../../dist/musicfs.service"); if !service_path.exists() { - panic!("Issue 3.7: dist/musicfs.service does not exist at {:?}", service_path); + panic!( + "Issue 3.7: dist/musicfs.service does not exist at {:?}", + service_path + ); } let content = std::fs::read_to_string(service_path).unwrap(); @@ -574,18 +668,27 @@ fn test_sd_notify_ready_sent() { let dir = TempDir::new().unwrap(); let socket_path = dir.path().join("notify.sock"); let socket = UnixDatagram::bind(&socket_path).unwrap(); - socket.set_read_timeout(Some(Duration::from_secs(1))).unwrap(); + socket + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); std::env::set_var("NOTIFY_SOCKET", &socket_path); let result = sd_notify::notify(false, &[sd_notify::NotifyState::Ready]); - assert!(result.is_ok(), "sd_notify should succeed when NOTIFY_SOCKET is set"); + assert!( + result.is_ok(), + "sd_notify should succeed when NOTIFY_SOCKET is set" + ); let mut buf = [0u8; 256]; let len = socket.recv(&mut buf).unwrap(); let msg = std::str::from_utf8(&buf[..len]).unwrap(); - assert!(msg.contains("READY=1"), "sd_notify should send READY=1, got: {}", msg); + assert!( + msg.contains("READY=1"), + "sd_notify should send READY=1, got: {}", + msg + ); std::env::remove_var("NOTIFY_SOCKET"); } @@ -615,7 +718,9 @@ async fn test_shutdown_flushes_tantivy() { { let index = SearchIndex::open(&idx_path).unwrap(); - index.index_file(&make_file_meta(1, "/a.flac", 1000)).unwrap(); + index + .index_file(&make_file_meta(1, "/a.flac", 1000)) + .unwrap(); index.commit().unwrap(); } @@ -678,7 +783,9 @@ async fn test_sigterm_triggers_shutdown() { let musicfs_bin = std::env::var("CARGO_BIN_EXE_musicfs").ok(); if musicfs_bin.is_none() { - eprintln!("Skipping test_sigterm_triggers_shutdown: musicfs binary not available in test context"); + eprintln!( + "Skipping test_sigterm_triggers_shutdown: musicfs binary not available in test context" + ); return; } @@ -690,7 +797,12 @@ async fn test_sigterm_triggers_shutdown() { std::fs::create_dir_all(&origin).unwrap(); let mut child = Command::new(&bin_path) - .args(["mount", "--origin", origin.to_str().unwrap(), mountpoint.to_str().unwrap()]) + .args([ + "mount", + "--origin", + origin.to_str().unwrap(), + mountpoint.to_str().unwrap(), + ]) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn(); @@ -716,7 +828,11 @@ async fn test_sigterm_triggers_shutdown() { } } child.wait().unwrap() - }).await; + }) + .await; - assert!(exit_result.is_ok(), "Issue 2.1: Process should exit within 10s after SIGTERM"); + assert!( + exit_result.is_ok(), + "Issue 2.1: Process should exit within 10s after SIGTERM" + ); } diff --git a/flake.lock b/flake.lock index 33c558f..483db26 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,21 @@ { "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -18,19 +34,75 @@ "type": "github" } }, - "nixpkgs-old": { - "flake": false, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": "nixpkgs" + }, "locked": { - "lastModified": 1558254092, - "narHash": "sha256-5v6XuO9dOVpB3ZGNyDvLqOvCnzlyyvscTYbNiQouSZo=", + "lastModified": 1778507602, + "narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1770073757, + "narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a7e559a5504572008567383c3dc8e142fa7a8633", + "rev": "47472570b1e607482890801aeaf29bfb749884f6", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-18.09", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1778443072, + "narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } @@ -38,7 +110,8 @@ "root": { "inputs": { "flake-utils": "flake-utils", - "nixpkgs-old": "nixpkgs-old" + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs_2" } }, "systems": { diff --git a/flake.nix b/flake.nix index 7123446..2dd29f8 100644 --- a/flake.nix +++ b/flake.nix @@ -1,115 +1,72 @@ { - description = "beetfs - FUSE filesystem for beets with metadata overlay (Python 2.7)"; + description = "MusicFS - FUSE filesystem for music with metadata overlay"; inputs = { - # Using nixos-18.09 - has all Python 2 packages: fuse, mutagen, jellyfish, munkres - # Mark as non-flake since 18.09 predates flakes - nixpkgs-old = { - url = "github:NixOS/nixpkgs/nixos-18.09"; - flake = false; - }; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; + git-hooks.url = "github:cachix/git-hooks.nix"; }; - outputs = { self, nixpkgs-old, flake-utils }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = import nixpkgs-old { - inherit system; - }; + outputs = { self, nixpkgs, flake-utils, git-hooks }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + }; - # Build beets 1.4.9 for Python 2 (last Python 2 compatible version) - # Using minimal deps - autotagger features (munkres, jellyfish) not needed for beetfs - beets-py2 = pkgs.python2Packages.buildPythonPackage rec { - pname = "beets"; - version = "1.4.9"; - - # Use legacy setup.py install (not pip/wheel) - format = "other"; - - src = pkgs.fetchFromGitHub { - owner = "beetbox"; - repo = "beets"; - rev = "v${version}"; - sha256 = "sha256-KO5jtqxw82ylbSKsJgUGr3bfxUPH8vanf/+kv//CreM="; - }; - - # No patching needed - nixpkgs-18.09 has all required Python 2 packages - - nativeBuildInputs = with pkgs.python2Packages; [ setuptools ]; - - buildPhase = "true"; # No build needed - - installPhase = '' - # Direct copy to avoid dependency resolution - mkdir -p $out/lib/python2.7/site-packages - cp -r beets $out/lib/python2.7/site-packages/ - cp -r beetsplug $out/lib/python2.7/site-packages/ - - # Create version file - echo "__version__ = '${version}'" >> $out/lib/python2.7/site-packages/beets/__init__.py - - # Create beet command wrapper - mkdir -p $out/bin - cat > $out/bin/beet << 'EOF' -#!${pkgs.python2.interpreter} -import sys -from beets.ui import main -main() -EOF - chmod +x $out/bin/beet - ''; - - propagatedBuildInputs = with pkgs.python2Packages; [ - pyyaml - mutagen - unidecode - enum34 - six - musicbrainzngs - jellyfish - munkres - ]; - - # Disable tests for faster builds - doCheck = false; - - meta = { - description = "Music library manager and tagger"; - homepage = "https://beets.io/"; + pre-commit-check = git-hooks.lib.${system}.run { + src = ./.; + hooks = { + rustfmt = { + enable = true; + packageOverrides = { + cargo = pkgs.cargo; + rustfmt = pkgs.rustfmt; }; }; - - pythonEnv = pkgs.python2.withPackages (ps: with ps; [ - fuse - mutagen - pyyaml - enum34 - six - jellyfish - munkres - unidecode - musicbrainzngs - ] ++ [ beets-py2 ]); - - in { - devShells.default = pkgs.mkShell { - buildInputs = [ - pythonEnv - pkgs.fuse - pkgs.ffmpeg - pkgs.flac - ]; - - shellHook = '' - unset PYTHONPATH - export PYTHONPATH="$PWD/beetsplug:$PWD/tests" - echo "beetfs development environment (Python 2.7)" - echo " Python: $(python --version 2>&1)" - echo " Run tests: cd tests && python -m unittest discover" - echo " Mount: beet mount " - ''; + clippy = { + enable = true; + packageOverrides = { + cargo = pkgs.cargo; + clippy = pkgs.clippy; + }; }; - } - ); + }; + }; + in { + checks = { + inherit pre-commit-check; + }; + + devShells.default = pkgs.mkShell rec { + inherit (pre-commit-check) shellHook; + + buildInputs = with pkgs; [ + pre-commit + gitleaks + + just + + pkg-config + fuse3 + sqlite + openssl + + rustc + cargo + cargo-watch + cargo-nextest + cargo-criterion + rust-analyzer + clippy + rustfmt + + clang + lld + + protobuf + grpcurl + ]; + }; + }); } diff --git a/musicfs/.cargo/config.toml b/musicfs/.cargo/config.toml deleted file mode 100644 index 6f74a14..0000000 --- a/musicfs/.cargo/config.toml +++ /dev/null @@ -1,10 +0,0 @@ -[build] -rustflags = ["-C", "link-arg=-fuse-ld=lld"] - -[target.x86_64-unknown-linux-gnu] -linker = "clang" - -[alias] -t = "test" -c = "check" -b = "build" diff --git a/musicfs/.gitignore b/musicfs/.gitignore deleted file mode 100644 index 2f7896d..0000000 --- a/musicfs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target/ diff --git a/musicfs/dist/PKGBUILD b/musicfs/dist/PKGBUILD deleted file mode 100644 index a4cc8c3..0000000 --- a/musicfs/dist/PKGBUILD +++ /dev/null @@ -1,23 +0,0 @@ -pkgname=musicfs -pkgver=0.1.0 -pkgrel=1 -pkgdesc="Metadata-Organized Music Filesystem" -arch=('x86_64') -url="https://github.com/yourusername/musicfs" -license=('MIT') -depends=('fuse3') -makedepends=('rust' 'cargo') -source=("$pkgname-$pkgver.tar.gz") -sha256sums=('SKIP') - -build() { - cd "$srcdir/$pkgname-$pkgver" - cargo build --release --locked -} - -package() { - cd "$srcdir/$pkgname-$pkgver" - install -Dm755 "target/release/musicfs" "$pkgdir/usr/bin/musicfs" - install -Dm644 "dist/musicfs.service" "$pkgdir/usr/lib/systemd/system/musicfs.service" - install -Dm644 "config.example.toml" "$pkgdir/etc/musicfs/config.example.toml" -} diff --git a/musicfs/dist/config.example.toml b/musicfs/dist/config.example.toml deleted file mode 100644 index 2171978..0000000 --- a/musicfs/dist/config.example.toml +++ /dev/null @@ -1,30 +0,0 @@ -mount_point = "/mnt/music" -cache_dir = "/var/cache/musicfs" - -[logging] -log_dir = "/var/log/musicfs" -json_output = true -journald = true -level = "musicfs=info,warn" -trace_sample_rate = 1.0 - -[cache] -metadata_cache_mb = 100 -content_cache_gb = 10 - -[health] -check_interval_secs = 30 -timeout_ms = 5000 -unhealthy_threshold = 3 - -[[origins]] -id = "local" -origin_type = "local" -priority = 1 -path = "/srv/music" - -[[origins]] -id = "nas" -origin_type = "nfs" -priority = 2 -mount_point = "/mnt/nas/music" diff --git a/musicfs/dist/logrotate.d/musicfs b/musicfs/dist/logrotate.d/musicfs deleted file mode 100644 index ec37e65..0000000 --- a/musicfs/dist/logrotate.d/musicfs +++ /dev/null @@ -1,9 +0,0 @@ -/var/log/musicfs/*.log { - daily - rotate 30 - compress - delaycompress - missingok - notifempty - create 0640 musicfs musicfs -} diff --git a/musicfs/dist/musicfs.service b/musicfs/dist/musicfs.service deleted file mode 100644 index 5fd030e..0000000 --- a/musicfs/dist/musicfs.service +++ /dev/null @@ -1,29 +0,0 @@ -[Unit] -Description=MusicFS - Metadata-Organized Music Filesystem -After=network.target - -[Service] -Type=notify -ExecStart=/usr/bin/musicfs mount --config /etc/musicfs/config.toml /mnt/music -ExecStop=/usr/bin/musicfs shutdown -ExecStopPost=/usr/bin/fusermount -uz /mnt/music || true -Restart=on-failure -RestartSec=5 -User=musicfs -Group=musicfs - -Environment="RUST_LOG=musicfs=info,warn" -StandardOutput=journal -StandardError=journal -SyslogIdentifier=musicfs -RateLimitIntervalSec=30s -RateLimitBurst=1000 - -NoNewPrivileges=true -ProtectSystem=strict -ProtectHome=read-only -ReadWritePaths=/var/cache/musicfs /var/log/musicfs /mnt/music -PrivateTmp=true - -[Install] -WantedBy=multi-user.target diff --git a/musicfs/dist/musicfs.spec b/musicfs/dist/musicfs.spec deleted file mode 100644 index 800da91..0000000 --- a/musicfs/dist/musicfs.spec +++ /dev/null @@ -1,39 +0,0 @@ -Name: musicfs -Version: 0.1.0 -Release: 1%{?dist} -Summary: Metadata-Organized Music Filesystem - -License: MIT -URL: https://github.com/yourusername/musicfs -Source0: %{name}-%{version}.tar.gz - -BuildRequires: rust >= 1.70 -BuildRequires: cargo -BuildRequires: fuse3-devel - -Requires: fuse3 - -%description -MusicFS is a virtual FUSE filesystem that organizes music files by metadata. - -%prep -%autosetup - -%build -cargo build --release --locked - -%install -install -Dm755 target/release/musicfs %{buildroot}%{_bindir}/musicfs -install -Dm644 dist/musicfs.service %{buildroot}%{_unitdir}/musicfs.service -install -Dm644 config.example.toml %{buildroot}%{_sysconfdir}/musicfs/config.example.toml - -%files -%license LICENSE -%doc README.md -%{_bindir}/musicfs -%{_unitdir}/musicfs.service -%config(noreplace) %{_sysconfdir}/musicfs/config.example.toml - -%changelog -* Mon Jan 01 2024 MusicFS Team - 0.1.0-1 -- Initial package diff --git a/musicfs/flake.lock b/musicfs/flake.lock deleted file mode 100644 index e93f5ce..0000000 --- a/musicfs/flake.lock +++ /dev/null @@ -1,96 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1778443072, - "narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1744536153, - "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" - } - }, - "rust-overlay": { - "inputs": { - "nixpkgs": "nixpkgs_2" - }, - "locked": { - "lastModified": 1778555852, - "narHash": "sha256-55EmwooVAS4UpA0oWd5wilKPRqCiHD5BAej9QiNwheY=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "f29b0f7a9f367e0056b716f8aa137cb41e784444", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/musicfs/flake.nix b/musicfs/flake.nix deleted file mode 100644 index d909c77..0000000 --- a/musicfs/flake.nix +++ /dev/null @@ -1,46 +0,0 @@ -{ - description = "MusicFS - FUSE filesystem for music libraries"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - rust-overlay.url = "github:oxalica/rust-overlay"; - flake-utils.url = "github:numtide/flake-utils"; - }; - - outputs = { self, nixpkgs, rust-overlay, flake-utils }: - flake-utils.lib.eachDefaultSystem (system: - let - overlays = [ (import rust-overlay) ]; - pkgs = import nixpkgs { inherit system overlays; }; - - rustToolchain = pkgs.rust-bin.stable.latest.default.override { - extensions = [ "rust-src" "rust-analyzer" "clippy" "rustfmt" ]; - }; - in - { - devShells.default = pkgs.mkShell rec { - buildInputs = with pkgs; [ - rustToolchain - pkg-config - fuse3 - sqlite - openssl - - clang - lld - - cargo-watch - cargo-nextest - cargo-criterion - - protobuf - grpcurl - ]; - - LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs; - RUST_BACKTRACE = "1"; - RUST_LOG = "debug"; - }; - } - ); -} diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index c442d70..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,580 +0,0 @@ -# -*- coding: utf-8 -*- -""" -beetfs E2E Test Infrastructure - -Base test class, fixtures, and utilities for testing the real beetfs FUSE filesystem. -Python 2.7 compatible - no mocks, real filesystem operations. -""" -from __future__ import print_function, unicode_literals - -import unittest -import subprocess -import tempfile -import shutil -import os -import sys -import time -import threading -import sqlite3 - -# Add parent directory to path for imports -BEETFS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, BEETFS_ROOT) -sys.path.insert(0, os.path.join(BEETFS_ROOT, 'beetsplug')) - -# Test constants -MOUNT_TIMEOUT = 30 # seconds -TEARDOWN_TIMEOUT = 5 # seconds -SYNTHETIC_FLAC_DURATION = 3 # seconds (smaller = faster tests) - -# Real music files location (from qBittorrent container) -REAL_MUSIC_PATH = '/home/fujin/.local/share/docker/volumes/containers_downloads/_data/Metallica - 72 Seasons (2023) [FLAC] 88' - - -def is_fuse_available(): - """Check if FUSE is available on the system""" - try: - with open(os.devnull, 'w') as devnull: - subprocess.check_call(['which', 'fusermount'], - stdout=devnull, stderr=devnull) - return True - except (subprocess.CalledProcessError, OSError): - return False - - -def is_ffmpeg_available(): - """Check if ffmpeg is available for synthetic FLAC generation""" - try: - with open(os.devnull, 'w') as devnull: - subprocess.check_call(['which', 'ffmpeg'], - stdout=devnull, stderr=devnull) - return True - except (subprocess.CalledProcessError, OSError): - return False - - -def is_flac_available(): - """Check if flac encoder is available""" - try: - with open(os.devnull, 'w') as devnull: - subprocess.check_call(['which', 'flac'], - stdout=devnull, stderr=devnull) - return True - except (subprocess.CalledProcessError, OSError): - return False - - -def create_synthetic_flac(output_path, duration_sec=SYNTHETIC_FLAC_DURATION, - artist='Test Artist', title='Test Track', - album='Test Album', year='2024', track='01', - genre='Test Genre'): - """ - Create a minimal FLAC file with known metadata. - - Uses ffmpeg to generate silence, then flac to encode with tags. - Result is ~100-500KB depending on duration. - - Args: - output_path: Where to write the FLAC file - duration_sec: Duration in seconds (default 3) - artist, title, album, year, track, genre: Metadata tags - - Returns: - Path to created FLAC file - - Raises: - subprocess.CalledProcessError: If ffmpeg or flac fails - RuntimeError: If tools not available - """ - if not is_ffmpeg_available(): - raise RuntimeError("ffmpeg not available - install it or use nix develop") - if not is_flac_available(): - raise RuntimeError("flac not available - install it or use nix develop") - - # Create temp WAV file - wav_fd, wav_path = tempfile.mkstemp(suffix='.wav') - os.close(wav_fd) - - try: - # Generate silence WAV using ffmpeg - with open(os.devnull, 'w') as devnull: - subprocess.check_call([ - 'ffmpeg', '-f', 'lavfi', '-i', - 'anullsrc=r=44100:cl=stereo', - '-t', str(duration_sec), - '-y', wav_path - ], stdout=devnull, stderr=devnull) - - # Convert to FLAC with metadata tags - with open(os.devnull, 'w') as devnull: - subprocess.check_call([ - 'flac', '--best', '--silent', - '-T', 'ARTIST={}'.format(artist), - '-T', 'TITLE={}'.format(title), - '-T', 'ALBUM={}'.format(album), - '-T', 'DATE={}'.format(year), - '-T', 'TRACKNUMBER={}'.format(track), - '-T', 'GENRE={}'.format(genre), - '-o', output_path, - wav_path - ], stdout=devnull, stderr=devnull) - - return output_path - finally: - # Cleanup temp WAV - if os.path.exists(wav_path): - os.unlink(wav_path) - - -class BeetsLibraryFixture(object): - """ - Creates an isolated beets library for testing. - - Sets up: - - Temp directory for library files - - SQLite database with test items - - Config pointing to the test library - """ - - def __init__(self, temp_dir=None): - self.temp_dir = temp_dir or tempfile.mkdtemp(prefix='beetfs_lib_') - self.db_path = os.path.join(self.temp_dir, 'library.db') - self.music_dir = os.path.join(self.temp_dir, 'music') - self.config_path = os.path.join(self.temp_dir, 'config.yaml') - - os.makedirs(self.music_dir) - self._create_config() - self._create_database() - - def _create_config(self): - """Create minimal beets config""" - config_content = """ -directory: {music_dir} -library: {db_path} -import: - copy: no - move: no - write: no -plugins: [] -""".format(music_dir=self.music_dir, db_path=self.db_path) - - with open(self.config_path, 'w') as f: - f.write(config_content) - - def _create_database(self): - """Create empty beets SQLite database with correct schema""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - # Create items table (simplified schema matching beets) - cursor.execute(''' - CREATE TABLE IF NOT EXISTS items ( - id INTEGER PRIMARY KEY, - path BLOB NOT NULL, - album_id INTEGER, - title TEXT, - artist TEXT, - album TEXT, - genre TEXT, - year INTEGER, - track INTEGER, - disc INTEGER, - length REAL, - bitrate INTEGER, - format TEXT, - samplerate INTEGER, - bitdepth INTEGER, - channels INTEGER, - mtime REAL, - added REAL - ) - ''') - - # Create albums table - cursor.execute(''' - CREATE TABLE IF NOT EXISTS albums ( - id INTEGER PRIMARY KEY, - artpath BLOB, - added REAL, - albumartist TEXT, - album TEXT, - genre TEXT, - year INTEGER - ) - ''') - - conn.commit() - conn.close() - - def add_item(self, path, title='Test Track', artist='Test Artist', - album='Test Album', genre='Test Genre', year=2024, - track=1, format='flac'): - """ - Add an item to the test library database. - - Args: - path: Absolute path to the audio file - title, artist, album, genre, year, track: Metadata - format: Audio format (flac, mp3, etc.) - - Returns: - Item ID - """ - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - cursor.execute(''' - INSERT INTO items (path, title, artist, album, genre, year, track, format, mtime, added) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', (path, title, artist, album, genre, year, track, format, - time.time(), time.time())) - - item_id = cursor.lastrowid - conn.commit() - conn.close() - - return item_id - - def get_item(self, item_id): - """Get item by ID""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute('SELECT * FROM items WHERE id = ?', (item_id,)) - row = cursor.fetchone() - conn.close() - return row - - def update_item(self, item_id, **kwargs): - """Update item metadata""" - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - set_clause = ', '.join('{} = ?'.format(k) for k in kwargs.keys()) - values = list(kwargs.values()) + [item_id] - - cursor.execute( - 'UPDATE items SET {} WHERE id = ?'.format(set_clause), - values - ) - - conn.commit() - conn.close() - - def cleanup(self): - """Remove all temp files""" - if os.path.exists(self.temp_dir): - shutil.rmtree(self.temp_dir, ignore_errors=True) - - -class BeetFSTestCase(unittest.TestCase): - """ - Base test class for beetfs e2e tests. - - Provides: - - FUSE availability checking - - Mount/unmount helpers with timeout - - Beets library fixture management - - Cleanup on test failure - - Python 2.7 compatible (no subprocess.timeout, etc.) - """ - - @classmethod - def setUpClass(cls): - """Check FUSE availability once for all tests""" - if not is_fuse_available(): - raise unittest.SkipTest("fusermount not available - FUSE required for e2e tests") - - def setUp(self): - """Create fresh temp directory and library for each test""" - self.temp_dir = tempfile.mkdtemp(prefix='beetfs_test_') - self.mount_dir = os.path.join(self.temp_dir, 'mount') - os.makedirs(self.mount_dir) - - self.library = BeetsLibraryFixture( - temp_dir=os.path.join(self.temp_dir, 'library') - ) - - self.fs_process = None - self._timeout_timer = None - - def tearDown(self): - """Cleanup: unmount filesystem and remove temp files""" - # Cancel any pending timeout - if self._timeout_timer: - self._timeout_timer.cancel() - - # Unmount if mounted - if self.fs_process and os.path.ismount(self.mount_dir): - self._unmount() - - # Kill process if still running - if self.fs_process and self.fs_process.poll() is None: - self.fs_process.terminate() - self._wait_for_process_exit(TEARDOWN_TIMEOUT) - if self.fs_process.poll() is None: - self.fs_process.kill() - - # Cleanup library - if hasattr(self, 'library'): - self.library.cleanup() - - # Remove temp directory - if hasattr(self, 'temp_dir') and os.path.exists(self.temp_dir): - shutil.rmtree(self.temp_dir, ignore_errors=True) - - def mount_beetfs(self, foreground=True): - """ - Mount beetfs filesystem. - - Args: - foreground: Run in foreground mode (required for testing) - - Raises: - AssertionError: If mount fails or times out - """ - # Build command to run beetfs - # We need to invoke beets with our test config and library - env = os.environ.copy() - env['BEETSDIR'] = self.library.temp_dir - - mount_script = ''' -import sys -sys.path.insert(0, "{beetfs_root}") -sys.path.insert(0, "{beetsplug}") - -import os -import re -os.environ["BEETSDIR"] = "{beetsdir}" - -from beets import config -from beets.library import Library - -config.read(user=False) -config["directory"] = "{music_dir}" -config["library"] = "{db_path}" - -lib = Library("{db_path}") - -import beetFs -import fuse - -fuse.fuse_python_api = (0, 2) - -beetFs.library = lib -beetFs.structure_depth = 4 -beetFs.structure_split = [0, 1, 2, 3] -beetFs.directory_structure = beetFs.FSNode({{}}, {{}}) - -for item in lib.items(): - mapping = beetFs.template_mapping(lib, item) - path_str = beetFs.PATH_FORMAT - for key, val in mapping.items(): - if val is not None: - clean_val = re.sub(r"[\\\\/:]|^\\.", "_", unicode(val)) - path_str = path_str.replace("$" + key, clean_val) - elements = path_str.split("/") - sub_elements = elements[0:beetFs.structure_depth-1] - for level in range(len(sub_elements)): - level_subbed = sub_elements[0:level+1] - beetFs.directory_structure.adddir(sub_elements, level_subbed[level]) - beetFs.directory_structure.addfile( - sub_elements, - elements[beetFs.structure_depth-1], - item.id - ) - -fs = beetFs.beetFileSystem( - version="%prog " + fuse.__version__, - usage="beetfs test mount", - dash_s_do="setsingle" -) - -fs.parser.add_option(mountopt="root", metavar="PATH", default="{music_dir}", - help="music library root path") -fs.parse(args=["{mount_dir}"], errex=1) -fs.flags = 0 -fs.multithreaded = False -fs.fuse_args.setmod("foreground") -fs.fuse_args.add("fsname=beetfs") -fs.fuse_args.add("nonempty") -fs.lib = lib - -fs.main() -'''.format( - beetfs_root=BEETFS_ROOT, - beetsplug=os.path.join(BEETFS_ROOT, 'beetsplug'), - beetsdir=self.library.temp_dir, - music_dir=self.library.music_dir, - db_path=self.library.db_path, - mount_dir=self.mount_dir - ) - - cmd = [sys.executable, '-c', mount_script] - - # Start process - self.fs_process = subprocess.Popen( - cmd, - env=env, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) - - # Setup timeout using threading.Timer (Python 2.7 compatible) - self._timeout_timer = threading.Timer( - MOUNT_TIMEOUT, - self._mount_timeout_handler - ) - self._timeout_timer.start() - - # Wait for mount - try: - self._wait_for_mount() - finally: - self._timeout_timer.cancel() - - def _mount_timeout_handler(self): - """Called if mount times out""" - if self.fs_process and self.fs_process.poll() is None: - self.fs_process.kill() - - def _wait_for_mount(self): - """Poll until filesystem is mounted""" - start = time.time() - while time.time() - start < MOUNT_TIMEOUT: - if os.path.ismount(self.mount_dir): - return - - # Check if process died - if self.fs_process.poll() is not None: - stdout, stderr = self.fs_process.communicate() - self.fail( - "beetfs process terminated prematurely with code {}.\n" - "stdout: {}\nstderr: {}".format( - self.fs_process.returncode, - stdout.decode('utf-8', errors='replace'), - stderr.decode('utf-8', errors='replace') - ) - ) - - time.sleep(0.1) - - self.fail("Mount timeout after {} seconds".format(MOUNT_TIMEOUT)) - - def _unmount(self): - """Unmount the filesystem using fusermount""" - with open(os.devnull, 'w') as devnull: - # Use -z for lazy unmount (handles busy mounts) - subprocess.call( - ['fusermount', '-z', '-u', self.mount_dir], - stdout=devnull, - stderr=devnull - ) - - def _wait_for_process_exit(self, timeout): - """Wait for process to exit (Python 2.7 compatible)""" - start = time.time() - while time.time() - start < timeout: - if self.fs_process.poll() is not None: - return True - time.sleep(0.1) - return False - - def create_test_flac(self, filename='test.flac', **metadata): - """ - Create a synthetic FLAC in the library music directory. - - Args: - filename: Name for the file - **metadata: Passed to create_synthetic_flac() - - Returns: - Absolute path to created file - """ - output_path = os.path.join(self.library.music_dir, filename) - create_synthetic_flac(output_path, **metadata) - return output_path - - def add_test_track(self, filename='test.flac', **metadata): - """ - Create a synthetic FLAC and add it to the library. - - Args: - filename: Name for the file - **metadata: Metadata for both file and database - - Returns: - Tuple of (file_path, item_id) - """ - # Default metadata - defaults = { - 'artist': 'Test Artist', - 'title': 'Test Track', - 'album': 'Test Album', - 'genre': 'Test Genre', - 'year': '2024', - 'track': '01' - } - defaults.update(metadata) - - # Create the file - file_path = self.create_test_flac(filename, **defaults) - - # Add to database (convert year and track to int for DB) - item_id = self.library.add_item( - path=file_path, - title=defaults['title'], - artist=defaults['artist'], - album=defaults['album'], - genre=defaults['genre'], - year=int(defaults['year']), - track=int(defaults['track']), - format='flac' - ) - - return file_path, item_id - - -class RealMusicTestCase(BeetFSTestCase): - """ - Test case that uses real music files from qBittorrent. - - Skipped if real music not available or E2E env var not set. - """ - - @classmethod - def setUpClass(cls): - super(RealMusicTestCase, cls).setUpClass() - - # Check if E2E tests are enabled - if not os.environ.get('E2E'): - raise unittest.SkipTest("E2E tests disabled - set E2E=1 to enable") - - # Check if real music is available - if not os.path.isdir(REAL_MUSIC_PATH): - raise unittest.SkipTest( - "Real music not available at {}".format(REAL_MUSIC_PATH) - ) - - def get_real_flac_files(self): - """Get list of real FLAC files""" - files = [] - for f in os.listdir(REAL_MUSIC_PATH): - if f.endswith('.flac'): - files.append(os.path.join(REAL_MUSIC_PATH, f)) - return sorted(files) - - -# Export test utilities -__all__ = [ - 'BeetFSTestCase', - 'RealMusicTestCase', - 'BeetsLibraryFixture', - 'create_synthetic_flac', - 'is_fuse_available', - 'is_ffmpeg_available', - 'is_flac_available', - 'BEETFS_ROOT', - 'REAL_MUSIC_PATH', - 'MOUNT_TIMEOUT', -] diff --git a/musicfs/tests/e2e/e2e_players.rs b/tests/e2e/e2e_players.rs similarity index 100% rename from musicfs/tests/e2e/e2e_players.rs rename to tests/e2e/e2e_players.rs diff --git a/musicfs/tests/integration/docker-compose.yml b/tests/integration/docker-compose.yml similarity index 100% rename from musicfs/tests/integration/docker-compose.yml rename to tests/integration/docker-compose.yml diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py deleted file mode 100644 index ee50af9..0000000 --- a/tests/test_error_handling.py +++ /dev/null @@ -1,161 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function, unicode_literals - -import errno -import os -import unittest - -from conftest import BeetFSTestCase - - -class TestENOENT(BeetFSTestCase): - - def test_stat_nonexistent_file(self): - self.mount_beetfs() - - nonexistent = os.path.join(self.mount_dir, 'does_not_exist.flac') - - with self.assertRaises(OSError) as ctx: - os.stat(nonexistent) - - self.assertEqual(ctx.exception.errno, errno.ENOENT) - - def test_open_nonexistent_file(self): - self.mount_beetfs() - - nonexistent = os.path.join(self.mount_dir, 'does_not_exist.flac') - - with self.assertRaises(IOError) as ctx: - open(nonexistent, 'rb') - - self.assertEqual(ctx.exception.errno, errno.ENOENT) - - def test_listdir_nonexistent_directory(self): - self.mount_beetfs() - - nonexistent = os.path.join(self.mount_dir, 'Nonexistent Artist') - - with self.assertRaises(OSError) as ctx: - os.listdir(nonexistent) - - self.assertEqual(ctx.exception.errno, errno.ENOENT) - - def test_stat_deeply_nested_nonexistent(self): - self.mount_beetfs() - - nonexistent = os.path.join( - self.mount_dir, 'Artist', 'Album', 'Track', 'Nested', 'Path' - ) - - with self.assertRaises(OSError) as ctx: - os.stat(nonexistent) - - self.assertEqual(ctx.exception.errno, errno.ENOENT) - - -class TestEOPNOTSUPP(BeetFSTestCase): - - def test_mkdir_not_supported(self): - self.mount_beetfs() - - new_dir = os.path.join(self.mount_dir, 'new_directory') - - with self.assertRaises(OSError) as ctx: - os.mkdir(new_dir) - - self.assertIn(ctx.exception.errno, [errno.EOPNOTSUPP, errno.EACCES, errno.EROFS]) - - def test_unlink_not_supported(self): - self.add_test_track(filename='delete_test.flac') - self.mount_beetfs() - - tracks = self._find_any_track() - if not tracks: - self.skipTest("No tracks found") - - with self.assertRaises(OSError) as ctx: - os.unlink(tracks[0]) - - self.assertIn(ctx.exception.errno, [errno.EOPNOTSUPP, errno.EACCES, errno.EROFS]) - - def test_rmdir_not_supported(self): - self.add_test_track(filename='rmdir_test.flac', artist='Rmdir Artist') - self.mount_beetfs() - - artist_dir = os.path.join(self.mount_dir, 'Rmdir Artist') - if not os.path.isdir(artist_dir): - self.skipTest("Artist directory not found") - - with self.assertRaises(OSError) as ctx: - os.rmdir(artist_dir) - - self.assertIn(ctx.exception.errno, [errno.EOPNOTSUPP, errno.EACCES, errno.EROFS, errno.ENOTEMPTY]) - - def test_rename_not_supported(self): - self.add_test_track(filename='rename_test.flac') - self.mount_beetfs() - - tracks = self._find_any_track() - if not tracks: - self.skipTest("No tracks found") - - new_path = tracks[0] + '.renamed' - - with self.assertRaises(OSError) as ctx: - os.rename(tracks[0], new_path) - - self.assertIn(ctx.exception.errno, [errno.EOPNOTSUPP, errno.EACCES, errno.EROFS]) - - def test_symlink_not_supported(self): - self.mount_beetfs() - - link_path = os.path.join(self.mount_dir, 'test_link') - - with self.assertRaises(OSError) as ctx: - os.symlink('/tmp', link_path) - - self.assertIn(ctx.exception.errno, [errno.EOPNOTSUPP, errno.EACCES, errno.EROFS]) - - def test_chmod_not_supported(self): - self.add_test_track(filename='chmod_test.flac') - self.mount_beetfs() - - tracks = self._find_any_track() - if not tracks: - self.skipTest("No tracks found") - - with self.assertRaises(OSError) as ctx: - os.chmod(tracks[0], 0o777) - - self.assertIn(ctx.exception.errno, [errno.EOPNOTSUPP, errno.EACCES, errno.EROFS]) - - def _find_any_track(self): - results = [] - for root, dirs, files in os.walk(self.mount_dir): - for f in files: - if f.endswith('.flac'): - results.append(os.path.join(root, f)) - return results - - -class TestEdgeCaseErrors(BeetFSTestCase): - - def test_access_empty_path(self): - self.mount_beetfs() - - with self.assertRaises((OSError, IOError)): - os.stat(os.path.join(self.mount_dir, '')) - - def test_open_directory_as_file(self): - self.add_test_track(filename='test.flac', artist='Dir Artist') - self.mount_beetfs() - - artist_dir = os.path.join(self.mount_dir, 'Dir Artist') - if os.path.isdir(artist_dir): - with self.assertRaises((OSError, IOError)): - with open(artist_dir, 'rb') as f: - f.read() - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_nested_bug.py b/tests/test_nested_bug.py deleted file mode 100644 index 36ddbda..0000000 --- a/tests/test_nested_bug.py +++ /dev/null @@ -1,141 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Tests to verify the nested methods bug in beetFs.py. - -Oracle discovered that lines 758-1144 are indented inside access() method, -making readdir, open, read, write, etc. unreachable as class methods. -These tests will fail until the bug is fixed. -""" -from __future__ import print_function, unicode_literals - -import os -import sys -import unittest - -from conftest import BEETFS_ROOT - -sys.path.insert(0, os.path.join(BEETFS_ROOT, 'beetsplug')) - - -class TestNestedMethodsBug(unittest.TestCase): - """Verify FUSE methods exist at class level (not nested inside access).""" - - @classmethod - def setUpClass(cls): - try: - from beetFs import beetFileSystem - cls.fs_class = beetFileSystem - except ImportError as e: - raise unittest.SkipTest("Cannot import beetFs: {}".format(e)) - - def test_readdir_is_class_method(self): - self.assertTrue( - hasattr(self.fs_class, 'readdir'), - "readdir should be a class method, not nested inside access()" - ) - - def test_open_is_class_method(self): - self.assertTrue( - hasattr(self.fs_class, 'open'), - "open should be a class method, not nested inside access()" - ) - - def test_read_is_class_method(self): - self.assertTrue( - hasattr(self.fs_class, 'read'), - "read should be a class method, not nested inside access()" - ) - - def test_write_is_class_method(self): - self.assertTrue( - hasattr(self.fs_class, 'write'), - "write should be a class method, not nested inside access()" - ) - - def test_release_is_class_method(self): - self.assertTrue( - hasattr(self.fs_class, 'release'), - "release should be a class method, not nested inside access()" - ) - - def test_opendir_is_class_method(self): - self.assertTrue( - hasattr(self.fs_class, 'opendir'), - "opendir should be a class method, not nested inside access()" - ) - - def test_releasedir_is_class_method(self): - self.assertTrue( - hasattr(self.fs_class, 'releasedir'), - "releasedir should be a class method, not nested inside access()" - ) - - def test_mkdir_is_class_method(self): - self.assertTrue( - hasattr(self.fs_class, 'mkdir'), - "mkdir should be a class method, not nested inside access()" - ) - - def test_unlink_is_class_method(self): - self.assertTrue( - hasattr(self.fs_class, 'unlink'), - "unlink should be a class method, not nested inside access()" - ) - - def test_truncate_is_class_method(self): - self.assertTrue( - hasattr(self.fs_class, 'truncate'), - "truncate should be a class method, not nested inside access()" - ) - - def test_fgetattr_is_class_method(self): - self.assertTrue( - hasattr(self.fs_class, 'fgetattr'), - "fgetattr should be a class method, not nested inside access()" - ) - - def test_flush_is_class_method(self): - self.assertTrue( - hasattr(self.fs_class, 'flush'), - "flush should be a class method, not nested inside access()" - ) - - def test_fsync_is_class_method(self): - self.assertTrue( - hasattr(self.fs_class, 'fsync'), - "fsync should be a class method, not nested inside access()" - ) - - -class TestMethodsCallable(unittest.TestCase): - """Verify methods can be called (not just exist).""" - - @classmethod - def setUpClass(cls): - try: - from beetFs import beetFileSystem - cls.fs_class = beetFileSystem - except ImportError as e: - raise unittest.SkipTest("Cannot import beetFs: {}".format(e)) - - def test_readdir_callable(self): - method = getattr(self.fs_class, 'readdir', None) - if method is None: - self.skipTest("readdir not found - nested bug not fixed yet") - self.assertTrue(callable(method)) - - def test_open_callable(self): - method = getattr(self.fs_class, 'open', None) - if method is None: - self.skipTest("open not found - nested bug not fixed yet") - self.assertTrue(callable(method)) - - def test_read_callable(self): - method = getattr(self.fs_class, 'read', None) - if method is None: - self.skipTest("read not found - nested bug not fixed yet") - self.assertTrue(callable(method)) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_read.py b/tests/test_read.py deleted file mode 100644 index 440c6f6..0000000 --- a/tests/test_read.py +++ /dev/null @@ -1,296 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Tests for file reading with metadata overlay. - -This is the CORE FEATURE of beetfs: -- File headers contain metadata from beets database -- Audio data passes through unchanged from original file -""" -from __future__ import print_function, unicode_literals - -import os -import unittest -from io import BytesIO - -from conftest import BeetFSTestCase, is_ffmpeg_available, is_flac_available - - -class TestReadBasic(BeetFSTestCase): - - def test_open_file(self): - file_path, item_id = self.add_test_track( - filename='test.flac', - artist='Test Artist', - title='Test Track' - ) - - self.mount_beetfs() - - entries = self._find_track_path('Test Artist', 'Test Track') - if not entries: - self.skipTest("Could not find track in mounted filesystem") - - track_path = entries[0] - - with open(track_path, 'rb') as f: - data = f.read(1024) - - self.assertIsInstance(data, bytes) - self.assertTrue(len(data) > 0) - - def test_read_returns_bytes(self): - self.add_test_track(filename='test.flac') - self.mount_beetfs() - - entries = self._find_any_track() - if not entries: - self.skipTest("No tracks found") - - with open(entries[0], 'rb') as f: - data = f.read(100) - - self.assertIsInstance(data, bytes) - - def test_read_flac_magic_bytes(self): - self.add_test_track(filename='test.flac') - self.mount_beetfs() - - entries = self._find_any_track() - if not entries: - self.skipTest("No tracks found") - - with open(entries[0], 'rb') as f: - magic = f.read(4) - - self.assertEqual(magic, b'fLaC', "FLAC files should start with 'fLaC' magic bytes") - - def _find_track_path(self, artist, title): - """Find track path in mounted filesystem.""" - results = [] - for root, dirs, files in os.walk(self.mount_dir): - for f in files: - if title.lower() in f.lower(): - results.append(os.path.join(root, f)) - return results - - def _find_any_track(self): - """Find any track in mounted filesystem.""" - results = [] - for root, dirs, files in os.walk(self.mount_dir): - for f in files: - if f.endswith('.flac'): - results.append(os.path.join(root, f)) - return results - - -@unittest.skipUnless(is_ffmpeg_available() and is_flac_available(), - "ffmpeg and flac required for metadata overlay tests") -class TestMetadataOverlay(BeetFSTestCase): - """ - Tests that verify the core metadata overlay functionality. - - The key insight: beetfs should return metadata from the DATABASE, - not from the original FILE. This allows presenting different metadata - than what's embedded in the file. - """ - - def setUp(self): - super(TestMetadataOverlay, self).setUp() - - try: - import mutagen.flac - self.mutagen_available = True - except ImportError: - self.mutagen_available = False - - def test_overlay_artist_from_database(self): - if not self.mutagen_available: - self.skipTest("mutagen required for metadata verification") - - import mutagen.flac - - file_path, item_id = self.add_test_track( - filename='overlay_test.flac', - artist='File Artist', - title='Test Track', - album='Test Album' - ) - - self.library.update_item(item_id, artist='Database Artist') - - self.mount_beetfs() - - mounted_tracks = self._find_track_in_mount('Database Artist') - if not mounted_tracks: - self.skipTest("Track not found under 'Database Artist' - path uses DB metadata") - - with open(mounted_tracks[0], 'rb') as f: - mounted_data = f.read() - - mounted_flac = mutagen.flac.FLAC(BytesIO(mounted_data)) - - artist_tag = mounted_flac.get('artist', [None])[0] - self.assertEqual(artist_tag, 'Database Artist', - "Mounted file should have artist from database, not file") - - def test_overlay_title_from_database(self): - if not self.mutagen_available: - self.skipTest("mutagen required for metadata verification") - - import mutagen.flac - - file_path, item_id = self.add_test_track( - filename='title_test.flac', - artist='Test Artist', - title='File Title' - ) - - self.library.update_item(item_id, title='Database Title') - - self.mount_beetfs() - - mounted_tracks = self._find_any_track() - if not mounted_tracks: - self.skipTest("No tracks found") - - with open(mounted_tracks[0], 'rb') as f: - mounted_data = f.read() - - mounted_flac = mutagen.flac.FLAC(BytesIO(mounted_data)) - - title_tag = mounted_flac.get('title', [None])[0] - self.assertEqual(title_tag, 'Database Title') - - def test_original_file_unchanged(self): - if not self.mutagen_available: - self.skipTest("mutagen required for metadata verification") - - import mutagen.flac - - file_path, item_id = self.add_test_track( - filename='original_test.flac', - artist='Original Artist', - title='Original Title' - ) - - self.library.update_item(item_id, artist='Modified Artist') - - self.mount_beetfs() - - original_flac = mutagen.flac.FLAC(file_path) - original_artist = original_flac.get('artist', [None])[0] - - self.assertEqual(original_artist, 'Original Artist', - "Original file should remain unchanged") - - def test_audio_data_passthrough(self): - file_path, item_id = self.add_test_track( - filename='audio_test.flac', - artist='Test Artist' - ) - - with open(file_path, 'rb') as f: - original_data = f.read() - - self.mount_beetfs() - - mounted_tracks = self._find_any_track() - if not mounted_tracks: - self.skipTest("No tracks found") - - with open(mounted_tracks[0], 'rb') as f: - mounted_data = f.read() - - original_audio_start = original_data.find(b'\xff\xf8') - mounted_audio_start = mounted_data.find(b'\xff\xf8') - - if original_audio_start > 0 and mounted_audio_start > 0: - original_audio = original_data[original_audio_start:original_audio_start + 1000] - mounted_audio = mounted_data[mounted_audio_start:mounted_audio_start + 1000] - - self.assertEqual(original_audio, mounted_audio, - "Audio data should pass through unchanged") - - def _find_track_in_mount(self, artist_name): - """Find tracks under a specific artist directory.""" - results = [] - artist_dir = os.path.join(self.mount_dir, artist_name) - if not os.path.isdir(artist_dir): - return results - - for root, dirs, files in os.walk(artist_dir): - for f in files: - if f.endswith('.flac'): - results.append(os.path.join(root, f)) - return results - - def _find_any_track(self): - """Find any track in mounted filesystem.""" - results = [] - for root, dirs, files in os.walk(self.mount_dir): - for f in files: - if f.endswith('.flac'): - results.append(os.path.join(root, f)) - return results - - -class TestReadFullFile(BeetFSTestCase): - - def test_read_entire_file(self): - file_path, item_id = self.add_test_track(filename='full_read.flac') - - original_size = os.path.getsize(file_path) - - self.mount_beetfs() - - mounted_tracks = self._find_any_track() - if not mounted_tracks: - self.skipTest("No tracks found") - - with open(mounted_tracks[0], 'rb') as f: - data = f.read() - - self.assertTrue(len(data) > 0) - self.assertAlmostEqual(len(data), original_size, delta=original_size * 0.5) - - def test_read_with_offset(self): - self.add_test_track(filename='offset_test.flac') - self.mount_beetfs() - - mounted_tracks = self._find_any_track() - if not mounted_tracks: - self.skipTest("No tracks found") - - with open(mounted_tracks[0], 'rb') as f: - f.seek(100) - data = f.read(100) - - self.assertEqual(len(data), 100) - - def test_multiple_reads(self): - self.add_test_track(filename='multi_read.flac') - self.mount_beetfs() - - mounted_tracks = self._find_any_track() - if not mounted_tracks: - self.skipTest("No tracks found") - - with open(mounted_tracks[0], 'rb') as f: - chunk1 = f.read(512) - chunk2 = f.read(512) - chunk3 = f.read(512) - - self.assertEqual(len(chunk1), 512) - self.assertEqual(len(chunk2), 512) - - def _find_any_track(self): - results = [] - for root, dirs, files in os.walk(self.mount_dir): - for f in files: - if f.endswith('.flac'): - results.append(os.path.join(root, f)) - return results - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_readdir.py b/tests/test_readdir.py deleted file mode 100644 index f41746a..0000000 --- a/tests/test_readdir.py +++ /dev/null @@ -1,179 +0,0 @@ -# -*- coding: utf-8 -*- -"""Tests for directory listing operations.""" -from __future__ import print_function, unicode_literals - -import os -import unittest - -from conftest import BeetFSTestCase - - -class TestReaddirEmpty(BeetFSTestCase): - - def test_list_empty_root(self): - self.mount_beetfs() - - entries = os.listdir(self.mount_dir) - self.assertEqual(entries, []) - - def test_list_root_returns_list(self): - self.mount_beetfs() - - entries = os.listdir(self.mount_dir) - self.assertIsInstance(entries, list) - - -class TestReaddirWithContent(BeetFSTestCase): - - def setUp(self): - super(TestReaddirWithContent, self).setUp() - - self.add_test_track( - filename='track01.flac', - artist='Pink Floyd', - title='Comfortably Numb', - album='The Wall', - year='1979', - track='01' - ) - self.add_test_track( - filename='track02.flac', - artist='Pink Floyd', - title='Another Brick in the Wall', - album='The Wall', - year='1979', - track='02' - ) - self.add_test_track( - filename='track03.flac', - artist='Led Zeppelin', - title='Stairway to Heaven', - album='Led Zeppelin IV', - year='1971', - track='01' - ) - - def test_list_root_shows_artists(self): - self.mount_beetfs() - - entries = os.listdir(self.mount_dir) - - self.assertIn('Pink Floyd', entries) - self.assertIn('Led Zeppelin', entries) - - def test_list_artist_shows_albums(self): - self.mount_beetfs() - - artist_dir = os.path.join(self.mount_dir, 'Pink Floyd') - entries = os.listdir(artist_dir) - - self.assertTrue(any('The Wall' in e for e in entries)) - - def test_list_album_shows_tracks(self): - self.mount_beetfs() - - artist_dir = os.path.join(self.mount_dir, 'Pink Floyd') - albums = os.listdir(artist_dir) - wall_album = [a for a in albums if 'The Wall' in a][0] - - album_dir = os.path.join(artist_dir, wall_album) - tracks = os.listdir(album_dir) - - self.assertEqual(len(tracks), 2) - - def test_path_format_includes_year(self): - self.mount_beetfs() - - artist_dir = os.path.join(self.mount_dir, 'Pink Floyd') - albums = os.listdir(artist_dir) - - wall_album = [a for a in albums if 'The Wall' in a][0] - self.assertIn('1979', wall_album) - - def test_path_format_includes_format(self): - self.mount_beetfs() - - artist_dir = os.path.join(self.mount_dir, 'Pink Floyd') - albums = os.listdir(artist_dir) - - wall_album = [a for a in albums if 'The Wall' in a][0] - self.assertIn('FLAC', wall_album.upper()) - - def test_track_filename_includes_number(self): - self.mount_beetfs() - - artist_dir = os.path.join(self.mount_dir, 'Pink Floyd') - albums = os.listdir(artist_dir) - wall_album = [a for a in albums if 'The Wall' in a][0] - album_dir = os.path.join(artist_dir, wall_album) - - tracks = os.listdir(album_dir) - - self.assertTrue(any(t.startswith('01') or t.startswith('1') for t in tracks)) - - -class TestReaddirUnicode(BeetFSTestCase): - - def test_unicode_artist_name(self): - self.add_test_track( - filename='bjork.flac', - artist='Björk', - title='Hyperballad', - album='Post' - ) - - self.mount_beetfs() - entries = os.listdir(self.mount_dir) - - self.assertTrue( - any('Bj' in e for e in entries), - "Unicode artist name should appear in listing" - ) - - def test_unicode_album_name(self): - self.add_test_track( - filename='sigur.flac', - artist='Sigur Ros', - title='Hoppipolla', - album='Takk...' - ) - - self.mount_beetfs() - artist_dir = os.path.join(self.mount_dir, 'Sigur Ros') - - if os.path.isdir(artist_dir): - albums = os.listdir(artist_dir) - self.assertTrue(len(albums) > 0) - - -class TestReaddirSpecialChars(BeetFSTestCase): - - def test_artist_with_ampersand(self): - self.add_test_track( - filename='guns.flac', - artist="Guns N' Roses", - title='Welcome to the Jungle', - album='Appetite for Destruction' - ) - - self.mount_beetfs() - entries = os.listdir(self.mount_dir) - - self.assertTrue(len(entries) > 0) - - def test_title_with_question_mark(self): - self.add_test_track( - filename='who.flac', - artist='The Who', - title='Who Are You?', - album='Who Are You' - ) - - self.mount_beetfs() - entries = os.listdir(self.mount_dir) - - self.assertIn('The Who', entries) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_smoke.py b/tests/test_smoke.py deleted file mode 100644 index fac5992..0000000 --- a/tests/test_smoke.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -"""Smoke tests for beetfs mount/unmount lifecycle.""" -from __future__ import print_function, unicode_literals - -import os -import unittest - -from conftest import BeetFSTestCase, is_fuse_available - - -class TestMountLifecycle(BeetFSTestCase): - - def test_fuse_available(self): - self.assertTrue(is_fuse_available()) - - def test_mount_empty_library(self): - self.mount_beetfs() - self.assertTrue(os.path.ismount(self.mount_dir)) - - def test_unmount_clean(self): - self.mount_beetfs() - self.assertTrue(os.path.ismount(self.mount_dir)) - - self._unmount() - self._wait_for_process_exit(5) - - self.assertFalse(os.path.ismount(self.mount_dir)) - - def test_mount_with_single_track(self): - self.add_test_track( - filename='track01.flac', - artist='Smoke Test Artist', - title='Smoke Test Track', - album='Smoke Test Album' - ) - - self.mount_beetfs() - self.assertTrue(os.path.ismount(self.mount_dir)) - - def test_mount_directory_accessible(self): - self.mount_beetfs() - - entries = os.listdir(self.mount_dir) - self.assertIsInstance(entries, list) - - def test_temp_directory_created(self): - self.assertTrue(os.path.isdir(self.temp_dir)) - self.assertTrue(os.path.isdir(self.mount_dir)) - - def test_library_fixture_created(self): - self.assertTrue(os.path.isfile(self.library.db_path)) - self.assertTrue(os.path.isdir(self.library.music_dir)) - - -class TestMountWithContent(BeetFSTestCase): - - def test_mount_with_multiple_tracks(self): - for i in range(3): - self.add_test_track( - filename='track{:02d}.flac'.format(i + 1), - artist='Test Artist', - title='Track {}'.format(i + 1), - album='Test Album', - track=str(i + 1) - ) - - self.mount_beetfs() - self.assertTrue(os.path.ismount(self.mount_dir)) - - def test_mount_with_unicode_metadata(self): - self.add_test_track( - filename='unicode_track.flac', - artist='Tëst Ärtîst', - title='Ünïcödé Träck', - album='Spëcîäl Chäräctërs' - ) - - self.mount_beetfs() - self.assertTrue(os.path.ismount(self.mount_dir)) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_stat.py b/tests/test_stat.py deleted file mode 100644 index a865004..0000000 --- a/tests/test_stat.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function, unicode_literals - -import os -import stat -import unittest - -from conftest import BeetFSTestCase - - -class TestStatFile(BeetFSTestCase): - - def setUp(self): - super(TestStatFile, self).setUp() - self.file_path, self.item_id = self.add_test_track( - filename='stat_test.flac', - artist='Test Artist', - title='Test Track', - album='Test Album' - ) - - def test_stat_returns_stat_result(self): - self.mount_beetfs() - - tracks = self._find_any_track() - if not tracks: - self.skipTest("No tracks found") - - result = os.stat(tracks[0]) - self.assertTrue(hasattr(result, 'st_mode')) - self.assertTrue(hasattr(result, 'st_size')) - - def test_stat_file_mode(self): - self.mount_beetfs() - - tracks = self._find_any_track() - if not tracks: - self.skipTest("No tracks found") - - result = os.stat(tracks[0]) - self.assertTrue(stat.S_ISREG(result.st_mode)) - - def test_stat_file_size_positive(self): - self.mount_beetfs() - - tracks = self._find_any_track() - if not tracks: - self.skipTest("No tracks found") - - result = os.stat(tracks[0]) - self.assertGreater(result.st_size, 0) - - def test_stat_file_size_matches_original(self): - original_size = os.path.getsize(self.file_path) - - self.mount_beetfs() - - tracks = self._find_any_track() - if not tracks: - self.skipTest("No tracks found") - - result = os.stat(tracks[0]) - self.assertAlmostEqual(result.st_size, original_size, delta=original_size * 0.5) - - def _find_any_track(self): - results = [] - for root, dirs, files in os.walk(self.mount_dir): - for f in files: - if f.endswith('.flac'): - results.append(os.path.join(root, f)) - return results - - -class TestStatDirectory(BeetFSTestCase): - - def test_stat_root(self): - self.mount_beetfs() - - result = os.stat(self.mount_dir) - self.assertTrue(stat.S_ISDIR(result.st_mode)) - - def test_stat_artist_directory(self): - self.add_test_track( - filename='test.flac', - artist='Stat Test Artist' - ) - - self.mount_beetfs() - - artist_dir = os.path.join(self.mount_dir, 'Stat Test Artist') - if os.path.isdir(artist_dir): - result = os.stat(artist_dir) - self.assertTrue(stat.S_ISDIR(result.st_mode)) - - -class TestStatfs(BeetFSTestCase): - - def test_statvfs_returns_result(self): - self.mount_beetfs() - - result = os.statvfs(self.mount_dir) - self.assertTrue(hasattr(result, 'f_bsize')) - self.assertTrue(hasattr(result, 'f_blocks')) - - -class TestAccess(BeetFSTestCase): - - def test_access_root_readable(self): - self.mount_beetfs() - - self.assertTrue(os.access(self.mount_dir, os.R_OK)) - - def test_access_root_executable(self): - self.mount_beetfs() - - self.assertTrue(os.access(self.mount_dir, os.X_OK)) - - def test_access_file_readable(self): - self.add_test_track(filename='access_test.flac') - self.mount_beetfs() - - tracks = self._find_any_track() - if not tracks: - self.skipTest("No tracks found") - - self.assertTrue(os.access(tracks[0], os.R_OK)) - - def _find_any_track(self): - results = [] - for root, dirs, files in os.walk(self.mount_dir): - for f in files: - if f.endswith('.flac'): - results.append(os.path.join(root, f)) - return results - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_write.py b/tests/test_write.py deleted file mode 100644 index b63eefa..0000000 --- a/tests/test_write.py +++ /dev/null @@ -1,161 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function, unicode_literals - -import os -import unittest -from io import BytesIO - -from conftest import BeetFSTestCase, is_ffmpeg_available, is_flac_available - - -@unittest.skipUnless(is_ffmpeg_available() and is_flac_available(), - "ffmpeg and flac required for write tests") -class TestWriteMetadata(BeetFSTestCase): - - def setUp(self): - super(TestWriteMetadata, self).setUp() - try: - import mutagen.flac - self.mutagen = mutagen.flac - except ImportError: - self.mutagen = None - - def test_write_opens_without_error(self): - self.add_test_track(filename='write_test.flac') - self.mount_beetfs() - - tracks = self._find_any_track() - if not tracks: - self.skipTest("No tracks found") - - try: - with open(tracks[0], 'r+b') as f: - pass - except IOError: - self.skipTest("Write not supported") - - def test_write_to_header_updates_db(self): - if not self.mutagen: - self.skipTest("mutagen required") - - file_path, item_id = self.add_test_track( - filename='db_write_test.flac', - artist='Original Artist', - title='Original Title' - ) - - self.mount_beetfs() - - tracks = self._find_any_track() - if not tracks: - self.skipTest("No tracks found") - - with open(tracks[0], 'rb') as f: - original_data = f.read() - - flac = self.mutagen.FLAC(BytesIO(original_data)) - flac['artist'] = ['Modified Artist'] - - output = BytesIO() - flac.save(output) - modified_data = output.getvalue() - - try: - with open(tracks[0], 'r+b') as f: - f.write(modified_data[:1024]) - except IOError: - self.skipTest("Write not supported") - - item = self.library.get_item(item_id) - if item: - self.assertIn('Modified', str(item)) - - def test_original_file_unchanged_after_write(self): - if not self.mutagen: - self.skipTest("mutagen required") - - file_path, item_id = self.add_test_track( - filename='unchanged_test.flac', - artist='Original Artist' - ) - - with open(file_path, 'rb') as f: - original_content = f.read() - - self.mount_beetfs() - - tracks = self._find_any_track() - if tracks: - try: - with open(tracks[0], 'r+b') as f: - f.write(b'\x00' * 100) - except IOError: - pass - - with open(file_path, 'rb') as f: - after_content = f.read() - - self.assertEqual(original_content, after_content) - - def _find_any_track(self): - results = [] - for root, dirs, files in os.walk(self.mount_dir): - for f in files: - if f.endswith('.flac'): - results.append(os.path.join(root, f)) - return results - - -class TestWriteAudioDiscarded(BeetFSTestCase): - - def test_write_to_audio_region_no_error(self): - file_path, item_id = self.add_test_track(filename='audio_write.flac') - - self.mount_beetfs() - - tracks = self._find_any_track() - if not tracks: - self.skipTest("No tracks found") - - try: - with open(tracks[0], 'r+b') as f: - f.seek(10000) - f.write(b'\x00' * 100) - except IOError: - pass - - def test_audio_unchanged_after_write_attempt(self): - file_path, item_id = self.add_test_track(filename='audio_unchanged.flac') - - with open(file_path, 'rb') as f: - f.seek(10000) - original_audio = f.read(100) - - self.mount_beetfs() - - tracks = self._find_any_track() - if tracks: - try: - with open(tracks[0], 'r+b') as f: - f.seek(10000) - f.write(b'\xff' * 100) - except IOError: - pass - - with open(file_path, 'rb') as f: - f.seek(10000) - after_audio = f.read(100) - - self.assertEqual(original_audio, after_audio) - - def _find_any_track(self): - results = [] - for root, dirs, files in os.walk(self.mount_dir): - for f in files: - if f.endswith('.flac'): - results.append(os.path.join(root, f)) - return results - - -if __name__ == '__main__': - unittest.main()