feat: add file upload & storage support (Roadmap 2.2)#267
Conversation
Add file/image field types to the code generator, enabling resources like `lvt gen resource gallery title:string photo:image doc:file`. New packages: - pkg/storage: Store interface with LocalStore (filesystem) and S3Store - pkg/s3presigner: S3 presigned URL generator (ported from livetemplate core) - pkg/imaging: thumbnail generation using disintegration/imaging Parser & generator: - file/image types in parser with IsFile/IsImage flags on Field/FieldData - File fields excluded from AddInput/UpdateInput (data arrives via upload protocol) - NonFileFields(), FileFields() helpers on ResourceData - UseUpload flag on ComponentUsage drives conditional code generation - getDisplayField skips file/image fields - Type inference for common names (avatar, photo, document, etc.) Templates (multi + single kits): - Handler: WithUpload config, ctx.GetCompletedUploads → storage.Save, defer f.Close(), error-checked GetByID, file cleanup on delete - Form: file input with lvt-upload attribute, upload progress, edit preview - Detail: image preview, file download link - SQL: file fields expand to 4 columns (url, filename, content_type, size) Route injection skipped for upload resources (requires manual storage setup). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds first-class file/image upload support to LiveTemplate’s code generator and runtime helpers, enabling lvt gen resource ... photo:image doc:file to generate upload-capable handlers, templates, and SQL schema.
Changes:
- Introduces
file/imagefield types in the parser + generator plumbing, including expanded SQL columns for file metadata. - Adds new runtime packages for storage backends (
pkg/storage), S3 presigned PUT URL generation (pkg/s3presigner), and thumbnail generation (pkg/imaging). - Updates resource handler + UI templates to configure uploads and process completed uploads into storage.
Reviewed changes
Copilot reviewed 33 out of 34 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| testdata/golden/resource_handler.go.golden | Updates baseline handler golden output. |
| testdata/golden/file_upload_handler.go.golden | Adds golden output for an upload-enabled resource handler. |
| pkg/storage/storage.go | Adds storage.Store interface for upload persistence. |
| pkg/storage/local.go | Implements filesystem-backed LocalStore. |
| pkg/storage/s3.go | Implements S3-backed S3Store. |
| pkg/storage/storage_test.go | Adds tests for LocalStore + S3Store URL behavior. |
| pkg/s3presigner/s3presigner.go | Adds S3 presigned PUT URL generator. |
| pkg/s3presigner/s3presigner_test.go | Adds tests for S3 presigning + key generation/sanitization. |
| pkg/imaging/thumbnail.go | Adds thumbnail generation helper using disintegration/imaging. |
| pkg/imaging/thumbnail_test.go | Adds tests for thumbnail generation behavior. |
| internal/parser/fields.go | Adds file/image parsing + IsFile/IsImage flags. |
| internal/parser/fields_test.go | Adds tests for file/image field parsing + metadata. |
| internal/generator/components.go | Detects upload usage (UseUpload) based on fields. |
| internal/generator/types.go | Adds FileFields/NonFileFields helpers and IsFile/IsImage in template data. |
| internal/generator/resource.go | Skips route auto-injection when uploads require storage wiring. |
| internal/kits/system/single/templates/resource/schema.sql.tmpl | Expands file/image fields to 4 DB columns in schema. |
| internal/kits/system/single/templates/resource/migration.sql.tmpl | Expands file/image fields to 4 DB columns in migrations. |
| internal/kits/system/single/templates/resource/queries.sql.tmpl | Expands INSERT/UPDATE columns/placeholders for file fields. |
| internal/kits/system/single/templates/resource/embedded_queries.sql.tmpl | Expands embedded resource SQL for file fields. |
| internal/kits/system/single/templates/resource/handler.go.tmpl | Generates upload config + completed-upload → storage save flow. |
| internal/kits/system/single/components/form.tmpl | Adds upload inputs + progress/error UI and edit previews. |
| internal/kits/system/single/components/detail.tmpl | Renders images/files on detail page. |
| internal/kits/system/multi/templates/resource/schema.sql.tmpl | Expands file/image fields to 4 DB columns in schema. |
| internal/kits/system/multi/templates/resource/migration.sql.tmpl | Expands file/image fields to 4 DB columns in migrations. |
| internal/kits/system/multi/templates/resource/queries.sql.tmpl | Expands INSERT/UPDATE columns/placeholders for file fields. |
| internal/kits/system/multi/templates/resource/embedded_queries.sql.tmpl | Expands embedded resource SQL for file fields. |
| internal/kits/system/multi/templates/resource/handler.go.tmpl | Generates upload config + completed-upload → storage save flow. |
| internal/kits/system/multi/components/form.tmpl | Adds upload inputs + progress/error UI and edit previews. |
| internal/kits/system/multi/components/detail.tmpl | Renders images/files on detail page. |
| commands/gen.go | Prints manual route + storage setup instructions for upload resources. |
| integration_test.go | Adds integration coverage for upload resource generation and full-flow compile. |
| golden_test.go | Refactors golden testing helper + adds upload handler golden test. |
| go.mod | Adds imaging dependency (and x/image indirect). |
| go.sum | Records new module sums for imaging + transitive deps. |
| state.Filtered[[.ResourceNamePlural]] = [][[.ResourceName]]Item{} | ||
| query := strings.ToLower(state.SearchQuery) | ||
| for _, item := range [[.ResourceNameLower]]s { | ||
| // Search across all text fields | ||
| // Search across all text fields (skip file/image fields) | ||
| [[- range .Fields]] |
There was a problem hiding this comment.
This hunk still relies on the [[.ResourceNameLower]]s pattern for plural identifiers (e.g., for _, item := range [[.ResourceNameLower]]s). For resource names like "gallery" this generates awkward/incorrect plurals like gallerys. Consider using [[.TableName]] (already pluralized) or otherwise deriving a proper plural-lower name for template identifiers.
| // Store is the interface for file storage backends. | ||
| type Store interface { | ||
| Save(ctx context.Context, key string, reader io.Reader) error | ||
| Open(ctx context.Context, key string) (io.ReadCloser, error) | ||
| Delete(ctx context.Context, key string) error | ||
| URL(key string) string |
There was a problem hiding this comment.
The Store interface uses parameter name key for Open/Delete/URL, but LocalStore.Delete accepts either a key or a URL (strips urlPrefix) and the generator appears to persist URLs. This makes the contract ambiguous and is currently incompatible with S3Store. Consider clarifying the contract in the interface docs (key vs URL) and aligning all implementations + generated code accordingly.
| func (s *LocalStore) Save(_ context.Context, key string, reader io.Reader) error { | ||
| path := filepath.Join(s.baseDir, filepath.FromSlash(key)) | ||
|
|
There was a problem hiding this comment.
LocalStore.Save builds the destination path with filepath.Join(baseDir, filepath.FromSlash(key)) without validating/sanitizing key. If key contains path traversal segments (e.g. "../"), this can write outside baseDir and overwrite arbitrary files. Consider cleaning the key and rejecting absolute/parent-relative paths, and/or verifying the final path is within baseDir before creating directories/files.
| key := fmt.Sprintf("[[$.TableName]]/%s/%s", id, entry.ClientName) | ||
| if err := c.Store.Save(dbCtx, key, f); err != nil { | ||
| return state, fmt.Errorf("failed to save file: %w", err) | ||
| } | ||
| [[.Name]]Val = c.Store.URL(key) | ||
| [[.Name]]Filename = entry.ClientName | ||
| [[.Name]]ContentType = entry.ClientType | ||
| [[.Name]]Size = entry.ClientSize |
There was a problem hiding this comment.
The storage key includes entry.ClientName directly. Client-provided filenames can contain path separators or traversal sequences ("../", "..\"), which becomes part of the storage key and can lead to unsafe paths for LocalStore. Sanitize the filename (e.g., strip directories, normalize separators) before building the key, or ensure the storage backend enforces a safe key policy.
| [[- if .Components.UseUpload]] | ||
| // Delete associated files from storage | ||
| if existing, err := c.Queries.Get[[.ResourceNameSingular]]ByID(dbCtx, state.PendingDeleteID); err == nil { | ||
| [[- range .FileFields]] | ||
| if existing.[[.Name | camelCase]] != "" { | ||
| _ = c.Store.Delete(dbCtx, existing.[[.Name | camelCase]]) | ||
| } |
There was a problem hiding this comment.
ConfirmDelete passes existing. to Store.Delete, but earlier the code sets to c.Store.URL(key), so the DB value is a URL rather than the storage key. This will break backends that expect a key (e.g. the current S3Store.Delete) and makes deletions unreliable when CDN prefixes/custom endpoints are used. Consider storing the storage key in the DB (derive the public URL at render time), or standardize Store.Delete/Open to accept URLs and reliably normalize them back to keys for all backends.
| // Delete removes the object at the given key from S3. | ||
| func (s *S3Store) Delete(ctx context.Context, key string) error { | ||
| _, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{ | ||
| Bucket: aws.String(s.config.Bucket), | ||
| Key: aws.String(key), | ||
| }) | ||
| if err != nil { | ||
| return fmt.Errorf("storage: s3 delete: %w", err) | ||
| } |
There was a problem hiding this comment.
S3Store.Delete treats its argument as a raw S3 object key, but generated handlers store file fields as Store.URL(key) and then pass the stored value into Delete. That means S3Store.Delete will be called with a full URL and will attempt to delete the wrong key. Either normalize URL→key inside S3Store (similar to LocalStore.resolveKey), or change the generator/data model to persist object keys rather than URLs.
| // generateKey creates S3 object key from upload entry. | ||
| // Format: {KeyPrefix}/{entryID}/{sanitized_filename} | ||
| func (p *S3Presigner) generateKey(entry *livetemplate.UploadEntry) string { | ||
| filename := filepath.Base(entry.ClientName) | ||
|
|
There was a problem hiding this comment.
generateKey uses filepath.Base(entry.ClientName) as its only sanitization. On non-Windows builds, filepath.Base does not treat backslashes ("\") as path separators, so a client filename like "..\..\evil.txt" won’t be reduced to a safe basename. Consider normalizing separators (replace "\" with "/") and using path.Base or otherwise rejecting any name containing path traversal characters.
| if len(data) == 0 { | ||
| t.Error("GenerateThumbnail() returned empty result") | ||
| } | ||
|
|
||
| // Verify JPEG header (starts with 0xFF 0xD8) | ||
| if data[0] != 0xFF || data[1] != 0xD8 { | ||
| t.Error("GenerateThumbnail() output is not JPEG") |
There was a problem hiding this comment.
This test indexes data[0] and data[1] after only checking len(data) == 0. If the output were unexpectedly 1 byte long, this would panic and obscure the real failure. Guard with len(data) < 2 before checking the JPEG magic bytes.
| key := fmt.Sprintf("[[$.TableName]]/%s/%s", id, entry.ClientName) | ||
| if err := c.Store.Save(dbCtx, key, f); err != nil { | ||
| return state, fmt.Errorf("failed to save file: %w", err) | ||
| } | ||
| [[.Name]]Val = c.Store.URL(key) | ||
| [[.Name]]Filename = entry.ClientName | ||
| [[.Name]]ContentType = entry.ClientType | ||
| [[.Name]]Size = entry.ClientSize |
There was a problem hiding this comment.
The storage key includes entry.ClientName directly. Client-provided filenames can contain path separators or traversal sequences ("../", "..\"), which becomes part of the storage key and can lead to unsafe paths for LocalStore. Sanitize the filename (e.g., strip directories, normalize separators) before building the key, or ensure the storage backend enforces a safe key policy.
| [[- if .Components.UseUpload]] | ||
| // Delete associated files from storage | ||
| if existing, err := c.Queries.Get[[.ResourceNameSingular]]ByID(dbCtx, state.PendingDeleteID); err == nil { | ||
| [[- range .FileFields]] | ||
| if existing.[[.Name | camelCase]] != "" { | ||
| _ = c.Store.Delete(dbCtx, existing.[[.Name | camelCase]]) | ||
| } |
There was a problem hiding this comment.
ConfirmDelete passes existing. to Store.Delete, but earlier the code sets to c.Store.URL(key), so the DB value is a URL rather than the storage key. This will break backends that expect a key (e.g. the current S3Store.Delete) and makes deletions unreliable when CDN prefixes/custom endpoints are used. Consider storing the storage key in the DB (derive the public URL at render time), or standardize Store.Delete/Open to accept URLs and reliably normalize them back to keys for all backends.
Code Review: feat: add file upload & storage support (Roadmap 2.2)This is a substantial, well-structured PR. The storage abstraction is clean, test coverage is thorough, and the golden file + integration test approach gives strong confidence in generated output. A few items to address before merging: SecurityPath traversal in
path := filepath.Join(s.baseDir, filepath.FromSlash(key))
In practice, the generated handler builds keys from server-controlled upload entry IDs, and Suggested fix (add to all three methods, or factor into a shared func (s *LocalStore) resolvePath(key string) (string, error) {
path := filepath.Join(s.baseDir, filepath.FromSlash(key))
base := filepath.Clean(s.baseDir) + string(os.PathSeparator)
if !strings.HasPrefix(path, base) {
return "", fmt.Errorf("storage: key escapes base directory")
}
return path, nil
}Bugs
The handler template calls Fix: add key resolution to
_, err := s.client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.config.Bucket),
Key: aws.String(key),
Body: reader,
})Objects will be stored with Dependencies
This is ~6 years old. The current stable tagged version is The Minor / NitpicksManual setup instructions hard-code The post-generation message in This is helpful for quick-start, but users who have an S3 setup get no guidance. Could mention both options or link to docs.
What's well done
SummaryThe two most important items: |
Security: - LocalStore: add safePath() to reject path traversal in Save/Open/Delete - S3Presigner: normalize backslashes before extracting filename basename - Handler template: sanitize client filenames with path.Base before building storage keys Store interface: - Clarify interface docs: Delete accepts both keys and URLs - S3Store: add resolveKey() to strip known URL prefixes in Delete - Aligns S3Store.Delete behavior with LocalStore.Delete Templates: - Add rel="noopener noreferrer" to target="_blank" links in detail views - Fix err := vs err = scoping in Update method when UseUpload varies CLI: - Add resource package import to manual route setup instructions Tests: - Guard JPEG header check with len(data) < 2 to prevent panic Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR Review: feat: add file upload and storage support (Roadmap 2.2)\n\nThis is a solid, well-structured feature addition. The storage abstraction is clean, test coverage is thorough, and path traversal protection is handled correctly in both the S3 key generator and the LocalStore. The page-mode refactor from per-request handler cloning to a shared handler with query-param routing is a meaningful improvement. Notes below are roughly ordered by severity.\n\n---\n\n### Bugs / Correctness\n\n1. Old files not deleted on update (resource leak)\n\nIn the generated Update handler, when a new file is uploaded the old file stored at existing.Photo / existing.Doc is never removed from storage. Only ConfirmDelete cleans up files. Over time this will silently accumulate orphaned files in both LocalStore and S3.\n\n2. Stale golang.org/x/image dependency\n\nThe pinned commit is from October 2019 (~6 years old). Worth bumping to at least v0.18.0.\n\n---\n\n### Design / API Concerns\n\n3. pkg/imaging is not wired into generated code\n\nGenerateThumbnail is implemented and tested but the generated handler template never calls it.\n\n4. disintegration/imaging is an archived package\n\nThe upstream repo is archived (read-only, no new releases).\n\n5. No server-side MIME type validation\n\nThe generated handler trusts entry.ClientType (client-provided) for content type.\n\n---\n\n### Minor / Polish\n\n6. Silent file deletion errors in ConfirmDelete - a log.Printf would help operators detect orphaned files.\n\n7. queries.sql.tmpl UPDATE uses .Fields instead of .NonReferenceFields\n\n8. _resource_id query param: mutating r.URL directly - Passing via context.WithValue would be cleaner.\n\n---\n\n### Positives\n\n- Clean Store interface with consistent URL-vs-key semantics\n- Path traversal protection is correct in safePath and key sanitization\n- IsFile fields are correctly excluded from search, sort, and form input structs\n- Golden file tests for the upload handler are a nice addition\n- Full flow test (generate -> sqlc -> build) that catches schema/query/model alignment is excellent\n- The page-mode handler refactor is a real improvement over per-request template cloning |
Code Review: feat: add file upload & storage support (Roadmap 2.2)This is a well-scoped, well-tested feature addition. The architecture is clean -- Store interface with LocalStore/S3Store implementations, clear field flag propagation (IsFile/IsImage), and good coverage with unit + golden + integration tests. A few issues worth addressing before merge. 1. Old file not deleted on update (storage leak): In handler.go.tmpl, the Update handler reads the existing record and falls back to existing file values when no new upload is provided -- but it never deletes the old file from storage when a replacement is uploaded. Over time this leaks orphaned files in both LocalStore and S3. After saving the new file, the old URL should be passed to c.Store.Delete. 2. S3Store.Save() drops the Content-Type: pkg/storage/s3.go calls PutObject without setting ContentType. Files served directly from S3 will default to application/octet-stream, so images will not render inline in browsers. Compare with the presigner which correctly sets ContentType. 3. defer f.Close() inside a range loop: In the generated Add and Update handlers, each iteration over FileFields opens a temp file and defers its close. With multiple file fields the defers only fire when the function returns, holding OS file descriptors for the full handler duration. Prefer closing explicitly after Store.Save returns. 4. Client-supplied MIME type is not server-validated: The generated handler stores entry.ClientType directly in the database as ContentType. This value comes from the browser and can be spoofed. For image fields, consider validating actual bytes (http.DetectContentType) before saving. 5. Filename stored verbatim: entry.ClientName is stored as-is and rendered into the DOM. Go html/template auto-escapes in most contexts so this is likely safe, but worth a deliberate check -- especially the href rendering path. 6. 10 MB limit is hardcoded: MaxFileSize: 10 << 20 will be wrong for many use cases. Consider making this a generator argument or named constant. 7. pkg/imaging is shipped but not wired: pkg/imaging.GenerateThumbnail is added with tests but the generated handler never calls it. Either wire it in for image fields or document it as a manual utility. 8. S3Store.Delete() is not idempotent: LocalStore.Delete() silently ignores os.IsNotExist; S3Store.Delete() propagates any error. Aligning the two implementations is cleaner. 9. Redundant fieldTypeTable entries for file/image: These are also handled by the early-return branch in ParseFields that sets IsFile/IsImage. The table entries produce a Field without those flags set, creating a subtly different code path if MapType is called directly. 10. err2 variable in integration test: integration_test.go uses err2 when reading main.go. The prior err has already been checked and can be reused. Test coverage that is good: parser tests for IsFile/IsImage flags, LocalStore round-trip, path traversal prevention in safePath and generateKey, golden file for file-upload handler, content-validity assertions in TestFileUploadResourceGeneration, full compile flow in TestFileUploadFullFlow. Missing coverage to consider: S3Store.Delete with a non-existent key; verification that old file is cleaned up during Update (once issue 1 is fixed); Content-Type header on S3Store.Save. Overall this is solid work. The critical items are 1 (storage leak on update) and 2 (missing Content-Type on S3 upload) -- the rest are improvements. |
|
REPLACE_COMMENT_PLACEHOLDER |
|
This is a solid, well-structured feature addition. The storage abstraction is clean, test coverage is thorough, and path traversal protection is handled correctly in both the S3 key generator and the LocalStore. The page-mode refactor is a meaningful improvement. Notes below are roughly ordered by severity. Bug 1 - Old files not deleted on update: In the generated Update handler, when a new file is uploaded the old file at existing.Photo or existing.Doc is never removed from storage. Only ConfirmDelete cleans up files. Over time orphaned files accumulate in both LocalStore and S3. Fix: delete the old key before saving the new one, similar to the ConfirmDelete pattern. Bug 2 - Stale golang.org/x/image dependency: v0.0.0-20191009234506-e7c1f5e7dbb8 is from October 2019 (~6 years old). The current release includes security and correctness fixes. Worth bumping to at least v0.18.0. Design 1 - pkg/imaging is not wired into generated code: GenerateThumbnail is implemented and tested but the generated handler template never calls it. Files are saved as-is, so image resources are not actually thumbnailed. Either wire it into the handler template or clarify in the package doc that this is a utility callers opt into manually. Design 2 - disintegration/imaging is an archived package: The upstream repo is archived (read-only, no new releases). For a code-generation tool shipping a new pkg/imaging package, this is a long-term maintenance concern. Alternatives include golang.org/x/image directly or stdlib image/* packages for basic resize. Design 3 - No server-side MIME type validation: The generated handler trusts entry.ClientType (client-provided) for content type. For image fields there is no server-side check that the uploaded bytes are actually an image. Consider using net/http.DetectContentType on the first 512 bytes before saving. Minor 1 - Silent file deletion errors in ConfirmDelete: Ignoring Store.Delete errors is reasonable since the DB row is deleted regardless, but a log.Printf would help operators detect orphaned files. Minor 2 - queries.sql.tmpl UPDATE uses .Fields instead of .NonReferenceFields: Both queries.sql.tmpl and embedded_queries.sql.tmpl use .Fields for the UPDATE SET clause, meaning foreign key reference fields are included in UPDATE. Pre-existing behavior, but since this PR touches these templates it might be worth aligning with the handler use of NonReferenceFields. Minor 3 - Mutating r.URL directly in page-mode handler: r.URL.RawQuery = q.Encode() mutates the incoming request URL in place. Passing the resource ID via context.WithValue would be cleaner and avoid potential edge cases with middleware that reads the URL. Positives: Clean Store interface, correct path traversal protection in safePath and key sanitization, IsFile fields correctly excluded from search/sort/input structs, golden file tests for upload handler, excellent full flow test (generate->sqlc->build), and the page-mode refactor to a single shared handler is a real improvement over per-request template cloning. |
…mage - Re-add old file deletion in Update handler when a replacement is uploaded - Bump golang.org/x/image from 2019 commit to v0.37.0 - Update golden files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR Review: feat: add file upload and storage support (Roadmap 2.2)This is a solid, well-structured feature addition. Good test coverage and clean architecture. Below are observations ranging from bugs to minor nits. Bugs / Correctness Issues1. S3Store.Save missing ContentType
2.
3. go.sum missing /go.mod entry for golang.org/x/tools v0.42.0 The diff adds the Security Concerns4. No MIME type validation for image fields The generated handler stores any uploaded file without verifying it is actually an image. An attacker could upload HTML, SVG, or an executable to an image field. Consider validating that 5. Client-reported file size stored verbatim The generated code uses 6. LocalStore.FileServer() serves entire baseDir without auth
Design / API Issues7. Store.Delete accepting URLs is a surprising contract The interface says "Delete also accepts URLs returned by URL()" but callers must remember this dual key/URL behavior. The generated handler passes a URL (the existing stored URL) to 8. Inconsistent not-found behavior: LocalStore vs S3Store
Minor Issues
What is Done Well
The most important items before merging: 1 (S3 ContentType, affects correctness of the primary use case), 3 (go.sum integrity), and 4 (MIME validation in generated code, since this lands in user apps). Generated with Claude Code |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code Review: feat: add file upload & storage support (Roadmap 2.2)Great work on this feature — a substantial, well-structured addition. The Security[Medium]
http.FileServer(http.Dir(s.baseDir))
type noListFS struct{ http.FileSystem }
func (f noListFS) Open(name string) (http.File, error) {
file, err := f.FileSystem.Open(name)
if err != nil { return nil, err }
if stat, _ := file.Stat(); stat.IsDir() { file.Close(); return nil, fs.ErrPermission }
return file, nil
}[Low]
for _, prefix := range []string{
s.config.CDNPrefix + "/", // becomes "/" when CDNPrefix is ""
fmt.Sprintf("%s/%s/", s.config.Endpoint, s.config.Bucket), // becomes "/bucket/" when Endpoint is ""
...The guard Bugs / Correctness[Medium]
// for each file field:
f, err := os.Open(entry.TempPath)
...
defer f.Close() // ← deferred until Add/Update returns, not until after Store.SaveWith N file fields, N descriptors stay open simultaneously. The fix is to close explicitly after f, err := os.Open(entry.TempPath)
...
_, err = c.Store.Save(ctx, key, f, contentType)
f.Close() // explicit, not deferred[Low]
[Low]
Code Quality[Low] Duplicated AWS config initialization in Both packages have nearly identical [Low] Golden file struct alignment likely fails
PhotoContentType: photoContentType,
PhotoSize: photoSize, // extra space hereThe extra space in the template alignment means [Low]
// This test must not be run in parallel — it calls os.Chdir which affects the whole process.Test Coverage Gaps
Performance Notes
Summary
The medium issues should be addressed before merging. The low-priority items are mostly polish and documentation gaps that would be fine in a follow-up. Overall this is solid work — the architecture is clean and the test suite is comprehensive for a feature of this scope. |
- S3Store.Save: infer ContentType from key extension via mime.TypeByExtension - Handler template: replace defer f.Close() with explicit close after Save (avoids holding all FDs in range loop until handler returns) - S3Store.resolveKey: fix empty-endpoint edge case that could match wrong keys - LocalStore.FileServer: disable directory listings (noListDir wrapper) - Integration test: remove err2 variable, reuse err - S3Store.Delete: document idempotent behavior - Update golden files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
fileandimagefield types to the code generator:lvt gen resource gallery title:string photo:image doc:filepkg/storagepackage withStoreinterface,LocalStore(filesystem), andS3Store(AWS S3)pkg/s3presignerpackage ported from livetemplate core (generates presigned PUT URLs)pkg/imagingpackage for thumbnail generation usingdisintegration/imagingWithUploadconfig,ctx.GetCompletedUploads→storage.Saveflow<input type="file" lvt-upload="...">with upload progress and edit previewTest plan
🤖 Generated with Claude Code