❯ go-clikit
v0.10.0 pkg.go.dev ↗

guides › keep files safe

Keep files safe

Replace a file whole, change a shared file under a lock, and refuse a file that another user could have written.

A tool that several users, or several runs, share has three problems with its files. A reader can see a file written in part. Two runs can change a shared file at once, and one change is lost. And a file that another user can write can make the tool run their commands. fsutil solves the three (decision 37).

ProblemUse
A reader sees half a filefsutil.WriteAtomic: a temporary file, a sync and a rename
Two runs change one filefsutil.Update: the change under fsutil.Lock
Another user put the file therefsutil.CheckTrusted and fsutil.EnsureDir, which refuse with fsutil.ErrUntrusted
A private file was made readablefsutil.ExactMode

Change a shared file#

  1. Give Update the path, the mode and a function that changes the old content into the new.
  2. Update takes the lock beside the file, waits at most fsutil.DefaultLockWait, calls the function, and writes the result whole.
  3. When the wait passes, the error is a timeout and names the holder of the lock.
dir, _ := os.MkdirTemp("", "prog-")
defer func() { _ = os.RemoveAll(dir) }()
path := filepath.Join(dir, "runs")

count := func(current []byte) ([]byte, error) {
	n := 0
	if current != nil { // nil: there is no file yet
		var err error
		if n, err = strconv.Atoi(string(current)); err != nil {
			return nil, err // nothing is written
		}
	}
	return []byte(strconv.Itoa(n + 1)), nil
}
for range 3 {
	if err := fsutil.Update(context.Background(), path, 0o644, count); err != nil {
		fmt.Println(err)
		return
	}
}
data, _ := os.ReadFile(path)
fmt.Println(string(data))
 3
ExampleUpdate in fsutil/example_test.go. CI runs it and checks its output.

Refuse what others wrote#

dir, _ := os.MkdirTemp("", "prog-")
defer func() { _ = os.RemoveAll(dir) }()
path := filepath.Join(dir, "config.toml")
_ = os.WriteFile(path, []byte("ssh = \"ssh\"\n"), 0o644)

fmt.Println(fsutil.CheckTrusted(path))
_ = os.Chmod(path, 0o666)
fmt.Println(errors.Is(fsutil.CheckTrusted(path), fsutil.ErrUntrusted))
<nil>
true
ExampleCheckTrusted in fsutil/example_test.go. CI runs it and checks its output.
WARNING

Read configuration that names commands to run only after CheckTrusted accepts it. A file that another user could write gives that user the commands of your program.

The name and the mode of the lock file are an interface of the kit, so two releases of a program lock the same file.