diff --git a/common.py b/common.py index a4a6ed0..33d0a35 100644 --- a/common.py +++ b/common.py @@ -36,13 +36,21 @@ ''' LOG_DATE_FMT = '[%m/%d/%Y %I:%M %p]' -LOG_FMT = '%(asctime)s[%(name)s] %(levelname)s:%(message)s' +LOG_FMT = '%(asctime)s %(message)s' LOG_DEFAULTS = {'level': logging.INFO, 'format': LOG_FMT, 'datefmt': LOG_DATE_FMT} def EscapeHTML(html): + """Escape characters in the given HTML. + + Args: + html: A string containing HTML to be escaped + + Returns: + A string containing escaped HTML + """ html_codes = (('&', '&'), ('<', '<'), ('>', '>'), @@ -54,18 +62,29 @@ return html def GenerateUUID(): + """Generate a UUID. + + Returns: + A string containing a valid UUID + """ uuid_prefix = '4a682b0b-0361-dbae-6155' uuid_suffix = str(uuid.uuid4()).split('-')[-1] return '-'.join([uuid_prefix, uuid_suffix]) def LoadOrCreateConfig(): - """Load an existing configuration or create one.""" + """Load an existing configuration or create one. + + Returns: + ConfigParser.RawConfigParser + """ + logger = logging.getLogger('pc_autobackup.common') + config = ConfigParser.RawConfigParser() config.read(CONFIG_FILE) if not config.has_section('AUTOBACKUP'): - logging.info('Creating configuration file %s', CONFIG_FILE) + logger.info('Creating configuration file %s', CONFIG_FILE) config.add_section('AUTOBACKUP') if not config.has_option('AUTOBACKUP', 'backup_dir'): config.set('AUTOBACKUP', 'backup_dir', @@ -77,7 +96,7 @@ config.set('AUTOBACKUP', 'default_interface', socket.gethostbyname(socket.gethostname())) except socket.error: - logging.error('Unable to determine IP address. Please set manually!') + logger.error('Unable to determine IP address. Please set manually!') config.set('AUTOBACKUP', 'default_interface', '127.0.0.1') if not config.has_option('AUTOBACKUP', 'server_name'): config.set('AUTOBACKUP', 'server_name', '[PC]AutoBackup') diff --git a/diagnose_me.sh b/diagnose_me.sh index 9f99ed4..9dddead 100755 --- a/diagnose_me.sh +++ b/diagnose_me.sh @@ -71,7 +71,7 @@ read -p "Press [Enter] key once AutoBackup fails/finishes..." echo "Shutting down pc_autobackup..." -pkill pc_autobackup.py +pkill -f pc_autobackup echo "Shutting down network capture..." pkill tcpdump diff --git a/mediaserver.py b/mediaserver.py index 8a608e7..cc321e3 100644 --- a/mediaserver.py +++ b/mediaserver.py @@ -14,7 +14,6 @@ import xml.dom.minidom from twisted.internet import reactor -from twisted.web.error import NoResource from twisted.web.resource import Resource from twisted.web.server import Site @@ -56,18 +55,40 @@ backup_objects = {} def __init__(self): - self.logger = logging.getLogger('MediaServer.Backup') + self.logger = logging.getLogger('pc_autobackup.mediaserver.backup') self.config = common.LoadOrCreateConfig() def _GenerateObjectID(self, obj_date, length=10): + """Generate an ObjectID for a new backup item. + + Args: + obj_date: A string containing the object date + length: An int containing the length of the object id + + Returns: + A tuple containing the parent id and the object id + """ 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_class, obj_name, obj_date, obj_type, obj_size, - obj_subtype): + def CreateObject(self, obj_class, obj_date, obj_name, obj_size, obj_subtype, + obj_type): + """Create a new object. + + Args: + obj_class: A string containing the objects upnp class + obj_date: A string containing the objects date + obj_name: A string containing the objects name + obj_size: A string containing the objects size + obj_subtype: A string containing the objects subtype + obj_type: A string containing the objects type + + Returns: + A string containing the created object id + """ (parent_id, obj_id) = self._GenerateObjectID(obj_date) self.logger.debug('Creating Backup Object for %s (type:%s size:%s)', obj_name, obj_type, obj_size) @@ -84,12 +105,26 @@ pass def GetObjectDetails(self, obj_id): + """Get details about an object. + + Args: + obj_id: A string containing the object id + + Returns: + A dict containing the object details or None if the object does not exist + """ return self.backup_objects.get(obj_id) def StartBackup(self): pass def WriteObject(self, obj_id, data): + """Save an object to disk. + + Args: + obj_id: A string containing the object to write + data: The data to write to disk + """ obj_details = self.GetObjectDetails(obj_id) obj_dir = [self.config.get('AUTOBACKUP', 'backup_dir')] @@ -119,16 +154,17 @@ isLeaf = True def __init__(self): - self.logger = logging.getLogger('MediaServer') + self.logger = logging.getLogger('pc_autobackup.mediaserver') self.config = common.LoadOrCreateConfig() def render_GET(self, request): - self.logger.debug('[%s] GET request for %s', request.getClientIP(), - request.path) - self.logger.debug('Request args for %s from %s: %s', request.path, - request.getClientIP(), request.args) - self.logger.debug('Request headers for %s from %s: %s', request.path, - request.getClientIP(), request.args) + if request.path != '/favicon.ico': + self.logger.debug('[%s] GET request for %s', request.getClientIP(), + request.path) + self.logger.debug('Request args for %s from %s: %s', request.path, + request.getClientIP(), request.args) + self.logger.debug('Request headers for %s from %s: %s', request.path, + request.getClientIP(), request.args) if request.path == '/DMS/SamsungDmsDesc.xml': self.logger.info('New connection from %s (%s)', request.getClientIP(), @@ -144,11 +180,16 @@ else: self.logger.error('Unhandled GET request from %s: %s', request.getClientIP(), request.path) - return NoResource() + request.setResponseCode(404) + return '' - self.logger.debug('Response: %s', response) + if isinstance(response, unicode): + response = response.encode('utf-8') + request.setHeader("Content-Type", "text/xml; charset=utf-8") - return response.encode('utf-8') + self.logger.debug('Sending response for %s to %s: %s', request.path, + request.getClientIP(), response) + return response def render_POST(self, request): self.logger.debug('Request args for %s from %s: %s', request.path, @@ -163,14 +204,28 @@ else: self.logger.error('Unhandled POST request from %s: %s', request.getClientIP(), request.path) - return NoResource() + request.setResponseCode(404) + return '' + if isinstance(response, unicode): + response = response.encode('utf-8') + + request.setHeader("Content-Type", "text/xml; charset=utf-8") self.logger.debug('Sending response for %s to %s: %s', request.path, request.getClientIP(), response) - request.setHeader("Content-Type", "text/xml; charset=utf-8") - return response.encode('utf-8') + return response def GetContentDirectoryResponse(self, request): + """Generate the ContentDirectory response XML. + + Args: + request: A twisted.web.server.Request + + Returns: + A string containing the XML contents + In an error, the HTTP response code is set to 404 and an empty string is + returned. + """ self.logger.debug('Request content for %s from %s: %s', request.path, request.getClientIP(), request.content.read()) request.content.seek(0) @@ -198,11 +253,12 @@ obj_subtype = parsed_data.get('protocolInfo').split(':')[3] except IndexError: self.logger.error('Invalid DIDL: %s', soap_xml) - return NoResource() + request.setResponseCode(404) + return '' backup = Backup() - obj_id = backup.CreateObject(obj_class, obj_name, obj_date, obj_type, - obj_size, obj_subtype) + obj_id = backup.CreateObject(obj_class, obj_date, obj_name, obj_size, + obj_subtype, obj_type) obj_details = backup.GetObjectDetails(obj_id) self.logger.info('Ready to receive %s (%s size:%s)', obj_name, obj_type, @@ -226,11 +282,17 @@ response = X_BACKUP_RESPONSE % 'DONE' else: self.logger.error('Unhandled soapaction: %s', soapaction) - return NoResource() + request.setResponseCode(404) + return '' return response def GetDMSDescriptionResponse(self): + """Generate the DMS Description response XML. + + Returns: + A string containing the XML contents + """ with open(os.path.join('DMS', 'SamsungDmsDesc.xml'), 'r') as dms_desc: response = dms_desc.read() % { 'friendly_name': self.config.get('AUTOBACKUP', 'server_name'), @@ -239,14 +301,33 @@ return response def ParseDIDL(self, didl): - # - # - # SAM_0001.JPG - # 2012-01-01 - # object.item.imageItem - # - # - # + """Parse DIDL. + + The following is an example of the DIDL to be parsed: + + + + SAM_0001.JPG + 2012-01-01 + object.item.imageItem + + + + + The example DIDL would return the following dict: + + {'class': 'object.item.imageItem', + 'date': '2012-01-01', + 'name': 'SAM_0001.JPG', + 'protocolInfo': '*:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_CI=0', + 'size': '4429673'} + + Args: + didl: A string containing the DIDL to be parsed + + Returns: + A dict containing the item's elements + """ parser = HTMLParser.HTMLParser() didl = parser.unescape(didl) @@ -280,6 +361,14 @@ return didl_elements def ReceiveUpload(self, request): + """Receive an uploaded file. + + Args: + request: A twisted.web.server.Request + + Returns: + An empty string + """ response = '' obj_id = request.args['didx'][0].split('=')[1] @@ -292,6 +381,10 @@ def StartMediaServer(): + """Start a MediaServer server. + + Used for debugging/testing just a MediaServer server. + """ logging.info('MediaServer started') resource = MediaServer() factory = Site(resource) diff --git a/pc_autobackup.py b/pc_autobackup.py index 2728239..9c4abca 100755 --- a/pc_autobackup.py +++ b/pc_autobackup.py @@ -6,6 +6,7 @@ __author__ = 'jeff@rebeiro.net (Jeff Rebeiro)' import logging +from logging.handlers import TimedRotatingFileHandler import optparse import os import platform @@ -23,7 +24,15 @@ def GetCameraConfig(mountpoint): - logger = logging.getLogger('PCAutoBackup') + """Get configuration options for a camera. + + Args: + mountpoint: A string containing the path to the cameras SD card + + Returns: + A dict containing the cameras config + """ + logger = logging.getLogger('pc_autobackup') device_file = None for f in common.CAMERA_INFO_FILE: @@ -46,7 +55,8 @@ def GetSystemInfo(): - logger = logging.getLogger('PCAutoBackup') + """Log basic system information to the debug logger.""" + logger = logging.getLogger('pc_autobackup') logger.debug('Command-line: %s', ' '.join(sys.argv)) logger.debug('Python Version: %s', platform.python_version()) logger.debug('System Information (platform): %s', platform.platform()) @@ -62,7 +72,12 @@ def ImportCameraConfig(mountpoint): - logger = logging.getLogger('PCAutoBackup') + """Import the PC AutoBackup settings from a camera. + + Args: + mountpoint: A string containing the path to the cameras SD card + """ + logger = logging.getLogger('pc_autobackup') camera_config = GetCameraConfig(mountpoint) desc_file = os.path.join(mountpoint, camera_config['desc_file']) @@ -77,14 +92,14 @@ if m: friendly_name = m.group(1) else: - logging.error('Unable to determine server name from camera config') + logger.error('Unable to determine server name from camera config') sys.exit(1) m = common.DESC_UUID.search(desc_data) if m: uuid = m.group(1) else: - logging.error('Unable to determine server name from camera config') + logger.error('Unable to determine server name from camera config') sys.exit(1) config.set('AUTOBACKUP', 'server_name', friendly_name) @@ -106,7 +121,13 @@ def UpdateCameraConfig(mountpoint, create_desc_file=False): - logger = logging.getLogger('PCAutoBackup') + """Update the PC AutoBackup settings on a camera. + + Args: + mountpoint: A string containing the path to the cameras SD card + create_desc_file: True if the settings file should be created + """ + logger = logging.getLogger('pc_autobackup') mac_address = hex(uuid.getnode()) mac_address = re.findall('..', mac_address) @@ -117,7 +138,7 @@ if create_desc_file: with open(desc_file, 'w+') as f: - logging.info('Creating %s', desc_file) + logger.info('Creating %s', desc_file) if os.path.isfile(desc_file): with open(desc_file, 'wb') as f: @@ -147,8 +168,8 @@ parser.add_option('--import_camera_config', dest='import_camera_config', help='update server with cameras configuration', metavar='MOUNTPOINT') - parser.add_option('--log_file', dest='log_file', default='backup.log', - help='change output log file (default: backup.log)', + parser.add_option('--log_file', dest='log_file', default='autobackup.log', + help='change output log file (default: autobackup.log)', metavar='FILE') parser.add_option('-n', '--name', dest='server_name', help='change server name', metavar='NAME') @@ -164,23 +185,34 @@ metavar='MOUNTPOINT') (options, args) = parser.parse_args() - console_logging_options = common.LOG_DEFAULTS.copy() - logging_options = common.LOG_DEFAULTS.copy() + log_opts = common.LOG_DEFAULTS.copy() + lf_log_opts = common.LOG_DEFAULTS.copy() if options.quiet: - console_logging_options['level'] = logging.WARN + log_opts['level'] = logging.WARN if options.debug: - logging_options['level'] = logging.DEBUG + print 'enabling debug' + lf_log_opts['level'] = logging.DEBUG - logging_options['filename'] = options.log_file + logger = logging.getLogger('pc_autobackup') + logger.setLevel(logging.DEBUG) - logging.basicConfig(**logging_options) + lf_handler = TimedRotatingFileHandler(options.log_file, + when="midnight", + backupCount=3) + lf_handler.setLevel(lf_log_opts['level']) + lf_log_opts['format'] = '%(asctime)s[%(name)s] %(levelname)s:%(message)s' + formatter = logging.Formatter(lf_log_opts['format'], + lf_log_opts['datefmt']) + lf_handler.setFormatter(formatter) + logger.addHandler(lf_handler) console = logging.StreamHandler() - console.setLevel(console_logging_options['level']) - formatter = logging.Formatter('%(asctime)s %(message)s', common.LOG_DATE_FMT) + console.setLevel(log_opts['level']) + formatter = logging.Formatter(log_opts['format'], + log_opts['datefmt']) console.setFormatter(formatter) - logging.getLogger('').addHandler(console) + logger.addHandler(console) config = common.LoadOrCreateConfig() update_config = False @@ -214,7 +246,6 @@ UpdateCameraConfig(options.update_camera_config) sys.exit(0) - logger = logging.getLogger('PCAutoBackup') logger.info('PCAutoBackup started on %s', config.get('AUTOBACKUP', 'default_interface')) logger.info('Server name: %s', config.get('AUTOBACKUP', 'server_name')) diff --git a/ssdp.py b/ssdp.py index c298c97..c2df9fe 100644 --- a/ssdp.py +++ b/ssdp.py @@ -21,7 +21,7 @@ class SSDPServer(DatagramProtocol): def __init__(self): - self.logger = logging.getLogger('SSDPServer') + self.logger = logging.getLogger('pc_autobackup.ssdp') self.config = common.LoadOrCreateConfig() def startProtocol(self): @@ -45,7 +45,18 @@ self.SendSSDPResponse(address) def GenerateSSDPResponse(self, response_type, ip_address, uuid, - notify_fields=None): + notify_fields={}): + """Generate an SSDP response. + + Args: + response_type: One of m-search or notify + ip_address: IP address to use for the response + uuid: UUID to use for the response + notify_fields: A dictionary containing NT, NTS, and USN fields + + Returns: + A string containing an SSDP response + """ location = 'LOCATION: http://%s:52235/DMS/SamsungDmsDesc.xml' % ip_address if response_type == 'm-search': response = ['HTTP/1.1 200 OK', @@ -70,6 +81,14 @@ return '\r\n'.join(response) def ParseSSDPDiscovery(self, datagram): + """Parse an SSDP UDP datagram. + + Args: + datagram: A string containing an SSDP request data + + Returns: + A dict containing the parsed data + """ parsed_data = {} for line in datagram.splitlines(): @@ -100,11 +119,15 @@ address_info = ':'.join([str(x) for x in address]) self.logger.info('Sending SSDP response to %s', address_info) - self.logger.debug('Response: %r', response) + self.logger.debug('Sending SSDP response to %s: %r', address_info, response) self.transport.write(response, address) def StartSSDPServer(): + """Start an SSDP server. + + Used for debugging/testing just an SSDP server. + """ logging.info('SSDPServer started') reactor.listenMulticast(1900, SSDPServer(), listenMultiple=True) reactor.run()