disk 0.8.11 → 0.8.13
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/README.md +74 -0
- package/dist/cli.js +415 -22
- package/dist/index.cjs +382 -20
- package/dist/index.d.cts +435 -4
- package/dist/index.d.ts +435 -4
- package/dist/index.js +378 -19
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -82,6 +82,80 @@ await archil.createApiKey({ name: "ci-bot", description: "GitHub Actions" });
|
|
|
82
82
|
await archil.deleteApiKey("key-abc123");
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
### Reading and writing objects
|
|
86
|
+
|
|
87
|
+
A `Disk` doubles as an S3-compatible bucket: read, write, delete, and list its
|
|
88
|
+
files by key without mounting it. These methods talk to Archil's S3 endpoint
|
|
89
|
+
using your same API key (no separate S3 credentials or SigV4 signing on your
|
|
90
|
+
part).
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const d = await archil.getDisk("dsk-abc123");
|
|
94
|
+
|
|
95
|
+
// Write — accepts a string, Uint8Array/Buffer, or ArrayBuffer. Returns the etag.
|
|
96
|
+
const { etag } = await d.putObject("reports/2026-01/data.json", JSON.stringify(report), "application/json");
|
|
97
|
+
|
|
98
|
+
// Read — returns the bytes (a Uint8Array).
|
|
99
|
+
const bytes = await d.getObject("reports/2026-01/data.json");
|
|
100
|
+
const text = new TextDecoder().decode(bytes);
|
|
101
|
+
|
|
102
|
+
// Metadata / existence without downloading the body
|
|
103
|
+
const meta = await d.headObject("reports/2026-01/data.json"); // null if absent
|
|
104
|
+
if (await d.objectExists("reports/2026-01/data.json")) { /* … */ }
|
|
105
|
+
|
|
106
|
+
// Delete (idempotent — deleting a missing key succeeds)
|
|
107
|
+
await d.deleteObject("reports/2026-01/data.json");
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`listObjects` auto-paginates by default, returning every matching key. The first
|
|
111
|
+
argument is a key prefix; a non-recursive listing (the default) returns the
|
|
112
|
+
immediate level as `objects` plus subdirectory `commonPrefixes`:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const { objects, commonPrefixes } = await d.listObjects("reports/"); // one level
|
|
116
|
+
const all = await d.listObjects("reports/", { recursive: true }); // whole subtree
|
|
117
|
+
const first100 = await d.listObjects("reports/", { limit: 100 }); // cap the total
|
|
118
|
+
|
|
119
|
+
// Stream pages instead of buffering everything (large listings):
|
|
120
|
+
for await (const page of d.listObjectsPages("reports/")) {
|
|
121
|
+
for (const obj of page.objects) console.log(obj.key, obj.size, obj.lastModified);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Or drive pagination yourself:
|
|
125
|
+
const page = await d.listObjects("reports/", { singlePage: true });
|
|
126
|
+
if (page.isTruncated) {
|
|
127
|
+
const next = await d.listObjects("reports/", { singlePage: true, continuationToken: page.nextContinuationToken });
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Failures throw `ArchilS3Error` with `status` (HTTP status), `code` (the S3 error
|
|
132
|
+
code, e.g. `"NoSuchKey"`), `requestId`, and the raw body on `raw`. `getObject`
|
|
133
|
+
on a missing key throws a 404 — use `headObject`/`objectExists` to probe without
|
|
134
|
+
catching. All SDK errors extend `ArchilError`, so `catch (e) { if (e instanceof
|
|
135
|
+
ArchilError) … }` handles control-plane and S3 failures uniformly.
|
|
136
|
+
|
|
137
|
+
The S3 endpoint is derived from your region automatically. To target a custom
|
|
138
|
+
environment, set `s3BaseUrl` on the `Archil` constructor (or the
|
|
139
|
+
`ARCHIL_S3_BASE_URL` env var).
|
|
140
|
+
|
|
141
|
+
### Sharing files
|
|
142
|
+
|
|
143
|
+
`share` mints a signed, time-limited link to a single file. Anyone with the link
|
|
144
|
+
can download that file — no API key, no mounting. The link carries a
|
|
145
|
+
cryptographically signed token (disk + key + expiry); when it expires it stops
|
|
146
|
+
working.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const d = await archil.getDisk("dsk-abc123");
|
|
150
|
+
|
|
151
|
+
// Default lifetime is 24 hours.
|
|
152
|
+
const { url, expiresIn } = await d.share("reports/2026-01/summary.pdf");
|
|
153
|
+
console.log(url); // https://control.…/api/shared/<token>
|
|
154
|
+
|
|
155
|
+
// Set the lifetime in seconds (any positive integer, up to 604800 = 7 days):
|
|
156
|
+
const weekLink = await d.share("reports/2026-01/summary.pdf", { expiresIn: 604800 });
|
|
157
|
+
```
|
|
158
|
+
|
|
85
159
|
### Named imports
|
|
86
160
|
|
|
87
161
|
If you prefer named imports over the namespace style, they work the same way:
|
package/dist/cli.js
CHANGED
|
@@ -1,23 +1,83 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// bin/cli.ts
|
|
4
|
-
import { createRequire
|
|
4
|
+
import { createRequire } from "module";
|
|
5
5
|
import { Command, Option } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/client.ts
|
|
8
|
-
import
|
|
8
|
+
import createClientDefault from "openapi-fetch";
|
|
9
|
+
|
|
10
|
+
// src/s3xml.ts
|
|
11
|
+
import { XMLParser } from "fast-xml-parser";
|
|
12
|
+
var parser = new XMLParser({
|
|
13
|
+
isArray: (name) => name === "Contents" || name === "CommonPrefixes",
|
|
14
|
+
parseTagValue: false,
|
|
15
|
+
trimValues: true,
|
|
16
|
+
ignoreAttributes: true
|
|
17
|
+
});
|
|
18
|
+
function parseXml(xml) {
|
|
19
|
+
if (!xml.trim()) return {};
|
|
20
|
+
return parser.parse(xml);
|
|
21
|
+
}
|
|
9
22
|
|
|
10
23
|
// src/errors.ts
|
|
11
|
-
var
|
|
24
|
+
var ArchilError = class extends Error {
|
|
25
|
+
/** HTTP status code associated with the failure. */
|
|
12
26
|
status;
|
|
27
|
+
/** Machine-readable error code (e.g. an S3 code like "NoSuchKey"), if known. */
|
|
13
28
|
code;
|
|
14
29
|
constructor(message, status, code) {
|
|
15
30
|
super(message);
|
|
16
|
-
this.name = "
|
|
31
|
+
this.name = "ArchilError";
|
|
17
32
|
this.status = status;
|
|
18
33
|
this.code = code;
|
|
19
34
|
}
|
|
20
35
|
};
|
|
36
|
+
var ArchilApiError = class extends ArchilError {
|
|
37
|
+
constructor(message, status, code) {
|
|
38
|
+
super(message, status, code);
|
|
39
|
+
this.name = "ArchilApiError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var ArchilS3Error = class extends ArchilError {
|
|
43
|
+
/** S3 request id, if the gateway returned one. */
|
|
44
|
+
requestId;
|
|
45
|
+
/** Raw response body (the XML document), for debugging. */
|
|
46
|
+
raw;
|
|
47
|
+
constructor(opts) {
|
|
48
|
+
const detail = opts.message ?? opts.statusText ?? "";
|
|
49
|
+
const codePart = opts.code ? ` ${opts.code}` : "";
|
|
50
|
+
super(
|
|
51
|
+
`S3 ${opts.operation} failed: ${opts.statusCode}${codePart}${detail ? ` \u2014 ${detail}` : ""}`,
|
|
52
|
+
opts.statusCode,
|
|
53
|
+
opts.code
|
|
54
|
+
);
|
|
55
|
+
this.name = "ArchilS3Error";
|
|
56
|
+
this.requestId = opts.requestId;
|
|
57
|
+
this.raw = opts.raw;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
function tagString(obj, tag) {
|
|
61
|
+
const value = obj[tag];
|
|
62
|
+
return value === void 0 || value === null ? void 0 : String(value);
|
|
63
|
+
}
|
|
64
|
+
function parseS3Error(operation, statusCode, statusText, body) {
|
|
65
|
+
let err = {};
|
|
66
|
+
try {
|
|
67
|
+
err = parseXml(body).Error ?? {};
|
|
68
|
+
} catch {
|
|
69
|
+
err = {};
|
|
70
|
+
}
|
|
71
|
+
return new ArchilS3Error({
|
|
72
|
+
operation,
|
|
73
|
+
statusCode,
|
|
74
|
+
statusText,
|
|
75
|
+
code: tagString(err, "Code"),
|
|
76
|
+
message: tagString(err, "Message"),
|
|
77
|
+
requestId: tagString(err, "RequestId"),
|
|
78
|
+
raw: body
|
|
79
|
+
});
|
|
80
|
+
}
|
|
21
81
|
|
|
22
82
|
// src/regions.ts
|
|
23
83
|
var REGION_URLS = {
|
|
@@ -35,14 +95,32 @@ function resolveBaseUrl(region) {
|
|
|
35
95
|
}
|
|
36
96
|
return url;
|
|
37
97
|
}
|
|
98
|
+
function deriveS3BaseUrl(controlBaseUrl) {
|
|
99
|
+
try {
|
|
100
|
+
const u = new URL(controlBaseUrl);
|
|
101
|
+
u.hostname = u.hostname.replace(/^control\./, "s3.");
|
|
102
|
+
return u.toString().replace(/\/$/, "");
|
|
103
|
+
} catch {
|
|
104
|
+
return void 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/version.ts
|
|
109
|
+
var VERSION = true ? "0.8.13" : "0.0.0-dev";
|
|
110
|
+
var USER_AGENT = `archil-js/${VERSION}`;
|
|
38
111
|
|
|
39
112
|
// src/client.ts
|
|
113
|
+
var createClient = typeof createClientDefault === "function" ? createClientDefault : createClientDefault.default;
|
|
40
114
|
function createApiClient(opts) {
|
|
41
115
|
const baseUrl = opts.baseUrl ?? resolveBaseUrl(opts.region);
|
|
42
116
|
return createClient({
|
|
43
117
|
baseUrl,
|
|
44
118
|
headers: {
|
|
45
|
-
Authorization: `key-${opts.apiKey.replace(/^key-/, "")}
|
|
119
|
+
Authorization: `key-${opts.apiKey.replace(/^key-/, "")}`,
|
|
120
|
+
// Identifies the JS SDK (and its version) to the control plane. Honored
|
|
121
|
+
// by Node's fetch; browsers treat User-Agent as a forbidden header and
|
|
122
|
+
// drop it, which is fine — the SDK's primary use is server-side.
|
|
123
|
+
"User-Agent": USER_AGENT
|
|
46
124
|
}
|
|
47
125
|
});
|
|
48
126
|
}
|
|
@@ -81,8 +159,6 @@ async function unwrapEmpty(promise) {
|
|
|
81
159
|
}
|
|
82
160
|
|
|
83
161
|
// src/disk.ts
|
|
84
|
-
import { createRequire } from "module";
|
|
85
|
-
import { pathToFileURL } from "url";
|
|
86
162
|
var Disk = class {
|
|
87
163
|
id;
|
|
88
164
|
name;
|
|
@@ -104,8 +180,10 @@ var Disk = class {
|
|
|
104
180
|
_client;
|
|
105
181
|
/** @internal */
|
|
106
182
|
_archilRegion;
|
|
183
|
+
/** Base URL for the S3-compatible API (port 9000 ingress). Empty if unset. */
|
|
184
|
+
_s3BaseUrl;
|
|
107
185
|
/** @internal */
|
|
108
|
-
constructor(data, client, archilRegion) {
|
|
186
|
+
constructor(data, client, archilRegion, s3BaseUrl) {
|
|
109
187
|
this.id = data.id;
|
|
110
188
|
this.name = data.name;
|
|
111
189
|
this.organization = data.organization;
|
|
@@ -124,6 +202,7 @@ var Disk = class {
|
|
|
124
202
|
this.allowedIps = data.allowedIps;
|
|
125
203
|
this._client = client;
|
|
126
204
|
this._archilRegion = archilRegion;
|
|
205
|
+
this._s3BaseUrl = s3BaseUrl ?? "";
|
|
127
206
|
}
|
|
128
207
|
toJSON() {
|
|
129
208
|
return {
|
|
@@ -223,6 +302,244 @@ var Disk = class {
|
|
|
223
302
|
})
|
|
224
303
|
);
|
|
225
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Constant-time parallel grep across files on this disk. Listing and
|
|
307
|
+
* matching are fanned out across ephemeral exec containers; the request
|
|
308
|
+
* finishes within the supplied time budget regardless of dataset size.
|
|
309
|
+
*
|
|
310
|
+
* The returned `stoppedReason` says whether the search ran to completion
|
|
311
|
+
* or short-circuited on `maxResults` / `maxDurationSeconds`. When
|
|
312
|
+
* stopping early, the matches returned are a sample (whichever workers
|
|
313
|
+
* reported first), not the lexicographically first N.
|
|
314
|
+
*/
|
|
315
|
+
async grep(opts) {
|
|
316
|
+
return unwrap(
|
|
317
|
+
this._client.POST("/api/disks/{id}/grep", {
|
|
318
|
+
params: { path: { id: this.id } },
|
|
319
|
+
body: {
|
|
320
|
+
directory: opts.directory,
|
|
321
|
+
pattern: opts.pattern,
|
|
322
|
+
recursive: opts.recursive ?? false,
|
|
323
|
+
maxDurationSeconds: opts.maxDurationSeconds ?? 30,
|
|
324
|
+
concurrency: opts.concurrency ?? 50,
|
|
325
|
+
maxResults: opts.maxResults ?? 1e3
|
|
326
|
+
}
|
|
327
|
+
})
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Create a signed, time-limited URL that lets anyone download a single file
|
|
332
|
+
* from this disk without authentication. The returned URL embeds a
|
|
333
|
+
* cryptographically signed token carrying the disk, the file's key, and an
|
|
334
|
+
* expiry — share it directly; no API key is needed to redeem it.
|
|
335
|
+
*
|
|
336
|
+
* @param key Path to the file on the disk (e.g. "reports/2026-01/data.pdf").
|
|
337
|
+
* @param opts `expiresIn` sets the URL lifetime in seconds (any positive
|
|
338
|
+
* integer, max 604800 = 7 days). Defaults to 24h.
|
|
339
|
+
*/
|
|
340
|
+
async share(key, opts = {}) {
|
|
341
|
+
const body = { key };
|
|
342
|
+
if (opts.expiresIn !== void 0) body.expiresIn = opts.expiresIn;
|
|
343
|
+
const call = this._client.POST;
|
|
344
|
+
return unwrap(call(`/api/disks/${this.id}/share`, { body }));
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Read an object from the disk via the S3-compatible GetObject API and return
|
|
348
|
+
* its full contents as bytes. Throws `ArchilS3Error` if the object does not
|
|
349
|
+
* exist (status 404, code "NoSuchKey") or the request is rejected; use
|
|
350
|
+
* `headObject`/`objectExists` to check existence without throwing.
|
|
351
|
+
*
|
|
352
|
+
* @param key Path on the disk (e.g. "reports/2026-01/data.json")
|
|
353
|
+
*/
|
|
354
|
+
async getObject(key) {
|
|
355
|
+
const resp = await this._s3Request("GET", key);
|
|
356
|
+
if (!resp.ok) {
|
|
357
|
+
throw parseS3Error("GetObject", resp.status, resp.statusText, decodeText(resp.body));
|
|
358
|
+
}
|
|
359
|
+
return resp.body;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Fetch an object's metadata (size, etag, content type, last-modified) without
|
|
363
|
+
* downloading its contents, via the S3-compatible HeadObject API. Returns
|
|
364
|
+
* `null` if the object does not exist.
|
|
365
|
+
*
|
|
366
|
+
* @param key Path on the disk (e.g. "reports/2026-01/data.json")
|
|
367
|
+
*/
|
|
368
|
+
async headObject(key) {
|
|
369
|
+
const resp = await this._s3Request("HEAD", key);
|
|
370
|
+
if (resp.status === 404) return null;
|
|
371
|
+
if (!resp.ok) {
|
|
372
|
+
throw parseS3Error("HeadObject", resp.status, resp.statusText, decodeText(resp.body));
|
|
373
|
+
}
|
|
374
|
+
const lastModified = resp.headers.get("last-modified");
|
|
375
|
+
return {
|
|
376
|
+
size: Number(resp.headers.get("content-length") ?? 0),
|
|
377
|
+
etag: resp.headers.get("etag") ?? void 0,
|
|
378
|
+
contentType: resp.headers.get("content-type") ?? void 0,
|
|
379
|
+
lastModified: lastModified ? new Date(lastModified) : void 0
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/** Whether an object exists on the disk (a HeadObject that maps 404 → false). */
|
|
383
|
+
async objectExists(key) {
|
|
384
|
+
return await this.headObject(key) !== null;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Write an object to the disk using the S3-compatible PutObject API. Faster
|
|
388
|
+
* than exec for large files — no container overhead, no command-length limits.
|
|
389
|
+
* Returns the entity tag the server assigned.
|
|
390
|
+
*
|
|
391
|
+
* @param key Path on the disk (e.g. "reports/2026-01/data.json")
|
|
392
|
+
* @param body Contents as a string, Uint8Array/Buffer, or ArrayBuffer
|
|
393
|
+
* @param contentType MIME type (default: "application/octet-stream")
|
|
394
|
+
*/
|
|
395
|
+
async putObject(key, body, contentType = "application/octet-stream") {
|
|
396
|
+
const resp = await this._s3Request("PUT", key, { body, contentType });
|
|
397
|
+
if (!resp.ok) {
|
|
398
|
+
throw parseS3Error("PutObject", resp.status, resp.statusText, decodeText(resp.body));
|
|
399
|
+
}
|
|
400
|
+
return { etag: resp.headers.get("etag") ?? void 0 };
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Delete an object from the disk via the S3-compatible DeleteObject API.
|
|
404
|
+
* Idempotent: deleting a key that doesn't exist resolves successfully, per
|
|
405
|
+
* S3 semantics.
|
|
406
|
+
*
|
|
407
|
+
* @param key Path on the disk (e.g. "project/dist/server.cjs")
|
|
408
|
+
*/
|
|
409
|
+
async deleteObject(key) {
|
|
410
|
+
const resp = await this._s3Request("DELETE", key);
|
|
411
|
+
if (!resp.ok && resp.status !== 404) {
|
|
412
|
+
throw parseS3Error("DeleteObject", resp.status, resp.statusText, decodeText(resp.body));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* List objects on the disk via the S3-compatible ListObjectsV2 API. By
|
|
417
|
+
* default this follows continuation tokens until the listing is exhausted and
|
|
418
|
+
* returns every matching key. Use `limit` to cap the total, `singlePage` for a
|
|
419
|
+
* single request, or {@link listObjectsPages} to stream pages without loading
|
|
420
|
+
* everything into memory.
|
|
421
|
+
*
|
|
422
|
+
* @param prefix Only return keys beginning with this prefix (omit for all).
|
|
423
|
+
* @param opts Listing and pagination options.
|
|
424
|
+
*/
|
|
425
|
+
async listObjects(prefix, opts = {}) {
|
|
426
|
+
if (opts.singlePage) {
|
|
427
|
+
return this._listObjectsPage(prefix, opts);
|
|
428
|
+
}
|
|
429
|
+
const objects = [];
|
|
430
|
+
const commonPrefixes = [];
|
|
431
|
+
const seenPrefixes = /* @__PURE__ */ new Set();
|
|
432
|
+
let echoedPrefix;
|
|
433
|
+
let truncated = false;
|
|
434
|
+
outer: for await (const page of this.listObjectsPages(prefix, opts)) {
|
|
435
|
+
echoedPrefix = page.prefix;
|
|
436
|
+
for (const cp of page.commonPrefixes) {
|
|
437
|
+
if (!seenPrefixes.has(cp)) {
|
|
438
|
+
seenPrefixes.add(cp);
|
|
439
|
+
commonPrefixes.push(cp);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
for (const obj of page.objects) {
|
|
443
|
+
if (opts.limit !== void 0 && objects.length >= opts.limit) {
|
|
444
|
+
truncated = true;
|
|
445
|
+
break outer;
|
|
446
|
+
}
|
|
447
|
+
objects.push(obj);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return { objects, commonPrefixes, isTruncated: truncated, keyCount: objects.length, prefix: echoedPrefix };
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Yield ListObjectsV2 pages lazily, following continuation tokens. A
|
|
454
|
+
* memory-friendly way to process a large listing without materializing it:
|
|
455
|
+
*
|
|
456
|
+
* ```ts
|
|
457
|
+
* for await (const page of disk.listObjectsPages("logs/")) {
|
|
458
|
+
* for (const obj of page.objects) process(obj);
|
|
459
|
+
* }
|
|
460
|
+
* ```
|
|
461
|
+
*
|
|
462
|
+
* @param prefix Only return keys beginning with this prefix (omit for all).
|
|
463
|
+
* @param opts Listing options (`limit` / `singlePage` are ignored here —
|
|
464
|
+
* control your own loop).
|
|
465
|
+
*/
|
|
466
|
+
async *listObjectsPages(prefix, opts = {}) {
|
|
467
|
+
const seenTokens = /* @__PURE__ */ new Set();
|
|
468
|
+
let continuationToken = opts.continuationToken;
|
|
469
|
+
for (; ; ) {
|
|
470
|
+
const page = await this._listObjectsPage(prefix, { ...opts, continuationToken });
|
|
471
|
+
yield page;
|
|
472
|
+
const next = page.isTruncated ? page.nextContinuationToken : void 0;
|
|
473
|
+
if (!next || seenTokens.has(next)) break;
|
|
474
|
+
seenTokens.add(next);
|
|
475
|
+
continuationToken = next;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/** Fetch a single ListObjectsV2 page. @internal */
|
|
479
|
+
async _listObjectsPage(prefix, opts) {
|
|
480
|
+
const query = { "list-type": 2 };
|
|
481
|
+
if (prefix !== void 0) query.prefix = prefix;
|
|
482
|
+
if (!opts.recursive) query.delimiter = "/";
|
|
483
|
+
if (opts.continuationToken !== void 0) query["continuation-token"] = opts.continuationToken;
|
|
484
|
+
if (opts.startAfter !== void 0) query["start-after"] = opts.startAfter;
|
|
485
|
+
const resp = await this._s3Request("GET", "", { query });
|
|
486
|
+
if (!resp.ok) {
|
|
487
|
+
throw parseS3Error("ListObjectsV2", resp.status, resp.statusText, decodeText(resp.body));
|
|
488
|
+
}
|
|
489
|
+
return parseListObjectsResult(decodeText(resp.body));
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Send a single request to the disk's S3-compatible endpoint. This reuses the
|
|
493
|
+
* control-plane client purely for its credential and transport — the same
|
|
494
|
+
* `Authorization` header is sent and verified by the same code server-side —
|
|
495
|
+
* pointed at the S3 host. Returns the response status and fully-buffered body
|
|
496
|
+
* so callers can inspect both regardless of the verb used.
|
|
497
|
+
*
|
|
498
|
+
* The S3 routes (`/{diskId}/{key}` for objects, `/{diskId}` for the bucket)
|
|
499
|
+
* are not part of the typed control-plane API, so the path and per-request
|
|
500
|
+
* options are passed untyped. An empty `key` targets the bucket itself (used
|
|
501
|
+
* by listObjects).
|
|
502
|
+
*
|
|
503
|
+
* @internal
|
|
504
|
+
*/
|
|
505
|
+
async _s3Request(method, key, opts = {}) {
|
|
506
|
+
if (!this._s3BaseUrl) {
|
|
507
|
+
throw new Error(
|
|
508
|
+
"S3 base URL not configured. Pass s3BaseUrl to new Archil({...}) or set ARCHIL_S3_BASE_URL."
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
const call = this._client[method];
|
|
512
|
+
const trimmedKey = key.replace(/^\//, "");
|
|
513
|
+
const encodedKey = trimmedKey.split("/").map(encodeURIComponent).join("/");
|
|
514
|
+
const path = encodedKey ? `/${this.id}/${encodedKey}` : `/${this.id}`;
|
|
515
|
+
const { error, response } = await call(path, {
|
|
516
|
+
baseUrl: this._s3BaseUrl,
|
|
517
|
+
parseAs: "stream",
|
|
518
|
+
...opts.query ? { params: { query: opts.query } } : {},
|
|
519
|
+
// fetch accepts string / Uint8Array / ArrayBuffer bodies directly; pass it
|
|
520
|
+
// through unchanged (no Node Buffer in the data path).
|
|
521
|
+
...opts.body !== void 0 ? { body: opts.body, bodySerializer: (b) => b } : {},
|
|
522
|
+
...opts.contentType ? { headers: { "Content-Type": opts.contentType } } : {}
|
|
523
|
+
});
|
|
524
|
+
if (!response.ok) {
|
|
525
|
+
const message = typeof error === "string" ? error : error ? JSON.stringify(error) : "";
|
|
526
|
+
return {
|
|
527
|
+
ok: false,
|
|
528
|
+
status: response.status,
|
|
529
|
+
statusText: response.statusText,
|
|
530
|
+
headers: response.headers,
|
|
531
|
+
body: new TextEncoder().encode(message)
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
const bytes = method === "GET" ? new Uint8Array(await response.arrayBuffer()) : new Uint8Array(0);
|
|
535
|
+
return {
|
|
536
|
+
ok: true,
|
|
537
|
+
status: response.status,
|
|
538
|
+
statusText: response.statusText,
|
|
539
|
+
headers: response.headers,
|
|
540
|
+
body: bytes
|
|
541
|
+
};
|
|
542
|
+
}
|
|
226
543
|
/**
|
|
227
544
|
* Connect to this disk's data plane via the native ArchilClient.
|
|
228
545
|
*
|
|
@@ -231,13 +548,15 @@ var Disk = class {
|
|
|
231
548
|
async mount(opts) {
|
|
232
549
|
let ArchilClient;
|
|
233
550
|
try {
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
ArchilClient
|
|
551
|
+
const nativeSpecifier = "@archildata/native";
|
|
552
|
+
const native = await import(nativeSpecifier);
|
|
553
|
+
ArchilClient = native.ArchilClient ?? native.default?.ArchilClient;
|
|
554
|
+
if (!ArchilClient) {
|
|
555
|
+
throw new Error("@archildata/native did not export ArchilClient");
|
|
556
|
+
}
|
|
238
557
|
} catch {
|
|
239
558
|
throw new Error(
|
|
240
|
-
"Native client not available. Install @archildata/native (platform-specific binary) or use ArchilClient.connect() directly."
|
|
559
|
+
"Native client not available. Install @archildata/native (platform-specific binary) or use ArchilClient.connect() directly. mount() is not supported in browsers."
|
|
241
560
|
);
|
|
242
561
|
}
|
|
243
562
|
return ArchilClient.connect({
|
|
@@ -250,6 +569,34 @@ var Disk = class {
|
|
|
250
569
|
});
|
|
251
570
|
}
|
|
252
571
|
};
|
|
572
|
+
function optionalString(value) {
|
|
573
|
+
return value === void 0 || value === null ? void 0 : String(value);
|
|
574
|
+
}
|
|
575
|
+
function optionalDate(value) {
|
|
576
|
+
return value === void 0 || value === null ? void 0 : new Date(String(value));
|
|
577
|
+
}
|
|
578
|
+
function decodeText(bytes) {
|
|
579
|
+
return new TextDecoder().decode(bytes);
|
|
580
|
+
}
|
|
581
|
+
function parseListObjectsResult(xml) {
|
|
582
|
+
const root = parseXml(xml).ListBucketResult ?? {};
|
|
583
|
+
const contents = root.Contents ?? [];
|
|
584
|
+
const objects = contents.map((c) => ({
|
|
585
|
+
key: String(c.Key ?? ""),
|
|
586
|
+
size: Number(c.Size ?? 0),
|
|
587
|
+
etag: optionalString(c.ETag),
|
|
588
|
+
lastModified: optionalDate(c.LastModified)
|
|
589
|
+
}));
|
|
590
|
+
const commonPrefixes = (root.CommonPrefixes ?? []).map((cp) => optionalString(cp.Prefix)).filter((p) => p !== void 0);
|
|
591
|
+
return {
|
|
592
|
+
objects,
|
|
593
|
+
commonPrefixes,
|
|
594
|
+
isTruncated: root.IsTruncated === "true" || root.IsTruncated === true,
|
|
595
|
+
nextContinuationToken: optionalString(root.NextContinuationToken),
|
|
596
|
+
keyCount: root.KeyCount !== void 0 ? Number(root.KeyCount) : objects.length,
|
|
597
|
+
prefix: optionalString(root.Prefix)
|
|
598
|
+
};
|
|
599
|
+
}
|
|
253
600
|
|
|
254
601
|
// src/disks.ts
|
|
255
602
|
var Disks = class {
|
|
@@ -258,9 +605,12 @@ var Disks = class {
|
|
|
258
605
|
/** @internal */
|
|
259
606
|
_region;
|
|
260
607
|
/** @internal */
|
|
261
|
-
|
|
608
|
+
_s3BaseUrl;
|
|
609
|
+
/** @internal */
|
|
610
|
+
constructor(client, region, s3BaseUrl) {
|
|
262
611
|
this._client = client;
|
|
263
612
|
this._region = region;
|
|
613
|
+
this._s3BaseUrl = s3BaseUrl;
|
|
264
614
|
}
|
|
265
615
|
async list(opts) {
|
|
266
616
|
const data = await unwrap(
|
|
@@ -269,7 +619,7 @@ var Disks = class {
|
|
|
269
619
|
})
|
|
270
620
|
);
|
|
271
621
|
return data.map(
|
|
272
|
-
(d) => new Disk(d, this._client, this._region)
|
|
622
|
+
(d) => new Disk(d, this._client, this._region, this._s3BaseUrl)
|
|
273
623
|
);
|
|
274
624
|
}
|
|
275
625
|
async get(id) {
|
|
@@ -278,7 +628,7 @@ var Disks = class {
|
|
|
278
628
|
params: { path: { id } }
|
|
279
629
|
})
|
|
280
630
|
);
|
|
281
|
-
return new Disk(data, this._client, this._region);
|
|
631
|
+
return new Disk(data, this._client, this._region, this._s3BaseUrl);
|
|
282
632
|
}
|
|
283
633
|
/**
|
|
284
634
|
* Create a new disk with an auto-generated mount token.
|
|
@@ -338,6 +688,9 @@ var Tokens = class {
|
|
|
338
688
|
};
|
|
339
689
|
|
|
340
690
|
// src/archil.ts
|
|
691
|
+
function envVar(name) {
|
|
692
|
+
return typeof process !== "undefined" && process.env ? process.env[name] : void 0;
|
|
693
|
+
}
|
|
341
694
|
function isExecMountSpec(m) {
|
|
342
695
|
return typeof m === "object" && m !== null && "disk" in m;
|
|
343
696
|
}
|
|
@@ -350,21 +703,23 @@ var Archil = class {
|
|
|
350
703
|
/** @internal */
|
|
351
704
|
_client;
|
|
352
705
|
constructor(opts = {}) {
|
|
353
|
-
const apiKey = opts.apiKey ??
|
|
354
|
-
const region = opts.region ??
|
|
706
|
+
const apiKey = opts.apiKey ?? envVar("ARCHIL_API_KEY");
|
|
707
|
+
const region = opts.region ?? envVar("ARCHIL_REGION");
|
|
355
708
|
if (!apiKey) {
|
|
356
709
|
throw new Error("Missing API key: pass apiKey in options or set ARCHIL_API_KEY environment variable");
|
|
357
710
|
}
|
|
358
711
|
if (!region) {
|
|
359
712
|
throw new Error("Missing region: pass region in options or set ARCHIL_REGION environment variable");
|
|
360
713
|
}
|
|
714
|
+
const controlBaseUrl = opts.baseUrl ?? resolveBaseUrl(region);
|
|
361
715
|
const client = createApiClient({
|
|
362
716
|
apiKey,
|
|
363
717
|
region,
|
|
364
|
-
baseUrl:
|
|
718
|
+
baseUrl: controlBaseUrl
|
|
365
719
|
});
|
|
720
|
+
const s3BaseUrl = opts.s3BaseUrl ?? envVar("ARCHIL_S3_BASE_URL") ?? deriveS3BaseUrl(controlBaseUrl);
|
|
366
721
|
this._client = client;
|
|
367
|
-
this.disks = new Disks(client, region);
|
|
722
|
+
this.disks = new Disks(client, region, s3BaseUrl);
|
|
368
723
|
this.tokens = new Tokens(client);
|
|
369
724
|
}
|
|
370
725
|
/**
|
|
@@ -427,7 +782,7 @@ function deleteApiKey(id) {
|
|
|
427
782
|
}
|
|
428
783
|
|
|
429
784
|
// bin/cli.ts
|
|
430
|
-
var pkg =
|
|
785
|
+
var pkg = createRequire(import.meta.url)("../package.json");
|
|
431
786
|
var program = new Command();
|
|
432
787
|
program.name("disk").description("Manage Archil disks from the command line").version(pkg.version).addOption(new Option("-k, --api-key <key>", "Archil API key").env("ARCHIL_API_KEY")).addOption(new Option("-r, --region <region>", "Archil region").env("ARCHIL_REGION")).addOption(new Option("--base-url <url>", "Override control-plane base URL")).hook("preAction", () => {
|
|
433
788
|
const opts = program.opts();
|
|
@@ -607,6 +962,44 @@ program.command("exec <id|name> <command...>").description("Run a command in a c
|
|
|
607
962
|
fail(err);
|
|
608
963
|
}
|
|
609
964
|
});
|
|
965
|
+
program.command("grep <pattern> <target>").description(
|
|
966
|
+
"Constant-time parallel server-side grep across a disk. <target> is <id|name>[/<path>] \u2014 the path after the first '/' is the directory to search (defaults to the disk root). Accepts a disk id or name."
|
|
967
|
+
).option("--no-recursive", "Search only the given directory, not its subdirectories (recursive is the default)").option("--max-duration <seconds>", "Wall-clock deadline for the whole search", (v) => parseInt(v, 10)).option("--concurrency <n>", "Max parallel grep workers (clamped to fleet capacity)", (v) => parseInt(v, 10)).option("--max-results <n>", "Stop once this many matches are collected", (v) => parseInt(v, 10)).option("-o, --output <format>", "Output format: text | json", "text").action(
|
|
968
|
+
async (pattern, target, opts) => {
|
|
969
|
+
try {
|
|
970
|
+
const slash = target.indexOf("/");
|
|
971
|
+
const idOrName = slash === -1 ? target : target.slice(0, slash);
|
|
972
|
+
const directory = slash === -1 ? "" : target.slice(slash + 1);
|
|
973
|
+
const d = await resolveDisk(idOrName);
|
|
974
|
+
const res = await d.grep({
|
|
975
|
+
directory,
|
|
976
|
+
pattern,
|
|
977
|
+
recursive: opts.recursive !== false,
|
|
978
|
+
maxDurationSeconds: opts.maxDuration,
|
|
979
|
+
concurrency: opts.concurrency,
|
|
980
|
+
maxResults: opts.maxResults
|
|
981
|
+
});
|
|
982
|
+
if (opts.output === "json") {
|
|
983
|
+
console.log(JSON.stringify(res, null, 2));
|
|
984
|
+
} else {
|
|
985
|
+
for (const m of res.matches) {
|
|
986
|
+
console.log(`${m.file}:${m.line}:${m.text}`);
|
|
987
|
+
}
|
|
988
|
+
console.error(
|
|
989
|
+
`${res.matches.length} match${res.matches.length === 1 ? "" : "es"} \xB7 ${res.filesScanned} files scanned \xB7 ${res.containersDispatched} containers \xB7 ${res.durationMs}ms \xB7 ${res.stoppedReason}`
|
|
990
|
+
);
|
|
991
|
+
if (res.stoppedReason !== "completed") {
|
|
992
|
+
console.error(
|
|
993
|
+
`warning: search did not complete (${res.stoppedReason}); results may be partial`
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
process.exit(res.matches.length > 0 ? 0 : 1);
|
|
998
|
+
} catch (err) {
|
|
999
|
+
fail(err);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
);
|
|
610
1003
|
var keys = program.command("api-keys").description("Manage Archil API keys (account-level credentials)");
|
|
611
1004
|
keys.command("list").description("List API keys").option("--limit <n>", "Maximum number of keys to return", (v) => parseInt(v, 10)).action(async (opts) => {
|
|
612
1005
|
try {
|
|
@@ -644,7 +1037,7 @@ keys.command("delete <id>").description("Delete an API key").action(async (id) =
|
|
|
644
1037
|
}
|
|
645
1038
|
});
|
|
646
1039
|
function rewriteSugar(argv) {
|
|
647
|
-
const known = /* @__PURE__ */ new Set(["list", "get", "create", "delete", "exec", "api-keys", "help", "--help", "-h", "--version", "-V"]);
|
|
1040
|
+
const known = /* @__PURE__ */ new Set(["list", "get", "create", "delete", "exec", "grep", "api-keys", "help", "--help", "-h", "--version", "-V"]);
|
|
648
1041
|
const head = argv[2];
|
|
649
1042
|
const next = argv[3];
|
|
650
1043
|
if (head && next === "exec" && !known.has(head) && !head.startsWith("-")) {
|