Inkrementell

Die Absicherung von Backups gegen heimtückische Modifikationen von Informationswerten hilft natürlich nicht gegen versehentliche Löschungen und andere Ungeschicklichkeiten, wenn jeweils das bestehende Backup durch ein neues vollständig überschrieben wird. Eine inkrementelle Backup-Methode ermöglicht dagegen die regelmäßige Sicherung eines veränderten Datenbestands, ohne die vorangehenden Sicherungen zu beeinträchtigen.

Bis vor kurzem bin ich davon ausgegangen, dass ich entweder rsync nutzen oder inkrementelle Backups erstellen kann. Tatsächlich bietet rsync (natürlich!) mit dem unscheinbaren Parameter --link-dest eine Möglichkeit, die zu sichernden Daten mit einem bestehenden Ziel-Verzeichnis abzugleichen und für alle unveränderten Dateien lediglich einen zusätzlichen hard link zu setzen. Mein Wrapper rbackup muss wie folgt ergänzt werden:

class BackupProfile: # ... @property def target_dir(self): if self.incremental: return f'{self.target_path}/{datetime.date.today().strftime("%Y-%m-%d")}' else: return self.target_path @property def link_dest(self): if self.incremental: return [f'--link-dest=../latest'] else: return [] # ... def backup(self): print(f'Executing backup profile "{self.label}" ...') self.ransom() command_list = ["/opt/homebrew/bin/rsync", "-e", "ssh"] command_list.extend(self.options) command_list.extend(self.link_dest) command_list.extend(self.excludepath) command_list.extend([self.source, self.target_dir]) return_code = subprocess.call(command_list) self.rotate()

../latest ist ein Symlink auf das jeweils aktuellste Zielverzeichnis, er muss also initial von Hand erstellt (ln -s /volume1/backup/YYYY-MM-DD latest) und nach jeder Sicherung aktualisiert werden (mit der Funktion self.rotate()). Für lokale Speichermedien ist das sehr simpel (os.unlink() und os.symlink()), aber für serverbasierte Backups muss eine SFTP-Verbindung her:

def ssh_key(self, id_file): if hasattr(self, 'keytype'): if self.keytype == 'ecdsa': return paramiko.ECDSAKey.from_private_key(open(id_file)) else: return paramiko.RSAKey.from_private_key(open(id_file)) else: return False def rotate(self): if self.incremental: if hasattr(self, 'target_host'): my_config = paramiko.SSHConfig() my_config.parse(open('/Users/jan/.ssh/config')) conf = my_config.lookup(self.target_host) transport = paramiko.Transport((conf['hostname'],int(conf['port']))) transport.connect(username=conf['user'], pkey=self.ssh_key(conf['identityfile'][0])) sftp = paramiko.SFTPClient.from_transport(transport) sftp.unlink(self.sftp_latest_dir) sftp.symlink(self.target_dir, self.sftp_latest_dir) sftp.close() transport.close() else: os.unlink(self.latest_dir) os.symlink(self.target_dir, self.latest_dir)

Wenn man ein Synology NAS zur Sicherung nutzt, gibt es eine weitere Besonderheit: SSH-Verbindungen erfordern eine vollständige Pfadangabe samt Volume (z.B. /volume1/backup), für SFTP-Verbindungen dagegen wird der jeweilige shared folder als Root-Verzeichnis betrachtet (/backup). Der Symlink in unserer Konstellation muss daher als /backup/latest aktualisiert werden, aber auf /volume1/backup/YYYY-MM-DD verweisen (andernfalls findet rsync den Vergleichsordner beim nächsten Durchlauf nicht). Deshalb bedarf es eines ausgesprochen schmutzigen Hacks, um self.sftp_latest_dir abhängig vom Zielsystem zu generieren (während self.target_dir für jedes Zielsystem unverändert genutzt werden kann):

@property def sftp_latest_dir(self): if hasattr(self, 'target_host'): if self.target_host.startswith('synology'): return self.latest_dir[8:] else: return self.latest_dir

Eventuell werde ich demnächst vom interstellaren Servicedesk hinter Synology abgeholt.

Um den Platzbedarf der einzelnen Backup-Verzeichnisse zu eruieren, lässt sich das Verhalten eines altgedienten Unix-Tools bei Verwendung mehrerer Argumente ausnutzen (Files having multiple hard links are counted (and displayed) a single time per du execution.) –

snafu@localhost % du -sh . 2022-04-05 2022-04-07 2022-04-08 27G . 27G 2022-04-05 60M 2022-04-07 177M 2022-04-08

– und neue/geänderte Dateien im aktuellsten Backup-Verzeichnis lassen sich als diejenigen identifizieren, für die nur ein Link existiert (find . -type f -links 1 -print).