-
Notifications
You must be signed in to change notification settings - Fork 1
feat: [CODE-4993] refactor the upload flow of Git LFS #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,25 +15,58 @@ | |
| package lfs | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "crypto/sha256" | ||
| "encoding/hex" | ||
| "errors" | ||
| "fmt" | ||
| "hash" | ||
| "io" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/harness/gitness/app/api/usererror" | ||
| "github.com/harness/gitness/app/auth" | ||
| "github.com/harness/gitness/blob" | ||
| "github.com/harness/gitness/store" | ||
| "github.com/harness/gitness/types" | ||
| "github.com/harness/gitness/types/enum" | ||
|
|
||
| "github.com/google/uuid" | ||
| "github.com/rs/zerolog/log" | ||
| ) | ||
|
|
||
| const ( | ||
| lfsTempPathFormat = "lfs/tmp/%s" | ||
| ) | ||
|
|
||
| type UploadOut struct { | ||
| ObjectPath string `json:"object_path"` | ||
| ObjectPath string `json:"object_path"` //nolint:tagliatelle | ||
| } | ||
|
|
||
| // hashingReader wraps an io.Reader and calculates a hash while reading. | ||
| type hashingReader struct { | ||
| reader io.Reader | ||
| hasher hash.Hash | ||
| } | ||
|
|
||
| func newHashingReader(r io.Reader) *hashingReader { | ||
| return &hashingReader{ | ||
| reader: r, | ||
| hasher: sha256.New(), | ||
| } | ||
| } | ||
|
|
||
| func (h *hashingReader) Read(p []byte) (int, error) { | ||
| n, err := h.reader.Read(p) | ||
| if n > 0 { | ||
| h.hasher.Write(p[:n]) | ||
| } | ||
| return n, err | ||
| } | ||
|
|
||
| func (h *hashingReader) Sum() string { | ||
| return hex.EncodeToString(h.hasher.Sum(nil)) | ||
| } | ||
|
|
||
| func (c *Controller) Upload(ctx context.Context, | ||
|
|
@@ -61,28 +94,43 @@ func (c *Controller) Upload(ctx context.Context, | |
| return nil, usererror.Conflict("LFS object already exists and cannot be modified") | ||
| } | ||
|
|
||
| expectedHash := strings.TrimPrefix(pointer.OId, "sha256:") | ||
|
|
||
| // Generate a unique temp path for staging the upload | ||
| tempPath := fmt.Sprintf(lfsTempPathFormat, uuid.NewString()) | ||
| finalPath := getLFSObjectPath(pointer.OId) | ||
|
|
||
| // Stream the content to temp path while calculating hash | ||
| limitedReader := io.LimitReader(file, pointer.Size) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 AICR Agent (local Opus) · 🟡 MEDIUM · confidence 60 Hash is computed over truncated bytes, but a too-short upload is never rejected. |
||
| content, err := io.ReadAll(limitedReader) | ||
| hashReader := newHashingReader(limitedReader) | ||
|
|
||
| err = c.blobStore.Upload(ctx, hashReader, tempPath) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to read uploaded content: %w", err) | ||
| return nil, fmt.Errorf("failed to upload file to temp path: %w", err) | ||
| } | ||
|
|
||
| hasher := sha256.New() | ||
| hasher.Write(content) | ||
| calculatedHash := hex.EncodeToString(hasher.Sum(nil)) | ||
|
|
||
| expectedHash := strings.TrimPrefix(pointer.OId, "sha256:") | ||
|
|
||
| calculatedHash := hashReader.Sum() | ||
| if calculatedHash != expectedHash { | ||
| if deleteErr := c.blobStore.Delete(ctx, tempPath); deleteErr != nil { | ||
| if !errors.Is(deleteErr, blob.ErrNotFound) { | ||
| log.Ctx(ctx).Warn().Err(deleteErr). | ||
| Str("temp_path", tempPath). | ||
| Msg("failed to delete temp file after hash mismatch") | ||
| } | ||
| } | ||
| return nil, usererror.BadRequest("content hash doesn't match provided OID") | ||
| } | ||
|
|
||
| contentReader := bytes.NewReader(content) | ||
| objPath := getLFSObjectPath(pointer.OId) | ||
|
|
||
| err = c.blobStore.Upload(ctx, contentReader, objPath) | ||
| err = c.blobStore.Move(ctx, tempPath, finalPath) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to upload file: %w", err) | ||
| if deleteErr := c.blobStore.Delete(ctx, tempPath); deleteErr != nil { | ||
| if !errors.Is(deleteErr, blob.ErrNotFound) { | ||
| log.Ctx(ctx).Warn().Err(deleteErr). | ||
| Str("temp_path", tempPath). | ||
| Msg("failed to delete temp file after move failure") | ||
| } | ||
| } | ||
| return nil, fmt.Errorf("failed to move file to final path: %w", err) | ||
| } | ||
|
|
||
| now := time.Now() | ||
|
|
@@ -101,6 +149,6 @@ func (c *Controller) Upload(ctx context.Context, | |
| } | ||
|
|
||
| return &UploadOut{ | ||
| ObjectPath: objPath, | ||
| ObjectPath: finalPath, | ||
| }, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -102,3 +102,33 @@ func (c *FileSystemStore) Download(_ context.Context, filePath string) (io.ReadC | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return io.ReadCloser(file), nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (c *FileSystemStore) Move(_ context.Context, srcPath, dstPath string) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| srcDiskPath := fmt.Sprintf(fileDiskPathFmt, c.basePath, srcPath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dstDiskPath := fmt.Sprintf(fileDiskPathFmt, c.basePath, dstPath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Ensure destination directory exists | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dstDir, _ := path.Split(dstDiskPath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _, err := os.Stat(dstDir); errors.Is(err, fs.ErrNotExist) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err = os.MkdirAll(dstDir, os.ModeDir|os.ModePerm); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to create destination directory: %w", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := os.Rename(srcDiskPath, dstDiskPath); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to move file: %w", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+106
to
+122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the source file doesn't exist, This inconsistency means callers cannot reliably use Proposed fix if err := os.Rename(srcDiskPath, dstDiskPath); err != nil {
+ if errors.Is(err, fs.ErrNotExist) {
+ return ErrNotFound
+ }
return fmt.Errorf("failed to move file: %w", err)
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (c *FileSystemStore) Delete(_ context.Context, filePath string) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fileDiskPath := fmt.Sprintf(fileDiskPathFmt, c.basePath, filePath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := os.Remove(fileDiskPath); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if errors.Is(err, fs.ErrNotExist) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ErrNotFound | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to delete file: %w", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -371,3 +371,164 @@ func TestFileSystemStore_Interface(t *testing.T) { | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| _ = store | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| func TestFileSystemStore_Move(t *testing.T) { | ||||||||||||||||||||||||||||||||
| tempDir, err := os.MkdirTemp("", "blob-test-*") | ||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||
| t.Fatalf("failed to create temp dir: %v", err) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| defer os.RemoveAll(tempDir) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| store := &FileSystemStore{basePath: tempDir} | ||||||||||||||||||||||||||||||||
| ctx := context.Background() | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| tests := []struct { | ||||||||||||||||||||||||||||||||
| name string | ||||||||||||||||||||||||||||||||
| srcPath string | ||||||||||||||||||||||||||||||||
| dstPath string | ||||||||||||||||||||||||||||||||
| content string | ||||||||||||||||||||||||||||||||
| expectError bool | ||||||||||||||||||||||||||||||||
| errorType error | ||||||||||||||||||||||||||||||||
| }{ | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| name: "move file same directory", | ||||||||||||||||||||||||||||||||
| srcPath: "src.txt", | ||||||||||||||||||||||||||||||||
| dstPath: "dst.txt", | ||||||||||||||||||||||||||||||||
| content: "test content", | ||||||||||||||||||||||||||||||||
| expectError: false, | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| name: "move file to nested directory", | ||||||||||||||||||||||||||||||||
| srcPath: "src2.txt", | ||||||||||||||||||||||||||||||||
| dstPath: "nested/dir/dst2.txt", | ||||||||||||||||||||||||||||||||
| content: "nested content", | ||||||||||||||||||||||||||||||||
| expectError: false, | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| name: "move non-existent file", | ||||||||||||||||||||||||||||||||
| srcPath: "nonexistent.txt", | ||||||||||||||||||||||||||||||||
| dstPath: "dst.txt", | ||||||||||||||||||||||||||||||||
| content: "", | ||||||||||||||||||||||||||||||||
| expectError: true, | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
|
Comment on lines
+407
to
+413
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing The "move non-existent file" case only asserts that some error is returned but doesn't verify it's Proposed fix {
name: "move non-existent file",
srcPath: "nonexistent.txt",
dstPath: "dst.txt",
content: "",
expectError: true,
+ errorType: ErrNotFound,
},📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| for _, test := range tests { | ||||||||||||||||||||||||||||||||
| t.Run(test.name, func(t *testing.T) { | ||||||||||||||||||||||||||||||||
| // Setup: create source file if content is provided | ||||||||||||||||||||||||||||||||
| if test.content != "" { | ||||||||||||||||||||||||||||||||
| srcFullPath := filepath.Join(tempDir, test.srcPath) | ||||||||||||||||||||||||||||||||
| if err := os.WriteFile(srcFullPath, []byte(test.content), 0600); err != nil { | ||||||||||||||||||||||||||||||||
| t.Fatalf("failed to create source file: %v", err) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| err := store.Move(ctx, test.srcPath, test.dstPath) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if test.expectError { | ||||||||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||||||||
| t.Error("expected error but got none") | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||
| t.Fatalf("unexpected error: %v", err) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Verify source file no longer exists | ||||||||||||||||||||||||||||||||
| srcFullPath := filepath.Join(tempDir, test.srcPath) | ||||||||||||||||||||||||||||||||
| if _, err := os.Stat(srcFullPath); !os.IsNotExist(err) { | ||||||||||||||||||||||||||||||||
| t.Error("source file should not exist after move") | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Verify destination file exists with correct content | ||||||||||||||||||||||||||||||||
| dstFullPath := filepath.Join(tempDir, test.dstPath) | ||||||||||||||||||||||||||||||||
| data, err := os.ReadFile(dstFullPath) | ||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||
| t.Fatalf("failed to read destination file: %v", err) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if string(data) != test.content { | ||||||||||||||||||||||||||||||||
| t.Errorf("expected content %q, got %q", test.content, string(data)) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| func TestFileSystemStore_Delete(t *testing.T) { | ||||||||||||||||||||||||||||||||
| tempDir, err := os.MkdirTemp("", "blob-test-*") | ||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||
| t.Fatalf("failed to create temp dir: %v", err) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| defer os.RemoveAll(tempDir) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| store := &FileSystemStore{basePath: tempDir} | ||||||||||||||||||||||||||||||||
| ctx := context.Background() | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| tests := []struct { | ||||||||||||||||||||||||||||||||
| name string | ||||||||||||||||||||||||||||||||
| filePath string | ||||||||||||||||||||||||||||||||
| content string | ||||||||||||||||||||||||||||||||
| expectError bool | ||||||||||||||||||||||||||||||||
| errorType error | ||||||||||||||||||||||||||||||||
| }{ | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| name: "delete existing file", | ||||||||||||||||||||||||||||||||
| filePath: "to-delete.txt", | ||||||||||||||||||||||||||||||||
| content: "delete me", | ||||||||||||||||||||||||||||||||
| expectError: false, | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| name: "delete nested file", | ||||||||||||||||||||||||||||||||
| filePath: "nested/to-delete.txt", | ||||||||||||||||||||||||||||||||
| content: "delete nested", | ||||||||||||||||||||||||||||||||
| expectError: false, | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| name: "delete non-existent file", | ||||||||||||||||||||||||||||||||
| filePath: "nonexistent.txt", | ||||||||||||||||||||||||||||||||
| content: "", | ||||||||||||||||||||||||||||||||
| expectError: true, | ||||||||||||||||||||||||||||||||
| errorType: ErrNotFound, | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| for _, test := range tests { | ||||||||||||||||||||||||||||||||
| t.Run(test.name, func(t *testing.T) { | ||||||||||||||||||||||||||||||||
| // Setup: create file if content is provided | ||||||||||||||||||||||||||||||||
| if test.content != "" { | ||||||||||||||||||||||||||||||||
| fullPath := filepath.Join(tempDir, test.filePath) | ||||||||||||||||||||||||||||||||
| dir := filepath.Dir(fullPath) | ||||||||||||||||||||||||||||||||
| if err := os.MkdirAll(dir, 0755); err != nil { | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||
| t.Fatalf("failed to create directory: %v", err) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if err := os.WriteFile(fullPath, []byte(test.content), 0600); err != nil { | ||||||||||||||||||||||||||||||||
| t.Fatalf("failed to create test file: %v", err) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| err := store.Delete(ctx, test.filePath) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if test.expectError { | ||||||||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||||||||
| t.Error("expected error but got none") | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if test.errorType != nil && !errors.Is(err, test.errorType) { | ||||||||||||||||||||||||||||||||
| t.Errorf("expected error type %v, got %v", test.errorType, err) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||
| t.Fatalf("unexpected error: %v", err) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Verify file no longer exists | ||||||||||||||||||||||||||||||||
| fullPath := filepath.Join(tempDir, test.filePath) | ||||||||||||||||||||||||||||||||
| if _, err := os.Stat(fullPath); !os.IsNotExist(err) { | ||||||||||||||||||||||||||||||||
| t.Error("file should not exist after delete") | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -163,6 +163,47 @@ func (c *GCSStore) Download(ctx context.Context, filePath string) (io.ReadCloser | |
| return rc, nil | ||
| } | ||
|
|
||
| func (c *GCSStore) Move(ctx context.Context, srcPath, dstPath string) error { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 AICR Agent (local Opus) · 🟡 MEDIUM · confidence 80 Move is non-atomic on GCS (copy-then-delete), so a crash between copy and delete leaves a dangling temp object. Unlike the filesystem backend which uses |
||
| gcsClient, err := c.getClient(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to retrieve latest client: %w", err) | ||
| } | ||
|
|
||
| bkt := gcsClient.Bucket(c.config.Bucket) | ||
| srcObj := bkt.Object(srcPath) | ||
| dstObj := bkt.Object(dstPath) | ||
|
|
||
| if _, err := dstObj.CopierFrom(srcObj).Run(ctx); err != nil { | ||
| if errors.Is(err, storage.ErrObjectNotExist) { | ||
| return ErrNotFound | ||
| } | ||
| return fmt.Errorf("failed to copy file from %q to %q: %w", srcPath, dstPath, err) | ||
| } | ||
|
|
||
| if err := srcObj.Delete(ctx); err != nil { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 AICR Agent (local Opus) · 🟡 MEDIUM · confidence 80 Successful-path temp object can leak silently on GCS. On the happy path the temp object is removed only as a side effect of |
||
| log.Ctx(ctx).Warn().Err(err).Msgf( | ||
| "failed to delete source file %q after successful copy to %q", srcPath, dstPath) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (c *GCSStore) Delete(ctx context.Context, filePath string) error { | ||
| gcsClient, err := c.getClient(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to retrieve latest client: %w", err) | ||
| } | ||
|
|
||
| bkt := gcsClient.Bucket(c.config.Bucket) | ||
| if err := bkt.Object(filePath).Delete(ctx); err != nil { | ||
| if errors.Is(err, storage.ErrObjectNotExist) { | ||
| return ErrNotFound | ||
| } | ||
| return fmt.Errorf("failed to delete file %q: %w", filePath, err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func createNewImpersonatedClient(ctx context.Context, cfg Config) (*storage.Client, error) { | ||
| // Use workload identity impersonation default credentials (GKE environment) | ||
| ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing validation that
pointer.OIdactually has thesha256:prefix.strings.TrimPrefixis a no-op when the prefix is absent —expectedHashwould silently become the full OID string. The hash comparison on Line 113 would then always fail, and the uploaded temp file would be deleted. This masks the real problem (unsupported/malformed OID) behind a misleading "content hash doesn't match" error.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents