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
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.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}`).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