@ttt-productions/media-processing-core 0.8.15 → 0.8.17
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/audio/audio-processor.d.ts +2 -2
- package/dist/audio/audio-processor.d.ts.map +1 -1
- package/dist/audio/audio-processor.js +62 -48
- package/dist/audio/audio-processor.js.map +1 -1
- package/dist/audio/probe.d.ts +1 -1
- package/dist/audio/probe.d.ts.map +1 -1
- package/dist/audio/probe.js +6 -3
- package/dist/audio/probe.js.map +1 -1
- package/dist/duration/media-duration.d.ts +2 -2
- package/dist/duration/media-duration.d.ts.map +1 -1
- package/dist/duration/media-duration.js +23 -21
- package/dist/duration/media-duration.js.map +1 -1
- package/dist/image/formats.d.ts +2 -2
- package/dist/image/formats.js +25 -25
- package/dist/image/formats.js.map +1 -1
- package/dist/image/image-processor.d.ts +2 -2
- package/dist/image/image-processor.d.ts.map +1 -1
- package/dist/image/image-processor.js +47 -37
- package/dist/image/image-processor.js.map +1 -1
- package/dist/image/resize.d.ts +2 -2
- package/dist/image/resize.d.ts.map +1 -1
- package/dist/image/resize.js +13 -13
- package/dist/image/resize.js.map +1 -1
- package/dist/index.d.ts +19 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -16
- package/dist/index.js.map +1 -1
- package/dist/inspection/inspect-media.d.ts +13 -13
- package/dist/inspection/inspect-media.d.ts.map +1 -1
- package/dist/inspection/inspect-media.js +196 -140
- package/dist/inspection/inspect-media.js.map +1 -1
- package/dist/io/fs.d.ts +1 -1
- package/dist/io/fs.js +4 -4
- package/dist/io/fs.js.map +1 -1
- package/dist/io/local-input.d.ts +1 -1
- package/dist/io/local-input.d.ts.map +1 -1
- package/dist/io/local-input.js +15 -15
- package/dist/io/local-input.js.map +1 -1
- package/dist/io/types.d.ts.map +1 -1
- package/dist/io/types.js.map +1 -1
- package/dist/moderation/merge.d.ts +1 -1
- package/dist/moderation/merge.d.ts.map +1 -1
- package/dist/moderation/merge.js +1 -1
- package/dist/moderation/merge.js.map +1 -1
- package/dist/moderation/types.d.ts +3 -3
- package/dist/moderation/types.js.map +1 -1
- package/dist/process-media.d.ts +2 -3
- package/dist/process-media.d.ts.map +1 -1
- package/dist/process-media.js +9 -9
- package/dist/process-media.js.map +1 -1
- package/dist/processing/errors.d.ts.map +1 -1
- package/dist/processing/errors.js +1 -1
- package/dist/processing/errors.js.map +1 -1
- package/dist/processing/result.d.ts +2 -2
- package/dist/processing/result.js +1 -1
- package/dist/processing/result.js.map +1 -1
- package/dist/run-pipeline.d.ts +5 -5
- package/dist/run-pipeline.d.ts.map +1 -1
- package/dist/run-pipeline.js +70 -54
- package/dist/run-pipeline.js.map +1 -1
- package/dist/server/download-token.d.ts +14 -0
- package/dist/server/download-token.d.ts.map +1 -0
- package/dist/server/download-token.js +19 -0
- package/dist/server/download-token.js.map +1 -0
- package/dist/server/firebase-media-io.d.ts +2 -2
- package/dist/server/firebase-media-io.js +1 -1
- package/dist/server/firebase-media-io.js.map +1 -1
- package/dist/server/gcs-errors.js +2 -2
- package/dist/server/gcs-errors.js.map +1 -1
- package/dist/server/index.d.ts +7 -5
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +7 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/pending-media.d.ts +2 -2
- package/dist/server/pending-media.d.ts.map +1 -1
- package/dist/server/pending-media.js +12 -6
- package/dist/server/pending-media.js.map +1 -1
- package/dist/server/r2-retry.d.ts +4 -4
- package/dist/server/r2-retry.d.ts.map +1 -1
- package/dist/server/r2-retry.js +48 -47
- package/dist/server/r2-retry.js.map +1 -1
- package/dist/server/staged-object.d.ts +2 -2
- package/dist/server/staged-object.d.ts.map +1 -1
- package/dist/server/staged-object.js +4 -4
- package/dist/server/staged-object.js.map +1 -1
- package/dist/server/storage-ops.d.ts +3 -5
- package/dist/server/storage-ops.d.ts.map +1 -1
- package/dist/server/storage-ops.js +101 -97
- package/dist/server/storage-ops.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js.map +1 -1
- package/dist/utils/log.d.ts.map +1 -1
- package/dist/utils/log.js +3 -3
- package/dist/utils/log.js.map +1 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +1 -4
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/safe-path.js +8 -8
- package/dist/utils/safe-path.js.map +1 -1
- package/dist/validation/validate-duration.d.ts.map +1 -1
- package/dist/validation/validate-duration.js.map +1 -1
- package/dist/validation/validate-mime.d.ts.map +1 -1
- package/dist/validation/validate-mime.js.map +1 -1
- package/dist/validation/validate-size.d.ts.map +1 -1
- package/dist/validation/validate-size.js.map +1 -1
- package/dist/video/ffmpeg.d.ts +1 -1
- package/dist/video/ffmpeg.d.ts.map +1 -1
- package/dist/video/ffmpeg.js +51 -43
- package/dist/video/ffmpeg.js.map +1 -1
- package/dist/video/probe.d.ts +1 -1
- package/dist/video/probe.d.ts.map +1 -1
- package/dist/video/probe.js +10 -7
- package/dist/video/probe.js.map +1 -1
- package/dist/video/video-processor.d.ts +2 -2
- package/dist/video/video-processor.d.ts.map +1 -1
- package/dist/video/video-processor.js +123 -81
- package/dist/video/video-processor.js.map +1 -1
- package/dist/workspace/temp.js +4 -4
- package/dist/workspace/temp.js.map +1 -1
- package/package.json +2 -2
- package/dist/audio/index.d.ts +0 -3
- package/dist/audio/index.d.ts.map +0 -1
- package/dist/audio/index.js +0 -3
- package/dist/audio/index.js.map +0 -1
- package/dist/image/index.d.ts +0 -4
- package/dist/image/index.d.ts.map +0 -1
- package/dist/image/index.js +0 -4
- package/dist/image/index.js.map +0 -1
- package/dist/video/index.d.ts +0 -4
- package/dist/video/index.d.ts.map +0 -1
- package/dist/video/index.js +0 -4
- package/dist/video/index.js.map +0 -1
|
@@ -17,18 +17,15 @@
|
|
|
17
17
|
//
|
|
18
18
|
// Writes return `{ key }` — never URLs. Display URLs are built at render time
|
|
19
19
|
// from asset refs (see the consuming app's media-asset-url helper).
|
|
20
|
-
import { createReadStream, createWriteStream } from
|
|
21
|
-
import { stat } from
|
|
22
|
-
import { Readable } from
|
|
23
|
-
import { pipeline } from
|
|
24
|
-
import { AwsClient } from
|
|
25
|
-
import { NEUTRAL_CONTENT_TYPE } from
|
|
26
|
-
import { isObjectNotFoundError } from
|
|
27
|
-
import { defaultR2RetrySeams, fetchWithR2Retry, R2StorageError, } from
|
|
28
|
-
import { requireStorageObjectGeneration, UnreadableStorageObjectMetadataError } from
|
|
29
|
-
// Re-export the exhaustion error so callers can recognize an exhausted R2
|
|
30
|
-
// operation. The rest of the retry helper stays private to this module.
|
|
31
|
-
export { R2StorageError };
|
|
20
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
21
|
+
import { stat } from 'node:fs/promises';
|
|
22
|
+
import { Readable } from 'node:stream';
|
|
23
|
+
import { pipeline } from 'node:stream/promises';
|
|
24
|
+
import { AwsClient } from 'aws4fetch';
|
|
25
|
+
import { NEUTRAL_CONTENT_TYPE } from '@ttt-productions/media-schemas';
|
|
26
|
+
import { isObjectNotFoundError } from './gcs-errors.js';
|
|
27
|
+
import { defaultR2RetrySeams, fetchWithR2Retry, R2StorageError, } from './r2-retry.js';
|
|
28
|
+
import { requireStorageObjectGeneration, UnreadableStorageObjectMetadataError, } from './staged-object.js';
|
|
32
29
|
/**
|
|
33
30
|
* A copy found an object at its destination key that is not this copy's result, and
|
|
34
31
|
* wrote nothing; that object is untouched. The key is a PROPERTY, never message text, so
|
|
@@ -37,17 +34,17 @@ export { R2StorageError };
|
|
|
37
34
|
export class ObjectAlreadyExistsError extends Error {
|
|
38
35
|
key;
|
|
39
36
|
constructor(key) {
|
|
40
|
-
super(
|
|
37
|
+
super('copy destination already holds another object');
|
|
41
38
|
this.key = key;
|
|
42
|
-
this.name =
|
|
39
|
+
this.name = 'ObjectAlreadyExistsError';
|
|
43
40
|
}
|
|
44
41
|
}
|
|
45
42
|
// Every copy records on its destination which source version it holds. A copy whose
|
|
46
43
|
// destination already exists compares this record with its own, so it recognizes its
|
|
47
44
|
// own earlier result and never mistakes another writer's object for it. The values are
|
|
48
45
|
// URI-encoded because R2 carries custom metadata in HTTP headers, which must be ASCII.
|
|
49
|
-
const COPY_SOURCE_METADATA_KEY =
|
|
50
|
-
const COPY_SOURCE_VERSION_METADATA_KEY =
|
|
46
|
+
const COPY_SOURCE_METADATA_KEY = 'copy-source';
|
|
47
|
+
const COPY_SOURCE_VERSION_METADATA_KEY = 'copy-source-version';
|
|
51
48
|
function copyProvenance(fromKey, sourceVersion) {
|
|
52
49
|
return {
|
|
53
50
|
[COPY_SOURCE_METADATA_KEY]: encodeURIComponent(fromKey),
|
|
@@ -66,24 +63,24 @@ function holdsProvenance(readMetadata, provenance) {
|
|
|
66
63
|
function recordsCopyOf(readMetadata, fromKey) {
|
|
67
64
|
const version = readMetadata(COPY_SOURCE_VERSION_METADATA_KEY);
|
|
68
65
|
return (readMetadata(COPY_SOURCE_METADATA_KEY) === encodeURIComponent(fromKey) &&
|
|
69
|
-
typeof version ===
|
|
70
|
-
version !==
|
|
66
|
+
typeof version === 'string' &&
|
|
67
|
+
version !== '');
|
|
71
68
|
}
|
|
72
69
|
/** The content headers a copy carries from its source: HTTP header name → Cloud Storage field. */
|
|
73
70
|
const CARRIED_CONTENT_HEADERS = {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
'content-type': 'contentType',
|
|
72
|
+
'cache-control': 'cacheControl',
|
|
73
|
+
'content-disposition': 'contentDisposition',
|
|
74
|
+
'content-encoding': 'contentEncoding',
|
|
78
75
|
};
|
|
79
|
-
const DEFAULT_CACHE_CONTROL =
|
|
76
|
+
const DEFAULT_CACHE_CONTROL = 'public, max-age=31536000, immutable';
|
|
80
77
|
export function createR2ObjectStore(args) {
|
|
81
|
-
const endpoint = (args.endpoint ?? `https://${args.accountId}.r2.cloudflarestorage.com`).replace(/\/$/,
|
|
78
|
+
const endpoint = (args.endpoint ?? `https://${args.accountId}.r2.cloudflarestorage.com`).replace(/\/$/, '');
|
|
82
79
|
const client = new AwsClient({
|
|
83
80
|
accessKeyId: args.accessKeyId,
|
|
84
81
|
secretAccessKey: args.secretAccessKey,
|
|
85
|
-
service:
|
|
86
|
-
region:
|
|
82
|
+
service: 's3',
|
|
83
|
+
region: 'auto',
|
|
87
84
|
});
|
|
88
85
|
const doFetch = args.fetchImpl ?? fetch;
|
|
89
86
|
// Retry seams: production defaults are internal; tests inject deterministic
|
|
@@ -95,10 +92,10 @@ export function createR2ObjectStore(args) {
|
|
|
95
92
|
random: args.randomImpl ?? defaults.random,
|
|
96
93
|
now: args.nowImpl ?? defaults.now,
|
|
97
94
|
};
|
|
98
|
-
const objectUrl = (key) => `${endpoint}/${args.bucket}/${key.split(
|
|
95
|
+
const objectUrl = (key) => `${endpoint}/${args.bucket}/${key.split('/').map(encodeURIComponent).join('/')}`;
|
|
99
96
|
async function signedFetch(url, init) {
|
|
100
97
|
const signed = await client.sign(url, { ...init, aws: { allHeaders: true } });
|
|
101
|
-
return doFetch(signed, { ...(init.body ? { duplex:
|
|
98
|
+
return doFetch(signed, { ...(init.body ? { duplex: 'half' } : {}) });
|
|
102
99
|
}
|
|
103
100
|
// A GET whose body is discarded unread, not a HEAD: a HEAD's error response carries no
|
|
104
101
|
// body, so the S3 error code that tells a missing key from a missing bucket would be lost.
|
|
@@ -109,11 +106,11 @@ export function createR2ObjectStore(args) {
|
|
|
109
106
|
key,
|
|
110
107
|
seams,
|
|
111
108
|
runAttempt: async () => {
|
|
112
|
-
const res = await signedFetch(objectUrl(key), { method:
|
|
109
|
+
const res = await signedFetch(objectUrl(key), { method: 'GET' });
|
|
113
110
|
if (!res.ok)
|
|
114
|
-
return { kind:
|
|
111
|
+
return { kind: 'response', response: res };
|
|
115
112
|
await res.body?.cancel();
|
|
116
|
-
return { kind:
|
|
113
|
+
return { kind: 'success', value: res.headers };
|
|
117
114
|
},
|
|
118
115
|
});
|
|
119
116
|
}
|
|
@@ -122,15 +119,15 @@ export function createR2ObjectStore(args) {
|
|
|
122
119
|
// content has the same MD5-derived ETag and lands within the same second. A multipart
|
|
123
120
|
// upload's ETag is not the content's MD5, but it is the same kind of version token.
|
|
124
121
|
function readSourceVersion(headers, key) {
|
|
125
|
-
const etag = headers.get(
|
|
122
|
+
const etag = headers.get('etag');
|
|
126
123
|
if (!etag)
|
|
127
|
-
throw new UnreadableStorageObjectMetadataError(args.bucket, key,
|
|
128
|
-
const lastModified = headers.get(
|
|
124
|
+
throw new UnreadableStorageObjectMetadataError(args.bucket, key, 'etag');
|
|
125
|
+
const lastModified = headers.get('last-modified');
|
|
129
126
|
if (!lastModified)
|
|
130
|
-
throw new UnreadableStorageObjectMetadataError(args.bucket, key,
|
|
131
|
-
const size = headers.get(
|
|
127
|
+
throw new UnreadableStorageObjectMetadataError(args.bucket, key, 'lastModified');
|
|
128
|
+
const size = headers.get('content-length');
|
|
132
129
|
if (!size)
|
|
133
|
-
throw new UnreadableStorageObjectMetadataError(args.bucket, key,
|
|
130
|
+
throw new UnreadableStorageObjectMetadataError(args.bucket, key, 'size');
|
|
134
131
|
return { etag, version: `${etag} ${lastModified} ${size}` };
|
|
135
132
|
}
|
|
136
133
|
return {
|
|
@@ -140,7 +137,7 @@ export function createR2ObjectStore(args) {
|
|
|
140
137
|
const { size } = await stat(localPath);
|
|
141
138
|
const ct = contentType ?? getMimeFromExt(extractExt(localPath));
|
|
142
139
|
return fetchWithR2Retry({
|
|
143
|
-
operation:
|
|
140
|
+
operation: 'putFile',
|
|
144
141
|
bucket: args.bucket,
|
|
145
142
|
key,
|
|
146
143
|
seams,
|
|
@@ -148,35 +145,35 @@ export function createR2ObjectStore(args) {
|
|
|
148
145
|
// consumed stream is never reused.
|
|
149
146
|
runAttempt: async () => {
|
|
150
147
|
const res = await signedFetch(objectUrl(key), {
|
|
151
|
-
method:
|
|
148
|
+
method: 'PUT',
|
|
152
149
|
headers: {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
'content-type': ct,
|
|
151
|
+
'content-length': String(size),
|
|
152
|
+
'cache-control': DEFAULT_CACHE_CONTROL,
|
|
156
153
|
},
|
|
157
154
|
body: createReadStream(localPath),
|
|
158
155
|
});
|
|
159
156
|
if (res.ok) {
|
|
160
|
-
return { kind:
|
|
157
|
+
return { kind: 'success', value: { key, sizeBytes: size, contentType: ct } };
|
|
161
158
|
}
|
|
162
|
-
return { kind:
|
|
159
|
+
return { kind: 'response', response: res };
|
|
163
160
|
},
|
|
164
161
|
});
|
|
165
162
|
},
|
|
166
163
|
async copy({ fromKey, toKey }) {
|
|
167
|
-
const source = await readObjectHeaders(fromKey,
|
|
164
|
+
const source = await readObjectHeaders(fromKey, 'copy');
|
|
168
165
|
const { etag, version } = readSourceVersion(source, fromKey);
|
|
169
166
|
const provenance = copyProvenance(fromKey, version);
|
|
170
167
|
const headers = {
|
|
171
|
-
|
|
168
|
+
'x-amz-copy-source': `/${args.bucket}/${fromKey.split('/').map(encodeURIComponent).join('/')}`,
|
|
172
169
|
// Pins the copy to the source version the provenance names.
|
|
173
|
-
|
|
170
|
+
'x-amz-copy-source-if-match': etag,
|
|
174
171
|
// R2's destination-conditional CopyObject extension, checked when the copy commits;
|
|
175
172
|
// S3's x-amz-copy-source-if-* headers condition only the source.
|
|
176
|
-
|
|
173
|
+
'cf-copy-destination-if-none-match': '*',
|
|
177
174
|
// The destination takes exactly the headers below rather than the source's, so it
|
|
178
175
|
// commits fully formed in this one call.
|
|
179
|
-
|
|
176
|
+
'x-amz-metadata-directive': 'REPLACE',
|
|
180
177
|
};
|
|
181
178
|
for (const name of Object.keys(CARRIED_CONTENT_HEADERS)) {
|
|
182
179
|
const value = source.get(name);
|
|
@@ -187,15 +184,15 @@ export function createR2ObjectStore(args) {
|
|
|
187
184
|
headers[`x-amz-meta-${name}`] = value;
|
|
188
185
|
try {
|
|
189
186
|
await fetchWithR2Retry({
|
|
190
|
-
operation:
|
|
187
|
+
operation: 'copy',
|
|
191
188
|
bucket: args.bucket,
|
|
192
189
|
key: toKey,
|
|
193
190
|
seams,
|
|
194
191
|
runAttempt: async () => {
|
|
195
|
-
const res = await signedFetch(objectUrl(toKey), { method:
|
|
192
|
+
const res = await signedFetch(objectUrl(toKey), { method: 'PUT', headers });
|
|
196
193
|
if (res.ok)
|
|
197
|
-
return { kind:
|
|
198
|
-
return { kind:
|
|
194
|
+
return { kind: 'success', value: undefined };
|
|
195
|
+
return { kind: 'response', response: res };
|
|
199
196
|
},
|
|
200
197
|
});
|
|
201
198
|
return { key: toKey, alreadyPresent: false };
|
|
@@ -207,7 +204,7 @@ export function createR2ObjectStore(args) {
|
|
|
207
204
|
// after it was read; only an existing destination can be this copy's result.
|
|
208
205
|
let destination;
|
|
209
206
|
try {
|
|
210
|
-
destination = await readObjectHeaders(toKey,
|
|
207
|
+
destination = await readObjectHeaders(toKey, 'copy');
|
|
211
208
|
}
|
|
212
209
|
catch (readError) {
|
|
213
210
|
if (isObjectNotFoundError(readError))
|
|
@@ -222,35 +219,35 @@ export function createR2ObjectStore(args) {
|
|
|
222
219
|
},
|
|
223
220
|
async readToFile({ key, localPath }) {
|
|
224
221
|
return fetchWithR2Retry({
|
|
225
|
-
operation:
|
|
222
|
+
operation: 'readToFile',
|
|
226
223
|
bucket: args.bucket,
|
|
227
224
|
key,
|
|
228
225
|
seams,
|
|
229
226
|
runAttempt: async () => {
|
|
230
|
-
const res = await signedFetch(objectUrl(key), { method:
|
|
227
|
+
const res = await signedFetch(objectUrl(key), { method: 'GET' });
|
|
231
228
|
if (!res.ok || !res.body) {
|
|
232
|
-
return { kind:
|
|
229
|
+
return { kind: 'response', response: res };
|
|
233
230
|
}
|
|
234
|
-
const contentType = res.headers.get(
|
|
231
|
+
const contentType = res.headers.get('content-type') ?? NEUTRAL_CONTENT_TYPE;
|
|
235
232
|
// Pipe ONLY after a successful response, and open the destination in
|
|
236
233
|
// truncating mode ('w') on every attempt so a partial download from a
|
|
237
234
|
// prior attempt can never prefix/corrupt this one. res.body is a web
|
|
238
235
|
// ReadableStream; stream it to disk so large media never sits fully in
|
|
239
236
|
// memory. A transient body/network stream failure here throws and is
|
|
240
237
|
// retried by the helper; a deterministic local fs error is not.
|
|
241
|
-
await pipeline(Readable.fromWeb(res.body), createWriteStream(localPath, { flags:
|
|
238
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(localPath, { flags: 'w' }));
|
|
242
239
|
const { size } = await stat(localPath);
|
|
243
|
-
return { kind:
|
|
240
|
+
return { kind: 'success', value: { sizeBytes: size, contentType } };
|
|
244
241
|
},
|
|
245
242
|
});
|
|
246
243
|
},
|
|
247
244
|
async delete(key) {
|
|
248
245
|
// Intentionally one-shot: delete is never retried (see r2-retry.ts header).
|
|
249
|
-
const res = await signedFetch(objectUrl(key), { method:
|
|
246
|
+
const res = await signedFetch(objectUrl(key), { method: 'DELETE' });
|
|
250
247
|
// S3 DELETE returns 204 even for missing keys; anything else non-ok is real.
|
|
251
248
|
if (!res.ok && res.status !== 404) {
|
|
252
249
|
throw new R2StorageError({
|
|
253
|
-
operation:
|
|
250
|
+
operation: 'delete',
|
|
254
251
|
bucket: args.bucket,
|
|
255
252
|
key,
|
|
256
253
|
attempts: 1,
|
|
@@ -262,23 +259,23 @@ export function createR2ObjectStore(args) {
|
|
|
262
259
|
async deleteCopy({ fromKey, toKey }) {
|
|
263
260
|
let destination;
|
|
264
261
|
try {
|
|
265
|
-
destination = await readObjectHeaders(toKey,
|
|
262
|
+
destination = await readObjectHeaders(toKey, 'deleteCopy');
|
|
266
263
|
}
|
|
267
264
|
catch (e) {
|
|
268
265
|
if (isObjectNotFoundError(e))
|
|
269
|
-
return
|
|
266
|
+
return 'absent';
|
|
270
267
|
throw e;
|
|
271
268
|
}
|
|
272
269
|
if (!recordsCopyOf((name) => destination.get(`x-amz-meta-${name}`), fromKey))
|
|
273
|
-
return
|
|
270
|
+
return 'notThisCopy';
|
|
274
271
|
// Unconditioned: R2's DeleteObject honors no precondition header, so none is sent — one
|
|
275
272
|
// it silently ignored would only look like protection. `MediaObjectStore.deleteCopy`
|
|
276
273
|
// states why the gap after the check is harmless. One-shot, like `delete`.
|
|
277
|
-
const res = await signedFetch(objectUrl(toKey), { method:
|
|
274
|
+
const res = await signedFetch(objectUrl(toKey), { method: 'DELETE' });
|
|
278
275
|
if (res.ok)
|
|
279
|
-
return
|
|
276
|
+
return 'deleted';
|
|
280
277
|
const error = new R2StorageError({
|
|
281
|
-
operation:
|
|
278
|
+
operation: 'deleteCopy',
|
|
282
279
|
bucket: args.bucket,
|
|
283
280
|
key: toKey,
|
|
284
281
|
attempts: 1,
|
|
@@ -286,7 +283,7 @@ export function createR2ObjectStore(args) {
|
|
|
286
283
|
responseText: await safeText(res),
|
|
287
284
|
});
|
|
288
285
|
if (isObjectNotFoundError(error))
|
|
289
|
-
return
|
|
286
|
+
return 'absent';
|
|
290
287
|
throw error;
|
|
291
288
|
},
|
|
292
289
|
};
|
|
@@ -296,14 +293,14 @@ async function safeText(res) {
|
|
|
296
293
|
return (await res.text()).slice(0, 500);
|
|
297
294
|
}
|
|
298
295
|
catch {
|
|
299
|
-
return
|
|
296
|
+
return '<no body>';
|
|
300
297
|
}
|
|
301
298
|
}
|
|
302
299
|
// ---------------------------------------------------------------------------
|
|
303
300
|
// Firebase Storage stores — deployed (token-free) and emulator (fixed token)
|
|
304
301
|
// ---------------------------------------------------------------------------
|
|
305
302
|
/** Custom-metadata key Firebase Storage reads download tokens from. */
|
|
306
|
-
const DOWNLOAD_TOKENS_METADATA_KEY =
|
|
303
|
+
const DOWNLOAD_TOKENS_METADATA_KEY = 'firebaseStorageDownloadTokens';
|
|
307
304
|
/**
|
|
308
305
|
* How many times `deleteCopy` checks a Firebase Storage destination before giving up. Keys
|
|
309
306
|
* are write-once, so one change between the check and the delete is already rare; an object
|
|
@@ -341,19 +338,24 @@ function createFirebaseStore(bucket, downloadToken) {
|
|
|
341
338
|
metadata: {
|
|
342
339
|
contentType: ct,
|
|
343
340
|
cacheControl: DEFAULT_CACHE_CONTROL,
|
|
344
|
-
...(downloadToken === null
|
|
341
|
+
...(downloadToken === null
|
|
342
|
+
? {}
|
|
343
|
+
: { metadata: { [DOWNLOAD_TOKENS_METADATA_KEY]: downloadToken } }),
|
|
345
344
|
},
|
|
346
345
|
});
|
|
347
346
|
return { key, sizeBytes: size, contentType: ct };
|
|
348
347
|
},
|
|
349
348
|
async copy({ fromKey, toKey }) {
|
|
350
349
|
const [source] = await bucket.file(fromKey).getMetadata();
|
|
351
|
-
const generation = requireStorageObjectGeneration(source, {
|
|
350
|
+
const generation = requireStorageObjectGeneration(source, {
|
|
351
|
+
bucket: bucket.name,
|
|
352
|
+
key: fromKey,
|
|
353
|
+
});
|
|
352
354
|
const provenance = copyProvenance(fromKey, generation);
|
|
353
355
|
const contentHeaders = {};
|
|
354
356
|
for (const field of Object.values(CARRIED_CONTENT_HEADERS)) {
|
|
355
357
|
const value = source[field];
|
|
356
|
-
if (typeof value ===
|
|
358
|
+
if (typeof value === 'string' && value !== '')
|
|
357
359
|
contentHeaders[field] = value;
|
|
358
360
|
}
|
|
359
361
|
try {
|
|
@@ -364,7 +366,9 @@ function createFirebaseStore(bucket, downloadToken) {
|
|
|
364
366
|
// 412 can only mean the destination exists; a replaced source fails as not-found.
|
|
365
367
|
await bucket.file(fromKey, { generation }).copy(bucket.file(toKey), {
|
|
366
368
|
...contentHeaders,
|
|
367
|
-
metadata: downloadToken === null
|
|
369
|
+
metadata: downloadToken === null
|
|
370
|
+
? provenance
|
|
371
|
+
: { ...provenance, [DOWNLOAD_TOKENS_METADATA_KEY]: downloadToken },
|
|
368
372
|
preconditionOpts: { ifGenerationMatch: 0 },
|
|
369
373
|
});
|
|
370
374
|
return { key: toKey, alreadyPresent: false };
|
|
@@ -409,20 +413,20 @@ function createFirebaseStore(bucket, downloadToken) {
|
|
|
409
413
|
}
|
|
410
414
|
catch (e) {
|
|
411
415
|
if (isObjectNotFoundError(e))
|
|
412
|
-
return
|
|
416
|
+
return 'absent';
|
|
413
417
|
throw e;
|
|
414
418
|
}
|
|
415
419
|
if (!recordsCopyOf((name) => custom?.[name], fromKey))
|
|
416
|
-
return
|
|
420
|
+
return 'notThisCopy';
|
|
417
421
|
// The delete names the generation just checked, so it can only remove that object.
|
|
418
422
|
const checked = requireStorageObjectGeneration({ generation }, { bucket: bucket.name, key: toKey });
|
|
419
423
|
try {
|
|
420
424
|
await file.delete({ ifGenerationMatch: checked });
|
|
421
|
-
return
|
|
425
|
+
return 'deleted';
|
|
422
426
|
}
|
|
423
427
|
catch (e) {
|
|
424
428
|
if (isObjectNotFoundError(e))
|
|
425
|
-
return
|
|
429
|
+
return 'absent';
|
|
426
430
|
// A 412 means the object changed after it was checked: check what is there now.
|
|
427
431
|
if (!isPreconditionFailedError(e) || read >= DELETE_COPY_MAX_READS)
|
|
428
432
|
throw e;
|
|
@@ -437,27 +441,27 @@ function isPreconditionFailedError(e) {
|
|
|
437
441
|
}
|
|
438
442
|
// ---------------------------------------------------------------------------
|
|
439
443
|
function extractExt(localPath) {
|
|
440
|
-
return localPath.split(
|
|
444
|
+
return localPath.split('.').pop() || '';
|
|
441
445
|
}
|
|
442
446
|
export function getMimeFromExt(ext) {
|
|
443
447
|
switch (ext.toLowerCase()) {
|
|
444
|
-
case
|
|
445
|
-
case
|
|
446
|
-
return
|
|
447
|
-
case
|
|
448
|
-
return
|
|
449
|
-
case
|
|
450
|
-
return
|
|
451
|
-
case
|
|
452
|
-
return
|
|
453
|
-
case
|
|
454
|
-
return
|
|
455
|
-
case
|
|
456
|
-
return
|
|
457
|
-
case
|
|
458
|
-
return
|
|
459
|
-
case
|
|
460
|
-
return
|
|
448
|
+
case 'jpg':
|
|
449
|
+
case 'jpeg':
|
|
450
|
+
return 'image/jpeg';
|
|
451
|
+
case 'png':
|
|
452
|
+
return 'image/png';
|
|
453
|
+
case 'webp':
|
|
454
|
+
return 'image/webp';
|
|
455
|
+
case 'mp4':
|
|
456
|
+
return 'video/mp4';
|
|
457
|
+
case 'webm':
|
|
458
|
+
return 'video/webm';
|
|
459
|
+
case 'm4a':
|
|
460
|
+
return 'audio/mp4';
|
|
461
|
+
case 'aac':
|
|
462
|
+
return 'audio/aac';
|
|
463
|
+
case 'mp3':
|
|
464
|
+
return 'audio/mpeg';
|
|
461
465
|
default:
|
|
462
466
|
return NEUTRAL_CONTENT_TYPE;
|
|
463
467
|
}
|