package release import ( "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "io" "os" "github.com/carlosframework/platform/internal/safefile" ) // ErrTooLarge: the object exceeded the caller's limit. The reader is cut at // limit+1 bytes, so an object EXACTLY at the limit is told from one over it. var ErrTooLarge = errors.New("store: object larger than the caller's limit") // FetchToFile streams key into dst with a running sha256, never holding the // object in memory: temp file, fsync, rename, directory fsync. Any failure // removes the temp and leaves dst untouched. func FetchToFile(ctx context.Context, s Store, key, dst string, limit int64, wantSHA256 string) (err error) { rc, _, err := OpenObject(ctx, s, key) if err != nil { return err } defer rc.Close() return safefile.Write(dst, 0o600, func(f io.Writer) error { h := sha256.New() n, err := io.Copy(io.MultiWriter(f, h), io.LimitReader(rc, limit+1)) if err != nil { return fmt.Errorf("fetch %s: %w", key, err) } if n > limit { return fmt.Errorf("fetch %s: %w (limit %d)", key, ErrTooLarge, limit) } if got := hex.EncodeToString(h.Sum(nil)); got != wantSHA256 { return fmt.Errorf("fetch %s: sha256 %s does not match the signed %s", key, got, wantSHA256) } return nil }) } // syncDir makes a rename durable: the directory entry is what fsync(file) // does not cover. func syncDir(dir string) error { d, err := os.Open(dir) if err != nil { return err } defer d.Close() return d.Sync() }