UNPKG

tspace-nfs

Version:

tspace-nfs is a Network File System (NFS) and provides both server and client capabilities for accessing files over a network.

588 lines 23.8 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __asyncValues = (this && this.__asyncValues) || function (o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NfsClient = void 0; const events_1 = __importDefault(require("events")); const axios_1 = __importDefault(require("axios")); const form_data_1 = __importDefault(require("form-data")); const fs_1 = __importDefault(require("fs")); const http_1 = __importDefault(require("http")); const https_1 = __importDefault(require("https")); const bcrypt_1 = __importDefault(require("bcrypt")); const tspace_utils_1 = require("tspace-utils"); /** * * The 'NfsClient' class is a client for nfs server * @example * import { NfsClient } from "tspace-nfs"; * import PathSystem from 'path'; * import pathSystem from 'path'; * * const nfs = new NfsClient({ * token : '<YOUR TOKEN>', // token * secret : '<YOUR SECRET>', // secret * bucket : '<YOUR BUCKET>', // bucket name * url : '<YOUR URL>' // https://nfs-server.example.com * }) * .onError((err, nfs) => { * console.log('nfs client failed to connect') * console.log(err.message) * nfs.quit() * }) * .onConnect((nfs) => { * console.log('nfs client connected') * }) * * const mycat = 'cats/my-cat.png' * const url = await nfs.toURL(mycat) * * console.log(url) */ class NfsClient { constructor({ token, secret, bucket, url }) { this._directory = ''; this._event = new events_1.default(); this._authorization = ''; this._url = 'http://localhost:8000'; this._fileExpired = 60 * 60; this._ENDPOINT_CONNECT = 'connect'; this._ENDPOINT_REMOVE = 'remove'; this._ENDPOINT_FILE = 'file'; this._ENDPOINT_FILE_BASE64 = 'base64'; this._ENDPOINT_FILE_STREAM = 'stream'; this._ENDPOINT_STORAGE = 'storage'; this._ENDPOINT_FOLDERS = 'folders'; this._ENDPOINT_UPLOAD = 'upload'; this._ENDPOINT_MERGE = 'upload/merge'; this._ENDPOINT_UPLOAD_BASE64 = 'upload/base64'; this._TOKEN_EXPIRED_MESSAGE = 'Token has expired'; this._credentials = { token: '', secret: '', bucket: '' }; this._credentials = { token, secret, bucket }; this._url = url; this._getConnect(this._credentials); } /** * The 'default' method is used to default prefix the directory every path * * @param {string} directory * @returns {this} */ default(directory) { this._directory = directory; return this; } /** * The 'onError' method is used to handle the error that occurs when trying connect to nfs server * * @param {Function} callback * @returns {this} */ onError(callback) { this._event.on('error', callback); return this; } /** * The 'onConnect' method is used cheke the connection to nfs server * * @param {Function} callback * @returns {this} */ onConnect(callback) { this._event.on('connected', callback); return this; } /** * The 'quit' method is used quit the connection and stop the serivce * * @return {never} */ quit() { return process.exit(0); } /** * The 'toURL' method is used to converts a given file path to a URL * * @param {string} path * @param {object} options * @property {boolean} options.download * @property {number} options.expired // expires in seconds * @property {number} options.exists // checks the file exists only * @return {promise<string>} */ toURL(path, { download = true, expired, exists = false } = {}) { var _a; return __awaiter(this, void 0, void 0, function* () { try { if (exists != null && exists) { const url = this._URL(this._ENDPOINT_FILE); const response = yield this._fetch({ url, data: { path: this._normalizeDefaultDirectory(path), download, expired } }); return `${this._url}/${(_a = response.data) === null || _a === void 0 ? void 0 : _a.endpoint}`; } const fileName = `${path}`.replace(/^\/+/, ''); const { token, bucket } = this._credentials; const accessKey = String(token); const expires = new tspace_utils_1.Time().addSeconds(expired == null || Number.isNaN(Number(expired)) ? this._fileExpired : Number(expired)).toTimeStamp(); const downloaded = `${Buffer.from(`${expires}@${download}`).toString('base64').replace(/[=|?|&]+$/g, '')}`; const combined = `@{${path}-${bucket}-${accessKey}-${expires}-${downloaded}}`; const signature = Buffer.from(bcrypt_1.default.hashSync(combined, 1)).toString('base64'); const endpoint = [ `${bucket}/${fileName}?AccessKey=${accessKey}`, `Expires=${expires}`, `Download=${downloaded}`, `Signature=${signature}` ].join('&'); return `${this._url}/${endpoint}`; } catch (err) { return yield this._retryConnect(err, () => __awaiter(this, void 0, void 0, function* () { return yield this.toURL(path, { download, expired, exists }); })); } }); } /** * The 'toBase64' method is used to converts a given file path to base64 encoded * * @param {string} path * @return {promise<string>} */ toBase64(path) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { try { const url = this._URL(this._ENDPOINT_FILE_BASE64); const response = yield this._fetch({ url, data: { path: this._normalizeDefaultDirectory(path), } }); return (_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.base64) !== null && _b !== void 0 ? _b : ''; } catch (err) { return yield this._retryConnect(err, () => __awaiter(this, void 0, void 0, function* () { return yield this.toBase64(path); })); } }); } /** * The 'toStream' method is used to converts a given file path to stream format * * @param {string} path * @param {string?} range * @return {promise<string>} */ toStream(path, range) { return __awaiter(this, void 0, void 0, function* () { try { const url = this._URL(this._ENDPOINT_FILE_STREAM); const response = yield this._fetch({ url, data: { path: this._normalizeDefaultDirectory(path), range }, type: 'stream' }); return response.data; } catch (err) { return yield this._retryConnect(err, () => __awaiter(this, void 0, void 0, function* () { return yield this.toStream(path, range); })); } }); } /** * The 'upload' method is used uploading file * * @param {object} obj * @property {string} obj.file * @property {string} obj.name * @property {string?} obj.folder * @property {number?} obj.chunkSize // unit mb by default 200 mb * @return {promise<{size : number , path : string , name : string , url : string}>} */ upload({ file, name, extension, folder, chunkSize }) { var _a, e_1, _b, _c; var _d; return __awaiter(this, void 0, void 0, function* () { const CHUNK_SIZE = 1024 * 1024 * (chunkSize == null ? 200 : chunkSize); const stats = fs_1.default.statSync(file); const fileSize = stats.size; const totalParts = Math.ceil(fileSize / CHUNK_SIZE); const fileStream = fs_1.default.createReadStream(file, { highWaterMark: CHUNK_SIZE }); let partNumber = 0; const files = []; try { for (var _e = true, fileStream_1 = __asyncValues(fileStream), fileStream_1_1; fileStream_1_1 = yield fileStream_1.next(), _a = fileStream_1_1.done, !_a; _e = true) { _c = fileStream_1_1.value; _e = false; const chunk = _c; partNumber++; const fileId = Math.random().toString(36).substring(2, 12).replace(/[.@]/g, ''); const form = new form_data_1.default(); const fileName = `${name.split('.')[0]}_${fileId}@${`0${partNumber}`.slice(-2)}`; form.append('file', chunk, fileName); form.append('folder', this._normalizeDefaultDirectory(folder)); const url = this._URL(this._ENDPOINT_UPLOAD); const response = yield this._fetch({ url, data: form, type: 'form-data' }) .catch(_ => 'fail to upload file'); if (response === 'fail to upload file') break; files.push(fileName); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (!_e && !_a && (_b = fileStream_1.return)) yield _b.call(fileStream_1); } finally { if (e_1) throw e_1.error; } } if (totalParts !== files.length) { for (const file of files) { const path = folder == null ? file : `${folder}/${file}`; yield this.delete(this._normalizeDefaultDirectory(path)).catch(_ => null); } throw new Error('Could not upload files. Please verify your file and try again.'); } try { const response = yield this._fetch({ url: this._URL(this._ENDPOINT_MERGE), data: { folder: this._normalizeDefaultDirectory(folder), name: this._normalizeFilename({ name, extension }), paths: files, totalSize: fileSize } }); const normalizedPath = String((_d = response.data) === null || _d === void 0 ? void 0 : _d.path) .replace(this._directory, '') .replace(/^\/+/, ''); return Object.assign(Object.assign({}, response.data), { url: yield this.toURL(normalizedPath), path: normalizedPath }); } catch (err) { for (const file of files) { const path = folder == null ? file : `${folder}/${file}`; yield this.delete(path).catch(_ => null); } return yield this._retryConnect(err, () => __awaiter(this, void 0, void 0, function* () { return yield this.upload({ file, name: this._normalizeFilename({ name, extension }), extension, folder, chunkSize }); })); } }); } /** * The 'save' method is used uploading file * * @param {object} obj * @property {string} obj.file * @property {string} obj.name * @property {string?} obj.folder * @property {number?} obj.chunkSize // unit mb by default 200 mb * @return {promise<{size : number , path : string , name : string , url : string}>} */ save({ file, name, extension, folder, chunkSize }) { return __awaiter(this, void 0, void 0, function* () { return yield this.upload({ file, name, extension, folder, chunkSize }); }); } /** * The 'uploadBase64' method is used uploading file with base64 encoded * * @param {object} obj * @property {string} obj.base64 * @property {string} obj.name * @property {string?} obj.folder * @return {promise<{size : number , path : string , name : string}>} */ uploadBase64({ base64, name, extension, folder }) { var _a; return __awaiter(this, void 0, void 0, function* () { try { const url = this._URL(this._ENDPOINT_UPLOAD_BASE64); const response = yield this._fetch({ url, data: { base64, folder: this._normalizeDefaultDirectory(folder), name: this._normalizeFilename({ name, extension }) } }); return Object.assign({ url: yield this.toURL((_a = response.data) === null || _a === void 0 ? void 0 : _a.path) }, response.data); } catch (err) { return yield this._retryConnect(err, () => __awaiter(this, void 0, void 0, function* () { return yield this.uploadBase64({ base64, name: this._normalizeFilename({ name, extension }), folder, extension }); })); } }); } /** * The 'saveAS' method is used uploading file with base64 encoded * * @param {object} obj * @property {string} obj.base64 * @property {string} obj.name * @property {string?} obj.folder * @return {promise<{size : number , path : string , name : string}>} */ saveAs({ base64, name, extension, folder }) { return __awaiter(this, void 0, void 0, function* () { return yield this.uploadBase64({ base64, name, extension, folder }); }); } /** * The 'delete' method is used to delete a file * * @param {string} path * @return {promise<string>} */ delete(path) { return __awaiter(this, void 0, void 0, function* () { try { const url = this._URL(this._ENDPOINT_REMOVE); yield this._fetch({ url, data: { path: this._normalizeDefaultDirectory(path) } }); return; } catch (err) { return yield this._retryConnect(err, () => __awaiter(this, void 0, void 0, function* () { return yield this.delete(path); })); } }); } /** * The 'remove' method is used to delete a file * * @param {string} path * @return {promise<string>} */ remove(path) { return __awaiter(this, void 0, void 0, function* () { return yield this.delete(path); }); } /** * The 'storage' method is used to get information about the storage * * @param {string?} folder * @return {Promise<{name : string , size : number }[]>} */ storage(folder) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { try { const url = this._URL(this._ENDPOINT_STORAGE); const response = yield this._fetch({ url, data: { folder: this._normalizeDefaultDirectory(folder) } }); return (_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.storage) !== null && _b !== void 0 ? _b : []; } catch (err) { return yield this._retryConnect(err, () => __awaiter(this, void 0, void 0, function* () { return yield this.storage(folder); })); } }); } /** * The 'folders' method is used to get list of folders * * @return {Promise<{name : string , size : number }[]>} */ folders() { var _a, _b; return __awaiter(this, void 0, void 0, function* () { try { const url = this._URL(this._ENDPOINT_FOLDERS); const response = yield this._fetch({ url, data: {} }); return (_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.folders) !== null && _b !== void 0 ? _b : []; } catch (err) { return yield this._retryConnect(err, () => __awaiter(this, void 0, void 0, function* () { return yield this.folders(); })); } }); } _fetch({ url, data, type = 'json', method }) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { try { let headers = { authorization: `Bearer ${this._authorization}`, Connection: 'keep-alive' }; if (type === 'form-data') { headers = Object.assign(Object.assign({}, headers), data.getHeaders()); } const configs = { url, data, headers, method: method == null ? 'POST' : method, maxBodyLength: Infinity, maxContentLength: Infinity, httpAgent: new http_1.default.Agent({ keepAlive: true, timeout: 0, maxSockets: 10, maxFreeSockets: 5 }), httpsAgent: new https_1.default.Agent({ keepAlive: true, rejectUnauthorized: false, timeout: 0, maxSockets: 10, maxFreeSockets: 5, }), timeout: 0, maxRate: [Infinity, Infinity], responseType: type }; return yield (0, axios_1.default)(configs); } catch (err) { const message = ((_b = (_a = err.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.message) || err.message; throw new Error(message); } }); } _URL(endpoint) { return `${this._url}/api/${endpoint}`; } _getConnect({ token, secret, bucket }) { const url = this._URL(this._ENDPOINT_CONNECT); axios_1.default.post(url, { token, secret, bucket }) .then((response) => { var _a; this._authorization = (_a = response.data) === null || _a === void 0 ? void 0 : _a.accessToken; this._event.emit('connected', this); }) .catch((err) => { var _a, _b; const message = ((_b = (_a = err.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.message) || (err === null || err === void 0 ? void 0 : err.message) || 'Server error'; if (message !== this._TOKEN_EXPIRED_MESSAGE) { if (message.includes('connect ECONNREFUSED')) { this._event.emit('error', new Error('Cannot connect to the NFS server, Please try again later.'), this); return; } } this._event.emit('error', new Error(message), this); }); return; } _retryConnect(err, fn) { var _a, _b, _c; return __awaiter(this, void 0, void 0, function* () { const message = ((_b = (_a = err.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.message) || err.message; if (message !== this._TOKEN_EXPIRED_MESSAGE) { if (message.includes('connect ECONNREFUSED')) { throw new Error('Cannot connect to the NFS server, Please try again later.'); } throw new Error(message); } try { const response = yield (0, axios_1.default)({ url: this._URL(this._ENDPOINT_CONNECT), data: Object.assign({}, this._credentials), method: 'POST' }); this._authorization = (_c = response.data) === null || _c === void 0 ? void 0 : _c.accessToken; return yield fn(); } catch (err) { throw new Error('Failed to connect to nfs server, Please check your credentials and try again.'); } }); } _normalizeFilename({ name, extension }) { return extension == null ? name : `${name.split('.')[0]}.${extension}`; } _normalizeDefaultDirectory(directory) { if (directory == null) { return this._directory === '' ? this._directory : this._directory.replace(/\/\//g, "/"); } const normalized = (this._directory === '' ? directory : `${this._directory}/${directory}`).replace(/\/\//g, "/"); return normalized; } } exports.NfsClient = NfsClient; exports.default = NfsClient; //# sourceMappingURL=index.js.map