Add POST /v1/groups/{group_id}/files — the missing file-creation endpoint. Slices 1-8 (plans 0088-0095) built every operation on existing files (list, rename, get, download, soft-delete, new version, list versions, folder CRUD) but left the initial file+v1 creation gap unfilled. This slice closes it.
Reuses the same two-phase commit pattern as post_new_version (plan 0094):
- Validate MIME type from
Content-Typeheader (fail-fast 415). - Validate
X-File-Nameheader (1-500 chars, no control chars, no leading/trailing whitespace). - Optionally validate
X-Folder-Idheader (folder must exist in group and not be soft-deleted). - Read body bytes with operator cap (
storage.max_patch_bytes). - Compute SHA-256 of body.
- Open Postgres transaction + set RLS context (same
set_rls_contexthelper). - Resolve
created_by_labelfromusers.display_name(same pattern ascreate_folder). ObjectStore::put(blob-first — runs before COMMIT).- INSERT into
files(id, group_id, folder_id, name, current_version=1, total_versions=1, size_bytes, mime_type, created_by, created_by_label). - INSERT into
file_versions(file_id, group_id, version=1, object_key, etag, checksum_sha256, integrity_hmac, size_bytes, mime_type, created_by, created_by_label). - Emit
WorkspaceAuditAction::FileCreatedaudit event (new variant). - COMMIT → 201 +
FileCreatedResponse.
Object key scheme: groups/{group_id}/files/{file_id}/v1/{version_uuid} (consistent with post_new_version's groups/{group_id}/files/{file_id}/v{N}/{version_uuid}).
- Rust stable, Axum 0.8, sqlx 0.8, garraia-storage ObjectStore trait.
- No new dependencies; no schema migration (files/file_versions already exist — migration 003).
- Feature:
garraia-gateway/test-helpersfor integration test harness.
- Two-phase ordering: ObjectStore PUT before Postgres COMMIT (orphaned blob on rollback is acceptable per ADR 0004 §5.3.1).
- No
unwrap()outside tests. - No SQL string concat: all queries use
sqlx::query()withbind(). - No PII in audit metadata: carry
name_lennotnamein audit payload. - MIME allow-list: delegate to
garraia_storage::mime_allowlist::is_mime_allowed. - Cross-group guard:
path_group_idmust equalprincipal.group_id(same as all other file endpoints). - RLS context: set both
app.current_user_idANDapp.current_group_idvia parameterizedset_config(plan 0056 pattern).
-
filesschema:name NOT NULL CHECK (length(name) BETWEEN 1 AND 500),folder_idnullable with compound FK →folders(id, group_id)(MATCH SIMPLE, so NULL is valid for root files). Verified in migration 003. -
file_versionsschema: compound FK(file_id, group_id) REFERENCES files(id, group_id)+object_key UNIQUE. Verified in migration 003. -
WorkspaceAuditActionenum ingarraia-auth/src/audit_workspace.rsalready hasFileDeleted,FileRenamed,FileVersionCreated— addingFileCreatedfollows the same pattern. -
create_folderhandler (plan 0092) demonstrates thedisplay_namelookup pattern. - Route
/v1/groups/{group_id}/filesonly hasget()— nopost()— confirmed inmod.rsline 412.
- Presigned URL initUpload/completeUpload pattern.
- tus resumable upload (already shipped, GAR-395 / plan 0047).
- Virus scanning hook (feature flag
av-clamav). - Folder-name deduplication within a group (not required by schema).
No schema migration → rollback = revert the PR. No data loss risk since no migration is included.
crates/garraia-auth/src/audit_workspace.rs — add FileCreated variant + as_str() + test
crates/garraia-gateway/src/rest_v1/files.rs — add FileCreatedResponse struct, validate_file_name(), create_file() handler
crates/garraia-gateway/src/rest_v1/mod.rs — add .post(files::create_file) to route + fail-soft branch
tests/rest_v1_files_create.rs (new) — integration tests
plans/0099-gar-577-files-api-slice9-create.md — this file
plans/README.md — new row
- T1: Add
FileCreatedvariant toWorkspaceAuditAction(audit_workspace.rs) — includeas_str()arm + unit test assertion. - T2: Add
FileCreatedResponsestruct +validate_file_name()helper infiles.rs. - T3: Implement
create_file()handler infiles.rs. - T4: Register route in
mod.rs(full + fail-soft branches). - T5: Write integration test file
tests/rest_v1_files_create.rs(TDD: write tests first, verify red, then fix with T3). - T6:
cargo check -p garraia-gateway -p garraia-authgreen. - T7:
cargo clippy --workspace --tests --exclude garraia-desktop --features garraia-gateway/test-helpers --no-deps -- -D warningsgreen. - T8: Update
plans/README.md+ ROADMAP.md checkboxes.
| Risk | Likelihood | Mitigation |
|---|---|---|
| Compound FK insertion order (files before file_versions) | Low | INSERT files first (get file_id) then INSERT file_versions referencing it |
object_key collision (UUID-based, statistically impossible) |
Negligible | UNIQUE index on file_versions.object_key returns 409 on conflict |
| Body read OOM on large files | Low | to_bytes(body, cap) with operator cap (same as post_new_version) |
| Orphaned blob on Postgres rollback | Accepted | Per ADR 0004 §5.3.1; future maintenance job reclaims |
POST /v1/groups/{group_id}/filesreturns 201 with file_id + version=1.- MIME not in allow-list → 415.
- Body exceeds cap → 413.
- Missing
X-File-Name→ 400. - Invalid name (empty, >500, control char) → 400.
- Bad
X-Folder-Id(not found / soft-deleted in group) → 400. - Cross-group
path_group_idmismatch → 403. - No storage configured → 503.
- Integration test: newly created file queryable via
GET /v1/groups/{group_id}/files/{file_id}. - Audit event
file.createdpresent inaudit_eventstable after happy path. - All CI checks green (Format, Clippy, Test×3, Build, MSRV, cargo-deny, Security Audit, Coverage, CodeQL, Playwright, E2E, Secret Scan, Dependency Review).
None — all design decisions resolved by the existing post_new_version + create_folder patterns.
- Plan 0088 (GAR-555): files slice 1 (list + delete).
- Plan 0094 (GAR-567): files slice 7 (new version — pattern reused here).
- Plan 0092 (GAR-562): folder POST (creator_label resolution pattern reused here).
- ADR 0004: object storage design.
- Implementação: ~3h
- LOC: ~250 Rust + ~150 test
- CI wall-time: ~25 min