shutil.copytree() - PermissionError

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
Kniffte
User
Beiträge: 64
Registriert: Dienstag 27. September 2016, 11:05

Hallo Zusammen,

Folgendes Programm soll einen Ordner per shutil.copytree() in zwei verschiedene Zielordener kopieren.
Wenn im Zielordner bereits ein Ordner mit gleichem Namen vorhanden, dann soll dieser vorher als .zip archiviert und
in den Ordner "archive" verschoben werden.
Problem ist, dass ab und zu ein PermissionError bei shutil.copytree() auftritt.

- System Windows 7 64bit
- Python 3.5
- bin als Admin angemeldet
- Kommandozeile wird als Admin ausgeführt

Wie kann den ein Zugriff auf einen Ordner verweigert werden, der eigentlich zuvor gelöscht wurde und mit copytree() erst wieder erzeugt wird?
Hab ich was übersehen?

Code: Alles auswählen

import tkinter as tk
from tkinter import ttk, messagebox, filedialog

import os
import shutil
from datetime import datetime

class MainApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title('Backup tool')
        self.main_frame = MainFrame(self).pack(side='top', fill='both', expand=True)

    def start(self):
        self.mainloop()


class MainFrame(ttk.Frame):
    def __init__(self, parent):
        super().__init__(parent)

        self.source = tk.StringVar()
        self.dst = tk.StringVar()
        self.dst_2 = tk.StringVar()

        # self.source.set('C:\\Users\\ssb\\Desktop\\copytree\\prepro5')
        # self.dst.set('C:\\Users\\ssb\\Desktop\\copytree2')
        # self.dst_2.set('C:\\Users\\ssb\\Desktop\\copytree3')

        self.source_entry = ttk.Entry(self, width=50, textvariable=self.source)
        self.dst_entry = ttk.Entry(self, width=50, textvariable=self.dst)
        self.dst_2_entry = ttk.Entry(self, width=50, textvariable=self.dst_2)
        self.source_button = ttk.Button(self,
                                        text='Source',
                                        command=lambda: self._choose_dir(self.source))
        self.dst_button = ttk.Button(self,
                                     text='Destination 1',
                                     command=lambda: self._choose_dir(self.dst))
        self.dst_2_button = ttk.Button(self,
                                       text='Destination 2',
                                       command=lambda: self._choose_dir(self.dst_2))
        self.backup_button = ttk.Button(self, text='Backup', command=self._backup)

        self.source_entry.grid(row=0, column=0, sticky='nswe')
        self.dst_entry.grid(row=1, column=0, sticky='nswe')
        self.dst_2_entry.grid(row=2, column=0, sticky='nswe')
        self.source_button.grid(row=0, column=1, sticky='nswe')
        self.dst_button.grid(row=1, column=1, sticky='nswe')
        self.dst_2_button.grid(row=2, column=1, sticky='nswe')
        self.backup_button.grid(row=3, column=0, columnspan=2, sticky='nswe')

    def _choose_dir(self, tkvar_to_set):
        dir_path = filedialog.askdirectory()
        tkvar_to_set.set(dir_path)

    def _backup(self):
        if os.path.isdir(self.source.get()):
            backup_status = []
            for index, folder in enumerate([self.dst.get(), self.dst_2.get()]):
                # check if destination path is valid directory
                if os.path.isdir(folder):
                    dst_folder = os.path.join(folder, os.path.basename(self.source.get()))
                    # check if destination folder exists and archive it
                    if os.path.isdir(dst_folder):
                        self._archive_old(dst_folder)
                    shutil.copytree(self.source.get(), dst_folder)
                    backup_status.append('Source backup saved to destination {0}: {1}'.format(index + 1, dst_folder))
                else:
                    backup_status.append('Destination {0} not defined'.format(index + 1))
            messagebox.showinfo('Info...', '{0}\n{1}'.format(backup_status[0], backup_status[1]))
        else:
            messagebox.showerror('Error...', 'Please define a valid source directory and try again')

    def _archive_old(self, dst_folder):
        file_name = '{}_{}'.format(os.path.basename(dst_folder),
                                   datetime.today().strftime('%Y%m%d'))
        archive_path = os.path.join(os.path.dirname(dst_folder), 'archive', file_name)
        shutil.make_archive(archive_path, 'zip',
                            os.path.dirname(dst_folder),
                            os.path.basename(dst_folder))
        shutil.rmtree(dst_folder)

if __name__ == '__main__':
    MainApp().start()

Exception:

Code: Alles auswählen

Traceback (most recent call last):
  File "C:\Users\ssb\AppData\Local\Continuum\Miniconda3\envs\msp_py35\lib\tkinter\__init__.py", line 1558, in __call__
    return self.func(*args)
  File "backup.py", line 70, in _backup
    shutil.copytree(self.source.get(), dst_folder)
  File "C:\Users\ssb\AppData\Local\Continuum\Miniconda3\envs\msp_py35\lib\shutil.py", line 315, in copytree
    os.makedirs(dst)
  File "C:\Users\ssb\AppData\Local\Continuum\Miniconda3\envs\msp_py35\lib\os.py", line 241, in makedirs
    mkdir(name, mode)
PermissionError: [WinError 5] Zugriff verweigert: 'C:\\Users\\ssb\\Desktop\\copytree2\\prepro5'
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Willkommen in der wundervollen Welt von Windows, in der File-Zugriff exklusiv ist, und Hintergrundprozesse jederzeit den Dateizugriff beeeintraechigen koennen. ZB Virenkiller oder Such-Indexer.

Das wirst du nur auf eine von zwei Arten loesen koennen:

- bau dir dein copytree selbst, mit mehr Robustheit.
- fang den Fehler ab & mach die Arbeit von vorne.

Was du waehlst ist eine Abwaegung von Programmkomplexitaet vs. Zeitverhalten. Ich wuerde mit dem zweiten anfangen, es sei denn du kannst schon absehen, dass das nicht geht.
Kniffte
User
Beiträge: 64
Registriert: Dienstag 27. September 2016, 11:05

@__deets__:
Danke für die schnelle Anwort.
Ich würde jetzt das shutil.copytree() wie folgt behandeln:

Code: Alles auswählen

test_success = False
counter = 0
while not test_success:
    test_success = self._try_to_copy(dst_folder)
    counter += 1
print('copytree() executed {} times'.format(counter))
Wenn ich copytree() nun mehrmals nacheinander ausführe, könnten dann noch weitere Probleme auftreten?
Da ich ja Daten sichern will, wäre es Mist, wenn z.b. die Datei dabei nicht korrekt kopiert bzw. beschädigt werden.


EDIT:
Beim Einfügen eines time.sleep(0.5) zwischen shutil.rmtree() und shutil.copytree() funktioniert das Kopieren der
Verzeichnisse nach dem ersten Versuch. Also scheint das Problem für Windows beim direkten Nacheinanderausführen
von rmtree() & copytree() zu liegen.
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

@Kniffte: Du könntest das folgende versuchen:

Code: Alles auswählen

locations = []  # hier stehen die Verzeichnispfade drin
while locations:
    location = locations.pop(0)  # oder eine deque verwenden
    try:
        copy_location(location)
    except PermissonError:
        locations.append(location)
(Eine deque ist sicher schöner, aber IO ist sowieso lahm, da fällt das nicht ins Gewicht.)
Gegen dauerhafte PermissonErrors brauchst Du natürlich noch eine weitere Abbruchbedingung.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Naja, das es jetzt funktioniert haengt halt damit zusammen, dass die Aenderungen am Dateisystem dann irgendwann von den entsprechenden Prozessen erfasst & sie damit die Dateien/Verzeichnisse losgelassen haben.

Deterministisch funktioniert das aber immer noch nicht, kann auch mal laenger dauern als deine 0.5 Sekunden. Auf eine solche retry-Geschichte wuerde ich trotzdem nicht verzichten.
Kniffte
User
Beiträge: 64
Registriert: Dienstag 27. September 2016, 11:05

Danke euch beiden.
Hab noch eine Abbruchbedingung eingefügt.
Antworten