Repository: borgbackup/borg

Issue:
include backup start time / duration in borg info
If you use the -s parameter on borg create you get an info when the backup started and how long it took.

It would be useful to also include it for display with borg info.



Discussion:
`borg info` is showing infos stored in the archive (at any time later) and duration is not yet stored.

`borg create --stats` might show some additional infos available at the time it runs, but that are maybe **not** stored in the archive.

This would need changing at least the archive metadata dict (see Archive.save) and maybe increasing the version number for that structure.

The archive metadata item "timestamp" (also duplicated in manifest archive list) currently has the **ending** time of the backup. Maybe it would make more sense to put the backup **starting** time there (which is about the same as the snapshot time if one creates a snapshot before invoking borg create). We could add a duration field to the metadata. As this stuff is a dict, we maybe do not need to change the version number if we just add stuff.

Changing the meaning of one field is more critical, but maybe not really because these timestamps are only used by humans and for pruning.

BTW, one does not need this if the archive name includes the current (backup start) time.
As the archive metadata dict contains the timestamp of the backup end, you can see how long it took.
Also, if you have log files of your backups, you can also see it there.


Resolving patch:
diff --git a/borg/archive.py b/borg/archive.py
--- a/borg/archive.py
+++ b/borg/archive.py
@@ -128,7 +128,7 @@ class IncompatibleFilesystemEncodingError(Error):
     def __init__(self, repository, key, manifest, name, cache=None, create=False,
                  checkpoint_interval=300, numeric_owner=False, progress=False,
                  chunker_params=CHUNKER_PARAMS,
-                 start=datetime.now(), end=datetime.now()):
+                 start=datetime.utcnow(), end=datetime.utcnow()):
         self.cwd = os.getcwd()
         self.key = key
         self.repository = repository
@@ -172,15 +172,20 @@ def _load_meta(self, id):
     def load(self, id):
         self.id = id
         self.metadata = self._load_meta(self.id)
-        decode_dict(self.metadata, (b'name', b'hostname', b'username', b'time'))
+        decode_dict(self.metadata, (b'name', b'hostname', b'username', b'time', b'time_end'))
         self.metadata[b'cmdline'] = [arg.decode('utf-8', 'surrogateescape') for arg in self.metadata[b'cmdline']]
         self.name = self.metadata[b'name']
 
     @property
     def ts(self):
-        """Timestamp of archive creation in UTC"""
+        """Timestamp of archive creation (start) in UTC"""
         return parse_timestamp(self.metadata[b'time'])
 
+    @property
+    def ts_end(self):
+        """Timestamp of archive creation (end) in UTC"""
+        return parse_timestamp(self.metadata[b'time_end'])
+
     @property
     def fpr(self):
         return hexlify(self.id).decode('ascii')
@@ -226,7 +231,11 @@ def save(self, name=None, timestamp=None):
             raise self.AlreadyExists(name)
         self.items_buffer.flush(flush=True)
         if timestamp is None:
-            timestamp = datetime.utcnow()
+            start = self.start
+            end = self.end
+        else:
+            start = timestamp
+            end = timestamp  # we only have 1 value
         metadata = StableDict({
             'version': 1,
             'name': name,
@@ -234,7 +243,8 @@ def save(self, name=None, timestamp=None):
             'cmdline': sys.argv,
             'hostname': socket.gethostname(),
             'username': getuser(),
-            'time': timestamp.isoformat(),
+            'time': start.isoformat(),
+            'time_end': end.isoformat(),
         })
         data = msgpack.packb(metadata, unicode_errors='surrogateescape')
         self.id = self.key.id_hash(data)
@@ -851,7 +861,7 @@ def report(msg, chunk_id, chunk_no):
                 archive = StableDict(msgpack.unpackb(data))
                 if archive[b'version'] != 1:
                     raise Exception('Unknown archive metadata version')
-                decode_dict(archive, (b'name', b'hostname', b'username', b'time'))
+                decode_dict(archive, (b'name', b'hostname', b'username', b'time', b'time_end'))
                 archive[b'cmdline'] = [arg.decode('utf-8', 'surrogateescape') for arg in archive[b'cmdline']]
                 items_buffer = ChunkBuffer(self.key)
                 items_buffer.write_chunk = add_callback
diff --git a/borg/archiver.py b/borg/archiver.py
--- a/borg/archiver.py
+++ b/borg/archiver.py
@@ -191,7 +191,7 @@ def create_inner(archive, cache):
                 if args.progress:
                     archive.stats.show_progress(final=True)
                 if args.stats:
-                    archive.end = datetime.now()
+                    archive.end = datetime.utcnow()
                     log_multi(DASHES,
                               str(archive),
                               DASHES,
@@ -202,7 +202,7 @@ def create_inner(archive, cache):
         self.output_filter = args.output_filter
         self.output_list = args.output_list
         dry_run = args.dry_run
-        t0 = datetime.now()
+        t0 = datetime.utcnow()
         if not dry_run:
             repository = self.open_repository(args, exclusive=True)
             manifest, key = Manifest.load(repository)
@@ -489,7 +489,8 @@ def do_info(self, args):
             print('Fingerprint: %s' % hexlify(archive.id).decode('ascii'))
             print('Hostname:', archive.metadata[b'hostname'])
             print('Username:', archive.metadata[b'username'])
-            print('Time: %s' % format_time(to_localtime(archive.ts)))
+            print('Time (start): %s' % format_time(to_localtime(archive.ts)))
+            print('Time (end):   %s' % format_time(to_localtime(archive.ts_end)))
             print('Command line:', remove_surrogates(' '.join(archive.metadata[b'cmdline'])))
             print('Number of files: %d' % stats.nfiles)
             print()


Test patch: