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.

693 lines 30.7 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 __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NfsServerCore = void 0; const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const xml_1 = __importDefault(require("xml")); const bcrypt_1 = __importDefault(require("bcrypt")); const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); const tspace_utils_1 = require("tspace-utils"); const default_html_1 = __importDefault(require("./default-html")); const server_queue_1 = require("./server.queue"); const utils_1 = require("../utils"); class NfsServerCore { constructor() { this._queue = new server_queue_1.Queue(3); this._fileExpired = 60 * 60; this._rootFolder = 'nfs'; this._jwtExipred = 60 * 60; this._jwtSecret = `<secret@${+new Date()}:${Math.floor(Math.random() * 9999)}>`; this._cluster = false; this._progress = false; this._debug = false; this._trash = '@Recycle bin'; this._metadata = '@meta.json'; this._utils = new utils_1.Utils(this._buckets, this._rootFolder, this._metadata, this._trash); this._default = ({ res }) => __awaiter(this, void 0, void 0, function* () { return res.html(this._html == null ? default_html_1.default : String(this._html)); }); this._benchmark = () => { return 'benchmark in nfs server'; }; this._media = ({ req, res, query, params }) => __awaiter(this, void 0, void 0, function* () { var _a; try { const { AccessKey, Expires, Signature, Download } = query; const bucket = params.bucket; if ([ AccessKey, Expires, Signature, Download, bucket ].some(v => v === '' || v == null)) { res.writeHead(400, { 'Content-Type': 'text/xml' }); const error = { Error: [ { Code: 'Bad request' }, { Message: 'The request was invalid' }, { Resource: req.url }, { RequestKey: query === null || query === void 0 ? void 0 : query.key } ] }; return res.end((0, xml_1.default)([error], { declaration: true })); } const path = String(params['*']).replace(/^\/+/, '').replace(/\.{2}(?!\.)/g, ""); const combined = `@{${path}-${bucket}-${AccessKey}-${Expires}-${Download}}`; const compare = bcrypt_1.default.compareSync(combined, Buffer.from(Signature, 'base64').toString('utf-8')); const expired = Number.isNaN(+Expires) ? true : new Date(+Expires) < new Date(); if (!compare || expired) { res.writeHead(400, { 'Content-Type': 'text/xml' }); const error = { Error: [ { Code: expired ? 'Expired' : 'AccessDenied' }, { Message: expired ? 'Request has expired' : 'The signature is not correct' }, { Resource: req.url }, { RequestKey: query.AccessKey } ] }; return res.end((0, xml_1.default)([error], { declaration: true })); } const { stream, header, set } = yield this._utils.makeStream({ bucket: bucket, filePath: String(path), range: (_a = req.headers) === null || _a === void 0 ? void 0 : _a.range, download: Download === Buffer.from(`${Expires}@true`).toString('base64').replace(/[=|?|&]+$/g, '') }); if (stream == null || header == null) { res.writeHead(404, { 'Content-Type': 'text/xml' }); const error = { Error: [ { Code: 'Not found' }, { Message: 'The file does not exist in our records' }, { Resource: req.url }, { RequestKey: query.AccessKey } ] }; return res.end((0, xml_1.default)([error], { declaration: true })); } set(res); return stream.pipe(res); } catch (err) { const message = String(err.message); const path = String(params['*']).replace(/^\/+/, '').replace(/\.{2}(?!\.)/g, ""); const isNotFound = message.includes('ENOENT: no such file or directory'); res.writeHead(isNotFound ? 404 : 400, { 'Content-Type': 'text/xml' }); const error = { Error: [ { Code: isNotFound ? 'Not found' : 'AccessDenied' }, { Message: isNotFound ? `The file '${path}' does not exist` : message }, { Resource: req.url }, { RequestKey: query.AccessKey } ] }; return res.end((0, xml_1.default)([error], { declaration: true })); } }); this._apiFile = ({ req, res, body }) => __awaiter(this, void 0, void 0, function* () { try { const { bucket, token } = req; let { path, download, expired } = body; const fileName = `${path}`.replace(/^\/+/, ''); const directory = this._utils.normalizeDirectory({ bucket, folder: null }); const fullPath = this._utils.normalizePath({ directory, path: String(path), full: true }); if (!(yield this._utils.fileExists(fullPath))) { if (this._debug) { console.log({ fullPath, path, download, expired }); } return res.notFound(`No such directory or file, '${fileName}'`); } const key = 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}-${key}-${expires}-${downloaded}}`; const signature = Buffer.from(bcrypt_1.default.hashSync(combined, 1)).toString('base64'); return res.ok({ endpoint: [ `${bucket}/${fileName}?AccessKey=${key}`, `Expires=${expires}`, `Download=${downloaded}`, `Signature=${signature}` ].join('&') }); } catch (err) { if (this._debug) { console.log(err); } return res.serverError(err.message); } }); this._apiBase64 = ({ req, res, body }) => __awaiter(this, void 0, void 0, function* () { try { const { bucket } = req; const { path: filename } = body; const directory = this._utils.normalizeDirectory({ bucket, folder: null }); const path = this._utils.normalizePath({ directory, path: String(filename), full: true }); if (!(yield this._utils.fileExists(path))) { return res.notFound(`no such file or directory, '${filename}'`); } const stat = fs_1.default.statSync(path); if (stat.isDirectory()) { return res.badRequest('The path is a directory, cannot be read from the filesystem'); } return res.ok({ base64: yield fs_1.default.promises.readFile(path, 'base64') }); } catch (err) { if (this._debug) { console.log(err); } return res.serverError(err.message); } }); this._apiStream = ({ req, res, body }) => __awaiter(this, void 0, void 0, function* () { try { const { bucket } = req; const { path: filename, range } = body; const directory = this._utils.normalizeDirectory({ bucket, folder: null }); const fullPath = this._utils.normalizePath({ directory, path: String(filename), full: true }); if (!(yield this._utils.fileExists(fullPath))) { return res.notFound(`no such file or directory, '${filename}'`); } const stat = fs_1.default.statSync(fullPath); const fileSize = stat.size; if (range) { const parts = String(range).replace(/bytes=/, "").split("-"); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; const chunksize = (end - start) + 1; const file = fs_1.default.createReadStream(fullPath, { start, end }); const head = { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4', }; res.writeHead(206, head); return file.pipe(res); } const head = { 'Content-Length': fileSize, 'Content-Type': 'video/mp4', }; res.writeHead(200, head); return fs_1.default.createReadStream(fullPath).pipe(res); } catch (err) { if (this._debug) { console.log(err); } throw err; } }); this._apiStorage = ({ req, res, body }) => __awaiter(this, void 0, void 0, function* () { try { const { bucket } = req; let { folder } = body; if (folder != null) { folder = this._utils.normalizeFolder(String(folder)); } const directory = this._utils.normalizeDirectory({ bucket, folder }); if (!(yield this._utils.fileExists(directory))) { return res.notFound(`No such directory or folder, '${folder}'`); } const fileDirectories = yield this._utils.files(directory, { ignore: this._trash }); const storage = fileDirectories.map((name) => { const stat = fs_1.default.statSync(name); return { name: path_1.default.relative(directory, name).replace(/\\/g, '/'), size: Number((stat.size / (1024 * 1024))) }; }); return res.ok({ storage }); } catch (err) { if (this._debug) { console.log(err); } return res.serverError(err.message); } }); this._apiFolders = ({ req, res }) => __awaiter(this, void 0, void 0, function* () { try { const { bucket } = req; const directory = this._utils.normalizeDirectory({ bucket, folder: null }); const folders = fs_1.default.readdirSync(directory); return res.ok({ folders }); } catch (err) { if (this._debug) { console.log(err); } return res.serverError(err.message); } }); this._apiUpload = ({ req, res, files, body }) => __awaiter(this, void 0, void 0, function* () { try { const { bucket } = req; if (!Array.isArray(files === null || files === void 0 ? void 0 : files.file)) { return res.badRequest('The file is required.'); } const file = files === null || files === void 0 ? void 0 : files.file[0]; if (file == null) { return res.badRequest('The file is required.'); } let { folder } = body; if (folder != null) { folder = this._utils.normalizeFolder(String(folder)); } const directory = this._utils.normalizeDirectory({ bucket, folder }); if (!(yield this._utils.fileExists(directory))) { if (this._debug) { console.log({ directory, bucket, folder }); } yield fs_1.default.promises.mkdir(directory, { recursive: true }); } const writeFile = (file, to) => { return new Promise((resolve, reject) => { fs_1.default.createReadStream(file) .pipe(fs_1.default.createWriteStream(to)) .on('finish', () => { // remove temporary from chunked by nfs-client this._utils.remove(to); // remove temporary from server this._utils.remove(file, { delayMs: 0 }); return resolve(null); }) .on('error', (err) => reject(err)); return; }); }; yield writeFile(file.tempFilePath, this._utils.normalizePath({ directory, path: file.name, full: true })); yield this._utils.getMetadata(bucket); return res.ok({ path: this._utils.normalizePath({ directory: folder, path: file.name }), name: file.name, size: file.size }); } catch (err) { if (this._debug) { console.log(err, 'here!'); } throw err; } }); this._apiMerge = ({ req, res, body }) => __awaiter(this, void 0, void 0, function* () { try { const { bucket } = req; let { folder, name, paths, totalSize } = body; if (folder != null) { folder = this._utils.normalizeFolder(String(folder)); } const directory = this._utils.normalizeDirectory({ bucket, folder }); if (!(yield this._utils.fileExists(directory))) { yield fs_1.default.promises.mkdir(directory, { recursive: true }); } const writeFile = (to) => __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => { const writeStream = fs_1.default.createWriteStream(to, { flags: 'a' }); writeStream.on('error', (err) => { return reject(err); }); let processedSize = 0; const next = (index = 0) => { if (index >= paths.length) { writeStream.end(); writeStream.close(); return resolve(null); } const partPath = this._utils.normalizePath({ directory, path: paths[index], full: true }); const readStream = fs_1.default.createReadStream(partPath, { highWaterMark: 1024 * 1024 * 100 }); if (this._progress) { readStream.on('data', (chunk) => { processedSize += chunk.length; const progress = ((processedSize / totalSize) * 100).toFixed(2); console.log(`The file '${path_1.default.basename(to)}' in progress: ${progress}%`); }); } readStream.on('error', (err) => { return reject(err); }); readStream.on('end', () => { this._utils.remove(partPath, { delayMs: 0 }); next(index + 1); }); readStream.pipe(writeStream, { end: false }); }; next(); }); }); const to = this._utils.normalizePath({ directory, path: name, full: true }); yield writeFile(to); yield this._utils.getMetadata(bucket); return res.ok({ path: this._utils.normalizePath({ directory: folder, path: name }), name: name, size: fs_1.default.statSync(to).size }); } catch (err) { if (this._debug) { console.log(err); } throw err; } }); this._apiUploadBase64 = ({ req, res, body }) => __awaiter(this, void 0, void 0, function* () { try { const { bucket } = req; let { folder, base64, name } = body; if (folder != null) { folder = this._utils.normalizeFolder(String(folder)); } if (base64 === '' || base64 == null) { return res.badRequest('The base64 is required.'); } if (name === '' || name == null) { return res.badRequest('The name is required.'); } const directory = this._utils.normalizeDirectory({ bucket, folder }); if (!(yield this._utils.fileExists(directory))) { yield fs_1.default.promises.mkdir(directory, { recursive: true }); } const writeFile = (base64, to) => __awaiter(this, void 0, void 0, function* () { return fs_1.default.promises.writeFile(to, String(base64), 'base64'); }); const to = path_1.default.join(path_1.default.resolve(), `${directory}/${name}`); yield writeFile(String(base64), to); yield this._utils.getMetadata(bucket); return res.ok({ path: folder ? `${folder}/${name}` : name, name: name, size: fs_1.default.statSync(to).size }); } catch (err) { if (this._debug) { console.log(err); } throw err; } }); this._apiRemove = ({ req, res, body }) => __awaiter(this, void 0, void 0, function* () { try { const { bucket } = req; const { path: p } = body; const path = `${p}`.replace(/^\/+/, ''); const directory = this._utils.normalizeDirectory({ bucket, folder: null }); const fullPath = this._utils.normalizePath({ directory, path: path, full: true }); if (!(yield this._utils.fileExists(fullPath))) { return res.notFound(`No such directory or file, '${path}'`); } this._queue.add(() => __awaiter(this, void 0, void 0, function* () { return yield this._utils.trashed({ path, bucket }); })); yield this._utils.getMetadata(bucket); return res.ok(); } catch (err) { if (this._debug) { console.log(err); } return res.serverError(err.message); } }); this._apiConnect = ({ res, body }) => __awaiter(this, void 0, void 0, function* () { const { token, secret, bucket } = body; if (this._credentials != null) { const credentials = yield this._credentials({ token: String(token), secret: String(secret), bucket: String(bucket) }); if (!credentials) { return res.unauthorized('Invalid credentials. Please check the your credentials'); } } const directory = path_1.default.join(path_1.default.resolve(), this._utils.normalizeDirectory({ bucket: String(bucket) })); if (!(yield this._utils.fileExists(directory))) { yield fs_1.default.promises.mkdir(directory, { recursive: true }); } return res.ok({ accessToken: jsonwebtoken_1.default.sign({ data: { issuer: 'nfs-server', sub: { bucket, token } } }, this._jwtSecret, { expiresIn: this._jwtExipred, algorithm: 'HS256' }) }); }); this._apiHealthCheck = ({ res, headers }) => __awaiter(this, void 0, void 0, function* () { const token = String(headers.authorization).split(' ')[1]; if (token == null) { return res.unauthorized('Please check your credentials. Are they valid ?'); } const payload = token.split('.')[1]; if (payload == null || payload === '') { return res.unauthorized('Please check your credentials. Are they valid ?'); } const decodedPayload = this._utils.safelyParseJSON(Buffer.from(payload, 'base64').toString('utf-8')); if (decodedPayload.exp) { const currentTime = Math.floor(Date.now() / 1000); const timeRemaining = decodedPayload.exp - currentTime; if (timeRemaining > 0) { const days = Math.floor(timeRemaining / (24 * 60 * 60)); const hours = Math.floor((timeRemaining % (24 * 60 * 60)) / (60 * 60)); const minutes = Math.floor((timeRemaining % (60 * 60)) / 60); const seconds = timeRemaining % 60; return res.ok({ iat: new Date(decodedPayload.iat * 1000), exp: new Date(decodedPayload.exp * 1000), expire: { days, hours, minutes, seconds } }); } return res.badRequest('Token has expired'); } return res.badRequest("Token does not have an expiration time."); }); this._authMiddleware = ({ req, res, headers }, next) => { const authorization = String(headers.authorization).split(' ')[1]; if (authorization == null) { return res.unauthorized('Please check your credentials. Are they valid ?'); } const { bucket, token } = this._verify(authorization); req.bucket = bucket; req.token = token; return next(); }; this._authStudioMiddleware = ({ req, res, cookies }, next) => { var _a, _b; const authorization = cookies['auth.session']; if (authorization == null || authorization === '') { if ((_a = req.url) === null || _a === void 0 ? void 0 : _a.includes('/studio/preview')) { res.writeHead(401, { 'Content-Type': 'text/xml' }); const error = { Error: [ { Code: 'Unauthorized' }, { Message: 'Please check your credentials. Are they valid ?' }, { Resource: req.url }, { RequestKey: "" } ] }; return res.end((0, xml_1.default)([error], { declaration: true })); } return res.unauthorized('Please check your credentials. Are they valid ?'); } try { const { buckets, token } = this._verify(authorization); req.buckets = buckets; req.token = token; return next(); } catch (e) { if ((_b = req.url) === null || _b === void 0 ? void 0 : _b.includes('/studio/preview')) { res.writeHead(400, { 'Content-Type': 'text/xml' }); const error = { Error: [ { Code: 'Bad Request' }, { Message: e.message }, { Resource: req.url }, { RequestKey: "" } ] }; return res.end((0, xml_1.default)([error], { declaration: true })); } return res.badRequest(e.message); } }; } get instance() { return this._app; } /** * The 'progress' is method used to view the progress of the file upload. * * @returns {this} */ debug() { this._debug = true; return this; } /** * The 'progress' is method used to view the progress of the file upload. * * @returns {this} */ progress() { this._progress = true; return this; } /** * The 'defaultPage' is method used to set default home page. * * @param {string} html * @returns {this} */ defaultPage(html) { this._html = html; return this; } /** * The 'directory' is method used to set directory for root directory * * @param {string} folder * @returns {this} */ directory(folder) { this._rootFolder = folder; return this; } /** * The 'cluster' is method used to make cluster for server * * @param {number} workers * @returns {this} */ cluster(workers) { this._cluster = workers == null ? true : workers; return this; } /** * The 'fileExpired' is method used to set file expiration * * @param {number} seconds * @returns {this} */ fileExpired(seconds) { this._fileExpired = seconds; return this; } /** * The 'credentials' is method used to set expiration and secret for credentials * * @param {object} credentials * @property {number} credentials.expired by seconds * @property {string?} credentials.secret * @returns {this} */ credentials({ expired, secret }) { this._jwtExipred = expired; if (secret) { this._jwtSecret = secret; } return this; } /** * The 'bucketLists' method is used to inform the server about the available bucket lists. * * @param {function} callback * @returns {this} */ bucketLists(callback) { this._buckets = callback; return this; } /** * The 'onBucketLists' method is used to inform the server about the available bucket lists. * * @param {function} callback * @returns {this} */ onLoadBucketLists(callback) { this._buckets = callback; return this; } /** * The 'onCredentials' is method used to wrapper to check the credentials. * * @param {function} callback * @returns {this} */ onCredentials(callback) { this._credentials = callback; return this; } _verify(token) { try { const decoded = jsonwebtoken_1.default.verify(token, this._jwtSecret); return decoded.data.sub; } catch (err) { let message = err.message; if (err.name === 'JsonWebTokenError') { message = 'Invalid credentials'; } if (err.name === 'TokenExpiredError') { message = 'Token has expired'; } const error = new Error(message); error.statusCode = 400; throw error; } } } exports.NfsServerCore = NfsServerCore; exports.default = NfsServerCore; //# sourceMappingURL=server.core.js.map