Skip to content

Release v0.1.5 — Phase 05: File Storage & Metadata#76

Merged
menvil merged 16 commits into
mainfrom
release/v0.1.5-phase05-file-storage-metadata
May 29, 2026
Merged

Release v0.1.5 — Phase 05: File Storage & Metadata#76
menvil merged 16 commits into
mainfrom
release/v0.1.5-phase05-file-storage-metadata

Conversation

@menvil

@menvil menvil commented May 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • Phase 05 backend foundation for uploaded file storage and metadata (CONV-058 → CONV-070).
  • Adds files table, FileRecord model + factory, FileStatus enum, FileFormatDetector, ImageMetadataExtractor, StoreUploadedFileAction, UploadedFileRules, FileExpirationPolicy, FileRecordCreator boundary, ownership scope, and cleanup-on-failure.
  • No UI, no conversion jobs, no billing — backend-only as scoped.

Included tasks

  • CONV-058 Create FileRecord model and migration
  • CONV-059 Add FileStatus enum
  • CONV-060 Test file format detection
  • CONV-061 Implement FileFormatDetector
  • CONV-062 Test image metadata extraction
  • CONV-063 Implement ImageMetadataExtractor
  • CONV-064 Test uploaded file storage
  • CONV-065 Implement StoreUploadedFileAction
  • CONV-066 Add uploaded file validation rules
  • CONV-067 Add file ownership rules
  • CONV-068 Add file expiration defaults
  • CONV-069 Add stored file cleanup safety test
  • CONV-070 Add file storage smoke tests

Test plan

  • composer test — 166 passed, 414 assertions
  • composer lint — passed
  • npm run build — passed
  • Reviewer: run php artisan migrate:fresh on a clean DB
  • Reviewer: spot-check Phase 05 tests under tests/Feature/Files and tests/Unit/Files

🤖 Generated with Claude Code


Summary by cubic

Adds backend support for storing uploaded files and basic metadata using a new files table and storage pipeline. Implements format detection, image dimension extraction, validation, ownership, expiration, and safe cleanup; fulfills Linear CONV-058–CONV-070.

  • New Features

    • files table and FileRecord model with forUser scope; factory included.
    • FileStatus enum and lifecycle fields (status, expires_at).
    • Storage via StoreUploadedFileAction: detect format, extract image dimensions, save to local disk, compute size and SHA-256 checksum, create record.
    • Validation with UploadedFileRules (png/jpg/jpeg/webp/pdf, max 10 MB).
    • FileExpirationPolicy defaults to 1 day; ownership enforced.
    • Cleanup-on-failure: removes the stored file if record creation fails.
  • Migration

    • Run: php artisan migrate (or migrate:fresh on a clean DB).

Written for commit c4da252. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • File upload system with validation for images and PDFs, maximum 10MB per file
    • Automatic image metadata extraction (dimensions, transparency detection)
    • Configurable file expiration policy with automatic cleanup
    • User-scoped file access and ownership tracking
  • Tests

    • Comprehensive test coverage for file upload pipeline, validation, and storage behavior

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@menvil, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 40 minutes and 4 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ca039fd0-5b93-4468-bf3a-8c57eb45ca06

📥 Commits

Reviewing files that changed from the base of the PR and between c4da252 and 272a374.

📒 Files selected for processing (5)
  • app/Actions/Files/StoreUploadedFileAction.php
  • app/Support/Files/FileExpirationPolicy.php
  • app/Support/Files/ImageMetadataExtractor.php
  • database/migrations/2026_05_29_154149_create_files_table.php
  • tests/Unit/Files/ImageMetadataExtractorTest.php
📝 Walkthrough

Walkthrough

This pull request introduces a complete file upload system for Laravel that orchestrates file format detection, metadata extraction, storage, and database persistence with comprehensive validation and error handling.

Changes

File Upload System with Format Detection and Storage Orchestration

Layer / File(s) Summary
File Record Data Model and Database Schema
app/Enums/FileStatus.php, app/Models/FileRecord.php, database/migrations/2026_05_29_154149_create_files_table.php, database/factories/FileRecordFactory.php, tests/Feature/Files/FileRecordTest.php, tests/Feature/Files/FileStatusEnumTest.php
FileStatus enum defines five upload lifecycle states. FileRecord Eloquent model with fillable attributes, attribute casting (status enum, metadata array, expiration datetime), user relationship, and scopeForUser query scope. Database migration creates files table with user foreign key, file metadata columns, status field defaulting to uploaded, and composite indexes for user and status queries. Factory generates test records with random but consistent attributes.
Upload File Validation Rules
app/Support/Files/UploadedFileRules.php, tests/Unit/Files/UploadedFileRulesTest.php
UploadedFileRules defines maximum upload size (10240 KB) and static rules() method returning validation constraints requiring file presence and restricting MIME types to png, jpg, jpeg, webp, and pdf. Unit tests verify rule structure, allowed MIME list, size constraint, and validation pass/fail outcomes for supported and unsupported uploads.
File Format Detection and Metadata Extraction
app/Exceptions/Files/UnsupportedFileFormatException.php, app/Exceptions/Files/FileMetadataException.php, app/Support/Files/FileFormatDetector.php, app/Support/Files/ImageMetadataExtractor.php, tests/Unit/Files/FileFormatDetectorTest.php, tests/Unit/Files/ImageMetadataExtractorTest.php
FileFormatDetector maps uploaded file MIME types and extensions to canonical formats (png, jpg, webp, pdf) via lookup maps, throwing UnsupportedFileFormatException when no match is found. ImageMetadataExtractor validates format against allowlist, reads file dimensions via getimagesize(), returns width/height/transparency for images, empty array for PDF, and throws FileMetadataException on real path or sizing failure. Exception classes provide static factory methods with formatted messages and optional prior exception chaining.
File Expiration Policy
app/Support/Files/FileExpirationPolicy.php, tests/Feature/Files/FileExpirationPolicyTest.php
FileExpirationPolicy computes expiration timestamps for uploaded and result files as current time plus one day, returning CarbonInterface values. Feature tests assert expiration times are set to the future relative to now().
File Storage Orchestration and Persistence
app/Exceptions/Files/FileStorageException.php, app/Support/Files/FileRecordCreator.php, app/Actions/Files/StoreUploadedFileAction.php, tests/Feature/Files/StoreUploadedFileActionTest.php, tests/Feature/Files/FileStorageSmokeTest.php, tests/Feature/Files/StoreUploadedFileCleanupTest.php, tests/Feature/Files/FileOwnershipTest.php
StoreUploadedFileAction orchestrates the upload pipeline: detects file format, extracts metadata, stores file on local disk under uploads/{user_id} directory, computes SHA-256 checksum, and creates FileRecord with Analyzed status and expiration timestamp. On record creation failure, deletes the stored file and throws FileStorageException with the prior exception attached. FileRecordCreator delegates record creation to FileRecord::query()->create(). Feature tests validate image and PDF storage with correct extensions and metadata, unsupported format rejection, cleanup behavior when record creation fails, and user ownership constraints via the forUser() scope.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the phase of development (Phase 05) and the main focus of the changes (File Storage & Metadata), which directly aligns with the PR's comprehensive implementation of file storage infrastructure, metadata extraction, and database models.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/v0.1.5-phase05-file-storage-metadata

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the release Triggers AI code review (CodeRabbit, Cubic) label May 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
database/migrations/2026_05_29_154149_create_files_table.php (1)

15-15: ⚡ Quick win

Consider adding a unique constraint to stored_path.

If two FileRecord instances accidentally reference the same stored_path, deleting one record (and its associated file on disk) would orphan the other record. While UUID-based path generation makes collisions unlikely, enforcing uniqueness at the database level provides defense-in-depth and prevents data corruption.

♻️ Proposed fix to add unique constraint
-            $table->string('stored_path');
+            $table->string('stored_path')->unique();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@database/migrations/2026_05_29_154149_create_files_table.php` at line 15, Add
a unique constraint on the stored_path column in the CreateFilesTable migration
to prevent duplicate file paths: update the up() migration (where
$table->string('stored_path') is defined) to create a unique index on
stored_path (or make the column unique), and update the down() method to drop
that unique index when rolling back so migrations remain reversible; reference
the CreateFilesTable class, the up() and down() methods, and the stored_path
column when making the change.
app/Support/Files/FileExpirationPolicy.php (1)

11-19: 💤 Low value

Consider consolidating duplicate expiration logic.

Both methods currently return identical values. While they accept a User parameter for future differentiation (e.g., premium users with longer expiration), the logic could be extracted to a private helper to follow DRY principles.

♻️ Optional refactor to reduce duplication
+    private function calculateExpiration(User $user): CarbonInterface
+    {
+        return Date::now()->addDay();
+    }
+
     public function forUploadedFile(User $user): CarbonInterface
     {
-        return Date::now()->addDay();
+        return $this->calculateExpiration($user);
     }

     public function forResultFile(User $user): CarbonInterface
     {
-        return Date::now()->addDay();
+        return $this->calculateExpiration($user);
     }

Note: If these methods are expected to diverge soon (e.g., different expiration rules per file type), the current duplication is acceptable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Support/Files/FileExpirationPolicy.php` around lines 11 - 19, Both
forUploadedFile(User $user) and forResultFile(User $user) return the same
Date::now()->addDay() value; extract that shared logic into a private helper
(e.g., private function calculateExpiration(User $user): CarbonInterface) and
have both forUploadedFile and forResultFile call this helper, preserving their
public signatures so future per-file-type rules (or user-based adjustments) can
be added in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Actions/Files/StoreUploadedFileAction.php`:
- Around line 45-46: In StoreUploadedFileAction, validate the filesystem calls
before creating the record: check that $disk->size($path) does not return false
and that $disk->path($path) is valid and hash_file('sha256', $disk->path($path))
does not return false; if either returns false, throw a clear exception or
return an error response (with a descriptive message) instead of passing false
into the record payload so you avoid storing invalid size/checksum or triggering
a DB error.

In `@app/Support/Files/ImageMetadataExtractor.php`:
- Around line 38-41: The method ImageMetadataExtractor::hasTransparency
currently returns $format !== 'jpg' which checks format capability rather than
per-image transparency; rename it to formatSupportsTransparency and update its
signature/usages and tests (tests/Unit/Files/ImageMetadataExtractorTest.php) to
assert capability rather than actual transparency, OR implement real detection
by changing hasTransparency to accept an image path/resource and inspect the
image's alpha channel (for PNG/WebP use imagecreatefromstring + check for any
non-opaque pixels or
imagecolortransparent/imagealphablending/imagecreatetruecolor logic) and update
calls/tests accordingly so the method's name matches its behavior.

---

Nitpick comments:
In `@app/Support/Files/FileExpirationPolicy.php`:
- Around line 11-19: Both forUploadedFile(User $user) and forResultFile(User
$user) return the same Date::now()->addDay() value; extract that shared logic
into a private helper (e.g., private function calculateExpiration(User $user):
CarbonInterface) and have both forUploadedFile and forResultFile call this
helper, preserving their public signatures so future per-file-type rules (or
user-based adjustments) can be added in one place.

In `@database/migrations/2026_05_29_154149_create_files_table.php`:
- Line 15: Add a unique constraint on the stored_path column in the
CreateFilesTable migration to prevent duplicate file paths: update the up()
migration (where $table->string('stored_path') is defined) to create a unique
index on stored_path (or make the column unique), and update the down() method
to drop that unique index when rolling back so migrations remain reversible;
reference the CreateFilesTable class, the up() and down() methods, and the
stored_path column when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fe282805-6bed-408c-b886-3e1f0a3543b9

📥 Commits

Reviewing files that changed from the base of the PR and between 6298131 and c4da252.

📒 Files selected for processing (23)
  • app/Actions/Files/StoreUploadedFileAction.php
  • app/Enums/FileStatus.php
  • app/Exceptions/Files/FileMetadataException.php
  • app/Exceptions/Files/FileStorageException.php
  • app/Exceptions/Files/UnsupportedFileFormatException.php
  • app/Models/FileRecord.php
  • app/Support/Files/FileExpirationPolicy.php
  • app/Support/Files/FileFormatDetector.php
  • app/Support/Files/FileRecordCreator.php
  • app/Support/Files/ImageMetadataExtractor.php
  • app/Support/Files/UploadedFileRules.php
  • database/factories/FileRecordFactory.php
  • database/migrations/2026_05_29_154149_create_files_table.php
  • tests/Feature/Files/FileExpirationPolicyTest.php
  • tests/Feature/Files/FileOwnershipTest.php
  • tests/Feature/Files/FileRecordTest.php
  • tests/Feature/Files/FileStatusEnumTest.php
  • tests/Feature/Files/FileStorageSmokeTest.php
  • tests/Feature/Files/StoreUploadedFileActionTest.php
  • tests/Feature/Files/StoreUploadedFileCleanupTest.php
  • tests/Unit/Files/FileFormatDetectorTest.php
  • tests/Unit/Files/ImageMetadataExtractorTest.php
  • tests/Unit/Files/UploadedFileRulesTest.php

Comment thread app/Actions/Files/StoreUploadedFileAction.php Outdated
Comment thread app/Support/Files/ImageMetadataExtractor.php Outdated
- StoreUploadedFileAction: validate disk size and hash_file results before persisting record
- ImageMetadataExtractor: rename hasTransparency -> formatSupportsTransparency and key supports_transparency to match capability semantics
- FileExpirationPolicy: extract shared calculateExpiration() helper
- files migration: add unique index on stored_path
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release Triggers AI code review (CodeRabbit, Cubic)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant