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
JavaScript
"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