15 — Flutter Implementation Guide (Files Module)
- 1. Upload with progress (dio)
- 2. List
- 3. Download with progress
- 4. Delete
- 5. Permissions & guards
(planned) - 6. Offline & outbox
(forward-looking) - 7. Tests (per
00-shared/10_QA_Baseline+ module QA14_QA_Checklist.md)
How to build the Files module on the existing app architecture (00-shared/11_Flutter_App_Architecture, 00-shared/06_State_Management). Backend contract is fixed by 12_API_Mapping.md; state model by 13_State_Management.md. Deps:
dio(transport, already core),path_provider,file_picker(or platform pickers),open_filex/share(forward-looking).
1. Upload with progress (dio)
Future<FileRecord> upload(PickedFile f, {void Function(int, int)? onProgress}) async {
final form = FormData.fromMap({
'file': await MultipartFile.fromFile(
f.path,
filename: f.name,
contentType: DioMediaType.parse(f.mimeType), // echo client type — server trusts it
),
});
final res = await api.post('/files/upload',
data: form,
onSendProgress: onProgress,
options: Options(contentType: 'multipart/form-data'),
);
return FileRecord.fromJson(res.data['data']); // envelope per 00-shared/07
}
- Server contract: multipart part named
file(files.controller.ts:31), one file per request; field name is fixed —MultipartFile.fromFilemust be wrapped in a map with keyfile. - Progress: dio
onSendProgressgives(sent, total). Until the first event (server buffering), show indeterminateAppProgress(10_Interaction_Specification.md §3). - Cancel:
CancelTokenper upload; abort discards the body client-side (no server partial state). - Retry: re-POST the whole body; expect a new
FileRecord(no idempotency). - Auth: attach Bearer token via the shared dio interceptor; guards are
(planned), so test with a role that holdsfile.upload(permissions.constants.ts:87).
UploadCubit wiring
UploadIdle → UploadPicking → UploadSelected → UploadUploading(progress) → UploadSuccess | UploadFailed | cancelled (mermaid in 13_State_Management.md). On success call
FilesCubit.upsertLocal(record); on failure keep the picked file for Retry.
2. List
final res = await api.get('/files'); // full tenant list, createdAt desc
final files = (res.data['data'] as List).map(FileRecord.fromJson).toList();
- No pagination/filter params (
files.service.ts:48-50) — load once, group client-side by context key inbyContext(see13_State_Management.md).
3. Download with progress
Future<String> download(FileRecord f, {void Function(int, int)? onProgress, CancelToken? cancel}) async {
final dir = await getTemporaryDirectory();
final path = '${dir.path}/${sanitizeForFs(f.originalName)}';
await api.download('/files/${f.id}/download', path,
onReceiveProgress: onProgress, cancelToken: cancel);
return path;
}
- Server: buffered proxied bytes with
Content-Disposition: attachment(files.controller.ts:55-64) — dio downloads the body to disk; no redirect handling needed. - Cancel:
CancelToken; no resume (noRange) — retry restarts from 0. - Open:
open_filex/url_launcherfor the temp path; Share viashare_plus(forward-looking). - Filename: the server returns the original name only in the record + header; sanitize
locally for filesystem safety (server does not sanitize,
files.service.ts:31).
DownloadManagerSheet
Single DownloadCubit owning a task list (13_State_Management.md §1.3); sheet lists
queued/active/completed tasks with per-task AppProgress and cancel.
4. Delete
await api.delete('/files/${f.id}'); // { message: 'File deleted' }
filesCubit.removeLocal(f.id); // optimistic, rollback on error
- 404 → treat as already gone, remove row (
files.service.ts:54). - Only render delete when the role holds
file.delete(permissions.constants.ts:88).
5. Permissions & guards (planned)
- Route decorators exist (
files.controller.ts:30,44,50,56,67); RBAC guards not yet implemented. Client: read permission flags from the roles endpoint and hide/show Attach/Delete/Download accordingly; handle 403 uniformly once guards land.
6. Offline & outbox (forward-looking)
- Upload/download actions disabled while offline (
AppOfflineBanner); queue uploads in a local outbox and drain on reconnect.
7. Tests (per 00-shared/10_QA_Baseline + module QA 14_QA_Checklist.md)
- Unit:
UploadCubitstate machine (cancel, retry, success path);byContextgrouping. - Widget:
FileTilelabels; progress card percent rendering; delete dialog copy. - Integration (mock dio): multipart body contains part
file; progress callback invoked; envelope unwrap; 404 handling. - E2E: upload → appears in list → download → file matches bytes → delete → gone.