UNPKG

@uploadx/gcs

Version:
195 lines (194 loc) 7.29 kB
"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;