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.

590 lines 31 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.NfsStudio = void 0; const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); const archiver_1 = __importDefault(require("archiver")); const html_minifier_terser_1 = require("html-minifier-terser"); const xml_1 = __importDefault(require("xml")); const server_core_1 = require("./server.core"); class NfsStudio extends server_core_1.NfsServerCore { constructor() { super(...arguments); this.BASE_FOLDER_STUDIO = 'studio-html'; this.studio = ({ res, cookies }) => __awaiter(this, void 0, void 0, function* () { const auth = cookies['auth.session']; if (!auth || auth == null) { const html = yield fs_1.default.promises.readFile(path_1.default.join(__dirname, this.BASE_FOLDER_STUDIO, 'login.html'), 'utf8'); const minifiedHtml = yield (0, html_minifier_terser_1.minify)(html, { collapseWhitespace: true, removeComments: true, minifyCSS: true, minifyJS: true }); return res.html(this._htmlFormatted(minifiedHtml)); } const html = yield fs_1.default.promises.readFile(path_1.default.join(__dirname, this.BASE_FOLDER_STUDIO, 'index.html'), 'utf8'); const minifiedHtml = yield (0, html_minifier_terser_1.minify)(html, { collapseWhitespace: true, removeComments: true, minifyCSS: true, minifyJS: true }); return res.html(this._htmlFormatted(minifiedHtml)); }); this.studioStorage = ({ req, res }) => __awaiter(this, void 0, void 0, function* () { var _a, _b, _c; const allowBuckets = (_a = req.buckets) !== null && _a !== void 0 ? _a : []; const rootFolder = this._rootFolder; const buckets = (this._buckets == null ? fs_1.default.readdirSync(path_1.default.join(path_1.default.resolve(), rootFolder)).filter((name) => { return fs_1.default.statSync(path_1.default.join(rootFolder, name)).isDirectory(); }) : yield this._buckets()).filter((bucket) => allowBuckets.includes(bucket) || allowBuckets[0] === '*'); let totalSize = 0; for (const bucket of buckets) { const metadata = yield this._utils.getMetadata(bucket); if (metadata == null) continue; totalSize += Number((_c = (_b = metadata.info) === null || _b === void 0 ? void 0 : _b.size) !== null && _c !== void 0 ? _c : 0); } return res.ok({ buckets: buckets.length, storage: { bytes: totalSize, kb: Number((totalSize / 1024).toFixed(2)), mb: Number((totalSize / (1024 * 1024)).toFixed(2)), gb: Number((totalSize / (1024 * 1024 * 1024)).toFixed(2)) } }); }); this.studioPreview = ({ req, res, params }) => __awaiter(this, void 0, void 0, function* () { var _d, _e; const path = String(params['*']).replace(/^\/+/, '').replace(/\.{2}(?!\.)/g, ""); const [bucket, ...rest] = String(path).split('/'); const allowBuckets = req.buckets || []; if (!(allowBuckets.includes(bucket) || allowBuckets[0] === '*')) { return res.forbidden(); } let filePath = rest.join('/'); if (filePath != null) { filePath = this._utils.normalizeFolder(String(filePath)); } const directory = this._utils.normalizeDirectory({ bucket, folder: null }); if (!fs_1.default.existsSync(path_1.default.join(path_1.default.resolve(), directory, filePath))) { return res.notFound(`The directory '${path}' does not exist`); } const extension = path_1.default.extname(filePath).replace(/\./g, ''); const textFileExtensions = [ 'txt', 'md', 'csv', 'json', 'xml', 'html', 'css', 'js', 'ts', 'php', 'h', 'c', 'java', 'go', 'rs', 'py', 'sh', 'sql', 'log', 'ini', 'bat', 'yml', 'yaml', 'key', 'conf', 'rtf', 'tex', 'srt', 'plist', 'env', ]; if (textFileExtensions.includes(extension)) { const html = yield fs_1.default.promises.readFile(path_1.default.join(__dirname, this.BASE_FOLDER_STUDIO, 'vs-code.html'), 'utf8'); const minifiedHtml = yield (0, html_minifier_terser_1.minify)(html, { collapseWhitespace: true, removeComments: true, minifyCSS: true, minifyJS: true }); const language = (_d = { 'ts': 'typescript', 'js': 'javascript' }[extension]) !== null && _d !== void 0 ? _d : extension; const formatted = minifiedHtml .replace('{{language}}', language); return res.html(res.html(this._htmlFormatted(formatted))); } const { stream, set } = yield this._utils.makeStream({ bucket: bucket, filePath: String(filePath), range: (_e = req.headers) === null || _e === void 0 ? void 0 : _e.range, download: true }); set(res); return stream.pipe(res); }); this.studioPreviewText = ({ req, res, params }) => __awaiter(this, void 0, void 0, function* () { const path = String(params['*']).replace(/^\/+/, '').replace(/\.{2}(?!\.)/g, ""); const [bucket, ...rest] = String(path).split('/'); const allowBuckets = req.buckets || []; if (!(allowBuckets.includes(bucket) || allowBuckets[0] === '*')) { return res.forbidden(); } let filePath = rest.join('/'); if (filePath != null) { filePath = this._utils.normalizeFolder(String(filePath)); } const directory = this._utils.normalizeDirectory({ bucket, folder: null }); const fullPath = this._utils.normalizePath({ directory, path: String(filePath), full: true }); if (!fs_1.default.existsSync(fullPath)) { return res.notFound(`The directory '${path}' does not exist`); } const stat = fs_1.default.statSync(fullPath); if (stat.isDirectory()) { return res.badRequest('The path is a directory, cannot be read from the filesystem'); } const text = yield fs_1.default.promises.readFile(fullPath, 'utf8'); return res.send(text); }); this.studioPreviewTextEdit = ({ req, res, params, body }) => __awaiter(this, void 0, void 0, function* () { if (body.content == null) { return res.badRequest('Please enter a content for rewrite file'); } const path = String(params['*']).replace(/^\/+/, '').replace(/\.{2}(?!\.)/g, ""); const [bucket, ...rest] = String(path).split('/'); const allowBuckets = req.buckets || []; if (!(allowBuckets.includes(bucket) || allowBuckets[0] === '*')) { return res.forbidden(); } let filePath = rest.join('/'); if (filePath != null) { filePath = this._utils.normalizeFolder(String(filePath)); } const directory = this._utils.normalizeDirectory({ bucket, folder: null }); const fullPath = path_1.default.join(path_1.default.resolve(), directory, filePath); if (!fs_1.default.existsSync(fullPath)) { return res.notFound(`The directory '${path}' does not exist`); } fs_1.default.writeFileSync(fullPath, String(body.content), 'utf-8'); return res.ok(); }); this.studioLogin = ({ res, body }) => __awaiter(this, void 0, void 0, function* () { var _f; if (this._onStudioCredentials == null) { return res.badRequest('Please enable the studio'); } const { username, password } = body; if (!username) { return res.badRequest('Please enter an username'); } const check = yield this._onStudioCredentials({ username: String(username), password: String(password) }); if (!(check === null || check === void 0 ? void 0 : check.logged)) return res.unauthorized('Please check your username and password'); const EXPIRED = 43200; const session = jsonwebtoken_1.default.sign({ data: { issuer: 'nfs-studio', sub: { buckets: (_f = check === null || check === void 0 ? void 0 : check.buckets) !== null && _f !== void 0 ? _f : [], permissions: ['*'], token: Buffer.from(`${+new Date()}`).toString('base64') } } }, this._jwtSecret, { expiresIn: EXPIRED, algorithm: 'HS256' }); res.setHeader('Set-Cookie', `auth.session=${session}; HttpOnly; Max-Age=${EXPIRED}; Path=/studio`); return res.ok(); }); this.studioLogout = ({ res }) => __awaiter(this, void 0, void 0, function* () { res.setHeader('Set-Cookie', `auth.session=; HttpOnly; Max-Age=0; Path=/studio`); return res.ok(); }); this.studioUpload = ({ req, res, files, body }) => __awaiter(this, void 0, void 0, function* () { try { if (!Array.isArray(files === null || files === void 0 ? void 0 : files.file) || (files === null || files === void 0 ? void 0 : files.file[0]) == null) { return res.badRequest('The file is required.'); } if (body.path == null || body.path === '') { return res.badRequest('The path is required.'); } const file = files === null || files === void 0 ? void 0 : files.file[0]; const [bucket, ...rest] = String(body.path).split('/'); const allowBuckets = req.buckets || []; if (!(allowBuckets.includes(bucket) || allowBuckets[0] === '*')) { return res.forbidden(); } if (this._buckets != null && !(yield this._buckets()).includes(bucket)) { return res.forbidden(); } let folder = rest.join('/'); 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 server this._utils.remove(file, { delayMs: 0 }); return resolve(null); }) .on('error', (err) => reject(err)); return; }); }; const name = `${file.name}`; yield writeFile(file.tempFilePath, this._utils.normalizePath({ directory, path: name, full: true })); yield this._utils.syncMetadata(bucket); return res.ok({ path: this._utils.normalizePath({ directory: folder, path: name }), name: name, size: file.size }); } catch (err) { if (this._debug) { console.log(err); } throw err; } }); this.studioDownload = ({ req, res, body }) => __awaiter(this, void 0, void 0, function* () { try { const { files } = body; if (!(files === null || files === void 0 ? void 0 : files.length)) { return res.badRequest('Please specify which files to download.'); } const items = files.map(v => { return Object.assign(Object.assign({}, v), { path: this._utils.normalizePath({ directory: this._rootFolder, path: String(v.path), full: true }) }); }); const archive = (0, archiver_1.default)('zip', { zlib: { level: 1 } }); res.setHeader('Content-Disposition', `attachment; filename="NFS-studio_${+new Date()}.zip"`); res.setHeader('Content-Type', 'application/zip'); archive.pipe(res); for (const item of items) { if (item.isFolder) { archive.directory(item.path, item.name); continue; } archive.file(item.path, { name: item.name }); } archive.finalize(); } catch (err) { if (this._debug) { console.log(err); } throw err; } }); this.studioBucket = ({ req, res }) => __awaiter(this, void 0, void 0, function* () { var _g, _h, _j, _k; const allowBuckets = (_g = req.buckets) !== null && _g !== void 0 ? _g : []; const rootFolder = this._rootFolder; const buckets = (this._buckets == null ? fs_1.default.readdirSync(path_1.default.join(path_1.default.resolve(), rootFolder)).filter((name) => { return fs_1.default.statSync(path_1.default.join(rootFolder, name)).isDirectory(); }) : yield this._buckets()).filter((bucket) => allowBuckets.includes(bucket) || allowBuckets[0] === '*'); const lists = []; for (const bucket of buckets) { if (allowBuckets.includes(bucket) || allowBuckets[0] === '*') { const fullPath = path_1.default.join(path_1.default.resolve(), this._rootFolder, bucket); if (!(yield this._utils.fileExists(fullPath))) { yield fs_1.default.promises.mkdir(fullPath, { recursive: true }); if (this._onStudioBucketCreated != null) { const random = () => Array.from({ length: 5 }, (_, i) => i).map(v => Math.random().toString(36).substring(3)).join(''); yield this._onStudioBucketCreated({ bucket: String(bucket), token: String(random()), secret: String(random()) }); } } const loadCredentials = this._onLoadBucketCredentials == null ? [] : yield this._onLoadBucketCredentials(); const credentials = loadCredentials.find(v => v.bucket === bucket); const bytes = Number((_k = (_j = (_h = (yield this._utils.getMetadata(bucket))) === null || _h === void 0 ? void 0 : _h.info) === null || _j === void 0 ? void 0 : _j.size) !== null && _k !== void 0 ? _k : 0); lists.push({ [bucket]: { credentials, storage: { bytes, kb: Number((bytes / 1024).toFixed(2)), mb: Number((bytes / (1024 * 1024)).toFixed(2)), gb: Number((bytes / (1024 * 1024 * 1024)).toFixed(2)) } } }); } } return res.ok({ buckets: lists }); }); this.studioBucketCreate = ({ req, res, body }) => __awaiter(this, void 0, void 0, function* () { const { bucket, token, secret } = body; if ([bucket].some(v => v == null || v === '')) { return res.badRequest('The bucket is required.'); } const buckets = (this._buckets == null ? [] : yield this._buckets()); if (buckets.some(v => v === bucket)) { return res.badRequest('The bucket already exists.'); } const directory = this._utils.normalizeDirectory({ bucket: String(bucket), folder: null }); if (!(yield this._utils.fileExists(directory))) { yield fs_1.default.promises.mkdir(directory, { recursive: true }); } const randomString = (length = 8) => Math.random().toString(36).substr(2, length); if (this._onStudioBucketCreated != null) { yield this._onStudioBucketCreated({ bucket: String(bucket), token: String(token == null || token === '' ? randomString() : token), secret: String(secret == null || secret === '' ? randomString() : secret), }); } return res.ok(); }); this.studioFiles = ({ req, res, params }) => __awaiter(this, void 0, void 0, function* () { const rootFolder = this._rootFolder; const path = String(params['*']).replace(/^\/+/, '').replace(/\.{2}(?!\.)/g, ""); const [bucket] = String(path).split('/'); const allowBuckets = req.buckets || []; if (!(allowBuckets.includes(bucket) || allowBuckets[0] === '*')) { return res.forbidden(); } const targetDir = `${rootFolder}/${path}`; if (!fs_1.default.existsSync(path_1.default.join(path_1.default.resolve(), targetDir))) { return res.notFound(`The directory '${path}' does not exist`); } const files = yield this._utils.fileStructure(targetDir, { includeFiles: true, bucket }); return res.ok({ files: files.sort((a, b) => { if (a.name.includes('@') && !b.name.includes('@')) { return -1; } if (!a.name.includes('@') && b.name.includes('@')) { return 1; } if (a.isFolder !== b.isFolder) { return b.isFolder - a.isFolder; } return +new Date(a.lastModified) - +new Date(b.lastModified); }) }); }); this.studioEdit = ({ req, res, params, body }) => __awaiter(this, void 0, void 0, function* () { const path = String(params['*']).replace(/^\/+/, '').replace(/\.{2}(?!\.)/g, ""); const [bucket, ...rest] = String(path).split('/'); const allowBuckets = req.buckets || []; if (!(allowBuckets.includes(bucket) || allowBuckets[0] === '*')) { return res.forbidden(); } const { rename } = body; if (rename == null || rename === '') { return res.badRequest('Please enter the name you wish to use.'); } let filePath = rest.join('/'); if (filePath != null) { filePath = this._utils.normalizeFolder(String(filePath)); } const oldPath = this._utils.normalizeDirectory({ bucket, folder: filePath }); if (!fs_1.default.existsSync(path_1.default.join(path_1.default.resolve(), oldPath))) return res.notFound(); const newPath = this._utils.normalizeDirectory({ bucket, folder: `${path_1.default.dirname(filePath)}/${rename}${path_1.default.extname(filePath)}` }); fs_1.default.renameSync(path_1.default.join(path_1.default.resolve(), oldPath), path_1.default.join(path_1.default.resolve(), newPath)); return res.ok({ name: rename }); }); this.studioRemove = ({ req, res, body, params }) => __awaiter(this, void 0, void 0, function* () { const data = String(params['*']).replace(/^\/+/, '').replace(/\.{2}(?!\.)/g, ""); const [bucket, ...rest] = String(data).split('/'); const path = rest.join('/'); const allowBuckets = req.buckets || []; if (!(allowBuckets.includes(bucket) || allowBuckets[0] === '*')) { return res.forbidden(); } const filePath = this._utils.normalizeFolder(String(path)); const fullPath = path_1.default.join(path_1.default.resolve(), this._utils.normalizeDirectory({ bucket, folder: filePath })); if (!fs_1.default.existsSync(fullPath)) return res.notFound(); const stats = fs_1.default.statSync(fullPath); if (stats.isDirectory()) { if (path.includes(this._trash)) { fs_1.default.rmSync(fullPath, { recursive: true, force: true }); return res.ok(); } this._queue.add(() => __awaiter(this, void 0, void 0, function* () { return yield this._utils.trashedWithFolder({ path, bucket }); })); return res.ok(); } if (path.includes(this._trash)) { this._utils.remove(fullPath, { delayMs: 0 }); yield this._utils.syncMetadata(bucket); return res.ok(); } this._queue.add(() => __awaiter(this, void 0, void 0, function* () { return yield this._utils.trashed({ path, bucket }); })); return res.ok(); }); this.studioGetPathShared = ({ req, res, params }) => __awaiter(this, void 0, void 0, function* () { const path = String(params['*']).replace(/^\/+/, '').replace(/\.{2}(?!\.)/g, ""); const [bucket, ...rest] = String(path).split('/'); const allowBuckets = req.buckets || []; if (!(allowBuckets.includes(bucket) || allowBuckets[0] === '*')) { return res.forbidden(); } let filePath = rest.join('/'); if (filePath != null) { filePath = this._utils.normalizeFolder(String(filePath)); } const directory = this._utils.normalizeDirectory({ bucket, folder: null }); if (!fs_1.default.existsSync(path_1.default.join(path_1.default.resolve(), directory, filePath))) { return res.notFound(`The directory '${path}' does not exist`); } const token = jsonwebtoken_1.default.sign({ data: { issuer: 'nfs-studio', sub: { name: filePath } } }, this._jwtSecret, { expiresIn: 60 * 60 * 24 * 30, algorithm: 'HS256' }); return res.ok({ path: `studio/shared/${path}?key=${token}` }); }); this.studioShared = ({ req, res, params, query }) => __awaiter(this, void 0, void 0, function* () { var _l; const path = String(params['*']).replace(/^\/+/, '').replace(/\.{2}(?!\.)/g, ""); const [bucket, ...rest] = String(path).split('/'); const token = query.key; if (token == null || token === '') { res.writeHead(400, { 'Content-Type': 'text/xml' }); const error = { Error: [ { Code: 'BadRequest' }, { Message: 'The key is required?' }, { Resource: req.url }, ] }; return res.end((0, xml_1.default)([error], { declaration: true })); } let filePath = rest.join('/'); if (filePath != null) { filePath = this._utils.normalizeFolder(String(filePath)); } const { success, data } = this._verifyShared(token); if (!success || data.name !== filePath) { res.writeHead(400, { 'Content-Type': 'text/xml' }); const error = { Error: [ { Code: 'BadRequest' }, { Message: !success ? data : 'The request was denied by studio, Please check key is valid?' }, { Resource: req.url }, ] }; return res.end((0, xml_1.default)([error], { declaration: true })); } const directory = this._utils.normalizeDirectory({ bucket, folder: null }); if (!fs_1.default.existsSync(path_1.default.join(path_1.default.resolve(), directory, filePath))) { return res.notFound(`The directory '${path}' does not exist`); } const { stream, set } = yield this._utils.makeStream({ bucket: bucket, filePath: String(filePath), range: (_l = req.headers) === null || _l === void 0 ? void 0 : _l.range, download: true }); set(res); return stream.pipe(res); }); this._htmlFormatted = (html) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; const setup = this._onSetup == null ? {} : this._onSetup(); return String(html) .replace('{{name}}', (_a = setup === null || setup === void 0 ? void 0 : setup.name) !== null && _a !== void 0 ? _a : 'NFS-Studio') .replace('{{title}}', (_b = setup === null || setup === void 0 ? void 0 : setup.title) !== null && _b !== void 0 ? _b : 'NFS-Studio') .replace('{{subtitle}}', (_c = setup === null || setup === void 0 ? void 0 : setup.subtitle) !== null && _c !== void 0 ? _c : '') .replace('{{description}}', (_d = setup === null || setup === void 0 ? void 0 : setup.description) !== null && _d !== void 0 ? _d : '') .replace('{{logo.login}}', (_f = (_e = setup === null || setup === void 0 ? void 0 : setup.logo) === null || _e === void 0 ? void 0 : _e.login) !== null && _f !== void 0 ? _f : '') .replace('{{logo.fav}}', (_h = (_g = setup === null || setup === void 0 ? void 0 : setup.logo) === null || _g === void 0 ? void 0 : _g.fav) !== null && _h !== void 0 ? _h : '') .replace('{{logo.index}}', (_k = (_j = setup === null || setup === void 0 ? void 0 : setup.logo) === null || _j === void 0 ? void 0 : _j.index) !== null && _k !== void 0 ? _k : ` <svg class="w-12 h-12 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"> <path stroke="currentColor" stroke-linejoin="round" stroke-width="2" d="M15 4v3a1 1 0 0 1-1 1h-3m2 10v1a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-7.13a1 1 0 0 1 .24-.65L6.7 8.35A1 1 0 0 1 7.46 8H9m-1 4H4m16-7v10a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V7.87a1 1 0 0 1 .24-.65l2.46-2.87a1 1 0 0 1 .76-.35H19a1 1 0 0 1 1 1Z"/> </svg> `); }; } /** * The 'useStudio' is method used to wrapper to check the credentials for studio. * @param {object} studio * @property {function} studio.onCredentials * @property {function} studio.onBucketCreated * @returns {this} */ useStudio({ onCredentials, onBucketCreated, onLoadBucketCredentials, onSetup }) { this._onStudioCredentials = onCredentials; this._onStudioBucketCreated = onBucketCreated; this._onLoadBucketCredentials = onLoadBucketCredentials; this._onSetup = onSetup; // creating file meta for any buckets this._utils.syncMetadata('*') .catch(err => console.log(err)); return this; } /** * The 'onMonitors' is method used to monitors the server. * * @param {function} callback * @property {string} callback.host * @property {object} callback.memory * @property {object} callback.cpu * @returns {this} */ onMonitors(callback) { this._monitors = callback; return this; } _verifyShared(token) { try { const decoded = jsonwebtoken_1.default.verify(token, this._jwtSecret); return { success: true, data: decoded.data.sub }; } catch (err) { let message = err.message; if (err.name === 'JsonWebTokenError') { message = 'Invalid credentials'; } if (err.name === 'TokenExpiredError') { message = 'Token has expired'; } return { success: false, data: message }; } } } exports.NfsStudio = NfsStudio; exports.default = NfsStudio; //# sourceMappingURL=server.studio.js.map