|
@@ -1,6 +1,7 @@
|
|
|
import contextlib
|
|
|
import os
|
|
|
import platform
|
|
|
+import shlex
|
|
|
import subprocess
|
|
|
import sys
|
|
|
import time
|
|
@@ -54,7 +55,7 @@ class BackupException(Exception):
|
|
|
class Job(object):
|
|
|
pool = []
|
|
|
maxthreads = 4
|
|
|
- verbosity = 4
|
|
|
+ verbosity = 3
|
|
|
logTransitions = False
|
|
|
READY = 0
|
|
|
QUEUED = 1
|
|
@@ -114,27 +115,47 @@ class Job(object):
|
|
|
|
|
|
@property
|
|
|
def args(self):
|
|
|
- if Backup.engine == "rdiff-backup":
|
|
|
- args = ['rdiff-backup', '-v{}'.format(Backup.verbosity)]
|
|
|
- if 'filters' in self.config:
|
|
|
- for item in self.config['filters']:
|
|
|
- if 'include' in item:
|
|
|
- args += ['--include', item['include']]
|
|
|
+ if not hasattr(self, '_args'):
|
|
|
+ if Backup.engine == "rdiff-backup":
|
|
|
+ args = ['rdiff-backup', '-v{}'.format(Backup.verbosity)]
|
|
|
+ if 'filters' in self.config:
|
|
|
+ for item in self.config['filters']:
|
|
|
+ if 'include' in item:
|
|
|
+ args += ['--include', item['include']]
|
|
|
|
|
|
- elif 'exclude' in item:
|
|
|
- args += ['--exclude', item['exclude']]
|
|
|
+ elif 'exclude' in item:
|
|
|
+ args += ['--exclude', item['exclude']]
|
|
|
|
|
|
- else:
|
|
|
- raise BackupException(
|
|
|
- '{0} has an invalid filter {1}'.format(self, item))
|
|
|
+ else:
|
|
|
+ raise BackupException(
|
|
|
+ '{0} has an invalid filter {1}'.format(self, item))
|
|
|
+
|
|
|
+ self._args = args + [self.fromPath, self.toPath]
|
|
|
|
|
|
- return args + [self.fromPath, self.toPath]
|
|
|
+ else:
|
|
|
+ raise StateException(
|
|
|
+ 'Invalid backup engine {}'.format(Backup.engine))
|
|
|
|
|
|
- raise StateException('Invalid backup engine {}'.format(Backup.engine))
|
|
|
+ return self._args
|
|
|
|
|
|
@property
|
|
|
def logfile(self):
|
|
|
- return self._backup.logfile
|
|
|
+ if not hasattr(self, '_logfile'):
|
|
|
+ path = os.path.dirname(self.logpath)
|
|
|
+ if not os.path.exists(path):
|
|
|
+ os.makedirs(path, exist_ok=True)
|
|
|
+
|
|
|
+ self._logfile = open(self.logpath, 'w')
|
|
|
+
|
|
|
+ return self._logfile
|
|
|
+
|
|
|
+ @property
|
|
|
+ def logpath(self):
|
|
|
+ if not hasattr(self, '_logpath'):
|
|
|
+ self._logpath = os.path.join(os.path.dirname(
|
|
|
+ self._backup.logpath), 'job{}.log'.format(self.index))
|
|
|
+
|
|
|
+ return self._logpath
|
|
|
|
|
|
@property
|
|
|
def fromPath(self):
|
|
@@ -143,7 +164,15 @@ class Job(object):
|
|
|
if 'roots' in self._backup.config:
|
|
|
roots = self._backup.config['roots']
|
|
|
if 'from' in roots:
|
|
|
- fromPath = os.path.join(roots['from'], fromPath)
|
|
|
+ if '::' in roots['from']:
|
|
|
+ if roots['from'].endswith('::'):
|
|
|
+ fromPath = roots['from'] + fromPath
|
|
|
+
|
|
|
+ else:
|
|
|
+ fromPath = roots['from'] + os.sep + fromPath
|
|
|
+
|
|
|
+ else:
|
|
|
+ fromPath = os.path.join(roots['from'], fromPath)
|
|
|
|
|
|
self._fromPath = fromPath
|
|
|
|
|
@@ -156,7 +185,15 @@ class Job(object):
|
|
|
if 'roots' in self._backup.config:
|
|
|
roots = self._backup.config['roots']
|
|
|
if 'to' in roots:
|
|
|
- toPath = os.path.join(roots['to'], toPath)
|
|
|
+ if '::' in roots['to']:
|
|
|
+ if roots['to'].endswith('::'):
|
|
|
+ toPath = roots['to'] + toPath
|
|
|
+
|
|
|
+ else:
|
|
|
+ toPath = roots['to'] + os.sep + toPath
|
|
|
+
|
|
|
+ else:
|
|
|
+ toPath = os.path.join(roots['to'], toPath)
|
|
|
|
|
|
self._toPath = toPath
|
|
|
|
|
@@ -177,8 +214,8 @@ class Job(object):
|
|
|
text = '[Backup {0} Job #{1}] {2}\n'.format(
|
|
|
self._backup.name, self.index, text)
|
|
|
print(text, end='')
|
|
|
- self.logfile.write(text)
|
|
|
- self.logfile.flush()
|
|
|
+ self._backup.logfile.write(text)
|
|
|
+ self._backup.logfile.flush()
|
|
|
|
|
|
def start(self):
|
|
|
if self.state is not Job.QUEUED:
|
|
@@ -186,14 +223,17 @@ class Job(object):
|
|
|
|
|
|
self._backup.setStatus(Backup.RUNNING)
|
|
|
self.setState(Job.RUNNING)
|
|
|
+ self.logfile.write(' '.join([shlex.quote(x) for x in self.args]) + '\n')
|
|
|
+ self.logfile.flush()
|
|
|
self._process = subprocess.Popen(
|
|
|
self.args, stdout=self.logfile, stderr=subprocess.STDOUT,
|
|
|
stdin=subprocess.DEVNULL, universal_newlines=True, bufsize=1)
|
|
|
|
|
|
def setState(self, state):
|
|
|
- self._state = state
|
|
|
- self.log('{0} -> {1}'.format(
|
|
|
- self.getState(self._state), self.getState(state)))
|
|
|
+ if self._state != state:
|
|
|
+ self.log('{0} -> {1}'.format(
|
|
|
+ self.getState(self._state), self.getState(state)))
|
|
|
+ self._state = state
|
|
|
|
|
|
def getState(self, state=None):
|
|
|
return {
|
|
@@ -269,7 +309,7 @@ class Backup(object):
|
|
|
self._path = self._config['path']
|
|
|
self._name = os.path.basename(self._path)
|
|
|
self._logpath = os.path.realpath(os.path.join(
|
|
|
- Backup.logdir, '{}.log'.format(slugify(self._path))))
|
|
|
+ Backup.logdir, slugify(self._path), 'backup.log'))
|
|
|
self._status = Backup.READY
|
|
|
if self.blocking:
|
|
|
self.setStatus(Backup.BLOCKED)
|
|
@@ -300,6 +340,10 @@ class Backup(object):
|
|
|
@property
|
|
|
def logfile(self):
|
|
|
if not hasattr(self, '_logfile'):
|
|
|
+ path = os.path.dirname(self.logpath)
|
|
|
+ if not os.path.exists(path):
|
|
|
+ os.makedirs(path, exist_ok=True)
|
|
|
+
|
|
|
self._logfile = open(self.logpath, 'w+')
|
|
|
|
|
|
return self._logfile
|
|
@@ -379,9 +423,10 @@ class Backup(object):
|
|
|
return [x for x in self.jobs if x.state is Job.FAILED]
|
|
|
|
|
|
def setStatus(self, status):
|
|
|
- self.log('{0} -> {1}'.format(
|
|
|
- self.getStatus(self._status), self.getStatus(status)))
|
|
|
- self._status = status
|
|
|
+ if self._status != status:
|
|
|
+ self.log('{0} -> {1}'.format(
|
|
|
+ self.getStatus(self._status), self.getStatus(status)))
|
|
|
+ self._status = status
|
|
|
|
|
|
def getStatus(self, status=None):
|
|
|
return {
|