mirror of
https://github.com/HeyPuter/puter.git
synced 2026-04-29 19:50:10 -05:00
2019 lines
67 KiB
JavaScript
2019 lines
67 KiB
JavaScript
/*
|
|
* Copyright (C) 2024 Puter Technologies Inc.
|
|
*
|
|
* This file is part of Puter.
|
|
*
|
|
* Puter is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const _path = require('path');
|
|
const micromatch = require('micromatch');
|
|
const config = require('./config')
|
|
const mime = require('mime-types');
|
|
const PerformanceMonitor = require('./monitor/PerformanceMonitor.js');
|
|
const { generate_identifier } = require('./util/identifier.js');
|
|
const { ManagedError } = require('./util/errorutil.js');
|
|
const { spanify } = require('./util/otelutil.js');
|
|
const APIError = require('./api/APIError.js');
|
|
const { DB_READ, DB_WRITE } = require('./services/database/consts.js');
|
|
const { BaseDatabaseAccessService } = require('./services/database/BaseDatabaseAccessService.js');
|
|
const { LLRmNode } = require('./filesystem/ll_operations/ll_rmnode');
|
|
const { Context } = require('./util/context');
|
|
const { NodeUIDSelector } = require('./filesystem/node/selectors');
|
|
|
|
let systemfs = null;
|
|
let services = null;
|
|
const tmp_provide_services = async ss => {
|
|
services = ss;
|
|
await services.ready;
|
|
systemfs = services.get('filesystem').get_systemfs();
|
|
}
|
|
|
|
async function is_empty(dir_uuid){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'filesystem');
|
|
|
|
// first check if this entry is shared
|
|
let rows = await db.read(
|
|
`SELECT EXISTS(SELECT 1 FROM fsentries WHERE parent_uid = ? LIMIT 1) AS not_empty`,
|
|
[dir_uuid]
|
|
);
|
|
|
|
return !rows[0].not_empty;
|
|
}
|
|
|
|
/**
|
|
* @deprecated - sharing will be implemented with user-to-user ACL
|
|
*/
|
|
async function has_shared_with(user_id, recipient_user_id){
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks to see if this file/directory is shared with the user identified by `recipient_user_id`
|
|
*
|
|
* @param {*} fsentry_id
|
|
* @param {*} recipient_user_id
|
|
*
|
|
* @deprecated - sharing will be implemented with user-to-user ACL
|
|
*/
|
|
async function is_shared_with(fsentry_id, recipient_user_id){
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks to see if this file/directory is shared with at least one other user
|
|
*
|
|
* @param {*} fsentry_id
|
|
* @param {*} recipient_user_id
|
|
*
|
|
* @deprecated - sharing will be implemented with user-to-user ACL
|
|
*/
|
|
async function is_shared_with_anyone(fsentry_id){
|
|
return false;
|
|
}
|
|
|
|
const chkperm = spanify('chkperm', async (target_fsentry, requester_user_id, action) => {
|
|
// basic cases where false is the default response
|
|
if(!target_fsentry)
|
|
return false;
|
|
|
|
// pseudo-entry from FSNodeContext
|
|
if ( target_fsentry.is_root ) {
|
|
return action === 'read';
|
|
}
|
|
|
|
// requester is the owner of this entry
|
|
if(target_fsentry.user_id === requester_user_id){
|
|
return true;
|
|
}
|
|
// this entry was shared with the requester
|
|
else if(await is_shared_with(target_fsentry.id, requester_user_id)){
|
|
return true;
|
|
}
|
|
// special case: owner of entry has shared at least one entry with requester and requester is asking for the owner's root directory: /[owner_username]
|
|
else if(target_fsentry.parent_uid === null && await has_shared_with(target_fsentry.user_id, requester_user_id) && action !== 'write')
|
|
return true;
|
|
else
|
|
return false;
|
|
});
|
|
|
|
/**
|
|
* Checks if the string provided is a valid FileSystem Entry name.
|
|
*
|
|
* @param {string} name
|
|
* @returns
|
|
*/
|
|
function validate_fsentry_name(name){
|
|
if(!name)
|
|
throw {message: 'Name can not be empty.'}
|
|
else if(!isString(name))
|
|
throw {message: "Name can only be a string."}
|
|
else if(name.includes('/'))
|
|
throw {message: "Name can not contain the '/' character."}
|
|
else if(name === '.')
|
|
throw {message: "Name can not be the '.' character."};
|
|
else if(name === '..')
|
|
throw {message: "Name can not be the '..' character."};
|
|
else if(name.length > config.max_fsentry_name_length)
|
|
throw {message: `Name can not be longer than ${config.max_fsentry_name_length} characters`}
|
|
else
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Convert a FSEntry ID to UUID
|
|
*
|
|
* @param {integer} id - `id` of FSEntry
|
|
* @returns {Promise} Promise object represents the UUID of the FileSystem Entry
|
|
*/
|
|
async function id2uuid(id){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'filesystem');
|
|
|
|
let fsentry = await db.requireRead("SELECT `uuid`, immutable FROM `fsentries` WHERE `id` = ? LIMIT 1", [id]);
|
|
|
|
if(!fsentry[0])
|
|
return null;
|
|
else
|
|
return fsentry[0].uuid;
|
|
}
|
|
|
|
/**
|
|
* Get total data stored by a user
|
|
*
|
|
* @param {integer} user_id - `user_id` of user
|
|
* @returns {Promise} Promise object represents the UUID of the FileSystem Entry
|
|
*/
|
|
async function df(user_id){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'filesystem');
|
|
|
|
const fsentry = await db.read("SELECT SUM(size) AS total FROM `fsentries` WHERE `user_id` = ? LIMIT 1", [user_id]);
|
|
if(!fsentry[0] || !fsentry[0].total)
|
|
return 0;
|
|
else
|
|
return fsentry[0].total;
|
|
}
|
|
|
|
/**
|
|
* Get user by a variety of IDs
|
|
*
|
|
* Pass `cached: false` to options if a cached user entry would not be appropriate;
|
|
* for example: when performing authentication.
|
|
*
|
|
* @param {string} options - `options`
|
|
* @returns {Promise}
|
|
*/
|
|
async function get_user(options){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'filesystem');
|
|
|
|
let user;
|
|
|
|
const cached = options.cached ?? true;
|
|
|
|
if ( cached ) {
|
|
if (options.username) user = kv.get('users:username:' + options.username);
|
|
else if (options.email) user = kv.get('users:email:' + options.email);
|
|
else if (options.uuid) user = kv.get('users:uuid:' + options.uuid);
|
|
else if (options.id) user = kv.get('users:id:' + options.id);
|
|
else if (options.referral_code) user = kv.get('users:referral_code:' + options.referral_code);
|
|
|
|
if ( user ) return user;
|
|
}
|
|
|
|
if(options.username)
|
|
user = await db.read("SELECT * FROM `user` WHERE `username` = ? LIMIT 1", [options.username]);
|
|
else if(options.email)
|
|
user = await db.read("SELECT * FROM `user` WHERE `email` = ? LIMIT 1", [options.email]);
|
|
else if(options.uuid)
|
|
user = await db.read("SELECT * FROM `user` WHERE `uuid` = ? LIMIT 1", [options.uuid]);
|
|
else if(options.id)
|
|
user = await db.read("SELECT * FROM `user` WHERE `id` = ? LIMIT 1", [options.id]);
|
|
else if(options.referral_code)
|
|
user = await db.read("SELECT * FROM `user` WHERE `referral_code` = ? LIMIT 1", [options.referral_code]);
|
|
|
|
if(!user || !user[0]){
|
|
if(options.username)
|
|
user = await db.pread("SELECT * FROM `user` WHERE `username` = ? LIMIT 1", [options.username])
|
|
else if(options.email)
|
|
user = await db.pread("SELECT * FROM `user` WHERE `email` = ? LIMIT 1", [options.email]);
|
|
else if(options.uuid)
|
|
user = await db.pread("SELECT * FROM `user` WHERE `uuid` = ? LIMIT 1", [options.uuid]);
|
|
else if(options.id)
|
|
user = await db.pread("SELECT * FROM `user` WHERE `id` = ? LIMIT 1", [options.id]);
|
|
else if(options.referral_code)
|
|
user = await db.pread("SELECT * FROM `user` WHERE `referral_code` = ? LIMIT 1", [options.referral_code]);
|
|
}
|
|
|
|
user = user ? user[0] : null;
|
|
|
|
if ( ! user ) return user;
|
|
|
|
try {
|
|
kv.set('users:username:' + user.username, user);
|
|
kv.set('users:email:' + user.email, user);
|
|
kv.set('users:uuid:' + user.uuid, user);
|
|
kv.set('users:id:' + user.id, user);
|
|
kv.set('users:referral_code:' + user.referral_code, user);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
/**
|
|
* Invalidate the cached entries for a user object
|
|
*
|
|
* @param {User} userID - the user entry to invalidate
|
|
*/
|
|
function invalidate_cached_user (user) {
|
|
kv.del('users:username:' + user.username);
|
|
kv.del('users:uuid:' + user.uuid);
|
|
kv.del('users:email:' + user.email);
|
|
kv.del('users:id:' + user.id);
|
|
}
|
|
|
|
/**
|
|
* Invalidate the cached entries for the user specified by an id
|
|
* @param {number} id - the id of the user to invalidate
|
|
*/
|
|
function invalidate_cached_user_by_id (id) {
|
|
const user = kv.get('users:id:' + id);
|
|
if ( ! user ) return;
|
|
invalidate_cached_user(user);
|
|
}
|
|
|
|
/**
|
|
* Refresh apps cache
|
|
*
|
|
* @param {string} options - `options`
|
|
* @returns {Promise}
|
|
*/
|
|
async function refresh_apps_cache(options, override){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'apps');
|
|
|
|
const log = services.get('log-service').create('refresh_apps_cache');
|
|
log.tick('refresh apps cache');
|
|
// if options is not provided, refresh all apps
|
|
if(!options){
|
|
let apps = await db.read('SELECT * FROM apps');
|
|
for (let index = 0; index < apps.length; index++) {
|
|
const app = apps[index];
|
|
kv.set('apps:name:' + app.name, app);
|
|
kv.set('apps:id:' + app.id, app);
|
|
kv.set('apps:uid:' + app.uid, app);
|
|
}
|
|
}
|
|
// refresh only apps that are approved for listing
|
|
else if(options.only_approved_for_listing){
|
|
let apps = await db.read('SELECT * FROM apps WHERE approved_for_listing = 1');
|
|
for (let index = 0; index < apps.length; index++) {
|
|
const app = apps[index];
|
|
kv.set('apps:name:' + app.name, app);
|
|
kv.set('apps:id:' + app.id, app);
|
|
kv.set('apps:uid:' + app.uid, app);
|
|
}
|
|
}
|
|
// if options is provided, refresh only the app specified
|
|
else{
|
|
let app;
|
|
|
|
if(options.name)
|
|
app = await db.read('SELECT * FROM apps WHERE name = ?', [options.name]);
|
|
else if(options.uid)
|
|
app = await db.read('SELECT * FROM apps WHERE uid = ?', [options.uid]);
|
|
else if(options.id)
|
|
app = await db.read('SELECT * FROM apps WHERE id = ?', [options.id]);
|
|
else {
|
|
log.error('invalid options to refresh_apps_cache');
|
|
throw new Error('Invalid options provided');
|
|
}
|
|
|
|
if(!app || !app[0]) {
|
|
log.error('refresh_apps_cache could not find the app');
|
|
return;
|
|
} else {
|
|
app = app[0];
|
|
if ( override ) {
|
|
Object.assign(app, override);
|
|
}
|
|
kv.set('apps:name:' + app.name, app);
|
|
kv.set('apps:id:' + app.id, app);
|
|
kv.set('apps:uid:' + app.uid, app);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function refresh_associations_cache(){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'apps');
|
|
|
|
const log = services.get('log-service').create('refresh_apps_cache');
|
|
log.tick('refresh associations cache');
|
|
const associations = await db.read('SELECT * FROM app_filetype_association');
|
|
const lists = {};
|
|
for ( const association of associations ) {
|
|
let ext = association.type;
|
|
if ( ext.startsWith('.') ) ext = ext.slice(1);
|
|
if ( ! lists.hasOwnProperty(ext) ) lists[ext] = [];
|
|
lists[ext].push(association.app_id);
|
|
}
|
|
|
|
for ( const k in lists ) {
|
|
kv.set(`assocs:${k}:apps`, lists[k]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get App by a variety of IDs
|
|
*
|
|
* @param {string} options - `options`
|
|
* @returns {Promise}
|
|
*/
|
|
async function get_app(options){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'apps');
|
|
|
|
const log = services.get('log-service').create('get_app');
|
|
let app = [];
|
|
if(options.uid){
|
|
// try cache first
|
|
app[0] = kv.get(`apps:uid:${options.uid}`);
|
|
// not in cache, try db
|
|
if(!app[0]) {
|
|
log.cache(false, 'apps:uid:' + options.uid);
|
|
app = await db.read("SELECT * FROM `apps` WHERE `uid` = ? LIMIT 1", [options.uid]);
|
|
}
|
|
}else if(options.name){
|
|
// try cache first
|
|
app[0] = kv.get(`apps:name:${options.name}`);
|
|
// not in cache, try db
|
|
if(!app[0]) {
|
|
log.cache(false, 'apps:name:' + options.name);
|
|
app = await db.read("SELECT * FROM `apps` WHERE `name` = ? LIMIT 1", [options.name]);
|
|
}
|
|
}
|
|
else if(options.id){
|
|
// try cache first
|
|
app[0] = kv.get(`apps:id:${options.id}`);
|
|
// not in cache, try db
|
|
if(!app[0]) {
|
|
log.cache(false, 'apps:id:' + options.id);
|
|
app = await db.read("SELECT * FROM `apps` WHERE `id` = ? LIMIT 1", [options.id]);
|
|
}
|
|
}
|
|
app = app && app[0] ? app[0] : null;
|
|
|
|
if ( app === null ) return null;
|
|
|
|
// shallow clone because we use the `delete` operator
|
|
// and it corrupts the cache otherwise
|
|
app = { ...app };
|
|
return app;
|
|
}
|
|
|
|
/**
|
|
* Checks to see if an app exists
|
|
*
|
|
* @param {string} options - `options`
|
|
* @returns {Promise}
|
|
*/
|
|
async function app_exists(options){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'apps');
|
|
|
|
let app;
|
|
if(options.uid)
|
|
app = await db.read("SELECT `id` FROM `apps` WHERE `uid` = ? LIMIT 1", [options.uid]);
|
|
else if(options.name)
|
|
app = await db.read("SELECT `id` FROM `apps` WHERE `name` = ? LIMIT 1", [options.name]);
|
|
else if(options.id)
|
|
app = await db.read("SELECT `id` FROM `apps` WHERE `id` = ? LIMIT 1", [options.id]);
|
|
|
|
return app[0];
|
|
}
|
|
|
|
|
|
/**
|
|
* change username
|
|
*
|
|
* @param {string} options - `options`
|
|
* @returns {Promise}
|
|
*/
|
|
async function change_username(user_id, new_username){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_WRITE, 'auth');
|
|
|
|
const old_username = (await get_user({id: user_id})).username;
|
|
|
|
// update username
|
|
await db.write("UPDATE `user` SET username = ? WHERE `id` = ? LIMIT 1", [new_username, user_id]);
|
|
// update root directory name for this user
|
|
await db.write("UPDATE `fsentries` SET `name` = ? WHERE `user_id` = ? AND parent_uid IS NULL LIMIT 1", [new_username, user_id]);
|
|
|
|
const log = services.get('log-service').create('change_username');
|
|
log.noticeme(`User ${old_username} changed username to ${new_username}`);
|
|
await services.get('filesystem').update_child_paths(`/${old_username}`, `/${new_username}`, user_id);
|
|
|
|
invalidate_cached_user_by_id(user_id);
|
|
}
|
|
|
|
|
|
/**
|
|
* Find a FSEntry by its uuid
|
|
*
|
|
* @param {integer} id - `id` of FSEntry
|
|
* @returns {Promise} Promise object represents the UUID of the FileSystem Entry
|
|
* @deprecated Use fs middleware instead
|
|
*/
|
|
async function uuid2fsentry(uuid, return_thumbnail){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'filesystem');
|
|
|
|
// todo optim, check if uuid is not exactly 36 characters long, if not it's invalid
|
|
// and we can avoid one unnecessary DB lookup
|
|
let fsentry = await db.requireRead(
|
|
`SELECT
|
|
id,
|
|
associated_app_id,
|
|
uuid,
|
|
public_token,
|
|
bucket,
|
|
bucket_region,
|
|
file_request_token,
|
|
user_id,
|
|
parent_uid,
|
|
is_dir,
|
|
is_public,
|
|
is_shortcut,
|
|
shortcut_to,
|
|
sort_by,
|
|
${return_thumbnail ? 'thumbnail,' : ''}
|
|
immutable,
|
|
name,
|
|
metadata,
|
|
modified,
|
|
created,
|
|
accessed,
|
|
size
|
|
FROM fsentries WHERE uuid = ? LIMIT 1`,
|
|
[uuid]
|
|
);
|
|
|
|
if(!fsentry[0])
|
|
return false;
|
|
else
|
|
return fsentry[0];
|
|
}
|
|
|
|
/**
|
|
* Find a FSEntry by its id
|
|
*
|
|
* @param {integer} id - `id` of FSEntry
|
|
* @returns {Promise} Promise object represents the UUID of the FileSystem Entry
|
|
*/
|
|
async function id2fsentry(id, return_thumbnail){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'filesystem');
|
|
|
|
// todo optim, check if uuid is not exactly 36 characters long, if not it's invalid
|
|
// and we can avoid one unnecessary DB lookup
|
|
let fsentry = await db.requireRead(
|
|
`SELECT
|
|
id,
|
|
uuid,
|
|
public_token,
|
|
file_request_token,
|
|
associated_app_id,
|
|
user_id,
|
|
parent_uid,
|
|
is_dir,
|
|
is_public,
|
|
is_shortcut,
|
|
shortcut_to,
|
|
sort_by,
|
|
${return_thumbnail ? 'thumbnail,' : ''}
|
|
immutable,
|
|
name,
|
|
metadata,
|
|
modified,
|
|
created,
|
|
accessed,
|
|
size
|
|
FROM fsentries WHERE id = ? LIMIT 1`,
|
|
[id]
|
|
);
|
|
|
|
if(!fsentry[0]){
|
|
return false;
|
|
}else
|
|
return fsentry[0];
|
|
}
|
|
|
|
/**
|
|
* Takes a an absolute path and returns its corresponding FSEntry.
|
|
*
|
|
* @param {string} path - absolute path of the filesystem entry to be resolved
|
|
* @param {boolean} return_content - if FSEntry is a file, determines whether its content should be returned
|
|
* @returns {false|object} - `false` if path could not be resolved, otherwise an object representing the FSEntry
|
|
* @deprecated Use fs middleware instead
|
|
*/
|
|
async function convert_path_to_fsentry(path){
|
|
// todo optim, check if path is valid (e.g. contaisn valid characters)
|
|
// if syntactical errors are found we can potentially avoid some expensive db lookups
|
|
|
|
// '/' means that parent_uid is null
|
|
// TODO: facade fsentry for root (devlog:2023-06-01)
|
|
if(path === '/')
|
|
return null;
|
|
//first slash is redundant
|
|
path = path.substr(path.indexOf('/') + 1)
|
|
//last slash, if existing is redundant
|
|
if(path[path.length - 1] === '/')
|
|
path = path.slice(0, -1);
|
|
//split path into parts
|
|
const fsentry_names = path.split('/');
|
|
|
|
// if no parts, return false
|
|
if(fsentry_names.length === 0)
|
|
return false;
|
|
|
|
let parent_uid = null;
|
|
let final_res = null;
|
|
let is_public = false
|
|
let result
|
|
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'filesystem');
|
|
|
|
// Try stored path first
|
|
result = await db.read(
|
|
`SELECT * FROM fsentries WHERE path=? LIMIT 1`,
|
|
['/' + path],
|
|
);
|
|
|
|
if ( result[0] ) {
|
|
return result[0];
|
|
}
|
|
|
|
for(let i=0; i < fsentry_names.length; i++){
|
|
if(parent_uid === null){
|
|
result = await db.read(
|
|
`SELECT * FROM fsentries WHERE parent_uid IS NULL AND name=? LIMIT 1`,
|
|
[fsentry_names[i]]
|
|
);
|
|
}
|
|
else{
|
|
result = await db.read(
|
|
`SELECT * FROM fsentries WHERE parent_uid = ? AND name=? LIMIT 1`,
|
|
[parent_uid, fsentry_names[i]]
|
|
);
|
|
}
|
|
|
|
if(result[0] ){
|
|
parent_uid = result[0].uuid;
|
|
// is_public is either directly specified or inherited from parent dir
|
|
if(result[0].is_public === null)
|
|
result[0].is_public = is_public
|
|
else
|
|
is_public = result[0].is_public
|
|
|
|
}else{
|
|
return false;
|
|
}
|
|
final_res = result
|
|
}
|
|
return final_res[0];
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {integer} bytes - size in bytes
|
|
* @returns {string} bytes in human-readable format
|
|
*/
|
|
function byte_format(bytes){
|
|
// calculate and return bytes in human-readable format
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
if (typeof bytes !== "number" || bytes < 1) {
|
|
return '0 B';
|
|
}
|
|
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
|
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
|
|
};
|
|
|
|
const get_dir_size = async (path, user)=>{
|
|
let size = 0;
|
|
const descendants = await get_descendants(path, user);
|
|
for(let i=0; i < descendants.length; i++){
|
|
if(!descendants[i].is_dir){
|
|
size += descendants[i].size;
|
|
}
|
|
}
|
|
|
|
return size;
|
|
}
|
|
|
|
/**
|
|
* Recursively retrieve all files, directories, and subdirectories under `path`.
|
|
* Optionally the `depth` can be set.
|
|
*
|
|
* @param {string} path
|
|
* @param {object} user
|
|
* @param {integer} depth
|
|
* @returns
|
|
*/
|
|
const get_descendants_0 = async (path, user, depth, return_thumbnail = false) => {
|
|
const log = services.get('log-service').create('get_descendants');
|
|
log.called();
|
|
|
|
// decrement depth if it's set
|
|
depth !== undefined && depth--;
|
|
// turn path into absolute form
|
|
path = _path.resolve('/', path)
|
|
// get parent dir
|
|
const parent = await convert_path_to_fsentry(path);
|
|
// holds array that will be returned
|
|
const ret = [];
|
|
// holds immediate children of this path
|
|
let children;
|
|
|
|
// try to extract username from path
|
|
let username;
|
|
let split_path = path.split('/');
|
|
if(split_path.length === 2 && split_path[0] === '')
|
|
username = split_path[1];
|
|
|
|
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'filesystem');
|
|
|
|
// -------------------------------------
|
|
// parent is root ('/')
|
|
// -------------------------------------
|
|
if(parent === null){
|
|
path = '';
|
|
// direct children under root
|
|
children = await db.read(
|
|
`SELECT
|
|
id, uuid, parent_uid, name, metadata, is_dir, bucket, bucket_region,
|
|
modified, created, immutable, shortcut_to, is_shortcut, sort_by, associated_app_id,
|
|
${return_thumbnail ? 'thumbnail, ' : ''}
|
|
accessed, size
|
|
FROM fsentries
|
|
WHERE user_id = ? AND parent_uid IS NULL`,
|
|
[user.id]
|
|
);
|
|
// users that have shared files/dirs with this user
|
|
sharing_users = await db.read(
|
|
`SELECT DISTINCT(owner_user_id), user.username
|
|
FROM share
|
|
INNER JOIN user ON user.id = share.owner_user_id
|
|
WHERE share.recipient_user_id = ?`,
|
|
[user.id]
|
|
);
|
|
if(sharing_users.length>0){
|
|
for(let i=0; i<sharing_users.length; i++){
|
|
let dir = {};
|
|
dir.id = null;
|
|
dir.uuid = null;
|
|
dir.parent_uid = null;
|
|
dir.name = sharing_users[i].username;
|
|
dir.is_dir = true;
|
|
dir.immutable = true;
|
|
children.push(dir)
|
|
}
|
|
}
|
|
}
|
|
// -------------------------------------
|
|
// parent doesn't exist
|
|
// -------------------------------------
|
|
else if(parent === false){
|
|
return [];
|
|
}
|
|
// -------------------------------------
|
|
// Parent is a shared-user directory: /[some_username](/)
|
|
// but make sure `[some_username]` is not the same as the requester's username
|
|
// -------------------------------------
|
|
else if(username && username !== user.username){
|
|
children = [];
|
|
let sharing_user;
|
|
sharing_user = await get_user({username: username});
|
|
if(!sharing_user)
|
|
return [];
|
|
|
|
// shared files/dirs with this user
|
|
shared_fsentries = await db.read(
|
|
`SELECT
|
|
fsentries.id, fsentries.user_id, fsentries.uuid, fsentries.parent_uid, fsentries.bucket, fsentries.bucket_region,
|
|
fsentries.name, fsentries.shortcut_to, fsentries.is_shortcut, fsentries.metadata, fsentries.is_dir, fsentries.modified,
|
|
fsentries.created, fsentries.accessed, fsentries.size, fsentries.sort_by, fsentries.associated_app_id,
|
|
fsentries.is_symlink, fsentries.symlink_path,
|
|
fsentries.immutable ${return_thumbnail ? ', fsentries.thumbnail' : ''}
|
|
FROM share
|
|
INNER JOIN fsentries ON fsentries.id = share.fsentry_id
|
|
WHERE share.recipient_user_id = ? AND owner_user_id = ?`,
|
|
[user.id, sharing_user.id]
|
|
);
|
|
// merge `children` and `shared_fsentries`
|
|
if(shared_fsentries.length>0){
|
|
for(let i=0; i<shared_fsentries.length; i++){
|
|
shared_fsentries[i].path = await id2path(shared_fsentries[i].id);
|
|
children.push(shared_fsentries[i])
|
|
}
|
|
}
|
|
}
|
|
// -------------------------------------
|
|
// All other cases
|
|
// -------------------------------------
|
|
else{
|
|
children = [];
|
|
let temp_children = await db.read(
|
|
`SELECT
|
|
id, user_id, uuid, parent_uid, name, metadata, is_shortcut,
|
|
shortcut_to, is_dir, modified, created, accessed, size, sort_by, associated_app_id,
|
|
is_symlink, symlink_path,
|
|
immutable ${return_thumbnail ? ', thumbnail' : ''}
|
|
FROM fsentries
|
|
WHERE parent_uid = ?`,
|
|
[parent.uuid]
|
|
);
|
|
// check if user has access to each file, if yes add it
|
|
if(temp_children.length>0){
|
|
for(let i=0; i<temp_children.length; i++){
|
|
const tchild = temp_children[i];
|
|
if(await chkperm(tchild, user.id))
|
|
children.push(tchild);
|
|
}
|
|
}
|
|
}
|
|
|
|
// shortcut on empty result set
|
|
if ( children.length === 0 ) return [];
|
|
|
|
const ids = children.map(child => child.id);
|
|
const qmarks = ids.map(() => '?').join(',');
|
|
|
|
let rows = await db.read(
|
|
`SELECT root_dir_id FROM subdomains WHERE root_dir_id IN (${qmarks}) AND user_id=?`,
|
|
[...ids, user.id]);
|
|
|
|
log.debug('rows???', rows);
|
|
|
|
const websiteMap = {};
|
|
for ( const row of rows ) websiteMap[row.root_dir_id] = true;
|
|
|
|
for(let i=0; i<children.length; i++){
|
|
const contentType = mime.contentType(children[i].name)
|
|
|
|
// has_website
|
|
let has_website = false;
|
|
if(children[i].is_dir){
|
|
has_website = websiteMap[children[i].id];
|
|
}
|
|
|
|
// object to return
|
|
// TODO: DRY creation of response fsentry from db fsentry
|
|
ret.push({
|
|
path: children[i].path ?? (path + '/' + children[i].name),
|
|
name: children[i].name,
|
|
metadata: children[i].metadata,
|
|
_id: children[i].id,
|
|
id: children[i].uuid,
|
|
uid: children[i].uuid,
|
|
is_shortcut: children[i].is_shortcut,
|
|
shortcut_to: (children[i].shortcut_to ? await id2uuid(children[i].shortcut_to) : undefined),
|
|
shortcut_to_path: (children[i].shortcut_to ? await id2path(children[i].shortcut_to) : undefined),
|
|
is_symlink: children[i].is_symlink,
|
|
symlink_path: children[i].symlink_path,
|
|
immutable: children[i].immutable,
|
|
is_dir: children[i].is_dir,
|
|
modified: children[i].modified,
|
|
created: children[i].created,
|
|
accessed: children[i].accessed,
|
|
size: children[i].size,
|
|
sort_by: children[i].sort_by,
|
|
thumbnail: children[i].thumbnail,
|
|
associated_app_id: children[i].associated_app_id,
|
|
type: contentType ? contentType : null,
|
|
has_website: has_website,
|
|
})
|
|
if( children[i].is_dir &&
|
|
(depth === undefined || (depth !== undefined && depth > 0))
|
|
){
|
|
ret.push(await get_descendants(path + '/' + children[i].name, user, depth))
|
|
}
|
|
}
|
|
return ret.flat();
|
|
}
|
|
|
|
const get_descendants = async (...args) => {
|
|
const tracer = services.get('traceService').tracer;
|
|
let ret;
|
|
await tracer.startActiveSpan('get_descendants', async span => {
|
|
ret = await get_descendants_0(...args);
|
|
span.end();
|
|
});
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {integer} entry_id
|
|
* @returns
|
|
*/
|
|
const id2path = async (entry_uid)=>{
|
|
if ( entry_uid == null ) {
|
|
throw new Error('got null or undefined entry id');
|
|
}
|
|
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'filesystem');
|
|
|
|
const traces = services.get('traceService');
|
|
const log = services.get('log-service').create('helpers.id2path');
|
|
log.traceOn();
|
|
const errors = services.get('error-service').create(log);
|
|
log.called();
|
|
|
|
let result;
|
|
|
|
return await traces.spanify(`helpers:id2path`, async () => {
|
|
log.debug(`entry id: ${entry_uid}`)
|
|
if ( typeof entry_uid === 'number' ) {
|
|
const old = entry_uid;
|
|
entry_uid = await id2uuid(entry_uid);
|
|
log.debug(`entry id resolved: resolved ${old} ${entry_uid}`)
|
|
}
|
|
|
|
try {
|
|
result = await db.read(`
|
|
WITH RECURSIVE cte AS (
|
|
SELECT uuid, parent_uid, name, name AS path
|
|
FROM fsentries
|
|
WHERE uuid = ?
|
|
|
|
UNION ALL
|
|
|
|
SELECT e.uuid, e.parent_uid, e.name, ${
|
|
db.case({
|
|
sqlite: `e.name || '/' || cte.path`,
|
|
otherwise: `CONCAT(e.name, '/', cte.path)`,
|
|
})
|
|
}
|
|
FROM fsentries e
|
|
INNER JOIN cte ON cte.parent_uid = e.uuid
|
|
)
|
|
SELECT *
|
|
FROM cte
|
|
WHERE parent_uid IS NULL
|
|
`, [entry_uid]);
|
|
} catch (e) {
|
|
errors.report('id2path.select', {
|
|
alarm: true,
|
|
source: e,
|
|
message: `error while resolving path for ${entry_uid}: ${e.message}`,
|
|
extra: {
|
|
entry_uid,
|
|
}
|
|
});
|
|
throw new ManagedError(`cannot create path for ${entry_uid}`);
|
|
}
|
|
|
|
if ( ! result || ! result[0] ) {
|
|
errors.report('id2path.select', {
|
|
alarm: true,
|
|
message: `no result for ${entry_uid}: ${e.message}`,
|
|
extra: {
|
|
entry_uid,
|
|
}
|
|
});
|
|
throw new ManagedError(`cannot create path for ${entry_uid}`);
|
|
}
|
|
|
|
return '/' + result[0].path;
|
|
})
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} glob
|
|
* @param {object} user
|
|
* @returns
|
|
*/
|
|
async function resolve_glob(glob, user){
|
|
//turn glob into abs path
|
|
glob = _path.resolve('/', glob)
|
|
//get base of glob
|
|
const base = micromatch.scan(glob).base
|
|
//estimate needed depth
|
|
let depth = 1
|
|
const dirs = glob.split('/')
|
|
for(let i=0; i< dirs.length; i++){
|
|
if(dirs[i].includes('**')){
|
|
depth = undefined
|
|
break
|
|
}else{
|
|
depth++
|
|
}
|
|
}
|
|
|
|
const descendants = await get_descendants(base, user, depth)
|
|
|
|
return descendants.filter((fsentry) => {
|
|
return fsentry.path && micromatch.isMatch(fsentry.path, glob)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Copies a FSEntry represented by `source_path` to `dest_path`.
|
|
*
|
|
* @param {string} source_path
|
|
* @param {string} dest_path
|
|
* @param {object} user
|
|
* @returns
|
|
*/
|
|
function cp(source_path, dest_path, user, overwrite, change_name, check_perms = true){
|
|
throw new Error(`legacy copy function called`);
|
|
}
|
|
|
|
isString = function (variable) {
|
|
return typeof variable === 'string' || variable instanceof String;
|
|
}
|
|
|
|
// checks to see if given variable is an object
|
|
isObject = function (variable) {
|
|
return variable !== null && typeof variable === 'object';
|
|
}
|
|
|
|
/**
|
|
* Recusrively deletes all files under `path`
|
|
*
|
|
* @param {string} source_path
|
|
* @param {object} user
|
|
* @returns
|
|
*/
|
|
function rm(source_path, user, descendants_only = false){
|
|
throw new Error(`legacy remove function called`);
|
|
}
|
|
|
|
const body_parser_error_handler = (err, req, res, next) => {
|
|
if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
|
|
return res.status(400).send(err); // Bad request
|
|
}
|
|
next();
|
|
}
|
|
|
|
async function is_ancestor_of(ancestor_uid, descendant_uid){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'filesystem');
|
|
|
|
// root is an ancestor to all FSEntries
|
|
if(ancestor_uid === null)
|
|
return true;
|
|
// root is never a descendant to any FSEntries
|
|
if(descendant_uid === null)
|
|
return false;
|
|
|
|
if ( typeof ancestor_uid === 'number' ) {
|
|
ancestor_uid = await id2uuid(ancestor_uid);
|
|
}
|
|
if ( typeof descendant_uid === 'number' ) {
|
|
descendant_uid = await id2uuid(descendant_uid);
|
|
}
|
|
|
|
let parent = await db.read("SELECT `uuid`, `parent_uid` FROM `fsentries` WHERE `uuid` = ? LIMIT 1", [descendant_uid]);
|
|
if(parent[0] === undefined)
|
|
parent = await db.pread("SELECT `uuid`, `parent_uid` FROM `fsentries` WHERE `uuid` = ? LIMIT 1", [descendant_uid]);
|
|
if(parent[0].uuid === ancestor_uid || parent[0].parent_uid === ancestor_uid){
|
|
return true;
|
|
}
|
|
// keep checking as long as parent of parent is not root
|
|
while(parent[0].parent_uid !== null){
|
|
parent = await db.read("SELECT `uuid`, `parent_uid` FROM `fsentries` WHERE `uuid` = ? LIMIT 1", [parent[0].parent_uid]);
|
|
if(parent[0] === undefined) {
|
|
parent = await db.pread("SELECT `uuid`, `parent_uid` FROM `fsentries` WHERE `uuid` = ? LIMIT 1", [descendant_uid]);
|
|
}
|
|
|
|
if(parent[0].uuid === ancestor_uid || parent[0].parent_uid === ancestor_uid){
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async function sign_file(fsentry, action){
|
|
const sha256 = require('js-sha256').sha256;
|
|
|
|
// fsentry not found
|
|
if(fsentry === false){
|
|
throw {message: 'No entry found with this uid'};
|
|
}
|
|
|
|
const uid = fsentry.uuid ?? (fsentry.uid ?? fsentry._id);
|
|
const ttl = 9999999999999;
|
|
const secret = config.url_signature_secret;
|
|
const expires = Math.ceil(Date.now() / 1000) + ttl;
|
|
const signature = sha256(`${uid}/${action}/${secret}/${expires}`);
|
|
const contentType = mime.contentType(fsentry.name);
|
|
|
|
// return
|
|
return {
|
|
uid: uid,
|
|
expires: expires,
|
|
signature: signature,
|
|
url: `${config.api_base_url}/file?uid=${uid}&expires=${expires}&signature=${signature}`,
|
|
read_url: `${config.api_base_url}/file?uid=${uid}&expires=${expires}&signature=${signature}`,
|
|
write_url: `${config.api_base_url}/writeFile?uid=${uid}&expires=${expires}&signature=${signature}`,
|
|
metadata_url: `${config.api_base_url}/itemMetadata?uid=${uid}&expires=${expires}&signature=${signature}`,
|
|
fsentry_type: contentType,
|
|
fsentry_is_dir: !! fsentry.is_dir,
|
|
fsentry_name: fsentry.name,
|
|
fsentry_size: fsentry.size,
|
|
fsentry_accessed: fsentry.accessed,
|
|
fsentry_modified: fsentry.modified,
|
|
fsentry_created: fsentry.created,
|
|
}
|
|
}
|
|
|
|
async function gen_public_token(file_uuid, ttl = 24 * 60 * 60){
|
|
const { v4: uuidv4 } = require('uuid');
|
|
|
|
// get fsentry
|
|
let fsentry = await uuid2fsentry(file_uuid);
|
|
|
|
// fsentry not found
|
|
if(fsentry === false){
|
|
throw {message: 'No entry found with this uid'};
|
|
}
|
|
|
|
const uid = fsentry.uuid;
|
|
const expires = Math.ceil(Date.now() / 1000) + ttl;
|
|
const token = uuidv4();
|
|
const contentType = mime.contentType(fsentry.name);
|
|
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_WRITE, 'filesystem');
|
|
|
|
// insert into DB
|
|
try{
|
|
await db.write(
|
|
`UPDATE fsentries SET public_token = ? WHERE id = ?`,
|
|
[
|
|
//token
|
|
token,
|
|
//fsentry_id
|
|
fsentry.id,
|
|
]);
|
|
}catch(e){
|
|
console.log(e);
|
|
return false;
|
|
}
|
|
|
|
// return
|
|
return {
|
|
uid: uid,
|
|
token: token,
|
|
url: `${config.api_base_url}/pubfile?token=${token}`,
|
|
fsentry_type: contentType,
|
|
fsentry_is_dir: fsentry.is_dir,
|
|
fsentry_name: fsentry.name,
|
|
}
|
|
}
|
|
|
|
async function deleteUser(user_id){
|
|
console.log('THIS IS deleteUser ---');
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'filesystem');
|
|
|
|
// get a list of all files owned by this user
|
|
let files = await db.read(
|
|
`SELECT uuid, bucket, bucket_region FROM fsentries WHERE user_id = ? AND is_dir = 0`,
|
|
[user_id]
|
|
);
|
|
|
|
// delete all files from S3
|
|
if(files !== null && files.length > 0){
|
|
for(let i=0; i<files.length; i++){
|
|
// init S3 SDK
|
|
const svc_fs = Context.get('services').get('filesystem');
|
|
const storage = Context.get('storage');
|
|
const op_delete = storage.create_delete();
|
|
await op_delete.run({
|
|
node: await svc_fs.node(new NodeUIDSelector(files[i].uuid))
|
|
});
|
|
}
|
|
}
|
|
|
|
// delete all fsentries from DB
|
|
await db.write(`DELETE FROM fsentries WHERE user_id = ?`,[user_id]);
|
|
|
|
// delete user
|
|
await db.write(`DELETE FROM user WHERE id = ?`,[user_id]);
|
|
}
|
|
|
|
function subdomain(req){
|
|
if ( config.experimental_no_subdomain ) return 'api';
|
|
return req.hostname.slice(0, -1 * (config.domain.length + 1));
|
|
}
|
|
|
|
async function jwt_auth(req){
|
|
let token;
|
|
// HTTML Auth header
|
|
if(req.header && req.header('Authorization'))
|
|
token = req.header('Authorization');
|
|
// Cookie
|
|
else if(req.cookies && req.cookies[config.cookie_name])
|
|
token = req.cookies[config.cookie_name];
|
|
// Auth token in URL
|
|
else if(req.query && req.query.auth_token)
|
|
token = req.query.auth_token;
|
|
// Socket
|
|
else if(req.handshake && req.handshake.query && req.handshake.query.auth_token)
|
|
token = req.handshake.query.auth_token;
|
|
|
|
if(!token || token === 'null')
|
|
throw('No auth token found');
|
|
else if (typeof token !== 'string')
|
|
throw('token must be a string.')
|
|
else
|
|
token = token.replace('Bearer ', '')
|
|
|
|
try{
|
|
const svc_auth = Context.get('services').get('auth');
|
|
const actor = await svc_auth.authenticate_from_token(token);
|
|
|
|
if ( ! actor.type?.constructor?.name === 'UserActorType' ) {
|
|
throw({
|
|
message: APIError.create('token_unsupported')
|
|
.serialize(),
|
|
});
|
|
}
|
|
|
|
return {
|
|
user: actor.type.user,
|
|
token: token,
|
|
};
|
|
}catch(e){
|
|
console.log('ERROR', e);
|
|
throw(e.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* returns all ancestors of an fsentry
|
|
*
|
|
* @param {*} fsentry_id
|
|
*/
|
|
async function ancestors(fsentry_id){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'filesystem');
|
|
|
|
const ancestors = [];
|
|
// first parent
|
|
let parent = await db.read("SELECT * FROM `fsentries` WHERE `id` = ? LIMIT 1", [fsentry_id]);
|
|
if(parent.length === 0){
|
|
return ancestors;
|
|
}
|
|
// get all subsequent parents
|
|
while(parent[0].parent_uid !== null){
|
|
const parent_fsentry = await uuid2fsentry(parent[0].parent_uid);
|
|
parent = await db.read("SELECT * FROM `fsentries` WHERE `id` = ? LIMIT 1", [parent_fsentry.id]);
|
|
if(parent[0].length !== 0){
|
|
ancestors.push(parent[0])
|
|
}
|
|
}
|
|
|
|
return ancestors;
|
|
}
|
|
|
|
// THIS LEGACY FUNCTION IS STILL IN USE
|
|
// by: generate_system_fsentries
|
|
// TODO: migrate generate_system_fsentries to use QuickMkdir
|
|
async function mkdir(options){
|
|
const fs = systemfs;
|
|
|
|
const dirpath = _path.dirname(_path.resolve('/', options.path));
|
|
let target_name = _path.basename(_path.resolve('/', options.path));
|
|
const overwrite = options.overwrite ?? false;
|
|
const dedupe_name = options.dedupe_name ?? false;
|
|
const immutable = options.immutable ?? false;
|
|
const return_id = options.return_id ?? false;
|
|
const no_perm_check = options.no_perm_check ?? false;
|
|
|
|
// make parent directories as needed
|
|
const create_missing_parents = options.create_missing_parents ?? false;
|
|
|
|
// hold a list of all parent directories created in the process
|
|
let parent_dirs_created = [];
|
|
let overwritten_uid;
|
|
|
|
// target_name validation
|
|
try{
|
|
validate_fsentry_name(target_name)
|
|
}catch(e){
|
|
throw e.message;
|
|
}
|
|
|
|
// resolve dirpath to its fsentry
|
|
let parent = await convert_path_to_fsentry(dirpath);
|
|
|
|
// dirpath not found
|
|
if(parent === false && !create_missing_parents)
|
|
throw "Target path not found";
|
|
// create missing parent directories
|
|
else if(parent === false && create_missing_parents){
|
|
const dirs = _path.resolve('/', dirpath).split('/');
|
|
let cur_path = '';
|
|
for(let j=0; j < dirs.length; j++){
|
|
if(dirs[j] === '')
|
|
continue;
|
|
|
|
cur_path += '/'+dirs[j];
|
|
// skip creating '/[username]'
|
|
if(j === 1)
|
|
continue;
|
|
try{
|
|
let d = await mkdir(fs, {path: cur_path, user: options.user});
|
|
d.path = cur_path;
|
|
parent_dirs_created.push(d);
|
|
}catch(e){
|
|
console.log(`Skipped mkdir ${cur_path}`);
|
|
}
|
|
}
|
|
// try setting parent again
|
|
parent = await convert_path_to_fsentry(dirpath);
|
|
if(parent === false)
|
|
throw "Target path not found";
|
|
}
|
|
|
|
// check permission
|
|
if(!no_perm_check && !await chkperm(parent, options.user.id, 'write'))
|
|
throw { code:`forbidden`, message: `permission denied.`};
|
|
|
|
// check if a fsentry with the same name exists under this path
|
|
const existing_fsentry = await convert_path_to_fsentry(_path.resolve('/', dirpath + '/' + target_name ));
|
|
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_WRITE, 'filesystem');
|
|
|
|
// if trying to create a directory with an existing path and overwrite==false, throw an error
|
|
if(!overwrite && !dedupe_name && existing_fsentry !== false){
|
|
throw {
|
|
code: 'path_exists',
|
|
message:"A file/directory with the same path already exists.",
|
|
entry_name: existing_fsentry.name,
|
|
existing_fsentry: {
|
|
name: existing_fsentry.name,
|
|
uid: existing_fsentry.uuid,
|
|
}
|
|
};
|
|
}
|
|
else if(overwrite && existing_fsentry){
|
|
overwritten_uid = existing_fsentry.uuid;
|
|
// check permission
|
|
if(!await chkperm(existing_fsentry, options.user.id, 'write'))
|
|
throw {code:`forbidden`, message: `permission denied.`};
|
|
// delete existing dir
|
|
await db.write(
|
|
`DELETE FROM fsentries WHERE id = ? AND user_id = ?`,
|
|
[
|
|
//parent_uid
|
|
existing_fsentry.uuid,
|
|
//user_id
|
|
options.user.id,
|
|
]);
|
|
}
|
|
// dedupe name, generate a new name until its unique
|
|
else if(dedupe_name && existing_fsentry !== false){
|
|
for( let i = 1; ; i++){
|
|
let try_new_name = existing_fsentry.name + ' (' + i + ')';
|
|
let check_dupe = await db.read(
|
|
"SELECT `id` FROM `fsentries` WHERE `parent_uid` = ? AND name = ? LIMIT 1",
|
|
[existing_fsentry.parent_uid, try_new_name]
|
|
);
|
|
if(check_dupe[0] === undefined){
|
|
target_name = try_new_name;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// shrotcut?
|
|
let shortcut_fsentry;
|
|
if(options.shortcut_to){
|
|
shortcut_fsentry = await uuid2fsentry(options.shortcut_to);
|
|
if(shortcut_fsentry === false){
|
|
throw ({ code:`not_found`, message: `shortcut_to not found.`})
|
|
}else if(!parent.is_dir){
|
|
throw ({ code:`not_dir`, message: `parent of shortcut_to must be a directory`})
|
|
}else if(!await chkperm(shortcut_fsentry, options.user.id, 'read')){
|
|
throw ({ code:`forbidden`, message: `shortcut_to permission denied.`})
|
|
}
|
|
}
|
|
|
|
// current epoch
|
|
const ts = Math.round(Date.now() / 1000)
|
|
const uid = uuidv4();
|
|
|
|
// record in db
|
|
let user_id = (parent === null ? options.user.id : parent.user_id);
|
|
const { insertId: mkdir_db_id } = await db.write(
|
|
`INSERT INTO fsentries
|
|
(uuid, parent_uid, user_id, name, is_dir, created, modified, immutable, shortcut_to, is_shortcut) VALUES
|
|
( ?, ?, ?, ?, true, ?, ?, ?, ?, ?)`,
|
|
[
|
|
//uuid
|
|
uid,
|
|
//parent_uid
|
|
(parent === null) ? null : parent.uuid,
|
|
//user_id
|
|
user_id,
|
|
//name
|
|
target_name,
|
|
//created
|
|
ts,
|
|
//modified
|
|
ts,
|
|
//immutable
|
|
immutable,
|
|
//shortcut_to,
|
|
shortcut_fsentry ? shortcut_fsentry.id : null,
|
|
//is_shortcut,
|
|
shortcut_fsentry ? 1 : 0,
|
|
]
|
|
);
|
|
|
|
const ret_obj = {
|
|
uid : uid,
|
|
name: target_name,
|
|
immutable: immutable,
|
|
is_dir: true,
|
|
path: options.path ?? false,
|
|
dirpath: dirpath,
|
|
is_shared: await is_shared_with_anyone(mkdir_db_id),
|
|
overwritten_uid: overwritten_uid,
|
|
shortcut_to: shortcut_fsentry ? shortcut_fsentry.uuid : null,
|
|
shortcut_to_path: shortcut_fsentry ? await id2path(shortcut_fsentry.id) : null,
|
|
parent_dirs_created: parent_dirs_created,
|
|
original_client_socket_id: options.original_client_socket_id,
|
|
};
|
|
// add existing_fsentry if exists
|
|
if(existing_fsentry){
|
|
ret_obj.existing_fsentry ={
|
|
name: existing_fsentry.name,
|
|
uid: existing_fsentry.uuid,
|
|
}
|
|
}
|
|
|
|
if(return_id)
|
|
ret_obj.id = mkdir_db_id;
|
|
|
|
// send realtime success msg to client
|
|
let socketio = require('./socketio.js').getio();
|
|
if(socketio){
|
|
socketio.to(user_id).emit('item.added', ret_obj)
|
|
}
|
|
|
|
return ret_obj;
|
|
}
|
|
|
|
function is_valid_uuid ( uuid ) {
|
|
let s = "" + uuid;
|
|
s = s.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
|
|
return !! s;
|
|
}
|
|
|
|
function is_valid_uuid4 ( uuid ) {
|
|
return is_valid_uuid(uuid);
|
|
}
|
|
|
|
function is_specifically_uuidv4 ( uuid ) {
|
|
let s = "" + uuid;
|
|
|
|
s = s.match(/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i);
|
|
if (!s) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function is_valid_url ( url ) {
|
|
let s = "" + url;
|
|
|
|
try {
|
|
new URL(s);
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function hyphenize_confirm_code(email_confirm_code){
|
|
email_confirm_code = email_confirm_code.toString();
|
|
email_confirm_code =
|
|
email_confirm_code[0] +
|
|
email_confirm_code[1] +
|
|
email_confirm_code[2] +
|
|
'-' +
|
|
email_confirm_code[3] +
|
|
email_confirm_code[4] +
|
|
email_confirm_code[5];
|
|
return email_confirm_code;
|
|
}
|
|
|
|
async function username_exists(username){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'filesystem');
|
|
|
|
let rows = await db.read(`SELECT EXISTS(SELECT 1 FROM user WHERE username=?) AS username_exists`, [username]);
|
|
if(rows[0].username_exists)
|
|
return true;
|
|
}
|
|
|
|
async function app_name_exists(name){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_READ, 'filesystem');
|
|
|
|
let rows = await db.read(`SELECT EXISTS(SELECT 1 FROM apps WHERE apps.name=?) AS app_name_exists`, [name]);
|
|
if(rows[0].app_name_exists)
|
|
return true;
|
|
}
|
|
|
|
|
|
// generates all the default files and directories a user needs,
|
|
// generally used for a brand new account
|
|
async function generate_system_fsentries(user){
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_WRITE, 'filesystem');
|
|
|
|
//-------------------------------------------------------------
|
|
// create root `/[username]/`
|
|
//-------------------------------------------------------------
|
|
const root_dir = await mkdir({
|
|
path: '/' + user.username,
|
|
user: user,
|
|
immutable: true,
|
|
no_perm_check: true,
|
|
return_id: true,
|
|
});
|
|
|
|
// Normally, it is recommended to use mkdir() to create new folders,
|
|
// but during signup this could result in multiple queries to the DB server
|
|
// and for servers in remote regions such as Asia this could result in a
|
|
// very long time for /signup to finish, sometimes up to 30-40 seconds!
|
|
// by combining as many queries as we can into one and avoiding multiple back-and-forth
|
|
// with the DB server, we can speed this process up significantly.
|
|
const ts = Date.now()/1000;
|
|
|
|
// Generate UUIDs for all the default folders and files
|
|
let trash_uuid = uuidv4();
|
|
let appdata_uuid = uuidv4();
|
|
let desktop_uuid = uuidv4();
|
|
let documents_uuid = uuidv4();
|
|
let pictures_uuid = uuidv4();
|
|
let videos_uuid = uuidv4();
|
|
|
|
const insert_res = await db.write(
|
|
`INSERT INTO fsentries
|
|
(uuid, parent_uid, user_id, name, path, is_dir, created, modified, immutable) VALUES
|
|
( ?, ?, ?, ?, ?, true, ?, ?, true),
|
|
( ?, ?, ?, ?, ?, true, ?, ?, true),
|
|
( ?, ?, ?, ?, ?, true, ?, ?, true),
|
|
( ?, ?, ?, ?, ?, true, ?, ?, true),
|
|
( ?, ?, ?, ?, ?, true, ?, ?, true),
|
|
( ?, ?, ?, ?, ?, true, ?, ?, true)
|
|
`,
|
|
[
|
|
// Trash
|
|
trash_uuid, root_dir.uid, user.id, 'Trash', `/${user.username}/Trash`, ts, ts,
|
|
// AppData
|
|
appdata_uuid, root_dir.uid, user.id, 'AppData', `/${user.username}/AppData`, ts, ts,
|
|
// Desktop
|
|
desktop_uuid, root_dir.uid, user.id, 'Desktop', `/${user.username}/Desktop`, ts, ts,
|
|
// Documents
|
|
documents_uuid, root_dir.uid, user.id, 'Documents', `/${user.username}/Documents`, ts, ts,
|
|
// Pictures
|
|
pictures_uuid, root_dir.uid, user.id, 'Pictures', `/${user.username}/Pictures`, ts, ts,
|
|
// Videos
|
|
videos_uuid, root_dir.uid, user.id, 'Videos', `/${user.username}/Videos`, ts, ts,
|
|
]
|
|
);
|
|
|
|
// https://stackoverflow.com/a/50103616
|
|
let trash_id = insert_res.insertId;
|
|
let appdata_id = insert_res.insertId + 1;
|
|
let desktop_id = insert_res.insertId + 2;
|
|
let documents_id = insert_res.insertId + 3;
|
|
let pictures_id = insert_res.insertId + 4;
|
|
let videos_id = insert_res.insertId + 5;
|
|
|
|
// Asynchronously set the user's system folders uuids in database
|
|
// This is for caching purposes, so we don't have to query the DB every time we need to access these folders
|
|
// This is also possible because we know the user's system folders uuids will never change
|
|
|
|
// TODO: pass to IIAFE manager to avoid unhandled promise rejection
|
|
// (IIAFE manager doesn't exist yet, hence this is a TODO)
|
|
db.write(
|
|
`UPDATE user SET
|
|
trash_uuid=?, appdata_uuid=?, desktop_uuid=?, documents_uuid=?, pictures_uuid=?, videos_uuid=?,
|
|
trash_id=?, appdata_id=?, desktop_id=?, documents_id=?, pictures_id=?, videos_id=?
|
|
|
|
WHERE id=?`,
|
|
[
|
|
trash_uuid, appdata_uuid, desktop_uuid, documents_uuid, pictures_uuid, videos_uuid,
|
|
trash_id, appdata_id, desktop_id, documents_id, pictures_id, videos_id,
|
|
user.id
|
|
]
|
|
);
|
|
invalidate_cached_user(user);
|
|
}
|
|
|
|
function send_email_verification_code(email_confirm_code, email){
|
|
const nodemailer = require("nodemailer");
|
|
|
|
// send email notif
|
|
let transporter = nodemailer.createTransport({
|
|
host: config.smtp_server,
|
|
port: config.smpt_port,
|
|
secure: true, // STARTTLS
|
|
auth: {
|
|
user: config.smtp_username,
|
|
pass: config.smtp_password,
|
|
},
|
|
});
|
|
|
|
transporter.sendMail({
|
|
from: '"Puter" no-reply@puter.com', // sender address
|
|
to: email, // list of receivers
|
|
subject: `${hyphenize_confirm_code(email_confirm_code)} is your confirmation code`, // Subject line
|
|
html: `<p>Hi there,</p>
|
|
<p><strong>${hyphenize_confirm_code(email_confirm_code)}</strong> is your email confirmation code.</p>
|
|
<p>Sincerely,</p>
|
|
<p>Puter</p>
|
|
`,
|
|
});
|
|
}
|
|
|
|
function send_email_verification_token(email_confirm_token, email, user_uuid){
|
|
const nodemailer = require("nodemailer");
|
|
|
|
// send email notif
|
|
let transporter = nodemailer.createTransport({
|
|
host: config.smtp_server,
|
|
port: config.smpt_port,
|
|
secure: true, // STARTTLS
|
|
auth: {
|
|
user: config.smtp_username,
|
|
pass: config.smtp_password,
|
|
},
|
|
});
|
|
|
|
let link = `${config.origin}/confirm-email-by-token?user_uuid=${user_uuid}&token=${email_confirm_token}`;
|
|
transporter.sendMail({
|
|
from: '"Puter" no-reply@puter.com', // sender address
|
|
to: email, // list of receivers
|
|
subject: `Please confirm your email`, // Subject line
|
|
html: `<p>Hi there,</p>
|
|
<p>Please confirm your email address using this link: <strong><a href="${link}">${link}</a></strong>.</p>
|
|
<p>Sincerely,</p>
|
|
<p>Puter</p>
|
|
`,
|
|
});
|
|
}
|
|
|
|
async function generate_random_username(){
|
|
let username;
|
|
do {
|
|
username = generate_identifier();
|
|
} while (await username_exists(username));
|
|
return username;
|
|
}
|
|
|
|
function generate_random_str(length) {
|
|
var result = '';
|
|
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
var charactersLength = characters.length;
|
|
for ( var i = 0; i < length; i++ ) {
|
|
result += characters.charAt(Math.floor(Math.random() *
|
|
charactersLength));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Converts a given number of seconds into a human-readable string format.
|
|
*
|
|
* @param {number} seconds - The number of seconds to be converted.
|
|
* @returns {string} The time represented in the format: 'X years Y days Z hours A minutes B seconds'.
|
|
* @throws {TypeError} If the `seconds` parameter is not a number.
|
|
*/
|
|
function seconds_to_string(seconds) {
|
|
var numyears = Math.floor(seconds / 31536000);
|
|
var numdays = Math.floor((seconds % 31536000) / 86400);
|
|
var numhours = Math.floor(((seconds % 31536000) % 86400) / 3600);
|
|
var numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60);
|
|
var numseconds = (((seconds % 31536000) % 86400) % 3600) % 60;
|
|
return numyears + " years " + numdays + " days " + numhours + " hours " + numminutes + " minutes " + numseconds + " seconds";
|
|
}
|
|
|
|
|
|
/**
|
|
* returns a list of apps that could open the fsentry, ranked by relevance
|
|
* @param {*} fsentry
|
|
* @param {*} options
|
|
*/
|
|
async function suggest_app_for_fsentry(fsentry, options){
|
|
const monitor = PerformanceMonitor.createContext("suggest_app_for_fsentry");
|
|
const suggested_apps = [];
|
|
|
|
let content_type = mime.contentType(fsentry.name);
|
|
if(content_type === null || content_type === undefined || content_type === false)
|
|
content_type = '';
|
|
|
|
// IIFE just so fsname can stay `const`
|
|
const fsname = (() => {
|
|
if ( ! fsentry.name ) {
|
|
const fs = require('fs');
|
|
fs.writeFileSync('/tmp/missing-fsentry-name.txt', JSON.stringify(fsentry, null, 2));
|
|
return 'missing-fsentry-name';
|
|
}
|
|
let fsname = fsentry.name.toLowerCase();
|
|
// We add `.directory` so that this works as a file association
|
|
if ( fsentry.is_dir ) fsname += '.directory';
|
|
return fsname;
|
|
})();
|
|
const file_extension = _path.extname(fsname).toLowerCase();
|
|
|
|
//---------------------------------------------
|
|
// Code
|
|
//---------------------------------------------
|
|
if(
|
|
fsname.endsWith('.asm') ||
|
|
fsname.endsWith('.asp') ||
|
|
fsname.endsWith('.aspx') ||
|
|
fsname.endsWith('.bash') ||
|
|
fsname.endsWith('.c') ||
|
|
fsname.endsWith('.cpp') ||
|
|
fsname.endsWith('.css') ||
|
|
fsname.endsWith('.csv') ||
|
|
fsname.endsWith('.dhtml') ||
|
|
fsname.endsWith('.f') ||
|
|
fsname.endsWith('.go') ||
|
|
fsname.endsWith('.h') ||
|
|
fsname.endsWith('.htm') ||
|
|
fsname.endsWith('.html') ||
|
|
fsname.endsWith('.html5') ||
|
|
fsname.endsWith('.java') ||
|
|
fsname.endsWith('.jl') ||
|
|
fsname.endsWith('.js') ||
|
|
fsname.endsWith('.jsa') ||
|
|
fsname.endsWith('.json') ||
|
|
fsname.endsWith('.jsonld') ||
|
|
fsname.endsWith('.jsf') ||
|
|
fsname.endsWith('.jsp') ||
|
|
fsname.endsWith('.kt') ||
|
|
fsname.endsWith('.log') ||
|
|
fsname.endsWith('.lock') ||
|
|
fsname.endsWith('.lua') ||
|
|
fsname.endsWith('.md') ||
|
|
fsname.endsWith('.perl') ||
|
|
fsname.endsWith('.phar') ||
|
|
fsname.endsWith('.php') ||
|
|
fsname.endsWith('.pl') ||
|
|
fsname.endsWith('.py') ||
|
|
fsname.endsWith('.r') ||
|
|
fsname.endsWith('.rb') ||
|
|
fsname.endsWith('.rdata') ||
|
|
fsname.endsWith('.rda') ||
|
|
fsname.endsWith('.rdf') ||
|
|
fsname.endsWith('.rds') ||
|
|
fsname.endsWith('.rs') ||
|
|
fsname.endsWith('.rlib') ||
|
|
fsname.endsWith('.rpy') ||
|
|
fsname.endsWith('.scala') ||
|
|
fsname.endsWith('.sc') ||
|
|
fsname.endsWith('.scm') ||
|
|
fsname.endsWith('.sh') ||
|
|
fsname.endsWith('.sol') ||
|
|
fsname.endsWith('.sql') ||
|
|
fsname.endsWith('.ss') ||
|
|
fsname.endsWith('.svg') ||
|
|
fsname.endsWith('.swift') ||
|
|
fsname.endsWith('.toml') ||
|
|
fsname.endsWith('.ts') ||
|
|
fsname.endsWith('.wasm') ||
|
|
fsname.endsWith('.xhtml') ||
|
|
fsname.endsWith('.xml') ||
|
|
fsname.endsWith('.yaml') ||
|
|
// files with no extension
|
|
!fsname.includes('.')
|
|
){
|
|
suggested_apps.push(await get_app({name: 'code'}))
|
|
suggested_apps.push(await get_app({name: 'editor'}))
|
|
}
|
|
//---------------------------------------------
|
|
// Editor
|
|
//---------------------------------------------
|
|
if(
|
|
fsname.endsWith('.txt') ||
|
|
// files with no extension
|
|
!fsname.includes('.')
|
|
){
|
|
suggested_apps.push(await get_app({name: 'editor'}))
|
|
suggested_apps.push(await get_app({name: 'code'}))
|
|
}
|
|
//---------------------------------------------
|
|
// Markus
|
|
//---------------------------------------------
|
|
if(fsname.endsWith('.md')){
|
|
suggested_apps.push(await get_app({name: 'markus'}))
|
|
}
|
|
//---------------------------------------------
|
|
// Viewer
|
|
//---------------------------------------------
|
|
if(
|
|
fsname.endsWith('.jpg') ||
|
|
fsname.endsWith('.png') ||
|
|
fsname.endsWith('.webp') ||
|
|
fsname.endsWith('.svg') ||
|
|
fsname.endsWith('.bmp') ||
|
|
fsname.endsWith('.jpeg')
|
|
){
|
|
suggested_apps.push(await get_app({name: 'viewer'}));
|
|
}
|
|
//---------------------------------------------
|
|
// Draw
|
|
//---------------------------------------------
|
|
if(
|
|
fsname.endsWith('.bmp') ||
|
|
content_type.startsWith('image/')
|
|
){
|
|
suggested_apps.push(await get_app({name: 'draw'}));
|
|
}
|
|
//---------------------------------------------
|
|
// PDF
|
|
//---------------------------------------------
|
|
if(fsname.endsWith('.pdf')){
|
|
suggested_apps.push(await get_app({name: 'pdf'}));
|
|
}
|
|
//---------------------------------------------
|
|
// Player
|
|
//---------------------------------------------
|
|
if(
|
|
fsname.endsWith('.mp4') ||
|
|
fsname.endsWith('.webm') ||
|
|
fsname.endsWith('.mpg') ||
|
|
fsname.endsWith('.mpv') ||
|
|
fsname.endsWith('.mp3') ||
|
|
fsname.endsWith('.m4a') ||
|
|
fsname.endsWith('.ogg')
|
|
){
|
|
suggested_apps.push(await get_app({name: 'player'}));
|
|
}
|
|
|
|
//---------------------------------------------
|
|
// 3rd-party apps
|
|
//---------------------------------------------
|
|
const apps = kv.get(`assocs:${file_extension.slice(1)}:apps`)
|
|
|
|
monitor.label("third party associations");
|
|
if(apps && apps.length > 0){
|
|
for (let index = 0; index < apps.length; index++) {
|
|
// retrieve app from DB
|
|
const third_party_app = await get_app({id: apps[index]})
|
|
if ( ! third_party_app ) continue;
|
|
// only add if the app is approved for opening items or the app is owned by this user
|
|
if( third_party_app.approved_for_opening_items ||
|
|
(options !== undefined && options.user !== undefined && options.user.id === third_party_app.owner_user_id))
|
|
suggested_apps.push(third_party_app)
|
|
}
|
|
}
|
|
monitor.stamp();
|
|
monitor.end();
|
|
|
|
// return list
|
|
return suggested_apps;
|
|
}
|
|
|
|
function build_item_object(item){
|
|
|
|
}
|
|
|
|
async function get_taskbar_items(user) {
|
|
/** @type BaseDatabaseAccessService */
|
|
const db = services.get('database').get(DB_WRITE, 'filesystem');
|
|
|
|
let taskbar_items_from_db = [];
|
|
// If taskbar items don't exist (specifically NULL)
|
|
// add default apps.
|
|
if(!user.taskbar_items){
|
|
taskbar_items_from_db = [
|
|
{name: 'editor', type: 'app'},
|
|
{name: 'dev-center', type: 'app'},
|
|
{name: 'draw', type: 'app'},
|
|
{name: 'code', type: 'app'},
|
|
{name: 'camera', type: 'app'},
|
|
{name: 'recorder', type: 'app'},
|
|
{name: 'terminal', type: 'app'},
|
|
{name: 'about', type: 'app'},
|
|
];
|
|
await db.write(
|
|
`UPDATE user SET taskbar_items = ? WHERE id = ?`,
|
|
[
|
|
JSON.stringify(taskbar_items_from_db),
|
|
user.id,
|
|
]
|
|
);
|
|
invalidate_cached_user(user);
|
|
}
|
|
// there are items from before
|
|
else{
|
|
try {
|
|
taskbar_items_from_db = JSON.parse(user.taskbar_items);
|
|
}catch(e){
|
|
|
|
}
|
|
}
|
|
|
|
// get apps that these taskbar items represent
|
|
let taskbar_items = [];
|
|
for (let index = 0; index < taskbar_items_from_db.length; index++) {
|
|
const taskbar_item_from_db = taskbar_items_from_db[index];
|
|
if(taskbar_item_from_db.type === 'app' && taskbar_item_from_db.name !== 'explorer'){
|
|
let item = {};
|
|
if(taskbar_item_from_db.name)
|
|
item = await get_app({name: taskbar_item_from_db.name});
|
|
else if(taskbar_item_from_db.id)
|
|
item = await get_app({id: taskbar_item_from_db.id});
|
|
else if(taskbar_item_from_db.uid)
|
|
item = await get_app({uid: taskbar_item_from_db.uid});
|
|
|
|
// if item not found, skip it
|
|
if(!item) continue;
|
|
|
|
// delete sensitive attributes
|
|
delete item.id;
|
|
delete item.owner_user_id;
|
|
delete item.timestamp;
|
|
// delete item.godmode;
|
|
delete item.approved_for_listing;
|
|
delete item.approved_for_opening_items;
|
|
|
|
// add to final object
|
|
taskbar_items.push(item)
|
|
}
|
|
}
|
|
|
|
return taskbar_items;
|
|
}
|
|
|
|
function validate_signature_auth(url, action) {
|
|
const query = new URL(url).searchParams;
|
|
|
|
if(!query.get('uid'))
|
|
throw {message: '`uid` is required for signature-based authentication.'}
|
|
else if(!action)
|
|
throw {message: '`action` is required for signature-based authentication.'}
|
|
else if(!query.get('expires'))
|
|
throw {message: '`expires` is required for signature-based authentication.'}
|
|
else if(!query.get('signature'))
|
|
throw {message: '`signature` is required for signature-based authentication.'}
|
|
|
|
const expired = query.get('expires') && (query.get('expires') < Date.now() / 1000);
|
|
|
|
// expired?
|
|
if(expired)
|
|
throw {message: 'Authentication failed. Signature expired.'}
|
|
|
|
const uid = query.get('uid');
|
|
const secret = config.url_signature_secret;
|
|
const sha256 = require('js-sha256').sha256;
|
|
|
|
// before doing anything, see if this signature is valid for 'write' action, if yes that means every action is allowed
|
|
if(!expired && query.get('signature') === sha256(`${uid}/write/${secret}/${query.get('expires')}`))
|
|
return true;
|
|
// if not, check specific actions
|
|
else if(!expired && query.get('signature') === sha256(`${uid}/${action}/${secret}/${query.get('expires')}`))
|
|
return true;
|
|
// auth failed
|
|
else
|
|
throw {message: 'Authentication failed'}
|
|
}
|
|
|
|
function get_url_from_req(req) {
|
|
return req.protocol + '://' + req.get('host') + req.originalUrl;
|
|
}
|
|
|
|
async function mv(options){
|
|
throw new Error('legacy mv function called');
|
|
}
|
|
|
|
/**
|
|
* Formats a number with grouped thousands.
|
|
*
|
|
* @param {number|string} number - The number to be formatted. If a string is provided, it must only contain numerical characters, plus and minus signs, and the letter 'E' or 'e' (for scientific notation).
|
|
* @param {number} decimals - The number of decimal points. If a non-finite number is provided, it defaults to 0.
|
|
* @param {string} [dec_point='.'] - The character used for the decimal point. Defaults to '.' if not provided.
|
|
* @param {string} [thousands_sep=','] - The character used for the thousands separator. Defaults to ',' if not provided.
|
|
* @returns {string} The formatted number with grouped thousands, using the specified decimal point and thousands separator characters.
|
|
* @throws {TypeError} If the `number` parameter cannot be converted to a finite number, or if the `decimals` parameter is non-finite and cannot be converted to an absolute number.
|
|
*/
|
|
function number_format (number, decimals, dec_point, thousands_sep) {
|
|
// Strip all characters but numerical ones.
|
|
number = (number + '').replace(/[^0-9+\-Ee.]/g, '');
|
|
var n = !isFinite(+number) ? 0 : +number,
|
|
prec = !isFinite(+decimals) ? 0 : Math.abs(decimals),
|
|
sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep,
|
|
dec = (typeof dec_point === 'undefined') ? '.' : dec_point,
|
|
s = '',
|
|
toFixedFix = function (n, prec) {
|
|
var k = Math.pow(10, prec);
|
|
return '' + Math.round(n * k) / k;
|
|
};
|
|
// Fix for IE parseFloat(0.55).toFixed(0) = 0;
|
|
s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
|
|
if (s[0].length > 3) {
|
|
s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep);
|
|
}
|
|
if ((s[1] || '').length < prec) {
|
|
s[1] = s[1] || '';
|
|
s[1] += new Array(prec - s[1].length + 1).join('0');
|
|
}
|
|
return s.join(dec);
|
|
}
|
|
|
|
module.exports = {
|
|
ancestors,
|
|
app_name_exists,
|
|
app_exists,
|
|
body_parser_error_handler,
|
|
build_item_object,
|
|
byte_format,
|
|
change_username,
|
|
chkperm,
|
|
convert_path_to_fsentry,
|
|
cp,
|
|
deleteUser,
|
|
get_descendants,
|
|
get_dir_size,
|
|
gen_public_token,
|
|
get_taskbar_items,
|
|
get_url_from_req,
|
|
generate_system_fsentries,
|
|
generate_random_str,
|
|
generate_random_username,
|
|
get_app,
|
|
get_user,
|
|
invalidate_cached_user,
|
|
invalidate_cached_user_by_id,
|
|
has_shared_with,
|
|
hyphenize_confirm_code,
|
|
id2fsentry,
|
|
id2path,
|
|
id2uuid,
|
|
is_ancestor_of,
|
|
is_empty,
|
|
is_shared_with,
|
|
is_shared_with_anyone,
|
|
is_valid_uuid4,
|
|
is_valid_uuid,
|
|
is_specifically_uuidv4,
|
|
is_valid_url,
|
|
jwt_auth,
|
|
mkdir,
|
|
mv,
|
|
number_format,
|
|
refresh_apps_cache,
|
|
refresh_associations_cache,
|
|
resolve_glob,
|
|
rm,
|
|
seconds_to_string,
|
|
send_email_verification_code,
|
|
send_email_verification_token,
|
|
sign_file,
|
|
subdomain,
|
|
suggest_app_for_fsentry,
|
|
df,
|
|
username_exists,
|
|
uuid2fsentry,
|
|
validate_fsentry_name,
|
|
validate_signature_auth,
|
|
tmp_provide_services,
|
|
};
|