@lossless.org/objectstorage 10.7.1 โ 11.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.smartconfig.json +30 -10
- package/dist_rust/ruststorage_linux_amd64 +0 -0
- package/dist_rust/ruststorage_linux_amd64.tsrust-build.json +5 -5
- package/dist_rust/ruststorage_linux_amd64_musl +0 -0
- package/dist_rust/ruststorage_linux_amd64_musl.tsrust-build.json +5 -5
- package/dist_rust/ruststorage_linux_arm64 +0 -0
- package/dist_rust/ruststorage_linux_arm64.tsrust-build.json +5 -5
- package/dist_rust/ruststorage_linux_arm64_musl +0 -0
- package/dist_rust/ruststorage_linux_arm64_musl.tsrust-build.json +5 -5
- package/dist_rust/ruststorage_macos_amd64 +0 -0
- package/dist_rust/ruststorage_macos_amd64.tsrust-build.json +5 -5
- package/dist_rust/ruststorage_macos_arm64 +0 -0
- package/dist_rust/ruststorage_macos_arm64.tsrust-build.json +5 -5
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dashboard.d.ts +49 -0
- package/dist_ts/classes.dashboard.js +234 -0
- package/dist_ts/dashboard.bundled.d.ts +4 -0
- package/dist_ts/dashboard.bundled.js +20 -0
- package/dist_ts/dashboard.interfaces.d.ts +132 -0
- package/dist_ts/dashboard.interfaces.js +2 -0
- package/dist_ts/dashboard.plugins.d.ts +5 -0
- package/dist_ts/dashboard.plugins.js +10 -0
- package/dist_ts/index.d.ts +219 -1
- package/dist_ts/index.js +209 -8
- package/package.json +17 -16
- package/readme.md +711 -30
- package/third-party-notices.md +21 -5
- package/third_party_licenses/borsh-1.8.1-license.txt +23 -0
- package/third_party_licenses/borsh-derive-1.8.1-apache-2.0.txt +201 -0
- package/third_party_licenses/cargo-dependencies.html +420 -0
- package/third_party_licenses/dyn-clone-1.0.20-license.txt +23 -0
- package/third_party_licenses/equivalent-1.0.3-license.txt +25 -0
- package/third_party_licenses/hashbrown-0.17.1-license.txt +25 -0
- package/third_party_licenses/indexmap-2.14.2-license.txt +25 -0
- package/third_party_licenses/inventory.json +310 -8
- package/third_party_licenses/proc-macro-crate-3.5.0-license.txt +23 -0
- package/third_party_licenses/ref-cast-1.0.27-license.txt +23 -0
- package/third_party_licenses/ref-cast-impl-1.0.27-license.txt +23 -0
- package/third_party_licenses/schemars-1.2.3-license.txt +21 -0
- package/third_party_licenses/syn-3.0.9-license.txt +23 -0
- package/third_party_licenses/toml_datetime-1.1.2-license.txt +19 -0
- package/third_party_licenses/toml_edit-0.25.17-license.txt +19 -0
- package/third_party_licenses/toml_parser-1.1.5-license.txt +19 -0
- package/third_party_licenses/winnow-1.0.4-license.txt +18 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dashboard.ts +280 -0
- package/ts/dashboard.bundled.ts.third-party-notices.json +1741 -0
- package/ts/dashboard.bundled.ts.third-party-notices.txt +5015 -0
- package/ts/dashboard.interfaces.ts +148 -0
- package/ts/dashboard.plugins.ts +13 -0
- package/ts/index.ts +473 -14
package/readme.md
CHANGED
|
@@ -69,13 +69,13 @@ described below.
|
|
|
69
69
|
### Clustering Features
|
|
70
70
|
|
|
71
71
|
- ๐ **Erasure coding** โ Reed-Solomon (configurable k data + m parity shards) for storage efficiency and fault tolerance
|
|
72
|
-
- ๐ **QUIC transport** โ multiplexed
|
|
72
|
+
- ๐ **QUIC transport** โ multiplexed inter-node communication via `quinn` with zero head-of-line blocking. Mutual TLS with a cluster CA when `cluster.tls` is set; without it, encrypted but not authenticated, for trusted networks only (see Cluster transport trust).
|
|
73
73
|
- ๐ฝ **Multi-drive awareness** โ each node manages multiple independent storage paths with health monitoring
|
|
74
74
|
- ๐ฉบ **Cluster health introspection** โ query native node, drive, quorum, and healing status for product dashboards
|
|
75
75
|
- ๐ค **Cluster membership** โ static seed config + runtime join, heartbeat-based failure detection
|
|
76
76
|
- โ๏ธ **Quorum writes** โ data is only acknowledged after k+1 shards are persisted
|
|
77
77
|
- ๐ **Quorum reads** โ reconstruct from any k available shards, local-first fast path
|
|
78
|
-
- ๐ฉน **Self-healing** โ background scanner
|
|
78
|
+
- ๐ฉน **Self-healing** โ background scanner rebuilds missing shards on the drives their manifest names
|
|
79
79
|
|
|
80
80
|
## Installation
|
|
81
81
|
|
|
@@ -114,7 +114,7 @@ glibc LGPL terms, Cargo licenses and native distribution inventory.
|
|
|
114
114
|
## Native builds and verification
|
|
115
115
|
|
|
116
116
|
`pnpm build` builds the current host architecture (GNU and musl on Linux), then
|
|
117
|
-
builds the TypeScript facade. Test preparation uses `
|
|
117
|
+
builds the TypeScript facade. Test preparation uses `rsrepo --configured-targets`
|
|
118
118
|
to cover all configured host-OS binaries in the packaging checks. Rust 1.95.0 is
|
|
119
119
|
pinned in `rust-toolchain.toml`. Mac builders
|
|
120
120
|
need Xcode command-line tools, both Darwin Rust targets and Rosetta 2 for Intel
|
|
@@ -126,7 +126,7 @@ with aliases from their full `*-unknown-linux-musl-gcc` names to the names above
|
|
|
126
126
|
Rust supplies its pinned, patched musl runtime. Native runtime notices are included
|
|
127
127
|
in the package; retain them when distributing standalone binaries.
|
|
128
128
|
|
|
129
|
-
Releases use `
|
|
129
|
+
Releases use `rsrepo matrix build` to assemble all six binaries from one clean
|
|
130
130
|
Git commit. Configure these environment variables on the release coordinator:
|
|
131
131
|
|
|
132
132
|
```sh
|
|
@@ -184,6 +184,11 @@ Cluster mode requires the control root and every drive root to exist before
|
|
|
184
184
|
startup. The process user must own them, no path component may be a symlink,
|
|
185
185
|
and group/world write permission must be disabled.
|
|
186
186
|
|
|
187
|
+
Without `cluster.tls` the cluster transport does not authenticate nodes: run
|
|
188
|
+
cluster mode only on a network where every host that can reach `quicPort` is a
|
|
189
|
+
cluster node, or set `cluster.tls` on every node (see
|
|
190
|
+
[Cluster transport trust](#cluster-transport-trust)).
|
|
191
|
+
|
|
187
192
|
```typescript
|
|
188
193
|
import { ObjectStorage } from '@lossless.org/objectstorage';
|
|
189
194
|
|
|
@@ -211,6 +216,21 @@ const storage = await ObjectStorage.createAndStart({
|
|
|
211
216
|
|
|
212
217
|
Objects are automatically split into chunks (default 4 MB), erasure-coded into 6 shards (4 data + 2 parity), and distributed across drives/nodes. Any 4 of 6 shards can reconstruct the original data.
|
|
213
218
|
|
|
219
|
+
Each drive root counts as one independent drive, so startup refuses two drive
|
|
220
|
+
roots on the same device (`st_dev`). Only the device number is checked:
|
|
221
|
+
partitions of one disk, LVM volumes, btrfs subvolumes and datasets of one ZFS
|
|
222
|
+
pool report distinct devices, are not detected, and keeping them apart is the
|
|
223
|
+
operator's responsibility. The control root may share a device with a drive.
|
|
224
|
+
For dev and test clusters whose drive roots are directories on one filesystem,
|
|
225
|
+
opt in explicitly; startup then logs which roots share a device:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
drives: {
|
|
229
|
+
paths: ['/srv/objectstorage-dev/drive-1', '/srv/objectstorage-dev/drive-2', '/srv/objectstorage-dev/drive-3'],
|
|
230
|
+
allowSharedDevice: true, // dev/test only: these roots are not independent drives
|
|
231
|
+
},
|
|
232
|
+
```
|
|
233
|
+
|
|
214
234
|
## Configuration
|
|
215
235
|
|
|
216
236
|
All config fields are optional โ sensible defaults are applied automatically.
|
|
@@ -263,22 +283,35 @@ const config: IObjectStorageConfig = {
|
|
|
263
283
|
multipart: {
|
|
264
284
|
expirationDays: 7, // Must be greater than zero
|
|
265
285
|
cleanupIntervalMinutes: 60, // Must be greater than zero
|
|
286
|
+
maxUploadsPerPrefixScope: 100, // Open uploads per prefix credential scope; must be non-zero
|
|
266
287
|
},
|
|
267
288
|
cluster: { // Optional โ omit for standalone mode
|
|
268
289
|
enabled: true,
|
|
269
|
-
nodeId: 'node-1', // Auto-generated UUID if omitted
|
|
290
|
+
nodeId: 'node-1', // Auto-generated UUID if omitted; required with tls
|
|
270
291
|
quicPort: 4000, // Default: 4000
|
|
271
292
|
seedNodes: [], // Addresses of existing cluster members
|
|
272
293
|
erasure: {
|
|
273
294
|
dataShards: 4, // Default: 4
|
|
274
295
|
parityShards: 2, // Default: 2
|
|
275
|
-
chunkSizeBytes: 4194304, // Default: 4 MB
|
|
296
|
+
chunkSizeBytes: 4194304, // Default: 4 MB; between 1 and 64 MiB
|
|
276
297
|
},
|
|
277
298
|
drives: {
|
|
278
299
|
paths: ['/mnt/disk1', '/mnt/disk2'],
|
|
300
|
+
allowSharedDevice: false, // Default: false; true only for dev/test roots on one device
|
|
279
301
|
},
|
|
280
302
|
heartbeatIntervalMs: 5000, // Default: 5000
|
|
281
303
|
heartbeatTimeoutMs: 30000, // Default: 30000
|
|
304
|
+
getDeadlineMs: 21600000, // Default: 6 h; 60000 (1 min) to 604800000 (7 d)
|
|
305
|
+
tls: { // Optional mutual TLS; set on every node or on none
|
|
306
|
+
caFile: '/etc/objectstorage/tls/ca.pem',
|
|
307
|
+
certFile: '/etc/objectstorage/tls/node-1.pem', // its DNS SAN is nodeId
|
|
308
|
+
keyFile: '/etc/objectstorage/tls/node-1.key', // mode 0600 or 0400
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
dashboard: { // Optional read-only dashboard; off unless enabled
|
|
312
|
+
enabled: true,
|
|
313
|
+
address: '127.0.0.1', // Default: '127.0.0.1'
|
|
314
|
+
port: 3001, // Default: 3001; 0 allocates an ephemeral TCP port; never the S3 port
|
|
282
315
|
},
|
|
283
316
|
};
|
|
284
317
|
|
|
@@ -460,6 +493,7 @@ interface IStorageCredential {
|
|
|
460
493
|
- Requests that were already authenticated before the replacement keep running; auth is evaluated when each request starts.
|
|
461
494
|
- No restart is required, and runtime-created credentials survive restart unless `storage.cleanSlate` clears the bounded storage contents; the owned root, maintenance-lock inode, and any durable provider/pool identity are preserved.
|
|
462
495
|
- Replacement input must contain at least one credential, each `accessKeyId` and `secretAccessKey` must be non-empty, and `accessKeyId` values must be unique.
|
|
496
|
+
- A new `accessKeyId` must not be held by a live or revoked prefix credential (`ECREDENTIAL_CONFLICT`); the same check applies to bucket tenant create, ensure and rotate. Prefix credentials are not part of `listCredentials()`; see [Prefix Credentials](#prefix-credentials).
|
|
463
497
|
|
|
464
498
|
## Bucket Tenants
|
|
465
499
|
|
|
@@ -499,6 +533,207 @@ const tenants = await storage.listBucketTenants();
|
|
|
499
533
|
- Tenant credentials can list, read, write, and delete objects in their assigned bucket, but cannot list all buckets, access other buckets, copy from other buckets, delete buckets, or mutate bucket policies.
|
|
500
534
|
- Bucket tenant APIs require `auth.enabled: true`.
|
|
501
535
|
|
|
536
|
+
## Prefix Credentials
|
|
537
|
+
|
|
538
|
+
A prefix credential is an ordinary SigV4 key that reaches only the keys below one
|
|
539
|
+
prefix of one bucket, and only until it expires. It is meant for handing a
|
|
540
|
+
short-lived key to a guest (a VM, a job, a mounted drive) that must see one
|
|
541
|
+
folder and nothing else.
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
const credential = await storage.createPrefixCredential({
|
|
545
|
+
bucketName: 'drives',
|
|
546
|
+
prefix: 'drives/user-42/',
|
|
547
|
+
ttlSeconds: 3600, // 1 to 43200 (12 h)
|
|
548
|
+
});
|
|
549
|
+
// { accessKeyId, secretAccessKey, bucketName, prefix, region, createdAt, expiresAt }
|
|
550
|
+
|
|
551
|
+
const client = new S3Client({
|
|
552
|
+
endpoint: 'http://localhost:3000',
|
|
553
|
+
region: credential.region,
|
|
554
|
+
credentials: {
|
|
555
|
+
accessKeyId: credential.accessKeyId,
|
|
556
|
+
secretAccessKey: credential.secretAccessKey,
|
|
557
|
+
},
|
|
558
|
+
forcePathStyle: true,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
await storage.revokePrefixCredential({ accessKeyId: credential.accessKeyId }); // { revoked: true }
|
|
562
|
+
const live = await storage.listPrefixCredentials({ bucketName: 'drives' }); // metadata, no secrets
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
interface IPrefixCredentialInput {
|
|
567
|
+
bucketName: string;
|
|
568
|
+
prefix: string;
|
|
569
|
+
ttlSeconds?: number; // first node: lifetime in seconds
|
|
570
|
+
expiresAt?: number; // further nodes: the first node's expiresAt (epoch ms)
|
|
571
|
+
accessKeyId?: string; // supplied together with secretAccessKey and expiresAt only
|
|
572
|
+
secretAccessKey?: string;
|
|
573
|
+
region?: string; // default: server.region
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Input rules.**
|
|
578
|
+
- Exactly one of `ttlSeconds` (an integer, 1 to 43 200) and `expiresAt` (an
|
|
579
|
+
integer epoch-ms time after now and at most 43 200 s ahead).
|
|
580
|
+
- Key material is generated when omitted (`SP` plus 20 uppercase hex
|
|
581
|
+
characters, a 64-hex-character secret). Supplied material needs `expiresAt`,
|
|
582
|
+
and both `accessKeyId` and `secretAccessKey`: an ID of 16 to 128 characters
|
|
583
|
+
of `A-Z0-9`, and a secret of 32 to 128 printable ASCII characters without
|
|
584
|
+
spaces.
|
|
585
|
+
- The prefix is validated, never normalised. It is 1 to 1023 bytes of
|
|
586
|
+
`A-Z a-z 0-9 - . _ ~ /` (RFC 3986 unreserved characters plus `/`, no
|
|
587
|
+
percent-encoding). It ends with `/` and does not start with `/`, and has no
|
|
588
|
+
empty, `.` or `..` segment.
|
|
589
|
+
- The bucket must exist. The access key ID must not belong to a static or bucket
|
|
590
|
+
tenant credential, and it must not have been revoked.
|
|
591
|
+
- The secret appears only in the create result. It is never listed, logged, or
|
|
592
|
+
stored in `storage.config`.
|
|
593
|
+
- Errors are `ObjectStorageCredentialError` with a `code`:
|
|
594
|
+
|
|
595
|
+
| Code | Meaning |
|
|
596
|
+
|---|---|
|
|
597
|
+
| `ECREDENTIAL_AUTH_DISABLED` | `auth.enabled` is false |
|
|
598
|
+
| `ECREDENTIAL_INVALID_PREFIX` | the prefix fails the grammar |
|
|
599
|
+
| `ECREDENTIAL_INVALID_TTL` | not exactly one of `ttlSeconds` and `expiresAt`, or `ttlSeconds` out of range |
|
|
600
|
+
| `ECREDENTIAL_INVALID_EXPIRY` | `expiresAt` is not in the future, or is more than 12 h ahead |
|
|
601
|
+
| `ECREDENTIAL_INVALID_KEY` | the key material or the region is malformed, or only part of it was supplied |
|
|
602
|
+
| `ECREDENTIAL_NO_SUCH_BUCKET` | the bucket does not exist |
|
|
603
|
+
| `ECREDENTIAL_BUCKET_UNAVAILABLE` | the bucket is a sealed or retired migration source |
|
|
604
|
+
| `ECREDENTIAL_CONFLICT` | the ID is held by another credential, or a replay differs from the stored record |
|
|
605
|
+
| `ECREDENTIAL_REVOKED` | the ID was revoked |
|
|
606
|
+
| `ECREDENTIAL_NOT_PREFIX` | a revoke named a static or bucket tenant credential |
|
|
607
|
+
| `ECREDENTIAL_LIMIT` | 10 000 live credentials and revocations are stored |
|
|
608
|
+
| `ECREDENTIAL_PERSISTENCE` | the store could not be written |
|
|
609
|
+
| `ECREDENTIAL_STORE_UNAVAILABLE` | the store file is unreadable (see below) |
|
|
610
|
+
|
|
611
|
+
**Access.** A key is covered when its bytes start with the prefix; a key equal
|
|
612
|
+
to the prefix (its folder marker) is covered.
|
|
613
|
+
- **Objects.** GetObject, HeadObject, PutObject, DeleteObject and the multipart
|
|
614
|
+
operations work on covered keys.
|
|
615
|
+
- Writes also refuse keys with `.`, `..` or empty segments after the prefix,
|
|
616
|
+
and keys with control characters.
|
|
617
|
+
- CopyObject and UploadPartCopy need the source to be covered too.
|
|
618
|
+
- Object requests accept only the query parameters the operation uses:
|
|
619
|
+
`uploads`, `uploadId`, `partNumber`, `max-parts` and `part-number-marker`,
|
|
620
|
+
the six `response-*` header overrides on GetObject and HeadObject, plus
|
|
621
|
+
the `x-id` operation label that AWS SDKs append. Any other object query
|
|
622
|
+
(`?acl`, `?tagging`, `?versionId`, `?retention`, โฆ) is refused with
|
|
623
|
+
`AccessDenied` (403), also a name S3 does not define (`?v=1`), and so is
|
|
624
|
+
any repeated query parameter. A prefix
|
|
625
|
+
credential never names a version: `?versionId` is refused even as `null`,
|
|
626
|
+
and so is a copy source with a `?versionId=` suffix.
|
|
627
|
+
- Requests that pass these checks still go through the router's allowlist
|
|
628
|
+
(see [Supported S3 Operations](#supported-s3-operations)), so a parameter
|
|
629
|
+
the operation does not implement gets `NotImplemented` (501).
|
|
630
|
+
- **DeleteObjects.** Every member is checked on its own: covered keys are
|
|
631
|
+
deleted, and the others get a per-key `AccessDenied`.
|
|
632
|
+
- **Listings.** ListObjects V1, ListObjectsV2, ListObjectVersions and
|
|
633
|
+
ListMultipartUploads need a `prefix` parameter that is covered. A missing or
|
|
634
|
+
wider prefix is refused, never clamped.
|
|
635
|
+
- **Bucket level.**
|
|
636
|
+
- ListBuckets returns only the credential's bucket.
|
|
637
|
+
- HeadBucket and `GET /{bucket}?location` work on that bucket.
|
|
638
|
+
- Any other bucket-level query (`?acl`, `?versioning`, `?lifecycle`, โฆ) is
|
|
639
|
+
refused with `AccessDenied`, and so is any repeated query parameter.
|
|
640
|
+
- Creating or deleting buckets and bucket policy operations are refused.
|
|
641
|
+
- **Policies.** The scope check runs before bucket policies, so a policy Allow
|
|
642
|
+
(including `Principal: "*"`) never widens a prefix credential. A policy Deny
|
|
643
|
+
still applies.
|
|
644
|
+
|
|
645
|
+
**Expiry and deadlines.**
|
|
646
|
+
- A credential authenticates while the engine clock is before `expiresAt`.
|
|
647
|
+
After that, or after revocation, requests get `InvalidAccessKeyId` (403).
|
|
648
|
+
- Requests that were admitted end at `expiresAt` as well: a trickled upload
|
|
649
|
+
fails with nothing stored, and a slowly read download is cut off.
|
|
650
|
+
- Revocation affects only requests admitted after it. Requests already running
|
|
651
|
+
continue until they finish or until `expiresAt`.
|
|
652
|
+
- There is no extend operation. To refresh, create a new key and revoke the old
|
|
653
|
+
one.
|
|
654
|
+
- **Multipart cap.** All prefix credentials of one scope (the same bucket and
|
|
655
|
+
prefix) together hold at most `multipart.maxUploadsPerPrefixScope`
|
|
656
|
+
(default 100, must be non-zero) open multipart uploads. The next
|
|
657
|
+
CreateMultipartUpload of that scope gets `AccessDenied` (403) until one of
|
|
658
|
+
them ends: completed or aborted by any credential, or removed by the
|
|
659
|
+
multipart expiry cleanup. Revoking, expiring or replacing a credential
|
|
660
|
+
frees none of its scope's slots, so issuing new keys does not raise the cap.
|
|
661
|
+
The count is held in memory and resets when the engine restarts. The
|
|
662
|
+
engine-wide limit of 100 000 uploads still applies.
|
|
663
|
+
|
|
664
|
+
**Bucket deletion.** Deletion attempts revoke first; callers re-issue.
|
|
665
|
+
- Every path that deletes a bucket revokes the bucket's prefix credentials
|
|
666
|
+
before it removes anything: S3 DeleteBucket, `deleteBucket()`,
|
|
667
|
+
`deleteBucketTenant()` without an `accessKeyId`, and migration source
|
|
668
|
+
cleanup. A recreated bucket of the same name is not reachable with them.
|
|
669
|
+
- S3 DeleteBucket checks that the bucket is empty before it revokes, so a
|
|
670
|
+
`BucketNotEmpty` (409) keeps the credentials.
|
|
671
|
+
- If the revocation cannot be stored, the deletion is refused.
|
|
672
|
+
- A deletion that fails or is refused after the revocation (for example a
|
|
673
|
+
stale resource fence, `EFENCE_STALE`) returns that error, never a success,
|
|
674
|
+
and leaves the credentials revoked. Callers that keep the bucket create new
|
|
675
|
+
prefix credentials.
|
|
676
|
+
|
|
677
|
+
**Persistence.**
|
|
678
|
+
- Credentials and revocations live in `.smartstorage/prefix-credentials.json`,
|
|
679
|
+
under the same root as `credentials.json`. The engine writes the file
|
|
680
|
+
atomically with mode `0600`, and `storage.cleanSlate` clears it.
|
|
681
|
+
- A sweep every 60 seconds drops expired entries.
|
|
682
|
+
- Engines older than prefix credentials never read the file, so a rollback
|
|
683
|
+
disables every prefix credential rather than widening it.
|
|
684
|
+
- **Fail closed.** If the file is unreadable, larger than 32 MiB, malformed,
|
|
685
|
+
or inconsistent (duplicate IDs, an ID held by a static credential, a record
|
|
686
|
+
that fails validation), the store is unavailable:
|
|
687
|
+
- every prefix credential is refused;
|
|
688
|
+
- create, revoke and list fail with `ECREDENTIAL_STORE_UNAVAILABLE`;
|
|
689
|
+
- no new static or tenant access key ID can be introduced, because the ID
|
|
690
|
+
collision check cannot run: `replaceCredentials()`,
|
|
691
|
+
`createBucketTenant()`, `ensureBucketTenant()` and
|
|
692
|
+
`rotateBucketTenantCredentials()` throw `ObjectStorageCredentialError`
|
|
693
|
+
with code `ECREDENTIAL_STORE_UNAVAILABLE` when they would add an access
|
|
694
|
+
key ID that is not in the current set;
|
|
695
|
+
- bucket deletion is refused, because the bucket's credentials cannot be
|
|
696
|
+
revoked: `deleteBucket()` and `deleteBucketTenant()` without an
|
|
697
|
+
`accessKeyId` throw `ObjectStorageCredentialError` with code
|
|
698
|
+
`ECREDENTIAL_STORE_UNAVAILABLE`, and S3 DeleteBucket returns
|
|
699
|
+
`InvalidRequest` (400) with a message starting
|
|
700
|
+
`ECREDENTIAL_STORE_UNAVAILABLE:`;
|
|
701
|
+
- static and tenant credentials keep working;
|
|
702
|
+
- `getHealth().auth.prefixCredentialStoreAvailable` is `false`;
|
|
703
|
+
- the engine logs the condition without secrets and leaves the file as found.
|
|
704
|
+
|
|
705
|
+
To recover, repair or delete the file and restart. Deleting it revokes every
|
|
706
|
+
prefix credential and forgets every revocation, so never reuse a revoked ID
|
|
707
|
+
afterwards. Generated IDs are random.
|
|
708
|
+
|
|
709
|
+
**Cluster mode.** Prefix credentials are node-local, like runtime credentials:
|
|
710
|
+
nothing replicates them, and the caller fans out.
|
|
711
|
+
1. Create on one node with `ttlSeconds`.
|
|
712
|
+
2. Create on every other node with the first node's `accessKeyId`,
|
|
713
|
+
`secretAccessKey`, `expiresAt` and `region`.
|
|
714
|
+
- Each node records its own `createdAt`; every other field matches the first
|
|
715
|
+
node's record.
|
|
716
|
+
- An identical replay returns the stored record unchanged. Any difference
|
|
717
|
+
gives `ECREDENTIAL_CONFLICT`.
|
|
718
|
+
- A late create never extends the lifetime.
|
|
719
|
+
3. To revoke, call `revokePrefixCredential` on every node and retry until each
|
|
720
|
+
node acknowledges.
|
|
721
|
+
- A node that never saw the ID records the revocation for 12 h, so a create
|
|
722
|
+
that reaches it later is refused with `ECREDENTIAL_REVOKED`.
|
|
723
|
+
- `revoked` is `true` only where a live credential was removed.
|
|
724
|
+
|
|
725
|
+
**Revocation guarantee.**
|
|
726
|
+
- Once a node has acknowledged a revocation, it refuses every request admitted
|
|
727
|
+
after the acknowledgement. It also refuses any later create for that ID until
|
|
728
|
+
the ID could no longer be created anyway: an `expiresAt` create must lie
|
|
729
|
+
within 12 h of the first node's create, and the revocation is kept at least
|
|
730
|
+
that long.
|
|
731
|
+
- A node that never acknowledged serves the key until the caller's retry
|
|
732
|
+
reaches it, or until `expiresAt`.
|
|
733
|
+
- The hard bound is `expiresAt`, at most 12 h after the first create. Each node
|
|
734
|
+
judges expiry by its own clock, so clock offsets shift these instants.
|
|
735
|
+
- `listPrefixCredentials()` lets the caller reconcile nodes.
|
|
736
|
+
|
|
502
737
|
## Bucket Backup/Restore
|
|
503
738
|
|
|
504
739
|
```typescript
|
|
@@ -716,7 +951,9 @@ const metrics = await storage.getMetrics();
|
|
|
716
951
|
```
|
|
717
952
|
|
|
718
953
|
- `getHealth()` reports running state, storage directory and pool observation, auth enabled state, credential counts, bucket count, object count, total bytes, and cluster health.
|
|
719
|
-
- `
|
|
954
|
+
- `getHealth().auth` also reports `prefixCredentialCount` (live prefix credentials), `prefixCredentialTombstoneCount` (unexpired revocations) and `prefixCredentialStoreAvailable` (`false` while the prefix credential file is unreadable and every prefix credential is refused).
|
|
955
|
+
- `getMetrics()` returns numeric counters and a Prometheus text snippet for bucket, object, byte, tenant credential, prefix credential (`objectstorage_prefix_credentials_total`, `objectstorage_prefix_credential_tombstones_total`, `objectstorage_prefix_credential_store_available`), and cluster-enabled metrics.
|
|
956
|
+
- `getHealth()` reads the prefix credential figures through the `getPrefixCredentialStatus` IPC command (no TypeScript method of its own), and `getMetrics()` derives its prefix metrics from `getHealth()`. The command takes no parameters and returns `{ available, credentialCount, tombstoneCount }`: counts and the store state only, never an access key ID, secret, bucket or prefix.
|
|
720
957
|
|
|
721
958
|
## Runtime Stats
|
|
722
959
|
|
|
@@ -785,6 +1022,23 @@ interface IClusterHealth {
|
|
|
785
1022
|
drives?: IClusterDriveHealth[];
|
|
786
1023
|
erasure?: IClusterErasureHealth;
|
|
787
1024
|
repairs?: IClusterRepairHealth;
|
|
1025
|
+
transport?: IClusterTransportHealth;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
interface IClusterTransportHealth {
|
|
1029
|
+
mode: 'mutualTls' | 'unauthenticated';
|
|
1030
|
+
certificate?: { sha256: string; notAfter: number }; // only with mutualTls
|
|
1031
|
+
caCertificates?: { sha256: string; notAfter: number }[]; // file order; only with mutualTls
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
type TDriveMedia = 'rotational' | 'smr' | 'flash' | 'zoned';
|
|
1035
|
+
|
|
1036
|
+
interface IClusterDriveHealth {
|
|
1037
|
+
// index, path, status, capacity, error and erasure set fields, plus:
|
|
1038
|
+
media: {
|
|
1039
|
+
declared?: TDriveMedia; // not reported yet: drives cannot declare their media
|
|
1040
|
+
detected: TDriveMedia | 'unknown'; // detected once at startup
|
|
1041
|
+
};
|
|
788
1042
|
}
|
|
789
1043
|
```
|
|
790
1044
|
|
|
@@ -792,8 +1046,203 @@ interface IClusterHealth {
|
|
|
792
1046
|
- Standalone mode returns `{ enabled: false }`.
|
|
793
1047
|
- Peer status is the local node's current view of cluster membership and heartbeats, so it is best-effort and may lag real network state.
|
|
794
1048
|
- Drive health is based on live native probe checks on the configured local drive paths. Capacity values are cheap filesystem snapshots.
|
|
1049
|
+
- `drives[].media.detected` is the medium the engine detected for each drive once at startup, never per health call. On Linux it follows the drive root's filesystem to the whole disk behind it (`st_dev` through `/sys/dev/block`; a partition counts as its disk) and reads the disk's `queue/zoned` and `queue/rotational`: `zoned` for host-managed or host-aware devices (SMR or ZNS), otherwise `rotational` or `flash`, as the disk's driver sets `queue/rotational`. Virtual and network block devices report the flag their driver or hypervisor sets: QEMU disks show `rotational` unless they are configured with `rotation_rate=1`, and NVMe volumes show `flash`. Declaring a drive's media, which is planned, is what will correct such a reading. It is `unknown`, never a guess, for filesystems without a block device of their own (tmpfs, overlay, network and FUSE filesystems, btrfs subvolumes), for device-mapper, md, loop and other virtual block devices (LVM, dm-crypt and software RAID included), when sysfs gives no usable answer, and on every platform but Linux. Drive-managed SMR reports itself as rotational and cannot be detected (`smr` can only ever be declared). The detected media change no behaviour today.
|
|
795
1050
|
- `quorumHealthy` means the local node currently sees majority quorum and enough available placements in every erasure set to satisfy the configured write quorum.
|
|
1051
|
+
- `transport.mode` is `mutualTls` when `cluster.tls` is set and `unauthenticated` otherwise. With mutual TLS, `certificate` and `caCertificates` describe the files the engine loaded at startup: the SHA-256 of each DER certificate (lowercase hex) and its `notAfter` in milliseconds since the Unix epoch. The files are read only at startup, so during a certificate or CA rotation these show what each node actually runs.
|
|
796
1052
|
- Repair fields expose the background healer's currently available runtime state. They are best-effort and limited to what the engine tracks today, such as whether a scan is active, the last completed run, and the last error.
|
|
1053
|
+
- `repairs.shardsChecked`, `shardsHealed` and `failed` count the last completed scan's placement checks, shards written and failures. `repairs.shardsPending` counts the pending shards (see [Self-Healing](#self-healing)) that scan left pending because their drive was out of reach, refused the write, or their object was busy. While a scan runs, `lastRunStartedAt` is the running scan's start and the counts are still the previous scan's; `runHealingScan()` returns one scan's start and counts together.
|
|
1054
|
+
|
|
1055
|
+
## Dashboard
|
|
1056
|
+
|
|
1057
|
+
Every node can serve a small read-only dashboard for quick visibility: the
|
|
1058
|
+
cluster's nodes, drives, erasure layout and quorum, object and byte totals,
|
|
1059
|
+
request, error and byte rates, healing state and recent engine events. It is
|
|
1060
|
+
off by default:
|
|
1061
|
+
|
|
1062
|
+
```typescript
|
|
1063
|
+
const storage = await ObjectStorage.createAndStart({
|
|
1064
|
+
// ...
|
|
1065
|
+
dashboard: { enabled: true, address: '10.0.0.5', port: 3001 },
|
|
1066
|
+
});
|
|
1067
|
+
console.log(storage.dashboardPort); // the bound port; undefined while off
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
Open `http://<address>:<port>/` in a browser. The page is bundled into the
|
|
1071
|
+
package (`ts_web/`, built into `ts/dashboard.bundled.ts`), so nothing is
|
|
1072
|
+
fetched from elsewhere, and it updates itself over one TypedSocket connection
|
|
1073
|
+
to the same origin.
|
|
1074
|
+
|
|
1075
|
+
- **Configuration.** `dashboard.enabled` must be set. `address` defaults to
|
|
1076
|
+
`127.0.0.1` and `port` to `3001`; `port: 0` asks the OS for a port. A port
|
|
1077
|
+
equal to `server.port` is refused at construction. A dashboard that is
|
|
1078
|
+
enabled but cannot bind fails `start()`, and the engine is stopped: it never
|
|
1079
|
+
runs silently without it. The `dashboard` section is not sent to the engine.
|
|
1080
|
+
- **What it shows.** Nodes (status and last heartbeat in this node's view,
|
|
1081
|
+
start time); local drives of every node (status, usage, errors,
|
|
1082
|
+
last probe); the erasure layout and quorum; objects and bytes per node and
|
|
1083
|
+
summed; request, error and byte rates per node and summed, with two-minute
|
|
1084
|
+
sparklines kept in the browser; healing per node (last scan, checked,
|
|
1085
|
+
healed, pending shards, failures); and the newest events of each node (peer
|
|
1086
|
+
status changes, drive status changes, healing scan results, engine start).
|
|
1087
|
+
A standalone node shows its totals, rates and events. Rates are derived
|
|
1088
|
+
from counters of the S3 listener; byte counts are taken at the TCP stream
|
|
1089
|
+
and include HTTP headers. Nothing is stored: a reload starts the sparklines
|
|
1090
|
+
again.
|
|
1091
|
+
- **Where the data comes from.** One management command, `getClusterSnapshot`
|
|
1092
|
+
(also `ObjectStorage.getClusterSnapshot()`), built by the engine from memory
|
|
1093
|
+
only: the drive states the last heartbeat probe left, the in-memory runtime
|
|
1094
|
+
stats, healing state, S3 listener counters and a ring of the newest 64
|
|
1095
|
+
events. It never probes a drive, syncs, walks a directory or reads the
|
|
1096
|
+
control store. A cluster node asks each online peer for that peer's own
|
|
1097
|
+
snapshot over QUIC (`NodeSnapshot`), in parallel, with one second per peer;
|
|
1098
|
+
a peer that is not online or does not answer in time shows as unavailable in
|
|
1099
|
+
that sample. Any node with the dashboard on therefore shows the whole
|
|
1100
|
+
cluster, and the other nodes need no dashboard of their own.
|
|
1101
|
+
- **Cost.** With no dashboard connected there is no timer, no IPC and no QUIC
|
|
1102
|
+
traffic. While at least one is connected, one shared sampler takes a
|
|
1103
|
+
snapshot every two seconds, keeps at most one in flight and skips ticks
|
|
1104
|
+
while one runs, and pushes the same sample (a few KB per node) to every
|
|
1105
|
+
connected dashboard. At most 16 dashboard sockets are admitted at once; the
|
|
1106
|
+
17th is refused with `503`. On the engine side, request and byte counting is
|
|
1107
|
+
a relaxed atomic add per response and per socket read or write. An enabled
|
|
1108
|
+
dashboard with nobody watching runs no timer and sends no traffic; on a
|
|
1109
|
+
three-node cluster against 11.0.0 its CPU cost per request was within 0.4 %,
|
|
1110
|
+
and the engine's memory was unchanged. Its server modules and bundled page
|
|
1111
|
+
add about 50 MiB to the Node.js process of each node once, at start, and
|
|
1112
|
+
that does not grow with time or stored objects. With 16 dashboards open on
|
|
1113
|
+
every node, large uploads ran about 1.5 % slower and the p99 latency was
|
|
1114
|
+
about 5 % higher.
|
|
1115
|
+
- **Security.** The dashboard is read-only by construction: its socket
|
|
1116
|
+
registers one read handler and nothing else, so it cannot start a healing
|
|
1117
|
+
scan, change configuration or touch S3 traffic or stored data. It has no
|
|
1118
|
+
authentication: protect it with the bind address and a firewall, for
|
|
1119
|
+
example by binding it to a private interface only. It shows node IDs,
|
|
1120
|
+
addresses, drive paths, counts and event messages, never credentials or
|
|
1121
|
+
object keys.
|
|
1122
|
+
- **Versions.** Run every node on the same version, as clusters already
|
|
1123
|
+
require. An engine that predates `NodeSnapshot` cannot decode the request,
|
|
1124
|
+
logs a stream error and closes the stream, and the asking node shows it as
|
|
1125
|
+
unavailable.
|
|
1126
|
+
|
|
1127
|
+
`getClusterHealth()` probes every local drive (a write, sync and read of a
|
|
1128
|
+
probe file) on each call; poll `getClusterSnapshot()` instead when you need a
|
|
1129
|
+
cheap periodic view.
|
|
1130
|
+
|
|
1131
|
+
## Supported S3 Operations
|
|
1132
|
+
|
|
1133
|
+
The router fails closed, in standalone and cluster mode alike. A request
|
|
1134
|
+
selects one operation from its method, its path (`/`, `/{bucket}`,
|
|
1135
|
+
`/{bucket}/{key}`) and its query names, and each query name must be one that
|
|
1136
|
+
operation implements. Anything else gets `NotImplemented` (501, with the S3 XML
|
|
1137
|
+
error body) before the request takes a lock, reads its body or touches a bucket
|
|
1138
|
+
or object. A credential the request is not allowed for still gets
|
|
1139
|
+
`AccessDenied` (403) first. Until 11.0.0 such requests were served as another
|
|
1140
|
+
operation: `PUT ?tagging` overwrote the object with the XML body,
|
|
1141
|
+
`DELETE ?lifecycle` deleted an empty bucket, `DELETE ?versionId=โฆ` deleted the
|
|
1142
|
+
current object, and `GET ?versioning` answered with an object listing.
|
|
1143
|
+
|
|
1144
|
+
| Operation | Request | Query names besides the selector |
|
|
1145
|
+
|---|---|---|
|
|
1146
|
+
| ListBuckets | `GET /` | none |
|
|
1147
|
+
| CreateBucket, DeleteBucket, HeadBucket | `PUT`, `DELETE`, `HEAD /{bucket}` | none |
|
|
1148
|
+
| ListObjects | `GET /{bucket}` | `prefix`, `delimiter`, `max-keys`, `marker`, `encoding-type` |
|
|
1149
|
+
| ListObjectsV2 | `GET /{bucket}?list-type=2` | `prefix`, `delimiter`, `max-keys`, `continuation-token`, `start-after`, `fetch-owner`, `encoding-type` |
|
|
1150
|
+
| ListObjectVersions | `GET /{bucket}?versions` | `prefix`, `delimiter`, `max-keys`, `key-marker`, `version-id-marker`, `encoding-type` |
|
|
1151
|
+
| ListMultipartUploads | `GET /{bucket}?uploads` | `prefix`, `encoding-type` |
|
|
1152
|
+
| GetBucketLocation | `GET /{bucket}?location` | none |
|
|
1153
|
+
| GetBucketVersioning | `GET /{bucket}?versioning` | none |
|
|
1154
|
+
| GetBucketLifecycleConfiguration | `GET /{bucket}?lifecycle` | none |
|
|
1155
|
+
| GetBucketPolicy, PutBucketPolicy, DeleteBucketPolicy | `GET`, `PUT`, `DELETE /{bucket}?policy` | none |
|
|
1156
|
+
| DeleteObjects | `POST /{bucket}?delete` | none |
|
|
1157
|
+
| GetObject, HeadObject | `GET`, `HEAD /{bucket}/{key}` | `versionId`, `response-cache-control`, `response-content-disposition`, `response-content-encoding`, `response-content-language`, `response-content-type`, `response-expires` |
|
|
1158
|
+
| DeleteObject | `DELETE /{bucket}/{key}` | `versionId` |
|
|
1159
|
+
| PutObject, CopyObject | `PUT /{bucket}/{key}`, CopyObject with `x-amz-copy-source` | none |
|
|
1160
|
+
| CreateMultipartUpload | `POST /{bucket}/{key}?uploads` | none |
|
|
1161
|
+
| UploadPart, UploadPartCopy | `PUT /{bucket}/{key}?partNumber=โฆ&uploadId=โฆ` | none |
|
|
1162
|
+
| CompleteMultipartUpload | `POST /{bucket}/{key}?uploadId=โฆ` | none |
|
|
1163
|
+
| AbortMultipartUpload | `DELETE /{bucket}/{key}?uploadId=โฆ` | none |
|
|
1164
|
+
| ListParts | `GET /{bucket}/{key}?uploadId=โฆ` | `max-parts`, `part-number-marker` |
|
|
1165
|
+
|
|
1166
|
+
Every operation also accepts the `x-id` label that AWS SDKs append and the SigV4
|
|
1167
|
+
query-signing parameters of a presigned URL (`X-Amz-Algorithm`,
|
|
1168
|
+
`X-Amz-Credential`, `X-Amz-Date`, `X-Amz-Expires`, `X-Amz-SignedHeaders`,
|
|
1169
|
+
`X-Amz-Signature`, `X-Amz-Security-Token`, `X-Amz-Content-Sha256`).
|
|
1170
|
+
Query-string signatures are not verified: a presigned request is served as an
|
|
1171
|
+
unsigned one, which is anonymous when auth is enabled.
|
|
1172
|
+
|
|
1173
|
+
An object GET or HEAD (GetObject, HeadObject, ListParts) ignores query names S3
|
|
1174
|
+
does not define, as S3 does, so links with cache-busters (`?v=1`) or tracking
|
|
1175
|
+
parameters (`?fbclid=โฆ`) keep working. A name S3 defines that the operation
|
|
1176
|
+
does not implement (`?acl`, `?partNumber=1`, โฆ) is still refused there, and
|
|
1177
|
+
every other request (bucket level, every write and delete) refuses unknown
|
|
1178
|
+
names too. The set of names S3 defines is taken from the AWS SDK v3 model
|
|
1179
|
+
(3.1039.0). With CORS enabled, the 501 and 405 responses carry the CORS headers.
|
|
1180
|
+
|
|
1181
|
+
The `response-*` parameters of GetObject and HeadObject replace the
|
|
1182
|
+
`Cache-Control`, `Content-Disposition`, `Content-Encoding`, `Content-Language`,
|
|
1183
|
+
`Content-Type` and `Expires` headers of a 200 or 206 response with the given
|
|
1184
|
+
value, verbatim. A value that is not a valid HTTP header value (a control
|
|
1185
|
+
character such as CR or LF) returns `InvalidArgument` (400). With auth enabled, an anonymous
|
|
1186
|
+
request that carries one returns `InvalidRequest` (400), as S3 does, because an
|
|
1187
|
+
override such as `response-content-type=text/html` would let a public object
|
|
1188
|
+
serve script from the storage origin; a presigned URL is such a request. With
|
|
1189
|
+
auth disabled the overrides apply.
|
|
1190
|
+
|
|
1191
|
+
Versioning is never enabled on a bucket:
|
|
1192
|
+
- `GET ?versioning` returns the empty `<VersioningConfiguration/>` of a bucket
|
|
1193
|
+
that was never versioned. `PUT ?versioning` is `NotImplemented`.
|
|
1194
|
+
- `GET ?lifecycle` returns `NoSuchLifecycleConfiguration` (404). `PUT` and
|
|
1195
|
+
`DELETE ?lifecycle` are `NotImplemented`.
|
|
1196
|
+
- `GET ?versions` lists every current object as its S3 null version
|
|
1197
|
+
(`VersionId` `null`, `IsLatest` true), with `CommonPrefixes` and paging on
|
|
1198
|
+
`key-marker` and `version-id-marker`. A truncated page returns the last key
|
|
1199
|
+
or common prefix as `NextKeyMarker`, plus `NextVersionIdMarker` `null` after a
|
|
1200
|
+
key. A `version-id-marker` needs a `key-marker`.
|
|
1201
|
+
- `versionId` on GET, HEAD and DELETE, and a `?versionId=` after the copy source
|
|
1202
|
+
of CopyObject and UploadPartCopy: `null` is the current object. A
|
|
1203
|
+
well-formed id (a lowercase UUID) names no version and returns
|
|
1204
|
+
`NoSuchVersion` (404; `NoSuchBucket` when the bucket is missing). Anything
|
|
1205
|
+
else, the empty string included, returns `InvalidArgument` (400). A `?`
|
|
1206
|
+
inside a copy-source key must be encoded as `%3F`.
|
|
1207
|
+
- These reads are authorized as `s3:GetBucketVersioning`,
|
|
1208
|
+
`s3:GetLifecycleConfiguration` and `s3:ListBucketVersions`. A `versionId`,
|
|
1209
|
+
`null` included, is authorized as `s3:GetObjectVersion` (GET, HEAD, and a
|
|
1210
|
+
copy source's `?versionId=`) or `s3:DeleteObjectVersion` (DELETE), not as
|
|
1211
|
+
`s3:GetObject` or `s3:DeleteObject`, so a policy that allows DeleteObject and
|
|
1212
|
+
denies DeleteObjectVersion keeps protecting history once buckets are
|
|
1213
|
+
versioned. Bucket tenants may use both actions in their bucket.
|
|
1214
|
+
|
|
1215
|
+
`encoding-type=url` percent-encodes, on ListObjects, ListObjectsV2,
|
|
1216
|
+
ListObjectVersions and ListMultipartUploads, every UTF-8 byte outside
|
|
1217
|
+
`A-Z a-z 0-9 - . _ ~` and `/` (a space is `%20`, never `+`) in the elements S3
|
|
1218
|
+
encodes for that operation, and the response carries
|
|
1219
|
+
`<EncodingType>url</EncodingType>`:
|
|
1220
|
+
- ListObjects: `Prefix`, `Marker`, `NextMarker`, `Delimiter`, `Key` and
|
|
1221
|
+
`CommonPrefixes/Prefix`.
|
|
1222
|
+
- ListObjectsV2: `Prefix`, `Delimiter`, `StartAfter`, `Key` and
|
|
1223
|
+
`CommonPrefixes/Prefix`; `ContinuationToken` and `NextContinuationToken`
|
|
1224
|
+
are opaque tokens, the same with or without `encoding-type`.
|
|
1225
|
+
- ListObjectVersions: `Prefix`, `KeyMarker`, `NextKeyMarker`, `Delimiter`,
|
|
1226
|
+
`Key` and `CommonPrefixes/Prefix`.
|
|
1227
|
+
- ListMultipartUploads: `Prefix` and `Key`.
|
|
1228
|
+
|
|
1229
|
+
Without the parameter keys are written as they are and no `EncodingType` is
|
|
1230
|
+
returned; any other value returns `InvalidArgument` (400). An object key may
|
|
1231
|
+
hold any UTF-8 character, also those XML 1.0 cannot carry (U+0001 and the other
|
|
1232
|
+
control characters except tab, line feed and carriage return): a listing that
|
|
1233
|
+
contains such a key is valid XML only with `encoding-type=url`. ListObjectsV2
|
|
1234
|
+
continuation tokens use only `A-Z a-z 0-9 - _`, so they never break the XML.
|
|
1235
|
+
|
|
1236
|
+
Accepted but not applied: owners are never returned (`fetch-owner`); responses
|
|
1237
|
+
say so by omitting `Owner`. Refused with `NotImplemented` although S3
|
|
1238
|
+
defines them: every other subresource (`?acl`, `?tagging`, `?retention`,
|
|
1239
|
+
`?legal-hold`, `?cors`, `?website`, `?object-lock`, `?attributes`, `?restore`,
|
|
1240
|
+
`?select`, โฆ), `partNumber` on GET and HEAD, the paging parameters of
|
|
1241
|
+
ListMultipartUploads (`key-marker`, `upload-id-marker`, `max-uploads`,
|
|
1242
|
+
`delimiter`), ListObjects V1 with V2 cursors, the ListBuckets parameters
|
|
1243
|
+
(`prefix`, `max-buckets`, `continuation-token`, `bucket-region`), headers
|
|
1244
|
+
hoisted into a presigned URL's query (`x-amz-checksum-crc32=โฆ`) on anything but
|
|
1245
|
+
an object read, and any `POST` that is not one of the operations above.
|
|
797
1246
|
|
|
798
1247
|
## Usage with AWS SDK v3
|
|
799
1248
|
|
|
@@ -999,7 +1448,23 @@ Client โโHTTP PUTโโโถ Node A (coordinator)
|
|
|
999
1448
|
2. **Objects are chunked** โ large objects split into fixed-size pieces (default 4 MB)
|
|
1000
1449
|
3. **Each chunk is erasure-coded** โ Reed-Solomon produces k data + m parity shards
|
|
1001
1450
|
4. **Shards are distributed** โ placed across different nodes and drives for fault isolation
|
|
1002
|
-
5. **Quorum guarantees consistency** โ writes need k+1 acks, reads need k shards
|
|
1451
|
+
5. **Quorum guarantees consistency** โ writes need k+1 acks, reads need k shards.
|
|
1452
|
+
A chunk with fewer than k+m written shards is published with the missing
|
|
1453
|
+
ones as pending shards that healing completes on their assigned drives.
|
|
1454
|
+
A node that membership marks offline is never contacted: its shards of a
|
|
1455
|
+
write are pending at once, reads take their shards from the other nodes,
|
|
1456
|
+
and below either quorum a request fails at once. Until a stopped node is
|
|
1457
|
+
marked offline (five missed heartbeat rounds), each chunk that needs it
|
|
1458
|
+
waits up to the QUIC idle timeout (10 s). Cluster QUIC connections send
|
|
1459
|
+
keep-alives every 2 s from both ends, so a slow but live peer stays
|
|
1460
|
+
connected however long a request takes; there is no whole-request
|
|
1461
|
+
deadline. A heartbeat to a silent peer therefore fails after 10 s. Each
|
|
1462
|
+
heartbeat round goes to every peer at once, so stopped nodes are marked
|
|
1463
|
+
offline after about 50 s by default, however many stop together (a whole
|
|
1464
|
+
location, for example). A node whose restart takes longer than that comes
|
|
1465
|
+
back to pending shards and healing work. `heartbeatTimeoutMs` (default 30 s) still bounds how long a live
|
|
1466
|
+
peer may take to answer a heartbeat, but cannot extend the wait for a
|
|
1467
|
+
silent one beyond 10 s.
|
|
1003
1468
|
|
|
1004
1469
|
### Cluster Root and Upgrade Requirements
|
|
1005
1470
|
|
|
@@ -1007,7 +1472,8 @@ Cluster mode never creates configured control or drive roots. Before startup,
|
|
|
1007
1472
|
create `storage.directory` and every `cluster.drives.paths` entry, make them
|
|
1008
1473
|
owned by the ObjectStorage process user, and remove group/world write permission.
|
|
1009
1474
|
Every configured root must be a real directory with no symlink path component,
|
|
1010
|
-
and every drive root must resolve to a distinct device/inode identity
|
|
1475
|
+
and every drive root must resolve to a distinct device/inode identity and,
|
|
1476
|
+
unless `cluster.drives.allowSharedDevice` is set, to a distinct device. The
|
|
1011
1477
|
control root may be distinct or may be the same identity as one drive root; it
|
|
1012
1478
|
cannot make two drive entries aliases of one another. Every root must support
|
|
1013
1479
|
the filesystem durability and locking semantics validated at startup.
|
|
@@ -1027,6 +1493,17 @@ has written or migrated the new state, do not restart an older binary and do not
|
|
|
1027
1493
|
run a mixed-version cluster. Back up the cluster roots before upgrading; rollback
|
|
1028
1494
|
requires restoring those roots from the pre-upgrade backup.
|
|
1029
1495
|
|
|
1496
|
+
Manifests with pending shards (written while a drive refused writes, see
|
|
1497
|
+
[Self-Healing](#self-healing)) are refused by engines that predate pending
|
|
1498
|
+
shards, because their chunks are short of placements. Before rolling a cluster
|
|
1499
|
+
back to such an engine, stop writing, complete or abort every open multipart
|
|
1500
|
+
upload (part descriptors record pending shards too, the scan does not count
|
|
1501
|
+
them, and an older engine drops them, so such an upload could not complete
|
|
1502
|
+
after the rollback), and then, on every node, run
|
|
1503
|
+
`runHealingScan()` until it reports `shardsPending` 0. Its scan starts after
|
|
1504
|
+
the request arrives, so it covers every write that finished before the call; a
|
|
1505
|
+
scan that could not list every manifest fails instead of reporting counts.
|
|
1506
|
+
|
|
1030
1507
|
### Erasure Coding
|
|
1031
1508
|
|
|
1032
1509
|
With the default `4+2` configuration:
|
|
@@ -1040,14 +1517,124 @@ With the default `4+2` configuration:
|
|
|
1040
1517
|
| 6+3 | 9 | 50% | 3 failures | 5 |
|
|
1041
1518
|
| 2+1 | 3 | 50% | 1 failure | 2 |
|
|
1042
1519
|
|
|
1520
|
+
### Streaming Reads
|
|
1521
|
+
|
|
1522
|
+
A cluster GET streams the object chunk by chunk; nothing is written to the
|
|
1523
|
+
control root. It reads from the generation of the manifest it loaded at its
|
|
1524
|
+
start, without holding the object's operation gate, so overwrites, deletes and
|
|
1525
|
+
other GETs of the same key run while it streams. Each chunk is rebuilt from the
|
|
1526
|
+
first k valid shards, read in parallel, and at most one rebuilt chunk waits
|
|
1527
|
+
ahead of the client.
|
|
1528
|
+
|
|
1529
|
+
- **Errors after the headers abort the response.** The first chunk is rebuilt
|
|
1530
|
+
before the status line, so an unreadable object still gets an error status.
|
|
1531
|
+
A failure in a later chunk ends the body with an error (the connection is
|
|
1532
|
+
closed), never with a clean end of body. A whole-object GET sends its last
|
|
1533
|
+
bytes only after the total length and the object MD5 match the manifest;
|
|
1534
|
+
range GETs are checked for length only.
|
|
1535
|
+
- **One deadline for the whole GET.** `cluster.getDeadlineMs` is the longest a
|
|
1536
|
+
single GET may run, counted from its start and including the client's
|
|
1537
|
+
transfer time; a GET that takes longer is aborted, and if its client stopped
|
|
1538
|
+
reading, its connection is closed. The default is 6 hours,
|
|
1539
|
+
the allowed range 1 minute to 7 days. The cleanup grace is derived from it:
|
|
1540
|
+
superseded and deleted generations keep their shards for the deadline plus
|
|
1541
|
+
the smaller of the deadline and one hour (7 hours by default, 2 minutes at
|
|
1542
|
+
the 1-minute floor), so every GET that started on a generation has ended
|
|
1543
|
+
before cleanup frees it. Writes in progress hold their object's gate and are
|
|
1544
|
+
never cleaned up, however short the grace. The setting must be the same on
|
|
1545
|
+
every node; a mismatch can abort streams, but never serves wrong bytes.
|
|
1546
|
+
CopyObject and CompleteMultipartUpload read their sources under the object
|
|
1547
|
+
gate with their own fixed 6-hour limit, counted from the start of the
|
|
1548
|
+
operation.
|
|
1549
|
+
- **GET memory budget.** Rebuilding chunks is charged to an engine-wide budget
|
|
1550
|
+
of `max(1 GiB, 16 ร chunk footprint)` at the configured chunk size. A chunk's
|
|
1551
|
+
footprint is โchunk/kโ ร (k + min(k, m)) + chunk: the k fetched shards, the
|
|
1552
|
+
data shards rebuilt for missing ones (a read rebuilds no parity shards) and
|
|
1553
|
+
the decoded chunk; 4+2 at 4 MiB is 10 MiB, 1+15 at 64 MiB is 192 MiB. A GET
|
|
1554
|
+
whose client went away keeps its charge until its local shard reads have
|
|
1555
|
+
ended, and shutdown waits for them before it releases the drive roots. The
|
|
1556
|
+
budget is separate from the upload budget. The first chunk waits at most 5 seconds for
|
|
1557
|
+
budget and is otherwise refused with `503 SlowDown` and `Retry-After: 1`;
|
|
1558
|
+
later chunks wait. Slow downloaders hold up to two rebuilt chunks each, so
|
|
1559
|
+
many of them can exhaust the budget and turn new GETs into `503`s under load.
|
|
1560
|
+
|
|
1043
1561
|
### QUIC Transport
|
|
1044
1562
|
|
|
1045
1563
|
Inter-node communication uses [QUIC](https://en.wikipedia.org/wiki/QUIC) via the `quinn` library:
|
|
1046
|
-
-
|
|
1564
|
+
- **Mutual TLS or unauthenticated** โ with `cluster.tls`, every connection is TLS 1.3 with a CA-signed certificate on both ends; without it, each node generates a self-signed certificate at startup and accepts any peer (see [Cluster transport trust](#cluster-transport-trust))
|
|
1047
1565
|
- ๐ **Multiplexed streams** โ concurrent shard transfers without head-of-line blocking
|
|
1048
1566
|
- โก **Connection pooling** โ persistent connections to peer nodes
|
|
1049
1567
|
- ๐ **Natural backpressure** โ QUIC flow control prevents overloading slow peers
|
|
1050
1568
|
|
|
1569
|
+
### Cluster transport trust
|
|
1570
|
+
|
|
1571
|
+
Without `cluster.tls` the cluster transport does not authenticate nodes. Each node generates a self-signed certificate at startup and accepts any peer's certificate without verifying it. QUIC encrypts the traffic against passive eavesdropping, but not against an attacker on the path. Any host that can reach a node's `quicPort` can join the cluster, sync topology, and read, write, tombstone and purge shards. S3 credentials and bucket policies do not apply to this port. Run such a cluster only on a network where every host that can reach `quicPort` (UDP, default 4000) is a cluster node: a private network or VPN between the nodes, with `quicPort` firewalled from everything else.
|
|
1572
|
+
|
|
1573
|
+
With `cluster.tls` set, the transport is mutual TLS (TLS 1.3, no session resumption, no early data) with a cluster CA you run:
|
|
1574
|
+
|
|
1575
|
+
- **Identity.** A node's identity is the single DNS subject alternative name (SAN) of its certificate, and it equals `cluster.nodeId`. With `cluster.tls`, `nodeId` is required and must be a lowercase DNS name such as `node-1` or `node-1.storage.internal`; it is never generated, and it must still match the node id persisted in the control root. One certificate serves as both the server and the client certificate, so it must be valid for both: an extended key usage extension listing `serverAuth` and `clientAuth`, or no extended key usage extension at all (the recipe below issues both). Use a dedicated CA per cluster: every certificate it signs with one lowercase DNS SAN is a member of the cluster.
|
|
1576
|
+
- **Checks.** A node connects to a known peer only if the peer's certificate names exactly that peer's node id (one DNS SAN, no wildcard), and to a seed (`seedNodes`, known only by its address) only if the seed presents a certificate from the cluster CA. A node accepts a connection only with a client certificate from the cluster CA, and a join or heartbeat only for the node id that certificate names. A peer without such a certificate fails the handshake before it can send any request. Every CA-signed node is a full member: it may sync topology and read, write, tombstone and purge any shard, so the way to remove a compromised node is to stop trusting its certificate (a new CA, see the rotation below).
|
|
1577
|
+
- **Startup.** A node checks its own certificate the way its peers will (chain, validity, server and client authentication, the SAN, and that `keyFile` matches `certFile`) and refuses to start if that check or any file check below fails. The error names the file and the reason. A node never falls back to the unauthenticated transport. At startup it logs one line with its certificate's `notAfter` and the SHA-256 of each CA certificate, and `getClusterHealth().transport` reports them (see [Cluster Health](#cluster-health)).
|
|
1578
|
+
- **Files.** `caFile` holds one CA certificate, or two during a CA rotation; anything else is refused, which keeps a system CA bundle out. `certFile` holds the node certificate, optionally followed by intermediates, and nothing but certificates. `keyFile` holds exactly one unencrypted PKCS#8, SEC1 or PKCS#1 key; ECDSA P-256 is recommended. Each path must be absolute and canonical, with no symbolic link anywhere, and lie outside the control root and every drive root (roots are backed up and replaced, and a key must not travel with them). That check compares paths, so a bind mount that makes a root reachable under another path is not detected. Each file must be a regular file of at most 64 KiB. The files and their directories must be owned by the engine's user or by root and must not be group- or world-writable, and `keyFile` must be owned by the engine's user and grant no group or world permission (mode 0600 or 0400).
|
|
1579
|
+
- **Reload.** The files are read once at startup; a restart is the reload. Certificates are checked at every handshake, so a connection established before a certificate's `notAfter` keeps running until it reconnects, and new handshakes after `notAfter` fail. Renew certificates and restart the nodes before they expire.
|
|
1580
|
+
|
|
1581
|
+
#### Making the CA and the node certificates
|
|
1582
|
+
|
|
1583
|
+
The engine ships no CA tooling. With OpenSSL 3 or LibreSSL 3.3 (the `openssl` of macOS), create the CA once and keep `ca.key` offline. `ec_param_enc:named_curve` is required for LibreSSL, which otherwise writes the curve as explicit parameters that the engine refuses; OpenSSL 3 writes the named curve either way:
|
|
1584
|
+
|
|
1585
|
+
```bash
|
|
1586
|
+
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \
|
|
1587
|
+
-pkeyopt ec_param_enc:named_curve -out ca.key
|
|
1588
|
+
openssl req -x509 -new -key ca.key -sha256 -days 3650 \
|
|
1589
|
+
-subj "/CN=objectstorage cluster CA" \
|
|
1590
|
+
-addext "basicConstraints=critical,CA:TRUE" \
|
|
1591
|
+
-addext "keyUsage=critical,keyCertSign,cRLSign" \
|
|
1592
|
+
-out ca.pem
|
|
1593
|
+
```
|
|
1594
|
+
|
|
1595
|
+
Then issue one certificate per node, with the node id as its only DNS SAN:
|
|
1596
|
+
|
|
1597
|
+
```bash
|
|
1598
|
+
NODE_ID=node-1
|
|
1599
|
+
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \
|
|
1600
|
+
-pkeyopt ec_param_enc:named_curve -out "$NODE_ID.key"
|
|
1601
|
+
openssl req -new -key "$NODE_ID.key" -subj "/CN=$NODE_ID" -out "$NODE_ID.csr"
|
|
1602
|
+
cat > "$NODE_ID.ext" <<EXT
|
|
1603
|
+
basicConstraints = critical,CA:FALSE
|
|
1604
|
+
keyUsage = critical,digitalSignature
|
|
1605
|
+
extendedKeyUsage = serverAuth,clientAuth
|
|
1606
|
+
subjectAltName = DNS:$NODE_ID
|
|
1607
|
+
EXT
|
|
1608
|
+
openssl x509 -req -in "$NODE_ID.csr" -CA ca.pem -CAkey ca.key -CAcreateserial \
|
|
1609
|
+
-days 365 -sha256 -extfile "$NODE_ID.ext" -out "$NODE_ID.pem"
|
|
1610
|
+
```
|
|
1611
|
+
|
|
1612
|
+
Install `ca.pem` and the node's certificate and key on the node, as the user that runs the engine and outside its control and drive roots:
|
|
1613
|
+
|
|
1614
|
+
```bash
|
|
1615
|
+
install -d -m 0755 /etc/objectstorage/tls
|
|
1616
|
+
install -m 0644 ca.pem "$NODE_ID.pem" /etc/objectstorage/tls/
|
|
1617
|
+
install -m 0600 "$NODE_ID.key" /etc/objectstorage/tls/
|
|
1618
|
+
```
|
|
1619
|
+
|
|
1620
|
+
Then set `cluster.tls` to the three paths (see [Configuration](#configuration)). The test suite runs exactly these commands (`test/test.cluster-tls.node.ts`).
|
|
1621
|
+
|
|
1622
|
+
**Docker secrets** are mounted owned by root with mode 0444 by default, which the key check refuses. Set the key secret's `uid` to the engine's user and its `mode` to `0400` in the service's long `secrets` syntax. Docker Compose ignores `uid`, `gid` and `mode` for secrets read from a file, because it bind-mounts the file; there the host file itself must be owned by the engine's uid with mode 0600 or 0400. Kubernetes secret volumes present their files through symbolic links, which the canonical-path rule refuses.
|
|
1623
|
+
|
|
1624
|
+
#### Enabling mutual TLS
|
|
1625
|
+
|
|
1626
|
+
Nodes with and without `cluster.tls` cannot talk to each other: a TLS node refuses peers without a client certificate and refuses self-signed servers. Enabling TLS is therefore not a rolling change: stop every node, configure `cluster.tls` on every node, then start every node. A node whose `nodeId` was generated keeps that id (the control root persists it, and `getClusterHealth().nodeId` reports it): configure it as `nodeId` and issue the node's certificate for it, or the node refuses to start. Disabling TLS takes the same full stop.
|
|
1627
|
+
|
|
1628
|
+
#### Rotating certificates and the CA
|
|
1629
|
+
|
|
1630
|
+
A node certificate is renewed by issuing a new one from the same CA and restarting the node; its peers need no change. A new CA takes three rolling restarts, with no cluster stop:
|
|
1631
|
+
|
|
1632
|
+
1. Put both CAs into every node's `caFile` (two certificates) and restart the nodes one at a time.
|
|
1633
|
+
2. Issue every node a certificate from the new CA and restart the nodes one at a time.
|
|
1634
|
+
3. Remove the old CA from every `caFile` and restart the nodes one at a time.
|
|
1635
|
+
|
|
1636
|
+
Every pair of nodes stays connected throughout, because trust only grows before the certificates change and only shrinks after. The order matters: a node with a certificate from the new CA cannot connect to a node that trusts only the old one.
|
|
1637
|
+
|
|
1051
1638
|
### Cluster Membership
|
|
1052
1639
|
|
|
1053
1640
|
- **Static seed nodes** โ initial cluster defined in config
|
|
@@ -1057,13 +1644,40 @@ Inter-node communication uses [QUIC](https://en.wikipedia.org/wiki/QUIC) via the
|
|
|
1057
1644
|
|
|
1058
1645
|
### Self-Healing
|
|
1059
1646
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1647
|
+
Every 24 hours, and whenever `runHealingScan()` asks for it, each node scans
|
|
1648
|
+
the manifests it coordinated (manifests live on the node that wrote them):
|
|
1649
|
+
1. Every shard placement is checked at the drive it names, locally or through
|
|
1650
|
+
its node, against the checksum and size the manifest recorded. The check
|
|
1651
|
+
reads the shard record's header and the record's length only, never its
|
|
1652
|
+
payload, so a scan costs a few small reads per shard. Verifying payloads
|
|
1653
|
+
is not the scan's job: a read detects a rotted payload through its CRC32C
|
|
1654
|
+
and rebuilds the chunk from the other shards.
|
|
1655
|
+
2. A missing shard is rebuilt from k valid shards with Reed-Solomon, checked
|
|
1656
|
+
against the recorded checksum, and written to the drive its placement
|
|
1657
|
+
names. The manifest does not change.
|
|
1658
|
+
3. A pending shard is written to the drive its manifest recorded and then
|
|
1659
|
+
moved into the chunk's placements, in the same version and generation; the
|
|
1660
|
+
completed manifest is identical to one whose write never failed.
|
|
1661
|
+
|
|
1662
|
+
Healing never places a shard on a drive of its own choosing. Placements and
|
|
1663
|
+
pending shards on an offline node wait for that node; a pending shard whose
|
|
1664
|
+
drive refuses writes stays pending and is reported in `repairs.shardsPending`
|
|
1665
|
+
(replacing a permanently lost drive is not supported yet). A shard record that
|
|
1666
|
+
cannot be read, such as a truncated one, is reported in `repairs.failed` and
|
|
1667
|
+
left in place, because shard records are immutable; a node that cannot read a
|
|
1668
|
+
record answers with an error, never as if the shard were missing. A minority
|
|
1669
|
+
partition does not heal: its scan fails. Placements are checked without the
|
|
1670
|
+
object's operation gate, as a GET reads them. The chunks of an object that need
|
|
1671
|
+
a write are healed in batches of up to 1 GiB of chunk data: each batch takes
|
|
1672
|
+
the object's gate, reloads the manifest, heals only chunks that are unchanged
|
|
1673
|
+
in the same generation, publishes its pending shards with one manifest store
|
|
1674
|
+
and releases the gate before the next batch. An object whose gate is busy is
|
|
1675
|
+
skipped until the next scan.
|
|
1676
|
+
|
|
1677
|
+
Scans never overlap. A scan that outlasts the 24-hour interval delays the next
|
|
1678
|
+
one. `runHealingScan()` joins the next scan that starts after its request and
|
|
1679
|
+
never a scan that is already running; requests that arrive together share one
|
|
1680
|
+
scan.
|
|
1067
1681
|
|
|
1068
1682
|
### Erasure Set Formation
|
|
1069
1683
|
|
|
@@ -1083,7 +1697,7 @@ Drives are interleaved across nodes for maximum fault isolation. New nodes form
|
|
|
1083
1697
|
|
|
1084
1698
|
```typescript
|
|
1085
1699
|
import { ObjectStorage } from '@lossless.org/objectstorage';
|
|
1086
|
-
import { tap, expect } from '@git.zone/
|
|
1700
|
+
import { tap, expect } from '@git.zone/tsrepo/tapbundle';
|
|
1087
1701
|
|
|
1088
1702
|
let storage: ObjectStorage;
|
|
1089
1703
|
|
|
@@ -1266,6 +1880,24 @@ List scoped tenant credential metadata without returning secrets.
|
|
|
1266
1880
|
|
|
1267
1881
|
Return endpoint, port, region, bucket, access key, secret key, SSL flag, legacy descriptor fields, and S3/AWS env values for the bucket tenant.
|
|
1268
1882
|
|
|
1883
|
+
#### `createPrefixCredential(input: IPrefixCredentialInput): Promise<IPrefixCredential>`
|
|
1884
|
+
|
|
1885
|
+
Create an expiring credential limited to one prefix of one bucket, or install
|
|
1886
|
+
one created on another node (`accessKeyId`, `secretAccessKey`, `expiresAt`). An
|
|
1887
|
+
identical replay returns the stored record. The secret is returned only here.
|
|
1888
|
+
See [Prefix Credentials](#prefix-credentials) for the input rules and the
|
|
1889
|
+
`ObjectStorageCredentialError` codes.
|
|
1890
|
+
|
|
1891
|
+
#### `revokePrefixCredential(options): Promise<{ revoked: boolean }>`
|
|
1892
|
+
|
|
1893
|
+
Revoke a prefix credential and keep its ID refused until the latest moment it
|
|
1894
|
+
could still be created. Options: `{ accessKeyId }`. `revoked` is `true` only
|
|
1895
|
+
when a live credential was removed; unknown IDs are recorded as revoked too.
|
|
1896
|
+
|
|
1897
|
+
#### `listPrefixCredentials(options?): Promise<IPrefixCredentialMetadata[]>`
|
|
1898
|
+
|
|
1899
|
+
List live prefix credentials without secrets. Options: `{ bucketName? }`.
|
|
1900
|
+
|
|
1269
1901
|
#### `getBucketMigrationCapability(): Promise<IObjectStorageBucketMigrationCapability>`
|
|
1270
1902
|
|
|
1271
1903
|
Return standalone provider identity and receipt-key evidence plus source seal,
|
|
@@ -1324,7 +1956,9 @@ the bucket's exclusive credential. Options:
|
|
|
1324
1956
|
|
|
1325
1957
|
Delete a fenced bucket. Supplying `accessKeyId` also durably revokes that
|
|
1326
1958
|
exact tenant credential and deletes the policy; omitting it preserves the
|
|
1327
|
-
legacy credential/policy behavior.
|
|
1959
|
+
legacy credential/policy behavior. The bucket's prefix credentials are revoked
|
|
1960
|
+
first in both cases, and the deletion is refused if that revocation cannot be
|
|
1961
|
+
stored. Options:
|
|
1328
1962
|
`{ bucketName, accessKeyId?, fence, holdPublication? }`. Setting
|
|
1329
1963
|
`holdPublication: true` returns a durable `resourcePublicationBarrier`; omitted
|
|
1330
1964
|
or `false` preserves immediate behavior.
|
|
@@ -1382,7 +2016,7 @@ Atomically replace the active runtime credential set without restarting the serv
|
|
|
1382
2016
|
|
|
1383
2017
|
#### `getClusterHealth(): Promise<IClusterHealth>`
|
|
1384
2018
|
|
|
1385
|
-
Read the Rust core's current cluster, drive, quorum, and
|
|
2019
|
+
Read the Rust core's current cluster, drive, quorum, repair, and transport health snapshot. Standalone mode returns `{ enabled: false }`.
|
|
1386
2020
|
|
|
1387
2021
|
In cluster mode, `quicAddress` is the local QUIC/UDP socket's actual bound
|
|
1388
2022
|
address. With `cluster.quicPort: 0`, the operating system assigns its port; use
|
|
@@ -1390,6 +2024,35 @@ address. With `cluster.quicPort: 0`, the operating system assigns its port; use
|
|
|
1390
2024
|
Membership advertisements and peer `quicAddress` values contain that same bound
|
|
1391
2025
|
address, including after a restart.
|
|
1392
2026
|
|
|
2027
|
+
#### `getClusterSnapshot(): Promise<IObjectStorageClusterSnapshot>`
|
|
2028
|
+
|
|
2029
|
+
The [dashboard](#dashboard)'s snapshot, built by the engine from memory only:
|
|
2030
|
+
`local` is this node's `IObjectStorageNodeSnapshot` (counters, totals, drives
|
|
2031
|
+
from the last heartbeat probe, healing state, recent events) and, in cluster
|
|
2032
|
+
mode, `cluster` adds quorum, the erasure layout and every peer, with the peer's
|
|
2033
|
+
own snapshot when it answered within a second. One snapshot runs at a time; a
|
|
2034
|
+
call while another runs rejects. Standalone mode returns `cluster: null`.
|
|
2035
|
+
|
|
2036
|
+
#### `dashboardPort: number | undefined`
|
|
2037
|
+
|
|
2038
|
+
The dashboard's bound TCP port while it runs; `undefined` while it is off or
|
|
2039
|
+
stopped.
|
|
2040
|
+
|
|
2041
|
+
#### `runHealingScan(options?: IRunHealingScanOptions): Promise<IClusterHealingScanResult>`
|
|
2042
|
+
|
|
2043
|
+
Run a healing scan on this cluster node (see [Self-Healing](#self-healing)) and
|
|
2044
|
+
return its `startedAt`, `completedAt`, `durationMs`, `shardsChecked`,
|
|
2045
|
+
`shardsHealed`, `shardsPending` and `failed`. The request joins the next scan
|
|
2046
|
+
that starts after it arrives; a scan already running finishes first. It
|
|
2047
|
+
rejects in standalone mode, when the scan fails (without a majority of the
|
|
2048
|
+
cluster, or when a bucket's manifests cannot be listed) and when the engine
|
|
2049
|
+
shuts down before the scan completes, and it is refused while 16 requests
|
|
2050
|
+
already wait for a scan to start. The engine answers other management calls
|
|
2051
|
+
while the scan runs. `options.timeoutMs` (default 300,000) bounds the wait;
|
|
2052
|
+
the engine cannot cancel a scan, so one that outlasts it keeps running, its
|
|
2053
|
+
result is discarded, and a later call joins the next scan that starts after
|
|
2054
|
+
it.
|
|
2055
|
+
|
|
1393
2056
|
#### `getHealth(): Promise<IObjectStorageHealth>`
|
|
1394
2057
|
|
|
1395
2058
|
Return running state, storage directory, per-location pool observations, auth
|
|
@@ -1416,7 +2079,7 @@ objectstorage uses a **hybrid Rust + TypeScript** architecture:
|
|
|
1416
2079
|
โ โ โโ FileStore (single-node mode) โ
|
|
1417
2080
|
โ โ โโ DistributedStore (cluster mode) โ
|
|
1418
2081
|
โ โ โโ ErasureCoder (Reed-Solomon) โ
|
|
1419
|
-
โ โ โโ
|
|
2082
|
+
โ โ โโ ShardBackend (per-drive shards) โ
|
|
1420
2083
|
โ โ โโ QuicTransport (quinn) โ
|
|
1421
2084
|
โ โ โโ ClusterState & Membership โ
|
|
1422
2085
|
โ โ โโ HealingService โ
|
|
@@ -1433,7 +2096,7 @@ objectstorage uses a **hybrid Rust + TypeScript** architecture:
|
|
|
1433
2096
|
|
|
1434
2097
|
**Why Rust?** The original TypeScript implementation had critical perf issues: OOM on multipart uploads (parts buffered in memory), double stream copying, file descriptor leaks on HEAD requests, full-file reads for range requests, and no backpressure. The Rust binary solves these with bounded-memory streaming I/O, backpressure, and direct `seek()` for range requests.
|
|
1435
2098
|
|
|
1436
|
-
**IPC Protocol:** TypeScript communicates with the `ruststorage` binary over newline-delimited JSON via stdin/stdout. The current management commands are `probeStoragePool`, `findOrphanObjectLeaves`, `getStoragePoolDiagnostic`, `start`, `stop`, `createBucket`, `createBucketTenant`, `ensureBucketTenant`, `deleteBucketTenant`, `rotateBucketTenantCredentials`, `listBucketTenants`, `getBucketTenantCredential`, `getBucketRetentionCapability`, `getBucketRetentionReceipt`, `getBucketMigrationCapability`, `createSourceMigration`, `createDestinationMigration`, `transitionSourceMigration`, `transitionDestinationMigration`, `cleanupSourceMigration`, `inspectBucketMigration`, `exportBucket`, `importBucket`, `deleteBucket`, `commitResourcePublication`, `getResourceFencingCapability`, `getBucketResourceFenceState`, `getStorageStats`, `listBucketSummaries`, `listCredentials`, `replaceCredentials`, and `
|
|
2099
|
+
**IPC Protocol:** TypeScript communicates with the `ruststorage` binary over newline-delimited JSON via stdin/stdout. The current management commands are `probeStoragePool`, `findOrphanObjectLeaves`, `getStoragePoolDiagnostic`, `start`, `stop`, `createBucket`, `createBucketTenant`, `ensureBucketTenant`, `deleteBucketTenant`, `rotateBucketTenantCredentials`, `listBucketTenants`, `getBucketTenantCredential`, `getBucketRetentionCapability`, `getBucketRetentionReceipt`, `getBucketMigrationCapability`, `createSourceMigration`, `createDestinationMigration`, `transitionSourceMigration`, `transitionDestinationMigration`, `cleanupSourceMigration`, `inspectBucketMigration`, `exportBucket`, `importBucket`, `deleteBucket`, `commitResourcePublication`, `getResourceFencingCapability`, `getBucketResourceFenceState`, `getStorageStats`, `listBucketSummaries`, `listCredentials`, `replaceCredentials`, `createPrefixCredential`, `revokePrefixCredential`, `listPrefixCredentials`, `getPrefixCredentialStatus`, `getClusterHealth`, `getClusterSnapshot`, and `runHealingScan`. `getStoragePoolDiagnostic` reads a weak, cached diagnostic source and remains responsive while another management command owns the server; it does not retain the mounted root after server ownership completes. `runHealingScan` answers when its scan completes, with the request's `id`; the engine serves other commands meanwhile, and a shutdown answers a pending request with an error before the `stop` response. `getClusterSnapshot` is answered the same deferred way, because a cluster node waits up to a second for each peer.
|
|
1437
2100
|
|
|
1438
2101
|
### S3-Compatible Operations
|
|
1439
2102
|
|
|
@@ -1483,7 +2146,8 @@ those versions cannot read the canonical v2 objects.
|
|
|
1483
2146
|
| ListParts | `GET /{bucket}/{key}?uploadId` | Optional `max-parts` and `part-number-marker` |
|
|
1484
2147
|
| CompleteMultipartUpload | `POST /{bucket}/{key}?uploadId` | |
|
|
1485
2148
|
| AbortMultipartUpload | `DELETE /{bucket}/{key}?uploadId` | |
|
|
1486
|
-
| ListMultipartUploads | `GET /{bucket}?uploads` | |
|
|
2149
|
+
| ListMultipartUploads | `GET /{bucket}?uploads` | Optional `prefix` |
|
|
2150
|
+
| GetBucketLocation | `GET /{bucket}?location` | Returns `server.region`; an empty constraint for `us-east-1` |
|
|
1487
2151
|
| GetBucketPolicy | `GET /{bucket}?policy` | |
|
|
1488
2152
|
| PutBucketPolicy | `PUT /{bucket}?policy` | |
|
|
1489
2153
|
| DeleteBucketPolicy | `DELETE /{bucket}?policy` | |
|
|
@@ -1507,6 +2171,14 @@ response echoes `ContinuationToken` and `StartAfter` when the request carried
|
|
|
1507
2171
|
them, reports `KeyCount` as contents plus common prefixes, and returns
|
|
1508
2172
|
`NextContinuationToken` whenever the page is truncated.
|
|
1509
2173
|
|
|
2174
|
+
A continuation token is opaque: pass `NextContinuationToken` back unchanged.
|
|
2175
|
+
It is the unpadded base64url (RFC 4648 ยง5) encoding of the UTF-8 key the next
|
|
2176
|
+
page resumes after, at most 1,366 characters, and is the same with or without
|
|
2177
|
+
`encoding-type=url`, so a client may change `encoding-type` between pages. A
|
|
2178
|
+
token the server could not have issued (outside the base64url alphabet, padded,
|
|
2179
|
+
not canonical, or not decoding to a 1 to 1,024-byte UTF-8 key) returns
|
|
2180
|
+
`InvalidArgument` (400); so do tokens issued before 11.0.0, which were raw keys.
|
|
2181
|
+
|
|
1510
2182
|
**ListObjects v1** (no `list-type`) reads `prefix`, `delimiter`, `max-keys` and
|
|
1511
2183
|
`marker`, and ignores the V2 cursors. The response always carries `Marker`,
|
|
1512
2184
|
empty when the request carried none, and carries `NextMarker` whenever the page
|
|
@@ -1523,14 +2195,16 @@ Paging rules shared by both versions:
|
|
|
1523
2195
|
cursor.
|
|
1524
2196
|
- `max-keys` above 1000 is clamped to 1000 and the response echoes the clamped
|
|
1525
2197
|
value in `MaxKeys`, as S3 does. A `max-keys` value that is not a non-negative
|
|
1526
|
-
integer is rejected with `InvalidRequest`, and so is a
|
|
1527
|
-
1,024-byte S3 key bound.
|
|
2198
|
+
integer is rejected with `InvalidRequest`, and so is a `start-after` or
|
|
2199
|
+
`marker` longer than the 1,024-byte S3 key bound.
|
|
1528
2200
|
- An empty `start-after`, `continuation-token` or `marker` value means no cursor.
|
|
1529
2201
|
- Keys are returned in ascending UTF-8 byte order, and a key is never returned
|
|
1530
2202
|
twice across pages of an unchanged bucket.
|
|
1531
2203
|
|
|
1532
2204
|
ListMultipartUploads is not paginated: it returns every in-flight upload of the
|
|
1533
|
-
bucket with `IsTruncated` false.
|
|
2205
|
+
bucket with `IsTruncated` false. An optional `prefix` parameter limits the
|
|
2206
|
+
result to uploads whose keys start with it and is echoed in the response;
|
|
2207
|
+
prefix credentials must supply one inside their prefix.
|
|
1534
2208
|
|
|
1535
2209
|
### On-Disk Format
|
|
1536
2210
|
|
|
@@ -1552,6 +2226,8 @@ bucket with `IsTruncated` false.
|
|
|
1552
2226
|
{bucket}.policy.json # Bucket policy (IAM JSON format)
|
|
1553
2227
|
.smartstorage/staging/v1/ # Private staging: a write lands here first; its
|
|
1554
2228
|
# key leaf appears only with the complete payload
|
|
2229
|
+
.smartstorage/credentials.json # Runtime and bucket tenant credentials (0600)
|
|
2230
|
+
.smartstorage/prefix-credentials.json # Prefix credentials and revocations (0600)
|
|
1555
2231
|
```
|
|
1556
2232
|
|
|
1557
2233
|
Rolling a standalone root back from 10.7.0 or later to 8.0.3โ10.6.0: a crash
|
|
@@ -1575,7 +2251,8 @@ were never published.
|
|
|
1575
2251
|
{object-address}.state.json
|
|
1576
2252
|
# Active/tombstoned object manifest
|
|
1577
2253
|
.multipart/{upload-id}/ # Descriptor-anchored multipart state
|
|
1578
|
-
|
|
2254
|
+
.smartstorage/credentials.json # This node's runtime credentials (not replicated)
|
|
2255
|
+
.smartstorage/prefix-credentials.json # This node's prefix credentials (not replicated)
|
|
1579
2256
|
.smartstorage/cluster/
|
|
1580
2257
|
identity.json # Node and cluster identity
|
|
1581
2258
|
topology.json # Last durable topology
|
|
@@ -1588,8 +2265,12 @@ were never published.
|
|
|
1588
2265
|
```
|
|
1589
2266
|
|
|
1590
2267
|
Superseded physical generations are queued with their exact shard placements.
|
|
1591
|
-
|
|
1592
|
-
|
|
2268
|
+
They keep their shards for the cleanup grace derived from `cluster.getDeadlineMs`
|
|
2269
|
+
(see Streaming Reads), and the cleanup worker, which runs once per grace period
|
|
2270
|
+
and at least hourly, then purges both shard records and tombstones. Earlier
|
|
2271
|
+
engines spooled GET responses into `.manifests/.get-tmp/`; startup removes a
|
|
2272
|
+
leftover spool directory once it holds only spool files, and otherwise leaves
|
|
2273
|
+
it in place, unused. Multipart
|
|
1593
2274
|
uploads older than `multipart.expirationDays`, plus committed uploads whose
|
|
1594
2275
|
first cleanup attempt failed, are retried during bounded cleanup passes
|
|
1595
2276
|
scheduled every `multipart.cleanupIntervalMinutes`; large registries are
|
|
@@ -1612,7 +2293,7 @@ established.
|
|
|
1612
2293
|
|
|
1613
2294
|
- [`@push.rocks/smartbucket`](https://code.foss.global/push.rocks/smartbucket) โ High-level S3-compatible abstraction layer
|
|
1614
2295
|
- [`@push.rocks/smartrust`](https://code.foss.global/push.rocks/smartrust) โ TypeScript โ Rust IPC bridge
|
|
1615
|
-
- [`@git.zone/
|
|
2296
|
+
- [`@git.zone/rsrepo`](https://code.foss.global/git.zone/rsrepo) โ Rust builds and cross-compilation for npm packages
|
|
1616
2297
|
|
|
1617
2298
|
## License and Legal Information
|
|
1618
2299
|
|