ccs_files_user_client (Flutter)
ccs_files_user_client is the File Service package for a Flutter app’s signed-in user, the Dart counterpart of ccs-files-user-client with the same routes, snake_case wire fields and errors. It runs the upload protocol (a ticket, one PUT or parallel parts with progress, retries, pause, resume after the app was killed, and cancel) and wraps folders, files, previews and URLs, search, tags, access entries, share links, the trash, capabilities and the public settings. It knows nothing about your posts or chats. This page covers installation and the behaviour you rely on; the release history is in the changelog. For a walk-through, start with the Flutter quickstart.
Install
Section titled “Install”The package is private and not on pub.dev. Add it from the ccs-file-server repository at a release tag, with path: pointing at the package inside it:
dependencies: ccs_files_user_client: git: url: git@github.com:central-code-station/ccs-file-server.git ref: ccs-files-flutter-v1.3.0 # a release tag, never a moving branch path: libs/flutter/ccs_files_user_clientYour machine and your CI need read access to the repository. The package depends on flutter and http only and never logs.
| Platform | Supported |
|---|---|
| Android, iOS, macOS, Windows, Linux | Yes: files are read through dart:io |
| Flutter web | No: use the web client there |
Releases are tagged ccs-files-flutter-v<version>; the package keeps its own semver in its pubspec.yaml and changelog.
New in 1.2.0
Section titled “New in 1.2.0”FileRecord and FolderRecord gain managed, managedBy (ManagedBy) and isReadOnly for application-managed folders, folders.releaseManagement(folderId, reason:) is the tenant owner’s emergency release, and FilesForbiddenException.isFolderManaged and .managedFolderId read a 403 folder_managed. The changelog lists every change.
New in 1.1.0
Section titled “New in 1.1.0”FileRecord.hold is typed (FileHold), files.hold.place and release and holds.list manage legal holds for a user with files.manage, a 409 file_on_hold carries the hold in FilesConflictException.hold, and SearchField.name is documented as the prefix match it is (Search). The changelog lists every change.
The two ways to upload
Section titled “The two ways to upload”| Flow | The app needs | Use |
|---|---|---|
| Your backend creates the ticket | No File Service token. Your API creates the ticket with the Node client, the app uploads it, your API completes it | UploadController.fromTicket(ticket, source) and result.completionBody() |
| The app uploads as the user | A user token from your backend’s token route | TokenProvider, FilesClient.fromTokenRoute, files.upload(source, request) or FilesUploadQueue |
This package has no generated reference on this site. The table lists its public API; every member has a doc comment in the package’s source.
| Member | What it does |
|---|---|
TokenProvider(tokenUrl:, headers:): getAccessToken, getConfig, clear |
Your token route, cached until a minute before the token expires, one request at a time |
FilesClient(apiUrl:, tenantId:, applicationId:, getAccessToken:, ...), FilesClient.fromTokenRoute(tokens) |
The client of one (tenant, application) context; fromTokenRoute reads api_url, tenant_id and application_id from the route; apiUrl is optional (default: production, FilesEndpoints.production) |
FilesEnvironment (production, staging, local): parse(String.fromEnvironment('CCS_ENVIRONMENT')), apiUrl; FilesEndpoints |
The File Service’s API base per environment, picked at build time with --dart-define=CCS_ENVIRONMENT=local |
upload(source, request), uploadVersion(fileId, source, ...), resumeUpload(json, source), activeUploads |
UploadControllers for new files and new versions |
preview(fileId, version:) |
The preview, cached |
urls(fileIds, rendition:, disposition:), invalidate([fileId]) |
Up to 200 URLs in one request, one entry per id in order, cached per item |
search(SearchQuery), tags(prefix:, limit:) |
Search (SearchField.name matches names that start with the query, ignoring case; SearchField.keywords and SearchField.content match whole words; hits carry highlights), tags |
capabilities(), settings.public() |
What the user may do; the application’s upload rules (allows(mimeType) checks a picked file) |
folders: root, create, get, children, path, update, move, copy, delete, restore, usage, releaseManagement(folderId, reason:) |
Folders; releaseManagement is the tenant owner’s release of an application-managed folder |
files: get, byExternalRef, update, move, copy, delete, restore, downloadUrl, versions, renditions, myActions |
Files |
files.hold: place(fileId, reason:, until:), release(fileId, reason:); holds.list(placedBy:, after:, limit:) |
The file’s legal hold and the application’s held files, for a user with files.manage; each answers the FileRecord (the list a Page<FileRecord>) |
uploads: create, createVersion, status, parts, complete, abort |
The protocol step by step |
permissions: get, replace, grant, revoke, roles; shares: create, list, mine, revoke; trash.list |
Sharing and the trash |
signOut(), close() |
Cancels the uploads and clears the caches and the token provider; releases the HTTP clients |
UploadController: start, pause, resume, cancel, progress, currentProgress, checkpoints, done, toJson, UploadController.fromJson, UploadController.fromTicket |
One upload |
UploadResult: uploadId, fileId, parts, file, completionBody() |
What start() and done complete with |
UploadSource.file, .path, .stream(stream, length:), .bytes |
Where the bytes come from |
FilesUploadQueue(client:, store:, concurrency:, pauseInBackground:): restore, add, pause, resume, cancel, remove, pauseAll, resumeAll, clearFinished, changes, flush, dispose |
Uploads with bounded concurrency, persisted, resumed at start and in the foreground |
FilesUploadStore: JsonFileUploadStore(file), MemoryUploadStore() |
Where the queue persists its snapshots |
Types: FileRecord (with hold: a FileHold with reason, placedBy, placedAt, until and inForce(), or null), FolderRecord (both FilesItem, with managed, managedBy and isReadOnly), ManagedBy (clientId, name, managedFolderId, managedAt), UploadTicket, UploadRequest, UploadedPart, UploadPartUrl, Preview, DownloadUrl, UrlResult, SearchQuery, SearchResult, SearchHit, AccessEntry, AccessEntryInput, ItemPermissions, ShareLink, PublicSettings, Capabilities, Page<T>. Each keeps the decoded wire object as raw for fields the package does not model.
Application-managed folders
Section titled “Application-managed folders”A folder a tenant’s backend marks managed, and everything below it, is read-only for every user, the tenant owner included: uploads, versions, renames, moves, edits, copies into it, trash, restore, purge, access entries and share links answer 403 folder_managed, and FilesForbiddenException.isFolderManaged is true, with the managing folder in .managedFolderId. Viewing, downloads, previews, search, copying out and legal holds work as usual. myActions still lists what the user holds: hide the editing controls when item.isReadOnly is true, and show who manages the item from item.managedBy?.name (or clientId when the name is null).
folders.releaseManagement(folderId, reason:) answers the FolderRecord. Only the tenant owner may call it (capabilities() says so; anyone else gets tenant_owner_required), with a reason of 1 to 500 characters, on the folder that carries the mark (managedBy.managedFolderId; 400 invalid_request otherwise). It is never retried. See Application-managed folders.
The upload engine
Section titled “The upload engine”| Behaviour | Rule |
|---|---|
| Ticket | Created with an Idempotency-Key kept in the snapshot, so a retried or resumed creation answers the same ticket. A refusal (quota_exceeded, file_too_large, type_not_allowed) surfaces here, before any byte |
| Single PUT | Exactly the ticket’s headers |
| Multipart | concurrency parts in flight (default 4); the first 100 part URLs come with the ticket, the rest in batches |
| Retries | Each PUT up to 5 retries on network errors and 5xx, backoff 1 to 30 seconds; a 403 on a part URL renews it once |
| Pause and resume | Pause stops the PUTs in flight and keeps the confirmed parts; resume asks the service which parts the store has and sends only the missing ones |
| Completion | Every part with its ETag, in order; a lost answer is recovered from the ticket’s status |
| Cancel | Stops the PUTs and releases the ticket (client uploads); done completes with FilesUploadCancelledException |
| States | idle, creating, uploading, paused, completing, done, failed, cancelled; resume() retries a failed upload |
| Resume after a restart | toJson() holds the ticket, the part URLs, the confirmed parts and what identifies the source (a file’s path, length and modification time). A changed source is refused with FilesUploadSourceMismatchException |
Snapshots contain signed URLs: keep them in the app’s private storage.
Errors
Section titled “Errors”| Dart | Node and web | When |
|---|---|---|
FilesApiException (status, code, body, retryAfter) |
FilesApiError |
Any non-2xx answer |
FilesUnauthorizedException |
FilesUnauthorizedError |
401 after one token refresh |
FilesForbiddenException, FilesNotFoundException, FilesConflictException |
the same names with Error |
403 (for example folder_managed, with isFolderManaged and managedFolderId), 404, 409 (for example scan_pending, or file_on_hold with the hold in FilesConflictException.hold) |
FilesPreconditionFailedException, FilesPayloadTooLargeException, FilesUnprocessableException, FilesRateLimitedException, FilesNotImplementedException |
the same | 412, 413, 422, 429 (never retried), 501 |
FilesContextException |
FilesContextError |
No tenant or application |
FilesUploadException (code, partNumber, uploadId, cause) |
FilesUploadError |
A PUT failed after its retries, was refused, or the ticket expired; part_url_expired for a ticket upload without refreshPartUrls |
FilesUploadSourceMismatchException |
UploadFileMismatchError |
upload_file_mismatch on resume |
FilesUploadCancelledException |
UploadCancelledError |
done after cancel() |
FilesNetworkException, FilesProtocolException, FilesTokenException, FilesConfigurationException |
The network, an unreadable answer, the token route, invalid arguments |
All extend FilesException. Reads and idempotent writes are retried twice after a network error or a 5xx; the writes that create (folders, copies, shares, tickets) send an Idempotency-Key that stays the same across their retries. A 401 asks the token provider for a fresh token and replays once.