135 lines
3.5 KiB
Go
135 lines
3.5 KiB
Go
package artifact
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type Filesystem struct {
|
|
root string
|
|
}
|
|
|
|
func NewFilesystem(root string) (*Filesystem, error) {
|
|
absolute, err := filepath.Abs(root)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve artifact root: %w", err)
|
|
}
|
|
if err := os.MkdirAll(absolute, 0o750); err != nil {
|
|
return nil, fmt.Errorf("create artifact root: %w", err)
|
|
}
|
|
return &Filesystem{root: absolute}, nil
|
|
}
|
|
|
|
func (store *Filesystem) Put(
|
|
ctx context.Context,
|
|
key string,
|
|
source io.Reader,
|
|
mediaType string,
|
|
) (Descriptor, error) {
|
|
target, err := store.resolve(key)
|
|
if err != nil {
|
|
return Descriptor{}, err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(target), 0o750); err != nil {
|
|
return Descriptor{}, fmt.Errorf("create artifact directory: %w", err)
|
|
}
|
|
|
|
temp, err := os.CreateTemp(filepath.Dir(target), ".rara-artifact-*")
|
|
if err != nil {
|
|
return Descriptor{}, fmt.Errorf("create artifact staging file: %w", err)
|
|
}
|
|
tempName := temp.Name()
|
|
defer func() { _ = os.Remove(tempName) }()
|
|
|
|
hash := sha256.New()
|
|
written, copyErr := copyWithContext(ctx, io.MultiWriter(temp, hash), source)
|
|
closeErr := temp.Close()
|
|
if copyErr != nil {
|
|
return Descriptor{}, copyErr
|
|
}
|
|
if closeErr != nil {
|
|
return Descriptor{}, fmt.Errorf("close artifact staging file: %w", closeErr)
|
|
}
|
|
if err := os.Rename(tempName, target); err != nil {
|
|
return Descriptor{}, fmt.Errorf("publish artifact: %w", err)
|
|
}
|
|
return Descriptor{
|
|
URI: "file://" + target,
|
|
ContentHash: "sha256:" + hex.EncodeToString(hash.Sum(nil)),
|
|
SizeBytes: written,
|
|
MediaType: mediaType,
|
|
}, nil
|
|
}
|
|
|
|
func (store *Filesystem) Open(_ context.Context, key string) (io.ReadCloser, error) {
|
|
target, err := store.resolve(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file, err := os.Open(target)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open artifact: %w", err)
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
func (store *Filesystem) Delete(_ context.Context, key string) error {
|
|
target, err := store.resolve(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.Remove(target); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("delete artifact: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (store *Filesystem) resolve(key string) (string, error) {
|
|
if key == "" {
|
|
return "", errors.New("artifact key is required")
|
|
}
|
|
clean := filepath.Clean(filepath.FromSlash(key))
|
|
if filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
|
|
return "", errors.New("artifact key escapes the configured root")
|
|
}
|
|
target := filepath.Join(store.root, clean)
|
|
relative, err := filepath.Rel(store.root, target)
|
|
if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
|
|
return "", errors.New("artifact key escapes the configured root")
|
|
}
|
|
return target, nil
|
|
}
|
|
|
|
func copyWithContext(ctx context.Context, destination io.Writer, source io.Reader) (int64, error) {
|
|
buffer := make([]byte, 32*1024)
|
|
var total int64
|
|
for {
|
|
if err := ctx.Err(); err != nil {
|
|
return total, err
|
|
}
|
|
read, readErr := source.Read(buffer)
|
|
if read > 0 {
|
|
written, writeErr := destination.Write(buffer[:read])
|
|
total += int64(written)
|
|
if writeErr != nil {
|
|
return total, fmt.Errorf("write artifact: %w", writeErr)
|
|
}
|
|
if written != read {
|
|
return total, io.ErrShortWrite
|
|
}
|
|
}
|
|
if errors.Is(readErr, io.EOF) {
|
|
return total, nil
|
|
}
|
|
if readErr != nil {
|
|
return total, fmt.Errorf("read artifact: %w", readErr)
|
|
}
|
|
}
|
|
}
|