Browse Source

added command 'status' to show timestamp of last backup

Fabian Peter Hammerle 7 years ago
parent
commit
c6a98f00c2
17 changed files with 198 additions and 0 deletions
  1. 10 0
      README.md
  2. 101 0
      duplitab/__init__.py
  3. 20 0
      scripts/duplitab
  4. BIN
      tests/data/collections/empty/multiple-full/duplicity-full-signatures.20161027T175733Z.sigtar.gz
  5. BIN
      tests/data/collections/empty/multiple-full/duplicity-full-signatures.20161027T175746Z.sigtar.gz
  6. BIN
      tests/data/collections/empty/multiple-full/duplicity-full-signatures.20161027T175747Z.sigtar.gz
  7. BIN
      tests/data/collections/empty/multiple-full/duplicity-full.20161027T175733Z.vol1.difftar.gz
  8. BIN
      tests/data/collections/empty/multiple-full/duplicity-full.20161027T175746Z.vol1.difftar.gz
  9. BIN
      tests/data/collections/empty/multiple-full/duplicity-full.20161027T175747Z.vol1.difftar.gz
  10. BIN
      tests/data/collections/empty/multiple-full/duplicity-inc.20161027T175733Z.to.20161027T175735Z.vol1.difftar.gz
  11. BIN
      tests/data/collections/empty/multiple-full/duplicity-inc.20161027T175735Z.to.20161027T175739Z.vol1.difftar.gz
  12. BIN
      tests/data/collections/empty/multiple-full/duplicity-inc.20161027T175747Z.to.20161027T175754Z.vol1.difftar.gz
  13. BIN
      tests/data/collections/empty/multiple-full/duplicity-new-signatures.20161027T175733Z.to.20161027T175735Z.sigtar.gz
  14. BIN
      tests/data/collections/empty/multiple-full/duplicity-new-signatures.20161027T175735Z.to.20161027T175739Z.sigtar.gz
  15. BIN
      tests/data/collections/empty/multiple-full/duplicity-new-signatures.20161027T175747Z.to.20161027T175754Z.sigtar.gz
  16. 57 0
      tests/test_collection.py
  17. 10 0
      tests/test_parse.py

+ 10 - 0
README.md

@@ -51,3 +51,13 @@ $ duplitab backup
 ```bash
 $ duplitab --filter-target-url '.*media/backup/[hs].*' backup
 ```
+
+## Show Status
+
+```bash
+$ duplitab --filter-target-url '.*media/backup/[hs].*' status --table-style tabular
+target url                               last_backup
+---------------------------------------  -------------------
+file:///media/backup/home                2016-10-23 08:35:13
+sftp://user@server//media/backup/secret  2016-09-22 09:36:14
+```

+ 101 - 0
duplitab/__init__.py

@@ -0,0 +1,101 @@
+import datetime
+import re
+import subprocess
+import sys
+import time
+
+
+def _duplicity(params):
+    stdout = subprocess.check_output(
+        ['duplicity'] + params,
+    )
+    return stdout.decode(sys.stdout.encoding)
+
+
+def _parse_duplicity_timestamp(timestamp):
+    return datetime.datetime.fromtimestamp(
+        time.mktime(time.strptime(timestamp))
+    )
+
+
+class Collection(object):
+
+    def __init__(self, url):
+        self.url = url
+
+    def request_status(self):
+        return _CollectionStatus._parse(
+            text=_duplicity(['collection-status', self.url])
+        )
+
+
+class _Status(object):
+
+    def __eq__(self, other):
+        return isinstance(self, type(other)) and vars(self) == vars(other)
+
+    def __neq__(self, other):
+        return not (self == other)
+
+
+class _CollectionStatus(_Status):
+
+    chain_separator_regex = r'-{25}\s'
+
+    def __init__(self, primary_chain):
+        self.primary_chain = primary_chain
+
+    @classmethod
+    def _parse(cls, text):
+        primary_chain_match = re.search(
+            '^Found primary backup chain.*\s{sep}([\w\W]*?)\s{sep}'.format(
+                sep=_CollectionStatus.chain_separator_regex,
+            ),
+            text,
+            re.MULTILINE,
+        )
+        return cls(
+            primary_chain=_ChainStatus._parse(
+                text=primary_chain_match.group(1),
+            ),
+        )
+
+
+class _ChainStatus(_Status):
+
+    def __init__(self, sets):
+        self.sets = sets
+
+    def __eq__(self, other):
+        return isinstance(self, type(other)) and vars(self) == vars(other)
+
+    def __neq__(self, other):
+        return not (self == other)
+
+    @classmethod
+    def _parse(cls, text):
+        sets = []
+        set_lines = re.split(r'Num volumes: *\r?\n', text)[1]
+        for set_line in re.split(r'\r?\n', set_lines):
+            set_attr = re.match(
+                r'\s*(?P<mode>\w+) {2,}(?P<ts>.+?) {2,} (?P<vol>\d+)',
+                set_line,
+            ).groupdict()
+            # duplicity uses time.asctime().
+            # time.strptime() without format inverts time.asctime().
+            sets.append(_SetStatus(
+                backup_time=_parse_duplicity_timestamp(set_attr['ts']),
+            ))
+        return cls(sets=sets)
+
+
+class _SetStatus(_Status):
+
+    def __init__(self, backup_time):
+        self.backup_time = backup_time
+
+    def __eq__(self, other):
+        return isinstance(self, type(other)) and vars(self) == vars(other)
+
+    def __neq__(self, other):
+        return not (self == other)

+ 20 - 0
scripts/duplitab

@@ -2,6 +2,7 @@
 # PYTHON_ARGCOMPLETE_OK
 
 import collections
+import duplitab
 import hashlib
 import os
 import re
@@ -232,6 +233,20 @@ def run(command, config_path, quiet, duplicity_verbosity,
             columns.keys(),
             tablefmt = table_style,
             ))
+    elif command == 'status':
+        table = []
+        for backup_attr in filtered_config:
+            collection = duplitab.Collection(url = backup_attr['target_url'])
+            status = collection.request_status()
+            table.append([
+                collection.url,
+                status.primary_chain.sets[-1].backup_time,
+                ])
+        print(tabulate.tabulate(
+            table,
+            ['target_url', 'last backup'],
+            tablefmt = table_style,
+            ))
     elif command == 'backup':
         backup(
             config = filtered_config,
@@ -277,6 +292,11 @@ def _init_argparser():
             dest = 'command',
             )
     subparser_list = subparsers.add_parser('list')
+    subparser_list.add_argument(
+            '--table-style',
+            default = 'plain',
+            )
+    subparser_list = subparsers.add_parser('status')
     subparser_list.add_argument(
             '--table-style',
             default = 'plain',

BIN
tests/data/collections/empty/multiple-full/duplicity-full-signatures.20161027T175733Z.sigtar.gz


BIN
tests/data/collections/empty/multiple-full/duplicity-full-signatures.20161027T175746Z.sigtar.gz


BIN
tests/data/collections/empty/multiple-full/duplicity-full-signatures.20161027T175747Z.sigtar.gz


BIN
tests/data/collections/empty/multiple-full/duplicity-full.20161027T175733Z.vol1.difftar.gz


BIN
tests/data/collections/empty/multiple-full/duplicity-full.20161027T175746Z.vol1.difftar.gz


BIN
tests/data/collections/empty/multiple-full/duplicity-full.20161027T175747Z.vol1.difftar.gz


BIN
tests/data/collections/empty/multiple-full/duplicity-inc.20161027T175733Z.to.20161027T175735Z.vol1.difftar.gz


BIN
tests/data/collections/empty/multiple-full/duplicity-inc.20161027T175735Z.to.20161027T175739Z.vol1.difftar.gz


BIN
tests/data/collections/empty/multiple-full/duplicity-inc.20161027T175747Z.to.20161027T175754Z.vol1.difftar.gz


BIN
tests/data/collections/empty/multiple-full/duplicity-new-signatures.20161027T175733Z.to.20161027T175735Z.sigtar.gz


BIN
tests/data/collections/empty/multiple-full/duplicity-new-signatures.20161027T175735Z.to.20161027T175739Z.sigtar.gz


BIN
tests/data/collections/empty/multiple-full/duplicity-new-signatures.20161027T175747Z.to.20161027T175754Z.sigtar.gz


+ 57 - 0
tests/test_collection.py

@@ -0,0 +1,57 @@
+import pytest
+
+import duplitab
+import datetime
+
+
+@pytest.mark.parametrize(('init_kwargs', 'expected_attr'), [
+    [
+        {
+            'url': 'file://media/backup/a',
+        },
+        {
+            'url': 'file://media/backup/a',
+        },
+    ],
+    [
+        {
+            'url': 'sftp://user@server//media/backup/阿',
+        },
+        {
+            'url': 'sftp://user@server//media/backup/阿',
+        },
+    ],
+])
+def test_collection_init(init_kwargs, expected_attr):
+    c = duplitab.Collection(**init_kwargs)
+    for name, value in expected_attr.items():
+        assert getattr(c, name) == value
+
+
+@pytest.mark.parametrize(('init_kwargs', 'ex_class'), [
+    [
+        {
+        },
+        TypeError,
+    ],
+])
+def test_collection_init_fail(init_kwargs, ex_class):
+    with pytest.raises(ex_class):
+        duplitab.Collection(**init_kwargs)
+
+
+@pytest.mark.parametrize(('url', 'expected_status'), [
+    [
+        'file://tests/data/collections/empty/multiple-full',
+        duplitab._CollectionStatus(
+            primary_chain=duplitab._ChainStatus(
+                sets=[
+                    duplitab._SetStatus(backup_time=datetime.datetime(2016, 10, 27, 19, 57, 47)),
+                    duplitab._SetStatus(backup_time=datetime.datetime(2016, 10, 27, 19, 57, 54)),
+                ]),
+        ),
+    ],
+])
+def test_collection_request_status(url, expected_status):
+    c = duplitab.Collection(url=url)
+    assert expected_status == c.request_status()

+ 10 - 0
tests/test_parse.py

@@ -0,0 +1,10 @@
+import pytest
+
+import datetime
+import duplitab
+
+@pytest.mark.parametrize(('duplicity_timestamp', 'expected'), [
+    ['Tue Oct 11 11:02:01 2016', datetime.datetime(2016, 10, 11, 11, 2, 1)],
+])
+def test_parse_duplicity_timestamp(duplicity_timestamp, expected):
+    assert expected == duplitab._parse_duplicity_timestamp(duplicity_timestamp)