B2 Native API v3 client for the ACG B2 account: status, buckets, keys, files, bucket-size, usage/cost ($0.00695/GB), gated create/delete bucket+key, and gated lifecycle-based delete-prefix/lifecycle-remove for prefix purges. Read-only by default; destructive ops require --confirm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
237 lines
9.2 KiB
Markdown
237 lines
9.2 KiB
Markdown
# Backblaze B2 Native API v3 Reference
|
|
|
|
Verified spec for the methods used by this skill, against the live ACG B2
|
|
account (accountId `46f69bc61163`, region `us-west-001`). All facts below were
|
|
confirmed against the live API.
|
|
|
|
---
|
|
|
|
## Authorization (v3)
|
|
|
|
- **Authorize URL (fixed, global):**
|
|
`https://api.backblazeb2.com/b2api/v3/b2_authorize_account`
|
|
- **Auth:** HTTP Basic. Username = key id, password = application key.
|
|
- **Method:** GET in the B2 docs; this skill issues it as an HTTPS POST with the
|
|
Basic header, which the endpoint accepts.
|
|
|
|
### Authorize response (v3 shape)
|
|
|
|
```json
|
|
{
|
|
"accountId": "46f69bc61163",
|
|
"authorizationToken": "<token, valid 24h>",
|
|
"apiInfo": {
|
|
"storageApi": {
|
|
"apiUrl": "https://api001.backblazeb2.com",
|
|
"s3ApiUrl": "https://s3.us-west-001.backblazeb2.com",
|
|
"downloadUrl": "https://f001.backblazeb2.com",
|
|
"recommendedPartSize": 100000000,
|
|
"absoluteMinimumPartSize": 5000000,
|
|
"capabilities": ["listBuckets", "listFiles", "..."],
|
|
"bucketId": null,
|
|
"namePrefix": null
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- **v3 nesting:** `apiUrl` / `s3ApiUrl` / `downloadUrl` / `capabilities` /
|
|
`bucketId` / `namePrefix` live under `apiInfo.storageApi`. (v2 returned
|
|
`apiUrl` / `downloadUrl` at the top level — do NOT parse them there for v3.)
|
|
- `bucketId == null` and `namePrefix == null` means an **account-wide** key;
|
|
non-null means the key is scoped to one bucket / name prefix.
|
|
- The per-account `apiUrl` is dynamic and comes ONLY from this response — never
|
|
from a config file.
|
|
|
|
### Cache model
|
|
|
|
The skill caches `authorizationToken` + `apiUrl` + `accountId` (plus the other
|
|
fields) in `.claude/skills/b2/.cache/auth.json` with an `authorized_at`
|
|
timestamp. It is treated as valid for ~23h (B2 tokens last 24h). A `401` with
|
|
code `expired_auth_token` or `bad_auth_token` triggers exactly one re-authorize
|
|
+ retry. **The cached token is a secret; the cache dir is gitignored.**
|
|
|
|
---
|
|
|
|
## Subsequent calls
|
|
|
|
- **URL:** `POST <apiUrl>/b2api/v3/<method>`
|
|
- **Header:** `Authorization: <authorizationToken>` (raw token, not Basic).
|
|
- **Body:** a JSON object.
|
|
- **Content-Type:** `application/json`.
|
|
|
|
### Error shape
|
|
|
|
B2 returns HTTP 4xx with a JSON body:
|
|
|
|
```json
|
|
{ "status": 400, "code": "<string>", "message": "<text>" }
|
|
```
|
|
|
|
The skill surfaces `status` / `code` / `message` verbatim. On `401` with
|
|
`expired_auth_token` / `bad_auth_token` it re-authorizes once and retries; all
|
|
other errors raise. Destructive calls are never retried automatically.
|
|
|
|
---
|
|
|
|
## Methods used
|
|
|
|
All POST to `<apiUrl>/b2api/v3/<method>`.
|
|
|
|
### b2_list_buckets
|
|
|
|
- **Body:** `{"accountId": "<acct>", "bucketId": <opt>}`
|
|
- **Returns:** `{"buckets": [{bucketName, bucketId, bucketType, revision,
|
|
fileLockConfiguration:{isFileLockEnabled}, lifecycleRules, options}]}`
|
|
- Pass `bucketId` to scope the result to a single bucket.
|
|
- **`revision`** (int) is the bucket's optimistic-concurrency version. NOTE:
|
|
this account's `b2_update_bucket` does NOT accept `ifRevisionMatch`, so this
|
|
value is informational only — it is read but never sent back on a write.
|
|
- **`lifecycleRules`** (array) is the bucket's current lifecycle rule set (see
|
|
the rule shape under `b2_update_bucket`). An empty array means no rules.
|
|
|
|
### b2_list_keys
|
|
|
|
- **Body:** `{"accountId": "<acct>", "maxKeyCount": 1000,
|
|
"startApplicationKeyId": <opt>}`
|
|
- **Returns:** `{"keys": [{keyName, applicationKeyId, capabilities:[...],
|
|
bucketId, namePrefix, expirationTimestamp}], "nextApplicationKeyId": <null
|
|
when done>}`
|
|
- Paginate on `nextApplicationKeyId`.
|
|
|
|
### b2_list_file_names
|
|
|
|
- **Body:** `{"bucketId": "<id>", "maxFileCount": 10000, "prefix": <opt>,
|
|
"startFileName": <opt>}`
|
|
- **Returns:** `{"files": [{fileName, contentLength, action, uploadTimestamp,
|
|
...}], "nextFileName": <null when done>}`
|
|
- Lists the **latest** name for each file. Use for quick listings; NOT for
|
|
size/cost.
|
|
|
|
### b2_list_file_versions
|
|
|
|
- **Body:** `{"bucketId": "<id>", "maxFileCount": 10000, "prefix": <opt>,
|
|
"startFileName": <opt>, "startFileId": <opt>}`
|
|
- **Returns:** `{"files": [{fileName, fileId, contentLength, action, ...}],
|
|
"nextFileName": <null when done>, "nextFileId": ...}`
|
|
- **Pagination:** carry BOTH `nextFileName` AND `nextFileId` into the next
|
|
request; stop when `nextFileName` is null.
|
|
|
|
#### action types
|
|
|
|
| action | meaning | billed? |
|
|
|---|---|---|
|
|
| `upload` | a real stored object | YES |
|
|
| `hide` | a hide marker (`contentLength` 0) | no |
|
|
| `start` | an unfinished large file | no |
|
|
| `folder` | a virtual folder placeholder | no |
|
|
|
|
**For size/cost, sum `contentLength` over ALL versions where
|
|
`action == "upload"`.** B2 bills every stored version, not just the latest, so
|
|
size MUST use `b2_list_file_versions`, never `b2_list_file_names`.
|
|
|
|
### b2_create_bucket (destructive — gated)
|
|
|
|
- **Body:** `{"accountId": "<acct>", "bucketName": "<name>", "bucketType":
|
|
"allPrivate"}`
|
|
- `bucketType` defaults to `allPrivate`; the CLI allows `allPublic` via `--type`.
|
|
|
|
### b2_create_key (destructive — gated)
|
|
|
|
- **Body:** `{"accountId": "<acct>", "keyName": "<name>",
|
|
"capabilities": [...], "bucketId": <opt>, "namePrefix": <opt>,
|
|
"validDurationInSeconds": <opt>}`
|
|
- **Returns:** `{applicationKeyId, applicationKey, keyName, capabilities,
|
|
bucketId, namePrefix}`.
|
|
- **`applicationKey` is the SECRET and is shown ONCE on creation** — it cannot be
|
|
retrieved later. The CLI prints it with a prominent warning to store it in the
|
|
vault immediately.
|
|
- `bucketId` scopes the key to one bucket (the CLI resolves a bucket name to its
|
|
id via `b2_list_buckets`).
|
|
|
|
### b2_delete_bucket (destructive — gated)
|
|
|
|
- **Body:** `{"accountId": "<acct>", "bucketId": "<id>"}`
|
|
- B2 **refuses to delete a non-empty bucket**; that error is surfaced verbatim.
|
|
|
|
### b2_delete_key (destructive — gated)
|
|
|
|
- **Body:** `{"applicationKeyId": "<id>"}`
|
|
|
|
### b2_update_bucket (destructive — gated)
|
|
|
|
Used for the prefix-purge feature: it writes the bucket's lifecycle rules.
|
|
|
|
- **Body:** `{"accountId": "<acct>", "bucketId": "<id>",
|
|
"lifecycleRules": [...]}`
|
|
- **Returns:** the updated bucket object (same shape as a `b2_list_buckets`
|
|
entry, including the NEW `revision` and the written `lifecycleRules`).
|
|
- **`lifecycleRules` REPLACES the entire array — it is NOT additive (caveat).**
|
|
To add/remove a single rule you MUST first read the current rules via
|
|
`b2_list_buckets` (scoped to the bucketId), merge your change into the full
|
|
array, then write the complete set back. Writing just the one rule you want
|
|
silently drops every other rule.
|
|
- **No `ifRevisionMatch` on this account/endpoint.** This B2 account's
|
|
`b2_update_bucket` REJECTS `ifRevisionMatch` with **HTTP 400** `bad_request`
|
|
("unknown field ... ifRevisionMatch"), so no optimistic-lock token is sent.
|
|
Writes are therefore **last-write-wins** — the read-merge-write of the full
|
|
rules array on every change is what keeps pre-existing rules intact. The skill
|
|
does not implement a 409-conflict retry (there is no revision guard); it makes
|
|
at most one defensive retry on a transient 5xx, never loops.
|
|
- B2 allows up to **100** lifecycle rules per bucket.
|
|
|
|
#### Lifecycle rule shape
|
|
|
|
```json
|
|
{ "fileNamePrefix": "MBS-<guid>/CBB_<machine>/",
|
|
"daysFromUploadingToHiding": 1,
|
|
"daysFromHidingToDeleting": 1 }
|
|
```
|
|
|
|
- `fileNamePrefix` — files whose name starts with this string are governed by the
|
|
rule. `""` means the WHOLE BUCKET.
|
|
- `daysFromUploadingToHiding` — after this many days, B2's daily lifecycle pass
|
|
HIDES the file (creates a hide marker).
|
|
- `daysFromHidingToDeleting` — after this many days hidden, B2 DELETES the file
|
|
version permanently.
|
|
- With both `= 1`, every version older than ~1 day is hidden then deleted on the
|
|
next daily passes -> full server-side purge of the prefix within ~24-48h. This
|
|
is the mechanism the skill uses to purge prefixes with 1.2M+ versions, where
|
|
per-file `b2_delete_file_version` would be impractical. **Irreversible — no
|
|
recycle bin.**
|
|
|
|
### b2_get_file_info / b2_delete_file_version
|
|
|
|
- `b2_get_file_info` body: `{"fileId": "<id>"}` (read).
|
|
- `b2_delete_file_version` body: `{"fileName": "<name>", "fileId": "<id>"}`
|
|
(destructive; exposed only via `raw --confirm` or the client helper).
|
|
|
|
---
|
|
|
|
## Cost formula
|
|
|
|
```
|
|
GB = bytes / 1_000_000_000 # decimal GB (billing unit, NOT 2^30)
|
|
cost = GB * RATE_PER_GB_USD # RATE_PER_GB_USD = 0.00695 (ACG cost basis)
|
|
```
|
|
|
|
`RATE_PER_GB_USD` is ACG's cost basis, recorded in
|
|
`.claude/memory/reference_backblaze_storage_rate.md`, and used by the GuruRMM
|
|
mspbackups storage-cost calc. Override with `--rate`. The `usage` report sums
|
|
upload-version bytes per bucket, converts to decimal GB, multiplies by the rate,
|
|
sorts by cost desc, and prints a TOTAL row plus grand totals. For `ACG-*`
|
|
buckets it derives a client label by stripping the `ACG-` prefix.
|
|
|
|
---
|
|
|
|
## Account structure (live)
|
|
|
|
- accountId `46f69bc61163`, region `us-west-001`.
|
|
- 12 buckets (all `allPrivate`): ACG-BST, ACG-Brett, ACG-Dataforth, ACG-GLAZTECH,
|
|
ACG-IX, ACG-Internal, ACG-Lens, ACG-PST, ACG-REDNOUR, Horseshoe,
|
|
MSPBackups20200311, VWP-Backup. Most hold MSP360/CloudBerry data
|
|
(`MBS-<guid>/CBB_<CLIENT>/...`).
|
|
- 2 application keys: `cloudberrykey` (`00146f69bc611630000000003`, the
|
|
MSP360/CloudBerry key) and `ClaudeTools` (`00146f69bc611630000000009`, used by
|
|
this skill).
|