@fileway/azure 0.0.0-stage → 0.5.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/LICENSE +21 -0
- package/README.md +80 -2
- package/dist/index.cjs +481 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +63 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +456 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { validateUploadOptions, ValidationError, urlEncodePath, abortError, isAbortError, StorageError } from "@fileway/core";
|
|
3
|
+
|
|
4
|
+
// src/sharedkey.ts
|
|
5
|
+
var SERVICE_VERSION = "2023-11-03";
|
|
6
|
+
var encoder = new TextEncoder();
|
|
7
|
+
var keyCache = /* @__PURE__ */ new Map();
|
|
8
|
+
function importKey(accountKey) {
|
|
9
|
+
let key = keyCache.get(accountKey);
|
|
10
|
+
if (!key) {
|
|
11
|
+
key = globalThis.crypto.subtle.importKey(
|
|
12
|
+
"raw",
|
|
13
|
+
base64ToBytes(accountKey),
|
|
14
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
15
|
+
false,
|
|
16
|
+
["sign"]
|
|
17
|
+
);
|
|
18
|
+
keyCache.set(accountKey, key);
|
|
19
|
+
}
|
|
20
|
+
return key;
|
|
21
|
+
}
|
|
22
|
+
async function hmacBase64(accountKey, message) {
|
|
23
|
+
const key = await importKey(accountKey);
|
|
24
|
+
const sig = await globalThis.crypto.subtle.sign("HMAC", key, encoder.encode(message));
|
|
25
|
+
return bytesToBase64(new Uint8Array(sig));
|
|
26
|
+
}
|
|
27
|
+
function buildStringToSign(method, url, accountName, headers, contentLength) {
|
|
28
|
+
const lower = {};
|
|
29
|
+
for (const [k, v] of Object.entries(headers)) lower[k.toLowerCase()] = v;
|
|
30
|
+
const h = (name) => lower[name] ?? "";
|
|
31
|
+
const canonicalHeaders = Object.keys(lower).filter((k) => k.startsWith("x-ms-")).sort().map((k) => `${k}:${(lower[k] ?? "").replace(/\s+/g, " ").trim()}
|
|
32
|
+
`).join("");
|
|
33
|
+
const params = /* @__PURE__ */ new Map();
|
|
34
|
+
for (const [k, v] of url.searchParams) {
|
|
35
|
+
const name = k.toLowerCase();
|
|
36
|
+
params.set(name, [...params.get(name) ?? [], v]);
|
|
37
|
+
}
|
|
38
|
+
const canonicalQuery = [...params.keys()].sort().map((k) => `
|
|
39
|
+
${k}:${(params.get(k) ?? []).sort().join(",")}`).join("");
|
|
40
|
+
const canonicalResource = `/${accountName}${url.pathname}${canonicalQuery}`;
|
|
41
|
+
return [
|
|
42
|
+
method.toUpperCase(),
|
|
43
|
+
h("content-encoding"),
|
|
44
|
+
h("content-language"),
|
|
45
|
+
contentLength ? String(contentLength) : "",
|
|
46
|
+
h("content-md5"),
|
|
47
|
+
h("content-type"),
|
|
48
|
+
h("date"),
|
|
49
|
+
h("if-modified-since"),
|
|
50
|
+
h("if-match"),
|
|
51
|
+
h("if-none-match"),
|
|
52
|
+
h("if-unmodified-since"),
|
|
53
|
+
h("range"),
|
|
54
|
+
canonicalHeaders + canonicalResource
|
|
55
|
+
].join("\n");
|
|
56
|
+
}
|
|
57
|
+
async function signRequest(method, url, accountName, accountKey, headers, contentLength, now = /* @__PURE__ */ new Date()) {
|
|
58
|
+
const signed = {
|
|
59
|
+
...headers,
|
|
60
|
+
"x-ms-date": now.toUTCString(),
|
|
61
|
+
"x-ms-version": SERVICE_VERSION
|
|
62
|
+
};
|
|
63
|
+
const stringToSign = buildStringToSign(method, url, accountName, signed, contentLength);
|
|
64
|
+
const signature = await hmacBase64(accountKey, stringToSign);
|
|
65
|
+
return { ...signed, authorization: `SharedKey ${accountName}:${signature}` };
|
|
66
|
+
}
|
|
67
|
+
async function blobReadSas(accountName, accountKey, container, blobName, expiry) {
|
|
68
|
+
const se = expiry.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
69
|
+
const fields = {
|
|
70
|
+
sp: "r",
|
|
71
|
+
st: "",
|
|
72
|
+
se,
|
|
73
|
+
resource: `/blob/${accountName}/${container}/${blobName}`,
|
|
74
|
+
si: "",
|
|
75
|
+
sip: "",
|
|
76
|
+
spr: "",
|
|
77
|
+
sv: SERVICE_VERSION,
|
|
78
|
+
sr: "b",
|
|
79
|
+
snapshot: "",
|
|
80
|
+
ses: "",
|
|
81
|
+
rscc: "",
|
|
82
|
+
rscd: "",
|
|
83
|
+
rsce: "",
|
|
84
|
+
rscl: "",
|
|
85
|
+
rsct: ""
|
|
86
|
+
};
|
|
87
|
+
const signature = await hmacBase64(accountKey, Object.values(fields).join("\n"));
|
|
88
|
+
const query = new URLSearchParams({ sv: SERVICE_VERSION, se, sr: "b", sp: "r", sig: signature });
|
|
89
|
+
return query.toString();
|
|
90
|
+
}
|
|
91
|
+
function base64ToBytes(value) {
|
|
92
|
+
const binary = atob(value);
|
|
93
|
+
const out = new Uint8Array(binary.length);
|
|
94
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
function bytesToBase64(bytes) {
|
|
98
|
+
let binary = "";
|
|
99
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
100
|
+
return btoa(binary);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/internal.ts
|
|
104
|
+
var DEV_ACCOUNT = "devstoreaccount1";
|
|
105
|
+
var DEV_KEY = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
|
|
106
|
+
var DEV_ENDPOINT = "http://127.0.0.1:10000/devstoreaccount1";
|
|
107
|
+
function parseConnectionString(value) {
|
|
108
|
+
const parts = {};
|
|
109
|
+
for (const segment of value.split(";")) {
|
|
110
|
+
const eq = segment.indexOf("=");
|
|
111
|
+
if (eq > 0) parts[segment.slice(0, eq).trim()] = segment.slice(eq + 1).trim();
|
|
112
|
+
}
|
|
113
|
+
if (parts.UseDevelopmentStorage === "true") {
|
|
114
|
+
return { accountName: DEV_ACCOUNT, accountKey: DEV_KEY, endpoint: DEV_ENDPOINT };
|
|
115
|
+
}
|
|
116
|
+
const accountName = parts.AccountName;
|
|
117
|
+
const endpoint = parts.BlobEndpoint ?? (accountName ? `${parts.DefaultEndpointsProtocol ?? "https"}://${accountName}.blob.${parts.EndpointSuffix ?? "core.windows.net"}` : void 0);
|
|
118
|
+
return {
|
|
119
|
+
...accountName ? { accountName } : {},
|
|
120
|
+
...parts.AccountKey ? { accountKey: parts.AccountKey } : {},
|
|
121
|
+
...parts.SharedAccessSignature ? { sasToken: parts.SharedAccessSignature } : {},
|
|
122
|
+
...endpoint ? { endpoint } : {}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function classifyAzureError(status, code) {
|
|
126
|
+
switch (code) {
|
|
127
|
+
case "ContainerNotFound":
|
|
128
|
+
case "ContainerBeingDeleted":
|
|
129
|
+
return "bucket-not-found";
|
|
130
|
+
case "BlobNotFound":
|
|
131
|
+
case "ResourceNotFound":
|
|
132
|
+
return "not-found";
|
|
133
|
+
case "AuthenticationFailed":
|
|
134
|
+
case "AuthorizationFailure":
|
|
135
|
+
case "AuthorizationPermissionMismatch":
|
|
136
|
+
case "AuthorizationResourceTypeMismatch":
|
|
137
|
+
case "NoAuthenticationInformation":
|
|
138
|
+
case "InvalidAuthenticationInfo":
|
|
139
|
+
return "auth-failed";
|
|
140
|
+
case "RequestBodyTooLarge":
|
|
141
|
+
case "BlockCountExceedsLimit":
|
|
142
|
+
return "size-exceeded";
|
|
143
|
+
case "ServerBusy":
|
|
144
|
+
case "TooManyRequests":
|
|
145
|
+
return "rate-limited";
|
|
146
|
+
}
|
|
147
|
+
if (status === 429) return "rate-limited";
|
|
148
|
+
if (status === 401 || status === 403) return "auth-failed";
|
|
149
|
+
if (status === 404) return "not-found";
|
|
150
|
+
if (status === 413) return "size-exceeded";
|
|
151
|
+
return "provider-error";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/index.ts
|
|
155
|
+
var randomUUID = () => globalThis.crypto.randomUUID();
|
|
156
|
+
var PROVIDER = "azure";
|
|
157
|
+
var DEFAULT_BLOCK_SIZE = 8 * 1024 * 1024;
|
|
158
|
+
var MIN_BLOCK_SIZE = 1024 * 1024;
|
|
159
|
+
var MAX_BLOCK_SIZE = 4e3 * 1024 * 1024;
|
|
160
|
+
var MAX_BLOCKS = 5e4;
|
|
161
|
+
var PRESIGN_DEFAULT_EXPIRY = 3600;
|
|
162
|
+
var PRESIGN_MIN_EXPIRY = 1;
|
|
163
|
+
var PRESIGN_MAX_EXPIRY = 604800;
|
|
164
|
+
var TOKEN_KEY = "fileway_upload_token";
|
|
165
|
+
var TOKEN_HEADER = `x-ms-meta-${TOKEN_KEY}`;
|
|
166
|
+
var METADATA_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
167
|
+
var AzureDriver = class {
|
|
168
|
+
name = "azure";
|
|
169
|
+
container;
|
|
170
|
+
auth;
|
|
171
|
+
endpoint;
|
|
172
|
+
baseUrl;
|
|
173
|
+
blockSize;
|
|
174
|
+
maxSizeBytes;
|
|
175
|
+
constructor(config) {
|
|
176
|
+
const fromString = config.connectionString ? parseConnectionString(config.connectionString) : {};
|
|
177
|
+
const accountName = config.accountName ?? fromString.accountName;
|
|
178
|
+
const accountKey = config.accountKey ?? fromString.accountKey;
|
|
179
|
+
const sasToken = config.sasToken ?? fromString.sasToken;
|
|
180
|
+
const endpoint = config.endpoint ?? fromString.endpoint;
|
|
181
|
+
if (!config.container) {
|
|
182
|
+
throw new StorageError("config", "AzureDriver requires a container", { provider: PROVIDER });
|
|
183
|
+
}
|
|
184
|
+
const name = accountName ?? (endpoint ? accountFromEndpoint(endpoint) : void 0);
|
|
185
|
+
if (!name) {
|
|
186
|
+
throw new StorageError("config", "AzureDriver requires accountName (or a connection string or endpoint)", { provider: PROVIDER });
|
|
187
|
+
}
|
|
188
|
+
if (!accountKey && !sasToken) {
|
|
189
|
+
throw new StorageError("config", "AzureDriver requires accountKey, sasToken or a connection string with credentials", { provider: PROVIDER });
|
|
190
|
+
}
|
|
191
|
+
this.container = config.container;
|
|
192
|
+
this.auth = {
|
|
193
|
+
accountName: name,
|
|
194
|
+
...accountKey ? { accountKey } : {},
|
|
195
|
+
...sasToken ? { sasToken: sasToken.replace(/^\?/, "") } : {}
|
|
196
|
+
};
|
|
197
|
+
this.endpoint = (endpoint ?? `https://${name}.blob.core.windows.net`).replace(/\/+$/, "");
|
|
198
|
+
this.baseUrl = (config.baseUrl ?? `${this.endpoint}/${config.container}`).replace(/\/+$/, "");
|
|
199
|
+
this.blockSize = Math.min(Math.max(config.blockSize ?? DEFAULT_BLOCK_SIZE, MIN_BLOCK_SIZE), MAX_BLOCK_SIZE);
|
|
200
|
+
this.maxSizeBytes = config.maxSizeBytes;
|
|
201
|
+
}
|
|
202
|
+
async upload(stream, options, context) {
|
|
203
|
+
validateUploadOptions(options);
|
|
204
|
+
for (const key of Object.keys(options.metadata ?? {})) {
|
|
205
|
+
if (!METADATA_NAME.test(key)) {
|
|
206
|
+
throw new ValidationError(
|
|
207
|
+
`Azure metadata keys must be letters, digits and underscores, not starting with a digit (got "${key}")`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (this.maxSizeBytes !== void 0 && options.size !== void 0 && options.size > this.maxSizeBytes) {
|
|
212
|
+
throw new StorageError("size-exceeded", `file size ${options.size} exceeds maxSizeBytes ${this.maxSizeBytes}`, { provider: PROVIDER });
|
|
213
|
+
}
|
|
214
|
+
const id = options.id ?? randomUUID();
|
|
215
|
+
const path = this.pathFor({ ...options, id });
|
|
216
|
+
const contentType = options.mimeType ?? "application/octet-stream";
|
|
217
|
+
const metaHeaders = {
|
|
218
|
+
...Object.fromEntries(Object.entries(options.metadata ?? {}).map(([k, v]) => [`x-ms-meta-${k}`, v])),
|
|
219
|
+
...context?.uploadToken ? { [TOKEN_HEADER]: context.uploadToken } : {}
|
|
220
|
+
};
|
|
221
|
+
const signal = options.signal;
|
|
222
|
+
const reader = stream.getReader();
|
|
223
|
+
let aborted = signal?.aborted ?? false;
|
|
224
|
+
const onAbort = () => {
|
|
225
|
+
aborted = true;
|
|
226
|
+
reader.cancel().catch(() => {
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
if (signal && !aborted) signal.addEventListener("abort", onAbort, { once: true });
|
|
230
|
+
const ensureNotAborted = () => {
|
|
231
|
+
if (aborted) throw abortError("upload aborted");
|
|
232
|
+
};
|
|
233
|
+
let buffer = [];
|
|
234
|
+
let bufferedLen = 0;
|
|
235
|
+
let totalBytes = 0;
|
|
236
|
+
const blockIds = [];
|
|
237
|
+
const putBlock = async (bytes) => {
|
|
238
|
+
if (blockIds.length >= MAX_BLOCKS) {
|
|
239
|
+
throw new StorageError("size-exceeded", `upload exceeds ${MAX_BLOCKS} blocks; raise blockSize`, { provider: PROVIDER });
|
|
240
|
+
}
|
|
241
|
+
const blockId = bytesToBase64(new TextEncoder().encode(`fileway-${String(blockIds.length).padStart(6, "0")}`));
|
|
242
|
+
const res = await this.request("PUT", path, {
|
|
243
|
+
query: { comp: "block", blockid: blockId },
|
|
244
|
+
body: bytes,
|
|
245
|
+
...signal ? { signal } : {}
|
|
246
|
+
});
|
|
247
|
+
if (!res.ok) throw await azureError(`Azure put block ${blockIds.length + 1} failed`, res);
|
|
248
|
+
blockIds.push(blockId);
|
|
249
|
+
};
|
|
250
|
+
try {
|
|
251
|
+
while (true) {
|
|
252
|
+
const { done, value } = await reader.read();
|
|
253
|
+
ensureNotAborted();
|
|
254
|
+
if (done) break;
|
|
255
|
+
totalBytes += value.byteLength;
|
|
256
|
+
if (this.maxSizeBytes !== void 0 && totalBytes > this.maxSizeBytes) {
|
|
257
|
+
await reader.cancel().catch(() => {
|
|
258
|
+
});
|
|
259
|
+
throw new StorageError("size-exceeded", `upload exceeds maxSizeBytes ${this.maxSizeBytes}`, { provider: PROVIDER });
|
|
260
|
+
}
|
|
261
|
+
buffer.push(value);
|
|
262
|
+
bufferedLen += value.byteLength;
|
|
263
|
+
while (bufferedLen >= this.blockSize) {
|
|
264
|
+
const merged = concatBytes(buffer, bufferedLen);
|
|
265
|
+
await putBlock(merged.subarray(0, this.blockSize));
|
|
266
|
+
const rest = merged.subarray(this.blockSize);
|
|
267
|
+
buffer = rest.byteLength > 0 ? [rest] : [];
|
|
268
|
+
bufferedLen = rest.byteLength;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (options.size !== void 0 && totalBytes !== options.size) {
|
|
272
|
+
throw new ValidationError(`stream delivered ${totalBytes} bytes, expected ${options.size}`);
|
|
273
|
+
}
|
|
274
|
+
let res;
|
|
275
|
+
if (blockIds.length === 0) {
|
|
276
|
+
res = await this.request("PUT", path, {
|
|
277
|
+
headers: { "x-ms-blob-type": "BlockBlob", "content-type": contentType, ...metaHeaders },
|
|
278
|
+
body: concatBytes(buffer, bufferedLen),
|
|
279
|
+
...signal ? { signal } : {}
|
|
280
|
+
});
|
|
281
|
+
if (!res.ok) throw await azureError("Azure upload failed", res);
|
|
282
|
+
} else {
|
|
283
|
+
if (bufferedLen > 0) await putBlock(concatBytes(buffer, bufferedLen));
|
|
284
|
+
ensureNotAborted();
|
|
285
|
+
const xml = `<?xml version="1.0" encoding="utf-8"?><BlockList>${blockIds.map((b) => `<Latest>${b}</Latest>`).join("")}</BlockList>`;
|
|
286
|
+
res = await this.request("PUT", path, {
|
|
287
|
+
query: { comp: "blocklist" },
|
|
288
|
+
headers: { "content-type": "application/xml", "x-ms-blob-content-type": contentType, ...metaHeaders },
|
|
289
|
+
body: new TextEncoder().encode(xml),
|
|
290
|
+
...signal ? { signal } : {}
|
|
291
|
+
});
|
|
292
|
+
if (!res.ok) throw await azureError("Azure put block list failed", res);
|
|
293
|
+
}
|
|
294
|
+
const etag = res.headers.get("etag") ?? void 0;
|
|
295
|
+
return {
|
|
296
|
+
id,
|
|
297
|
+
url: `${this.baseUrl}/${urlEncodePath(path)}`,
|
|
298
|
+
path,
|
|
299
|
+
size: totalBytes,
|
|
300
|
+
meta: { container: this.container, ...etag ? { etag } : {} }
|
|
301
|
+
};
|
|
302
|
+
} catch (err) {
|
|
303
|
+
reader.cancel().catch(() => {
|
|
304
|
+
});
|
|
305
|
+
throw err;
|
|
306
|
+
} finally {
|
|
307
|
+
signal?.removeEventListener("abort", onAbort);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/** Deletes the blob; a missing blob is not an error. */
|
|
311
|
+
async delete(path, options) {
|
|
312
|
+
const res = await this.request("DELETE", path, withSignal(options));
|
|
313
|
+
if (res.status === 404 && errorCode(res) !== "ContainerNotFound") return;
|
|
314
|
+
if (!res.ok) throw await azureError("Azure delete failed", res);
|
|
315
|
+
}
|
|
316
|
+
async getUrl(path, _options) {
|
|
317
|
+
return `${this.baseUrl}/${urlEncodePath(path)}`;
|
|
318
|
+
}
|
|
319
|
+
/** Streams the blob. `options.signal` also cancels reading the body. */
|
|
320
|
+
async get(path, options) {
|
|
321
|
+
const res = await this.request("GET", path, withSignal(options));
|
|
322
|
+
if (!res.ok) throw await azureError("Azure get failed", res);
|
|
323
|
+
if (!res.body) {
|
|
324
|
+
throw new StorageError("provider-error", "Azure get returned no response body", { provider: PROVIDER });
|
|
325
|
+
}
|
|
326
|
+
return res.body;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* A read-only service SAS URL for `path`, valid for `expiresIn` seconds
|
|
330
|
+
* (integer in 1..604800, default 3600). Needs an account key.
|
|
331
|
+
*/
|
|
332
|
+
async getPresignedUrl(path, options) {
|
|
333
|
+
const expiresIn = options?.expiresIn ?? PRESIGN_DEFAULT_EXPIRY;
|
|
334
|
+
if (!Number.isInteger(expiresIn) || expiresIn < PRESIGN_MIN_EXPIRY || expiresIn > PRESIGN_MAX_EXPIRY) {
|
|
335
|
+
throw new ValidationError(
|
|
336
|
+
`expiresIn must be an integer between ${PRESIGN_MIN_EXPIRY} and ${PRESIGN_MAX_EXPIRY} seconds`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
const { accountName, accountKey } = this.auth;
|
|
340
|
+
if (!accountKey) {
|
|
341
|
+
throw new StorageError("config", "AzureDriver needs accountKey to create presigned URLs", { provider: PROVIDER });
|
|
342
|
+
}
|
|
343
|
+
const sas = await blobReadSas(accountName, accountKey, this.container, path, new Date(Date.now() + expiresIn * 1e3));
|
|
344
|
+
return `${this.blobUrl(path)}?${sas}`;
|
|
345
|
+
}
|
|
346
|
+
/** The blob name an upload with `options.id` is stored under: `<path>/<id>.<ext>`. */
|
|
347
|
+
pathFor(options) {
|
|
348
|
+
const ext = options.filename.includes(".") ? options.filename.split(".").pop() : "";
|
|
349
|
+
const filename = ext ? `${options.id}.${ext}` : options.id;
|
|
350
|
+
return options.path ? `${options.path}/${filename}` : filename;
|
|
351
|
+
}
|
|
352
|
+
/** The blob at `path` (a `HEAD` request), or `null` if it does not exist. */
|
|
353
|
+
async head(path, options) {
|
|
354
|
+
const res = await this.request("HEAD", path, withSignal(options));
|
|
355
|
+
if (res.status === 404 && errorCode(res) !== "ContainerNotFound") return null;
|
|
356
|
+
if (!res.ok) throw await azureError("Azure head failed", res);
|
|
357
|
+
const etag = res.headers.get("etag") ?? void 0;
|
|
358
|
+
const token = res.headers.get(TOKEN_HEADER) ?? void 0;
|
|
359
|
+
const modified = res.headers.get("last-modified");
|
|
360
|
+
const contentType = res.headers.get("content-type") ?? void 0;
|
|
361
|
+
const name = path.split("/").pop() ?? path;
|
|
362
|
+
return {
|
|
363
|
+
id: name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : name,
|
|
364
|
+
url: `${this.baseUrl}/${urlEncodePath(path)}`,
|
|
365
|
+
path,
|
|
366
|
+
size: Number(res.headers.get("content-length") ?? 0),
|
|
367
|
+
meta: { container: this.container, ...etag ? { etag } : {} },
|
|
368
|
+
...token ? { uploadToken: token } : {},
|
|
369
|
+
...modified ? { lastModified: new Date(modified) } : {},
|
|
370
|
+
...contentType ? { contentType } : {}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
/** Whether blocks have been staged for `path` but not yet committed. */
|
|
374
|
+
async isPending(path, options) {
|
|
375
|
+
const res = await this.request("GET", path, {
|
|
376
|
+
query: { comp: "blocklist", blocklisttype: "uncommitted" },
|
|
377
|
+
...withSignal(options)
|
|
378
|
+
});
|
|
379
|
+
if (res.status === 404 && errorCode(res) !== "ContainerNotFound") return false;
|
|
380
|
+
const body = await res.text().catch(() => "");
|
|
381
|
+
if (!res.ok) throw azureErrorFrom("Azure get block list failed", res, body);
|
|
382
|
+
const uncommitted = body.match(/<UncommittedBlocks>(.*?)<\/UncommittedBlocks>/s)?.[1] ?? "";
|
|
383
|
+
return uncommitted.includes("<Block>");
|
|
384
|
+
}
|
|
385
|
+
blobUrl(path) {
|
|
386
|
+
return `${this.endpoint}/${this.container}/${path.split("/").map(encodeURIComponent).join("/")}`;
|
|
387
|
+
}
|
|
388
|
+
/** A signed (Shared Key) or SAS-authorized request to one blob. */
|
|
389
|
+
async request(method, path, init = {}) {
|
|
390
|
+
const url = new URL(this.blobUrl(path));
|
|
391
|
+
for (const [k, v] of Object.entries(init.query ?? {})) url.searchParams.set(k, v);
|
|
392
|
+
let headers;
|
|
393
|
+
const { accountName, accountKey, sasToken } = this.auth;
|
|
394
|
+
if (accountKey) {
|
|
395
|
+
headers = await signRequest(method, url, accountName, accountKey, init.headers ?? {}, init.body?.byteLength);
|
|
396
|
+
} else {
|
|
397
|
+
for (const [k, v] of new URLSearchParams(sasToken)) url.searchParams.set(k, v);
|
|
398
|
+
headers = {
|
|
399
|
+
...init.headers,
|
|
400
|
+
"x-ms-date": (/* @__PURE__ */ new Date()).toUTCString(),
|
|
401
|
+
"x-ms-version": SERVICE_VERSION
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return fetchWithAbort(url, {
|
|
405
|
+
method,
|
|
406
|
+
headers,
|
|
407
|
+
...init.body ? { body: init.body } : {},
|
|
408
|
+
signal: init.signal ?? null
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
function withSignal(options) {
|
|
413
|
+
return options?.signal ? { signal: options.signal } : {};
|
|
414
|
+
}
|
|
415
|
+
function accountFromEndpoint(endpoint) {
|
|
416
|
+
const url = new URL(endpoint);
|
|
417
|
+
const fromHost = url.hostname.match(/^([a-z0-9]+)\.blob\./)?.[1];
|
|
418
|
+
return fromHost ?? (url.pathname.split("/").filter(Boolean)[0] || void 0);
|
|
419
|
+
}
|
|
420
|
+
async function fetchWithAbort(url, init) {
|
|
421
|
+
try {
|
|
422
|
+
return await fetch(url, init);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
if (isAbortError(err)) throw err;
|
|
425
|
+
const cause = err instanceof Error ? err : new Error(String(err));
|
|
426
|
+
throw new StorageError("network", `${PROVIDER} network error: ${cause.message}`, { provider: PROVIDER, cause });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function errorCode(res, body) {
|
|
430
|
+
return res.headers.get("x-ms-error-code") ?? body?.match(/<Code>(.*?)<\/Code>/s)?.[1];
|
|
431
|
+
}
|
|
432
|
+
function azureErrorFrom(prefix, res, body) {
|
|
433
|
+
const code = errorCode(res, body);
|
|
434
|
+
return new StorageError(
|
|
435
|
+
classifyAzureError(res.status, code),
|
|
436
|
+
`${prefix}: ${res.status} ${res.statusText}${code ? ` (${code})` : ""}`,
|
|
437
|
+
{ statusCode: res.status, provider: PROVIDER }
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
async function azureError(prefix, res) {
|
|
441
|
+
const body = await res.text().catch(() => "");
|
|
442
|
+
return azureErrorFrom(prefix, res, body);
|
|
443
|
+
}
|
|
444
|
+
function concatBytes(chunks, total) {
|
|
445
|
+
const out = new Uint8Array(total);
|
|
446
|
+
let offset = 0;
|
|
447
|
+
for (const chunk of chunks) {
|
|
448
|
+
out.set(chunk, offset);
|
|
449
|
+
offset += chunk.byteLength;
|
|
450
|
+
}
|
|
451
|
+
return out;
|
|
452
|
+
}
|
|
453
|
+
export {
|
|
454
|
+
AzureDriver
|
|
455
|
+
};
|
|
456
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/sharedkey.ts","../src/internal.ts"],"sourcesContent":["import type { BaseDriver, OperationOptions, PresignedUrlOptions, StoredObject, UploadContext, UploadOptions, UploadResult } from \"@fileway/core\";\nimport { validateUploadOptions, ValidationError, urlEncodePath, abortError, isAbortError, StorageError } from \"@fileway/core\";\nimport { blobReadSas, bytesToBase64, signRequest, SERVICE_VERSION } from \"./sharedkey.js\";\nimport { classifyAzureError, parseConnectionString } from \"./internal.js\";\n\nconst randomUUID = () => globalThis.crypto.randomUUID();\nconst PROVIDER = \"azure\";\n\nconst DEFAULT_BLOCK_SIZE = 8 * 1024 * 1024;\nconst MIN_BLOCK_SIZE = 1024 * 1024;\n// Put Block accepts up to 4000 MiB per block.\nconst MAX_BLOCK_SIZE = 4000 * 1024 * 1024;\n// A block blob holds at most 50,000 committed blocks.\nconst MAX_BLOCKS = 50_000;\nconst PRESIGN_DEFAULT_EXPIRY = 3600;\nconst PRESIGN_MIN_EXPIRY = 1;\nconst PRESIGN_MAX_EXPIRY = 604800;\n// Metadata names must be valid C# identifiers, so no hyphens here.\nconst TOKEN_KEY = \"fileway_upload_token\";\nconst TOKEN_HEADER = `x-ms-meta-${TOKEN_KEY}`;\nconst METADATA_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n\nexport interface AzureDriverConfig {\n /** Container to store blobs in. */\n container: string;\n /** Storage account name. Required unless a connection string provides it. */\n accountName?: string;\n /** Base64 account key, used for Shared Key auth and presigned URLs. */\n accountKey?: string;\n /** A SAS token (with or without the leading `?`), used instead of an account key. */\n sasToken?: string;\n /** An Azure Storage connection string, e.g. from the portal or\n * `UseDevelopmentStorage=true` for Azurite. Explicit fields override it. */\n connectionString?: string;\n /** Blob service endpoint. Defaults to `https://<account>.blob.core.windows.net`.\n * For Azurite: `http://127.0.0.1:10000/devstoreaccount1`. */\n endpoint?: string;\n /** Public base URL for `getUrl` (e.g. a CDN). Defaults to `<endpoint>/<container>`. */\n baseUrl?: string;\n /** Block size in bytes. Default 8 MiB, clamped to 1 MiB..4000 MiB. Also the\n * largest upload sent as a single Put Blob. */\n blockSize?: number;\n /** Reject uploads larger than this many bytes with a `size-exceeded` error. */\n maxSizeBytes?: number;\n}\n\nexport type AzureMeta = {\n container: string;\n etag?: string;\n};\n\ninterface ResolvedAuth {\n accountName: string;\n accountKey?: string;\n sasToken?: string;\n}\n\nexport class AzureDriver implements BaseDriver<AzureMeta> {\n readonly name = \"azure\";\n private container: string;\n private auth: ResolvedAuth;\n private endpoint: string;\n private baseUrl: string;\n private blockSize: number;\n private maxSizeBytes: number | undefined;\n\n constructor(config: AzureDriverConfig) {\n const fromString = config.connectionString ? parseConnectionString(config.connectionString) : {};\n const accountName = config.accountName ?? fromString.accountName;\n const accountKey = config.accountKey ?? fromString.accountKey;\n const sasToken = config.sasToken ?? fromString.sasToken;\n const endpoint = config.endpoint ?? fromString.endpoint;\n\n if (!config.container) {\n throw new StorageError(\"config\", \"AzureDriver requires a container\", { provider: PROVIDER });\n }\n const name = accountName ?? (endpoint ? accountFromEndpoint(endpoint) : undefined);\n if (!name) {\n throw new StorageError(\"config\", \"AzureDriver requires accountName (or a connection string or endpoint)\", { provider: PROVIDER });\n }\n if (!accountKey && !sasToken) {\n throw new StorageError(\"config\", \"AzureDriver requires accountKey, sasToken or a connection string with credentials\", { provider: PROVIDER });\n }\n\n this.container = config.container;\n this.auth = {\n accountName: name,\n ...(accountKey ? { accountKey } : {}),\n ...(sasToken ? { sasToken: sasToken.replace(/^\\?/, \"\") } : {}),\n };\n this.endpoint = (endpoint ?? `https://${name}.blob.core.windows.net`).replace(/\\/+$/, \"\");\n this.baseUrl = (config.baseUrl ?? `${this.endpoint}/${config.container}`).replace(/\\/+$/, \"\");\n this.blockSize = Math.min(Math.max(config.blockSize ?? DEFAULT_BLOCK_SIZE, MIN_BLOCK_SIZE), MAX_BLOCK_SIZE);\n this.maxSizeBytes = config.maxSizeBytes;\n }\n\n async upload(\n stream: ReadableStream<Uint8Array>,\n options: UploadOptions,\n context?: UploadContext,\n ): Promise<UploadResult<AzureMeta>> {\n validateUploadOptions(options);\n for (const key of Object.keys(options.metadata ?? {})) {\n if (!METADATA_NAME.test(key)) {\n throw new ValidationError(\n `Azure metadata keys must be letters, digits and underscores, not starting with a digit (got \"${key}\")`,\n );\n }\n }\n if (this.maxSizeBytes !== undefined && options.size !== undefined && options.size > this.maxSizeBytes) {\n throw new StorageError(\"size-exceeded\", `file size ${options.size} exceeds maxSizeBytes ${this.maxSizeBytes}`, { provider: PROVIDER });\n }\n\n const id = options.id ?? randomUUID();\n const path = this.pathFor({ ...options, id });\n const contentType = options.mimeType ?? \"application/octet-stream\";\n const metaHeaders: Record<string, string> = {\n ...Object.fromEntries(Object.entries(options.metadata ?? {}).map(([k, v]) => [`x-ms-meta-${k}`, v])),\n ...(context?.uploadToken ? { [TOKEN_HEADER]: context.uploadToken } : {}),\n };\n const signal = options.signal;\n\n // Read one block at a time, so peak memory is bounded by `blockSize`.\n const reader = stream.getReader();\n let aborted = signal?.aborted ?? false;\n const onAbort = () => {\n aborted = true;\n // Cancelling the reader wakes any pending `read()` with `done: true`.\n reader.cancel().catch(() => {});\n };\n if (signal && !aborted) signal.addEventListener(\"abort\", onAbort, { once: true });\n // Uncommitted blocks are discarded by the service after 7 days, and never\n // touch an existing committed blob, so an abort needs no server cleanup.\n const ensureNotAborted = () => {\n if (aborted) throw abortError(\"upload aborted\");\n };\n\n let buffer: Uint8Array[] = [];\n let bufferedLen = 0;\n let totalBytes = 0;\n const blockIds: string[] = [];\n\n const putBlock = async (bytes: Uint8Array) => {\n if (blockIds.length >= MAX_BLOCKS) {\n throw new StorageError(\"size-exceeded\", `upload exceeds ${MAX_BLOCKS} blocks; raise blockSize`, { provider: PROVIDER });\n }\n const blockId = bytesToBase64(new TextEncoder().encode(`fileway-${String(blockIds.length).padStart(6, \"0\")}`));\n const res = await this.request(\"PUT\", path, {\n query: { comp: \"block\", blockid: blockId },\n body: bytes,\n ...(signal ? { signal } : {}),\n });\n if (!res.ok) throw await azureError(`Azure put block ${blockIds.length + 1} failed`, res);\n blockIds.push(blockId);\n };\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n ensureNotAborted();\n if (done) break;\n totalBytes += value.byteLength;\n if (this.maxSizeBytes !== undefined && totalBytes > this.maxSizeBytes) {\n await reader.cancel().catch(() => {});\n throw new StorageError(\"size-exceeded\", `upload exceeds maxSizeBytes ${this.maxSizeBytes}`, { provider: PROVIDER });\n }\n buffer.push(value);\n bufferedLen += value.byteLength;\n\n while (bufferedLen >= this.blockSize) {\n const merged = concatBytes(buffer, bufferedLen);\n await putBlock(merged.subarray(0, this.blockSize));\n const rest = merged.subarray(this.blockSize);\n buffer = rest.byteLength > 0 ? [rest] : [];\n bufferedLen = rest.byteLength;\n }\n }\n\n if (options.size !== undefined && totalBytes !== options.size) {\n throw new ValidationError(`stream delivered ${totalBytes} bytes, expected ${options.size}`);\n }\n\n let res: Response;\n if (blockIds.length === 0) {\n // Small file: one Put Blob carries content, type and metadata.\n res = await this.request(\"PUT\", path, {\n headers: { \"x-ms-blob-type\": \"BlockBlob\", \"content-type\": contentType, ...metaHeaders },\n body: concatBytes(buffer, bufferedLen),\n ...(signal ? { signal } : {}),\n });\n if (!res.ok) throw await azureError(\"Azure upload failed\", res);\n } else {\n if (bufferedLen > 0) await putBlock(concatBytes(buffer, bufferedLen));\n ensureNotAborted();\n const xml = `<?xml version=\"1.0\" encoding=\"utf-8\"?><BlockList>${blockIds\n .map((b) => `<Latest>${b}</Latest>`)\n .join(\"\")}</BlockList>`;\n res = await this.request(\"PUT\", path, {\n query: { comp: \"blocklist\" },\n headers: { \"content-type\": \"application/xml\", \"x-ms-blob-content-type\": contentType, ...metaHeaders },\n body: new TextEncoder().encode(xml),\n ...(signal ? { signal } : {}),\n });\n if (!res.ok) throw await azureError(\"Azure put block list failed\", res);\n }\n\n const etag = res.headers.get(\"etag\") ?? undefined;\n return {\n id,\n url: `${this.baseUrl}/${urlEncodePath(path)}`,\n path,\n size: totalBytes,\n meta: { container: this.container, ...(etag ? { etag } : {}) },\n };\n } catch (err) {\n reader.cancel().catch(() => {});\n throw err;\n } finally {\n signal?.removeEventListener(\"abort\", onAbort);\n }\n }\n\n /** Deletes the blob; a missing blob is not an error. */\n async delete(path: string, options?: OperationOptions): Promise<void> {\n const res = await this.request(\"DELETE\", path, withSignal(options));\n if (res.status === 404 && errorCode(res) !== \"ContainerNotFound\") return;\n if (!res.ok) throw await azureError(\"Azure delete failed\", res);\n }\n\n async getUrl(path: string, _options?: OperationOptions): Promise<string> {\n return `${this.baseUrl}/${urlEncodePath(path)}`;\n }\n\n /** Streams the blob. `options.signal` also cancels reading the body. */\n async get(path: string, options?: OperationOptions): Promise<ReadableStream<Uint8Array>> {\n const res = await this.request(\"GET\", path, withSignal(options));\n if (!res.ok) throw await azureError(\"Azure get failed\", res);\n if (!res.body) {\n throw new StorageError(\"provider-error\", \"Azure get returned no response body\", { provider: PROVIDER });\n }\n return res.body;\n }\n\n /**\n * A read-only service SAS URL for `path`, valid for `expiresIn` seconds\n * (integer in 1..604800, default 3600). Needs an account key.\n */\n async getPresignedUrl(path: string, options?: PresignedUrlOptions): Promise<string> {\n const expiresIn = options?.expiresIn ?? PRESIGN_DEFAULT_EXPIRY;\n if (!Number.isInteger(expiresIn) || expiresIn < PRESIGN_MIN_EXPIRY || expiresIn > PRESIGN_MAX_EXPIRY) {\n throw new ValidationError(\n `expiresIn must be an integer between ${PRESIGN_MIN_EXPIRY} and ${PRESIGN_MAX_EXPIRY} seconds`,\n );\n }\n const { accountName, accountKey } = this.auth;\n if (!accountKey) {\n throw new StorageError(\"config\", \"AzureDriver needs accountKey to create presigned URLs\", { provider: PROVIDER });\n }\n const sas = await blobReadSas(accountName, accountKey, this.container, path, new Date(Date.now() + expiresIn * 1000));\n return `${this.blobUrl(path)}?${sas}`;\n }\n\n /** The blob name an upload with `options.id` is stored under: `<path>/<id>.<ext>`. */\n pathFor(options: UploadOptions & { id: string }): string {\n const ext = options.filename.includes(\".\") ? options.filename.split(\".\").pop()! : \"\";\n const filename = ext ? `${options.id}.${ext}` : options.id;\n return options.path ? `${options.path}/${filename}` : filename;\n }\n\n /** The blob at `path` (a `HEAD` request), or `null` if it does not exist. */\n async head(path: string, options?: OperationOptions): Promise<StoredObject<AzureMeta> | null> {\n const res = await this.request(\"HEAD\", path, withSignal(options));\n if (res.status === 404 && errorCode(res) !== \"ContainerNotFound\") return null;\n if (!res.ok) throw await azureError(\"Azure head failed\", res);\n\n const etag = res.headers.get(\"etag\") ?? undefined;\n const token = res.headers.get(TOKEN_HEADER) ?? undefined;\n const modified = res.headers.get(\"last-modified\");\n const contentType = res.headers.get(\"content-type\") ?? undefined;\n const name = path.split(\"/\").pop() ?? path;\n return {\n id: name.includes(\".\") ? name.slice(0, name.lastIndexOf(\".\")) : name,\n url: `${this.baseUrl}/${urlEncodePath(path)}`,\n path,\n size: Number(res.headers.get(\"content-length\") ?? 0),\n meta: { container: this.container, ...(etag ? { etag } : {}) },\n ...(token ? { uploadToken: token } : {}),\n ...(modified ? { lastModified: new Date(modified) } : {}),\n ...(contentType ? { contentType } : {}),\n };\n }\n\n /** Whether blocks have been staged for `path` but not yet committed. */\n async isPending(path: string, options?: OperationOptions): Promise<boolean> {\n const res = await this.request(\"GET\", path, {\n query: { comp: \"blocklist\", blocklisttype: \"uncommitted\" },\n ...withSignal(options),\n });\n if (res.status === 404 && errorCode(res) !== \"ContainerNotFound\") return false;\n const body = await res.text().catch(() => \"\");\n if (!res.ok) throw azureErrorFrom(\"Azure get block list failed\", res, body);\n const uncommitted = body.match(/<UncommittedBlocks>(.*?)<\\/UncommittedBlocks>/s)?.[1] ?? \"\";\n return uncommitted.includes(\"<Block>\");\n }\n\n private blobUrl(path: string): string {\n return `${this.endpoint}/${this.container}/${path.split(\"/\").map(encodeURIComponent).join(\"/\")}`;\n }\n\n /** A signed (Shared Key) or SAS-authorized request to one blob. */\n private async request(\n method: string,\n path: string,\n init: {\n query?: Record<string, string>;\n headers?: Record<string, string>;\n body?: Uint8Array;\n signal?: AbortSignal;\n } = {},\n ): Promise<Response> {\n const url = new URL(this.blobUrl(path));\n for (const [k, v] of Object.entries(init.query ?? {})) url.searchParams.set(k, v);\n\n let headers: Record<string, string>;\n const { accountName, accountKey, sasToken } = this.auth;\n if (accountKey) {\n headers = await signRequest(method, url, accountName, accountKey, init.headers ?? {}, init.body?.byteLength);\n } else {\n for (const [k, v] of new URLSearchParams(sasToken)) url.searchParams.set(k, v);\n headers = {\n ...init.headers,\n \"x-ms-date\": new Date().toUTCString(),\n \"x-ms-version\": SERVICE_VERSION,\n };\n }\n\n return fetchWithAbort(url, {\n method,\n headers,\n ...(init.body ? { body: init.body as BodyInit } : {}),\n signal: init.signal ?? null,\n });\n }\n}\n\nfunction withSignal(options: OperationOptions | undefined): { signal?: AbortSignal } {\n return options?.signal ? { signal: options.signal } : {};\n}\n\n/** The account name from a `<account>.blob.<suffix>` host or an Azurite-style\n * `http://host:port/<account>` endpoint. */\nfunction accountFromEndpoint(endpoint: string): string | undefined {\n const url = new URL(endpoint);\n const fromHost = url.hostname.match(/^([a-z0-9]+)\\.blob\\./)?.[1];\n return fromHost ?? (url.pathname.split(\"/\").filter(Boolean)[0] || undefined);\n}\n\n/** `fetch` that converts non-abort failures into a typed `network` error,\n * while letting aborts (`AbortError`) pass through untouched. */\nasync function fetchWithAbort(url: string | URL, init: RequestInit): Promise<Response> {\n try {\n return await fetch(url, init);\n } catch (err) {\n if (isAbortError(err)) throw err;\n const cause = err instanceof Error ? err : new Error(String(err));\n throw new StorageError(\"network\", `${PROVIDER} network error: ${cause.message}`, { provider: PROVIDER, cause });\n }\n}\n\n/** The service error code: the `x-ms-error-code` header, which Azure sends on\n * every failure (including bodiless HEAD responses), or the XML `<Code>`. */\nfunction errorCode(res: Response, body?: string): string | undefined {\n return res.headers.get(\"x-ms-error-code\") ?? body?.match(/<Code>(.*?)<\\/Code>/s)?.[1];\n}\n\nfunction azureErrorFrom(prefix: string, res: Response, body: string): StorageError {\n const code = errorCode(res, body);\n return new StorageError(\n classifyAzureError(res.status, code),\n `${prefix}: ${res.status} ${res.statusText}${code ? ` (${code})` : \"\"}`,\n { statusCode: res.status, provider: PROVIDER },\n );\n}\n\nasync function azureError(prefix: string, res: Response): Promise<StorageError> {\n const body = await res.text().catch(() => \"\");\n return azureErrorFrom(prefix, res, body);\n}\n\nfunction concatBytes(chunks: Uint8Array[], total: number): Uint8Array {\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return out;\n}\n","// Azure Storage Shared Key and service SAS signing, using only Web Crypto.\n// https://learn.microsoft.com/rest/api/storageservices/authorize-with-shared-key\n// https://learn.microsoft.com/rest/api/storageservices/create-service-sas\n\nexport const SERVICE_VERSION = \"2023-11-03\";\n\nconst encoder = new TextEncoder();\nconst keyCache = new Map<string, Promise<CryptoKey>>();\n\nfunction importKey(accountKey: string): Promise<CryptoKey> {\n let key = keyCache.get(accountKey);\n if (!key) {\n key = globalThis.crypto.subtle.importKey(\n \"raw\",\n base64ToBytes(accountKey),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n keyCache.set(accountKey, key);\n }\n return key;\n}\n\n/** Base64 HMAC-SHA256 of `message` with the base64-encoded account key. */\nexport async function hmacBase64(accountKey: string, message: string): Promise<string> {\n const key = await importKey(accountKey);\n const sig = await globalThis.crypto.subtle.sign(\"HMAC\", key, encoder.encode(message));\n return bytesToBase64(new Uint8Array(sig));\n}\n\n/**\n * The Shared Key string-to-sign for a request. `headers` are the request\n * headers (any case); `contentLength` is the body size in bytes (0 or\n * undefined signs as an empty value, as the service requires since 2015-02-21).\n */\nexport function buildStringToSign(\n method: string,\n url: URL,\n accountName: string,\n headers: Record<string, string>,\n contentLength: number | undefined,\n): string {\n const lower: Record<string, string> = {};\n for (const [k, v] of Object.entries(headers)) lower[k.toLowerCase()] = v;\n const h = (name: string) => lower[name] ?? \"\";\n\n const canonicalHeaders = Object.keys(lower)\n .filter((k) => k.startsWith(\"x-ms-\"))\n .sort()\n .map((k) => `${k}:${(lower[k] ?? \"\").replace(/\\s+/g, \" \").trim()}\\n`)\n .join(\"\");\n\n // Query parameters: lowercase names, sorted, decoded values joined by commas.\n const params = new Map<string, string[]>();\n for (const [k, v] of url.searchParams) {\n const name = k.toLowerCase();\n params.set(name, [...(params.get(name) ?? []), v]);\n }\n const canonicalQuery = [...params.keys()]\n .sort()\n .map((k) => `\\n${k}:${(params.get(k) ?? []).sort().join(\",\")}`)\n .join(\"\");\n const canonicalResource = `/${accountName}${url.pathname}${canonicalQuery}`;\n\n return [\n method.toUpperCase(),\n h(\"content-encoding\"),\n h(\"content-language\"),\n contentLength ? String(contentLength) : \"\",\n h(\"content-md5\"),\n h(\"content-type\"),\n h(\"date\"),\n h(\"if-modified-since\"),\n h(\"if-match\"),\n h(\"if-none-match\"),\n h(\"if-unmodified-since\"),\n h(\"range\"),\n canonicalHeaders + canonicalResource,\n ].join(\"\\n\");\n}\n\n/** `x-ms-date`, `x-ms-version` and `Authorization: SharedKey` headers for a request. */\nexport async function signRequest(\n method: string,\n url: URL,\n accountName: string,\n accountKey: string,\n headers: Record<string, string>,\n contentLength: number | undefined,\n now: Date = new Date(),\n): Promise<Record<string, string>> {\n const signed = {\n ...headers,\n \"x-ms-date\": now.toUTCString(),\n \"x-ms-version\": SERVICE_VERSION,\n };\n const stringToSign = buildStringToSign(method, url, accountName, signed, contentLength);\n const signature = await hmacBase64(accountKey, stringToSign);\n return { ...signed, authorization: `SharedKey ${accountName}:${signature}` };\n}\n\n/**\n * Query string for a read-only service SAS on one blob. `blobName` is the raw\n * (unencoded) blob name; `expiry` is when the link stops working.\n */\nexport async function blobReadSas(\n accountName: string,\n accountKey: string,\n container: string,\n blobName: string,\n expiry: Date,\n): Promise<string> {\n const se = expiry.toISOString().replace(/\\.\\d{3}Z$/, \"Z\");\n const fields = {\n sp: \"r\",\n st: \"\",\n se,\n resource: `/blob/${accountName}/${container}/${blobName}`,\n si: \"\",\n sip: \"\",\n spr: \"\",\n sv: SERVICE_VERSION,\n sr: \"b\",\n snapshot: \"\",\n ses: \"\",\n rscc: \"\",\n rscd: \"\",\n rsce: \"\",\n rscl: \"\",\n rsct: \"\",\n };\n const signature = await hmacBase64(accountKey, Object.values(fields).join(\"\\n\"));\n const query = new URLSearchParams({ sv: SERVICE_VERSION, se, sr: \"b\", sp: \"r\", sig: signature });\n return query.toString();\n}\n\nexport function base64ToBytes(value: string) {\n const binary = atob(value);\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);\n return out;\n}\n\nexport function bytesToBase64(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n","import type { StorageErrorCode } from \"@fileway/core\";\n\n// Internal: connection-string parsing and error classification for\n// AzureDriver. Not exported from the package.\n\n// Azurite's well-known development account (public, documented by Microsoft).\nconst DEV_ACCOUNT = \"devstoreaccount1\";\nconst DEV_KEY = \"Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==\";\nconst DEV_ENDPOINT = \"http://127.0.0.1:10000/devstoreaccount1\";\n\n/** Splits a connection string into the fields this driver understands. */\nexport function parseConnectionString(value: string): {\n accountName?: string;\n accountKey?: string;\n sasToken?: string;\n endpoint?: string;\n} {\n const parts: Record<string, string> = {};\n for (const segment of value.split(\";\")) {\n const eq = segment.indexOf(\"=\");\n if (eq > 0) parts[segment.slice(0, eq).trim()] = segment.slice(eq + 1).trim();\n }\n if (parts.UseDevelopmentStorage === \"true\") {\n return { accountName: DEV_ACCOUNT, accountKey: DEV_KEY, endpoint: DEV_ENDPOINT };\n }\n const accountName = parts.AccountName;\n const endpoint =\n parts.BlobEndpoint ??\n (accountName\n ? `${parts.DefaultEndpointsProtocol ?? \"https\"}://${accountName}.blob.${parts.EndpointSuffix ?? \"core.windows.net\"}`\n : undefined);\n return {\n ...(accountName ? { accountName } : {}),\n ...(parts.AccountKey ? { accountKey: parts.AccountKey } : {}),\n ...(parts.SharedAccessSignature ? { sasToken: parts.SharedAccessSignature } : {}),\n ...(endpoint ? { endpoint } : {}),\n };\n}\n\n/** Classifies an Azure failure into a typed code from the service error code,\n * falling back to the HTTP status. */\nexport function classifyAzureError(status: number, code: string | undefined): StorageErrorCode {\n switch (code) {\n case \"ContainerNotFound\":\n case \"ContainerBeingDeleted\":\n return \"bucket-not-found\";\n case \"BlobNotFound\":\n case \"ResourceNotFound\":\n return \"not-found\";\n case \"AuthenticationFailed\":\n case \"AuthorizationFailure\":\n case \"AuthorizationPermissionMismatch\":\n case \"AuthorizationResourceTypeMismatch\":\n case \"NoAuthenticationInformation\":\n case \"InvalidAuthenticationInfo\":\n return \"auth-failed\";\n case \"RequestBodyTooLarge\":\n case \"BlockCountExceedsLimit\":\n return \"size-exceeded\";\n case \"ServerBusy\":\n case \"TooManyRequests\":\n return \"rate-limited\";\n }\n if (status === 429) return \"rate-limited\";\n if (status === 401 || status === 403) return \"auth-failed\";\n if (status === 404) return \"not-found\";\n if (status === 413) return \"size-exceeded\";\n return \"provider-error\";\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fileway/azure",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fileway",
|
|
7
|
+
"file-upload",
|
|
8
|
+
"storage",
|
|
9
|
+
"azure",
|
|
10
|
+
"azure-blob",
|
|
11
|
+
"blob-storage",
|
|
12
|
+
"azurite",
|
|
13
|
+
"stream"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/Goldenhub/fileway.git"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"worker": "./dist/index.js",
|
|
25
|
+
"deno": "./dist/index.js",
|
|
26
|
+
"bun": "./dist/index.js",
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"require": "./dist/index.cjs"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@fileway/core": "0.5.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"tsup": "^8.0.0",
|
|
42
|
+
"typescript": "^5.5.0",
|
|
43
|
+
"vitest": "^1.6.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"test": "vitest run"
|
|
48
|
+
}
|
|
6
49
|
}
|