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.

216 lines
5.0 KiB

  1. #!/usr/bin/env python
  2. # Copyright 2006 John-Mark Gurney <gurney_j@resnet.uroegon.edu>
  3. #
  4. # $Id$
  5. #
  6. import itertools
  7. import os.path
  8. import sets
  9. import time
  10. import iterzipfile
  11. zipfile = iterzipfile
  12. import FileDIDL
  13. from DIDLLite import StorageFolder, Item, VideoItem, AudioItem, TextItem, ImageItem, Resource
  14. from FSStorage import FSObject, registerklassfun
  15. from twisted.python import threadable, log
  16. from twisted.spread import pb
  17. from twisted.web import http
  18. from twisted.web import server
  19. from twisted.web import resource
  20. def inserthierdict(d, name, obj):
  21. if not name:
  22. return
  23. i = name.find('/')
  24. if i == -1:
  25. d[name] = obj
  26. return
  27. dname = name[:i]
  28. rname = name[i + 1:]
  29. # remaining path components
  30. try:
  31. inserthierdict(d[dname], rname, obj)
  32. except KeyError:
  33. d[dname] = {}
  34. inserthierdict(d[dname], rname, obj)
  35. def buildNameHier(names, objs):
  36. ret = {}
  37. for n, o in itertools.izip(names, objs):
  38. inserthierdict(ret, n, o)
  39. return ret
  40. class ZipFileTransfer(pb.Viewable):
  41. def __init__(self, zf, name, request):
  42. self.zf = zf
  43. self.size = zf.getinfo(name).file_size
  44. self.iter = zf.readiter(name)
  45. self.request = request
  46. self.written = 0
  47. request.registerProducer(self, 0)
  48. def resumeProducing(self):
  49. if not self.request:
  50. return
  51. # get data and write to request.
  52. try:
  53. data = self.iter.next()
  54. if data:
  55. self.written += len(data)
  56. # this .write will spin the reactor, calling
  57. # .doWrite and then .resumeProducing again, so
  58. # be prepared for a re-entrant call
  59. self.request.write(data)
  60. except StopIteration:
  61. if self.request:
  62. self.request.unregisterProducer()
  63. self.request.finish()
  64. self.request = None
  65. def pauseProducing(self):
  66. pass
  67. def stopProducing(self):
  68. # close zipfile
  69. self.request = None
  70. # Remotely relay producer interface.
  71. def view_resumeProducing(self, issuer):
  72. self.resumeProducing()
  73. def view_pauseProducing(self, issuer):
  74. self.pauseProducing()
  75. def view_stopProducing(self, issuer):
  76. self.stopProducing()
  77. synchronized = ['resumeProducing', 'stopProducing']
  78. threadable.synchronize(ZipFileTransfer)
  79. class ZipResource(resource.Resource):
  80. # processors = {}
  81. isLeaf = True
  82. def __init__(self, zf, name, mt):
  83. resource.Resource.__init__(self)
  84. self.zf = zf
  85. self.zi = zf.getinfo(name)
  86. self.name = name
  87. self.mt = mt
  88. def getFileSize(self):
  89. return self.zi.file_size
  90. def render(self, request):
  91. request.setHeader('content-type', self.mt)
  92. # We could possibly send the deflate data directly!
  93. if None and self.encoding:
  94. request.setHeader('content-encoding', self.encoding)
  95. if request.setLastModified(time.mktime(list(self.zi.date_time) +
  96. [ 0, 0, -1])) is http.CACHED:
  97. return ''
  98. request.setHeader('content-length', str(self.getFileSize()))
  99. if request.method == 'HEAD':
  100. return ''
  101. # return data
  102. ZipFileTransfer(self.zf, self.name, request)
  103. # and make sure the connection doesn't get closed
  104. return server.NOT_DONE_YET
  105. class ZipItem(Item):
  106. '''An item in the zip file'''
  107. def __init__(self, *args, **kwargs):
  108. self.zo = kwargs['zo']
  109. del kwargs['zo']
  110. self.zf = kwargs['zf']
  111. del kwargs['zf']
  112. self.name = kwargs['name']
  113. del kwargs['name']
  114. self.zi = self.zf.getinfo(self.name)
  115. self.mimetype = kwargs['mimetype']
  116. del kwargs['mimetype']
  117. kwargs['content'] = ZipResource(self.zf, self.name,
  118. self.mimetype)
  119. Item.__init__(self, *args, **kwargs)
  120. self.url = '%s/%s' % (self.cd.urlbase, self.id)
  121. def checkUpdate(self):
  122. self.doUpdate()
  123. return self.zo.checkUpdate()
  124. def doUpdate(self):
  125. self.res = Resource(self.url, 'http-get:*:%s:*' % self.mimetype)
  126. self.res.size = self.zi.file_size
  127. Item.doUpdate(self)
  128. class ZipChildDir(ZipItem, StorageFolder):
  129. '''This is to represent a child dir of the zip file.'''
  130. def __init__(self):
  131. raise NotImplementedError
  132. class ZipObject(FSObject, StorageFolder):
  133. def __init__(self, *args, **kwargs):
  134. '''If a zip argument is passed it, use that as the zip archive.'''
  135. path = kwargs['path']
  136. del kwargs['path']
  137. StorageFolder.__init__(self, *args, **kwargs)
  138. FSObject.__init__(self, path)
  139. # mapping from path to objectID
  140. self.pathObjmap = {}
  141. def doUpdate(self):
  142. # open the zipfile as necessary.
  143. self.zip = zipfile.ZipFile(self.FSpath)
  144. hier = buildNameHier(self.zip.namelist(), self.zip.infolist())
  145. children = sets.Set(hier.keys())
  146. for i in self.pathObjmap.keys():
  147. if i not in children:
  148. # delete
  149. self.cd.delItem(self.pathObjmap[i])
  150. del self.pathObjmap[i]
  151. for i in children:
  152. if i in self.pathObjmap:
  153. continue
  154. # new object
  155. if isinstance(hier[i], dict):
  156. # must be a dir
  157. self.pathObjmap[i] = self.cd.addItem(self.id,
  158. ZipChildDir, i, self, i)
  159. else:
  160. klass, mt = FileDIDL.buildClassMT(ZipItem, i)
  161. self.pathObjmap[i] = self.cd.addItem(self.id,
  162. klass, i, zf = self.zip, zo = self,
  163. name = i, mimetype = mt)
  164. # sort our children
  165. self.sort(lambda x, y: cmp(x.title, y.title))
  166. def detectzipfile(path, fobj):
  167. try:
  168. z = zipfile.ZipFile(fobj)
  169. except:
  170. return None, None
  171. return ZipObject, { 'path': path }
  172. registerklassfun(detectzipfile)