Ticket #7858: mythfs.py

File mythfs.py, 13.3 KB (added by Raymond Wagner <raymond@…>, 16 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