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()