~ubuntu-branches/ubuntu/trusty/musiclibrarian/trusty

« back to all changes in this revision

Viewing changes to build/lib/musiclibrarian/filestore.py

  • Committer: Bazaar Package Importer
  • Author(s): Barry deFreese
  • Date: 2008-02-12 22:36:16 UTC
  • mfrom: (3.1.2 lenny)
  • Revision ID: james.westby@ubuntu.com-20080212223616-smcqyhbtxq6h7smw
Tags: 1.6-2.1
* Non-maintainer upload.
* Bump versioned build-deps for debhelper and python-central.
* XS-Python-Version: all. (Closes: #445399).
* Add Copyright holder to debian/copyright.
  + Link to GPL-2.
* Add Homepage field.
  + Remove upstream URL from package description.
* Bump compat to 5 to match debhelper build-dep.
* Bump Standards Version to 3.7.3.
  + Menu policy transition.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# filestore.py
2
 
#  Copyright 2004 Daniel Burrows
3
 
#
4
 
#
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).
8
 
#
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.
15
 
#
16
 
#  Circular symlinks will be broken.  If a file has multiple names,
17
 
#  some of the names will be arbitrarily discarded.
18
 
 
19
 
import cache
20
 
import musicfile
21
 
import os
22
 
import os.path
23
 
import sets
24
 
import sys
25
 
 
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."""
30
 
 
31
 
    def __init__(self, failed, strerror):
32
 
        self.failed=failed
33
 
        self.__strerror=strerror
34
 
 
35
 
    def __getattr__(self, name):
36
 
        if name == 'strerror':
37
 
            def make_error(x):
38
 
                fn,strerror=x
39
 
 
40
 
                if strerror == None:
41
 
                    return fn
42
 
                else:
43
 
                    return '%s (%s)'%(fn,strerror)
44
 
 
45
 
            self.strerror='%s%s'%(self.__strerror,'\n'.join(map(make_error, self.failed)))
46
 
            return self.strerror
47
 
        else:
48
 
            raise AttributeError, name
49
 
 
50
 
class SaveError(FileStoreFileOperationError):
51
 
    """An error that indicates that some files failed to be saved."""
52
 
 
53
 
    def __init__(self, failed):
54
 
        FileStoreFileOperationError.__init__(self, failed, 'Changes to the following files could not be saved:\n')
55
 
 
56
 
    def __str__(self):
57
 
        # Return a list of the failed files
58
 
        return 'Failed to save files: %s'%','.join(map(lambda x:x[0], self.failed))
59
 
 
60
 
class LoadError(FileStoreFileOperationError):
61
 
    """An error that indicates that some files failed to be saved."""
62
 
 
63
 
    def __init__(self, failed):
64
 
        FileStoreFileOperationError.__init__(self, failed, 'The following files could not be read:\n')
65
 
 
66
 
    def __str__(self):
67
 
        # Return a list of the failed files
68
 
        return 'Failed to load files: %s'%','.join(map(lambda x:x[0], self.failed))
69
 
 
70
 
class NotDirectoryError(Exception):
71
 
    """This error is raised when a non-directory is passed to the add_dir method."""
72
 
 
73
 
    def __init__(self, dir):
74
 
        Exception.__init__(self, dir)
75
 
        self.strerror='%s is not a directory'%dir
76
 
 
77
 
def fname_ext(fn):
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:
80
 
        return None
81
 
 
82
 
    return fn[fn.rfind('.')+1:]
83
 
 
84
 
class FileStore:
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."""
91
 
 
92
 
        self.files={}
93
 
        self.modified_files=sets.Set()
94
 
        self.inodes=sets.Set()
95
 
        self.cache=cache
96
 
 
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'."""
102
 
 
103
 
        if not os.path.isdir(dir):
104
 
            raise NotDirectoryError(dir)
105
 
 
106
 
        candidates=[]
107
 
        self.__find_files(dir, sets.Set(), candidates, callback)
108
 
 
109
 
        cur=0
110
 
        max=len(candidates)
111
 
        failed=[]
112
 
        for fn in candidates:
113
 
            callback(cur, max)
114
 
            cur+=1
115
 
            try:
116
 
                self.add_file(fn, set)
117
 
            except LoadError,e:
118
 
                failed+=e.failed
119
 
        callback(max, max)
120
 
 
121
 
        self.cache.flush()
122
 
 
123
 
        if failed <> []:
124
 
            raise LoadError(failed)
125
 
 
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))
133
 
 
134
 
        dir_ino=os.stat(dir).st_ino
135
 
 
136
 
        callback(None, None)
137
 
 
138
 
        if dir_ino not in seen_dirs and os.access(dir, os.R_OK|os.X_OK):
139
 
            seen_dirs.add(dir_ino)
140
 
 
141
 
            for name in os.listdir(dir):
142
 
                fullname=os.path.join(dir, name)
143
 
 
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)
148
 
 
149
 
        return output
150
 
 
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])
160
 
            return
161
 
 
162
 
        st=os.stat(fn)
163
 
        file_ino=st.st_ino
164
 
        if file_ino not in self.inodes and os.access(fn, os.R_OK):
165
 
            self.inodes.add(file_ino)
166
 
 
167
 
            # Find the file extension and associated handler
168
 
            ext=fname_ext(fn)
169
 
            if musicfile.file_types.has_key(ext):
170
 
                try:
171
 
                    try:
172
 
                        cacheinf=self.cache.get(fn, st)
173
 
                    except:
174
 
                        cacheinf=None
175
 
 
176
 
                    new_file=musicfile.file_types[ext](self, fn, cacheinf)
177
 
 
178
 
                    self.files[fn]=new_file
179
 
                    self.cache.put(new_file, st)
180
 
                    if set <> None:
181
 
                        set.add(new_file)
182
 
                except EnvironmentError,e:
183
 
                    raise LoadError([(fn, e.strerror)])
184
 
                except musicfile.MusicFileError,e:
185
 
                    raise LoadError([(fn, e.strerror)])
186
 
                except:
187
 
                    raise LoadError([(fn, None)])
188
 
 
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."""
192
 
        if S == None:
193
 
            modified=self.modified_files
194
 
        else:
195
 
            modified=S & self.modified_files
196
 
 
197
 
        cur=0
198
 
        max=len(modified)
199
 
        failed=[]
200
 
        for f in modified:
201
 
            callback(cur, max)
202
 
            cur+=1
203
 
            try:
204
 
                f.commit()
205
 
            except EnvironmentError,e:
206
 
                failed.append((f.fn,e.strerror))
207
 
            except musicfile.MusicFileError,e:
208
 
                failed.append((f.fn,e.strerror))
209
 
            except:
210
 
                failed.append((f.fn,None))
211
 
            try:
212
 
                cache.put(f)
213
 
            except:
214
 
                pass
215
 
        callback(max, max)
216
 
 
217
 
        if failed <> []:
218
 
            raise SaveError(failed)
219
 
 
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."""
223
 
        if S == None:
224
 
            modified=self.modified_files
225
 
        else:
226
 
            modified=S & self.modified_files
227
 
 
228
 
        cur=0
229
 
        max=len(modified)
230
 
        for f in modified:
231
 
            callback(cur, max)
232
 
            cur+=1
233
 
            f.revert()
234
 
        callback(max, max)
235
 
 
236
 
    def set_modified(self, file, modified):
237
 
        """Updates whether the given file is known to be modified."""
238
 
        if modified:
239
 
            self.modified_files.add(file)
240
 
        else:
241
 
            self.modified_files.remove(file)
242
 
 
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
246
 
        store."""
247
 
 
248
 
        if S == None:
249
 
            modified=self.modified_files
250
 
        else:
251
 
            modified=S & self.modified_files
252
 
 
253
 
        return len(modified)
254
 
 
255