@orthacms/media-provider-gcs 0.4.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 +7 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/fake-bucket.d.ts +48 -0
- package/dist/lib/fake-bucket.d.ts.map +1 -0
- package/dist/lib/fake-bucket.js +98 -0
- package/dist/lib/gcs-storage-provider.d.ts +50 -0
- package/dist/lib/gcs-storage-provider.d.ts.map +1 -0
- package/dist/lib/gcs-storage-provider.js +143 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ortha CMS contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACtE,YAAY,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createGcsStorageProvider = void 0;
|
|
4
|
+
var gcs_storage_provider_1 = require("./lib/gcs-storage-provider");
|
|
5
|
+
Object.defineProperty(exports, "createGcsStorageProvider", { enumerable: true, get: function () { return gcs_storage_provider_1.createGcsStorageProvider; } });
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { PassThrough, Readable } from 'node:stream';
|
|
2
|
+
import type { Bucket } from '@google-cloud/storage';
|
|
3
|
+
/** One object, as the fake holds it. */
|
|
4
|
+
interface FakeObject {
|
|
5
|
+
body: Buffer;
|
|
6
|
+
contentType?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A stand-in `Bucket` covering the five calls this adapter makes.
|
|
10
|
+
*
|
|
11
|
+
* The seam is the bucket handle, which the provider already accepts for
|
|
12
|
+
* deployments with hand-built auth — so these tests drive the same entry point
|
|
13
|
+
* a real caller uses rather than a private hook.
|
|
14
|
+
*
|
|
15
|
+
* It proves this adapter's logic and nothing about GCS: the fake agrees with
|
|
16
|
+
* whatever the code does. `AGENTS.md` names the acceptance step.
|
|
17
|
+
*/
|
|
18
|
+
export declare class FakeBucket {
|
|
19
|
+
readonly objects: Map<string, FakeObject>;
|
|
20
|
+
/** Flip to make `getMetadata` fail, as a missing bucket would. */
|
|
21
|
+
exists: boolean;
|
|
22
|
+
/** Make the next upload fail part-way, to exercise the cleanup path. */
|
|
23
|
+
failNextUpload: boolean;
|
|
24
|
+
/** Object names `delete` was called for, in order. */
|
|
25
|
+
readonly deleted: string[];
|
|
26
|
+
/** Options the last `getSignedUrl` was asked for. */
|
|
27
|
+
lastSignedUrlOptions: Record<string, unknown> | undefined;
|
|
28
|
+
file(name: string): {
|
|
29
|
+
createWriteStream: (options?: {
|
|
30
|
+
contentType?: string;
|
|
31
|
+
}) => PassThrough;
|
|
32
|
+
createReadStream: () => Readable;
|
|
33
|
+
getMetadata: () => Promise<{
|
|
34
|
+
size: number;
|
|
35
|
+
}[]>;
|
|
36
|
+
delete: (options?: {
|
|
37
|
+
ignoreNotFound?: boolean;
|
|
38
|
+
}) => Promise<{}[]>;
|
|
39
|
+
getSignedUrl: (options: Record<string, unknown>) => Promise<string[]>;
|
|
40
|
+
};
|
|
41
|
+
getMetadata(): Promise<{}[]>;
|
|
42
|
+
/** Object names currently held, sorted. */
|
|
43
|
+
keys(): string[];
|
|
44
|
+
/** Presents as the real thing to a provider that only uses the above. */
|
|
45
|
+
asBucket(): Bucket;
|
|
46
|
+
}
|
|
47
|
+
export {};
|
|
48
|
+
//# sourceMappingURL=fake-bucket.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake-bucket.d.ts","sourceRoot":"","sources":["../../src/lib/fake-bucket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAY,MAAM,aAAa,CAAC;AAC9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAEpD,wCAAwC;AACxC,UAAU,UAAU;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAOD;;;;;;;;;GASG;AACH,qBAAa,UAAU;IACnB,QAAQ,CAAC,OAAO,0BAAiC;IACjD,kEAAkE;IAClE,MAAM,UAAQ;IACd,wEAAwE;IACxE,cAAc,UAAS;IACvB,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAM;IAChC,qDAAqD;IACrD,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAE1D,IAAI,CAAC,IAAI,EAAE,MAAM;sCAEqB;YAAE,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,KAqBxB,WAAW;;;;;2BAkBhB;YAAE,cAAc,CAAC,EAAE,OAAO,CAAA;SAAE;gCAQvB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;IAOvD,WAAW;IAKjB,2CAA2C;IAC3C,IAAI,IAAI,MAAM,EAAE;IAIhB,yEAAyE;IACzE,QAAQ,IAAI,MAAM;CAGrB"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FakeBucket = void 0;
|
|
4
|
+
const node_stream_1 = require("node:stream");
|
|
5
|
+
/** The error shape the GCS client raises for a missing object. */
|
|
6
|
+
function objectNotFound() {
|
|
7
|
+
return Object.assign(new Error('No such object'), { code: 404 });
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A stand-in `Bucket` covering the five calls this adapter makes.
|
|
11
|
+
*
|
|
12
|
+
* The seam is the bucket handle, which the provider already accepts for
|
|
13
|
+
* deployments with hand-built auth — so these tests drive the same entry point
|
|
14
|
+
* a real caller uses rather than a private hook.
|
|
15
|
+
*
|
|
16
|
+
* It proves this adapter's logic and nothing about GCS: the fake agrees with
|
|
17
|
+
* whatever the code does. `AGENTS.md` names the acceptance step.
|
|
18
|
+
*/
|
|
19
|
+
class FakeBucket {
|
|
20
|
+
objects = new Map();
|
|
21
|
+
/** Flip to make `getMetadata` fail, as a missing bucket would. */
|
|
22
|
+
exists = true;
|
|
23
|
+
/** Make the next upload fail part-way, to exercise the cleanup path. */
|
|
24
|
+
failNextUpload = false;
|
|
25
|
+
/** Object names `delete` was called for, in order. */
|
|
26
|
+
deleted = [];
|
|
27
|
+
/** Options the last `getSignedUrl` was asked for. */
|
|
28
|
+
lastSignedUrlOptions;
|
|
29
|
+
file(name) {
|
|
30
|
+
return {
|
|
31
|
+
createWriteStream: (options) => {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
const sink = new node_stream_1.Writable({
|
|
34
|
+
write: (chunk, _encoding, done) => {
|
|
35
|
+
if (this.failNextUpload) {
|
|
36
|
+
this.failNextUpload = false;
|
|
37
|
+
done(new Error('upload failed'));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
41
|
+
done();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
sink.on('finish', () => {
|
|
45
|
+
this.objects.set(name, {
|
|
46
|
+
body: Buffer.concat(chunks),
|
|
47
|
+
contentType: options?.contentType
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
return sink;
|
|
51
|
+
},
|
|
52
|
+
createReadStream: () => {
|
|
53
|
+
const object = this.objects.get(name);
|
|
54
|
+
// Lazily, exactly like the real client — which is why the
|
|
55
|
+
// provider reads metadata first rather than trusting this.
|
|
56
|
+
if (object)
|
|
57
|
+
return node_stream_1.Readable.from(object.body);
|
|
58
|
+
return new node_stream_1.Readable({
|
|
59
|
+
read() {
|
|
60
|
+
this.destroy(objectNotFound());
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
getMetadata: async () => {
|
|
65
|
+
const object = this.objects.get(name);
|
|
66
|
+
if (!object)
|
|
67
|
+
throw objectNotFound();
|
|
68
|
+
return [{ size: object.body.byteLength }];
|
|
69
|
+
},
|
|
70
|
+
delete: async (options) => {
|
|
71
|
+
this.deleted.push(name);
|
|
72
|
+
const existed = this.objects.delete(name);
|
|
73
|
+
if (!existed && !options?.ignoreNotFound) {
|
|
74
|
+
throw objectNotFound();
|
|
75
|
+
}
|
|
76
|
+
return [{}];
|
|
77
|
+
},
|
|
78
|
+
getSignedUrl: async (options) => {
|
|
79
|
+
this.lastSignedUrlOptions = options;
|
|
80
|
+
return [`https://storage.test/${name}?signed=1`];
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async getMetadata() {
|
|
85
|
+
if (!this.exists)
|
|
86
|
+
throw objectNotFound();
|
|
87
|
+
return [{}];
|
|
88
|
+
}
|
|
89
|
+
/** Object names currently held, sorted. */
|
|
90
|
+
keys() {
|
|
91
|
+
return [...this.objects.keys()].sort();
|
|
92
|
+
}
|
|
93
|
+
/** Presents as the real thing to a provider that only uses the above. */
|
|
94
|
+
asBucket() {
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.FakeBucket = FakeBucket;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Bucket } from '@google-cloud/storage';
|
|
2
|
+
import type { StorageProvider } from '@orthacms/media-server';
|
|
3
|
+
/**
|
|
4
|
+
* Settings for Google Cloud Storage.
|
|
5
|
+
*
|
|
6
|
+
* **You may not need this package.** GCS speaks the S3 XML API in
|
|
7
|
+
* interoperability mode, so `@orthacms/media-provider-s3` reaches it today with
|
|
8
|
+
* `endpoint: 'https://storage.googleapis.com'` and an HMAC key. This adapter
|
|
9
|
+
* exists for the deployment that cannot use that: HMAC keys are a long-lived
|
|
10
|
+
* secret that many organizations forbid by policy, and they rule out Workload
|
|
11
|
+
* Identity. Native GCS auth is the reason to be here — if you are happy with an
|
|
12
|
+
* HMAC key, the S3 adapter is one less package to keep.
|
|
13
|
+
*/
|
|
14
|
+
export interface GcsStorageConfig {
|
|
15
|
+
/** Bucket every object lands in. */
|
|
16
|
+
bucket: string;
|
|
17
|
+
/** Project the bucket belongs to. Optional under ADC. */
|
|
18
|
+
projectId?: string;
|
|
19
|
+
/** Path to a service-account key file. */
|
|
20
|
+
keyFilename?: string;
|
|
21
|
+
/** Inline service-account credentials, when a file is not an option. */
|
|
22
|
+
credentials?: {
|
|
23
|
+
client_email: string;
|
|
24
|
+
private_key: string;
|
|
25
|
+
};
|
|
26
|
+
/** Prefix every object name with this, e.g. to share a bucket. */
|
|
27
|
+
keyPrefix?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Sign URLs through the IAM `signBlob` API instead of a local private key.
|
|
30
|
+
*
|
|
31
|
+
* This is how a Workload Identity deployment signs: it has no key, and the
|
|
32
|
+
* library asks IAM to sign for it. Opt-in because it needs the
|
|
33
|
+
* `iam.serviceAccounts.signBlob` permission — declaring the capability
|
|
34
|
+
* without it would mean minting URLs that fail at request time.
|
|
35
|
+
*/
|
|
36
|
+
signWithIam?: boolean;
|
|
37
|
+
/** An already-built bucket handle — custom auth, or a test stub. */
|
|
38
|
+
bucketClient?: Bucket;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The Google Cloud Storage {@link StorageProvider}.
|
|
42
|
+
*
|
|
43
|
+
* Like the Azure adapter, `capabilities.directUrl` is **computed**: signing
|
|
44
|
+
* needs either a private key or an explicit opt-in to IAM `signBlob`, and a
|
|
45
|
+
* bare Application Default Credentials deployment has neither. A hardcoded
|
|
46
|
+
* `true` would let `MediaServerPlugin` accept `directServe: 'signed-url'` on a
|
|
47
|
+
* deployment that cannot honour it.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createGcsStorageProvider(config: GcsStorageConfig): StorageProvider;
|
|
50
|
+
//# sourceMappingURL=gcs-storage-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gcs-storage-provider.d.ts","sourceRoot":"","sources":["../../src/lib/gcs-storage-provider.ts"],"names":[],"mappings":"AAGA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAE7D,OAAO,KAAK,EAGR,eAAe,EAElB,MAAM,wBAAwB,CAAC;AAEhC;;;;;;;;;;GAUG;AACH,MAAM,WAAW,gBAAgB;IAC7B,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,WAAW,CAAC,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAkBD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,gBAAgB,GACzB,eAAe,CAkIjB"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createGcsStorageProvider = createGcsStorageProvider;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const node_stream_1 = require("node:stream");
|
|
6
|
+
const promises_1 = require("node:stream/promises");
|
|
7
|
+
const storage_1 = require("@google-cloud/storage");
|
|
8
|
+
const media_server_1 = require("@orthacms/media-server");
|
|
9
|
+
/** Reduces a file name to one safe key segment. Mirrors the other providers. */
|
|
10
|
+
function sanitize(fileName) {
|
|
11
|
+
const cleaned = fileName.replace(/[^A-Za-z0-9_.-]+/g, '_');
|
|
12
|
+
return cleaned === '.' || cleaned === '..' ? `_${cleaned}` : cleaned;
|
|
13
|
+
}
|
|
14
|
+
/** True for the shapes GCS uses to say "no such object". */
|
|
15
|
+
function isMissing(error) {
|
|
16
|
+
const candidate = error;
|
|
17
|
+
return (candidate?.code === 404 ||
|
|
18
|
+
candidate?.status === 404 ||
|
|
19
|
+
candidate?.code === 'ENOENT');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The Google Cloud Storage {@link StorageProvider}.
|
|
23
|
+
*
|
|
24
|
+
* Like the Azure adapter, `capabilities.directUrl` is **computed**: signing
|
|
25
|
+
* needs either a private key or an explicit opt-in to IAM `signBlob`, and a
|
|
26
|
+
* bare Application Default Credentials deployment has neither. A hardcoded
|
|
27
|
+
* `true` would let `MediaServerPlugin` accept `directServe: 'signed-url'` on a
|
|
28
|
+
* deployment that cannot honour it.
|
|
29
|
+
*/
|
|
30
|
+
function createGcsStorageProvider(config) {
|
|
31
|
+
if (!config.bucket?.trim()) {
|
|
32
|
+
throw new Error('createGcsStorageProvider requires a bucket name.');
|
|
33
|
+
}
|
|
34
|
+
const bucket = config.bucketClient ??
|
|
35
|
+
new storage_1.Storage({
|
|
36
|
+
...(config.projectId ? { projectId: config.projectId } : {}),
|
|
37
|
+
...(config.keyFilename ? { keyFilename: config.keyFilename } : {}),
|
|
38
|
+
...(config.credentials ? { credentials: config.credentials } : {})
|
|
39
|
+
}).bucket(config.bucket);
|
|
40
|
+
const prefix = config.keyPrefix?.replace(/^\/+|\/+$/g, '');
|
|
41
|
+
const canSign = Boolean(config.keyFilename || config.credentials || config.signWithIam);
|
|
42
|
+
return {
|
|
43
|
+
id: 'gcs',
|
|
44
|
+
capabilities: {
|
|
45
|
+
directUrl: canSign,
|
|
46
|
+
contentTypeMetadata: true,
|
|
47
|
+
streamingPut: true
|
|
48
|
+
},
|
|
49
|
+
async put(object) {
|
|
50
|
+
const storageKey = [
|
|
51
|
+
prefix,
|
|
52
|
+
object.workspaceId,
|
|
53
|
+
object.assetId,
|
|
54
|
+
object.isVariant ? 'variants' : undefined,
|
|
55
|
+
sanitize(object.fileName)
|
|
56
|
+
]
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.join('/');
|
|
59
|
+
const hash = (0, node_crypto_1.createHash)('sha256');
|
|
60
|
+
let size = 0;
|
|
61
|
+
const meter = new node_stream_1.PassThrough();
|
|
62
|
+
meter.on('data', (chunk) => {
|
|
63
|
+
hash.update(chunk);
|
|
64
|
+
size += chunk.byteLength;
|
|
65
|
+
});
|
|
66
|
+
const file = bucket.file(storageKey);
|
|
67
|
+
try {
|
|
68
|
+
// `pipeline` rather than hand-wired events: it propagates a
|
|
69
|
+
// source error into the write stream and destroys both, which
|
|
70
|
+
// is what stops a dead upload hanging on a stream that will
|
|
71
|
+
// never end.
|
|
72
|
+
await (0, promises_1.pipeline)(object.body, meter, file.createWriteStream({
|
|
73
|
+
resumable: true,
|
|
74
|
+
contentType: object.contentType
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// A resumable upload that dies part-way leaves an incomplete
|
|
79
|
+
// object the bucket will keep until its lifecycle rules sweep
|
|
80
|
+
// it — and the key never reached a caller, so nothing else can
|
|
81
|
+
// reclaim it.
|
|
82
|
+
await file
|
|
83
|
+
.delete({ ignoreNotFound: true })
|
|
84
|
+
.catch(() => undefined);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
return { storageKey, size, checksum: hash.digest('hex') };
|
|
88
|
+
},
|
|
89
|
+
async get(storageKey) {
|
|
90
|
+
const file = bucket.file(storageKey);
|
|
91
|
+
// The metadata read is deliberate. `createReadStream` opens lazily,
|
|
92
|
+
// so a missing object surfaces as an error *on the stream* — by
|
|
93
|
+
// which point the response is already a streaming 200 that can no
|
|
94
|
+
// longer become the 404 the route owes the caller. One HEAD-shaped
|
|
95
|
+
// call buys that back.
|
|
96
|
+
try {
|
|
97
|
+
await file.getMetadata();
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (isMissing(error)) {
|
|
101
|
+
throw new media_server_1.ObjectNotFoundError(storageKey, error);
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
return file.createReadStream();
|
|
106
|
+
},
|
|
107
|
+
async remove(storageKey) {
|
|
108
|
+
// Idempotent by flag; the catch covers a gateway that answers 404
|
|
109
|
+
// anyway, since reclaim is post-commit and best-effort.
|
|
110
|
+
try {
|
|
111
|
+
await bucket.file(storageKey).delete({ ignoreNotFound: true });
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (!isMissing(error))
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
...(canSign
|
|
119
|
+
? {
|
|
120
|
+
async directUrl(storageKey, options) {
|
|
121
|
+
// Same rule as every signing provider: the disposition
|
|
122
|
+
// and content type are pinned onto the URL, because the
|
|
123
|
+
// redirect discards the app's own headers and the stored
|
|
124
|
+
// MIME type is the uploader's claim.
|
|
125
|
+
const fileName = options.fileName.replace(/"/g, '');
|
|
126
|
+
const [url] = await bucket.file(storageKey).getSignedUrl({
|
|
127
|
+
version: 'v4',
|
|
128
|
+
action: 'read',
|
|
129
|
+
expires: Date.now() + options.expiresInSeconds * 1000,
|
|
130
|
+
responseDisposition: `${options.disposition}; filename="${fileName}"`,
|
|
131
|
+
responseType: options.contentType
|
|
132
|
+
});
|
|
133
|
+
return url;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
: {}),
|
|
137
|
+
async verify() {
|
|
138
|
+
// A wrong bucket or a credential that cannot see it fails the boot
|
|
139
|
+
// rather than the first upload.
|
|
140
|
+
await bucket.getMetadata();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orthacms/media-provider-gcs",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "@orthacms/media-provider-gcs — part of Ortha CMS.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/ortha-source/ortha-cms/tree/main/packages/media/provider-gcs",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ortha-source/ortha-cms.git",
|
|
10
|
+
"directory": "packages/media/provider-gcs"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ortha-source/ortha-cms/issues"
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@google-cloud/storage": "^7.14.0",
|
|
29
|
+
"@orthacms/media-server": "^0.4.0",
|
|
30
|
+
"tslib": "^2.3.0"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|