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 }