tarfile.add() FileNotFoundError

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
DerAtrox
User
Beiträge: 5
Registriert: Sonntag 8. Mai 2016, 20:36
Kontaktdaten:

Guten Tag alle zusammen ^^

Hab mich nun nach längerer Zeit mal wieder etwas mit Python befasst und bin nun dabei ein Backuprotation Skript zu schreiben.

Ganz einfach beschrieben nimmt mein Skript ein Verzeichnis und packt dieses ein .tar-Archiv ein.

Bis jetzt funktioniert das ganze auch soweit ganz gut, es gibt nur ein Problem: tarfile.add(...) wirft mir einen FileNotFoundError, sobald er zu einer Datei gelangt die nicht existiert. Klingt komisch, ist aber so. :K
Durch diesen Fehler wird dann mein Skript natürlich komplett abgebrochen und der Backupvorgang nicht beendet.

Im folgenden die Fehlermeldung:

Code: Alles auswählen

Traceback (most recent call last):
  File "backup-rotation.py", line 274, in <module>
    main()
  File "backup-rotation.py", line 47, in main
    create_backup(backup_item, file_name)
  File "backup-rotation.py", line 151, in create_backup
    tar.add(backup_item.source, arcname=os.path.basename(backup_item.source))
  File "/usr/lib/python3.4/tarfile.py", line 1930, in add
    recursive, exclude, filter=filter)
  File "/usr/lib/python3.4/tarfile.py", line 1930, in add
    recursive, exclude, filter=filter)
  File "/usr/lib/python3.4/tarfile.py", line 1930, in add
    recursive, exclude, filter=filter)
  File "/usr/lib/python3.4/tarfile.py", line 1930, in add
    recursive, exclude, filter=filter)
  File "/usr/lib/python3.4/tarfile.py", line 1930, in add
    recursive, exclude, filter=filter)
  File "/usr/lib/python3.4/tarfile.py", line 1907, in add
    tarinfo = self.gettarinfo(name, arcname)
  File "/usr/lib/python3.4/tarfile.py", line 1779, in gettarinfo
    statres = os.lstat(name)
FileNotFoundError: [Errno 2] No such file or directory: '/home/autobackup/backup/serpens/atrox/home/users/deratrox-spam/.Catched/cur/X 88\\ 01h .1\x03P86|0er\x03'
Die Datei die er da packen will, existiert auch tatsächlich nicht, aber warum findet er sie dann überhaupt?

Ich habe das ganze nun schon mit einem Shell-Skript getestet, der das Linux tar-Paket verwendet. Dieser wirft interessanterweise auch einen Fehler, ignoriert ihn aber und bricht den Pack-Vorgang nicht ab.

Fehler im Shell-Skript:

Code: Alles auswählen

tar: /home/autobackup/backup/serpens/atrox/home/users/deratrox-spam/.Catched/cur/X 88\\ 01h .1\003P86|0er\003: Datei gelöscht, bevor sie gelesen wurde.
Klar, ein try-catch würde mir in so fern helfen, dass mein Skript noch bis zum Ende läuft, der Packvorgang würde aber trotzdem abgebrochen werden.

Meinen gesamten Skript könnt ihr auf GitHub einsehen: https://github.com/DerAtrox/backup-rota ... on.py#L134
Der Packvorgang findet in Zeile 151 statt.

Gibt es eine Möglichkeit diese Fehler so ignorieren, sodass er in so einem Fall die Datei einfach überspringt?
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

DerAtrox hat geschrieben:Die Datei die er da packen will, existiert auch tatsächlich nicht, aber warum findet er sie dann überhaupt?
Die Datei existiert wahrscheinlich nicht mehr. Tarfile.add() macht ein os.listdir() und versucht danach weitere Informationen zu bekommen, die Datei wird in der Zwischenzeit gelöscht und es kommt zu dem Fehler.

Tarfile.add() bietet keine Möglichkeit solche Fehler zu ignorieren, insofern bleibt dir wohl nichts anderes übrig als Tarfile.add() selbst zu implementieren - mit diesem Feature. Du solltest dabei großzügig kopieren können.
DerAtrox
User
Beiträge: 5
Registriert: Sonntag 8. Mai 2016, 20:36
Kontaktdaten:

Hmm okay, danke! Werde ich wohl darauf zurückgreifen müssen.. Frage mich dabei echt, warum die so etwas nicht schon implementiert haben. ^^
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Es hat sich halt noch niemand drum gekümmert. Das ist jetzt deine Chance CPython Contributor zu werden.
DerAtrox
User
Beiträge: 5
Registriert: Sonntag 8. Mai 2016, 20:36
Kontaktdaten:

Hehe :lol:
Ja mal sehen. Habe jetzt für meinen Skript erstmal die komplette Funktion kopiert und ein bisschen mit try-catches um mich geworfen. ^^ Funktioniert jetzt übrigens tatsächlich auch!

Kannst es dir gerne auf git ansehen. :)
BlackJack

``&`` ist kein Ersatz für ``and``! Es ist semantisch der falsche Operator und es gibt Fälle bei denen das sogar ein Programmierfehler ist.

Der Kommentar ``# Load configuration`` in der `main()`-Funktion und die nachfolgende Ausgabe ``Configuration loaded`` stimmen nicht. In dem Stück Quelltext wird nichts geladen.

Die Konfiguration ist sowieso etwas schräg. Dieses Muster von `__init__()` und danach dann auf *Klassenebene* Defaultwerte ist ungewöhnlich. Und dann diese ganzen ``if``\s für die Defaultwerte und auch die ganzen ``if``/``else`` in `BackupItem` sind äusserst unschön. Warum nicht einfach ein Wörterbuch mit den Defaultwerten und das dann Kopieren (copy()-Methode) und mit den geladenen JSON-Daten aktualisieren (`update()`-Methode)? Schon ist die `Config`-Klasse überflüssig. Das gleiche kann man dann in/statt `BackupItem` machen. Und Anstelle der Klasse einfach eine die Attributzugriff auf ein Wörterbuch erlaubt. Da sich an einem `BackupItem` nach dem es erstellt wurde, nichts mehr ändert, böte sich `collections.namedtuple` an.

Bei `BackupItem` ist das ``_config = Config`` auf Klassenebene und das ``_config = config_data`` in der `__init__()` ziemlich sinnfrei. Das auf Klassenebene wird nicht benutzt — ich wüsste auch nicht wofür, und in der `__init__()` hätte man an der Stelle auch einfach das Argument gleich richtig benennen können.

Das ``try``/``except`` in `Config.__init__()` ist deutlich zu weit gefasst. Es gibt ``else``! Allerdings gehört diese Ausnahmebehandlung dort nicht hinein. An der Stelle das ganze Programm abzubrechen ist keine sinnvolle Fehlerbehandlung die eine `Config`-Klasse zu entscheiden hat. Das nimmt dem Aufrufer jegliche Möglichkeit darauf zu reagieren das die Datei nicht vorhanden ist. Zum Beispiel an anderer Stelle danach zu suchen. Üblich wäre beispielsweise erst in `/etc/programmname.json` und danach in `~/.programmname.json` zu schauen.

Des weiteren steht viel zu viel im ``with``-Block. Da braucht nur die erste Zeile drin zu stehen. Alles danach hat nichts mehr mit dem Dateiobjekt zu tun.

Das bei Kompressionsangaben ist das erstellen der Dateinamenserweiterung und des Dateimodus für TarFile sehr aufwändig geschrieben, obwohl beide Beziehungen einem einfachen Muster folgen für das man keine Fallunterscheidung braucht.

In der `main()` steht viel Code der durch kopieren und einfügen und geringfügigen Änderungen besteht. So etwas versucht ein Programmierer zu vermeiden. Da kann man die Unterschiede in eine Datenstruktur herausziehen und eine Schleife schreiben.

Die ganzen ``continue``-Anweisungen wären unnötig wenn man nicht immer ``if`` sondern auch ``elif`` verwenden würde.

Bei den regulären Ausdrücken ist ein Fehler: `file_type` enthält Punkte, die heben in regulären Ausdrücken eine besondere Bedeutung, also matcht das mehr als Du eigentlich möchtest. Ausserdem prüft `match()` zwar automatisch vom Anfang der Zeichenkette, aber nicht ob danach noch etwas kommt oder nicht. Auch hier kann wieder mehr erwischt werden, als Du eigentlich haben möchtest.

``pass`` macht in einem nicht-leeren Zweig keinen Sinn.

Ich lande dann als Zwischenergebnis, völlig ungetestet, bei dem hier:
[codebox=python file=x]#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import os
import re
import sys
import tarfile
from collections import defaultdict, namedtuple
from datetime import datetime
from itertools import islice

__version__ = 'v0.2.0'

BackupItem = namedtuple(
'BackupItem',
[
'source', 'destination',
'create_backup_day_of_week', 'create_backup_day_of_month',
'create_backup_day_of_year',
'daily_backups', 'weekly_backups', 'monthly_backups',
'yearly_backups',
'compression',
]
)


def create_backup(backup_item, archive_filename):
print('Creating backup... Filename:', archive_filename)
file_path = os.path.normpath(
os.path.join(backup_item.destination, archive_filename)
)
if os.path.exists(file_path):
print(' {0} already exists. Skipping...'.format(file_path))
else:
with tarfile.open(file_path, 'w:' + backup_item.compression) as tar:
tar.add(
backup_item.source,
arcname=os.path.basename(backup_item.source)
)


def read_config(config_path):

with open(config_path) as config_file:
data = json.load(config_file)

result = {
'create_backup_day_of_week': 0,
'create_backup_day_of_month': 0,
'create_backup_day_of_year': 0,

'daily_backups': 7,
'weekly_backups': 4,
'monthly_backups': 6,
'yearly_backups': 4,

'compression': 'gz',

'backup_items': (),
}
result.update(data.get('default', {}))

backup_items = list()
for backup in result['backup_items']:
if (
os.path.isdir(backup['source'])
and os.path.isdir(backup['destination'])
):
backup_item_data = dict.fromkeys(BackupItem._fields)
backup_item_data.update(backup)
backup_items.append(BackupItem._make(backup_item_data))
result['backup_items'] = backup_items

return result


def main():
print(os.path.basename(sys.argv[0]), __version__)

# Load configuration
if len(sys.argv) < 2:
print('No configfile stated, using default (./config.json)')
config_path = './config.json'
else:
if not os.path.isfile(sys.argv[1]):
print('Stated configfile not found, aborting...')
exit()
else:
config_path = sys.argv[1]

try:
config_data = read_config(config_path)
except FileNotFoundError:
print('Could not find config file!')
exit()

print('Configuration loaded:', config_path)

now = datetime.now()

# Start rotation for every backup item
for backup_item in config_data['backup_items']:
print('\nStarting backup routine for {0}...'.format(backup_item.source))

timetuple = now.timetuple()
for should_backup, current_day, backup_day, name_part in [
(backup_item.daily_backups, None, None, 'DAILY'),
(
backup_item.weekly_backups,
timetuple.tm_wday,
backup_item.create_backup_day_of_week,
'WEEKLY',
),
(
backup_item.monthly_backups,
timetuple.tm_mday,
backup_item.create_backup_day_of_month,
'MONTHLY',
),
(
backup_item.yearly_backups,
timetuple.tm_yday,
backup_item.create_backup_day_of_year,
'YEARLY',
),
]:
if should_backup and current_day == backup_day:
create_backup(
backup_item,
'{0:%Y-%m-%d}-{1}.tar.{2}'.format(
now, name_part, backup_item.compression
)
)

# Check for old backups
backup_filename_re = re.compile(
r'\d{4}-\d{2}-\d{2}-(?P<period_name>DAILY|WEEKLY|MONTHLY|YEARLY)'
r'\.tar\.{0}$'.format(re.escape(backup_item.compression))
)
period_name2filenames = defaultdict(list)
for filename in os.listdir(backup_item.destination):
match = backup_filename_re.match(filename)
if match:
period_name2filenames[match.get('period_name')].append(filename)

# Check for overhang in old backups and delete it
for period_name, max_backups in [
('DAILY', backup_item.daily_backups),
('WEEKLY', backup_item.weekly_backups),
('MONTHLY', backup_item.monthly_backups),
('YEARLY', backup_item.yearly_backups),
]:
filenames = period_name2filenames[period_name]
overhang = len(filenames) - max_backups
if overhang > 0:
filenames.sort(key=os.path.getctime)
print(
'Overhang found ({0} backups).'
' Deleting {1} old backup(s)...'.format(
period_name.lower(), overhang
)
)
for filename in islice(filenames, overhang):
print('Deleting {0}...'.format(os.path.basename(filename)))
os.remove(filename)

print('\nBackup rotation finished.')


if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print('Aborting...')[/code]

Ist es wirklich eine gute Idee eine Sicherung ”lautlos” nicht durchzuführen wenn es das Zielverzeichnis nicht gibt? Da ist dann schnell mal die NFS- oder SMB-Freigabe zum NAS nicht eingehängt und monatelang fällt keinem auf, dass die Sicherung deshalb nicht durchgeführt wird.

Die Hauptfunktion ist für meinen Geschmack immer noch zu lang. Das kann man sinnvoll thematisch in weitere Funktionen aufteilen. Mindestens sichern und löschen von alten Sicherungen. Mit ein bisschen OOP kann man da vielleicht auch einiges an Entscheidungen besser Kapseln.

Die `print()`-Ausgaben würde ich noch durch `logging` ersetzen. Dann kann man die unterdrücken und auch verschiedene Stufen von ”Gesprächigkeit” wählbar machen.
DerAtrox
User
Beiträge: 5
Registriert: Sonntag 8. Mai 2016, 20:36
Kontaktdaten:

Danke für deine Antwort und die ganzen Tips! ^^

Hatte den Code gestern mehr so QAD hingeschrieben, weil ich den Skript dringend gebraucht habe.

Die Codestruktur die ich verwendet habe, habe ich bei anderen Programmiersprachen (hauptsächlich C# und PHP) so angewöhnt und wollte die jetzt auch hier mehr oder weniger anwenden. Finde Objektorientierung für Configs usw. meist echt schöner.

Feste Pfade wie /etc/programmconfig.json möchte ich übrigens nicht unbedingt einbauen, der Skript soll auch auf Windows funktionieren. ^^
BlackJack

@DerAtrox: Wenn Du Objektorientierung schöner findest, dann wende die doch auch an. Dein ursprünglicher Quelltext hat zwar mehr ``class``-Anweisungen, aber definitiv nicht mehr objektorientierte Programmierung. `Config` ist semantisch keine Klasse, genausowenig wie `BackupItem` eine ist. Nur weil man eine einfache Funktion unnötigerweise als Klasse aufplustert (`Config`), oder einen Verbunddatentyp definiert (`BackupItem`), ist das kein objektorientiertes Programm. Dein Programm hat also letztlich keine einzige *echte* Klasse. Es enthält aber sehr viel unnötige Schreibarbeit/Copy & Paste ohne Mehrwert, was die Fehleranfälligkeit erhöht.

Was die Codestruktur angeht: Als Klassenattribute machen in der Regel nur Konstanten Sinn, und die werden per Konvention komplett in Grossbuchstaben geschrieben. Ausserdem sollte man die am Anfang der Klasse definieren. Nach den Methoden ist sehr ungewöhnlich, dass erwartet kein Python-Programmierer wenn er Quelltext liest. Die `main()` steht üblicherweise auch als letztes. Insgesamt kann man sagen das alles was benutzt wird, weiter vorne im Quelltext definiert sein sollte. Das kann man in der Regel konsequent so machen, während es umgekehrt manchmal nicht geht, weil der Quelltext ja auch von oben nach unten ausgeführt wird, und dafür auch alles was benutzt wird, vorher, also weiter oben, definiert sein muss.

Man sollte vorsichtig sein Ideen und Strukturen aus anderen Programmiersprachen zu übernehmen. Python ist sehr dynamisch während C# eine statische Programmiersprache ist. PHP und Objektorientierung ist eine komische Argumentationsgrundlage. Ja bei PHP5 hat sich wieder ein bisschen was getan in der Richtung, aber das ist IMHO immer noch eine grundsätzlich schlechte Sprache auf die irgendwann OOP notdürftig draufgetackert wurde.

Nicht alles was in der einen Sprache bei Idiomen und Strukturierung Sinn macht, tut das auch in der anderen. Deshalb hat man ja verschiedene Sprachen. Wenn man in jeder alles auf die gleiche Weise lösen würde, bräuchte man nur eine Sprache. Man sollte die Idiome und Vorteile der jeweils verwendeten Sprache nutzen, und nicht X in Y-Quelltext pressen.

Es ging bei der Kritik nicht um den konkreten Pfad, sondern dass das die falsche Stelle ist a) die Ausnahme zu behandeln und b) den Prozess einfach komplett abzubrechen. Unter Windows möchte man dann vielleicht einen anderen Satz von Pfaden durchprobieren, denn auch dort gibt es ja Verzeichnisse für global installierte Programme und Benutzerverzeichnisse und ein aktuelles Arbeitsverzeichnis. Es geht darum, dass die Behandlung der Ausnahme in Deinem Programm unnötig einschränkt. Man könnte beispielsweise auch keine Funktion schreiben, die mehrere Konfigurationsdateien einliesst und deren Inhalt überprüft (illegale Kompression, nicht vorhandene oder angegebene Quellen/Ziele, Ziele die innerhalb der Quelle liegen, …), denn die erste Konfigurationsdatei die nicht gefunden wird, bricht das Programm komplett ab.

Links auf Github die immer auf den *aktuellen* Code zeigen sind übrigens keine gute Idee. Du hast jetzt im ersten Beitrag einen Link auf Quelltext der nicht mehr der ist, auf den ich geantwortet habe und der auch nicht mehr das Problem enthält nach dem Du gefragt hast.
DerAtrox
User
Beiträge: 5
Registriert: Sonntag 8. Mai 2016, 20:36
Kontaktdaten:

Ja, verstehe jetzt was du meinst. Bin von C# gewohnt erstmal für jede kleine Konfig usw. eine eigene Klasse zu erstellen, vor allem weil beispielsweise Json.NET (Json Library für C#) meist eine eigene Klasse für Konfigs usw. benötigt.
Dazu kommt dann noch, dass ich echt noch nicht so viel Erfahrung mit Python habe. ^^ Da sind mir Möglichkeiten, wie mehrere Variablen in einer for-Schleife einfach noch unbekannt.

Danke nochmal für deine ganzen Tipps! Habe mittlerweile schon einiges ein bisschen überarbeitet. :)

Ach oh und die Git Links... Ja ich weiß, da hab ich gestern etwas gepennt. War da schon halb im Bett. :lol:
Antworten