A Python UPnP Media Server
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

319 lines
7.4 KiB

  1. #!/usr/bin/env python
  2. # Copyright 2006-2008 John-Mark Gurney <jmg@funkthat.com>
  3. __version__ = '$Change$'
  4. # $Id$
  5. import itertools
  6. import os.path
  7. import sets
  8. import time
  9. import iterzipfile
  10. zipfile = iterzipfile
  11. import itertarfile
  12. tarfile = itertarfile
  13. try:
  14. import iterrarfile
  15. rarfile = iterrarfile
  16. except ImportError:
  17. class rarfile:
  18. pass
  19. rarfile = rarfile()
  20. rarfile.is_rarfile = lambda x: False
  21. import FileDIDL
  22. from DIDLLite import StorageFolder, Item, VideoItem, AudioItem, TextItem, ImageItem, Resource
  23. from FSStorage import FSObject, registerklassfun
  24. from twisted.python import log, threadable
  25. from twisted.spread import pb
  26. from twisted.web import http
  27. from twisted.web import server
  28. from twisted.web import resource
  29. def inserthierdict(d, name, obj):
  30. if not name:
  31. return
  32. i = name.find('/')
  33. if i == -1:
  34. d[name] = obj
  35. return
  36. dname = name[:i]
  37. rname = name[i + 1:]
  38. # remaining path components
  39. try:
  40. inserthierdict(d[dname], rname, obj)
  41. except KeyError:
  42. d[dname] = {}
  43. inserthierdict(d[dname], rname, obj)
  44. def buildNameHier(names, objs):
  45. ret = {}
  46. for n, o in itertools.izip(names, objs):
  47. inserthierdict(ret, n, o)
  48. return ret
  49. class ZipFileTransfer(pb.Viewable):
  50. def __init__(self, zf, name, request):
  51. self.zf = zf
  52. self.size = zf.getinfo(name).file_size
  53. self.iter = zf.readiter(name)
  54. self.request = request
  55. self.written = 0
  56. request.registerProducer(self, 0)
  57. def resumeProducing(self):
  58. if not self.request:
  59. return
  60. # get data and write to request.
  61. try:
  62. data = self.iter.next()
  63. if data:
  64. self.written += len(data)
  65. # this .write will spin the reactor, calling
  66. # .doWrite and then .resumeProducing again, so
  67. # be prepared for a re-entrant call
  68. self.request.write(data)
  69. except StopIteration:
  70. if self.request:
  71. self.request.unregisterProducer()
  72. self.request.finish()
  73. self.request = None
  74. def pauseProducing(self):
  75. pass
  76. def stopProducing(self):
  77. # close zipfile
  78. self.request = None
  79. # Remotely relay producer interface.
  80. def view_resumeProducing(self, issuer):
  81. self.resumeProducing()
  82. def view_pauseProducing(self, issuer):
  83. self.pauseProducing()
  84. def view_stopProducing(self, issuer):
  85. self.stopProducing()
  86. synchronized = ['resumeProducing', 'stopProducing']
  87. threadable.synchronize(ZipFileTransfer)
  88. class ZipResource(resource.Resource):
  89. # processors = {}
  90. isLeaf = True
  91. def __init__(self, zf, name, mt):
  92. resource.Resource.__init__(self)
  93. self.zf = zf
  94. self.zi = zf.getinfo(name)
  95. self.name = name
  96. self.mt = mt
  97. def getFileSize(self):
  98. return self.zi.file_size
  99. def render(self, request):
  100. request.setHeader('content-type', self.mt)
  101. # We could possibly send the deflate data directly!
  102. if None and self.encoding:
  103. request.setHeader('content-encoding', self.encoding)
  104. if request.setLastModified(time.mktime(list(self.zi.date_time) +
  105. [ 0, 0, -1])) is http.CACHED:
  106. return ''
  107. request.setHeader('content-length', str(self.getFileSize()))
  108. if request.method == 'HEAD':
  109. return ''
  110. # return data
  111. ZipFileTransfer(self.zf, self.name, request)
  112. # and make sure the connection doesn't get closed
  113. return server.NOT_DONE_YET
  114. class ZipItem:
  115. '''Basic zip stuff initalization'''
  116. def __init__(self, *args, **kwargs):
  117. self.zo = kwargs['zo']
  118. del kwargs['zo']
  119. self.zf = kwargs['zf']
  120. del kwargs['zf']
  121. self.name = kwargs['name']
  122. del kwargs['name']
  123. def checkUpdate(self):
  124. self.doUpdate()
  125. return self.zo.checkUpdate()
  126. class ZipFile(ZipItem, Item):
  127. def __init__(self, *args, **kwargs):
  128. self.mimetype = kwargs['mimetype']
  129. del kwargs['mimetype']
  130. ZipItem.__init__(self, *args, **kwargs)
  131. self.zi = self.zf.getinfo(self.name)
  132. kwargs['content'] = ZipResource(self.zf, self.name,
  133. self.mimetype)
  134. Item.__init__(self, *args, **kwargs)
  135. self.url = '%s/%s' % (self.cd.urlbase, self.id)
  136. self.res = Resource(self.url, 'http-get:*:%s:*' % self.mimetype)
  137. self.res.size = self.zi.file_size
  138. def doUpdate(self):
  139. pass
  140. class ZipChildDir(ZipItem, StorageFolder):
  141. '''This is to represent a child dir of the zip file.'''
  142. def __init__(self, *args, **kwargs):
  143. self.hier = kwargs['hier']
  144. del kwargs['hier']
  145. ZipItem.__init__(self, *args, **kwargs)
  146. del kwargs['zf'], kwargs['zo'], kwargs['name']
  147. StorageFolder.__init__(self, *args, **kwargs)
  148. # mapping from path to objectID
  149. self.pathObjmap = {}
  150. def doUpdate(self):
  151. doupdate = False
  152. children = sets.Set(self.hier.keys())
  153. for i in self.pathObjmap.keys():
  154. if i not in children:
  155. # delete
  156. doupdate = True
  157. self.cd.delItem(self.pathObjmap[i])
  158. del self.pathObjmap[i]
  159. for i in children:
  160. if i in self.pathObjmap:
  161. continue
  162. # new object
  163. pathname = os.path.join(self.name, i)
  164. if isinstance(self.hier[i], dict):
  165. # must be a dir
  166. self.pathObjmap[i] = self.cd.addItem(self.id,
  167. ZipChildDir, i, zf = self.zf, zo = self,
  168. name = pathname, hier = self.hier[i])
  169. else:
  170. klass, mt = FileDIDL.buildClassMT(ZipFile, i)
  171. if klass is None:
  172. continue
  173. self.pathObjmap[i] = self.cd.addItem(self.id,
  174. klass, i, zf = self.zf, zo = self,
  175. name = pathname, mimetype = mt)
  176. doupdate = True
  177. # sort our children
  178. self.sort(lambda x, y: cmp(x.title, y.title))
  179. if doupdate:
  180. StorageFolder.doUpdate(self)
  181. def __repr__(self):
  182. return '<ZipChildDir: len: %d>' % len(self.pathObjmap)
  183. def tryTar(path):
  184. # Try to see if it's a tar file
  185. if path[-2:] == 'gz':
  186. comp = tarfile.TAR_GZIPPED
  187. elif path[-3:] == 'bz2':
  188. comp = tarfile.TAR_BZ2
  189. else:
  190. comp = tarfile.TAR_PLAIN
  191. return tarfile.TarFileCompat(path, compression=comp)
  192. def canHandle(path):
  193. if zipfile.is_zipfile(path):
  194. return True
  195. if rarfile.is_rarfile(path):
  196. return True
  197. #tar is cheaper on __init__ than zipfile
  198. return tryTar(path)
  199. def genZipFile(path):
  200. if zipfile.is_zipfile(path):
  201. return zipfile.ZipFile(path)
  202. if rarfile.is_rarfile(path):
  203. return rarfile.RarFile(path)
  204. try:
  205. return tryTar(path)
  206. except:
  207. #import traceback
  208. #traceback.print_exc(file=log.logfile)
  209. raise
  210. class ZipObject(FSObject, StorageFolder):
  211. def __init__(self, *args, **kwargs):
  212. '''If a zip argument is passed it, use that as the zip archive.'''
  213. path = kwargs['path']
  214. del kwargs['path']
  215. StorageFolder.__init__(self, *args, **kwargs)
  216. FSObject.__init__(self, path)
  217. # mapping from path to objectID
  218. self.pathObjmap = {}
  219. def doUpdate(self):
  220. # open the zipfile as necessary.
  221. self.zip = genZipFile(self.FSpath)
  222. hier = buildNameHier(self.zip.namelist(), self.zip.infolist())
  223. doupdate = False
  224. children = sets.Set(hier.keys())
  225. for i in self.pathObjmap.keys():
  226. if i not in children:
  227. doupdate = True
  228. # delete
  229. self.cd.delItem(self.pathObjmap[i])
  230. del self.pathObjmap[i]
  231. for i in children:
  232. if i in self.pathObjmap:
  233. continue
  234. # new object
  235. if isinstance(hier[i], dict):
  236. # must be a dir
  237. self.pathObjmap[i] = self.cd.addItem(self.id,
  238. ZipChildDir, i, zf = self.zip, zo = self,
  239. name = i, hier = hier[i])
  240. else:
  241. klass, mt = FileDIDL.buildClassMT(ZipFile, i)
  242. if klass is None:
  243. continue
  244. self.pathObjmap[i] = self.cd.addItem(self.id,
  245. klass, i, zf = self.zip, zo = self,
  246. name = i, mimetype = mt)
  247. doupdate = True
  248. # sort our children
  249. self.sort(lambda x, y: cmp(x.title, y.title))
  250. if doupdate:
  251. StorageFolder.doUpdate(self)
  252. def detectzipfile(path, fobj):
  253. try:
  254. canHandle(path)
  255. except:
  256. return None, None
  257. return ZipObject, { 'path': path }
  258. registerklassfun(detectzipfile)