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()