For CI jobs, agents and scripts that upload and delete with nobody watching. It covers three things a script needs and a person in the web app does not: checking a folder before encrypting for it, waiting on a state instead of sleeping, and knowing when storage has been released.
The Quickstart covers keys, folder creation and Seal. Start there if you have not made a first upload yet. A folder in Console is a bucket in the API.
Files in a private folder are encrypted on your side, under the folder's Seal policy. Console stores the bytes it is given and cannot tell whether they will decrypt, so it checks the folder's policy instead and refuses the upload when the policy cannot be resolved. Ask for the same check before you encrypt:
GET /api/v1/buckets/{bucketId}/policy
Authorization: Bearer <api key>
{ "data": { "state": "resolvable", "seal_policy_id": "0x8f96..." } }
state |
Meaning | What to do |
|---|---|---|
resolvable |
The folder's recorded policy exists on Sui. The answer can be up to 10 minutes old. | Encrypt and upload. |
pending |
The folder is not finalized, so its group does not exist yet. | Finish finalize, then read again. |
missing |
The policy cannot be resolved. reason says why. |
Do not upload. Report the folder id and the reason. |
unknown |
Sui could not confirm the group yet. | Read again. A folder created in the last 10 minutes can take that long to settle, and a node that did not answer is asked again after 30 seconds. |
not_required |
A public folder. It has no policy. | Upload. |
reason is present only with missing:
reason |
Meaning |
|---|---|
policy_missing |
The folder has no policy recorded. Every other route answers 404 for such a folder. |
wrong_group |
The recorded policy is not the group derived for this folder. |
not_on_chain |
No object exists on Sui at the policy id. |
not_a_bucket_group |
An object exists at the policy id and it is not a folder group. |
resolvable is about the policy itself. Decrypting also needs your key to
hold a role in the folder's group, and the group not to be paused. This route
checks neither.
The upload route applies the same rule, and refuses an upload into a
missing folder before anything is stored. With policy_missing it answers
404, as every other route does. With any other reason it answers:
{
"error": "This bucket's encryption policy cannot be resolved, so files uploaded to it could not be decrypted. Nothing was uploaded.",
"code": "bucket_policy_unresolvable"
}
That is a 409, and retrying it will not help. Uploads are not refused on
unknown.
A folder that is being deleted answers 409 bucket_deleting here, then
bucket_gone, as an upload into it would.
A new API key gets 403 mirror_missing_grant on this route for a few
seconds, as on every folder route. The Quickstart explains the retry. If it
has not cleared after a minute, report the folder id.
This route needs a read grant on the folder, and an upload needs a write
grant. A key that can only read the folder gets resolvable here and 403
mirror_missing_grant on the upload.
Uploads and deletes both finish in the background. Each has a route that reports where it is.
upload POST .../files -> 202, data.id
GET .../files/{fileId}/status queued -> active -> completed
-> failed
delete DELETE .../files/{fileId} -> 204
GET .../files/{fileId}/deletion deleting -> deleted
-> delete_failed
GET /api/v1/buckets/{bucketId}/files/{fileId}/status
state |
Terminal | Meaning |
|---|---|---|
queued |
no | Accepted, waiting for a worker. An upload that is being retried comes back here. |
active |
no | Being written to Walrus. progress (0 to 1) appears once the worker reports it. |
completed |
yes | Stored. The file can be downloaded. |
failed |
yes | The upload did not complete. error.code says why. The storage it had reserved is returned. |
POST .../files returns 202 with data.id.completed or failed.The status of an old upload eventually answers 404, because only the most
recent finished jobs are kept. The file record is permanent:
GET .../files/{fileId} reports status as pending, uploading, active
or failed, and active is the same fact as completed.
GET /api/v1/buckets/{bucketId}/files/{fileId}/deletion
{ "data": { "state": "deleting" } }
state |
Terminal | Meaning |
|---|---|---|
deleting |
no | The delete was requested and the blob is being removed. |
deleted |
yes | The blob is gone and storage has been released. |
delete_failed |
yes | The file was not deleted and still counts toward storage. Report the file id. |
DELETE .../files/{fileId} returns 204.deleted or delete_failed.A delete usually takes a few seconds. Wait for an upload to reach completed
before deleting the file.
While the file is deleting, and once it is deleted, it is out of listings
and both GET .../files/{fileId} and its /status answer 404. Only
/deletion still knows it. After delete_failed the file record answers
again, with status failed: that is a failed delete, not a failed upload,
and /deletion is what tells the two apart. For a file nobody asked to
delete, /deletion answers 404.
When the worker hits a transient upstream error it tries again, five times in
all over about 30 seconds. Give your wait a timeout: a file still deleting
after 5 minutes has stalled and will not finish on its own. Report its id
instead of sending a second DELETE, which answers 404.
Deleting a whole folder is reported on the folder: status on
GET /api/v1/buckets/{bucketId} reads deleting, then deleted. Meanwhile
reads of the files under it answer 409 bucket_deleting, then bucket_gone.
storage_used on GET /api/v1/buckets/{bucketId} is the folder's counter. The
space quota that uploads are checked against moves with it, in the same
transaction.
| Event | Counters |
|---|---|
Upload accepted (202) |
Go up by the file's size straight away. |
Upload failed |
Come back down. |
File delete requested (204) |
Unchanged. The file still counts while it is deleting. |
File deletion reaches deleted |
Come down, in the same transaction that sets deleted. |
Folder delete requested (204) |
Come down straight away by the folder's stored files, before their blobs are removed. |
So there is no second delay to wait out: once a file's deletion reads
deleted, its storage is released, and an upload that was refused with
quota_exceeded can be sent again.