1 | #!/usr/bin/env python |
---|
2 | |
---|
3 | try: |
---|
4 | import _find_fuse_parts |
---|
5 | except ImportError: |
---|
6 | pass |
---|
7 | import fuse, re, errno, stat, os, sys |
---|
8 | from fuse import Fuse |
---|
9 | from time import time, mktime |
---|
10 | from datetime import date |
---|
11 | from MythTV import MythDB, MythVideo, ftopen, MythBE, Video, Recorded, MythLog |
---|
12 | |
---|
13 | if not hasattr(fuse, '__version__'): |
---|
14 | raise RuntimeError, \ |
---|
15 | "your fuse-py doesn't know of fuse.__version__, probably it's too old." |
---|
16 | |
---|
17 | fuse.fuse_python_api = (0, 2) |
---|
18 | |
---|
19 | TIMEOUT = 30 |
---|
20 | #LOG = MythLog('MythFS',logfile='mythlog.log') |
---|
21 | |
---|
22 | class URI( object ): |
---|
23 | def __init__(self, uri): |
---|
24 | self.uri = uri |
---|
25 | def open(self, mode='r'): |
---|
26 | return ftopen(self.uri, mode) |
---|
27 | |
---|
28 | class 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 | |
---|
43 | class 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 | |
---|
195 | class 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 | |
---|
258 | class 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 | |
---|
306 | class 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 | |
---|
328 | class 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 | |
---|
397 | def 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 | |
---|
410 | if __name__ == '__main__': |
---|
411 | main() |
---|
412 | |
---|