Browse Source

added command 'status' to show timestamp of last backup

Fabian Peter Hammerle 10 năm trước cách đây
mục cha
commit
c6a98f00c2
17 tập tin đã thay đổi với 198 bổ sung và 0 xóa
  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)