UNPKG

@uploadx/gcs

Version:
183 lines (182 loc) 6.68 kB
"use strict"; 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 google_auth_library_1 = require("google-auth-library"); 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 { GCSUploadURI; uri; } 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, * maxFileSize: '15GB', * allowedMimeTypes: ['video/*', 'image/*'], * namingFunction: file => file.originalName * }); * ``` */ class GCStorage extends core_1.BaseStorage { options; bucket; authClient; storageBaseURI; uploadBaseURI; meta; constructor(options = {}) { super(options); this.options = options; this.meta = (0, core_1.createMetaStorage)(options, gcs_meta_storage_1.GCSMetaStorage); this.bucket = options.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('/'); options.scopes ||= gcs_config_1.GCSConfig.authScopes; this.authClient = new google_auth_library_1.GoogleAuth(options); this.isReady = false; this.accessCheck() .then(() => (this.isReady = true)) .catch(err => this.logger.error('Storage access check failed: {err.message}', { 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: (0, core_1.mapValues)(file.metadata, String) }), 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.get('location'); if (this.options.clientDirectUpload) { file.GCSUploadURI = file.uri; this.logger.debug('Send uploadURI to client', { uri: 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); 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); } 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 AbortController(); body.on('aborted', _ => abortController.abort()); options.body = body; options.signal = abortController.signal; options.duplex = 'half'; } options.headers = { 'Content-Range': contentRange, Accept: 'application/json' }; try { const res = await fetch(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('Upload completed', { 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('Upload chunk failed', { uri, err }); return NaN; } } _onComplete = (file) => { return this.deleteMeta(file.id); }; } exports.GCStorage = GCStorage;