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
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 __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