All checks were successful
		
		
	
	Test / Create distribution (push) Successful in 33s
				
			Test / Sandbox (push) Successful in 2m15s
				
			Test / Hakurei (push) Successful in 3m11s
				
			Test / Hpkg (push) Successful in 4m0s
				
			Test / Sandbox (race detector) (push) Successful in 4m4s
				
			Test / Hakurei (race detector) (push) Successful in 4m52s
				
			Test / Flake checks (push) Successful in 1m30s
				
			This is a pretty solid implementation backed by robust tests, with a much cleaner interface. Signed-off-by: Ophestra <cat@gensokyo.uk>
		
			
				
	
	
		
			190 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			190 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2018 The Go Authors. All rights reserved.
 | 
						|
// Use of this source code is governed by a BSD-style
 | 
						|
// license that can be found in the LICENSE file.
 | 
						|
 | 
						|
// Package lockedfile creates and manipulates files whose contents should only
 | 
						|
// change atomically.
 | 
						|
package lockedfile
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"io/fs"
 | 
						|
	"os"
 | 
						|
	"runtime"
 | 
						|
)
 | 
						|
 | 
						|
// A File is a locked *os.File.
 | 
						|
//
 | 
						|
// Closing the file releases the lock.
 | 
						|
//
 | 
						|
// If the program exits while a file is locked, the operating system releases
 | 
						|
// the lock but may not do so promptly: callers must ensure that all locked
 | 
						|
// files are closed before exiting.
 | 
						|
type File struct {
 | 
						|
	osFile
 | 
						|
	closed bool
 | 
						|
	// cleanup panics when the file is no longer referenced and it has not been closed.
 | 
						|
	cleanup runtime.Cleanup
 | 
						|
}
 | 
						|
 | 
						|
// osFile embeds a *os.File while keeping the pointer itself unexported.
 | 
						|
// (When we close a File, it must be the same file descriptor that we opened!)
 | 
						|
type osFile struct {
 | 
						|
	*os.File
 | 
						|
}
 | 
						|
 | 
						|
// OpenFile is like os.OpenFile, but returns a locked file.
 | 
						|
// If flag includes os.O_WRONLY or os.O_RDWR, the file is write-locked;
 | 
						|
// otherwise, it is read-locked.
 | 
						|
func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) {
 | 
						|
	var (
 | 
						|
		f   = new(File)
 | 
						|
		err error
 | 
						|
	)
 | 
						|
	f.osFile.File, err = openFile(name, flag, perm)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	// Although the operating system will drop locks for open files when the go
 | 
						|
	// command exits, we want to hold locks for as little time as possible, and we
 | 
						|
	// especially don't want to leave a file locked after we're done with it. Our
 | 
						|
	// Close method is what releases the locks, so use a cleanup to report
 | 
						|
	// missing Close calls on a best-effort basis.
 | 
						|
	f.cleanup = runtime.AddCleanup(f, func(fileName string) {
 | 
						|
		panic(fmt.Sprintf("lockedfile.File %s became unreachable without a call to Close", fileName))
 | 
						|
	}, f.Name())
 | 
						|
 | 
						|
	return f, nil
 | 
						|
}
 | 
						|
 | 
						|
// Open is like os.Open, but returns a read-locked file.
 | 
						|
func Open(name string) (*File, error) {
 | 
						|
	return OpenFile(name, os.O_RDONLY, 0)
 | 
						|
}
 | 
						|
 | 
						|
// Create is like os.Create, but returns a write-locked file.
 | 
						|
func Create(name string) (*File, error) {
 | 
						|
	return OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
 | 
						|
}
 | 
						|
 | 
						|
// Edit creates the named file with mode 0666 (before umask),
 | 
						|
// but does not truncate existing contents.
 | 
						|
//
 | 
						|
// If Edit succeeds, methods on the returned File can be used for I/O.
 | 
						|
// The associated file descriptor has mode O_RDWR and the file is write-locked.
 | 
						|
func Edit(name string) (*File, error) {
 | 
						|
	return OpenFile(name, os.O_RDWR|os.O_CREATE, 0666)
 | 
						|
}
 | 
						|
 | 
						|
// Close unlocks and closes the underlying file.
 | 
						|
//
 | 
						|
// Close may be called multiple times; all calls after the first will return a
 | 
						|
// non-nil error.
 | 
						|
func (f *File) Close() error {
 | 
						|
	if f.closed {
 | 
						|
		return &fs.PathError{
 | 
						|
			Op:   "close",
 | 
						|
			Path: f.Name(),
 | 
						|
			Err:  fs.ErrClosed,
 | 
						|
		}
 | 
						|
	}
 | 
						|
	f.closed = true
 | 
						|
 | 
						|
	err := closeFile(f.osFile.File)
 | 
						|
	f.cleanup.Stop()
 | 
						|
	return err
 | 
						|
}
 | 
						|
 | 
						|
// Read opens the named file with a read-lock and returns its contents.
 | 
						|
func Read(name string) ([]byte, error) {
 | 
						|
	f, err := Open(name)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	defer f.Close()
 | 
						|
 | 
						|
	return io.ReadAll(f)
 | 
						|
}
 | 
						|
 | 
						|
// Write opens the named file (creating it with the given permissions if needed),
 | 
						|
// then write-locks it and overwrites it with the given content.
 | 
						|
func Write(name string, content io.Reader, perm fs.FileMode) (err error) {
 | 
						|
	f, err := OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	_, err = io.Copy(f, content)
 | 
						|
	if closeErr := f.Close(); err == nil {
 | 
						|
		err = closeErr
 | 
						|
	}
 | 
						|
	return err
 | 
						|
}
 | 
						|
 | 
						|
// Transform invokes t with the result of reading the named file, with its lock
 | 
						|
// still held.
 | 
						|
//
 | 
						|
// If t returns a nil error, Transform then writes the returned contents back to
 | 
						|
// the file, making a best effort to preserve existing contents on error.
 | 
						|
//
 | 
						|
// t must not modify the slice passed to it.
 | 
						|
func Transform(name string, t func([]byte) ([]byte, error)) (err error) {
 | 
						|
	f, err := Edit(name)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	defer f.Close()
 | 
						|
 | 
						|
	old, err := io.ReadAll(f)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	new, err := t(old)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if len(new) > len(old) {
 | 
						|
		// The overall file size is increasing, so write the tail first: if we're
 | 
						|
		// about to run out of space on the disk, we would rather detect that
 | 
						|
		// failure before we have overwritten the original contents.
 | 
						|
		if _, err := f.WriteAt(new[len(old):], int64(len(old))); err != nil {
 | 
						|
			// Make a best effort to remove the incomplete tail.
 | 
						|
			f.Truncate(int64(len(old)))
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// We're about to overwrite the old contents. In case of failure, make a best
 | 
						|
	// effort to roll back before we close the file.
 | 
						|
	defer func() {
 | 
						|
		if err != nil {
 | 
						|
			if _, err := f.WriteAt(old, 0); err == nil {
 | 
						|
				f.Truncate(int64(len(old)))
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	if len(new) >= len(old) {
 | 
						|
		if _, err := f.WriteAt(new[:len(old)], 0); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		if _, err := f.WriteAt(new, 0); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		// The overall file size is decreasing, so shrink the file to its final size
 | 
						|
		// after writing. We do this after writing (instead of before) so that if
 | 
						|
		// the write fails, enough filesystem space will likely still be reserved
 | 
						|
		// to contain the previous contents.
 | 
						|
		if err := f.Truncate(int64(len(new))); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 |