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>
262 lines
11 KiB
Markdown
262 lines
11 KiB
Markdown
---
|
|
name: b2
|
|
description: >-
|
|
Manage Arizona Computer Guru's (ACG) Backblaze B2 storage account via the B2
|
|
Native API v3. Talks to the LIVE production B2 account (accountId 46f69bc61163,
|
|
region us-west-001) that holds the per-client MSP360/CloudBerry backup
|
|
destinations. List buckets and application keys, list files / file versions,
|
|
compute per-bucket stored size, and produce the headline storage-cost report
|
|
(the mspbackups storage-cost calc). Provision buckets and scoped backup keys
|
|
and delete buckets/keys (all destructive ops are gated behind --confirm).
|
|
Read-only by default. Invoke for: "backblaze", "b2", "b2 storage", "bucket",
|
|
"storage cost", "backup storage", "mspbackups storage", "list buckets b2".
|
|
---
|
|
|
|
# Backblaze B2 Skill
|
|
|
|
Standalone CLI client for the Backblaze B2 Native API v3. Talks to the live ACG
|
|
B2 account. Read-only by default; destructive operations are gated behind
|
|
`--confirm`.
|
|
|
|
## Running the CLI
|
|
|
|
This machine's Python launcher is `py` (per identity.json). The scripts also
|
|
work with `python`/`python3`.
|
|
|
|
```bash
|
|
# from the scripts dir, or pass full paths
|
|
py "$CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" status
|
|
py "$CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" buckets
|
|
py "$CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py" usage --json
|
|
```
|
|
|
|
Transport auto-selects: uses `httpx` if installed, otherwise stdlib `urllib`
|
|
(no third-party dependency required).
|
|
|
|
## Credentials
|
|
|
|
Credentials are NEVER hardcoded. At runtime the client loads them from the SOPS
|
|
vault:
|
|
|
|
```
|
|
bash "$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" \
|
|
get-field projects/claudetools/backblaze-b2.sops.yaml key_id
|
|
bash "$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" \
|
|
get-field projects/claudetools/backblaze-b2.sops.yaml credentials.application_key
|
|
```
|
|
|
|
`CLAUDETOOLS_ROOT` resolves from the env var, else `claudetools_root` in
|
|
`.claude/identity.json`, else the repo root derived from the script's own
|
|
location (no hardcoded drive letters). For testing you can override with the
|
|
`B2_KEY_ID` and `B2_APPLICATION_KEY` env vars (both must be set to use the
|
|
override).
|
|
|
|
Authorization is HTTP Basic against `b2_authorize_account` (username = key id,
|
|
password = application key). The skill uses the "ClaudeTools" application key
|
|
(`00146f69bc611630000000009`).
|
|
|
|
## Cache model (important — the token is a SECRET)
|
|
|
|
After a successful authorize, the CLI caches the auth result at
|
|
`.claude/skills/b2/.cache/auth.json`:
|
|
|
|
- **Cached:** `authorizationToken` (the bearer token, valid ~24h), the
|
|
per-account `apiUrl` / `s3ApiUrl` / `downloadUrl`, `accountId`, key
|
|
capabilities, and key scope (`bucketId` / `namePrefix`).
|
|
- **TTL:** treated as valid for ~23h. The client re-authorizes when the cache is
|
|
stale, or automatically on a `401` with `expired_auth_token` / `bad_auth_token`
|
|
(exactly one re-authorize + retry per call).
|
|
- **This cache holds a live secret.** It is gitignored twice (root `.gitignore`
|
|
plus a local `.gitignore` in the skill dir) and must never be committed.
|
|
|
|
The per-account `apiUrl` always comes from the live authorize response — there
|
|
is no config file for endpoints (project rule).
|
|
|
|
## Safety gating
|
|
|
|
Destructive subcommands refuse to run without `--confirm`; without it they print
|
|
what they would do and exit non-zero (3):
|
|
|
|
- `create-bucket <name> [--type allPrivate|allPublic] --confirm`
|
|
- `create-key --name <n> --capabilities <c1,c2,...> [--bucket <name>] [--prefix <p>] [--duration-seconds N] --confirm`
|
|
- `delete-bucket <name> --confirm` (B2 refuses to delete a non-empty bucket — the error is surfaced verbatim)
|
|
- `delete-key <applicationKeyId> --confirm`
|
|
- `delete-prefix <name> <prefix> [<prefix> ...] --confirm` (schedules an IRREVERSIBLE lifecycle purge — see "Prefix purge / lifecycle" below)
|
|
- `lifecycle-remove <name> <prefix> [<prefix> ...] --confirm` (removes lifecycle rules)
|
|
|
|
`create-key` prints the returned `applicationKey` exactly ONCE with a warning —
|
|
B2 never shows it again, so store it in the vault immediately.
|
|
|
|
`raw` refuses any method name containing `create` / `delete` / `update` / `hide`
|
|
/ `cancel` unless `--confirm` is passed. `raw` prints the upstream response
|
|
verbatim, which may carry sensitive data (keys, tokens) — review before pasting
|
|
into tickets/logs. Destructive calls are NEVER retried automatically.
|
|
|
|
## Common commands
|
|
|
|
```bash
|
|
B2="py $CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py"
|
|
|
|
# Status / inventory
|
|
$B2 status
|
|
$B2 buckets
|
|
$B2 keys
|
|
|
|
# Files
|
|
$B2 files ACG-Internal # latest names
|
|
$B2 files ACG-Internal --prefix MBS- --limit 50
|
|
$B2 files ACG-Internal --versions # all versions
|
|
|
|
# Size + cost
|
|
$B2 bucket-size ACG-Internal
|
|
$B2 usage # headline cost report, all buckets
|
|
$B2 usage --bucket ACG-Dataforth # one bucket
|
|
$B2 cost --json # alias for usage
|
|
|
|
# Provisioning (gated)
|
|
$B2 create-bucket ACG-NewClient --confirm
|
|
$B2 create-key --name acg-newclient-backup \
|
|
--capabilities listBuckets,listFiles,readFiles,writeFiles,deleteFiles \
|
|
--bucket ACG-NewClient --confirm
|
|
$B2 delete-bucket ACG-OldClient --confirm
|
|
$B2 delete-key 00146f69bc611630000000abc --confirm
|
|
|
|
# Lifecycle / prefix purge (see next section)
|
|
$B2 lifecycle ACG-Internal # read-only: list current rules
|
|
$B2 delete-prefix ACG-Internal "MBS-<guid>/CBB_<machine>/" --confirm
|
|
$B2 lifecycle-remove ACG-Internal "MBS-<guid>/CBB_<machine>/" --confirm
|
|
|
|
# Power use — any v3 method directly
|
|
$B2 raw --method b2_list_buckets --body '{"accountId":"46f69bc61163"}'
|
|
```
|
|
|
|
## Prefix purge / lifecycle
|
|
|
|
Some backup destinations hold **1.2M+ file versions** under a single machine
|
|
prefix. Per-file deletion (`b2_delete_file_version` per version) is impractical
|
|
at that scale, so this skill purges a prefix via a **B2 bucket lifecycle rule**
|
|
instead. B2's own daily lifecycle pass does the deletion server-side.
|
|
|
|
### How the purge mechanism works
|
|
|
|
A purge rule is:
|
|
|
|
```json
|
|
{ "fileNamePrefix": "MBS-<guid>/CBB_<machine>/",
|
|
"daysFromUploadingToHiding": 1,
|
|
"daysFromHidingToDeleting": 1 }
|
|
```
|
|
|
|
With both day-counts at `1`, B2's daily lifecycle pass **hides** any file more
|
|
than 1 day old, then **deletes** hidden files more than 1 day old. Every version
|
|
under the prefix (all are years old) becomes eligible immediately, so the prefix
|
|
is fully purged within **~24-48h** (two daily passes). This is **irreversible**:
|
|
there is no recycle bin and no undo once the pass deletes a version.
|
|
|
|
`fileNamePrefix: ""` means the WHOLE BUCKET — the skill refuses that and any
|
|
other too-broad prefix (see safety below).
|
|
|
|
### Lifecycle rules REPLACE, they don't append
|
|
|
|
`b2_update_bucket`'s `lifecycleRules` **replaces the entire array** — it is not
|
|
additive. The skill therefore always reads the current rules (`b2_list_buckets`
|
|
scoped to the bucketId), merges the change, and writes the full set back. This
|
|
read-merge-write of the complete array is what preserves pre-existing rules.
|
|
|
|
**This account's `b2_update_bucket` does NOT support `ifRevisionMatch`** (it
|
|
returns HTTP 400 `bad_request: unknown field ... ifRevisionMatch`), so the skill
|
|
sends no optimistic-lock token and writes are **last-write-wins**. There is no
|
|
409-conflict retry (no revision guard to violate); the skill makes at most one
|
|
defensive retry on a transient 5xx, then fails — it never loops. The bucket
|
|
`revision` is still read and displayed for reference, but is never sent back.
|
|
B2 allows up to **100** lifecycle rules per bucket; the skill refuses an update
|
|
that would exceed that.
|
|
|
|
### Commands
|
|
|
|
```bash
|
|
B2="py $CLAUDETOOLS_ROOT/.claude/skills/b2/scripts/b2.py"
|
|
|
|
# 1. READ-ONLY: see the bucket's current lifecycle rules + revision
|
|
$B2 lifecycle ACG-Internal
|
|
$B2 lifecycle ACG-Internal --json
|
|
|
|
# 2. DRY-RUN: what WOULD be scheduled (no --confirm -> exits 3, writes nothing)
|
|
$B2 delete-prefix ACG-Internal "MBS-<guid>/CBB_<machine>/"
|
|
|
|
# 3. COMMIT the purge (gated). Idempotent: an identical rule already present
|
|
# is skipped. You can pass several prefixes at once.
|
|
$B2 delete-prefix ACG-Internal \
|
|
"MBS-<guid>/CBB_OLDPC1/" \
|
|
"MBS-<guid>/CBB_OLDPC2/" --confirm
|
|
|
|
# 4. After ~24-48h, verify the data is gone (size should have dropped)
|
|
$B2 bucket-size ACG-Internal
|
|
|
|
# 5. Clean up the now-spent purge rule(s) so they don't sit on the bucket
|
|
$B2 lifecycle-remove ACG-Internal \
|
|
"MBS-<guid>/CBB_OLDPC1/" \
|
|
"MBS-<guid>/CBB_OLDPC2/" --confirm
|
|
```
|
|
|
|
### Safety validations (hard-fail even with --confirm)
|
|
|
|
`delete-prefix` refuses (exit 2) any prefix that is:
|
|
|
|
- empty, `"/"`, `"*"`, or **contains no `/`** — too broad (would risk a
|
|
whole-bucket or whole-account-tree purge);
|
|
- exactly an **account root** `MBS-<guid>/` (one path segment + trailing slash) —
|
|
this would purge ALL machines under that account. Override only with the extra
|
|
`--allow-account-root` flag (NOT used by default — purge machine-level `CBB_`
|
|
prefixes instead).
|
|
|
|
A valid machine target looks like `MBS-<guid>/CBB_<machine>/`. The command also
|
|
**warns** (does not fail) when a prefix lacks a trailing `/`, since a slash-less
|
|
prefix can match a sibling whose name merely starts with the same characters.
|
|
|
|
Without `--confirm`, `delete-prefix` prints the exact rule(s) it WOULD add and a
|
|
prominent `[WARNING]` about irreversible deletion, then exits non-zero (3) —
|
|
matching the other gated commands. `lifecycle-remove` is similarly gated
|
|
(removing a rule is far less dangerous than adding one, but gated for
|
|
consistency); without `--confirm` it lists which existing rules it would remove
|
|
and exits 3.
|
|
|
|
### Cleanup workflow
|
|
|
|
`delete-prefix` -> wait ~24-48h for B2's lifecycle pass -> `bucket-size` to
|
|
verify the stored size dropped -> `lifecycle-remove` to strip the spent purge
|
|
rule. Leaving the rule in place is harmless (everything under the prefix is
|
|
already gone) but tidy buckets are easier to reason about.
|
|
|
|
## Storage cost
|
|
|
|
ACG's Backblaze B2 cost basis is **$0.00695 per GB** stored, defined as
|
|
`RATE_PER_GB_USD` in `scripts/b2_client.py` (recorded in
|
|
`.claude/memory/reference_backblaze_storage_rate.md`) and used by the GuruRMM
|
|
mspbackups storage-cost calc. Override at the CLI with `--rate`.
|
|
|
|
GB is **decimal** (`bytes / 1e9`), matching how storage providers bill — NOT
|
|
2^30. Cost = `GB * rate`.
|
|
|
|
Size MUST be summed over **all file versions** (`b2_list_file_versions`,
|
|
`action == "upload"`), not just the latest names, because B2 bills every stored
|
|
version. `bucket-size` and `usage` do this automatically. For very large buckets
|
|
this issues many list transactions — `usage` prints a `[WARNING]` to that effect.
|
|
|
|
## Account structure (for context)
|
|
|
|
- accountId `46f69bc61163`, region `us-west-001`.
|
|
- 12 buckets (all `allPrivate`), mostly per-client MSP360/CloudBerry backup
|
|
destinations with keys like `MBS-<guid>/CBB_<CLIENT>/...`: ACG-BST, ACG-Brett,
|
|
ACG-Dataforth, ACG-GLAZTECH, ACG-IX, ACG-Internal, ACG-Lens, ACG-PST,
|
|
ACG-REDNOUR, Horseshoe, MSPBackups20200311, VWP-Backup. `usage` derives a
|
|
client label by stripping the `ACG-` prefix.
|
|
- 2 application keys: `cloudberrykey` (the MSP360/CloudBerry key) and
|
|
`ClaudeTools` (the key this skill uses).
|
|
|
|
## Reference
|
|
|
|
Full verified v3 auth flow, every method used with request/response shape, the
|
|
file-version billing note, pagination, the error shape, and the cost formula:
|
|
`references/api-reference.md`.
|