Files
distributor/pkg/bundle/writer.go

188 lines
5.1 KiB
Go

package bundle
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
)
func WriteBundle(opts WriteBundleOptions) (Manifest, error) {
root, err := cleanBundleRoot(opts.Root)
if err != nil {
return Manifest{}, err
}
if opts.ID == "" {
return Manifest{}, fmt.Errorf("id is required")
}
if len(opts.Files) == 0 {
return Manifest{}, fmt.Errorf("files is required")
}
parent := filepath.Dir(root)
if err := os.MkdirAll(parent, 0o755); err != nil {
return Manifest{}, fmt.Errorf("create bundle parent: %w", err)
}
if !opts.Overwrite {
if _, err := os.Lstat(root); err == nil {
return Manifest{}, fmt.Errorf("bundle root %q already exists", root)
} else if !errors.Is(err, os.ErrNotExist) {
return Manifest{}, fmt.Errorf("stat bundle root: %w", err)
}
}
tempRoot, err := os.MkdirTemp(parent, "."+filepath.Base(root)+"-*.tmp")
if err != nil {
return Manifest{}, fmt.Errorf("create bundle temp root: %w", err)
}
removeTemp := true
defer func() {
if removeTemp {
_ = os.RemoveAll(tempRoot)
}
}()
paths, err := copyBundleFiles(tempRoot, opts.Files)
if err != nil {
return Manifest{}, err
}
manifest, err := BuildManifest(BuildOptions{
Root: tempRoot,
ID: opts.ID,
Created: opts.Created,
Files: paths,
})
if err != nil {
return Manifest{}, err
}
if err := WriteManifest(tempRoot, manifest, WriteManifestOptions{}); err != nil {
return Manifest{}, err
}
if err := ValidateBundle(tempRoot, manifest); err != nil {
return Manifest{}, err
}
if err := promoteBundleRoot(tempRoot, root, opts.Overwrite); err != nil {
return Manifest{}, err
}
removeTemp = false
return manifest, nil
}
func cleanBundleRoot(root string) (string, error) {
if root == "" {
return "", fmt.Errorf("root is required")
}
return filepath.Clean(root), nil
}
func copyBundleFiles(root string, files []BundleFile) ([]string, error) {
paths := make([]string, 0, len(files))
seen := make(map[string]struct{}, len(files))
for index, file := range files {
if file.SourcePath == "" {
return nil, fmt.Errorf("files[%d].source_path is required", index)
}
if err := ValidateSourcePath(file.Path); err != nil {
return nil, fmt.Errorf("files[%d].path: %w", index, err)
}
if _, exists := seen[file.Path]; exists {
return nil, fmt.Errorf("files[%d].path duplicates %q", index, file.Path)
}
seen[file.Path] = struct{}{}
if err := copyBundleFile(root, file); err != nil {
return nil, fmt.Errorf("files[%d]: %w", index, err)
}
paths = append(paths, file.Path)
}
return paths, nil
}
func copyBundleFile(root string, file BundleFile) error {
info, err := os.Lstat(file.SourcePath)
if err != nil {
return fmt.Errorf("stat source %q: %w", file.SourcePath, err)
}
if !info.Mode().IsRegular() {
return fmt.Errorf("source %q must be a regular file", file.SourcePath)
}
destination := filepath.Join(root, filepath.FromSlash(file.Path))
if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil {
return fmt.Errorf("create destination directory: %w", err)
}
source, err := os.Open(file.SourcePath)
if err != nil {
return fmt.Errorf("open source %q: %w", file.SourcePath, err)
}
defer source.Close()
mode := info.Mode().Perm()
if mode == 0 {
mode = 0o600
}
target, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode)
if err != nil {
return fmt.Errorf("create destination %q: %w", file.Path, err)
}
if _, err := io.Copy(target, source); err != nil {
_ = target.Close()
return fmt.Errorf("copy to destination %q: %w", file.Path, err)
}
if err := target.Close(); err != nil {
return fmt.Errorf("close destination %q: %w", file.Path, err)
}
return nil
}
func promoteBundleRoot(tempRoot, root string, overwrite bool) error {
if !overwrite {
if _, err := os.Lstat(root); err == nil {
return fmt.Errorf("bundle root %q already exists", root)
} else if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("stat bundle root: %w", err)
}
if err := os.Rename(tempRoot, root); err != nil {
return fmt.Errorf("promote bundle root: %w", err)
}
return nil
}
if _, err := os.Lstat(root); errors.Is(err, os.ErrNotExist) {
if err := os.Rename(tempRoot, root); err != nil {
return fmt.Errorf("promote bundle root: %w", err)
}
return nil
} else if err != nil {
return fmt.Errorf("stat bundle root: %w", err)
}
backupRoot, err := reserveSiblingPath(filepath.Dir(root), "."+filepath.Base(root)+"-backup-*.tmp")
if err != nil {
return err
}
if err := os.Rename(root, backupRoot); err != nil {
return fmt.Errorf("move existing bundle root: %w", err)
}
if err := os.Rename(tempRoot, root); err != nil {
restoreErr := os.Rename(backupRoot, root)
if restoreErr != nil {
return fmt.Errorf("promote bundle root: %w; restore existing bundle root: %v", err, restoreErr)
}
return fmt.Errorf("promote bundle root: %w", err)
}
_ = os.RemoveAll(backupRoot)
return nil
}
func reserveSiblingPath(parent, pattern string) (string, error) {
path, err := os.MkdirTemp(parent, pattern)
if err != nil {
return "", fmt.Errorf("reserve backup path: %w", err)
}
if err := os.Remove(path); err != nil {
return "", fmt.Errorf("reserve backup path: %w", err)
}
return path, nil
}