@stim-cli/cache-s3 1.21.0 → 1.22.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/README.md +15 -2
- package/dist/credentials-worker.d.mts +1 -0
- package/dist/credentials-worker.mjs +46 -0
- package/dist/extract-worker.d.mts +1 -0
- package/dist/extract-worker.mjs +36 -0
- package/dist/index.d.mts +4 -1
- package/dist/index.mjs +255 -100
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -33,6 +33,12 @@ SDK's standard credential chain. For a named profile in `~/.aws/credentials`,
|
|
|
33
33
|
set `cache.options.profile` to `stim-r2`, or run commands with
|
|
34
34
|
`AWS_PROFILE=stim-r2`, for example `AWS_PROFILE=stim-r2 stim ios`. An explicit
|
|
35
35
|
cache profile overrides the ambient AWS profile. Credentials never belong in `.stim.json`.
|
|
36
|
+
Programmatic Stim clients use their captured environment, including profile files
|
|
37
|
+
and `credential_process`. The optional provider resolves credentials and environment-backed request settings
|
|
38
|
+
in a cancellable helper process, keeping the calling process environment unchanged.
|
|
39
|
+
Captured endpoint settings are pinned for the request, with no fallback to the
|
|
40
|
+
calling process's endpoint. Credentials and settings return over private IPC and
|
|
41
|
+
are reused until the credentials need refreshing.
|
|
36
42
|
|
|
37
43
|
Create a private R2 bucket and an R2 API token with Object Read & Write access
|
|
38
44
|
scoped to that bucket. Copy the S3 endpoint shown by Cloudflare; jurisdiction
|
|
@@ -47,6 +53,11 @@ existing build entries unless overwrite was requested. Use matching native
|
|
|
47
53
|
toolchains for the initial test; the provider does not add toolchain information
|
|
48
54
|
to Stim's existing build key.
|
|
49
55
|
|
|
56
|
+
Archive extraction runs in an owned helper. Cancellation waits for that process
|
|
57
|
+
to exit before removing download staging, so extraction cannot keep writing
|
|
58
|
+
after the operation settles. A cleanup failure reports the retained directory
|
|
59
|
+
and filesystem cause; bounded plans refuse instead of counting it as a miss.
|
|
60
|
+
|
|
50
61
|
The file transport is also exported as `createS3FileCache()` from
|
|
51
62
|
`@stim-cli/cache-s3`, with the `FileCacheCapability` interface exported from
|
|
52
63
|
`@stim-cli/cache`. A cache adapter supplies a compatible key and a completed
|
|
@@ -61,5 +72,7 @@ Objects use `<prefix>/builds/<platform>/<key>` and
|
|
|
61
72
|
`<prefix>/native-android/v1/<key>`. A prefix defaults to `stim/v1`. Keep
|
|
62
73
|
incompatible snapshot formats in separate key namespaces. Retention belongs to
|
|
63
74
|
the bucket lifecycle configuration; local GC never removes shared objects.
|
|
64
|
-
Uploads use a single S3 PUT, so entries larger than S3's 5 GiB limit are
|
|
65
|
-
supported
|
|
75
|
+
Uploads use a single-object S3 PUT, so entries larger than S3's 5 GiB limit are
|
|
76
|
+
not supported. A connection reset retries the completed file with a fresh stream,
|
|
77
|
+
up to three PUT attempts with short, cancellable delays. Conditional writes still
|
|
78
|
+
preserve an existing entry if the first upload committed but its reply was lost.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
//#region credentials-worker.ts
|
|
3
|
+
process.once("message", async (input) => {
|
|
4
|
+
const client = new S3Client({
|
|
5
|
+
...input,
|
|
6
|
+
logger: {
|
|
7
|
+
debug() {},
|
|
8
|
+
info() {},
|
|
9
|
+
warn() {},
|
|
10
|
+
error() {}
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
try {
|
|
14
|
+
const config = client.config;
|
|
15
|
+
const credentials = await config.credentials();
|
|
16
|
+
const endpoint = input.endpoint ?? await config.serviceConfiguredEndpoint?.();
|
|
17
|
+
const message = {
|
|
18
|
+
credentials: {
|
|
19
|
+
...credentials,
|
|
20
|
+
expiration: credentials.expiration?.toISOString()
|
|
21
|
+
},
|
|
22
|
+
requestConfig: {
|
|
23
|
+
...endpoint === void 0 ? {} : { endpoint },
|
|
24
|
+
disableClockSkewCorrection: await config.disableClockSkewCorrection(),
|
|
25
|
+
useFipsEndpoint: await config.useFipsEndpoint(),
|
|
26
|
+
useDualstackEndpoint: await config.useDualstackEndpoint(),
|
|
27
|
+
useArnRegion: typeof config.useArnRegion === "function" ? await config.useArnRegion() : config.useArnRegion,
|
|
28
|
+
disableS3ExpressSessionAuth: typeof config.disableS3ExpressSessionAuth === "function" ? await config.disableS3ExpressSessionAuth() : config.disableS3ExpressSessionAuth,
|
|
29
|
+
authSchemePreference: typeof config.authSchemePreference === "function" ? await config.authSchemePreference() : config.authSchemePreference,
|
|
30
|
+
sigv4aSigningRegionSet: typeof config.sigv4aSigningRegionSet === "function" ? await config.sigv4aSigningRegionSet() : config.sigv4aSigningRegionSet,
|
|
31
|
+
maxAttempts: typeof config.maxAttempts === "function" ? await config.maxAttempts() : config.maxAttempts,
|
|
32
|
+
retryMode: typeof config.retryMode === "function" ? await config.retryMode() : config.retryMode,
|
|
33
|
+
defaultsMode: typeof config.defaultsMode === "function" ? await config.defaultsMode() : config.defaultsMode,
|
|
34
|
+
userAgentAppId: typeof config.userAgentAppId === "function" ? await config.userAgentAppId() : config.userAgentAppId
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
process.send?.({ value: message }, () => process.exit(0));
|
|
38
|
+
} catch {
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
process.disconnect?.();
|
|
41
|
+
} finally {
|
|
42
|
+
client.destroy();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
//#endregion
|
|
46
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { pipeline } from "node:stream/promises";
|
|
4
|
+
import { extract } from "tar";
|
|
5
|
+
//#region extract-worker.ts
|
|
6
|
+
process.once("message", async ({ archive, staging }) => {
|
|
7
|
+
try {
|
|
8
|
+
let invalidLink = false;
|
|
9
|
+
await pipeline(createReadStream(archive), extract({
|
|
10
|
+
cwd: staging,
|
|
11
|
+
strict: true,
|
|
12
|
+
filter(path, entry) {
|
|
13
|
+
if (!("type" in entry)) return true;
|
|
14
|
+
if (entry.type !== "SymbolicLink" && entry.type !== "Link") return true;
|
|
15
|
+
const link = entry.linkpath ?? "";
|
|
16
|
+
const target = resolve(staging, entry.type === "SymbolicLink" ? dirname(path) : "", link);
|
|
17
|
+
const root = relative(staging, resolve(staging, path)).split(sep)[0];
|
|
18
|
+
const outside = relative(resolve(staging, root ?? ""), target);
|
|
19
|
+
if (isAbsolute(link) || outside === ".." || outside.startsWith(`..${sep}`)) {
|
|
20
|
+
invalidLink = true;
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}));
|
|
26
|
+
if (invalidLink) throw new Error("The S3 build entry contains a link outside the artifact.");
|
|
27
|
+
process.send?.({ value: true }, () => process.exit(0));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
process.send?.({ error: {
|
|
30
|
+
message: error instanceof Error ? error.message : String(error),
|
|
31
|
+
...error instanceof Error && "code" in error && typeof error.code === "string" ? { code: error.code } : {}
|
|
32
|
+
} }, () => process.exit(1));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
//#endregion
|
|
36
|
+
export {};
|
package/dist/index.d.mts
CHANGED
|
@@ -8,12 +8,15 @@ interface S3FileCacheOptions {
|
|
|
8
8
|
region?: string | undefined;
|
|
9
9
|
profile?: string | undefined;
|
|
10
10
|
forcePathStyle?: boolean | undefined;
|
|
11
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
12
|
+
projectRoot?: string | undefined;
|
|
11
13
|
}
|
|
12
14
|
/** Stores opaque files; each cache adapter owns its key and serialization. Credentials use the AWS SDK chain. */
|
|
13
15
|
declare function createS3FileCache(options: S3FileCacheOptions): FileCacheCapability & Required<Pick<FileCacheCapability, "stat">>;
|
|
14
|
-
declare function createCacheProvider({ options }: {
|
|
16
|
+
declare function createCacheProvider({ options, projectRoot, env }: {
|
|
15
17
|
projectRoot: string;
|
|
16
18
|
options: Record<string, unknown>;
|
|
19
|
+
env?: NodeJS.ProcessEnv;
|
|
17
20
|
}): CacheProvider;
|
|
18
21
|
//#endregion
|
|
19
22
|
export { S3FileCacheOptions, apiVersion, createCacheProvider, createS3FileCache };
|
package/dist/index.mjs
CHANGED
|
@@ -3,12 +3,118 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
3
3
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
4
4
|
import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { basename, dirname,
|
|
6
|
+
import { basename, dirname, join } from "node:path";
|
|
7
7
|
import { Readable, Transform } from "node:stream";
|
|
8
8
|
import { pipeline } from "node:stream/promises";
|
|
9
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
9
10
|
import { deserialize, serialize } from "node:v8";
|
|
10
|
-
import { create
|
|
11
|
-
import { CACHE_PROVIDER_API_VERSION } from "@stim-cli/cache";
|
|
11
|
+
import { create } from "tar";
|
|
12
|
+
import { CACHE_PROVIDER_API_VERSION, buildResolveTimeoutMs } from "@stim-cli/cache";
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
//#region worker.ts
|
|
16
|
+
function scopedWorker(worker, input, { env, cwd, signal, timeoutMs, name }) {
|
|
17
|
+
signal.throwIfAborted();
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const child = spawn(process.execPath, [fileURLToPath(worker)], {
|
|
20
|
+
cwd,
|
|
21
|
+
env,
|
|
22
|
+
detached: process.platform !== "win32",
|
|
23
|
+
stdio: [
|
|
24
|
+
"ignore",
|
|
25
|
+
"ignore",
|
|
26
|
+
"ignore",
|
|
27
|
+
"ipc"
|
|
28
|
+
]
|
|
29
|
+
});
|
|
30
|
+
let reply;
|
|
31
|
+
let failure;
|
|
32
|
+
let closed = false;
|
|
33
|
+
let stopping;
|
|
34
|
+
const stop = () => {
|
|
35
|
+
if (closed || !child.pid) return;
|
|
36
|
+
if (process.platform === "win32") stopping ??= new Promise((done) => {
|
|
37
|
+
const killer = spawn(join(process.env.SystemRoot || "C:\\Windows", "System32", "taskkill.exe"), [
|
|
38
|
+
"/PID",
|
|
39
|
+
String(child.pid),
|
|
40
|
+
"/T",
|
|
41
|
+
"/F"
|
|
42
|
+
], {
|
|
43
|
+
stdio: "ignore",
|
|
44
|
+
windowsHide: true
|
|
45
|
+
});
|
|
46
|
+
killer.once("error", (error) => {
|
|
47
|
+
failure ??= error;
|
|
48
|
+
});
|
|
49
|
+
killer.once("close", (code) => {
|
|
50
|
+
if (code !== 0) failure ??= /* @__PURE__ */ new Error(`${name} process cleanup failed.`);
|
|
51
|
+
done();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
else try {
|
|
55
|
+
process.kill(-child.pid, "SIGKILL");
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) failure ??= error;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
failure = /* @__PURE__ */ new Error(`${name} did not finish within ${timeoutMs / 1e3} seconds.`);
|
|
62
|
+
stop();
|
|
63
|
+
}, timeoutMs);
|
|
64
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
65
|
+
if (signal.aborted) stop();
|
|
66
|
+
child.once("spawn", () => {
|
|
67
|
+
if (signal.aborted) stop();
|
|
68
|
+
else child.send(input, (error) => {
|
|
69
|
+
if (error) {
|
|
70
|
+
failure = error;
|
|
71
|
+
stop();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
child.once("message", (message) => {
|
|
76
|
+
reply = message;
|
|
77
|
+
});
|
|
78
|
+
child.once("error", (error) => {
|
|
79
|
+
failure = error;
|
|
80
|
+
});
|
|
81
|
+
child.once("close", async (code) => {
|
|
82
|
+
closed = true;
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
signal.removeEventListener("abort", stop);
|
|
85
|
+
await stopping;
|
|
86
|
+
if (signal.aborted) reject(signal.reason);
|
|
87
|
+
else if (failure) reject(failure);
|
|
88
|
+
else if (reply?.error) reject(Object.assign(new Error(reply.error.message), { code: reply.error.code }));
|
|
89
|
+
else if (code !== 0 || reply?.value === void 0) reject(/* @__PURE__ */ new Error(`Could not complete ${name}.`));
|
|
90
|
+
else resolve(reply.value);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region credentials.ts
|
|
96
|
+
function scopedClientContext(env, cwd, options, signal) {
|
|
97
|
+
return scopedWorker(new URL(import.meta.url.endsWith(".ts") ? "./credentials-worker.ts" : "./credentials-worker.mjs", import.meta.url), options, {
|
|
98
|
+
env: {
|
|
99
|
+
...env,
|
|
100
|
+
...options.profile ? { AWS_PROFILE: options.profile } : {}
|
|
101
|
+
},
|
|
102
|
+
cwd,
|
|
103
|
+
signal,
|
|
104
|
+
timeoutMs: 1e4,
|
|
105
|
+
name: "S3 credential resolution"
|
|
106
|
+
}).then((context) => {
|
|
107
|
+
const { expiration, ...credentials } = context.credentials;
|
|
108
|
+
return {
|
|
109
|
+
credentials: {
|
|
110
|
+
...credentials,
|
|
111
|
+
...expiration ? { expiration: new Date(expiration) } : {}
|
|
112
|
+
},
|
|
113
|
+
requestConfig: context.requestConfig
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
12
118
|
//#region index.ts
|
|
13
119
|
const apiVersion = CACHE_PROVIDER_API_VERSION;
|
|
14
120
|
async function fileDigests(path, signal) {
|
|
@@ -46,93 +152,147 @@ function createS3FileCache(options) {
|
|
|
46
152
|
error() {}
|
|
47
153
|
}
|
|
48
154
|
};
|
|
49
|
-
const
|
|
155
|
+
const environment = options.env === void 0 ? void 0 : { ...options.env };
|
|
156
|
+
const defaultClient = environment ? void 0 : new S3Client(config);
|
|
157
|
+
let context;
|
|
158
|
+
async function withClient(signal, run) {
|
|
159
|
+
signal.throwIfAborted();
|
|
160
|
+
if (defaultClient) return run(defaultClient);
|
|
161
|
+
if (!context || context.credentials.expiration && context.credentials.expiration.getTime() - Date.now() < 3e5) context = await scopedClientContext(environment ?? {}, options.projectRoot ?? process.cwd(), {
|
|
162
|
+
profile: options.profile,
|
|
163
|
+
region: options.region ?? "us-east-1",
|
|
164
|
+
endpoint: options.endpoint
|
|
165
|
+
}, signal);
|
|
166
|
+
signal.throwIfAborted();
|
|
167
|
+
const captured = context;
|
|
168
|
+
const scoped = new S3Client({
|
|
169
|
+
...config,
|
|
170
|
+
...captured.requestConfig,
|
|
171
|
+
credentials: captured.credentials,
|
|
172
|
+
ignoreConfiguredEndpointUrls: true,
|
|
173
|
+
useArnRegion: async () => captured.requestConfig.useArnRegion,
|
|
174
|
+
disableS3ExpressSessionAuth: async () => captured.requestConfig.disableS3ExpressSessionAuth,
|
|
175
|
+
sigv4aSigningRegionSet: async () => captured.requestConfig.sigv4aSigningRegionSet,
|
|
176
|
+
userAgentAppId: async () => captured.requestConfig.userAgentAppId
|
|
177
|
+
});
|
|
178
|
+
try {
|
|
179
|
+
return await run(scoped);
|
|
180
|
+
} finally {
|
|
181
|
+
scoped.destroy();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
50
184
|
const prefix = (options.prefix ?? "stim/v1").replace(/^\/+|\/+$/g, "");
|
|
51
185
|
const objectKey = (key) => `${prefix}/${key}`;
|
|
52
186
|
return {
|
|
53
187
|
async stat({ key, signal }) {
|
|
54
|
-
signal
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
188
|
+
return withClient(signal, async (client) => {
|
|
189
|
+
signal.throwIfAborted();
|
|
190
|
+
try {
|
|
191
|
+
const object = await client.send(new HeadObjectCommand({
|
|
192
|
+
Bucket: options.bucket,
|
|
193
|
+
Key: objectKey(key)
|
|
194
|
+
}), { abortSignal: signal });
|
|
195
|
+
return typeof object.ContentLength === "number" ? { size: object.ContentLength } : null;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (status(error) === 404) return null;
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
65
201
|
},
|
|
66
202
|
async resolve({ key, destinationDir, signal }) {
|
|
67
|
-
signal
|
|
68
|
-
let object;
|
|
69
|
-
try {
|
|
70
|
-
object = await client.send(new GetObjectCommand({
|
|
71
|
-
Bucket: options.bucket,
|
|
72
|
-
Key: objectKey(key)
|
|
73
|
-
}), { abortSignal: signal });
|
|
74
|
-
} catch (error) {
|
|
75
|
-
if (status(error) === 404) return null;
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
const expected = object.Metadata?.sha256;
|
|
79
|
-
if (!(object.Body instanceof Readable) || !expected || !/^[a-f0-9]{64}$/.test(expected)) {
|
|
80
|
-
if (object.Body instanceof Readable) object.Body.destroy();
|
|
81
|
-
throw new Error("The S3 cache entry has no SHA-256 metadata.");
|
|
82
|
-
}
|
|
83
|
-
const temporary = join(destinationDir, `${randomUUID()}.partial`);
|
|
84
|
-
const complete = join(destinationDir, `${randomUUID()}.cache`);
|
|
85
|
-
const digest = createHash("sha256");
|
|
86
|
-
try {
|
|
87
|
-
await mkdir(destinationDir, { recursive: true });
|
|
88
|
-
await pipeline(object.Body, new Transform({ transform(chunk, _encoding, callback) {
|
|
89
|
-
digest.update(chunk);
|
|
90
|
-
callback(null, chunk);
|
|
91
|
-
} }), createWriteStream(temporary, { flags: "wx" }), { signal });
|
|
92
|
-
if (digest.digest("hex") !== expected) throw new Error("The S3 cache entry failed SHA-256 verification.");
|
|
203
|
+
return withClient(signal, async (client) => {
|
|
93
204
|
signal.throwIfAborted();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
205
|
+
let object;
|
|
206
|
+
try {
|
|
207
|
+
object = await client.send(new GetObjectCommand({
|
|
208
|
+
Bucket: options.bucket,
|
|
209
|
+
Key: objectKey(key)
|
|
210
|
+
}), { abortSignal: signal });
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (status(error) === 404) return null;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
const expected = object.Metadata?.sha256;
|
|
216
|
+
if (!(object.Body instanceof Readable) || !expected || !/^[a-f0-9]{64}$/.test(expected)) {
|
|
217
|
+
if (object.Body instanceof Readable) object.Body.destroy();
|
|
218
|
+
throw new Error("The S3 cache entry has no SHA-256 metadata.");
|
|
219
|
+
}
|
|
220
|
+
const temporary = join(destinationDir, `${randomUUID()}.partial`);
|
|
221
|
+
const complete = join(destinationDir, `${randomUUID()}.cache`);
|
|
222
|
+
const digest = createHash("sha256");
|
|
223
|
+
try {
|
|
224
|
+
await mkdir(destinationDir, { recursive: true });
|
|
225
|
+
await pipeline(object.Body, new Transform({ transform(chunk, _encoding, callback) {
|
|
226
|
+
digest.update(chunk);
|
|
227
|
+
callback(null, chunk);
|
|
228
|
+
} }), createWriteStream(temporary, { flags: "wx" }), { signal });
|
|
229
|
+
if (digest.digest("hex") !== expected) throw new Error("The S3 cache entry failed SHA-256 verification.");
|
|
230
|
+
signal.throwIfAborted();
|
|
231
|
+
await rename(temporary, complete);
|
|
232
|
+
return complete;
|
|
233
|
+
} finally {
|
|
234
|
+
object.Body.destroy();
|
|
235
|
+
await rm(temporary, { force: true });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
100
238
|
},
|
|
101
239
|
async store({ key, sourcePath, overwrite, signal }) {
|
|
102
|
-
signal
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
240
|
+
return withClient(signal, async (client) => {
|
|
241
|
+
signal.throwIfAborted();
|
|
242
|
+
const digests = await fileDigests(sourcePath, signal);
|
|
243
|
+
const size = (await stat(sourcePath)).size;
|
|
244
|
+
for (let attempt = 0;; attempt++) {
|
|
245
|
+
signal.throwIfAborted();
|
|
246
|
+
const body = createReadStream(sourcePath, { signal });
|
|
247
|
+
try {
|
|
248
|
+
await client.send(new PutObjectCommand({
|
|
249
|
+
Bucket: options.bucket,
|
|
250
|
+
Key: objectKey(key),
|
|
251
|
+
Body: body,
|
|
252
|
+
ContentLength: size,
|
|
253
|
+
ContentMD5: digests.md5,
|
|
254
|
+
Metadata: { sha256: digests.sha256 },
|
|
255
|
+
IfNoneMatch: overwrite ? void 0 : "*"
|
|
256
|
+
}), { abortSignal: signal });
|
|
257
|
+
return;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (!overwrite && status(error) === 412) return;
|
|
260
|
+
if (attempt === 2 || signal.aborted || !(error instanceof Error) || !("code" in error) || error.code !== "ECONNRESET") throw error;
|
|
261
|
+
} finally {
|
|
262
|
+
body.destroy();
|
|
263
|
+
}
|
|
264
|
+
await setTimeout$1(100 * 2 ** attempt, void 0, { signal });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
120
267
|
}
|
|
121
268
|
};
|
|
122
269
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
recursive: true,
|
|
130
|
-
force: true
|
|
131
|
-
});
|
|
132
|
-
}
|
|
270
|
+
function cleanupFailure(path, cause) {
|
|
271
|
+
return Object.assign(new Error(`S3 cache cleanup failed. Retained ${path}.`, { cause }), {
|
|
272
|
+
code: "STIM_CLAIM_UNAVAILABLE",
|
|
273
|
+
reportCleanup: true,
|
|
274
|
+
remedy: "Check the retained directory permissions and remove it before retrying."
|
|
275
|
+
});
|
|
133
276
|
}
|
|
134
|
-
function
|
|
277
|
+
async function scratch(run, parent) {
|
|
278
|
+
if (parent) await mkdir(parent, { recursive: true });
|
|
279
|
+
const dir = await mkdtemp(join(parent ?? tmpdir(), "stim-s3-cache-"));
|
|
280
|
+
return Promise.resolve().then(() => run(dir)).finally(async () => {
|
|
281
|
+
try {
|
|
282
|
+
await rm(dir, {
|
|
283
|
+
recursive: true,
|
|
284
|
+
force: true
|
|
285
|
+
});
|
|
286
|
+
} catch (cause) {
|
|
287
|
+
throw cleanupFailure(parent ?? dir, cause);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function createCacheProvider({ options, projectRoot, env }) {
|
|
292
|
+
const environment = env === void 0 ? void 0 : { ...env };
|
|
135
293
|
const files = createS3FileCache({
|
|
294
|
+
env: environment,
|
|
295
|
+
projectRoot,
|
|
136
296
|
bucket: typeof options.bucket === "string" ? options.bucket : "",
|
|
137
297
|
prefix: typeof options.prefix === "string" ? options.prefix : void 0,
|
|
138
298
|
endpoint: typeof options.endpoint === "string" ? options.endpoint : void 0,
|
|
@@ -153,25 +313,16 @@ function createCacheProvider({ options }) {
|
|
|
153
313
|
if (!archive) return null;
|
|
154
314
|
const staging = await mkdtemp(join(destinationDir, "s3-"));
|
|
155
315
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const outside = relative(resolve(staging, root ?? ""), target);
|
|
167
|
-
if (isAbsolute(link) || outside === ".." || outside.startsWith(`..${sep}`)) {
|
|
168
|
-
invalidLink = true;
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
}), { signal });
|
|
174
|
-
if (invalidLink) throw new Error("The S3 build entry contains a link outside the artifact.");
|
|
316
|
+
await scopedWorker(new URL(import.meta.url.endsWith(".ts") ? "./extract-worker.ts" : "./extract-worker.mjs", import.meta.url), {
|
|
317
|
+
archive,
|
|
318
|
+
staging
|
|
319
|
+
}, {
|
|
320
|
+
env: { ...environment ?? process.env },
|
|
321
|
+
cwd: projectRoot,
|
|
322
|
+
signal,
|
|
323
|
+
timeoutMs: buildResolveTimeoutMs(environment),
|
|
324
|
+
name: "S3 archive extraction"
|
|
325
|
+
});
|
|
175
326
|
signal.throwIfAborted();
|
|
176
327
|
const entries = await readdir(staging);
|
|
177
328
|
const [entry] = entries;
|
|
@@ -181,13 +332,17 @@ function createCacheProvider({ options }) {
|
|
|
181
332
|
if (platform === "ios" ? !info.isDirectory() || !path.endsWith(".app") : !info.isFile() || !path.endsWith(".apk")) throw new Error("The S3 build entry has the wrong artifact type.");
|
|
182
333
|
return path;
|
|
183
334
|
} catch (error) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
335
|
+
try {
|
|
336
|
+
await rm(staging, {
|
|
337
|
+
recursive: true,
|
|
338
|
+
force: true
|
|
339
|
+
});
|
|
340
|
+
} catch (cause) {
|
|
341
|
+
throw cleanupFailure(destinationDir, cause);
|
|
342
|
+
}
|
|
188
343
|
throw error;
|
|
189
344
|
}
|
|
190
|
-
});
|
|
345
|
+
}, destinationDir);
|
|
191
346
|
},
|
|
192
347
|
async store({ platform, key, sourcePath, overwrite, signal }) {
|
|
193
348
|
await scratch(async (dir) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stim-cli/cache-s3",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Optional S3-compatible file, native artifact and Metro cache provider for Stim.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@aws-sdk/client-s3": "^3.1149.0",
|
|
28
|
-
"@stim-cli/cache": "^1.
|
|
28
|
+
"@stim-cli/cache": "^1.22.0",
|
|
29
29
|
"tar": "^7.5.22"
|
|
30
30
|
},
|
|
31
31
|
"engines": {
|