diff --git a/internal/vaultik/restore.go b/internal/vaultik/restore.go index 015c533..aa00a27 100644 --- a/internal/vaultik/restore.go +++ b/internal/vaultik/restore.go @@ -473,6 +473,53 @@ func (v *Vaultik) restoreRegularFile( return nil } +// BlobFetchResult holds the result of fetching and decrypting a blob. +type BlobFetchResult struct { + Data []byte + CompressedSize int64 +} + +// FetchAndDecryptBlob downloads a blob from storage, decrypts and decompresses it. +func (v *Vaultik) FetchAndDecryptBlob(ctx context.Context, blobHash string, expectedSize int64, identity age.Identity) (*BlobFetchResult, error) { + // Construct blob path with sharding + blobPath := fmt.Sprintf("blobs/%s/%s/%s", blobHash[:2], blobHash[2:4], blobHash) + + reader, err := v.Storage.Get(ctx, blobPath) + if err != nil { + return nil, fmt.Errorf("downloading blob: %w", err) + } + defer func() { _ = reader.Close() }() + + // Read encrypted data + encryptedData, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("reading blob data: %w", err) + } + + // Decrypt and decompress + blobReader, err := blobgen.NewReader(bytes.NewReader(encryptedData), identity) + if err != nil { + return nil, fmt.Errorf("creating decryption reader: %w", err) + } + defer func() { _ = blobReader.Close() }() + + data, err := io.ReadAll(blobReader) + if err != nil { + return nil, fmt.Errorf("decrypting blob: %w", err) + } + + log.Debug("Downloaded and decrypted blob", + "hash", blobHash[:16], + "encrypted_size", humanize.Bytes(uint64(len(encryptedData))), + "decrypted_size", humanize.Bytes(uint64(len(data))), + ) + + return &BlobFetchResult{ + Data: data, + CompressedSize: int64(len(encryptedData)), + }, nil +} + // downloadBlob downloads and decrypts a blob func (v *Vaultik) downloadBlob(ctx context.Context, blobHash string, expectedSize int64, identity age.Identity) ([]byte, error) { result, err := v.FetchAndDecryptBlob(ctx, blobHash, expectedSize, identity) diff --git a/internal/vaultik/snapshot.go b/internal/vaultik/snapshot.go index 39f4cc8..a6e498b 100644 --- a/internal/vaultik/snapshot.go +++ b/internal/vaultik/snapshot.go @@ -4,8 +4,8 @@ import ( "encoding/json" "fmt" "os" - "regexp" "path/filepath" + "regexp" "sort" "strings" "text/tabwriter" diff --git a/internal/vaultik/vaultik.go b/internal/vaultik/vaultik.go index 4ce6535..7fe5a3b 100644 --- a/internal/vaultik/vaultik.go +++ b/internal/vaultik/vaultik.go @@ -135,6 +135,34 @@ func (v *Vaultik) Outputf(format string, args ...any) { _, _ = fmt.Fprintf(v.Stdout, format, args...) } +// printfStdout writes formatted output to stdout. +func (v *Vaultik) printfStdout(format string, args ...any) { + _, _ = fmt.Fprintf(v.Stdout, format, args...) +} + +// printlnStdout writes a line to stdout. +func (v *Vaultik) printlnStdout(args ...any) { + _, _ = fmt.Fprintln(v.Stdout, args...) +} + +// scanlnStdin reads a line from stdin into the provided string pointer. +func (v *Vaultik) scanlnStdin(s *string) error { + _, err := fmt.Fscanln(v.Stdin, s) + return err +} + +// FetchBlob downloads a blob from storage and returns a reader for the encrypted data. +func (v *Vaultik) FetchBlob(ctx context.Context, blobHash string, expectedSize int64) (io.ReadCloser, int64, error) { + blobPath := fmt.Sprintf("blobs/%s/%s/%s", blobHash[:2], blobHash[2:4], blobHash) + + reader, err := v.Storage.Get(ctx, blobPath) + if err != nil { + return nil, 0, fmt.Errorf("downloading blob: %w", err) + } + + return reader, expectedSize, nil +} + // TestVaultik wraps a Vaultik with captured stdout/stderr for testing type TestVaultik struct { *Vaultik