Confine local file installation paths

This commit is contained in:
2026-08-10 17:59:29 +00:00
parent 59f3fe3d1d
commit 18ddf00d3d
20 changed files with 694 additions and 153 deletions

View File

@@ -0,0 +1,23 @@
package storage
import (
"context"
"fmt"
"io"
)
// WriterDownloader is implemented by storage backends that stream an object
// into a caller-owned file handle.
type WriterDownloader interface {
DownloadTo(ctx context.Context, key string, destination io.Writer) error
}
// DownloadTo streams one object into destination. Destination-confined callers
// require this capability rather than granting a backend a mutable pathname.
func DownloadTo(ctx context.Context, store ObjectStore, key string, destination io.Writer) error {
writer, ok := store.(WriterDownloader)
if !ok {
return fmt.Errorf("object store does not support handle-confined downloads")
}
return writer.DownloadTo(ctx, key, destination)
}

View File

@@ -3,6 +3,7 @@ package storage
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"sort"
@@ -87,34 +88,43 @@ func (f *FakeBackend) List(ctx context.Context, prefix string) ([]ObjectInfo, er
return out, nil
}
// Download writes one object to a local path.
func (f *FakeBackend) Download(ctx context.Context, key, localPath string) error {
// DownloadTo writes one object to a caller-owned destination writer.
func (f *FakeBackend) DownloadTo(ctx context.Context, key string, destination io.Writer) error {
if err := ctx.Err(); err != nil {
return err
}
if f.DownloadErr != nil {
return f.DownloadErr
}
if strings.TrimSpace(localPath) == "" {
return fmt.Errorf("download object: local path is required")
if destination == nil {
return fmt.Errorf("download object: destination writer is required")
}
obj, ok := f.Objects[normalizeObjectKey(key)]
if !ok {
return fmt.Errorf("download object %q: %w", key, os.ErrNotExist)
}
f.Downloads = append(f.Downloads, FakeDownloadCall{
Key: normalizeObjectKey(key),
LocalPath: localPath,
})
f.Downloads = append(f.Downloads, FakeDownloadCall{Key: normalizeObjectKey(key)})
if _, err := destination.Write(obj.Data); err != nil {
return fmt.Errorf("download object %q: write destination: %w", key, err)
}
return nil
}
// Download writes one object to a local path.
func (f *FakeBackend) Download(ctx context.Context, key, localPath string) error {
if strings.TrimSpace(localPath) == "" {
return fmt.Errorf("download object: local path is required")
}
if err := os.MkdirAll(filepath.Dir(localPath), 0o755); err != nil {
return fmt.Errorf("download object %q: create parent directory: %w", key, err)
}
if err := os.WriteFile(localPath, obj.Data, 0o644); err != nil {
return fmt.Errorf("download object %q: write local file: %w", key, err)
destination, err := os.Create(localPath)
if err != nil {
return fmt.Errorf("download object %q: create local file: %w", key, err)
}
return nil
defer destination.Close()
return f.DownloadTo(ctx, key, destination)
}
// Upload reads a local file and stores it under key.

View File

@@ -150,11 +150,11 @@ func (b *S3Backend) List(ctx context.Context, prefix string) ([]ObjectInfo, erro
return out, nil
}
// Download retrieves one object to localPath, creating parent directories as needed.
func (b *S3Backend) Download(ctx context.Context, key, localPath string) error {
// DownloadTo retrieves one object into the caller-owned destination writer.
func (b *S3Backend) DownloadTo(ctx context.Context, key string, destination io.Writer) error {
normalizedKey := normalizeObjectKey(key)
if strings.TrimSpace(localPath) == "" {
return fmt.Errorf("download object: local path is required")
if destination == nil {
return fmt.Errorf("download object: destination writer is required")
}
resp, err := b.client.GetObject(ctx, &s3.GetObjectInput{
@@ -166,20 +166,30 @@ func (b *S3Backend) Download(ctx context.Context, key, localPath string) error {
}
defer resp.Body.Close()
if _, err := io.Copy(destination, resp.Body); err != nil {
return fmt.Errorf("download object %q: copy body: %w", normalizedKey, err)
}
return nil
}
// Download retrieves one object to localPath, creating parent directories as needed.
func (b *S3Backend) Download(ctx context.Context, key, localPath string) error {
if strings.TrimSpace(localPath) == "" {
return fmt.Errorf("download object: local path is required")
}
if err := os.MkdirAll(filepath.Dir(localPath), 0o755); err != nil {
return fmt.Errorf("download object %q: create parent directory: %w", normalizedKey, err)
return fmt.Errorf("download object %q: create parent directory: %w", key, err)
}
dst, err := os.Create(localPath)
if err != nil {
return fmt.Errorf("download object %q: create local file: %w", normalizedKey, err)
return fmt.Errorf("download object %q: create local file: %w", key, err)
}
defer dst.Close()
if _, err := io.Copy(dst, resp.Body); err != nil {
return fmt.Errorf("download object %q: copy body: %w", normalizedKey, err)
if err := b.DownloadTo(ctx, key, dst); err != nil {
return err
}
if err := dst.Sync(); err != nil {
return fmt.Errorf("download object %q: sync local file: %w", normalizedKey, err)
return fmt.Errorf("download object %q: sync local file: %w", key, err)
}
return nil
}