@hot-updater/storage-s3 0.0.0-stage → 1.1.4
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/LICENSE +33 -0
- package/dist/index.cjs +167 -0
- package/dist/index.d.cts +34 -0
- package/dist/index.d.mts +34 -0
- package/dist/index.mjs +166 -0
- package/package.json +46 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Sungyu Kang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
ADDITIONAL DISCLAIMER
|
|
26
|
+
|
|
27
|
+
The authors of this software shall not be held responsible for any legal issues,
|
|
28
|
+
regulatory violations, policy violations, or disputes arising from the use,
|
|
29
|
+
distribution, or integration of this software.
|
|
30
|
+
|
|
31
|
+
Users are solely responsible for ensuring compliance with all applicable laws,
|
|
32
|
+
regulations, and third-party policies (including but not limited to app store
|
|
33
|
+
policies).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
|
|
3
|
+
let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
|
|
4
|
+
let _aws_sdk_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
|
|
5
|
+
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
6
|
+
//#region src/createS3Storage.ts
|
|
7
|
+
/** Devices download right after the update check that returns the URL. */
|
|
8
|
+
const DEFAULT_SIGNED_LIFETIME_SECONDS = 3600;
|
|
9
|
+
/** SigV4's limit on a presigned URL. */
|
|
10
|
+
const MAX_SIGNED_LIFETIME_SECONDS = 604800;
|
|
11
|
+
/** DeleteObjects takes at most 1000 keys a request. */
|
|
12
|
+
const DELETE_BATCH_SIZE = 1e3;
|
|
13
|
+
const isObjectNotFoundError = (error) => {
|
|
14
|
+
if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey")) return true;
|
|
15
|
+
return typeof error === "object" && error !== null && "$metadata" in error && error.$metadata?.httpStatusCode === 404;
|
|
16
|
+
};
|
|
17
|
+
const trimSlashes = (value) => value.replace(/^\/+|\/+$/g, "");
|
|
18
|
+
/**
|
|
19
|
+
* A storage adapter over a bucket's S3 API, which AWS S3, Cloudflare R2, and
|
|
20
|
+
* Neon Object Storage all serve. Each provider package passes its own name,
|
|
21
|
+
* URI protocol, and client defaults.
|
|
22
|
+
*/
|
|
23
|
+
const createS3Storage = ({ name, protocol }, config) => {
|
|
24
|
+
const { bucketName, basePath, getDownloadUrl, signedUrlExpiresIn = DEFAULT_SIGNED_LIFETIME_SECONDS, ...clientConfig } = config;
|
|
25
|
+
if (!Number.isFinite(signedUrlExpiresIn) || signedUrlExpiresIn <= 0 || signedUrlExpiresIn > MAX_SIGNED_LIFETIME_SECONDS) throw new Error(`${name} signedUrlExpiresIn must be between 1 and ${MAX_SIGNED_LIFETIME_SECONDS} seconds (seven days).`);
|
|
26
|
+
const client = new _aws_sdk_client_s3.S3Client(clientConfig);
|
|
27
|
+
const normalizedBasePath = trimSlashes(basePath ?? "");
|
|
28
|
+
const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(normalizedBasePath);
|
|
29
|
+
const getListPrefix = (prefix = "") => {
|
|
30
|
+
const value = [normalizedBasePath, trimSlashes(prefix)].filter(Boolean).join("/");
|
|
31
|
+
return value ? `${value}/` : "";
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The URI `put` returns for the key, or null for a key no `put` writes,
|
|
35
|
+
* such as a folder marker that ends in "/".
|
|
36
|
+
*/
|
|
37
|
+
const getListedStorageUri = (key) => {
|
|
38
|
+
try {
|
|
39
|
+
return (0, _hot_updater_plugin_core.createStorageUri)({
|
|
40
|
+
bucket: bucketName,
|
|
41
|
+
key,
|
|
42
|
+
protocol
|
|
43
|
+
});
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const getRelativeKey = (key) => {
|
|
49
|
+
if (!normalizedBasePath) return key;
|
|
50
|
+
const basePrefix = `${normalizedBasePath}/`;
|
|
51
|
+
return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
|
|
52
|
+
};
|
|
53
|
+
const parseAndValidate = (storageUri) => {
|
|
54
|
+
const parsed = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, protocol);
|
|
55
|
+
if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
|
|
56
|
+
return parsed;
|
|
57
|
+
};
|
|
58
|
+
return (0, _hot_updater_plugin_core.createStorageAdapter)({
|
|
59
|
+
name,
|
|
60
|
+
protocol,
|
|
61
|
+
async put({ key, body, contentLength, contentType }) {
|
|
62
|
+
const storageKey = getStorageKey(key);
|
|
63
|
+
await new _aws_sdk_lib_storage.Upload({
|
|
64
|
+
client,
|
|
65
|
+
params: {
|
|
66
|
+
Body: body,
|
|
67
|
+
Bucket: bucketName,
|
|
68
|
+
CacheControl: "max-age=31536000",
|
|
69
|
+
...contentLength === void 0 ? {} : { ContentLength: contentLength },
|
|
70
|
+
ContentType: contentType,
|
|
71
|
+
Key: storageKey
|
|
72
|
+
}
|
|
73
|
+
}).done();
|
|
74
|
+
return { storageUri: (0, _hot_updater_plugin_core.createStorageUri)({
|
|
75
|
+
bucket: bucketName,
|
|
76
|
+
key: storageKey,
|
|
77
|
+
protocol
|
|
78
|
+
}) };
|
|
79
|
+
},
|
|
80
|
+
async get({ storageUri }) {
|
|
81
|
+
const { key } = parseAndValidate(storageUri);
|
|
82
|
+
try {
|
|
83
|
+
const response = await client.send(new _aws_sdk_client_s3.GetObjectCommand({
|
|
84
|
+
Bucket: bucketName,
|
|
85
|
+
Key: key
|
|
86
|
+
}));
|
|
87
|
+
if (!response.Body) return { response: null };
|
|
88
|
+
const headers = new Headers();
|
|
89
|
+
if (response.ContentType) headers.set("content-type", response.ContentType);
|
|
90
|
+
if (response.ContentLength !== void 0) headers.set("content-length", String(response.ContentLength));
|
|
91
|
+
return { response: new Response(response.Body.transformToWebStream(), { headers }) };
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (isObjectNotFoundError(error)) return { response: null };
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
async getDownloadUrl({ storageUri }) {
|
|
98
|
+
const { key } = parseAndValidate(storageUri);
|
|
99
|
+
if (getDownloadUrl) return getDownloadUrl({ storageUri });
|
|
100
|
+
return { url: await (0, _aws_sdk_s3_request_presigner.getSignedUrl)(client, new _aws_sdk_client_s3.GetObjectCommand({
|
|
101
|
+
Bucket: bucketName,
|
|
102
|
+
Key: key
|
|
103
|
+
}), { expiresIn: signedUrlExpiresIn }) };
|
|
104
|
+
},
|
|
105
|
+
async exists({ storageUri }) {
|
|
106
|
+
const { key } = parseAndValidate(storageUri);
|
|
107
|
+
try {
|
|
108
|
+
await client.send(new _aws_sdk_client_s3.HeadObjectCommand({
|
|
109
|
+
Bucket: bucketName,
|
|
110
|
+
Key: key
|
|
111
|
+
}));
|
|
112
|
+
return { exists: true };
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (isObjectNotFoundError(error)) return { exists: false };
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
async delete({ storageUri }) {
|
|
119
|
+
const { key } = parseAndValidate(storageUri);
|
|
120
|
+
await client.send(new _aws_sdk_client_s3.DeleteObjectCommand({
|
|
121
|
+
Bucket: bucketName,
|
|
122
|
+
Key: key
|
|
123
|
+
}));
|
|
124
|
+
return { deleted: true };
|
|
125
|
+
},
|
|
126
|
+
async listObjects(prefix) {
|
|
127
|
+
const objects = [];
|
|
128
|
+
let continuationToken;
|
|
129
|
+
do {
|
|
130
|
+
const response = await client.send(new _aws_sdk_client_s3.ListObjectsV2Command({
|
|
131
|
+
Bucket: bucketName,
|
|
132
|
+
ContinuationToken: continuationToken,
|
|
133
|
+
Prefix: getListPrefix(prefix)
|
|
134
|
+
}));
|
|
135
|
+
for (const object of response.Contents ?? []) {
|
|
136
|
+
if (!object.Key) continue;
|
|
137
|
+
const storageUri = getListedStorageUri(object.Key);
|
|
138
|
+
if (storageUri === null) continue;
|
|
139
|
+
objects.push({
|
|
140
|
+
key: getRelativeKey(object.Key),
|
|
141
|
+
lastModifiedAt: object.LastModified,
|
|
142
|
+
size: object.Size ?? 0,
|
|
143
|
+
storageUri
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
continuationToken = response.NextContinuationToken;
|
|
147
|
+
} while (continuationToken);
|
|
148
|
+
return objects;
|
|
149
|
+
},
|
|
150
|
+
async deleteObjects(relativeKeys) {
|
|
151
|
+
const keys = relativeKeys.map((key) => getStorageKey(key));
|
|
152
|
+
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_SIZE) {
|
|
153
|
+
const batch = keys.slice(offset, offset + DELETE_BATCH_SIZE);
|
|
154
|
+
const response = await client.send(new _aws_sdk_client_s3.DeleteObjectsCommand({
|
|
155
|
+
Bucket: bucketName,
|
|
156
|
+
Delete: {
|
|
157
|
+
Objects: batch.map((Key) => ({ Key })),
|
|
158
|
+
Quiet: true
|
|
159
|
+
}
|
|
160
|
+
}));
|
|
161
|
+
if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s) from bucket "${bucketName}".`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
//#endregion
|
|
167
|
+
exports.createS3Storage = createS3Storage;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { S3ClientConfig } from "@aws-sdk/client-s3";
|
|
2
|
+
import { StorageAdapter, StorageAdapterWith } from "@hot-updater/plugin-core";
|
|
3
|
+
//#region src/createS3Storage.d.ts
|
|
4
|
+
/** Who the adapter is: its name and the scheme of the URIs it writes. */
|
|
5
|
+
interface S3StorageIdentity {
|
|
6
|
+
/** The adapter's name, such as `s3Storage`. */
|
|
7
|
+
name: string;
|
|
8
|
+
/** The scheme of the storage URIs it writes, such as `s3`. */
|
|
9
|
+
protocol: string;
|
|
10
|
+
}
|
|
11
|
+
interface S3StorageConfig extends S3ClientConfig {
|
|
12
|
+
bucketName: string;
|
|
13
|
+
/** Base path where bundles will be stored in the bucket. */
|
|
14
|
+
basePath?: string;
|
|
15
|
+
/**
|
|
16
|
+
* The URL devices download an object from, such as a CDN's. Without it, a
|
|
17
|
+
* presigned URL that expires after `signedUrlExpiresIn`.
|
|
18
|
+
*/
|
|
19
|
+
getDownloadUrl?: StorageAdapter["getDownloadUrl"];
|
|
20
|
+
/**
|
|
21
|
+
* Lifetime of the presigned download URLs in seconds, at most seven days,
|
|
22
|
+
* the SigV4 limit. @default 3600
|
|
23
|
+
*/
|
|
24
|
+
signedUrlExpiresIn?: number;
|
|
25
|
+
}
|
|
26
|
+
type S3StorageOperations = "put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects";
|
|
27
|
+
/**
|
|
28
|
+
* A storage adapter over a bucket's S3 API, which AWS S3, Cloudflare R2, and
|
|
29
|
+
* Neon Object Storage all serve. Each provider package passes its own name,
|
|
30
|
+
* URI protocol, and client defaults.
|
|
31
|
+
*/
|
|
32
|
+
declare const createS3Storage: ({ name, protocol }: S3StorageIdentity, config: S3StorageConfig) => StorageAdapterWith<S3StorageOperations>;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { type S3ClientConfig, S3StorageConfig, S3StorageIdentity, S3StorageOperations, createS3Storage };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { S3ClientConfig } from "@aws-sdk/client-s3";
|
|
2
|
+
import { StorageAdapter, StorageAdapterWith } from "@hot-updater/plugin-core";
|
|
3
|
+
//#region src/createS3Storage.d.ts
|
|
4
|
+
/** Who the adapter is: its name and the scheme of the URIs it writes. */
|
|
5
|
+
interface S3StorageIdentity {
|
|
6
|
+
/** The adapter's name, such as `s3Storage`. */
|
|
7
|
+
name: string;
|
|
8
|
+
/** The scheme of the storage URIs it writes, such as `s3`. */
|
|
9
|
+
protocol: string;
|
|
10
|
+
}
|
|
11
|
+
interface S3StorageConfig extends S3ClientConfig {
|
|
12
|
+
bucketName: string;
|
|
13
|
+
/** Base path where bundles will be stored in the bucket. */
|
|
14
|
+
basePath?: string;
|
|
15
|
+
/**
|
|
16
|
+
* The URL devices download an object from, such as a CDN's. Without it, a
|
|
17
|
+
* presigned URL that expires after `signedUrlExpiresIn`.
|
|
18
|
+
*/
|
|
19
|
+
getDownloadUrl?: StorageAdapter["getDownloadUrl"];
|
|
20
|
+
/**
|
|
21
|
+
* Lifetime of the presigned download URLs in seconds, at most seven days,
|
|
22
|
+
* the SigV4 limit. @default 3600
|
|
23
|
+
*/
|
|
24
|
+
signedUrlExpiresIn?: number;
|
|
25
|
+
}
|
|
26
|
+
type S3StorageOperations = "put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects";
|
|
27
|
+
/**
|
|
28
|
+
* A storage adapter over a bucket's S3 API, which AWS S3, Cloudflare R2, and
|
|
29
|
+
* Neon Object Storage all serve. Each provider package passes its own name,
|
|
30
|
+
* URI protocol, and client defaults.
|
|
31
|
+
*/
|
|
32
|
+
declare const createS3Storage: ({ name, protocol }: S3StorageIdentity, config: S3StorageConfig) => StorageAdapterWith<S3StorageOperations>;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { type S3ClientConfig, S3StorageConfig, S3StorageIdentity, S3StorageOperations, createS3Storage };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
import { Upload } from "@aws-sdk/lib-storage";
|
|
3
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
4
|
+
import { createStorageAdapter, createStorageKeyBuilder, createStorageUri, parseStorageUri } from "@hot-updater/plugin-core";
|
|
5
|
+
//#region src/createS3Storage.ts
|
|
6
|
+
/** Devices download right after the update check that returns the URL. */
|
|
7
|
+
const DEFAULT_SIGNED_LIFETIME_SECONDS = 3600;
|
|
8
|
+
/** SigV4's limit on a presigned URL. */
|
|
9
|
+
const MAX_SIGNED_LIFETIME_SECONDS = 604800;
|
|
10
|
+
/** DeleteObjects takes at most 1000 keys a request. */
|
|
11
|
+
const DELETE_BATCH_SIZE = 1e3;
|
|
12
|
+
const isObjectNotFoundError = (error) => {
|
|
13
|
+
if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey")) return true;
|
|
14
|
+
return typeof error === "object" && error !== null && "$metadata" in error && error.$metadata?.httpStatusCode === 404;
|
|
15
|
+
};
|
|
16
|
+
const trimSlashes = (value) => value.replace(/^\/+|\/+$/g, "");
|
|
17
|
+
/**
|
|
18
|
+
* A storage adapter over a bucket's S3 API, which AWS S3, Cloudflare R2, and
|
|
19
|
+
* Neon Object Storage all serve. Each provider package passes its own name,
|
|
20
|
+
* URI protocol, and client defaults.
|
|
21
|
+
*/
|
|
22
|
+
const createS3Storage = ({ name, protocol }, config) => {
|
|
23
|
+
const { bucketName, basePath, getDownloadUrl, signedUrlExpiresIn = DEFAULT_SIGNED_LIFETIME_SECONDS, ...clientConfig } = config;
|
|
24
|
+
if (!Number.isFinite(signedUrlExpiresIn) || signedUrlExpiresIn <= 0 || signedUrlExpiresIn > MAX_SIGNED_LIFETIME_SECONDS) throw new Error(`${name} signedUrlExpiresIn must be between 1 and ${MAX_SIGNED_LIFETIME_SECONDS} seconds (seven days).`);
|
|
25
|
+
const client = new S3Client(clientConfig);
|
|
26
|
+
const normalizedBasePath = trimSlashes(basePath ?? "");
|
|
27
|
+
const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
|
|
28
|
+
const getListPrefix = (prefix = "") => {
|
|
29
|
+
const value = [normalizedBasePath, trimSlashes(prefix)].filter(Boolean).join("/");
|
|
30
|
+
return value ? `${value}/` : "";
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The URI `put` returns for the key, or null for a key no `put` writes,
|
|
34
|
+
* such as a folder marker that ends in "/".
|
|
35
|
+
*/
|
|
36
|
+
const getListedStorageUri = (key) => {
|
|
37
|
+
try {
|
|
38
|
+
return createStorageUri({
|
|
39
|
+
bucket: bucketName,
|
|
40
|
+
key,
|
|
41
|
+
protocol
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const getRelativeKey = (key) => {
|
|
48
|
+
if (!normalizedBasePath) return key;
|
|
49
|
+
const basePrefix = `${normalizedBasePath}/`;
|
|
50
|
+
return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
|
|
51
|
+
};
|
|
52
|
+
const parseAndValidate = (storageUri) => {
|
|
53
|
+
const parsed = parseStorageUri(storageUri, protocol);
|
|
54
|
+
if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
|
|
55
|
+
return parsed;
|
|
56
|
+
};
|
|
57
|
+
return createStorageAdapter({
|
|
58
|
+
name,
|
|
59
|
+
protocol,
|
|
60
|
+
async put({ key, body, contentLength, contentType }) {
|
|
61
|
+
const storageKey = getStorageKey(key);
|
|
62
|
+
await new Upload({
|
|
63
|
+
client,
|
|
64
|
+
params: {
|
|
65
|
+
Body: body,
|
|
66
|
+
Bucket: bucketName,
|
|
67
|
+
CacheControl: "max-age=31536000",
|
|
68
|
+
...contentLength === void 0 ? {} : { ContentLength: contentLength },
|
|
69
|
+
ContentType: contentType,
|
|
70
|
+
Key: storageKey
|
|
71
|
+
}
|
|
72
|
+
}).done();
|
|
73
|
+
return { storageUri: createStorageUri({
|
|
74
|
+
bucket: bucketName,
|
|
75
|
+
key: storageKey,
|
|
76
|
+
protocol
|
|
77
|
+
}) };
|
|
78
|
+
},
|
|
79
|
+
async get({ storageUri }) {
|
|
80
|
+
const { key } = parseAndValidate(storageUri);
|
|
81
|
+
try {
|
|
82
|
+
const response = await client.send(new GetObjectCommand({
|
|
83
|
+
Bucket: bucketName,
|
|
84
|
+
Key: key
|
|
85
|
+
}));
|
|
86
|
+
if (!response.Body) return { response: null };
|
|
87
|
+
const headers = new Headers();
|
|
88
|
+
if (response.ContentType) headers.set("content-type", response.ContentType);
|
|
89
|
+
if (response.ContentLength !== void 0) headers.set("content-length", String(response.ContentLength));
|
|
90
|
+
return { response: new Response(response.Body.transformToWebStream(), { headers }) };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (isObjectNotFoundError(error)) return { response: null };
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
async getDownloadUrl({ storageUri }) {
|
|
97
|
+
const { key } = parseAndValidate(storageUri);
|
|
98
|
+
if (getDownloadUrl) return getDownloadUrl({ storageUri });
|
|
99
|
+
return { url: await getSignedUrl(client, new GetObjectCommand({
|
|
100
|
+
Bucket: bucketName,
|
|
101
|
+
Key: key
|
|
102
|
+
}), { expiresIn: signedUrlExpiresIn }) };
|
|
103
|
+
},
|
|
104
|
+
async exists({ storageUri }) {
|
|
105
|
+
const { key } = parseAndValidate(storageUri);
|
|
106
|
+
try {
|
|
107
|
+
await client.send(new HeadObjectCommand({
|
|
108
|
+
Bucket: bucketName,
|
|
109
|
+
Key: key
|
|
110
|
+
}));
|
|
111
|
+
return { exists: true };
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (isObjectNotFoundError(error)) return { exists: false };
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
async delete({ storageUri }) {
|
|
118
|
+
const { key } = parseAndValidate(storageUri);
|
|
119
|
+
await client.send(new DeleteObjectCommand({
|
|
120
|
+
Bucket: bucketName,
|
|
121
|
+
Key: key
|
|
122
|
+
}));
|
|
123
|
+
return { deleted: true };
|
|
124
|
+
},
|
|
125
|
+
async listObjects(prefix) {
|
|
126
|
+
const objects = [];
|
|
127
|
+
let continuationToken;
|
|
128
|
+
do {
|
|
129
|
+
const response = await client.send(new ListObjectsV2Command({
|
|
130
|
+
Bucket: bucketName,
|
|
131
|
+
ContinuationToken: continuationToken,
|
|
132
|
+
Prefix: getListPrefix(prefix)
|
|
133
|
+
}));
|
|
134
|
+
for (const object of response.Contents ?? []) {
|
|
135
|
+
if (!object.Key) continue;
|
|
136
|
+
const storageUri = getListedStorageUri(object.Key);
|
|
137
|
+
if (storageUri === null) continue;
|
|
138
|
+
objects.push({
|
|
139
|
+
key: getRelativeKey(object.Key),
|
|
140
|
+
lastModifiedAt: object.LastModified,
|
|
141
|
+
size: object.Size ?? 0,
|
|
142
|
+
storageUri
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
continuationToken = response.NextContinuationToken;
|
|
146
|
+
} while (continuationToken);
|
|
147
|
+
return objects;
|
|
148
|
+
},
|
|
149
|
+
async deleteObjects(relativeKeys) {
|
|
150
|
+
const keys = relativeKeys.map((key) => getStorageKey(key));
|
|
151
|
+
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_SIZE) {
|
|
152
|
+
const batch = keys.slice(offset, offset + DELETE_BATCH_SIZE);
|
|
153
|
+
const response = await client.send(new DeleteObjectsCommand({
|
|
154
|
+
Bucket: bucketName,
|
|
155
|
+
Delete: {
|
|
156
|
+
Objects: batch.map((Key) => ({ Key })),
|
|
157
|
+
Quiet: true
|
|
158
|
+
}
|
|
159
|
+
}));
|
|
160
|
+
if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s) from bucket "${bucketName}".`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
//#endregion
|
|
166
|
+
export { createS3Storage };
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/storage-s3",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.1.4",
|
|
5
|
+
"description": "React Native OTA solution for self-hosted",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.mjs",
|
|
8
|
+
"types": "dist/index.d.cts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/gronxb/hot-updater"
|
|
20
|
+
},
|
|
21
|
+
"author": "gronxb <gron1gh1@gmail.com> (https://github.com/gronxb)",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/gronxb/hot-updater/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/gronxb/hot-updater#readme",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"!**/*.spec.ts",
|
|
32
|
+
"package.json"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@aws-sdk/client-s3": "3.1066.0",
|
|
36
|
+
"@aws-sdk/lib-storage": "3.1066.0",
|
|
37
|
+
"@aws-sdk/s3-request-presigner": "3.1066.0",
|
|
38
|
+
"@hot-updater/plugin-core": "1.1.4"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^20",
|
|
42
|
+
"vitest": "4.1.4",
|
|
43
|
+
"@hot-updater/test-utils": "1.1.4"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsdown",
|
|
47
|
+
"test:type": "tsc --noEmit"
|
|
48
|
+
}
|
|
6
49
|
}
|
package/README.md
DELETED