@uploadx/gcs
Version:
Uploadx GCS module
195 lines (194 loc) • 7.29 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GCStorage = exports.GCSFile = void 0;
exports.getRangeEnd = getRangeEnd;
exports.buildContentRange = buildContentRange;
const core_1 = require("@uploadx/core");
const abort_controller_1 = require("abort-controller");
const google_auth_library_1 = require("google-auth-library");
const node_fetch_1 = __importDefault(require("node-fetch"));
const gcs_config_1 = require("./gcs-config");
const gcs_meta_storage_1 = require("./gcs-meta-storage");
function getRangeEnd(range) {
const end = +range.split(/0-/)[1];
return end > 0 ? end + 1 : 0;
}
function buildContentRange(part) {
if ((0, core_1.hasContent)(part)) {
const end = part.contentLength ? part.start + part.contentLength - 1 : '*';
return `bytes ${part.start}-${end}/${part.size ?? '*'}`;
}
else {
return `bytes */${part.size ?? '*'}`;
}
}
const validateStatus = (code) => (code >= 200 && code < 300) || code === 308 || code === 499;
class GCSFile extends core_1.File {
}
exports.GCSFile = GCSFile;
/**
* Google cloud storage based backend.
* @example
* ```ts
* const storage = new GCStorage({
* bucket: <YOUR_BUCKET>,
* keyFile: <PATH_TO_KEY_FILE>,
* metaStorage: new MetaStorage(),
* clientDirectUpload: true,
* maxUploadSize: '15GB',
* allowMIME: ['video/*', 'image/*'],
* filename: file => file.originalName
* });
* ```
*/
class GCStorage extends core_1.BaseStorage {
constructor(config = {}) {
super(config);
this.config = config;
this._onComplete = (file) => {
return this.deleteMeta(file.id);
};
if (config.metaStorage) {
this.meta = config.metaStorage;
}
else {
const metaConfig = { ...config, ...config.metaStorageConfig };
this.meta =
'directory' in metaConfig
? new core_1.LocalMetaStorage(metaConfig)
: new gcs_meta_storage_1.GCSMetaStorage(metaConfig);
}
config.keyFile || (config.keyFile = process.env.GCS_KEYFILE);
this.bucket = config.bucket || process.env.GCS_BUCKET || gcs_config_1.GCSConfig.bucketName;
this.storageBaseURI = [gcs_config_1.GCSConfig.storageAPI, this.bucket, 'o'].join('/');
this.uploadBaseURI = [gcs_config_1.GCSConfig.uploadAPI, this.bucket, 'o'].join('/');
config.scopes || (config.scopes = gcs_config_1.GCSConfig.authScopes);
this.authClient = new google_auth_library_1.GoogleAuth(config);
this.isReady = false;
this.accessCheck()
.then(() => (this.isReady = true))
.catch(err => this.logger.error('Storage access check failed: %O', err));
}
normalizeError(error) {
const statusCode = +error.code || 500;
if (error.config) {
return {
message: error.message,
code: `GCS${statusCode}`,
statusCode,
name: error.name,
retryable: statusCode >= 499
};
}
return super.normalizeError(error);
}
accessCheck() {
return this.authClient.request({ url: `${gcs_config_1.GCSConfig.storageAPI}/${this.bucket}` });
}
async create(req, config) {
const file = new GCSFile(config);
file.name = this.namingFunction(file, req);
await this.validate(file);
try {
const existing = await this.getMeta(file.id);
existing.bytesWritten = await this._write(existing);
return existing;
}
catch { }
const origin = (0, core_1.getHeader)(req, 'origin');
const headers = { 'Content-Type': 'application/json; charset=utf-8' };
headers['X-Upload-Content-Length'] = file.size.toString();
headers['X-Upload-Content-Type'] = file.contentType;
origin && (headers['Origin'] = origin);
const opts = {
body: JSON.stringify({ metadata: file.metadata }),
headers,
method: 'POST',
params: { name: file.name, size: file.size, uploadType: 'resumable' },
url: this.uploadBaseURI
};
const res = await this.authClient.request(opts);
file.uri = res.headers.location;
if (this.config.clientDirectUpload) {
file.GCSUploadURI = file.uri;
this.logger.debug('send uploadURI to client: %s', file.GCSUploadURI);
file.status = 'created';
return file;
}
await this.saveMeta(file);
file.status = 'created';
return file;
}
async write(part) {
const file = await this.getMeta(part.id);
await this.checkIfExpired(file);
if (file.status === 'completed')
return file;
if (!(0, core_1.partMatch)(part, file))
return (0, core_1.fail)(core_1.ERRORS.FILE_CONFLICT);
await this.lock(part.id);
try {
file.bytesWritten = await this._write({ ...file, ...part });
file.status = (0, core_1.getFileStatus)(file);
if (file.status === 'completed') {
file.uri = `${this.storageBaseURI}/${file.name}`;
await this._onComplete(file);
}
}
finally {
await this.unlock(part.id);
}
return file;
}
async delete({ id }) {
const file = await this.getMeta(id).catch(() => null);
if (file?.uri) {
file.status = 'deleted';
await Promise.all([
this.authClient.request({ method: 'DELETE', url: file.uri, validateStatus }),
this.deleteMeta(file.id)
]);
return [{ ...file }];
}
return [{ id }];
}
async _write(part) {
const { size, uri = '', body } = part;
const contentRange = buildContentRange(part);
const options = { method: 'PUT' };
if (body?.on) {
const abortController = new abort_controller_1.AbortController();
body.on('aborted', _ => abortController.abort());
options.body = body;
options.signal = abortController.signal;
}
options.headers = { 'Content-Range': contentRange, Accept: 'application/json' };
try {
const res = await (0, node_fetch_1.default)(uri, options);
if (res.status === 308) {
const range = res.headers.get('range');
return range ? getRangeEnd(range) : 0;
}
else if (res.ok) {
const data = (await res.json());
this.logger.debug('uploaded %O', data);
return size;
}
const message = await res.text();
return Promise.reject({
message,
code: `GCS${res.status}`,
config: { uri },
name: 'FetchError'
});
}
catch (err) {
this.logger.error(uri, err);
return NaN;
}
}
}
exports.GCStorage = GCStorage;