Bug fix/cleanup#13
Conversation
There was a problem hiding this comment.
Pull request overview
This PR is a broad “bug fix/cleanup” update across the core Buckt library, backends, repositories, and the web client, aiming to improve safety (path traversal + error wrapping), correctness (soft-delete restore + descendant path updates), and operability (migration backend + backend ping), while refreshing dependencies and examples.
Changes:
- Adds safer error-wrapping helpers, max file size enforcement, and content-type detection from bytes.
- Implements/extends soft-delete + restore flows and adds paginated list repository methods.
- Introduces/expands backend migration support (dual-write + background migration) and updates the web UI/handlers accordingly.
Reviewed changes
Copilot reviewed 51 out of 58 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/response/response.go | Adds WrapErrorSafe and cleans up error response formatting. |
| pkg/response/response_test.go | Updates tests and adds coverage for WrapErrorSafe. |
| pkg/logger/logger.go | Fixes/clarifies stdout vs discard selection logic comment/flow. |
| pkg/fileutil/fileutil.go | Adds multipart file processing helper (reads to memory). |
| internal/utils/file.go | Adds exec path validation for preview generation commands. |
| internal/service/folder.go | Attempts restore of soft-deleted folders on unique constraint violations. |
| internal/service/file.go | Adds max file size enforcement, content-type detection, restore-on-unique, hash change, cache decoding tweaks. |
| internal/service/file_test.go | Updates constructor usage for new max-file-size parameter. |
| internal/repository/folder.go | Adds folder restore + pagination, updates move/rename to also update descendant paths, implements name/path mangling on delete. |
| internal/repository/file.go | Adds file restore + pagination, implements name/path mangling on delete. |
| internal/model/user.go | Removes password field from UserModel. |
| internal/model/migration.go | Adds explicit TableName() for migration table. |
| internal/model/file.go | Adds UserID field to file model. |
| internal/model/db.go | Adds a Pagination helper type and methods. |
| internal/mocks/fs_old.go | Removes old filesystem mock file. |
| internal/mocks/folder_repo.go | Updates folder repo mock for new methods. |
| internal/mocks/file_repo.go | Updates file repo mock for new pagination method. |
| internal/domain/repository.go | Extends repository interfaces with pagination + restore methods. |
| internal/database/database.go | Updates Migrate signature and optionally migrates MigrationModel. |
| internal/database/database_test.go | Updates migration test for new Migrate(true) signature. |
| internal/backend/migrate.go | Implements migration backend behavior, background migration, and progress tracking. |
| internal/backend/localfs.go | Adds path traversal detection by constraining resolved paths under media dir. |
| go.work | Bumps workspace Go version. |
| go.sum | Updates dependency sums. |
| go.mod | Bumps Go version and updates core dependencies. |
| example/Makefile | Adds Cloudflare target. |
| example/go.mod | Bumps example module Go version and buckt dependency version. |
| example/cloud/migration/main.go | Adds backend ping before running migration example. |
| example/cloud/gcp/main.go | Adds backend ping before running GCP example. |
| example/cloud/cloudflare/main.go | Adds backend ping before running Cloudflare example. |
| example/cloud/azure/main.go | Adds backend ping before running Azure example. |
| example/cloud/aws/main.go | Adds backend ping before running AWS example. |
| cloud/gcp/go.sum | Updates GCP module dependency sums. |
| cloud/gcp/go.mod | Bumps Go version and updates GCP module dependencies. |
| cloud/gcp/backend.go | Adds Ping() and improves upload failure handling. |
| cloud/azure/go.sum | Updates Azure module dependency sums. |
| cloud/azure/go.mod | Bumps Go version and updates Azure module dependencies. |
| cloud/azure/backend.go | Adds Ping(), makes Stat nil-safe, improves Move polling + timeout. |
| cloud/aws/go.sum | Updates AWS module dependency sums. |
| cloud/aws/go.mod | Updates AWS SDK dependencies. |
| cloud/aws/backend.go | Adds Ping() and makes Move delete synchronous/best-effort. |
| client/web/templates/folders.html | Redesigns folder cards, adds context menu + drag/drop handlers. |
| client/web/templates/files.html | Redesigns file cards, adds previews and context menu. |
| client/web/templates/dashboard.html | Major UX refresh, modals, context menus, drag/drop move via forms. |
| client/web/templates/base.html | Updates layout/branding and adds new styles for cards/drag/drop. |
| client/web/router/router.go | Switches rename/move endpoints to POST routes. |
| client/web/middleware/middleware.go | Minor control-flow cleanup in middleware. |
| client/web/go.sum | Updates web client module sums. |
| client/web/go.mod | Bumps Go version and updates web module dependencies. |
| client/web/buckt_templates.go | Adds formatSize template helper and registers it. |
| client/web/app/web.go | Improves view model, HTMX delete responses, implements MoveFile, adds safe Content-Disposition helper, switches to pkg/fileutil. |
| client/web/app/api.go | Implements GetSubFolders/GetDescendants, adds safe Content-Disposition helper, switches to pkg/fileutil. |
| buckt.go | Wires max file size into services, updates migrate call, improves reader upload path, improves backend resolution. |
| buckt_test.go | Updates backend resolution tests to use PlaceholderBackend and new resolver. |
| buckt_conf.go | Adds max file size config + WithMaxFileSize, introduces PlaceholderBackend for local backend selection. |
| .gitignore | Adds ignores for sqlite/logs/media/bin and improves OS file ignores. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 57 out of 64 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (2)
client/web/app/web.go:218
- Upload handlers read the entire multipart file into memory via
fileutil.ProcessFile(file)before any size enforcement. Even though FileService enforcesmaxFileSize, the server can still OOM on large uploads. UseProcessFileWithLimit(and return 413 on overflow), ideally using the configured max size (or at leastbuckt.DefaultMaxFileSize) so the request is bounded before buffering.
files := form.File["files"]
// Loop through each file
for _, file := range files {
// Save each file (this example just prints)
fileName, fileByte, err := fileutil.ProcessFile(file)
if err != nil {
c.AbortWithStatusJSON(500, response.WrapError("failed to process file", err))
return
}
_, err = svc.client.UploadFile(user_id, folderID, fileName, file.Header.Get("Content-Type"), fileByte)
if err != nil {
client/web/app/api.go:246
- This API endpoint buffers the entire uploaded file into memory with
fileutil.ProcessFile(file)and does not apply any max-size limit. This can cause excessive memory usage / OOM before downstream size checks run. PreferProcessFileWithLimit(return 413 when exceeded) and wire it to the same max file size configuration used by the Buckt client.
// UploadFile implements domain.APIService.
func (svc *APIService) UploadFile(c *gin.Context) {
// get the user_id from the context
user_id := c.GetString("owner_id")
file, err := c.FormFile("file")
if err != nil {
c.AbortWithStatusJSON(400, response.Error("file is required", err.Error()))
return
}
// get the parent_id from the request
parentID := c.PostForm("parent_id")
if parentID == "" {
c.AbortWithStatusJSON(400, response.Error("parent_id is required", ""))
return
}
// Read file from request
fileName, fileByte, err := fileutil.ProcessFile(file)
if err != nil {
c.AbortWithStatusJSON(500, response.WrapError("failed to process file", err))
return
}
fileID, err := svc.client.UploadFile(user_id, parentID, fileName, file.Header.Get("Content-Type"), fileByte)
if err != nil {
c.AbortWithStatusJSON(500, response.WrapError("failed to create file", err))
return
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 57 out of 64 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 58 out of 65 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (2)
client/web/app/web.go:214
UploadFilereads each multipart file fully into memory viafileutil.ProcessFile(no limit), beforebucktenforcesMaxFileSize. This allows very large uploads to cause high memory usage / OOM even if they’ll later be rejected. Prefer streaming toUploadFileFromReader(usingfile.Open()), or callProcessFileWithLimitwith the configured max size so the request is bounded before allocation.
files := form.File["files"]
// Loop through each file
for _, file := range files {
// Save each file (this example just prints)
fileName, fileByte, err := fileutil.ProcessFile(file)
if err != nil {
c.AbortWithStatusJSON(500, response.WrapError("failed to process file", err))
return
}
_, err = svc.client.UploadFile(user_id, folderID, fileName, file.Header.Get("Content-Type"), fileByte)
if err != nil {
client/web/app/api.go:241
- This API upload path also reads the entire multipart file into memory (
fileutil.ProcessFile) with no size limit, which can defeatMaxFileSizeprotections and enable memory exhaustion on large uploads. Prefer streaming toUploadFileFromReaderor usingProcessFileWithLimitwith a max size before callingUploadFile.
// UploadFile implements domain.APIService.
func (svc *APIService) UploadFile(c *gin.Context) {
// get the user_id from the context
user_id := c.GetString("owner_id")
file, err := c.FormFile("file")
if err != nil {
c.AbortWithStatusJSON(400, response.Error("file is required", err.Error()))
return
}
// get the parent_id from the request
parentID := c.PostForm("parent_id")
if parentID == "" {
c.AbortWithStatusJSON(400, response.Error("parent_id is required", ""))
return
}
// Read file from request
fileName, fileByte, err := fileutil.ProcessFile(file)
if err != nil {
c.AbortWithStatusJSON(500, response.WrapError("failed to process file", err))
return
}
fileID, err := svc.client.UploadFile(user_id, parentID, fileName, file.Header.Get("Content-Type"), fileByte)
if err != nil {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 58 out of 65 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (2)
client/web/app/web.go:218
- UploadFile reads each multipart upload fully into memory via fileutil.ProcessFile (io.ReadAll) and then calls client.UploadFile with a []byte. With the new MaxFileSize enforcement, this still allows a large upload to allocate unbounded memory before the size check triggers, which is a DoS risk. Consider streaming to buckt via UploadFileFromReader(…)/UploadFileFromReaderContext (so the core size limiting logic can apply during reads) or using ProcessFileWithLimit with the configured max size and rejecting before reading the whole body.
// UploadFile implements domain.WebService.
func (svc *WebService) UploadFile(c *gin.Context) {
user_id := c.GetString("owner_id")
folderID := c.PostForm("folder_id")
if folderID == "" {
c.AbortWithStatusJSON(400, gin.H{"error": "folder_id is required"})
return
}
// Handle file upload
form, err := c.MultipartForm()
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": "Invalid form data"})
return
}
files := form.File["files"]
// Loop through each file
for _, file := range files {
// Save each file (this example just prints)
fileName, fileByte, err := fileutil.ProcessFile(file)
if err != nil {
c.AbortWithStatusJSON(500, response.WrapError("failed to process file", err))
return
}
_, err = svc.client.UploadFile(user_id, folderID, fileName, file.Header.Get("Content-Type"), fileByte)
if err != nil {
c.AbortWithStatusJSON(500, response.WrapError("failed to create file", err))
return
}
client/web/app/api.go:244
- API UploadFile fully buffers the uploaded multipart file into memory via fileutil.ProcessFile (io.ReadAll) before calling client.UploadFile. This bypasses the new MaxFileSize protections until after the allocation/read completes and can allow memory exhaustion on large requests. Prefer streaming into buckt with UploadFileFromReader(…)/UploadFileFromReaderContext, or enforce an early bound with ProcessFileWithLimit using the same max size limit configured for the client.
// UploadFile implements domain.APIService.
func (svc *APIService) UploadFile(c *gin.Context) {
// get the user_id from the context
user_id := c.GetString("owner_id")
file, err := c.FormFile("file")
if err != nil {
c.AbortWithStatusJSON(400, response.Error("file is required", err.Error()))
return
}
// get the parent_id from the request
parentID := c.PostForm("parent_id")
if parentID == "" {
c.AbortWithStatusJSON(400, response.Error("parent_id is required", ""))
return
}
// Read file from request
fileName, fileByte, err := fileutil.ProcessFile(file)
if err != nil {
c.AbortWithStatusJSON(500, response.WrapError("failed to process file", err))
return
}
fileID, err := svc.client.UploadFile(user_id, parentID, fileName, file.Header.Get("Content-Type"), fileByte)
if err != nil {
c.AbortWithStatusJSON(500, response.WrapError("failed to create file", err))
return
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 58 out of 65 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (1)
client/web/app/api.go:238
fileutil.ProcessFilebuffers the entire upload in memory without a size cap. Even with the new max file size enforcement in Buckt, this handler can allocate arbitrarily large payloads before the limit check happens, which is a DoS risk. Consider switching toProcessFileWithLimitand/or streaming uploads (UploadFileFromReader) with anio.LimitReaderat the HTTP layer.
// Read file from request
fileName, fileByte, err := fileutil.ProcessFile(file)
if err != nil {
c.AbortWithStatusJSON(500, response.WrapError("failed to process file", err))
return
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 58 out of 65 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 59 out of 66 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 59 out of 66 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 59 out of 66 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 59 out of 66 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func (db *DB) cleanupLegacySoftDeletes() error { | ||
| hasFileCol := db.Migrator().HasColumn(&model.FileModel{}, "deleted_at") | ||
| hasFolderCol := db.Migrator().HasColumn(&model.FolderModel{}, "deleted_at") | ||
|
|
| // Refuse oversized batches before doing any work — protects against | ||
| // runaway operations on large folder trees that would tie up the DB | ||
| // transaction and storage backend with thousands of sequential moves. | ||
| if f.maxTrashBatchSize > 0 && len(fileMoves) > f.maxTrashBatchSize { | ||
| return fmt.Errorf("folder contains %d files which exceeds the max trash batch size of %d; delete subtrees first", len(fileMoves), f.maxTrashBatchSize) | ||
| } |
| // Create the file | ||
| err = f.repo.Create(ctx, file) | ||
| if err != nil { | ||
| if err.Error() == "UNIQUE constraint failed: file_models.name, file_models.parent_id" { | ||
| file, err = f.repo.RestoreFile(ctx, file.ParentID, file.Name) | ||
| if err != nil { | ||
| return "", f.logger.WrapError("failed to restore file", err) | ||
| } | ||
| } else { | ||
| return "", f.logger.WrapError("failed to create file", err) | ||
| } | ||
| } else { | ||
| // Write the file to the file system | ||
| if err := f.fileBackend.Put(ctx, file.Path, file_data); err != nil { | ||
| return "", err | ||
| } | ||
| if err = f.repo.Create(ctx, file); err != nil { | ||
| return "", f.logger.WrapError("failed to create file", err) | ||
| } | ||
|
|
||
| // Write the file to the backend | ||
| if err := f.fileBackend.Put(ctx, file.Path, file_data); err != nil { | ||
| return "", err | ||
| } |
Bug Fixes