198 lines
5.2 KiB
Go
198 lines
5.2 KiB
Go
|
// Copyright 2013 Dmitry Chestnykh. All rights reserved.
|
||
|
// Use of this source code is governed by a BSD-style
|
||
|
// license that can be found in the LICENSE file.
|
||
|
|
||
|
// Package safefile implements safe "atomic" saving of files.
|
||
|
//
|
||
|
// Instead of truncating and overwriting the destination file, it creates a
|
||
|
// temporary file in the same directory, writes to it, and then renames the
|
||
|
// temporary file to the original name when calling Commit.
|
||
|
//
|
||
|
// Example:
|
||
|
//
|
||
|
// f, err := safefile.Create("/home/ken/report.txt", 0644)
|
||
|
// if err != nil {
|
||
|
// // ...
|
||
|
// }
|
||
|
// // Created temporary file /home/ken/sf-ppcyksu5hyw2mfec.tmp
|
||
|
//
|
||
|
// defer f.Close()
|
||
|
//
|
||
|
// _, err = io.WriteString(f, "Hello world")
|
||
|
// if err != nil {
|
||
|
// // ...
|
||
|
// }
|
||
|
// // Wrote "Hello world" to /home/ken/sf-ppcyksu5hyw2mfec.tmp
|
||
|
//
|
||
|
// err = f.Commit()
|
||
|
// if err != nil {
|
||
|
// // ...
|
||
|
// }
|
||
|
// // Renamed /home/ken/sf-ppcyksu5hyw2mfec.tmp to /home/ken/report.txt
|
||
|
//
|
||
|
package safefile
|
||
|
|
||
|
import (
|
||
|
"crypto/rand"
|
||
|
"encoding/base32"
|
||
|
"errors"
|
||
|
"io"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// ErrAlreadyCommitted error is returned when calling Commit on a file that
|
||
|
// has been already successfully committed.
|
||
|
var ErrAlreadyCommitted = errors.New("file already committed")
|
||
|
|
||
|
type File struct {
|
||
|
*os.File
|
||
|
origName string
|
||
|
closeFunc func(*File) error
|
||
|
isClosed bool // if true, temporary file has been closed, but not renamed
|
||
|
isCommitted bool // if true, the file has been successfully committed
|
||
|
}
|
||
|
|
||
|
func makeTempName(origname, prefix string) (tempname string, err error) {
|
||
|
origname = filepath.Clean(origname)
|
||
|
if len(origname) == 0 || origname[len(origname)-1] == filepath.Separator {
|
||
|
return "", os.ErrInvalid
|
||
|
}
|
||
|
// Generate 10 random bytes.
|
||
|
// This gives 80 bits of entropy, good enough
|
||
|
// for making temporary file name unpredictable.
|
||
|
var rnd [10]byte
|
||
|
if _, err := rand.Read(rnd[:]); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
name := prefix + "-" + strings.ToLower(base32.StdEncoding.EncodeToString(rnd[:])) + ".tmp"
|
||
|
return filepath.Join(filepath.Dir(origname), name), nil
|
||
|
}
|
||
|
|
||
|
// Create creates a temporary file in the same directory as filename,
|
||
|
// which will be renamed to the given filename when calling Commit.
|
||
|
func Create(filename string, perm os.FileMode) (*File, error) {
|
||
|
for {
|
||
|
tempname, err := makeTempName(filename, "sf")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
f, err := os.OpenFile(tempname, os.O_RDWR|os.O_CREATE|os.O_EXCL, perm)
|
||
|
if err != nil {
|
||
|
if os.IsExist(err) {
|
||
|
continue
|
||
|
}
|
||
|
return nil, err
|
||
|
}
|
||
|
return &File{
|
||
|
File: f,
|
||
|
origName: filename,
|
||
|
closeFunc: closeUncommitted,
|
||
|
}, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// OrigName returns the original filename given to Create.
|
||
|
func (f *File) OrigName() string {
|
||
|
return f.origName
|
||
|
}
|
||
|
|
||
|
// Close closes temporary file and removes it.
|
||
|
// If the file has been committed, Close is no-op.
|
||
|
func (f *File) Close() error {
|
||
|
return f.closeFunc(f)
|
||
|
}
|
||
|
|
||
|
func closeUncommitted(f *File) error {
|
||
|
err0 := f.File.Close()
|
||
|
err1 := os.Remove(f.Name())
|
||
|
f.closeFunc = closeAgainError
|
||
|
if err0 != nil {
|
||
|
return err0
|
||
|
}
|
||
|
return err1
|
||
|
}
|
||
|
|
||
|
func closeAfterFailedRename(f *File) error {
|
||
|
// Remove temporary file.
|
||
|
//
|
||
|
// The note from Commit function applies here too, as we may be
|
||
|
// removing a different file. However, since we rely on our temporary
|
||
|
// names being unpredictable, this should not be a concern.
|
||
|
f.closeFunc = closeAgainError
|
||
|
return os.Remove(f.Name())
|
||
|
}
|
||
|
|
||
|
func closeCommitted(f *File) error {
|
||
|
// noop
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func closeAgainError(f *File) error {
|
||
|
return os.ErrInvalid
|
||
|
}
|
||
|
|
||
|
// Commit safely commits data into the original file by syncing temporary
|
||
|
// file to disk, closing it and renaming to the original file name.
|
||
|
//
|
||
|
// In case of success, the temporary file is closed and no longer exists
|
||
|
// on disk. It is safe to call Close after Commit: the operation will do
|
||
|
// nothing.
|
||
|
//
|
||
|
// In case of error, the temporary file is still opened and exists on disk;
|
||
|
// it must be closed by callers by calling Close or by trying to commit again.
|
||
|
|
||
|
// Note that when trying to Commit again after a failed Commit when the file
|
||
|
// has been closed, but not renamed to its original name (the new commit will
|
||
|
// try again to rename it), safefile cannot guarantee that the temporary file
|
||
|
// has not been changed, or that it is the same temporary file we were dealing
|
||
|
// with. However, since the temporary name is unpredictable, it is unlikely
|
||
|
// that this happened accidentally. If complete atomicity is needed, do not
|
||
|
// Commit again after error, write the file again.
|
||
|
func (f *File) Commit() error {
|
||
|
if f.isCommitted {
|
||
|
return ErrAlreadyCommitted
|
||
|
}
|
||
|
if !f.isClosed {
|
||
|
// Sync to disk.
|
||
|
err := f.Sync()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
// Close underlying os.File.
|
||
|
err = f.File.Close()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
f.isClosed = true
|
||
|
}
|
||
|
// Rename.
|
||
|
err := rename(f.Name(), f.origName)
|
||
|
if err != nil {
|
||
|
f.closeFunc = closeAfterFailedRename
|
||
|
return err
|
||
|
}
|
||
|
f.closeFunc = closeCommitted
|
||
|
f.isCommitted = true
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// WriteFile is a safe analog of ioutil.WriteFile.
|
||
|
func WriteFile(filename string, data []byte, perm os.FileMode) error {
|
||
|
f, err := Create(filename, perm)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer f.Close()
|
||
|
n, err := f.Write(data)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if err == nil && n < len(data) {
|
||
|
err = io.ErrShortWrite
|
||
|
return err
|
||
|
}
|
||
|
return f.Commit()
|
||
|
}
|