| 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 |
|
|---|