Storage backend: - Add internal/storage package with Storer interface - Implement FileStorer for local filesystem storage (file:// URLs) - Implement S3Storer wrapping existing s3.Client - Support storage_url config field (s3:// or file://) - Migrate all consumers to use storage.Storer interface PID locking: - Add internal/pidlock package to prevent concurrent instances - Acquire lock before app start, release on exit - Detect stale locks from crashed processes Scan progress improvements: - Add fast file enumeration pass before stat() phase - Use enumerated set for deletion detection (no extra filesystem access) - Show progress with percentage, files/sec, elapsed time, and ETA - Change "changed" to "changed/new" for clarity Config improvements: - Add tilde expansion for paths (~/) - Use xdg library for platform-specific default index path
86 lines
2.3 KiB
Go
86 lines
2.3 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
|
|
"git.eeqj.de/sneak/vaultik/internal/s3"
|
|
)
|
|
|
|
// S3Storer wraps the existing s3.Client to implement Storer.
|
|
type S3Storer struct {
|
|
client *s3.Client
|
|
}
|
|
|
|
// NewS3Storer creates a new S3 storage backend.
|
|
func NewS3Storer(client *s3.Client) *S3Storer {
|
|
return &S3Storer{client: client}
|
|
}
|
|
|
|
// Put stores data at the specified key.
|
|
func (s *S3Storer) Put(ctx context.Context, key string, data io.Reader) error {
|
|
return s.client.PutObject(ctx, key, data)
|
|
}
|
|
|
|
// PutWithProgress stores data with progress reporting.
|
|
func (s *S3Storer) PutWithProgress(ctx context.Context, key string, data io.Reader, size int64, progress ProgressCallback) error {
|
|
// Convert storage.ProgressCallback to s3.ProgressCallback
|
|
var s3Progress s3.ProgressCallback
|
|
if progress != nil {
|
|
s3Progress = s3.ProgressCallback(progress)
|
|
}
|
|
return s.client.PutObjectWithProgress(ctx, key, data, size, s3Progress)
|
|
}
|
|
|
|
// Get retrieves data from the specified key.
|
|
func (s *S3Storer) Get(ctx context.Context, key string) (io.ReadCloser, error) {
|
|
return s.client.GetObject(ctx, key)
|
|
}
|
|
|
|
// Stat returns metadata about an object without retrieving its contents.
|
|
func (s *S3Storer) Stat(ctx context.Context, key string) (*ObjectInfo, error) {
|
|
info, err := s.client.StatObject(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ObjectInfo{
|
|
Key: info.Key,
|
|
Size: info.Size,
|
|
}, nil
|
|
}
|
|
|
|
// Delete removes an object.
|
|
func (s *S3Storer) Delete(ctx context.Context, key string) error {
|
|
return s.client.DeleteObject(ctx, key)
|
|
}
|
|
|
|
// List returns all keys with the given prefix.
|
|
func (s *S3Storer) List(ctx context.Context, prefix string) ([]string, error) {
|
|
return s.client.ListObjects(ctx, prefix)
|
|
}
|
|
|
|
// ListStream returns a channel of ObjectInfo for large result sets.
|
|
func (s *S3Storer) ListStream(ctx context.Context, prefix string) <-chan ObjectInfo {
|
|
ch := make(chan ObjectInfo)
|
|
go func() {
|
|
defer close(ch)
|
|
for info := range s.client.ListObjectsStream(ctx, prefix, false) {
|
|
ch <- ObjectInfo{
|
|
Key: info.Key,
|
|
Size: info.Size,
|
|
Err: info.Err,
|
|
}
|
|
}
|
|
}()
|
|
return ch
|
|
}
|
|
|
|
// Info returns human-readable storage location information.
|
|
func (s *S3Storer) Info() StorageInfo {
|
|
return StorageInfo{
|
|
Type: "s3",
|
|
Location: fmt.Sprintf("%s/%s", s.client.Endpoint(), s.client.BucketName()),
|
|
}
|
|
}
|