diff --git a/beetsplug/beetFs.py b/beetsplug/beetFs.py
index 9bb1f78..3415dce 100644
--- a/beetsplug/beetFs.py
+++ b/beetsplug/beetFs.py
@@ -1,47 +1,48 @@
-'''
- beetFs
- Copyright 2010 Martin Eve
+"""
+beetFs
+Copyright 2010 Martin Eve
- This file is part of beetFs.
+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 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.
+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 .
+You should have received a copy of the GNU General Public License
+along with beetFs. If not, see .
-'''
+"""
-from beets.plugins import BeetsPlugin
-from beets.ui import Subcommand
-import beets
+import calendar
+import datetime
+import errno
+import fuse
import logging
-from string import Template
import operator
import os
-import tempfile
import re
-import fuse
-import errno
import stat
-import datetime
-import time
-import calendar
-import mutagen
-from mutagen import flac, id3
-from mutagen.flac import FLAC, Padding, MetadataBlock, VCFLACDict, CueSheet, SeekTable, FLACNoHeaderError
-from mutagen.id3 import ID3
-import io
+import struct
+from errno import EINVAL
from io import BytesIO
+from string import Template
-path_format = "$artist/$album ($year) [$format_upper]/$track - $artist - $title.$format"
+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')
@@ -50,1054 +51,1094 @@ 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'),
+""" 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_FIELDS = [
+ ('length', 'real'),
+ ('bitrate', 'int'),
+] + METADATA_RW_FIELDS
-metadata_keys = map(operator.itemgetter(0), metadata_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'
+ """ 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
- if mapping['album'] == '':
- mapping['album'] = 'Unknown Album'
+ format_ = os.path.splitext(item.path)[1][1:]
+ mapping['format'] = re.sub(r'[\\/:]|^\.', '_', format_)
+ mapping['format_upper'] = re.sub(r'[\\/:]|^\.', '_', format_).upper()
- if mapping['year'] == '0':
- mapping['year'] = 'Unknown Year'
-
- if mapping['title'] == '':
- mapping['title'] = 'Unknown Track'
+ # 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
- 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)
- root = {}
- 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)
+ # check we have a command line argument
+ if not args:
+ raise beets.ui.UserError('no mountpoint specified')
- # do the substitutions
- level_subbed = {}
- for level in range(0, structure_depth):
- level_subbed[level] = templates[level].substitute(mapping)
+ # 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
- # 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])
+ # 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))
- # 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()
- pass
- 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]
-
-import mutagen
-from mutagen import flac
-from mutagen.flac import FLAC, Padding, MetadataBlock
+ """ 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)
+ """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.
+ 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)))
- If no filename is given, the one most recently loaded is used.
+ framedata = [self.__save_frame(frame) for (key, frame) in frames]
+ framedata.extend([data for data in self.unknown_frames
+ if len(data) > 10])
- 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
+ framedata = ''.join(framedata)
+ framesize = len(framedata)
- The lack of a way to update only an ID3v1 tag is intentional.
- """
+ 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)
- # 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)))
+ framesize = BitPaddedInt.to_str(outsize, width=4)
+ flags = 0
+ header = struct.pack('>3sBBB4s', 'ID3', 4, 0, flags, framesize)
+ data = header + framedata
- framedata = [self.__save_frame(frame) for (key, frame) in frames]
- framedata.extend([data for data in self.unknown_frames
- if len(data) > 10])
-
+ if (insize < outsize):
+ insert_bytes(f, outsize-insize, insize+10)
+ f.seek(0)
- framedata = ''.join(framedata)
- framesize = len(framedata)
+ try:
+ f.seek(-128, 2)
+ except IOError, err:
+ if err.errno != EINVAL:
+ raise
+ f.seek(0, 2) # ensure read won't get "TAG"
- if filename is None: filename = self.filename
- f = open(filename, 'rb+')
- try:
- idata = f.read(10)
- try: id3, vmaj, vrev, flags, insize = unpack('>3sBBB4s', idata)
- except struct.error: id3, insize = '', 0
- insize = BitPaddedInt(insize)
- if id3 != 'ID3': insize = -10
+ 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))
- if insize >= framesize: outsize = insize
- else: outsize = (framesize + 1023) & ~0x3FF
- framedata += '\x00' * (outsize - framesize)
+ finally:
+ f.close()
- framesize = BitPaddedInt.to_str(outsize, width=4)
- flags = 0
- header = 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:
- from errno import EINVAL
- 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)
- 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")
- 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")
- 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 error(
- "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 error("> 1 CueSheet block found")
- elif block.code == SeekTable.code:
- if self.seektable is None: self.seektable = block
- else: raise error("> 1 SeekTable block found")
- return (byte >> 7) ^ 1
+ logging.info("Loaded INF")
- def get_header(self, filename=None):
-
- f = self.fileobj
- f.seek(0)
+ 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
- # 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)
+ def get_header(self, filename=None):
- header = self.__check_header(f)
- available = self.__find_audio_offset(f) - header # "fLaC" and maybe ID3
- data = MetadataBlock.writeblocks(self.metadata_blocks)
+ f = self.fileobj
+ f.seek(0)
- 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
+ # 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)
- if len(data) != available:
- # We couldn't reduce the padding enough.
- diff = (len(data) - available)
-
- self.__offset = len("fLaC" + data)
+ 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
- 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
+ """ 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 == None:
- root = self
-
- if elements:
- topdir = elements.pop(0)
- return self.getnode(elements, root=root.dirs[topdir])
- else:
- # base case
- return root
+ 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()
- def adddir(self, elements, directory, root=None):
- if root == 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 == 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 == 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('/')
- subdepth = path.count('/') + 1
-
- # 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 __init__(self, path, lib):
+ self.path = path
+ self.lib = lib
- def release(self):
- # decrement the instance count
- if self.instance_count > 0:
- self.instance_count = self.instance_count - 1
- return False
- else:
- return True
+ pathsplit = path[1:].split('/')
- 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
+ # 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
- if offset < self.bound:
- # load interpolated file into memory
+ # open the on-disk file for reading
+ self.file_object = open(self.real_path, 'r+')
+ self.instance_count = 1
- filedata = self.header + self.music_data
+ # 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())
- # 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)
+ # 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
- # 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
+ 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):
+ 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
+ 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_atime(self):
+ return self.epoch_datetime(self.st_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 _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)
- 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 __init__(self, *args, **kwargs):
+ LOG_FILENAME = "LOG"
+ logging.basicConfig(filename=LOG_FILENAME, level=logging.INFO,)
- def fsinit(self):
- # called after filesystem is mounted
- #self.lib = self.cmdline[1][0]
- self.lib = library
- self.files = {}
+ logging.info("Preparing to mount file system")
+ super(beetFileSystem, self).__init__(*args, **kwargs)
- logging.info("Filesystem mounted")
-
- def fsdestroy(self):
- logging.info("Unmounting file system")
+ def fsinit(self):
+ # called after filesystem is mounted
+ #self.lib = self.cmdline[1][0]
+ self.lib = library
+ self.files = {}
- def statfs(self):
- logging.info("statfs")
+ logging.info("Filesystem mounted")
- # 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 fsdestroy(self):
+ logging.info("Unmounting file system")
-
- 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('/')
+ def statfs(self):
+ logging.info("statfs")
- 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
+ # 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("~"))
- 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):
+ def getattr(self, path):
+ logging.info("getattr: %s" % path)
- atime, mtime = times
- logging.info("utime: %s (atime %s, mtime %s)" % (path, atime, mtime))
- return -errno.EOPNOTSUPP
+ 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('/')
- def utimens(self, path, atime, mtime):
+ 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
- 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
+ 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
- # 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)
+ except Exception as e:
+ logging.error(e)
+ return -errno.ENOENT
- # 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 (A character special device, created with mknod)
- # S_IFBLK: 060000 (A block special device, created with mknod)
- # S_IFSOCK: 0140000 (A socket, created with mkfifo)
+ # 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
- # 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 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 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 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
- def unlink(self, path):
- """Deletes a file."""
- logging.info("unlink: %s" % path)
- return -errno.EOPNOTSUPP
+ # 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
- def rmdir(self, path):
- """Deletes a directory."""
- logging.info("rmdir: %s" % path)
- return -errno.EOPNOTSUPP
+ return 0
- def symlink(self, target, name):
- """
- Creates a symbolic link from path to target.
+ 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
- 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).
+ 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)
- 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
+ # 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)
- 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
+ # 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 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 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 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 unlink(self, path):
+ """ Deletes a file."""
+ logging.info("unlink: %s" % path)
+ 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 rmdir(self, path):
+ """ Deletes a directory."""
+ logging.info("rmdir: %s" % path)
+ 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
+ def symlink(self, target, name):
+ """
+ Creates a symbolic link from path to target.
- ### 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.
+ 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).
- 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))
+ 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 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).
+ 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
- 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 == 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)]]
-
+ 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).
- 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.
+ 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 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.
+ 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
- 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)))
-
- file_path = None
-
- try:
- if self.files == 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 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 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 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
- 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]
+ ### 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 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 opendir(self, path):
+ """
+ Checks permissions for listing a directory.
+ This should check the 'r' (read) permission on the directory.
- 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.
+ 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]])
- 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 == None:
- file_path = None
-
- try:
- if self.files == 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).
+ def releasedir(self, path, dh=None):
+ """ Closes an open directory. Allows filesystem to clean up."""
+ logging.info("releasedir: %s (dh %s)" % (path, dh))
- 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))
+ 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))
- if fh == None:
- file_path = None
-
- try:
- if self.files == 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 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).
- 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
+ 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