@oino-ts/blob-aws 1.0.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/dist/cjs/OINOBlobAwsS3.js +255 -0
- package/dist/cjs/index.js +5 -0
- package/dist/esm/OINOBlobAwsS3.js +251 -0
- package/dist/esm/index.js +1 -0
- package/dist/types/OINOBlobAwsS3.d.ts +90 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +40 -0
- package/src/OINOBlobAwsS3.ts +277 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.OINOBlobAwsS3 = void 0;
|
|
9
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
10
|
+
const common_1 = require("@oino-ts/common");
|
|
11
|
+
const blob_1 = require("@oino-ts/blob");
|
|
12
|
+
/**
|
|
13
|
+
* AWS S3 (and S3-compatible) implementation of `OINOBlob`.
|
|
14
|
+
*
|
|
15
|
+
* Authenticates using static access key credentials supplied via a JSON-encoded
|
|
16
|
+
* connection string. Connection parameters map as:
|
|
17
|
+
* - `params.url` → optional custom endpoint, e.g. `https://s3.eu-west-1.amazonaws.com`
|
|
18
|
+
* or a compatible service such as MinIO / Cloudflare R2
|
|
19
|
+
* - `params.container` → S3 bucket name
|
|
20
|
+
* - `params.connectionStr` → JSON string: `{"region":"…","accessKeyId":"…","secretAccessKey":"…"}`
|
|
21
|
+
*
|
|
22
|
+
* Register and use via the factory:
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { OINOBlobFactory } from "@oino-ts/blob"
|
|
25
|
+
* import { OINOBlobAwsS3 } from "@oino-ts/blob-aws"
|
|
26
|
+
*
|
|
27
|
+
* OINOBlobFactory.registerBlob("OINOBlobAwsS3", OINOBlobAwsS3)
|
|
28
|
+
*
|
|
29
|
+
* const blob = await OINOBlobFactory.createBlob({
|
|
30
|
+
* type: "OINOBlobAwsS3",
|
|
31
|
+
* url: "", // leave empty for default AWS endpoint
|
|
32
|
+
* container: "my-bucket",
|
|
33
|
+
* connectionStr: JSON.stringify({
|
|
34
|
+
* region: "us-east-1",
|
|
35
|
+
* accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
36
|
+
* secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
|
37
|
+
* })
|
|
38
|
+
* })
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
class OINOBlobAwsS3 extends blob_1.OINOBlob {
|
|
42
|
+
_s3Client = null;
|
|
43
|
+
// ── OINODataSource lifecycle ──────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Initialise the AWS SDK S3 client from the JSON-encoded `connectionStr`.
|
|
46
|
+
* Does not perform any network call.
|
|
47
|
+
*/
|
|
48
|
+
async connect() {
|
|
49
|
+
if (!this.blobParams.connectionStr) {
|
|
50
|
+
return new common_1.OINOResult({
|
|
51
|
+
success: false,
|
|
52
|
+
status: 400,
|
|
53
|
+
statusText: "OINOBlobAwsS3: params.connectionStr is required (JSON with region, accessKeyId, secretAccessKey)"
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
let creds;
|
|
57
|
+
try {
|
|
58
|
+
creds = JSON.parse(this.blobParams.connectionStr);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return new common_1.OINOResult({
|
|
62
|
+
success: false,
|
|
63
|
+
status: 400,
|
|
64
|
+
statusText: "OINOBlobAwsS3: params.connectionStr must be a valid JSON string"
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (!creds.region || !creds.accessKeyId || !creds.secretAccessKey) {
|
|
68
|
+
return new common_1.OINOResult({
|
|
69
|
+
success: false,
|
|
70
|
+
status: 400,
|
|
71
|
+
statusText: "OINOBlobAwsS3: connectionStr must include region, accessKeyId and secretAccessKey"
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const clientConfig = {
|
|
76
|
+
region: creds.region,
|
|
77
|
+
credentials: {
|
|
78
|
+
accessKeyId: creds.accessKeyId,
|
|
79
|
+
secretAccessKey: creds.secretAccessKey
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
if (this.blobParams.url) {
|
|
83
|
+
clientConfig.endpoint = this.blobParams.url;
|
|
84
|
+
clientConfig.forcePathStyle = true;
|
|
85
|
+
}
|
|
86
|
+
this._s3Client = new client_s3_1.S3Client(clientConfig);
|
|
87
|
+
this.isConnected = true;
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
return new common_1.OINOResult({ success: false, status: 500, statusText: "OINOBlobAwsS3 connect failed: " + e.message });
|
|
91
|
+
}
|
|
92
|
+
return new common_1.OINOResult();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Verify that the target bucket exists and is accessible using a `HeadBucket` call.
|
|
96
|
+
*/
|
|
97
|
+
async validate() {
|
|
98
|
+
if (!this._s3Client) {
|
|
99
|
+
return new common_1.OINOResult({ success: false, status: 500, statusText: "OINOBlobAwsS3: not connected" });
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
await this._s3Client.send(new client_s3_1.HeadBucketCommand({ Bucket: this.blobParams.container }));
|
|
103
|
+
this.isValidated = true;
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
console.error("OINOBlobAwsS3 validate error:", e);
|
|
107
|
+
const status = e.$metadata?.httpStatusCode ?? 500;
|
|
108
|
+
if (status === 404) {
|
|
109
|
+
return new common_1.OINOResult({
|
|
110
|
+
success: false,
|
|
111
|
+
status: 404,
|
|
112
|
+
statusText: "OINOBlobAwsS3: bucket '" + this.blobParams.container + "' not found"
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
else if (status === 403) {
|
|
116
|
+
return new common_1.OINOResult({
|
|
117
|
+
success: false,
|
|
118
|
+
status: 403,
|
|
119
|
+
statusText: "OINOBlobAwsS3: access to bucket '" + this.blobParams.container + "' forbidden (check credentials and permissions)"
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
return new common_1.OINOResult({ success: false, status: 500, statusText: "OINOBlobAwsS3 validate failed: " + e.message });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return new common_1.OINOResult();
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Release the S3 client (destroys the underlying HTTP connection pool).
|
|
130
|
+
*/
|
|
131
|
+
async disconnect() {
|
|
132
|
+
this._s3Client?.destroy();
|
|
133
|
+
this._s3Client = null;
|
|
134
|
+
this.isConnected = false;
|
|
135
|
+
this.isValidated = false;
|
|
136
|
+
}
|
|
137
|
+
// ── OINOBlob operations ───────────────────────────────────────────────
|
|
138
|
+
/**
|
|
139
|
+
* List all objects in the bucket, applying a server-side S3 `Prefix` filter
|
|
140
|
+
* where possible and in-memory result filtering for all other predicates.
|
|
141
|
+
*
|
|
142
|
+
* - The `name` field supports server-side prefix filtering via `ListObjectsV2`
|
|
143
|
+
* `Prefix` option (query filtering).
|
|
144
|
+
* - All other field predicates (`etag`, `lastModified`, `contentLength`,
|
|
145
|
+
* `contentType`) are evaluated in-memory after the listing (result filtering).
|
|
146
|
+
* Note: S3 listing does not return `contentType`; it defaults to
|
|
147
|
+
* `"application/octet-stream"` unless a `contentType` filter is applied.
|
|
148
|
+
*
|
|
149
|
+
* @param filter optional query filter to apply
|
|
150
|
+
*/
|
|
151
|
+
async listEntries(filter) {
|
|
152
|
+
if (!this._s3Client) {
|
|
153
|
+
throw new Error("OINOBlobAwsS3: not connected");
|
|
154
|
+
}
|
|
155
|
+
const queryPrefix = (filter && !filter.isEmpty())
|
|
156
|
+
? blob_1.OINOBlob.extractNamePrefix(filter)
|
|
157
|
+
: undefined;
|
|
158
|
+
const entries = [];
|
|
159
|
+
let continuationToken;
|
|
160
|
+
do {
|
|
161
|
+
const response = await this._s3Client.send(new client_s3_1.ListObjectsV2Command({
|
|
162
|
+
Bucket: this.blobParams.container,
|
|
163
|
+
Prefix: queryPrefix,
|
|
164
|
+
ContinuationToken: continuationToken
|
|
165
|
+
}));
|
|
166
|
+
for (const obj of response.Contents ?? []) {
|
|
167
|
+
entries.push({
|
|
168
|
+
name: obj.Key ?? "",
|
|
169
|
+
etag: (obj.ETag ?? "").replace(/^"|"$/g, ""),
|
|
170
|
+
lastModified: obj.LastModified ?? new Date(0),
|
|
171
|
+
contentLength: obj.Size ?? 0,
|
|
172
|
+
contentType: "application/octet-stream" // S3 does not return content type in listing, default to binary
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined;
|
|
176
|
+
} while (continuationToken);
|
|
177
|
+
if (!filter || filter.isEmpty()) {
|
|
178
|
+
return entries;
|
|
179
|
+
}
|
|
180
|
+
return entries.filter(e => blob_1.OINOBlob.matchesEntry(e, filter));
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Download the raw content of a named object.
|
|
184
|
+
*
|
|
185
|
+
* @param name full object key (path within the bucket)
|
|
186
|
+
*/
|
|
187
|
+
async fetchEntry(name) {
|
|
188
|
+
if (!this._s3Client) {
|
|
189
|
+
throw new Error("OINOBlobAwsS3: not connected");
|
|
190
|
+
}
|
|
191
|
+
const response = await this._s3Client.send(new client_s3_1.GetObjectCommand({
|
|
192
|
+
Bucket: this.blobParams.container,
|
|
193
|
+
Key: name
|
|
194
|
+
}));
|
|
195
|
+
const contentType = response.ContentType ?? "application/octet-stream";
|
|
196
|
+
if (!response.Body) {
|
|
197
|
+
throw new Error("OINOBlobAwsS3: no body returned for object '" + name + "'");
|
|
198
|
+
}
|
|
199
|
+
const content = await response.Body.transformToByteArray();
|
|
200
|
+
return { content, contentType };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Upload (create or replace) an object with the given binary content.
|
|
204
|
+
*
|
|
205
|
+
* @param name full object key (path within the bucket)
|
|
206
|
+
* @param content binary content to store
|
|
207
|
+
* @param contentType MIME type of the content (e.g. `"image/jpeg"`)
|
|
208
|
+
*/
|
|
209
|
+
async uploadEntry(name, content, contentType) {
|
|
210
|
+
if (!this._s3Client) {
|
|
211
|
+
throw new Error("OINOBlobAwsS3: not connected");
|
|
212
|
+
}
|
|
213
|
+
await this._s3Client.send(new client_s3_1.PutObjectCommand({
|
|
214
|
+
Bucket: this.blobParams.container,
|
|
215
|
+
Key: name,
|
|
216
|
+
Body: content,
|
|
217
|
+
ContentType: contentType
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Delete a named object.
|
|
222
|
+
*
|
|
223
|
+
* @param name full object key (path within the bucket)
|
|
224
|
+
*/
|
|
225
|
+
async deleteEntry(name) {
|
|
226
|
+
if (!this._s3Client) {
|
|
227
|
+
throw new Error("OINOBlobAwsS3: not connected");
|
|
228
|
+
}
|
|
229
|
+
await this._s3Client.send(new client_s3_1.DeleteObjectCommand({
|
|
230
|
+
Bucket: this.blobParams.container,
|
|
231
|
+
Key: name
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
// ── OINODataSource datamodel initialisation ───────────────────────────
|
|
235
|
+
/**
|
|
236
|
+
* Attach a static `OINOBlobDataModel` to the given API, adding only the
|
|
237
|
+
* four fields that S3 object listings return (`contentType` is omitted
|
|
238
|
+
* because S3 does not include it in listing responses).
|
|
239
|
+
*
|
|
240
|
+
* @param api the `OINOBlobApi` whose data model is to be initialised
|
|
241
|
+
*/
|
|
242
|
+
async initializeApiDatamodel(api) {
|
|
243
|
+
const blobApi = api;
|
|
244
|
+
const datamodel = new blob_1.OINOBlobDataModel(blobApi);
|
|
245
|
+
const ds = this;
|
|
246
|
+
const FIELD = { isPrimaryKey: false, isForeignKey: false, isAutoInc: false, isNotNull: false };
|
|
247
|
+
const PK = { isPrimaryKey: true, isForeignKey: false, isAutoInc: false, isNotNull: true };
|
|
248
|
+
datamodel.addField(new common_1.OINOStringDataField(ds, "name", "TEXT", PK, 1024));
|
|
249
|
+
datamodel.addField(new common_1.OINOStringDataField(ds, "etag", "TEXT", FIELD, 256));
|
|
250
|
+
datamodel.addField(new common_1.OINODatetimeDataField(ds, "lastModified", "DATETIME", FIELD));
|
|
251
|
+
datamodel.addField(new common_1.OINONumberDataField(ds, "contentLength", "INTEGER", FIELD));
|
|
252
|
+
blobApi.initializeDatamodel(datamodel);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
exports.OINOBlobAwsS3 = OINOBlobAwsS3;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OINOBlobAwsS3 = void 0;
|
|
4
|
+
var OINOBlobAwsS3_js_1 = require("./OINOBlobAwsS3.js");
|
|
5
|
+
Object.defineProperty(exports, "OINOBlobAwsS3", { enumerable: true, get: function () { return OINOBlobAwsS3_js_1.OINOBlobAwsS3; } });
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
*/
|
|
6
|
+
import { S3Client, HeadBucketCommand, ListObjectsV2Command, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
|
7
|
+
import { OINOResult, OINOStringDataField, OINONumberDataField, OINODatetimeDataField } from "@oino-ts/common";
|
|
8
|
+
import { OINOBlob, OINOBlobDataModel } from "@oino-ts/blob";
|
|
9
|
+
/**
|
|
10
|
+
* AWS S3 (and S3-compatible) implementation of `OINOBlob`.
|
|
11
|
+
*
|
|
12
|
+
* Authenticates using static access key credentials supplied via a JSON-encoded
|
|
13
|
+
* connection string. Connection parameters map as:
|
|
14
|
+
* - `params.url` → optional custom endpoint, e.g. `https://s3.eu-west-1.amazonaws.com`
|
|
15
|
+
* or a compatible service such as MinIO / Cloudflare R2
|
|
16
|
+
* - `params.container` → S3 bucket name
|
|
17
|
+
* - `params.connectionStr` → JSON string: `{"region":"…","accessKeyId":"…","secretAccessKey":"…"}`
|
|
18
|
+
*
|
|
19
|
+
* Register and use via the factory:
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { OINOBlobFactory } from "@oino-ts/blob"
|
|
22
|
+
* import { OINOBlobAwsS3 } from "@oino-ts/blob-aws"
|
|
23
|
+
*
|
|
24
|
+
* OINOBlobFactory.registerBlob("OINOBlobAwsS3", OINOBlobAwsS3)
|
|
25
|
+
*
|
|
26
|
+
* const blob = await OINOBlobFactory.createBlob({
|
|
27
|
+
* type: "OINOBlobAwsS3",
|
|
28
|
+
* url: "", // leave empty for default AWS endpoint
|
|
29
|
+
* container: "my-bucket",
|
|
30
|
+
* connectionStr: JSON.stringify({
|
|
31
|
+
* region: "us-east-1",
|
|
32
|
+
* accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
33
|
+
* secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
|
34
|
+
* })
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export class OINOBlobAwsS3 extends OINOBlob {
|
|
39
|
+
_s3Client = null;
|
|
40
|
+
// ── OINODataSource lifecycle ──────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Initialise the AWS SDK S3 client from the JSON-encoded `connectionStr`.
|
|
43
|
+
* Does not perform any network call.
|
|
44
|
+
*/
|
|
45
|
+
async connect() {
|
|
46
|
+
if (!this.blobParams.connectionStr) {
|
|
47
|
+
return new OINOResult({
|
|
48
|
+
success: false,
|
|
49
|
+
status: 400,
|
|
50
|
+
statusText: "OINOBlobAwsS3: params.connectionStr is required (JSON with region, accessKeyId, secretAccessKey)"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
let creds;
|
|
54
|
+
try {
|
|
55
|
+
creds = JSON.parse(this.blobParams.connectionStr);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return new OINOResult({
|
|
59
|
+
success: false,
|
|
60
|
+
status: 400,
|
|
61
|
+
statusText: "OINOBlobAwsS3: params.connectionStr must be a valid JSON string"
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (!creds.region || !creds.accessKeyId || !creds.secretAccessKey) {
|
|
65
|
+
return new OINOResult({
|
|
66
|
+
success: false,
|
|
67
|
+
status: 400,
|
|
68
|
+
statusText: "OINOBlobAwsS3: connectionStr must include region, accessKeyId and secretAccessKey"
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const clientConfig = {
|
|
73
|
+
region: creds.region,
|
|
74
|
+
credentials: {
|
|
75
|
+
accessKeyId: creds.accessKeyId,
|
|
76
|
+
secretAccessKey: creds.secretAccessKey
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
if (this.blobParams.url) {
|
|
80
|
+
clientConfig.endpoint = this.blobParams.url;
|
|
81
|
+
clientConfig.forcePathStyle = true;
|
|
82
|
+
}
|
|
83
|
+
this._s3Client = new S3Client(clientConfig);
|
|
84
|
+
this.isConnected = true;
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
return new OINOResult({ success: false, status: 500, statusText: "OINOBlobAwsS3 connect failed: " + e.message });
|
|
88
|
+
}
|
|
89
|
+
return new OINOResult();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Verify that the target bucket exists and is accessible using a `HeadBucket` call.
|
|
93
|
+
*/
|
|
94
|
+
async validate() {
|
|
95
|
+
if (!this._s3Client) {
|
|
96
|
+
return new OINOResult({ success: false, status: 500, statusText: "OINOBlobAwsS3: not connected" });
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
await this._s3Client.send(new HeadBucketCommand({ Bucket: this.blobParams.container }));
|
|
100
|
+
this.isValidated = true;
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
console.error("OINOBlobAwsS3 validate error:", e);
|
|
104
|
+
const status = e.$metadata?.httpStatusCode ?? 500;
|
|
105
|
+
if (status === 404) {
|
|
106
|
+
return new OINOResult({
|
|
107
|
+
success: false,
|
|
108
|
+
status: 404,
|
|
109
|
+
statusText: "OINOBlobAwsS3: bucket '" + this.blobParams.container + "' not found"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else if (status === 403) {
|
|
113
|
+
return new OINOResult({
|
|
114
|
+
success: false,
|
|
115
|
+
status: 403,
|
|
116
|
+
statusText: "OINOBlobAwsS3: access to bucket '" + this.blobParams.container + "' forbidden (check credentials and permissions)"
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
return new OINOResult({ success: false, status: 500, statusText: "OINOBlobAwsS3 validate failed: " + e.message });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return new OINOResult();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Release the S3 client (destroys the underlying HTTP connection pool).
|
|
127
|
+
*/
|
|
128
|
+
async disconnect() {
|
|
129
|
+
this._s3Client?.destroy();
|
|
130
|
+
this._s3Client = null;
|
|
131
|
+
this.isConnected = false;
|
|
132
|
+
this.isValidated = false;
|
|
133
|
+
}
|
|
134
|
+
// ── OINOBlob operations ───────────────────────────────────────────────
|
|
135
|
+
/**
|
|
136
|
+
* List all objects in the bucket, applying a server-side S3 `Prefix` filter
|
|
137
|
+
* where possible and in-memory result filtering for all other predicates.
|
|
138
|
+
*
|
|
139
|
+
* - The `name` field supports server-side prefix filtering via `ListObjectsV2`
|
|
140
|
+
* `Prefix` option (query filtering).
|
|
141
|
+
* - All other field predicates (`etag`, `lastModified`, `contentLength`,
|
|
142
|
+
* `contentType`) are evaluated in-memory after the listing (result filtering).
|
|
143
|
+
* Note: S3 listing does not return `contentType`; it defaults to
|
|
144
|
+
* `"application/octet-stream"` unless a `contentType` filter is applied.
|
|
145
|
+
*
|
|
146
|
+
* @param filter optional query filter to apply
|
|
147
|
+
*/
|
|
148
|
+
async listEntries(filter) {
|
|
149
|
+
if (!this._s3Client) {
|
|
150
|
+
throw new Error("OINOBlobAwsS3: not connected");
|
|
151
|
+
}
|
|
152
|
+
const queryPrefix = (filter && !filter.isEmpty())
|
|
153
|
+
? OINOBlob.extractNamePrefix(filter)
|
|
154
|
+
: undefined;
|
|
155
|
+
const entries = [];
|
|
156
|
+
let continuationToken;
|
|
157
|
+
do {
|
|
158
|
+
const response = await this._s3Client.send(new ListObjectsV2Command({
|
|
159
|
+
Bucket: this.blobParams.container,
|
|
160
|
+
Prefix: queryPrefix,
|
|
161
|
+
ContinuationToken: continuationToken
|
|
162
|
+
}));
|
|
163
|
+
for (const obj of response.Contents ?? []) {
|
|
164
|
+
entries.push({
|
|
165
|
+
name: obj.Key ?? "",
|
|
166
|
+
etag: (obj.ETag ?? "").replace(/^"|"$/g, ""),
|
|
167
|
+
lastModified: obj.LastModified ?? new Date(0),
|
|
168
|
+
contentLength: obj.Size ?? 0,
|
|
169
|
+
contentType: "application/octet-stream" // S3 does not return content type in listing, default to binary
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined;
|
|
173
|
+
} while (continuationToken);
|
|
174
|
+
if (!filter || filter.isEmpty()) {
|
|
175
|
+
return entries;
|
|
176
|
+
}
|
|
177
|
+
return entries.filter(e => OINOBlob.matchesEntry(e, filter));
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Download the raw content of a named object.
|
|
181
|
+
*
|
|
182
|
+
* @param name full object key (path within the bucket)
|
|
183
|
+
*/
|
|
184
|
+
async fetchEntry(name) {
|
|
185
|
+
if (!this._s3Client) {
|
|
186
|
+
throw new Error("OINOBlobAwsS3: not connected");
|
|
187
|
+
}
|
|
188
|
+
const response = await this._s3Client.send(new GetObjectCommand({
|
|
189
|
+
Bucket: this.blobParams.container,
|
|
190
|
+
Key: name
|
|
191
|
+
}));
|
|
192
|
+
const contentType = response.ContentType ?? "application/octet-stream";
|
|
193
|
+
if (!response.Body) {
|
|
194
|
+
throw new Error("OINOBlobAwsS3: no body returned for object '" + name + "'");
|
|
195
|
+
}
|
|
196
|
+
const content = await response.Body.transformToByteArray();
|
|
197
|
+
return { content, contentType };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Upload (create or replace) an object with the given binary content.
|
|
201
|
+
*
|
|
202
|
+
* @param name full object key (path within the bucket)
|
|
203
|
+
* @param content binary content to store
|
|
204
|
+
* @param contentType MIME type of the content (e.g. `"image/jpeg"`)
|
|
205
|
+
*/
|
|
206
|
+
async uploadEntry(name, content, contentType) {
|
|
207
|
+
if (!this._s3Client) {
|
|
208
|
+
throw new Error("OINOBlobAwsS3: not connected");
|
|
209
|
+
}
|
|
210
|
+
await this._s3Client.send(new PutObjectCommand({
|
|
211
|
+
Bucket: this.blobParams.container,
|
|
212
|
+
Key: name,
|
|
213
|
+
Body: content,
|
|
214
|
+
ContentType: contentType
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Delete a named object.
|
|
219
|
+
*
|
|
220
|
+
* @param name full object key (path within the bucket)
|
|
221
|
+
*/
|
|
222
|
+
async deleteEntry(name) {
|
|
223
|
+
if (!this._s3Client) {
|
|
224
|
+
throw new Error("OINOBlobAwsS3: not connected");
|
|
225
|
+
}
|
|
226
|
+
await this._s3Client.send(new DeleteObjectCommand({
|
|
227
|
+
Bucket: this.blobParams.container,
|
|
228
|
+
Key: name
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
// ── OINODataSource datamodel initialisation ───────────────────────────
|
|
232
|
+
/**
|
|
233
|
+
* Attach a static `OINOBlobDataModel` to the given API, adding only the
|
|
234
|
+
* four fields that S3 object listings return (`contentType` is omitted
|
|
235
|
+
* because S3 does not include it in listing responses).
|
|
236
|
+
*
|
|
237
|
+
* @param api the `OINOBlobApi` whose data model is to be initialised
|
|
238
|
+
*/
|
|
239
|
+
async initializeApiDatamodel(api) {
|
|
240
|
+
const blobApi = api;
|
|
241
|
+
const datamodel = new OINOBlobDataModel(blobApi);
|
|
242
|
+
const ds = this;
|
|
243
|
+
const FIELD = { isPrimaryKey: false, isForeignKey: false, isAutoInc: false, isNotNull: false };
|
|
244
|
+
const PK = { isPrimaryKey: true, isForeignKey: false, isAutoInc: false, isNotNull: true };
|
|
245
|
+
datamodel.addField(new OINOStringDataField(ds, "name", "TEXT", PK, 1024));
|
|
246
|
+
datamodel.addField(new OINOStringDataField(ds, "etag", "TEXT", FIELD, 256));
|
|
247
|
+
datamodel.addField(new OINODatetimeDataField(ds, "lastModified", "DATETIME", FIELD));
|
|
248
|
+
datamodel.addField(new OINONumberDataField(ds, "contentLength", "INTEGER", FIELD));
|
|
249
|
+
blobApi.initializeDatamodel(datamodel);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { OINOBlobAwsS3 } from "./OINOBlobAwsS3.js";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { OINOApi, OINOResult, OINOQueryFilter } from "@oino-ts/common";
|
|
2
|
+
import { OINOBlob } from "@oino-ts/blob";
|
|
3
|
+
import { type OINOBlobEntry, type OINOBlobFetchResult } from "@oino-ts/blob";
|
|
4
|
+
/**
|
|
5
|
+
* AWS S3 (and S3-compatible) implementation of `OINOBlob`.
|
|
6
|
+
*
|
|
7
|
+
* Authenticates using static access key credentials supplied via a JSON-encoded
|
|
8
|
+
* connection string. Connection parameters map as:
|
|
9
|
+
* - `params.url` → optional custom endpoint, e.g. `https://s3.eu-west-1.amazonaws.com`
|
|
10
|
+
* or a compatible service such as MinIO / Cloudflare R2
|
|
11
|
+
* - `params.container` → S3 bucket name
|
|
12
|
+
* - `params.connectionStr` → JSON string: `{"region":"…","accessKeyId":"…","secretAccessKey":"…"}`
|
|
13
|
+
*
|
|
14
|
+
* Register and use via the factory:
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { OINOBlobFactory } from "@oino-ts/blob"
|
|
17
|
+
* import { OINOBlobAwsS3 } from "@oino-ts/blob-aws"
|
|
18
|
+
*
|
|
19
|
+
* OINOBlobFactory.registerBlob("OINOBlobAwsS3", OINOBlobAwsS3)
|
|
20
|
+
*
|
|
21
|
+
* const blob = await OINOBlobFactory.createBlob({
|
|
22
|
+
* type: "OINOBlobAwsS3",
|
|
23
|
+
* url: "", // leave empty for default AWS endpoint
|
|
24
|
+
* container: "my-bucket",
|
|
25
|
+
* connectionStr: JSON.stringify({
|
|
26
|
+
* region: "us-east-1",
|
|
27
|
+
* accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
28
|
+
* secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
|
29
|
+
* })
|
|
30
|
+
* })
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare class OINOBlobAwsS3 extends OINOBlob {
|
|
34
|
+
private _s3Client;
|
|
35
|
+
/**
|
|
36
|
+
* Initialise the AWS SDK S3 client from the JSON-encoded `connectionStr`.
|
|
37
|
+
* Does not perform any network call.
|
|
38
|
+
*/
|
|
39
|
+
connect(): Promise<OINOResult>;
|
|
40
|
+
/**
|
|
41
|
+
* Verify that the target bucket exists and is accessible using a `HeadBucket` call.
|
|
42
|
+
*/
|
|
43
|
+
validate(): Promise<OINOResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Release the S3 client (destroys the underlying HTTP connection pool).
|
|
46
|
+
*/
|
|
47
|
+
disconnect(): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* List all objects in the bucket, applying a server-side S3 `Prefix` filter
|
|
50
|
+
* where possible and in-memory result filtering for all other predicates.
|
|
51
|
+
*
|
|
52
|
+
* - The `name` field supports server-side prefix filtering via `ListObjectsV2`
|
|
53
|
+
* `Prefix` option (query filtering).
|
|
54
|
+
* - All other field predicates (`etag`, `lastModified`, `contentLength`,
|
|
55
|
+
* `contentType`) are evaluated in-memory after the listing (result filtering).
|
|
56
|
+
* Note: S3 listing does not return `contentType`; it defaults to
|
|
57
|
+
* `"application/octet-stream"` unless a `contentType` filter is applied.
|
|
58
|
+
*
|
|
59
|
+
* @param filter optional query filter to apply
|
|
60
|
+
*/
|
|
61
|
+
listEntries(filter?: OINOQueryFilter): Promise<OINOBlobEntry[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Download the raw content of a named object.
|
|
64
|
+
*
|
|
65
|
+
* @param name full object key (path within the bucket)
|
|
66
|
+
*/
|
|
67
|
+
fetchEntry(name: string): Promise<OINOBlobFetchResult>;
|
|
68
|
+
/**
|
|
69
|
+
* Upload (create or replace) an object with the given binary content.
|
|
70
|
+
*
|
|
71
|
+
* @param name full object key (path within the bucket)
|
|
72
|
+
* @param content binary content to store
|
|
73
|
+
* @param contentType MIME type of the content (e.g. `"image/jpeg"`)
|
|
74
|
+
*/
|
|
75
|
+
uploadEntry(name: string, content: Uint8Array, contentType: string): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Delete a named object.
|
|
78
|
+
*
|
|
79
|
+
* @param name full object key (path within the bucket)
|
|
80
|
+
*/
|
|
81
|
+
deleteEntry(name: string): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Attach a static `OINOBlobDataModel` to the given API, adding only the
|
|
84
|
+
* four fields that S3 object listings return (`contentType` is omitted
|
|
85
|
+
* because S3 does not include it in listing responses).
|
|
86
|
+
*
|
|
87
|
+
* @param api the `OINOBlobApi` whose data model is to be initialised
|
|
88
|
+
*/
|
|
89
|
+
initializeApiDatamodel(api: OINOApi): Promise<void>;
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { OINOBlobAwsS3 } from "./OINOBlobAwsS3.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oino-ts/blob-aws",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OINO TS package for using AWS S3 (or S3-compatible) storage as a REST API.",
|
|
5
|
+
"author": "Matias Kiviniemi (pragmatta)",
|
|
6
|
+
"license": "MPL-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/pragmatta/oino-ts.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"blob",
|
|
13
|
+
"storage",
|
|
14
|
+
"rest-api",
|
|
15
|
+
"typescript",
|
|
16
|
+
"library",
|
|
17
|
+
"s3",
|
|
18
|
+
"aws"
|
|
19
|
+
],
|
|
20
|
+
"main": "./dist/cjs/index.js",
|
|
21
|
+
"module": "./dist/esm/index.js",
|
|
22
|
+
"types": "./dist/types/index.d.ts",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@aws-sdk/client-s3": "^3.0.0",
|
|
25
|
+
"@oino-ts/blob": "1.0.0",
|
|
26
|
+
"@oino-ts/common": "1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@oino-ts/types": "1.0.0",
|
|
30
|
+
"@types/bun": "^1.1.14",
|
|
31
|
+
"@types/node": "^22.0.00",
|
|
32
|
+
"typescript": "~5.9.0"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"src/*.ts",
|
|
36
|
+
"dist/cjs/*.js",
|
|
37
|
+
"dist/esm/*.js",
|
|
38
|
+
"dist/types/*.d.ts"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
S3Client,
|
|
9
|
+
HeadBucketCommand,
|
|
10
|
+
ListObjectsV2Command,
|
|
11
|
+
GetObjectCommand,
|
|
12
|
+
PutObjectCommand,
|
|
13
|
+
DeleteObjectCommand
|
|
14
|
+
} from "@aws-sdk/client-s3"
|
|
15
|
+
|
|
16
|
+
import { OINOApi, OINOResult, OINOQueryFilter, OINOStringDataField, OINONumberDataField, OINODatetimeDataField, type OINODataFieldParams } from "@oino-ts/common"
|
|
17
|
+
import { OINOBlob, OINOBlobDataModel, OINOBlobApi } from "@oino-ts/blob"
|
|
18
|
+
import { type OINOBlobEntry, type OINOBlobFetchResult } from "@oino-ts/blob"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* AWS S3 (and S3-compatible) implementation of `OINOBlob`.
|
|
22
|
+
*
|
|
23
|
+
* Authenticates using static access key credentials supplied via a JSON-encoded
|
|
24
|
+
* connection string. Connection parameters map as:
|
|
25
|
+
* - `params.url` → optional custom endpoint, e.g. `https://s3.eu-west-1.amazonaws.com`
|
|
26
|
+
* or a compatible service such as MinIO / Cloudflare R2
|
|
27
|
+
* - `params.container` → S3 bucket name
|
|
28
|
+
* - `params.connectionStr` → JSON string: `{"region":"…","accessKeyId":"…","secretAccessKey":"…"}`
|
|
29
|
+
*
|
|
30
|
+
* Register and use via the factory:
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { OINOBlobFactory } from "@oino-ts/blob"
|
|
33
|
+
* import { OINOBlobAwsS3 } from "@oino-ts/blob-aws"
|
|
34
|
+
*
|
|
35
|
+
* OINOBlobFactory.registerBlob("OINOBlobAwsS3", OINOBlobAwsS3)
|
|
36
|
+
*
|
|
37
|
+
* const blob = await OINOBlobFactory.createBlob({
|
|
38
|
+
* type: "OINOBlobAwsS3",
|
|
39
|
+
* url: "", // leave empty for default AWS endpoint
|
|
40
|
+
* container: "my-bucket",
|
|
41
|
+
* connectionStr: JSON.stringify({
|
|
42
|
+
* region: "us-east-1",
|
|
43
|
+
* accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
44
|
+
* secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
|
45
|
+
* })
|
|
46
|
+
* })
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export class OINOBlobAwsS3 extends OINOBlob {
|
|
50
|
+
private _s3Client: S3Client | null = null
|
|
51
|
+
|
|
52
|
+
// ── OINODataSource lifecycle ──────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Initialise the AWS SDK S3 client from the JSON-encoded `connectionStr`.
|
|
56
|
+
* Does not perform any network call.
|
|
57
|
+
*/
|
|
58
|
+
async connect(): Promise<OINOResult> {
|
|
59
|
+
if (!this.blobParams.connectionStr) {
|
|
60
|
+
return new OINOResult({
|
|
61
|
+
success: false,
|
|
62
|
+
status: 400,
|
|
63
|
+
statusText: "OINOBlobAwsS3: params.connectionStr is required (JSON with region, accessKeyId, secretAccessKey)"
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let creds: { region: string; accessKeyId: string; secretAccessKey: string }
|
|
68
|
+
try {
|
|
69
|
+
creds = JSON.parse(this.blobParams.connectionStr)
|
|
70
|
+
} catch {
|
|
71
|
+
return new OINOResult({
|
|
72
|
+
success: false,
|
|
73
|
+
status: 400,
|
|
74
|
+
statusText: "OINOBlobAwsS3: params.connectionStr must be a valid JSON string"
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!creds.region || !creds.accessKeyId || !creds.secretAccessKey) {
|
|
79
|
+
return new OINOResult({
|
|
80
|
+
success: false,
|
|
81
|
+
status: 400,
|
|
82
|
+
statusText: "OINOBlobAwsS3: connectionStr must include region, accessKeyId and secretAccessKey"
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const clientConfig: ConstructorParameters<typeof S3Client>[0] = {
|
|
88
|
+
region: creds.region,
|
|
89
|
+
credentials: {
|
|
90
|
+
accessKeyId: creds.accessKeyId,
|
|
91
|
+
secretAccessKey: creds.secretAccessKey
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (this.blobParams.url) {
|
|
95
|
+
clientConfig.endpoint = this.blobParams.url
|
|
96
|
+
clientConfig.forcePathStyle = true
|
|
97
|
+
}
|
|
98
|
+
this._s3Client = new S3Client(clientConfig)
|
|
99
|
+
this.isConnected = true
|
|
100
|
+
} catch (e: any) {
|
|
101
|
+
return new OINOResult({ success: false, status: 500, statusText: "OINOBlobAwsS3 connect failed: " + e.message })
|
|
102
|
+
}
|
|
103
|
+
return new OINOResult()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Verify that the target bucket exists and is accessible using a `HeadBucket` call.
|
|
108
|
+
*/
|
|
109
|
+
async validate(): Promise<OINOResult> {
|
|
110
|
+
if (!this._s3Client) {
|
|
111
|
+
return new OINOResult({ success: false, status: 500, statusText: "OINOBlobAwsS3: not connected" })
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await this._s3Client.send(new HeadBucketCommand({ Bucket: this.blobParams.container }))
|
|
115
|
+
this.isValidated = true
|
|
116
|
+
} catch (e: any) {
|
|
117
|
+
console.error("OINOBlobAwsS3 validate error:", e)
|
|
118
|
+
const status = e.$metadata?.httpStatusCode ?? 500
|
|
119
|
+
if (status === 404) {
|
|
120
|
+
return new OINOResult({
|
|
121
|
+
success: false,
|
|
122
|
+
status: 404,
|
|
123
|
+
statusText: "OINOBlobAwsS3: bucket '" + this.blobParams.container + "' not found"
|
|
124
|
+
})
|
|
125
|
+
} else if (status === 403) {
|
|
126
|
+
return new OINOResult({
|
|
127
|
+
success: false,
|
|
128
|
+
status: 403,
|
|
129
|
+
statusText: "OINOBlobAwsS3: access to bucket '" + this.blobParams.container + "' forbidden (check credentials and permissions)"
|
|
130
|
+
})
|
|
131
|
+
} else {
|
|
132
|
+
return new OINOResult({ success: false, status: 500, statusText: "OINOBlobAwsS3 validate failed: " + e.message })
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return new OINOResult()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Release the S3 client (destroys the underlying HTTP connection pool).
|
|
140
|
+
*/
|
|
141
|
+
async disconnect(): Promise<void> {
|
|
142
|
+
this._s3Client?.destroy()
|
|
143
|
+
this._s3Client = null
|
|
144
|
+
this.isConnected = false
|
|
145
|
+
this.isValidated = false
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── OINOBlob operations ───────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* List all objects in the bucket, applying a server-side S3 `Prefix` filter
|
|
152
|
+
* where possible and in-memory result filtering for all other predicates.
|
|
153
|
+
*
|
|
154
|
+
* - The `name` field supports server-side prefix filtering via `ListObjectsV2`
|
|
155
|
+
* `Prefix` option (query filtering).
|
|
156
|
+
* - All other field predicates (`etag`, `lastModified`, `contentLength`,
|
|
157
|
+
* `contentType`) are evaluated in-memory after the listing (result filtering).
|
|
158
|
+
* Note: S3 listing does not return `contentType`; it defaults to
|
|
159
|
+
* `"application/octet-stream"` unless a `contentType` filter is applied.
|
|
160
|
+
*
|
|
161
|
+
* @param filter optional query filter to apply
|
|
162
|
+
*/
|
|
163
|
+
async listEntries(filter?: OINOQueryFilter): Promise<OINOBlobEntry[]> {
|
|
164
|
+
if (!this._s3Client) {
|
|
165
|
+
throw new Error("OINOBlobAwsS3: not connected")
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const queryPrefix = (filter && !filter.isEmpty())
|
|
169
|
+
? OINOBlob.extractNamePrefix(filter)
|
|
170
|
+
: undefined
|
|
171
|
+
|
|
172
|
+
const entries: OINOBlobEntry[] = []
|
|
173
|
+
let continuationToken: string | undefined
|
|
174
|
+
|
|
175
|
+
do {
|
|
176
|
+
const response = await this._s3Client.send(new ListObjectsV2Command({
|
|
177
|
+
Bucket: this.blobParams.container,
|
|
178
|
+
Prefix: queryPrefix,
|
|
179
|
+
ContinuationToken: continuationToken
|
|
180
|
+
}))
|
|
181
|
+
|
|
182
|
+
for (const obj of response.Contents ?? []) {
|
|
183
|
+
entries.push({
|
|
184
|
+
name: obj.Key ?? "",
|
|
185
|
+
etag: (obj.ETag ?? "").replace(/^"|"$/g, ""),
|
|
186
|
+
lastModified: obj.LastModified ?? new Date(0),
|
|
187
|
+
contentLength: obj.Size ?? 0,
|
|
188
|
+
contentType: "application/octet-stream" // S3 does not return content type in listing, default to binary
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined
|
|
193
|
+
} while (continuationToken)
|
|
194
|
+
|
|
195
|
+
if (!filter || filter.isEmpty()) {
|
|
196
|
+
return entries
|
|
197
|
+
}
|
|
198
|
+
return entries.filter(e => OINOBlob.matchesEntry(e, filter))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Download the raw content of a named object.
|
|
203
|
+
*
|
|
204
|
+
* @param name full object key (path within the bucket)
|
|
205
|
+
*/
|
|
206
|
+
async fetchEntry(name: string): Promise<OINOBlobFetchResult> {
|
|
207
|
+
if (!this._s3Client) {
|
|
208
|
+
throw new Error("OINOBlobAwsS3: not connected")
|
|
209
|
+
}
|
|
210
|
+
const response = await this._s3Client.send(new GetObjectCommand({
|
|
211
|
+
Bucket: this.blobParams.container,
|
|
212
|
+
Key: name
|
|
213
|
+
}))
|
|
214
|
+
const contentType = response.ContentType ?? "application/octet-stream"
|
|
215
|
+
if (!response.Body) {
|
|
216
|
+
throw new Error("OINOBlobAwsS3: no body returned for object '" + name + "'")
|
|
217
|
+
}
|
|
218
|
+
const content = await response.Body.transformToByteArray()
|
|
219
|
+
return { content, contentType }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Upload (create or replace) an object with the given binary content.
|
|
224
|
+
*
|
|
225
|
+
* @param name full object key (path within the bucket)
|
|
226
|
+
* @param content binary content to store
|
|
227
|
+
* @param contentType MIME type of the content (e.g. `"image/jpeg"`)
|
|
228
|
+
*/
|
|
229
|
+
async uploadEntry(name: string, content: Uint8Array, contentType: string): Promise<void> {
|
|
230
|
+
if (!this._s3Client) {
|
|
231
|
+
throw new Error("OINOBlobAwsS3: not connected")
|
|
232
|
+
}
|
|
233
|
+
await this._s3Client.send(new PutObjectCommand({
|
|
234
|
+
Bucket: this.blobParams.container,
|
|
235
|
+
Key: name,
|
|
236
|
+
Body: content,
|
|
237
|
+
ContentType: contentType
|
|
238
|
+
}))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Delete a named object.
|
|
243
|
+
*
|
|
244
|
+
* @param name full object key (path within the bucket)
|
|
245
|
+
*/
|
|
246
|
+
async deleteEntry(name: string): Promise<void> {
|
|
247
|
+
if (!this._s3Client) {
|
|
248
|
+
throw new Error("OINOBlobAwsS3: not connected")
|
|
249
|
+
}
|
|
250
|
+
await this._s3Client.send(new DeleteObjectCommand({
|
|
251
|
+
Bucket: this.blobParams.container,
|
|
252
|
+
Key: name
|
|
253
|
+
}))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── OINODataSource datamodel initialisation ───────────────────────────
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Attach a static `OINOBlobDataModel` to the given API, adding only the
|
|
260
|
+
* four fields that S3 object listings return (`contentType` is omitted
|
|
261
|
+
* because S3 does not include it in listing responses).
|
|
262
|
+
*
|
|
263
|
+
* @param api the `OINOBlobApi` whose data model is to be initialised
|
|
264
|
+
*/
|
|
265
|
+
async initializeApiDatamodel(api: OINOApi): Promise<void> {
|
|
266
|
+
const blobApi = api as OINOBlobApi
|
|
267
|
+
const datamodel = new OINOBlobDataModel(blobApi)
|
|
268
|
+
const ds = this
|
|
269
|
+
const FIELD: OINODataFieldParams = { isPrimaryKey: false, isForeignKey: false, isAutoInc: false, isNotNull: false }
|
|
270
|
+
const PK: OINODataFieldParams = { isPrimaryKey: true, isForeignKey: false, isAutoInc: false, isNotNull: true }
|
|
271
|
+
datamodel.addField(new OINOStringDataField(ds, "name", "TEXT", PK, 1024))
|
|
272
|
+
datamodel.addField(new OINOStringDataField(ds, "etag", "TEXT", FIELD, 256))
|
|
273
|
+
datamodel.addField(new OINODatetimeDataField(ds, "lastModified", "DATETIME", FIELD))
|
|
274
|
+
datamodel.addField(new OINONumberDataField(ds, "contentLength", "INTEGER", FIELD))
|
|
275
|
+
blobApi.initializeDatamodel(datamodel)
|
|
276
|
+
}
|
|
277
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { OINOBlobAwsS3 } from "./OINOBlobAwsS3.js"
|