github.com/richardwilkes/toolbox@v1.121.0/xio/fs/safe/file.go (about)

     1  // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the Mozilla Public
     4  // License, version 2.0. If a copy of the MPL was not distributed with
     5  // this file, You can obtain one at http://mozilla.org/MPL/2.0/.
     6  //
     7  // This Source Code Form is "Incompatible With Secondary Licenses", as
     8  // defined by the Mozilla Public License, version 2.0.
     9  
    10  // Package safe provides safe, atomic saving of files.
    11  package safe
    12  
    13  import (
    14  	"os"
    15  	"path/filepath"
    16  
    17  	"github.com/richardwilkes/toolbox/xio/fs/internal"
    18  )
    19  
    20  // File provides safe, atomic saving of files. Instead of truncating and overwriting the destination file, it creates a
    21  // temporary file in the same directory, writes to it, and then renames the temporary file to the original name when
    22  // Commit() is called. If Close() is called without calling Commit(), or the Commit() fails, then the original file is
    23  // left untouched.
    24  type File struct {
    25  	*os.File
    26  	originalName string
    27  	committed    bool
    28  	closed       bool
    29  }
    30  
    31  // Create creates a temporary file in the same directory as filename, which will be renamed to the given filename when
    32  // calling Commit.
    33  func Create(filename string) (*File, error) {
    34  	return CreateWithMode(filename, 0o644)
    35  }
    36  
    37  // CreateWithMode creates a temporary file in the same directory as filename, which will be renamed to the given
    38  // filename when calling Commit.
    39  func CreateWithMode(filename string, mode os.FileMode) (*File, error) {
    40  	filename = filepath.Clean(filename)
    41  	if filename == "" || filename[len(filename)-1] == filepath.Separator {
    42  		return nil, os.ErrInvalid
    43  	}
    44  	f, err := internal.CreateTemp(filepath.Dir(filename), "safe", mode)
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  	return &File{
    49  		File:         f,
    50  		originalName: filename,
    51  	}, nil
    52  }
    53  
    54  // OriginalName returns the original filename passed into Create().
    55  func (f *File) OriginalName() string {
    56  	return f.originalName
    57  }
    58  
    59  // Commit the data into the original file and remove the temporary file from disk. Close() may still be called, but will
    60  // do nothing.
    61  func (f *File) Commit() error {
    62  	if f.committed {
    63  		return nil
    64  	}
    65  	if f.closed {
    66  		return os.ErrInvalid
    67  	}
    68  	f.committed = true
    69  	f.closed = true
    70  	var err error
    71  	name := f.Name()
    72  	defer func() {
    73  		if err != nil {
    74  			_ = os.Remove(name) //nolint:errcheck // no need to report this error, too
    75  		}
    76  	}()
    77  	if err = f.File.Close(); err != nil {
    78  		return err
    79  	}
    80  	err = os.Rename(name, f.originalName)
    81  	return err
    82  }
    83  
    84  // Close the temporary file and remove it, if it hasn't already been committed. If it has been committed, nothing
    85  // happens.
    86  func (f *File) Close() error {
    87  	if f.committed {
    88  		return nil
    89  	}
    90  	if f.closed {
    91  		return os.ErrInvalid
    92  	}
    93  	f.closed = true
    94  	err := f.File.Close()
    95  	if removeErr := os.Remove(f.Name()); removeErr != nil && err == nil {
    96  		err = removeErr
    97  	}
    98  	return err
    99  }