Add public local bundle writer
This commit is contained in:
187
pkg/bundle/writer.go
Normal file
187
pkg/bundle/writer.go
Normal file
@@ -0,0 +1,187 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user