Skip to content

Instantly share code, notes, and snippets.

@kylelemons
Last active May 9, 2021 00:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kylelemons/21539a152e9af1dd79c3775ca94efb60 to your computer and use it in GitHub Desktop.
Save kylelemons/21539a152e9af1dd79c3775ca94efb60 to your computer and use it in GitHub Desktop.
Sketch of writable filesystem interfaces for io/fs
package fs
import (
"fmt"
"io"
"os"
. "io/fs"
)
var ErrUnsupported = fmt.Errorf("unsupported operation")
type WriteFileFS interface {
WriteFile(name string, data []byte, mode FileMode) error
}
// WriteFileMode writes the data to a file.
//
// This requires that the underlying filesystem implement modes via
// WriteFileMode, OpenFileMode, and/or returning a file that supports Chmod.
func WriteFileMode(fs FS, name string, data []byte, mode FileMode) error {
switch fs := fs.(type) {
case WriteFileFS:
return fs.WriteFile(name, data, mode)
}
// If the FS only supports Open, try to set the FileMode after opening
f, err := OpenFile(fs, name, mode)
if err != nil {
return fmt.Errorf("Open: %w", err)
}
return writeContents(f, data)
}
type OpenFileFS interface {
OpenFile(name string, mode FileMode) (File, error)
}
type ChmodFile interface {
Chmod(mode FileMode) error
}
var _ ChmodFile = (*os.File)(nil)
func OpenFile(fs FS, name string, mode FileMode) (File, error) {
// If the FS supports opening with permissions, open the file
if fs, ok := fs.(OpenFileFS); ok {
return fs.OpenFile(name, mode)
}
// If the FS only supports Open, try to set the FileMode after opening
f, err := fs.Open(name)
if err != nil {
return nil, fmt.Errorf("Open: %w", err)
}
// Chmod isn't required if the mode bits are already correct.
stat, err := f.Stat()
if err == nil && stat.Mode()&ModePerm == mode {
return f, nil
}
cm, ok := f.(ChmodFile)
if !ok {
return f, fmt.Errorf("%w: %T does not support chmod", ErrUnsupported, f)
}
if err := cm.Chmod(mode); err != nil {
return f, fmt.Errorf("Chmod: %w", err)
}
return f, nil
}
// writeContents writes data to the file and then closes it.
func writeContents(f File, data []byte) error {
defer f.Close()
if n, err := Write(f, data); err != nil {
return fmt.Errorf("Write: %w", err)
} else if want := len(data); n != want {
return fmt.Errorf("%w: wrote %d bytes of %d", io.ErrShortWrite, n, want)
}
if err := f.Close(); err != nil {
return fmt.Errorf("Close: %w", err)
}
return nil
}
func Write(f File, data []byte) (n int, err error) {
w, ok := f.(io.Writer)
if !ok {
return 0, fmt.Errorf("%w: %T does not implement %T", ErrUnsupported, f, w)
}
return w.Write(data)
}
type CreateFS interface {
Create(name string) (File, error)
}
type TruncateFile interface {
Truncate(int64) error
}
var _ TruncateFile = (*os.File)(nil)
// Create returns a zero-length file with the given name.
//
// If the filesystem does not support Create directly, the Opened file will
// be Truncated before returning if its size is nonzero.
func Create(fs FS, name string) (_ File, _err error) {
// If the FS supports this as a direct operation, use it
if fs, ok := fs.(CreateFS); ok {
return fs.Create(name)
}
// Otherwise Create needs to try to truncate the file after opening.
f, err := fs.Open(name)
if err != nil {
return nil, fmt.Errorf("Open: %w", err)
}
defer func() {
if _err != nil {
f.Close()
}
}()
// Truncate isn't required if the file is already zero.
stat, err := f.Stat()
if err == nil && stat.Size() == 0 {
return f, nil
}
t, ok := f.(TruncateFile)
if !ok {
return f, fmt.Errorf("%w: %T does not support truncation, file may contain data", ErrUnsupported, f)
}
if err := t.Truncate(0); err != nil {
return f, fmt.Errorf("Truncate: %w", err)
}
return f, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment