2
# Copyright 2004 Daniel Burrows
5
# This maps filenames to active file objects. It is responsible for
6
# managing file objects that exist during the program's execution (as
7
# opposed to the cache).
9
# This could be merged with the cache, but then I'd have to write
10
# custom picklers to only pickle the cache data, which doesn't work
11
# with cPickle, and the only real advantage is that you don't have to
12
# maintain the cache data separately from the store. Given that
13
# invalid cache info is explicitly allowed (it just has to be
14
# recalculated on startup), this is not a huge deal.
16
# Circular symlinks will be broken. If a file has multiple names,
17
# some of the names will be arbitrarily discarded.
26
class FileStoreFileOperationError(Exception):
27
"""An error that indicates that an operation failed on some files.
28
The recommended (multi-line) error message is stored in the
29
strerror attribute."""
31
def __init__(self, failed, strerror):
33
self.__strerror=strerror
35
def __getattr__(self, name):
36
if name == 'strerror':
43
return '%s (%s)'%(fn,strerror)
45
self.strerror='%s%s'%(self.__strerror,'\n'.join(map(make_error, self.failed)))
48
raise AttributeError, name
50
class SaveError(FileStoreFileOperationError):
51
"""An error that indicates that some files failed to be saved."""
53
def __init__(self, failed):
54
FileStoreFileOperationError.__init__(self, failed, 'Changes to the following files could not be saved:\n')
57
# Return a list of the failed files
58
return 'Failed to save files: %s'%','.join(map(lambda x:x[0], self.failed))
60
class LoadError(FileStoreFileOperationError):
61
"""An error that indicates that some files failed to be saved."""
63
def __init__(self, failed):
64
FileStoreFileOperationError.__init__(self, failed, 'The following files could not be read:\n')
67
# Return a list of the failed files
68
return 'Failed to load files: %s'%','.join(map(lambda x:x[0], self.failed))
70
class NotDirectoryError(Exception):
71
"""This error is raised when a non-directory is passed to the add_dir method."""
73
def __init__(self, dir):
74
Exception.__init__(self, dir)
75
self.strerror='%s is not a directory'%dir
78
"Returns the extension of the given filename, or None if it has no extension."
79
if not '.' in fn or fn.rfind('.')==len(fn)-1:
82
return fn[fn.rfind('.')+1:]
85
"""A collection of music files, indexed by name. Files are added
86
to the store using the add_dir and add_file functions. Each file
87
has exactly one corresponding 'file object' in the store, whose
88
lifetime is equal to that of the store itself."""
89
def __init__(self, cache):
90
"""Initializes an empty store attached to the given cache."""
93
self.modified_files=sets.Set()
94
self.inodes=sets.Set()
97
def add_dir(self, dir, callback=lambda cur,max:None, set=None):
98
"""Adds the given directory and any files recursively
99
contained inside it to this file store. 'set' may be a
100
mutable set; file objects added as a result of this operation
101
will be placed in 'set'."""
103
if not os.path.isdir(dir):
104
raise NotDirectoryError(dir)
107
self.__find_files(dir, sets.Set(), candidates, callback)
112
for fn in candidates:
116
self.add_file(fn, set)
124
raise LoadError(failed)
126
# Finds all files in the given directory and subdirectories,
127
# following symlinks and avoiding cycles.
128
def __find_files(self, dir, seen_dirs, output, callback=lambda cur,max:None):
129
"""Returns a list of all files contained in the given directory and
130
subdirectories which have an extension that we recognize. The
131
result is built in the list 'output'."""
132
assert(os.path.isdir(dir))
134
dir_ino=os.stat(dir).st_ino
138
if dir_ino not in seen_dirs and os.access(dir, os.R_OK|os.X_OK):
139
seen_dirs.add(dir_ino)
141
for name in os.listdir(dir):
142
fullname=os.path.join(dir, name)
144
if os.path.isfile(fullname) and musicfile.file_types.has_key(fname_ext(fullname)):
145
output.append(fullname)
146
elif os.path.isdir(fullname):
147
self.__find_files(fullname, seen_dirs, output, callback)
151
def add_file(self, fn, set=None):
152
"""Adds the given file to the store. If an exception is
153
raised when we try to open the file, print it and
154
continue. 'set' may be a mutable set, in which case any file
155
object which is successfully created will be added to it."""
156
fn=os.path.normpath(fn)
157
if self.files.has_key(fn):
158
# We've already got one! (it's very nice, too)
159
set.add(self.files[fn])
164
if file_ino not in self.inodes and os.access(fn, os.R_OK):
165
self.inodes.add(file_ino)
167
# Find the file extension and associated handler
169
if musicfile.file_types.has_key(ext):
172
cacheinf=self.cache.get(fn, st)
176
new_file=musicfile.file_types[ext](self, fn, cacheinf)
178
self.files[fn]=new_file
179
self.cache.put(new_file, st)
182
except EnvironmentError,e:
183
raise LoadError([(fn, e.strerror)])
184
except musicfile.MusicFileError,e:
185
raise LoadError([(fn, e.strerror)])
187
raise LoadError([(fn, None)])
189
def commit(self, callback=lambda cur,max:None, S=None):
190
"""Commit all changes to files in the set S to the store. If
191
S is not specified or None, it defaults to the entire store."""
193
modified=self.modified_files
195
modified=S & self.modified_files
205
except EnvironmentError,e:
206
failed.append((f.fn,e.strerror))
207
except musicfile.MusicFileError,e:
208
failed.append((f.fn,e.strerror))
210
failed.append((f.fn,None))
218
raise SaveError(failed)
220
def revert(self, callback=lambda cur,max:None, S=None):
221
"""Revert all modifications to files in the set S. If S is
222
not specified or None, it defaults to the entire store."""
224
modified=self.modified_files
226
modified=S & self.modified_files
236
def set_modified(self, file, modified):
237
"""Updates whether the given file is known to be modified."""
239
self.modified_files.add(file)
241
self.modified_files.remove(file)
243
def modified_count(self, S=None):
244
"""Returns the number of files in the set S that are modified.
245
If S is not specified or None, it defaults to the entire
249
modified=self.modified_files
251
modified=S & self.modified_files