Ticket #7858: mythfs.py

File mythfs.py, 13.3 KB (added by Raymond Wagner <raymond@…>, 14 years ago)
Line 
1#!/usr/bin/env python
2
3try:
4    import _find_fuse_parts
5except ImportError:
6    pass
7import fuse, re, errno, stat, os, sys
8from fuse import Fuse
9from time import time, mktime
10from datetime import date
11from MythTV import MythDB, MythVideo, ftopen, MythBE, Video, Recorded, MythLog
12
13if not hasattr(fuse, '__version__'):
14    raise RuntimeError, \
15        "your fuse-py doesn't know of fuse.__version__, probably it's too old."
16
17fuse.fuse_python_api = (0, 2)
18
19TIMEOUT = 30
20#LOG = MythLog('MythFS',logfile='mythlog.log')
21
22class URI( object ):
23    def __init__(self, uri):
24        self.uri = uri
25    def open(self, mode='r'):
26        return ftopen(self.uri, mode)
27
28class Attr(fuse.Stat):
29    def __init__(self):
30        self.st_mode = 0
31        self.st_ino = 0
32        self.st_dev = 0
33        self.st_blksize = 0
34        self.st_nlink = 1
35        self.st_uid = os.getuid()
36        self.st_gid = os.getgid()
37        self.st_rdev = 0
38        self.st_size = 0
39        self.st_atime = 0
40        self.st_mtime = 0
41        self.st_ctime = 0
42
43class File( object ):
44    def __repr__(self):
45        return str(self.path)
46
47    def __init__(self, path='', data=None):
48        #LOG.log(LOG.IMPORTANT, 'Adding File', path)
49        self.db = None
50        self.time = time()
51        self.data = data
52        self.children = {}
53        self.attr = Attr()
54        self.path = path
55        self.attr.st_ino = 0
56        if self.data:   # file
57            self.attr.st_mode = stat.S_IFREG | 0444
58            self.fillAttr()
59        else:           # directory
60            self.attr.st_mode = stat.S_IFDIR | 0555
61            self.path += '/'
62        #LOG.log(LOG.IMPORTANT, 'Attr         ', str(self.attr.__dict__))
63
64    def update(self): pass
65        # refresh data and update attributes
66    def fillAttr(self): pass
67        # define attributes with existing data
68    def getPath(self, data): return []
69        # produce split path list from video object
70    def getData(self, full=False, single=None):
71        if full:
72            # return zip(objects, ids)
73            return (None, None)
74        elif single:
75            # return single video object
76            return None
77        else:
78            # return current list of ids
79            return None
80
81    def populate(self):
82        if len(self.children) == 0:
83            # run first population
84            self.pathlist = {}
85            for data,id in self.getData(full=True):
86                path = self.getPath(data)
87                self.add(path, data)
88                self.pathlist[id] = path
89        else:
90            if (time() - self.time) < 30:
91                return
92            self.time = time()
93            # run maintenance
94            curlist = self.pathlist.keys()
95            newlist = self.getData()
96            for i in range(len(curlist)-1, -1, -1):
97                # filter unchanged entries
98                if curlist[i] in newlist:
99                    del newlist[newlist.index(curlist[i])]
100                    del curlist[i]
101            #LOG.log(LOG.IMPORTANT, 'Maintenance add', str(newlist))
102            #LOG.log(LOG.IMPORTANT, 'Maintenance delete', str(curlist))
103            for id in curlist:
104                self.delete(self.pathlist[id])
105                del self.pathlist[id]
106            for id in newlist:
107                data = self.getData(single=id)
108                path = self.getPath(data)
109                self.add(path, data)
110                self.pathlist[id] = path
111            self.time = time()
112
113    def add(self, path, data):
114        # walk down list of folders
115        name = path[0]
116        if len(path) == 1: # creating file
117            if name not in self.children:
118                self.children[name] = self.__class__(self.path+name, data)
119            else:
120                count = 0
121                oldname = name.rsplit('.', 1)
122                while True:
123                    count += 1
124                    name = '%s (%d).%s' % (oldname[0], count, oldname[1])
125                    if name not in self.children:
126                        self.children[name] = \
127                                    self.__class__(self.path+name, data)
128                        break
129        else: # creating folder
130            if name not in self.children:
131                self.children[name] = self.__class__(self.path+name)
132            self.children[name].add(path[1:], data)
133        if self.children[name].attr.st_ctime > self.attr.st_ctime:
134            self.attr.st_ctime = self.children[name].attr.st_ctime
135        if self.children[name].attr.st_mtime > self.attr.st_mtime:
136            self.attr.st_mtime = self.children[name].attr.st_mtime
137        if self.children[name].attr.st_atime > self.attr.st_atime:
138            self.attr.st_atime = self.children[name].attr.st_atime
139        self.attr.st_size = len(self.children)
140        #LOG.log(LOG.IMPORTANT, self.path+' ATTR', str(self.attr.__dict__))
141
142    def delete(self, path):
143        name = str(path[0])
144        if len(path) == 1:
145            #LOG.log(LOG.IMPORTANT, 'Deleting File', name)
146            del self.children[name]
147            self.attr.st_size += -1
148        else:
149            self.children[name].delete(path[1:])
150            if self.children[name].attr.st_size == 0:
151                del self.children[name]
152                self.attr.st_size += -1
153
154    def getObj(self, path):
155        # returns object at end of list of folders
156        if path[0] == '': # root folder
157            return self
158        elif path[0] not in self.children: # no file
159            return None
160        elif len(path) == 1: # file object
161            self.children[path[0]].update()
162            return self.children[path[0]]
163        else: # recurse through folder
164            return self.children[path[0]].getObj(path[1:])
165
166    def getAttr(self, path):
167        f = self.getObj(path)
168        if f is None:
169            return None
170        else:
171            return f.attr
172
173    def open(self, path):
174        f = self.getObj(path)
175        if f is None:
176            return None
177        else:
178            return f.data.open()
179
180    def addStr(self, path, data=None):
181        self.add(path.lstrip('/').split('/'), data)
182
183    def getObjStr(self, path):
184        self.populate()
185        return self.getObj(path.lstrip('/').split('/'))
186
187    def getAttrStr(self, path):
188        self.populate()
189        return self.getAttr(path.lstrip('/').split('/'))
190
191    def openStr(self, path):
192        self.populate()
193        return self.open(path.lstrip('/').split('/'))
194
195class VideoFile( File ):
196    def fillAttr(self):
197        if self.data.insertdate is not None:
198            ctime = mktime(self.data.insertdate.timetuple())
199        else:
200            ctime = time()
201        self.attr.st_ctime = ctime
202        self.attr.st_mtime = ctime
203        self.attr.st_atime = ctime
204        self.attr.st_size = self.data.filesize
205
206    def getPath(self, data):
207        return data.filename.encode('utf-8').lstrip('/').split('/')
208
209    def getData(self, full=False, single=None):
210        if self.db is None:
211            self.db = MythVideo()
212        if full:
213            newlist = []
214            vids = self.db.searchVideos()
215            newlist = [vid.intid for vid in vids]
216            files = self.walkSG('Videos')
217            for vid in vids:
218                if '/'+vid.filename in files:
219                    vid.filesize = files['/'+vid.filename]
220                else:
221                    vid.filesize = 0
222            return zip(vids, newlist)
223        elif single:
224            return Video(id=single, db=self.db)
225        else:
226            c = self.db.cursor()
227            c.execute("""SELECT intid FROM videometadata""")
228            newlist = [id[0] for id in c.fetchall()]
229            c.close()
230            return newlist
231
232    def walkSG(self, group, myth=None, base=None, path=None):
233        fdict = {}
234        if myth is None:
235            # walk through backends
236            c = self.db.cursor()
237            c.execute("""SELECT DISTINCT hostname
238                                FROM storagegroup
239                                WHERE groupname=%s""", group)
240            for host in c.fetchall():
241                fdict.update(self.walkSG(group, MythBE(host[0], db=self.db)))
242            c.close()
243            return fdict
244
245        if base is None:
246            # walk through base directories
247            for base in myth.getSGList(myth.hostname, group, ''):
248                fdict.update(self.walkSG(group, myth, base, ''))
249            return fdict
250
251        dirs, files, sizes = myth.getSGList(myth.hostname, group, base+'/'+path)
252        for d in dirs:
253            fdict.update(self.walkSG(group, myth, base, path+'/'+d))
254        for f, s in zip(files, sizes):
255            fdict[path+'/'+f] = int(s)
256        return fdict
257
258class RecFile( File ):
259    def update(self):
260        if (time() - self.time) < 5:
261            self.time = time()
262            self.data._pull()
263            self.fillAttr()
264
265    def fillAttr(self):
266        ctime = mktime(self.data.lastmodified.timetuple())
267        self.attr.st_ctime = ctime
268        self.attr.st_mtime = ctime
269        self.attr.st_atime = ctime
270        self.attr.st_size = self.data.filesize
271        #LOG.log(LOG.IMPORTANT, 'Set ATTR %s' % self.path, str(self.attr.__dict__))
272
273    def getPath(self, data):
274        return data.formatPath(self.fmt, '-').encode('utf-8').\
275                        lstrip('/').split('/')
276
277    def getData(self, full=False, single=None):
278        def _processrec(rec):
279            for field in ('title','subtitle'):
280                if (rec[field] == '') or (rec[field] == None):
281                    rec[field] = 'Untitled'
282            if rec['originalairdate'] is None:
283                rec['originalairdate'] = date(1900,1,1)
284
285        if self.db is None:
286            self.db = MythDB()
287        if full:
288            recs = self.db.searchRecorded()
289            newlist = []
290            for rec in recs:
291                _processrec(rec)
292                newlist.append((rec.chanid, rec.starttime))
293            return zip(recs, newlist)
294        elif single:
295            rec = Recorded(data=single, db=self.db)
296            _processrec(rec)
297            return rec
298        else:
299            c = self.db.cursor()
300            c.execute("""SELECT chanid,starttime FROM recorded
301                            WHERE recgroup!='LiveTV'""")
302            newlist = list(c.fetchall())
303            c.close()
304            return newlist
305
306class URIFile( File ):
307    def fillAttr(self):
308        fp = self.data.open('r')
309        fp.seek(0,2)
310        self.attr.st_size = fp.tell()
311        fp.close()
312        ctime = time()
313        self.attr.st_ctime = ctime
314        self.attr.st_mtime = ctime
315        self.attr.st_atime = ctime
316
317    def getPath(self, data):
318        return ['file.%s' % data.uri.split('.')[-1]]
319
320    def getData(self, full=False, single=None):
321        if full:
322            return [(URI(self.uri), 0)]
323        elif single:
324            return URI(self.uri)
325        else:
326            return [0,]
327
328class MythFS( Fuse ):
329    def __init__(self, *args, **kw):
330        Fuse.__init__(self, *args, **kw)
331        self.open_files = {}
332
333    def prep(self):
334        fmt = self.parser.largs[0].split(',',1)
335        if fmt[0] == 'Videos':
336            self.files = VideoFile()
337            self.files.populate()
338        elif fmt[0] == 'Recordings':
339            self.files = RecFile()
340            self.files.fmt = fmt[1]
341            self.files.populate()
342        elif fmt[0] == 'Single':
343            self.files = URIFile()
344            self.files.uri = fmt[1]
345            self.files.populate()
346        #print URI(fmt[1]).open('r')
347        #self.open('/file.flv',os.O_RDONLY)
348
349    def getattr(self, path):
350        #LOG.log(LOG.IMPORTANT, 'Attempting Reading ATTR %s' %path)
351        rec = self.files.getAttrStr(path)
352        #LOG.log(LOG.IMPORTANT, 'Reading ATTR %s' %path, str(rec.__dict__))
353        if rec is None:
354            return -errno.ENOENT
355        else:
356            return rec
357
358    def readdir(self, path, offset):
359        d = self.files.getObjStr(path)
360        if d is None:
361            return -errno.ENOENT
362        #LOG.log(LOG.IMPORTANT, 'Reading from %s' %path, str(d.children.keys()))
363        return tuple([fuse.Direntry(e) for e in d.children.keys()])
364
365    def open(self, path, flags):
366        accmode = os.O_RDONLY | os.O_WRONLY | os.O_RDWR
367        if (flags & accmode) != os.O_RDONLY:
368            return -errno.EACCES
369
370        if path not in self.open_files:
371            f = self.files.getObjStr(path)
372            if f is None:
373                return -errno.ENOENT
374            if f.data is None:
375                return -errno.ENOENT
376            self.open_files[path] = [1, f.data.open()]
377        else:
378            self.open_files[path][0] += 1
379
380    def read(self, path, length, offset, fh=None):
381        if path not in self.open_files:
382            return -errno.ENOENT
383        if self.open_files[path][1].tell() != offset:
384            self.open_files[path][1].seek(offset)
385        return self.open_files[path][1].read(length)
386
387    def release(self, path, fh=None):
388        if path in self.open_files:
389            if self.open_files[path][0] == 1:
390                self.open_files[path].close()
391                del self.open_files[path]
392            else:
393                self.open_files[path][0] += -1
394        else:
395            return -errno.ENOENT
396
397def main():
398    fs = MythFS(version='MythFS 0.23.0', usage='', dash_s_do='setsingle')
399    fs.parse(errex=1)
400    fs.flags = 0
401    fs.multithreaded = False
402    fs.prep()   
403#    print fs.files.children
404#    print fs.readdir('/', 0)
405#    print fs.files.attr.__dict__
406#    print fs.files.getAttrStr('/').__dict__
407#    sys.exit()
408    fs.main()
409
410if __name__ == '__main__':
411    main()
412