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.

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