Ticket #289: fs_storage.py

File fs_storage.py, 39.1 kB (added by karatsu, 2 years ago)

Modified fs_storage

Line 
1 # -*- coding: utf-8 -*-
2
3 # Licensed under the MIT license
4 # http://opensource.org/licenses/mit-license.php
5
6 # Copyright 2006, Frank Scholz <coherence@beebits.net>
7
8 import os, stat
9 import tempfile
10 import shutil
11 import time
12 import re
13 from datetime import datetime
14 import urllib
15
16 from sets import Set
17
18 import mimetypes
19 mimetypes.init()
20 mimetypes.add_type('audio/x-m4a', '.m4a')
21 mimetypes.add_type('video/mp4', '.mp4')
22 mimetypes.add_type('video/mpegts', '.ts')
23 mimetypes.add_type('video/divx', '.divx')
24 mimetypes.add_type('video/divx', '.avi')
25 mimetypes.add_type('video/x-matroska', '.mkv')
26
27 from urlparse import urlsplit
28
29 from twisted.python.filepath import FilePath
30 from twisted.python import failure
31
32 from coherence.upnp.core.DIDLLite import classChooser, Container, Resource
33 from coherence.upnp.core.DIDLLite import DIDLElement
34 from coherence.upnp.core.DIDLLite import simple_dlna_tags
35 from coherence.upnp.core.soap_service import errorCode
36
37 from coherence.upnp.core import utils
38
39 try:
40     from coherence.extern.inotify import INotify
41     from coherence.extern.inotify import IN_CREATE, IN_DELETE, IN_MOVED_FROM, IN_MOVED_TO, IN_ISDIR
42     from coherence.extern.inotify import IN_CHANGED
43     haz_inotify = True
44 except Exception,msg:
45     haz_inotify = False
46     no_inotify_reason = msg
47
48 from coherence.extern.xdg import xdg_content
49
50 import coherence.extern.louie as louie
51
52 from coherence.backend import BackendItem, BackendStore
53
54 ## Sorting helpers
55 NUMS = re.compile('([0-9]+)')
56 def _natural_key(s):
57     # strip the spaces
58     s = s.get_name().strip()
59     return [ part.isdigit() and int(part) or part.lower() for part in NUMS.split(s) ]
60
61 class FSItem(BackendItem):
62     logCategory = 'fs_item'
63
64     def __init__(self, object_id, parent, path, mimetype, urlbase, UPnPClass,update=False,store=None):
65         self.id = object_id
66         self.parent = parent
67         if parent:
68             parent.add_child(self,update=update)
69         if mimetype == 'root':
70             self.location = unicode(path)
71         else:
72             if mimetype == 'item' and path is None:
73                 path = os.path.join(parent.get_realpath(),unicode(self.id))
74             #self.location = FilePath(unicode(path))
75             self.location = FilePath(path)
76         self.mimetype = mimetype
77         if urlbase[-1] != '/':
78             urlbase += '/'
79         self.url = urlbase + str(self.id)
80
81         self.store = store
82
83         if parent == None:
84             parent_id = -1
85         else:
86             parent_id = parent.get_id()
87
88         self.item = UPnPClass(object_id, parent_id, self.get_name())
89         if isinstance(self.item, Container):
90             self.item.childCount = 0
91         self.child_count = 0
92         self.children = []
93         self.sorted = False
94         self.caption = None
95
96
97         if mimetype in ['directory','root']:
98             self.update_id = 0
99             self.get_url = lambda : self.url
100             self.get_path = lambda : None
101             #self.item.searchable = True
102             #self.item.searchClass = 'object'
103             if(isinstance(self.location,FilePath) and
104                self.location.isdir() == True):
105                 self.check_for_cover_art()
106                 if hasattr(self, 'cover'):
107                     _,ext =  os.path.splitext(self.cover)
108                     """ add the cover image extension to help clients not reacting on
109                         the mimetype """
110                     self.item.albumArtURI = ''.join((urlbase,str(self.id),'?cover',ext))
111         else:
112             self.get_url = lambda : self.url
113
114             if self.mimetype.startswith('audio/'):
115                 if hasattr(parent, 'cover'):
116                     _,ext =  os.path.splitext(parent.cover)
117                     """ add the cover image extension to help clients not reacting on
118                         the mimetype """
119                     self.item.albumArtURI = ''.join((urlbase,str(self.id),'?cover',ext))
120
121             _,host_port,_,_,_ = urlsplit(urlbase)
122             if host_port.find(':') != -1:
123                 host,port = tuple(host_port.split(':'))
124             else:
125                 host = host_port
126
127             try:
128                 size = self.location.getsize()
129             except:
130                 size = 0
131
132             if self.store.server.coherence.config.get('transcoding', 'no') == 'yes':
133                 if self.mimetype in ('application/ogg','audio/ogg',
134                                      'audio/x-wav',
135                                      'audio/x-m4a',
136                                      'application/x-flac'):
137                     new_res = Resource(self.url+'/transcoded.mp3',
138                         'http-get:*:%s:*' % 'audio/mpeg')
139                     new_res.size = None
140                     #self.item.res.append(new_res)
141
142             if mimetype != 'item':
143                 res = Resource('file://'+ urllib.quote(self.get_path()), 'internal:%s:%s:*' % (host,self.mimetype))
144                 res.size = size
145                 self.item.res.append(res)
146
147             if mimetype != 'item':
148                 res = Resource(self.url, 'http-get:*:%s:*' % self.mimetype)
149             else:
150                 res = Resource(self.url, 'http-get:*:*:*')
151
152             res.size = size
153             self.item.res.append(res)
154
155             """ if this item is of type audio and we want to add a transcoding rule for it,
156                 this is the way to do it:
157
158                 create a new Resource object, at least a 'http-get'
159                 and maybe an 'internal' one too
160
161                 for transcoding to wav this looks like that
162
163                 res = Resource(url_for_transcoded audio,
164                         'http-get:*:audio/x-wav:%s'% ';'.join(['DLNA.ORG_PN=JPEG_TN']+simple_dlna_tags))
165                 res.size = None
166                 self.item.res.append(res)
167             """
168
169             if self.store.server.coherence.config.get('transcoding', 'no') == 'yes':
170                 if self.mimetype in ('audio/mpeg',
171                                      'application/ogg','audio/ogg',
172                                      'audio/x-wav',
173                                      'audio/x-m4a',
174                                      'audio/flac',
175                                      'application/x-flac'):
176                     dlna_pn = 'DLNA.ORG_PN=LPCM'
177                     dlna_tags = simple_dlna_tags[:]
178                     #dlna_tags[1] = 'DLNA.ORG_OP=00'
179                     dlna_tags[2] = 'DLNA.ORG_CI=1'
180                     new_res = Resource(self.url+'?transcoded=lpcm',
181                         'http-get:*:%s:%s' % ('audio/L16;rate=44100;channels=2', ';'.join([dlna_pn]+dlna_tags)))
182                     new_res.size = None
183                     #self.item.res.append(new_res)
184
185                     if self.mimetype  != 'audio/mpeg':
186                         new_res = Resource(self.url+'?transcoded=mp3',
187                             'http-get:*:%s:*' % 'audio/mpeg')
188                         new_res.size = None
189                         #self.item.res.append(new_res)
190
191             """ if this item is an image and we want to add a thumbnail for it
192                 we have to follow these rules:
193
194                 create a new Resource object, at least a 'http-get'
195                 and maybe an 'internal' one too
196
197                 for an JPG this looks like that
198
199                 res = Resource(url_for_thumbnail,
200                         'http-get:*:image/jpg:%s'% ';'.join(['DLNA.ORG_PN=JPEG_TN']+simple_dlna_tags))
201                 res.size = size_of_thumbnail
202                 self.item.res.append(res)
203
204                 and for a PNG the Resource creation is like that
205
206                 res = Resource(url_for_thumbnail,
207                         'http-get:*:image/png:%s'% ';'.join(simple_dlna_tags+['DLNA.ORG_PN=PNG_TN']))
208
209                 if not hasattr(self.item, 'attachments'):
210                     self.item.attachments = {}
211                 self.item.attachments[key] = utils.StaticFile(filename_of_thumbnail)
212             """
213
214             if self.mimetype in ('image/jpeg', 'image/png'):
215                 path = self.get_path()
216                 thumbnail = os.path.join(os.path.dirname(path),'.thumbs',os.path.basename(path))
217                 if os.path.exists(thumbnail):
218                     mimetype,_ = mimetypes.guess_type(thumbnail, strict=False)
219                     if mimetype in ('image/jpeg','image/png'):
220                         if mimetype == 'image/jpeg':
221                             dlna_pn = 'DLNA.ORG_PN=JPEG_TN'
222                         else:
223                             dlna_pn = 'DLNA.ORG_PN=PNG_TN'
224
225                         dlna_tags = simple_dlna_tags[:]
226                         dlna_tags[3] = 'DLNA.ORG_FLAGS=00f00000000000000000000000000000'
227
228                         hash_from_path = str(id(thumbnail))
229                         new_res = Resource(self.url+'?attachment='+hash_from_path,
230                             'http-get:*:%s:%s' % (mimetype, ';'.join([dlna_pn]+dlna_tags)))
231                         new_res.size = os.path.getsize(thumbnail)
232                         self.item.res.append(new_res)
233                         if not hasattr(self.item, 'attachments'):
234                             self.item.attachments = {}
235                         self.item.attachments[hash_from_path] = utils.StaticFile(thumbnail)
236
237             if self.mimetype.startswith('video/'):
238                 path = self.get_path()
239                 caption,_ =  os.path.splitext(path)
240                 caption = caption + '.srt'
241                 if os.path.exists(caption):
242                     hash_from_path = str(id(caption))
243                     mimetype = 'smi/caption'
244                     new_res = Resource(self.url+'?attachment='+hash_from_path,
245                         'http-get:*:%s:%s' % (mimetype, '*'))
246                     new_res.size = os.path.getsize(caption)
247                     self.caption = new_res.data
248                     self.item.res.append(new_res)
249                     if not hasattr(self.item, 'attachments'):
250                         self.item.attachments = {}
251                     self.item.attachments[hash_from_path] = utils.StaticFile(urllib.quote(caption))
252
253                 chemin = urllib.unquote(path)
254                 thumbnail = os.path.join(os.path.dirname(chemin),'.thumbs',os.path.basename(chemin)+'.jpg')
255                 if os.path.exists(thumbnail):
256                         dlna_pn = 'DLNA.ORG_PN=JPEG_TN'
257                         dlna_tags = simple_dlna_tags[:]
258                         dlna_tags[3] = 'DLNA.ORG_FLAGS=00f00000000000000000000000000000'
259
260                         hash_from_path = str(id(thumbnail))
261                         new_res = Resource(self.url+'?attachment='+hash_from_path,
262                             'http-get:*:%s:%s' % (mimetype, ';'.join([dlna_pn]+dlna_tags)))
263                         new_res.size = os.path.getsize(thumbnail)
264                         self.item.res.append(new_res)
265                         if not hasattr(self.item, 'attachments'):
266                             self.item.attachments = {}
267                         self.item.attachments[hash_from_path] = utils.StaticFile(thumbnail)
268
269                 thumbnail = os.path.join(os.path.dirname(chemin),'.thumbs',os.path.basename(chemin)+'.png')
270                 if os.path.exists(thumbnail):
271                         dlna_pn = 'DLNA.ORG_PN=PNG_TN'
272                         dlna_tags = simple_dlna_tags[:]
273                         dlna_tags[3] = 'DLNA.ORG_FLAGS=00f00000000000000000000000000000'
274
275                         hash_from_path = str(id(thumbnail))
276                         new_res = Resource(self.url+'?attachment='+hash_from_path,
277                             'http-get:*:%s:%s' % (mimetype, ';'.join([dlna_pn]+dlna_tags)))
278                         new_res.size = os.path.getsize(thumbnail)
279                         self.item.res.append(new_res)
280                         if not hasattr(self.item, 'attachments'):
281                             self.item.attachments = {}
282                         self.item.attachments[hash_from_path] = utils.StaticFile(thumbnail)
283
284             try:
285                 # FIXME: getmtime is deprecated in Twisted 2.6
286                 self.item.date = datetime.fromtimestamp(self.location.getmtime())
287             except:
288                 self.item.date = None
289
290     def rebuild(self, urlbase):
291         #print "rebuild", self.mimetype
292         if self.mimetype != 'item':
293             return
294         #print "rebuild for", self.get_path()
295         mimetype,_ = mimetypes.guess_type(self.get_path(),strict=False)
296         if mimetype == None:
297             return
298         self.mimetype = mimetype
299         #print "rebuild", self.mimetype
300         UPnPClass = classChooser(self.mimetype)
301         self.item = UPnPClass(self.id, self.parent.id, self.get_name())
302         if hasattr(self.parent, 'cover'):
303             _,ext =  os.path.splitext(self.parent.cover)
304             """ add the cover image extension to help clients not reacting on
305                 the mimetype """
306             self.item.albumArtURI = ''.join((urlbase,str(self.id),'?cover',ext))
307
308         _,host_port,_,_,_ = urlsplit(urlbase)
309         if host_port.find(':') != -1:
310             host,port = tuple(host_port.split(':'))
311         else:
312             host = host_port
313
314         res = Resource('file://'+urllib.quote(self.get_path()), 'internal:%s:%s:*' % (host,self.mimetype))
315         try:
316             res.size = self.location.getsize()
317         except:
318             res.size = 0
319         self.item.res.append(res)
320         res = Resource(self.url, 'http-get:*:%s:*' % self.mimetype)
321
322         try:
323             res.size = self.location.getsize()
324         except:
325             res.size = 0
326         self.item.res.append(res)
327
328         try:
329             # FIXME: getmtime is deprecated in Twisted 2.6
330             self.item.date = datetime.fromtimestamp(self.location.getmtime())
331         except:
332             self.item.date = None
333
334         self.parent.update_id += 1
335
336     def check_for_cover_art(self):
337         """ let's try to find in the current directory some jpg file,
338             or png if the jpg search fails, and take the first one
339             that comes around
340         """
341         try:
342             jpgs = [i.path for i in self.location.children() if i.splitext()[1] in ('.jpg', '.JPG')]
343             try:
344                 self.cover = jpgs[0]
345             except IndexError:
346                 pngs = [i.path for i in self.location.children() if i.splitext()[1] in ('.png', '.PNG')]
347                 try:
348                     self.cover = pngs[0]
349                 except IndexError:
350                     return
351         except UnicodeDecodeError:
352             self.warning("UnicodeDecodeError - there is something wrong with a file located in %r", self.location.path)
353
354     def remove(self):
355         #print "FSItem remove", self.id, self.get_name(), self.parent
356         if self.parent:
357             self.parent.remove_child(self)
358         del self.item
359
360     def add_child(self, child, update=False):
361         self.children.append(child)
362         self.child_count += 1
363         if isinstance(self.item, Container):
364             self.item.childCount += 1
365         if update == True:
366             self.update_id += 1
367         self.sorted = False
368
369     def remove_child(self, child):
370         #print "remove_from %d (%s) child %d (%s)" % (self.id, self.get_name(), child.id, child.get_name())
371         if child in self.children:
372             self.child_count -= 1
373             if isinstance(self.item, Container):
374                 self.item.childCount -= 1
375             self.children.remove(child)
376             self.update_id += 1
377         self.sorted = False
378
379     def get_children(self,start=0,request_count=0):
380         if self.sorted == False:
381             self.children.sort(key=_natural_key)
382             self.sorted = True
383         if request_count == 0:
384             return self.children[start:]
385         else:
386             return self.children[start:request_count]
387
388     def get_child_count(self):
389         return self.child_count
390
391     def get_id(self):
392         return self.id
393
394     def get_update_id(self):
395         if hasattr(self, 'update_id'):
396             return self.update_id
397         else:
398             return None
399
400     def get_path(self):
401         if isinstance( self.location,FilePath):
402             return self.location.path
403         else:
404             self.location
405
406     def get_realpath(self):
407         if isinstance( self.location,FilePath):
408             return self.location.path
409         else:
410             self.location
411
412     def set_path(self,path=None,extension=None):
413         if path is None:
414             path = self.get_path()
415         if extension is not None:
416             path,old_ext = os.path.splitext(path)
417             path = ''.join((path,extension))
418         if isinstance( self.location,FilePath):
419             self.location = FilePath(path)
420         else:
421             self.location = path
422
423     def get_name(self):
424         if isinstance(self.location,FilePath):
425             name = self.location.basename().decode("utf-8", "replace")
426         else:
427             name = self.location.decode("utf-8", "replace")
428         return name
429
430     def get_cover(self):
431         try:
432             return self.cover
433         except:
434             try:
435                 return self.parent.cover
436             except:
437                 return ''
438
439     def get_parent(self):
440         return self.parent
441
442     def get_item(self):
443         return self.item
444
445     def get_xml(self):
446         return self.item.toString()
447
448     def __repr__(self):
449         return 'id: ' + str(self.id) + ' @ ' + self.get_name().encode('ascii','xmlcharrefreplace')
450
451 class FSStore(BackendStore):
452     logCategory = 'fs_store'
453
454     implements = ['MediaServer']
455
456     description = """MediaServer exporting files from the file-system"""
457
458     options = [{'option':'name','type':'string','default':'my media','help': 'the name under this MediaServer shall show up with on other UPnP clients'},
459                {'option':'version','type':'int','default':2,'enum': (2,1),'help': 'the highest UPnP version this MediaServer shall support','level':'advance'},
460                {'option':'uuid','type':'string','help':'the unique (UPnP) identifier for this MediaServer, usually automatically set','level':'advance'},
461                {'option':'content','type':'string','default':xdg_content(),'help':'the path(s) this MediaServer shall export'},
462                {'option':'ignore_patterns','type':'string','help':'list of regex patterns, matching filenames will be ignored'},
463                {'option':'enable_destroy','type':'string','default':'no','help':'enable deleting a file via an UPnP method'},
464                {'option':'import_folder','type':'string','help':'The path to store files imported via an UPnP method, if empty the Import method is disabled'}
465               ]
466
467
468     def __init__(self, server, **kwargs):
469         BackendStore.__init__(self,server,**kwargs)
470         self.next_id = 1000
471         self.name = kwargs.get('name','my media')
472         self.content = kwargs.get('content',None)
473         if self.content != None:
474                 if isinstance(self.content,basestring):
475                     self.content = [self.content]
476                 l = []
477                 for a in self.content:
478                     l += a.split(',')
479                 self.content = l
480         else:
481             self.content = xdg_content()
482             self.content = [x[0] for x in self.content]
483         if self.content == None:
484             self.content = 'tests/content'
485         if not isinstance( self.content, list):
486             self.content = [self.content]
487         self.content = Set([os.path.abspath(x) for x in self.content])
488         ignore_patterns = kwargs.get('ignore_patterns',[])
489         self.store = {}
490
491         self.inotify = None
492
493         if haz_inotify == True:
494             try:
495                 self.inotify = INotify()
496             except Exception,msg:
497                 self.info("%s" %msg)
498         else:
499             self.info("%s" %no_inotify_reason)
500
501         if kwargs.get('enable_destroy','no') == 'yes':
502             self.upnp_DestroyObject = self.hidden_upnp_DestroyObject
503
504         self.import_folder = kwargs.get('import_folder',None)
505         if self.import_folder != None:
506             self.import_folder = os.path.abspath(self.import_folder)
507             if not os.path.isdir(self.import_folder):
508                 self.import_folder = None
509
510         self.ignore_file_pattern = re.compile('|'.join(['^\..*'] + list(ignore_patterns)))
511         parent = None
512         self.update_id = 0
513         if(len(self.content)>1 or
514            utils.means_true(kwargs.get('create_root',False)) or
515            self.import_folder != None):
516             UPnPClass = classChooser('root')
517             id = str(self.getnextID())
518             parent = self.store[id] = FSItem( id, parent, 'media', 'root', self.urlbase, UPnPClass, update=True,store=self)
519
520         if self.import_folder != None:
521             id = str(self.getnextID())
522             self.store[id] = FSItem( id, parent, self.import_folder, 'directory', self.urlbase, UPnPClass, update=True,store=self)
523             self.import_folder_id = id
524
525         for path in self.content:
526             if isinstance(path,(list,tuple)):
527                 path = path[0]
528             if self.ignore_file_pattern.match(path):
529                 continue
530             try:
531                 self.walk(path, parent, self.ignore_file_pattern)
532             except Exception,msg:
533                 self.warning('on walk of %r: %r' % (path,msg))
534                 import traceback
535                 self.debug(traceback.format_exc())
536
537         self.wmc_mapping.update({'14': '0',
538                                  '15': '0',
539                                  '16': '0',
540                                  '17': '0'
541                                 })
542
543         louie.send('Coherence.UPnP.Backend.init_completed', None, backend=self)
544
545     def __repr__(self):
546         return str(self.__class__).split('.')[-1]
547
548     def release(self):
549         if self.inotify != None:
550             self.inotify.release()
551
552     def len(self):
553         return len(self.store)
554
555     def get_by_id(self,id):
556         #print "get_by_id", id, type(id)
557         # we have referenced ids here when we are in WMC mapping mode
558         if isinstance(id, basestring):
559             id = id.split('@',1)
560             id = id[0]
561         #try:
562         #    id = int(id)
563         #except ValueError:
564         #    id = 1000
565
566         if id == '0':
567             id = '1000'
568         #print "get_by_id 2", id
569         try:
570             r = self.store[id]
571         except:
572             r = None
573         #print "get_by_id 3", r
574         return r
575
576     def get_id_by_name(self, parent='0', name=''):
577         self.info('get_id_by_name %r (%r) %r' % (parent, type(parent), name))
578         try:
579             parent = self.store[parent]
580             self.debug("%r %d" % (parent,len(parent.children)))
581             for child in parent.children:
582                 #if not isinstance(name, unicode):
583                 #    name = name.decode("utf8")
584                 self.debug("%r %r %r" % (child.get_name(),child.get_realpath(), name == child.get_realpath()))
585                 if name == child.get_realpath():
586                     return child.id
587         except:
588             import traceback
589             self.info(traceback.format_exc())
590         self.debug('get_id_by_name not found')
591
592         return None
593
594     def get_url_by_name(self,parent='0',name=''):
595         self.info('get_url_by_name %r %r' % (parent, name))
596         id = self.get_id_by_name(parent,name)
597         #print 'get_url_by_name', id
598         if id == None:
599             return ''
600         return self.store[id].url
601
602
603     def update_config(self,**kwargs):
604         print "update_config", kwargs
605         if 'content' in kwargs:
606             new_content = kwargs['content']
607             new_content = Set([os.path.abspath(x) for x in new_content.split(',')])
608             new_folders = new_content.difference(self.content)
609             obsolete_folders = self.content.difference(new_content)
610             print new_folders, obsolete_folders
611             for folder in obsolete_folders:
612                 self.remove_content_folder(folder)
613             for folder in new_folders:
614                 self.add_content_folder(folder)
615             self.content = new_content
616
617     def add_content_folder(self,path):
618         path = os.path.abspath(path)
619         if path not in self.content:
620             self.content.add(path)
621             self.walk(path, self.store['1000'], self.ignore_file_pattern)
622
623     def remove_content_folder(self,path):
624         path = os.path.abspath(path)
625         if path in self.content:
626             id = self.get_id_by_name('1000', path)
627             self.remove(id)
628             self.content.remove(path)
629
630     def walk(self, path, parent=None, ignore_file_pattern=''):
631         self.debug("walk %r" % path)
632         containers = []
633         parent = self.append(path,parent)
634         if parent != None:
635             containers.append(parent)
636         while len(containers)>0:
637             container = containers.pop()
638             try:
639                 self.debug('adding %r' % container.location)
640                 for child in container.location.children():
641                     if ignore_file_pattern.match(child.basename()) != None:
642                         continue
643                     new_container = self.append(child.path,container)
644                     if new_container != None:
645                         containers.append(new_container)
646             except UnicodeDecodeError:
647                 self.warning("UnicodeDecodeError - there is something wrong with a file located in %r", container.get_path())
648
649     def create(self, mimetype, path, parent):
650         self.debug("create ", mimetype, path, type(path), parent)
651         UPnPClass = classChooser(mimetype)
652         if UPnPClass == None:
653             return None
654
655         id = self.getnextID()
656         if mimetype in ('root','directory'):
657             id = str(id)
658         else:
659             _,ext =  os.path.splitext(path)
660             id = str(id) + ext.lower()
661         update = False
662         if hasattr(self, 'update_id'):
663             update = True
664
665         self.store[id] = FSItem( id, parent, path, mimetype, self.urlbase, UPnPClass, update=True,store=self)
666         if hasattr(self, 'update_id'):
667             self.update_id += 1
668             #print self.update_id
669             if self.server:
670                 if hasattr(self.server,'content_directory_server'):
671                     self.server.content_directory_server.set_variable(0, 'SystemUpdateID', self.update_id)
672             if parent is not None:
673                 value = (parent.get_id(),parent.get_update_id())
674                 if self.server:
675                     if hasattr(self.server,'content_directory_server'):
676                         self.server.content_directory_server.set_variable(0, 'ContainerUpdateIDs', value)
677
678         return id
679
680     def append(self,path,parent):
681         self.debug("append ", path, type(path), parent)
682         if os.path.exists(path) == False:
683             self.warning("path %r not available - ignored", path)
684             return None
685
686         if stat.S_ISFIFO(os.stat(path).st_mode):
687             self.warning("path %r is a FIFO - ignored", path)
688             return None
689
690         try:
691             mimetype,_ = mimetypes.guess_type(path, strict=False)
692             if mimetype == None:
693                 if os.path.isdir(path):
694                     mimetype = 'directory'
695             if mimetype == None:
696                 return None
697
698             id = self.create(mimetype,path,parent)
699
700             if mimetype == 'directory':
701                 if self.inotify is not None:
702                     mask = IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO | IN_CHANGED
703                     self.inotify.watch(path, mask=mask, auto_add=False, callbacks=(self.notify,id))
704                 return self.store[id]
705         except OSError, msg:
706             """ seems we have some permissions issues along the content path """
707             self.warning("path %r isn't accessible, error %r", path, msg)
708
709         return None
710
711     def remove(self, id):
712         print 'FSSTore remove id', id
713         try:
714             item = self.store[id]
715             parent = item.get_parent()
716             item.remove()
717             del self.store[id]
718             if hasattr(self, 'update_id'):
719                 self.update_id += 1
720                 if self.server:
721                     self.server.content_directory_server.set_variable(0, 'SystemUpdateID', self.update_id)
722                 #value = '%d,%d' % (parent.get_id(),parent_get_update_id())
723                 value = (parent.get_id(),parent.get_update_id())
724                 if self.server:
725                     self.server.content_directory_server.set_variable(0, 'ContainerUpdateIDs', value)
726
727         except:
728             pass
729
730
731     def notify(self, iwp, filename, mask, parameter=None):
732         self.info("Event %s on %s %s - parameter %r" % (
733                     ', '.join(self.inotify.flag_to_human(mask)), iwp.path, filename, parameter))
734
735         path = iwp.path
736         if filename:
737             path = os.path.join(path, filename)
738
739         if mask & IN_CHANGED:
740             # FIXME react maybe on access right changes, loss of read rights?
741             #print '%s was changed, parent %d (%s)' % (path, parameter, iwp.path)
742             pass
743
744         if(mask & IN_DELETE or mask & IN_MOVED_FROM):
745             self.info('%s was deleted, parent %r (%s)' % (path, parameter, iwp.path))
746             id = self.get_id_by_name(parameter,os.path.join(iwp.path,filename))
747             if id != None:
748                 self.remove(id)
749         if(mask & IN_CREATE or mask & IN_MOVED_TO):
750             if mask & IN_ISDIR:
751                 self.info('directory %s was created, parent %r (%s)' % (path, parameter, iwp.path))
752             else:
753                 self.info('file %s was created, parent %r (%s)' % (path, parameter, iwp.path))
754             if self.get_id_by_name(parameter,os.path.join(iwp.path,filename)) is None:
755                 if os.path.isdir(path):
756                     self.walk(path, self.get_by_id(parameter), self.ignore_file_pattern)
757                 else:
758                     if self.ignore_file_pattern.match(filename) == None:
759                         self.append(path, self.get_by_id(parameter))
760
761     def getnextID(self):
762         ret = self.next_id
763         self.next_id += 1
764         return ret
765
766     def backend_import(self,item,data):
767         try:
768             f = open(item.get_path(), 'w+b')
769             if hasattr(data,'read'):
770                 data = data.read()
771             f.write(data)
772             f.close()
773             item.rebuild(self.urlbase)
774             return 200
775         except IOError:
776             self.warning("import of file %s failed" % item.get_path())
777         except Exception,msg:
778             import traceback
779             self.warning(traceback.format_exc())
780         return 500
781
782     def upnp_init(self):
783         self.current_connection_id = None
784         if self.server:
785             self.server.connection_manager_server.set_variable(0, 'SourceProtocolInfo',
786                         [#'http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=01700000000000000000000000000000',
787                          #'http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=01700000000000000000000000000000',
788                          #'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000',
789                          #'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000',
790                          #'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000',
791                          #'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000',
792                          #'http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000',
793                          #'http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_BASE;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000',
794                          'internal:%s:audio/mpeg:*' % self.server.coherence.hostname,
795                          'http-get:*:audio/mpeg:*',
796                          'internal:%s:video/mp4:*' % self.server.coherence.hostname,
797                          'http-get:*:video/mp4:*',
798                          'internal:%s:application/ogg:*' % self.server.coherence.hostname,
799                          'http-get:*:application/ogg:*',
800                          'internal:%s:video/x-msvideo:*' % self.server.coherence.hostname,
801                          'http-get:*:video/x-msvideo:*',
802                          'internal:%s:video/mpeg:*' % self.server.coherence.hostname,
803                          'http-get:*:video/mpeg:*',
804                          'internal:%s:video/avi:*' % self.server.coherence.hostname,
805                          'http-get:*:video/avi:*',
806                          'internal:%s:video/divx:*' % self.server.coherence.hostname,
807                          'http-get:*:video/divx:*',
808                          'internal:%s:video/quicktime:*' % self.server.coherence.hostname,
809                          'http-get:*:video/quicktime:*',
810                          'internal:%s:image/gif:*' % self.server.coherence.hostname,
811                          'http-get:*:image/gif:*',
812                          'internal:%s:image/jpeg:*' % self.server.coherence.hostname,
813                          'http-get:*:image/jpeg:*'],
814                         default=True)
815             self.server.content_directory_server.set_variable(0, 'SystemUpdateID', self.update_id)
816             #self.server.content_directory_server.set_variable(0, 'SortCapabilities', '*')
817
818
819     def upnp_ImportResource(self, *args, **kwargs):
820         SourceURI = kwargs['SourceURI']
821         DestinationURI = kwargs['DestinationURI']
822
823         if DestinationURI.endswith('?import'):
824             id = DestinationURI.split('/')[-1]
825             id = id[:-7] # remove the ?import
826         else:
827             return failure.Failure(errorCode(718))
828
829         item = self.get_by_id(id)
830         if item == None:
831             return failure.Failure(errorCode(718))
832
833         def gotPage(headers):
834             #print "gotPage", headers
835             content_type = headers.get('content-type',[])
836             if not isinstance(content_type, list):
837                 content_type = list(content_type)
838             if len(content_type) > 0:
839                 extension = mimetypes.guess_extension(content_type[0], strict=False)
840                 item.set_path(None,extension)
841             shutil.move(tmp_path, item.get_path())
842             item.rebuild(self.urlbase)
843             if hasattr(self, 'update_id'):
844                 self.update_id += 1
845                 if self.server:
846                     if hasattr(self.server,'content_directory_server'):
847                         self.server.content_directory_server.set_variable(0, 'SystemUpdateID', self.update_id)
848                 if item.parent is not None:
849                     value = (item.parent.get_id(),item.parent.get_update_id())
850                     if self.server:
851                         if hasattr(self.server,'content_directory_server'):
852                             self.server.content_directory_server.set_variable(0, 'ContainerUpdateIDs', value)
853
854         def gotError(error, url):
855             self.warning("error requesting", url)
856             self.info(error)
857             os.unlink(tmp_path)
858             return failure.Failure(errorCode(718))
859
860         tmp_fp, tmp_path = tempfile.mkstemp()
861         os.close(tmp_fp)
862
863         utils.downloadPage(SourceURI,
864                            tmp_path).addCallbacks(gotPage, gotError, None, None, [SourceURI], None)
865
866         transfer_id = 0  #FIXME
867
868         return {'TransferID': transfer_id}
869
870     def upnp_CreateObject(self, *args, **kwargs):
871         #print "CreateObject", kwargs
872         if kwargs['ContainerID'] == 'DLNA.ORG_AnyContainer':
873             if self.import_folder != None:
874                 ContainerID = self.import_folder_id
875             else:
876                 return failure.Failure(errorCode(712))
877         else:
878             ContainerID = kwargs['ContainerID']
879         Elements = kwargs['Elements']
880
881         parent_item = self.get_by_id(ContainerID)
882         if parent_item == None:
883             return failure.Failure(errorCode(710))
884         if parent_item.item.restricted:
885             return failure.Failure(errorCode(713))
886
887         if len(Elements) == 0:
888             return failure.Failure(errorCode(712))
889
890         elt = DIDLElement.fromString(Elements)
891         if elt.numItems() != 1:
892             return failure.Failure(errorCode(712))
893
894         item = elt.getItems()[0]
895         if item.parentID == 'DLNA.ORG_AnyContainer':
896             item.parentID = ContainerID
897         if(item.id != '' or
898            item.parentID != ContainerID or
899            item.restricted == True or
900            item.title == ''):
901             return failure.Failure(errorCode(712))
902
903         if('..' in item.title or
904            '~' in item.title or
905            os.sep in item.title):
906             return failure.Failure(errorCode(712))
907
908         if item.upnp_class == 'object.container.storageFolder':
909             if len(item.res) != 0:
910                 return failure.Failure(errorCode(712))
911             path = os.path.join(parent_item.get_path(),item.title)
912             id = self.create('directory',path,parent_item)
913             try:
914                 os.mkdir(path)
915             except:
916                 self.remove(id)
917                 return failure.Failure(errorCode(712))
918
919             if self.inotify is not None:
920                 mask = IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO | IN_CHANGED
921                 self.inotify.watch(path, mask=mask, auto_add=False, callbacks=(self.notify,id))
922
923             new_item = self.get_by_id(id)
924             didl = DIDLElement()
925             didl.addItem(new_item.item)
926             return {'ObjectID': id, 'Result': didl.toString()}
927
928         if item.upnp_class.startswith('object.item'):
929             _,_,content_format,_ = item.res[0].protocolInfo.split(':')
930             extension = mimetypes.guess_extension(content_format, strict=False)
931             path = os.path.join(parent_item.get_realpath(),item.title+extension)
932             id = self.create('item',path,parent_item)
933
934             new_item = self.get_by_id(id)
935             for res in new_item.item.res:
936                 res.importUri = new_item.url+'?import'
937                 res.data = None
938             didl = DIDLElement()
939             didl.addItem(new_item.item)
940             return {'ObjectID': id, 'Result': didl.toString()}
941
942         return failure.Failure(errorCode(712))
943
944     def hidden_upnp_DestroyObject(self, *args, **kwargs):
945         ObjectID = kwargs['ObjectID']
946
947         item = self.get_by_id(ObjectID)
948         if item == None:
949             return failure.Failure(errorCode(701))
950
951         print "upnp_DestroyObject", item.location
952         try:
953             item.location.remove()
954         except Exception, msg:
955             print Exception, msg
956             return failure.Failure(errorCode(715))
957
958         return {}
959
960
961 if __name__ == '__main__':
962
963     from twisted.internet import reactor
964
965     p = 'tests/content'
966     f = FSStore(None,name='my media',content=p, urlbase='http://localhost/xyz')
967
968     print f.len()
969     print f.get_by_id(1000).child_count, f.get_by_id(1000).get_xml()
970     print f.get_by_id(1001).child_count, f.get_by_id(1001).get_xml()
971     print f.get_by_id(1002).child_count, f.get_by_id(1002).get_xml()
972     print f.get_by_id(1003).child_count, f.get_by_id(1003).get_xml()
973     print f.get_by_id(1004).child_count, f.get_by_id(1004).get_xml()
974     print f.get_by_id(1005).child_count, f.get_by_id(1005).get_xml()
975     print f.store[1000].get_children(0,0)
976     #print f.upnp_Search(ContainerID ='4',
977     #                    Filter ='dc:title,upnp:artist',
978     #                    RequestedCount = '1000',
979     #                    StartingIndex = '0',
980     #                    SearchCriteria = '(upnp:class = "object.container.album.musicAlbum")',
981     #                    SortCriteria = '+dc:title')
982
983     f.upnp_ImportResource(SourceURI='http://spiegel.de',DestinationURI='ttt')
984
985     reactor.run()