diff --git a/common.py b/common.py index fb6ed64..d8135de 100644 --- a/common.py +++ b/common.py @@ -20,10 +20,12 @@ if not config.has_section('AUTOBACKUP'): config.add_section('AUTOBACKUP') - config.set('AUTOBACKUP', 'uuid', uuid.uuid4()) + config.set('AUTOBACKUP', 'backup_dir', + os.path.expanduser('~/PCAutoBackup')) config.set('AUTOBACKUP', 'default_interface', socket.gethostbyname(socket.gethostname())) config.set('AUTOBACKUP', 'server_name', '[PC]AutoBackup') + config.set('AUTOBACKUP', 'uuid', uuid.uuid4()) with open(CONFIG_FILE, 'wb') as config_file: config.write(config_file) diff --git a/mediaserver.py b/mediaserver.py index 79d50d7..050e5ab 100644 --- a/mediaserver.py +++ b/mediaserver.py @@ -6,6 +6,10 @@ __author__ = 'jeff@rebeiro.net (Jeff Rebeiro)' import HTMLParser +import os +import random +import re +import string from twisted.internet import reactor from twisted.web.resource import Resource @@ -13,10 +17,21 @@ import common -CREATE_OBJ = re.compile(r'.*(.*)', re.DOTALL) -CREATE_OBJ_DETAILS = re.compile(r'<dc:title>(.*)</dc:title>.*protocolInfo="\*:\*:(.*):DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_CI=0"') +CREATE_OBJ = '"urn:schemas-upnp-org:service:ContentDirectory:1#CreateObject"' +CREATE_OBJ_DIDL = re.compile(r'.*(?P.*dc:title>(?P.*)</dc:title.*dc:date>(?P.*)</dc:date.*protocolInfo="\*:\*:(?P.*):DLNA.ORG_PN.*size="(?P\d+)".*).*') +CREATE_OBJ_RESPONSE = ''' + + + + %(obj_id)s + <DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp='urn:schemas-upnp-org:metadata-1-0/upnp/' xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/" xmlns:sec="http://www.sec.co.kr/"><item id="%(obj_id)s" parentID="%(parent_id)s" restricted="0" dlna:dlnaManaged="00000004"><dc:title></dc:title><res protocolInfo="http-get:*:%(obj_type)s:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=00D00000000000000000000000000000" importUri="http://%(interface)s:52235/cd/content?didx=0_id=%(obj_id)s" dlna:resumeUpload="1" dlna:uploadedSize="0" size="%(obj_size)s"></res><upnp:class>object.item.imageItem</upnp:class></item></DIDL-Lite> + + +''' -X_BACKUP = re.compile(r'') +X_BACKUP_DONE = '"urn:schemas-upnp-org:service:ContentDirectory:1#X_BACKUP_DONE"' +X_BACKUP_START = '"urn:schemas-upnp-org:service:ContentDirectory:1#X_BACKUP_START"' + X_BACKUP_RESPONSE = ''' @@ -24,7 +39,7 @@ ''' -DMS_DESC = ''' +DMS_DESC_RESPONSE = ''' 1 @@ -63,56 +78,154 @@ ''' -class MediaServer(Resource): +class Backup(object): - isLeaf = True + backup_objects = {} def __init__(self): self.config = common.LoadOrCreateConfig() + def _GenerateObjectID(self, obj_date, length=10): + chars = string.letters + string.digits + rand_chars = ''.join(random.choice(chars) for i in xrange(length)) + parent_id = 'UP_%s' % obj_date + obj_id = '%s_%s' % (parent_id, rand_chars) + return (parent_id, obj_id) + + def CreateObject(self, obj_name, obj_date, obj_type, obj_size): + (parent_id, obj_id) = self._GenerateObjectID(obj_date) + self.backup_objects[obj_id] = {'obj_name': obj_name, + 'obj_date': obj_date, + 'obj_type': obj_type, + 'parent_id': parent_id, + 'obj_size': obj_size} + return obj_id + + def FinishBackup(self): + pass + + def GetObjectDetails(self, obj_id): + return self.backup_objects.get(obj_id) + + def StartBackup(self): + pass + + def WriteObject(self, obj_id, data): + obj_details = self.GetObjectDetails(obj_id) + + obj_dir = os.path.join(self.config.get('AUTOBACKUP', 'backup_dir'), + obj_details['obj_date']) + + if not os.path.isdir(obj_dir): + os.makedirs(obj_dir) + + obj_file = os.path.join(obj_dir, obj_details['obj_name']) + + with open(obj_file, 'wb') as f: + f.write(data) + + del(self.backup_objects[obj_id]) + + +class MediaServer(Resource): + + isLeaf = True + + def __init__(self, debug=False): + self.config = common.LoadOrCreateConfig() + self.debug = debug + def render_GET(self, request): + if self.debug: + print 'Request headers:' + print request.getAllHeaders() if request.path == '/DMS/SamsungDmsDesc.xml': return self.GetDMSDescriptionResponse() + else: + print 'Unhandled GET request: %s' % request.path def render_POST(self, request): + if self.debug: + print 'Request args:' + print request.args + print 'Request headers:' + print request.getAllHeaders() + if request.path == '/cd/content': + return self.ReceiveUpload(request) if request.path == '/upnp/control/ContentDirectory1': - return self.GetContentDirectoryResponse(request.content.read()) + return self.GetContentDirectoryResponse(request) + else: + print 'Unhandled POST request: %s' % request.path - def GetContentDirectoryResponse(self, content): - if X_BACKUP.search(content): - action = X_BACKUP.search(content).group(1) - response = X_BACKUP_RESPONSE % action - print "Response:" + def GetContentDirectoryResponse(self, request): + soapaction = request.getHeader('soapaction') + + response = '' + + if soapaction == X_BACKUP_START: + response = X_BACKUP_RESPONSE % 'START' + if self.debug: + print 'Response:' + print response + if soapaction == CREATE_OBJ: + request.content.seek(0) + soap_xml = request.content.read() + + m = CREATE_OBJ_DIDL.match(soap_xml) + if m: + obj_name = m.group('name') + obj_date = m.group('date') + obj_type = m.group('type') + obj_size = m.group('size') + + backup = Backup() + obj_id = backup.CreateObject(obj_name, obj_date, obj_type, obj_size) + obj_details = backup.GetObjectDetails(obj_id) + + response = CREATE_OBJ_RESPONSE % { + 'interface': self.config.get('AUTOBACKUP', 'default_interface'), + 'obj_id': obj_id, + 'obj_type': obj_type, + 'obj_size': obj_size, + 'parent_id': obj_details['parent_id']} + if soapaction == X_BACKUP_DONE: + response = X_BACKUP_RESPONSE % 'DONE' + + if self.debug: + print 'Response:' print response - if CREATE_OBJECT.search(content): - obj_didl = CREATE_OBJ.search(content).group(1) - obj_details = CREATE_OBJ_DETAILS.search(obj_didl).groups() - obj_name = obj_details[0] - obj_type = obj_details[1] - return response def GetDMSDescriptionResponse(self): - response = DMS_DESC % {'friendly_name': self.config.get('AUTOBACKUP', - 'server_name'), - 'uuid': self.config.get('AUTOBACKUP', 'uuid')} - if __name__ == '__main__': - print "Response:" + response = DMS_DESC_RESPONSE % { + 'friendly_name': self.config.get('AUTOBACKUP', 'server_name'), + 'uuid': self.config.get('AUTOBACKUP', 'uuid')} + if self.debug: + print 'Response:' print response return response + def ReceiveUpload(self, request): + response = '' + obj_id = request.args['didx'][0].split('=')[1] + backup = Backup() -def StartMediaServer(): - resource = MediaServer() + data = request.content.read() + backup.WriteObject(obj_id, data) + return response + + +def StartMediaServer(debug=False): + resource = MediaServer(debug=debug) factory = Site(resource) reactor.listenTCP(52235, factory) reactor.run() def main(): - StartMediaServer() + StartMediaServer(debug=True) -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/pc-autobackup.py b/pc-autobackup.py new file mode 100644 index 0000000..b24e462 --- /dev/null +++ b/pc-autobackup.py @@ -0,0 +1,41 @@ +#!/usr/bin/python +# +# Copyright 2013 Jeff Rebeiro (jeff@rebeiro.net) All rights reserved +# Main runnable for PC Autobackup + +__author__ = 'jeff@rebeiro.net (Jeff Rebeiro)' + +import optparse + +from twisted.internet import reactor +from twisted.web.server import Site + +import common +import ssdp +import mediaserver + + +def main(): + parser = optparse.OptionParser() + parser.add_option('-b', '--bind', dest='bind', + help='Bind the server to a specific IP', + metavar='IPADDRESS') + parser.add_option('-d', '--debug', dest='debug', action="store_true", + default=False, help='Print debug information') + (options, args) = parser.parse_args() + + config = common.LoadOrCreateConfig() + if options.bind: + config.set('AUTOBACKUP', 'default_interface', options.bind) + + #ssdp.StartSSDPServer(debug=options.debug) + #mediaserver.StartMediaServer(debug=options.debug) + resource = mediaserver.MediaServer(debug=options.debug) + factory = Site(resource) + reactor.listenMulticast(1900, ssdp.SSDPServer(debug=options.debug)) + reactor.listenTCP(52235, factory) + reactor.run() + + +if __name__ == '__main__': + main() diff --git a/ssdp.py b/ssdp.py index 11cfd69..4a0bad1 100644 --- a/ssdp.py +++ b/ssdp.py @@ -27,8 +27,9 @@ class SSDPServer(DatagramProtocol): - def __init__(self): + def __init__(self, debug=False): self.config = common.LoadOrCreateConfig() + self.debug = debug def startProtocol(self): self.transport.setTTL(5) @@ -37,11 +38,10 @@ def datagramReceived(self, datagram, address): m = MSEARCH.match(datagram) if m: - # TODO(jrebeiro): Make this print in debug mode when the main runnable - # module is created and implements optparse # TODO(jrebeiro): Verify that MediaServer is the only discovery request # PCAutoBackup responds to. - print 'Received M-SEARCH for %s from %r' % (m.group(3), address) + if self.debug: + print 'Received M-SEARCH for %s from %r' % (m.group(3), address) if m.group(3) == 'MediaServer': self.SendSSDPResponse(address) @@ -55,18 +55,18 @@ 'default_interface'), self.config.get('AUTOBACKUP', 'uuid')) self.transport.write(response, address) - if __name__ == '__main__': + if self.debug: print "Response:" print response -def StartSSDPServer(): - reactor.listenMulticast(1900, SSDPServer()) +def StartSSDPServer(debug=False): + reactor.listenMulticast(1900, SSDPServer(debug=debug)) reactor.run() def main(): - StartSSDPServer() + StartSSDPServer(debug=True) if __name__ == "__main__":