You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
102 lines
3.2 KiB
102 lines
3.2 KiB
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
)
|
|
|
|
// The functions in this file are based on go's own cache in
|
|
// cmd/go/internal/cache/cache.go, particularly putIndexEntry and copyFile.
|
|
|
|
// writeActionFile writes the indexEntry metadata for an ActionID to disk. It
|
|
// may be called for the same actionID concurrently from multiple processes,
|
|
// and the outputID for a specific actionID may change from time to time due
|
|
// to non-deterministic builds. It makes a best-effort to delete the file if
|
|
// anything goes wrong.
|
|
func writeActionFile(dest string, b []byte) (retErr error) {
|
|
f, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE, 0o666)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
cerr := f.Close()
|
|
if retErr != nil || cerr != nil {
|
|
retErr = errors.Join(retErr, cerr, os.Remove(dest))
|
|
}
|
|
}()
|
|
|
|
_, err = f.Write(b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Truncate the file only *after* writing it.
|
|
// (This should be a no-op, but truncate just in case of previous corruption.)
|
|
//
|
|
// This differs from os.WriteFile, which truncates to 0 *before* writing
|
|
// via os.O_TRUNC. Truncating only after writing ensures that a second write
|
|
// of the same content to the same file is idempotent, and does not - even
|
|
// temporarily! - undo the effect of the first write.
|
|
return f.Truncate(int64(len(b)))
|
|
}
|
|
|
|
// writeOutputFile writes content to be cached to disk. The outputID is the
|
|
// sha256 hash of the content, and each file should only be written ~once,
|
|
// assuming no sha256 hash collisions. It may be written multiple times if
|
|
// concurrent processes are both populating the same output. The file is opened
|
|
// with FILE_SHARE_READ|FILE_SHARE_WRITE, which means both processes can write
|
|
// the same contents concurrently without conflict.
|
|
//
|
|
// It makes a best effort to clean up if anything goes wrong, but the file may
|
|
// be left in an inconsistent state in the event of disk-related errors such as
|
|
// another process taking file locks, or power loss etc.
|
|
func writeOutputFile(dest string, r io.Reader, size int64, outputID string) (_ int64, retErr error) {
|
|
info, err := os.Stat(dest)
|
|
if err == nil && info.Size() == size {
|
|
// Already exists, check the hash.
|
|
if f, err := os.Open(dest); err == nil {
|
|
h := sha256.New()
|
|
io.Copy(h, f)
|
|
f.Close()
|
|
if fmt.Sprintf("%x", h.Sum(nil)) == outputID {
|
|
// Still drain the reader to ensure associated resources are released.
|
|
return io.Copy(io.Discard, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Didn't successfully find the pre-existing file, write it.
|
|
mode := os.O_WRONLY | os.O_CREATE
|
|
if err == nil && info.Size() > size {
|
|
mode |= os.O_TRUNC // Should never happen, but self-heal.
|
|
}
|
|
f, err := os.OpenFile(dest, mode, 0644)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to open output file %q: %w", dest, err)
|
|
}
|
|
defer func() {
|
|
cerr := f.Close()
|
|
if retErr != nil || cerr != nil {
|
|
retErr = errors.Join(retErr, cerr, os.Remove(dest))
|
|
}
|
|
}()
|
|
|
|
// Copy file to f, but also into h to double-check hash.
|
|
h := sha256.New()
|
|
w := io.MultiWriter(f, h)
|
|
n, err := io.Copy(w, r)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if fmt.Sprintf("%x", h.Sum(nil)) != outputID {
|
|
return 0, errors.New("file content changed underfoot")
|
|
}
|
|
|
|
return n, nil
|
|
}
|
|
|