2009-11-28 29 views
15

Immagina di avere una libreria per lavorare con una sorta di file XML o di configurazione. La libreria legge l'intero file in memoria e fornisce metodi per modificare il contenuto. Quando hai terminato di manipolare il contenuto, puoi chiamare lo write per salvare il contenuto sul file. La domanda è come farlo in modo sicuro.Come scrivere in sicurezza su un file?

Sovrascrivere il file esistente (iniziando a scrivere sul file originale) non è ovviamente sicuro. Se il metodo write fallisce prima che sia terminato, si finisce con un file scritto a metà e si perdono i dati.

Una soluzione migliore sarebbe quella di scrivere in un file temporaneo da qualche parte, e quando il metodo write è terminata, è copiare il file temporaneo al file originale.

Ora, se la copia non riesce in qualche modo, è ancora stato salvato correttamente i dati nel file temporaneo. E se la copia riesce, è possibile rimuovere il file temporaneo.

Su sistemi POSIX, suppongo che sia possibile utilizzare la chiamata di sistema rename che è un'operazione atomica. Ma come faresti meglio su un sistema Windows? In particolare, come gestirli al meglio usando Python?

Inoltre, esiste un altro schema per scrivere in modo sicuro sui file?

+0

Perché copiare? Perché non rinominare? –

risposta

14

Se viene visualizzata la documentazione di Python, viene chiaramente indicato che os.rename() è un'operazione atomica. Quindi nel tuo caso, scrivere i dati su un file temporaneo e rinominarlo nel file originale sarebbe abbastanza sicuro.

Un altro modo potrebbe funzionare in questo modo:

  • lascia file originale sia abc.xml
  • creare abc.xml.tmp e scrivere nuovi dati su di esso
  • rinomina abc.xml a abc.xml .bak
  • rinomina abc.xml.tmp a abc.xml
  • dopo il nuovo abc.xml sia correttamente messo in atto, togliere abc.xml.bak

Come potete vedere avete il file abc.xml.bak with you che è possibile utilizzare per ripristinare se ci sono problemi relativi al file tmp e di copiarlo.

+0

Questo è simile alla risposta di S.Lott con l'aggiunta di eliminare il file di backup. Sembra che sia il modo migliore per farlo. Grazie. –

+0

In realtà ho visto questa implementazione nel modo in cui ZODB (Zope Object Database) esegue l'impacchettamento del suo file di database (Data.fs), ovvero la rimozione dello spazio inutilizzato delle transazioni precedenti dal file di database. Il codice è un normale codice Python, l'imballaggio viene eseguito in un file temporaneo e quindi vengono eseguiti i passaggi simili a quelli sopra riportati. ZODB è in circolazione da così tanti anni e funziona bene su piattaforme Windows e POSIX, quindi credo che questo approccio dovrebbe funzionare. –

+3

Python non può imporre la garanzia che rinominare sia atomico. Per quanto ne so, è solo chiamando la chiamata di sistema del sistema operativo. La procedura che date funziona bene, però. –

4

In Win API ho trovato una funzione piuttosto piacevole ReplaceFile che fa ciò che suggerisce il nome anche con il backup opzionale. È sempre disponibile la combinazione DeleteFile, MoveFile.

In generale quello che vuoi fare è davvero buono. E non riesco a pensare a uno schema di scrittura migliore.

+3

Sarebbe ancora meglio se lo illustrassi con il codice Python appropriato che chiama l'API della libreria MS. – RedGlyph

+1

Non ho realizzato che ReplaceFile esistesse. Leggendo i documenti, sembra fare molto di più che rinominare. Mantiene molti degli attributi del file sostituito, quindi sembra progettato specificamente per questo scopo. –

3

Una soluzione semplicistica. Utilizzare tempfile per creare un file temporaneo e se la scrittura ha esito positivo, basta rinominare il file nel file di configurazione originale.

Per bloccare un file, vedere portalocker.

+1

Se tempfile viene creato in un altro file system rispetto a quello di destinazione, quindi la rinomina finale non funzionerà o non sarà atomica. – tzot

+0

Ma a meno che il cambio di nome non sia atomico, rischio di perdere dati, giusto? –

4

La soluzione standard è questa.

  1. Scrivere un nuovo file con un nome simile. X.ext # per esempio.

  2. Quando quel file è stato chiuso (e forse anche letto e checksum), allora due due nomi.

    • X.ext (l'originale) per X.ext ~

    • X.ext # (quello nuovo) a X.ext

  3. (Solo per il folle paranoici) chiama la funzione di sincronizzazione del sistema operativo per forzare le scritture del buffer sporche.

In nessun momento è tutto perso o corruttibile. L'unico problema può capitare durante i nomi. Ma non hai perso nulla o non hai corrotto nulla. L'originale è recuperabile fino alla rinomina finale.

+0

Ma se non si rinomina due volte (creando un backup come hai fatto) si rischia di perdere dati se la rinomina non è atomica, giusto? –

+1

Rinomina * è * atomico in molti sistemi operativi. Tuttavia, il backup è più importante del tormentare a mano l'atomicità dell'operazione. Ricorda: le probabilità di un crash (tranne che in Windows) sono molto piccole. Le probabilità di un crash nel bel mezzo di un rename (che è solo un paio di istruzioni più una sincronizzazione è molto, molto piccola. –

11

Se si vuole essere POSIXly corretta e salvare si deve:

  1. Scrivi file temporaneo
  2. Flush e fsync il file (o fdatasync)
  3. Rinomina il file originale

Si noti che chiamare fsync ha effetti imprevedibili sulle prestazioni - Linux su ext3 potrebbe bloccarsi per l'intero numero di I/O su disco di secondi, a seconda di altri o ottimo I/O.

Si noti che rename è non un'operazione atomica in POSIX, almeno non in relazione ai dati di file come previsto. Tuttavia, la maggior parte dei sistemi operativi e dei filesystem funzionerà in questo modo. Ma sembra che tu abbia perso l'enorme discussione di Linux su Ext4 e le garanzie del filesystem sull'atomicità. Non so esattamente dove collegare, ma qui è un inizio: ext4 and data loss.

Si noti tuttavia che su molti sistemi, la rinomina sarà la più sicura nella pratica come previsto. Tuttavia non è possibile ottenere entrambe le prestazioni e l'affidabilità su tutte le confiugrazioni di Linux possibili!

Con una scrittura su un file temporaneo, quindi una rinomina del file temporaneo, ci si aspetterebbe che le operazioni siano dipendenti e che verrebbero eseguite in ordine.

Il problema tuttavia è che la maggior parte, se non tutti i file system, separano metadati e dati. Un rinominare è solo metadata. Può sembrare orribile per te, ma i filesystem valorizzano i metadati sui dati (ad esempio, Journaling in HFS + o Ext3,4)! Il motivo è che i metadati sono più leggeri e se i metadati sono corrotti, l'intero filesystem è corrotto - il filesystem deve ovviamente preservare se stesso, quindi conservare i dati dell'utente, in questo ordine.

Ext4 ha interrotto l'attesa di rename quando è stata rilasciata, tuttavia sono state aggiunte euristiche per risolverlo. Il problema è non un nome non riuscito, ma una rinomina riuscita. Ext4 potrebbe registrare con successo il cambio di nome, ma non scrivere i dati del file se un incidente si verifica poco dopo. Il risultato è quindi un file di lunghezza 0 e né dati originali né nuovi.

Quindi, in breve, POSIX non offre tale garanzia. Leggi l'articolo Ext4 collegato per maggiori informazioni!

+0

Forse ho frainteso. Ma se si rinomina su un sistema POSIX, non si è certi che il la destinazione non è modificata se la rinomina non ha esito positivo? "Se la funzione rename() fallisce per qualsiasi motivo diverso da [EIO], qualsiasi file nominato da new non verrà modificato." Suppongo che possa comunque lasciare dati corrotti. –

+0

il problema è rinominare con successo e che solo un rinominare non garantisce l'atomicità dell'intera operazione. – u0b34a0f6ae

+1

Quello che intendi è che rename() non è checkpoint. Certamente è atomico, vedi: http://www.opengroup.org/onlinepubs /009695399/functions/rename.html – geocar

1

Per suggerimento di RedGlyph, ho aggiunto un'implementazione di ReplaceFile che utilizza i ctype per accedere alle API di Windows. L'ho aggiunto per la prima volta a jaraco.windows.api.filesystem.

ReplaceFile = windll.kernel32.ReplaceFileW 
ReplaceFile.restype = BOOL 
ReplaceFile.argtypes = [ 
    LPWSTR, 
    LPWSTR, 
    LPWSTR, 
    DWORD, 
    LPVOID, 
    LPVOID, 
    ] 

REPLACEFILE_WRITE_THROUGH = 0x1 
REPLACEFILE_IGNORE_MERGE_ERRORS = 0x2 
REPLACEFILE_IGNORE_ACL_ERRORS = 0x4 

Ho quindi testato il comportamento utilizzando questo script.

from jaraco.windows.api.filesystem import ReplaceFile 
import os 

open('orig-file', 'w').write('some content') 
open('replacing-file', 'w').write('new content') 
ReplaceFile('orig-file', 'replacing-file', 'orig-backup', 0, 0, 0) 
assert open('orig-file').read() == 'new content' 
assert open('orig-backup').read() == 'some content' 
assert not os.path.exists('replacing-file') 

Mentre questo funziona solo in Windows, sembra avere un sacco di belle caratteristiche che altro sostituire le routine mancherebbero. Vedere lo API docs per i dettagli.

0

È possibile utilizzare il modulo fileinput per gestire il back-up e sul posto di scrittura per voi:

import fileinput 
for line in fileinput.input(filename,inplace=True, backup='.bak'): 
    # inplace=True causes the original file to be moved to a backup 
    # standard output is redirected to the original file. 
    # backup='.bak' specifies the extension for the backup file. 

    # manipulate line 
    newline=process(line) 
    print(newline) 

Se avete bisogno di leggere l'intero contenuto prima di poter scrivere il ritorno a capo del, allora si può farlo prima, quindi stampare interi nuovi contenuti con

newcontents=process(contents) 
for line in fileinput.input(filename,inplace=True, backup='.bak'): 
    print(newcontents) 
    break 

Se lo script termina bruscamente, si avrà ancora il backup.

3

Ora c'è un Python codificato e puro, e oserei dire una soluzione Pythonic a questo nello boltons utility library: boltons.fileutils.atomic_save.

Proprio pip install boltons, quindi:

from boltons.fileutils import atomic_save 

with atomic_save('/path/to/file.txt') as f: 
    f.write('this will only overwrite if it succeeds!\n') 

Ci sono un sacco di opzioni pratiche, all well-documented. Piena rivelazione, sono l'autore di boltons, ma questa parte particolare è stata costruita con un grande aiuto da parte della comunità. Non esitare a drop a note se qualcosa non è chiaro!