helpers.js 66 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980
  1. /*
  2. * Copyright (C) 2024 Puter Technologies Inc.
  3. *
  4. * This file is part of Puter.
  5. *
  6. * Puter is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published
  8. * by the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. const { v4: uuidv4 } = require('uuid');
  20. const _path = require('path');
  21. const micromatch = require('micromatch');
  22. const config = require('./config')
  23. const mime = require('mime-types');
  24. const PerformanceMonitor = require('./monitor/PerformanceMonitor.js');
  25. const { generate_identifier } = require('./util/identifier.js');
  26. const { ManagedError } = require('./util/errorutil.js');
  27. const { spanify } = require('./util/otelutil.js');
  28. const APIError = require('./api/APIError.js');
  29. const { DB_READ, DB_WRITE } = require('./services/database/consts.js');
  30. const { BaseDatabaseAccessService } = require('./services/database/BaseDatabaseAccessService.js');
  31. const { LLRmNode } = require('./filesystem/ll_operations/ll_rmnode');
  32. const { Context } = require('./util/context');
  33. const { NodeUIDSelector } = require('./filesystem/node/selectors');
  34. let systemfs = null;
  35. let services = null;
  36. const tmp_provide_services = async ss => {
  37. services = ss;
  38. await services.ready;
  39. systemfs = services.get('filesystem').get_systemfs();
  40. }
  41. async function is_empty(dir_uuid){
  42. /** @type BaseDatabaseAccessService */
  43. const db = services.get('database').get(DB_READ, 'filesystem');
  44. // first check if this entry is shared
  45. let rows = await db.read(
  46. `SELECT EXISTS(SELECT 1 FROM fsentries WHERE parent_uid = ? LIMIT 1) AS not_empty`,
  47. [dir_uuid]
  48. );
  49. return !rows[0].not_empty;
  50. }
  51. /**
  52. * @deprecated - sharing will be implemented with user-to-user ACL
  53. */
  54. async function has_shared_with(user_id, recipient_user_id){
  55. return false;
  56. }
  57. /**
  58. * Checks to see if this file/directory is shared with the user identified by `recipient_user_id`
  59. *
  60. * @param {*} fsentry_id
  61. * @param {*} recipient_user_id
  62. *
  63. * @deprecated - sharing will be implemented with user-to-user ACL
  64. */
  65. async function is_shared_with(fsentry_id, recipient_user_id){
  66. return false;
  67. }
  68. /**
  69. * Checks to see if this file/directory is shared with at least one other user
  70. *
  71. * @param {*} fsentry_id
  72. * @param {*} recipient_user_id
  73. *
  74. * @deprecated - sharing will be implemented with user-to-user ACL
  75. */
  76. async function is_shared_with_anyone(fsentry_id){
  77. return false;
  78. }
  79. const chkperm = spanify('chkperm', async (target_fsentry, requester_user_id, action) => {
  80. // basic cases where false is the default response
  81. if(!target_fsentry)
  82. return false;
  83. // pseudo-entry from FSNodeContext
  84. if ( target_fsentry.is_root ) {
  85. return action === 'read';
  86. }
  87. // requester is the owner of this entry
  88. if(target_fsentry.user_id === requester_user_id){
  89. return true;
  90. }
  91. // this entry was shared with the requester
  92. else if(await is_shared_with(target_fsentry.id, requester_user_id)){
  93. return true;
  94. }
  95. // 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]
  96. else if(target_fsentry.parent_uid === null && await has_shared_with(target_fsentry.user_id, requester_user_id) && action !== 'write')
  97. return true;
  98. else
  99. return false;
  100. });
  101. /**
  102. * Checks if the string provided is a valid FileSystem Entry name.
  103. *
  104. * @param {string} name
  105. * @returns
  106. */
  107. function validate_fsentry_name(name){
  108. if(!name)
  109. throw {message: 'Name can not be empty.'}
  110. else if(!isString(name))
  111. throw {message: "Name can only be a string."}
  112. else if(name.includes('/'))
  113. throw {message: "Name can not contain the '/' character."}
  114. else if(name === '.')
  115. throw {message: "Name can not be the '.' character."};
  116. else if(name === '..')
  117. throw {message: "Name can not be the '..' character."};
  118. else if(name.length > config.max_fsentry_name_length)
  119. throw {message: `Name can not be longer than ${config.max_fsentry_name_length} characters`}
  120. else
  121. return true
  122. }
  123. /**
  124. * Convert a FSEntry ID to UUID
  125. *
  126. * @param {integer} id - `id` of FSEntry
  127. * @returns {Promise} Promise object represents the UUID of the FileSystem Entry
  128. */
  129. async function id2uuid(id){
  130. /** @type BaseDatabaseAccessService */
  131. const db = services.get('database').get(DB_READ, 'filesystem');
  132. let fsentry = await db.requireRead("SELECT `uuid`, immutable FROM `fsentries` WHERE `id` = ? LIMIT 1", [id]);
  133. if(!fsentry[0])
  134. return null;
  135. else
  136. return fsentry[0].uuid;
  137. }
  138. /**
  139. * Get total data stored by a user
  140. *
  141. * @param {integer} user_id - `user_id` of user
  142. * @returns {Promise} Promise object represents the UUID of the FileSystem Entry
  143. */
  144. async function df(user_id){
  145. /** @type BaseDatabaseAccessService */
  146. const db = services.get('database').get(DB_READ, 'filesystem');
  147. const fsentry = await db.read("SELECT SUM(size) AS total FROM `fsentries` WHERE `user_id` = ? LIMIT 1", [user_id]);
  148. if(!fsentry[0] || !fsentry[0].total)
  149. return 0;
  150. else
  151. return fsentry[0].total;
  152. }
  153. /**
  154. * Get user by a variety of IDs
  155. *
  156. * Pass `cached: false` to options if a cached user entry would not be appropriate;
  157. * for example: when performing authentication.
  158. *
  159. * @param {string} options - `options`
  160. * @returns {Promise}
  161. */
  162. async function get_user(options){
  163. /** @type BaseDatabaseAccessService */
  164. const db = services.get('database').get(DB_READ, 'filesystem');
  165. let user;
  166. const cached = options.cached ?? true;
  167. if ( cached && ! options.force ) {
  168. if (options.username) user = kv.get('users:username:' + options.username);
  169. else if (options.email) user = kv.get('users:email:' + options.email);
  170. else if (options.uuid) user = kv.get('users:uuid:' + options.uuid);
  171. else if (options.id) user = kv.get('users:id:' + options.id);
  172. else if (options.referral_code) user = kv.get('users:referral_code:' + options.referral_code);
  173. if ( user ) return user;
  174. }
  175. if ( ! options.force ) {
  176. if(options.username)
  177. user = await db.read("SELECT * FROM `user` WHERE `username` = ? LIMIT 1", [options.username]);
  178. else if(options.email)
  179. user = await db.read("SELECT * FROM `user` WHERE `email` = ? LIMIT 1", [options.email]);
  180. else if(options.uuid)
  181. user = await db.read("SELECT * FROM `user` WHERE `uuid` = ? LIMIT 1", [options.uuid]);
  182. else if(options.id)
  183. user = await db.read("SELECT * FROM `user` WHERE `id` = ? LIMIT 1", [options.id]);
  184. else if(options.referral_code)
  185. user = await db.read("SELECT * FROM `user` WHERE `referral_code` = ? LIMIT 1", [options.referral_code]);
  186. }
  187. if(!user || !user[0]){
  188. if(options.username)
  189. user = await db.pread("SELECT * FROM `user` WHERE `username` = ? LIMIT 1", [options.username])
  190. else if(options.email)
  191. user = await db.pread("SELECT * FROM `user` WHERE `email` = ? LIMIT 1", [options.email]);
  192. else if(options.uuid)
  193. user = await db.pread("SELECT * FROM `user` WHERE `uuid` = ? LIMIT 1", [options.uuid]);
  194. else if(options.id)
  195. user = await db.pread("SELECT * FROM `user` WHERE `id` = ? LIMIT 1", [options.id]);
  196. else if(options.referral_code)
  197. user = await db.pread("SELECT * FROM `user` WHERE `referral_code` = ? LIMIT 1", [options.referral_code]);
  198. }
  199. user = user ? user[0] : null;
  200. if ( ! user ) return user;
  201. try {
  202. kv.set('users:username:' + user.username, user);
  203. kv.set('users:email:' + user.email, user);
  204. kv.set('users:uuid:' + user.uuid, user);
  205. kv.set('users:id:' + user.id, user);
  206. kv.set('users:referral_code:' + user.referral_code, user);
  207. } catch (e) {
  208. console.error(e);
  209. }
  210. return user;
  211. }
  212. /**
  213. * Invalidate the cached entries for a user object
  214. *
  215. * @param {User} userID - the user entry to invalidate
  216. */
  217. function invalidate_cached_user (user) {
  218. kv.del('users:username:' + user.username);
  219. kv.del('users:uuid:' + user.uuid);
  220. kv.del('users:email:' + user.email);
  221. kv.del('users:id:' + user.id);
  222. }
  223. /**
  224. * Invalidate the cached entries for the user specified by an id
  225. * @param {number} id - the id of the user to invalidate
  226. */
  227. function invalidate_cached_user_by_id (id) {
  228. const user = kv.get('users:id:' + id);
  229. if ( ! user ) return;
  230. invalidate_cached_user(user);
  231. }
  232. /**
  233. * Refresh apps cache
  234. *
  235. * @param {string} options - `options`
  236. * @returns {Promise}
  237. */
  238. async function refresh_apps_cache(options, override){
  239. /** @type BaseDatabaseAccessService */
  240. const db = services.get('database').get(DB_READ, 'apps');
  241. const log = services.get('log-service').create('refresh_apps_cache');
  242. log.tick('refresh apps cache');
  243. // if options is not provided, refresh all apps
  244. if(!options){
  245. let apps = await db.read('SELECT * FROM apps');
  246. for (let index = 0; index < apps.length; index++) {
  247. const app = apps[index];
  248. kv.set('apps:name:' + app.name, app);
  249. kv.set('apps:id:' + app.id, app);
  250. kv.set('apps:uid:' + app.uid, app);
  251. }
  252. }
  253. // refresh only apps that are approved for listing
  254. else if(options.only_approved_for_listing){
  255. let apps = await db.read('SELECT * FROM apps WHERE approved_for_listing = 1');
  256. for (let index = 0; index < apps.length; index++) {
  257. const app = apps[index];
  258. kv.set('apps:name:' + app.name, app);
  259. kv.set('apps:id:' + app.id, app);
  260. kv.set('apps:uid:' + app.uid, app);
  261. }
  262. }
  263. // if options is provided, refresh only the app specified
  264. else{
  265. let app;
  266. if(options.name)
  267. app = await db.read('SELECT * FROM apps WHERE name = ?', [options.name]);
  268. else if(options.uid)
  269. app = await db.read('SELECT * FROM apps WHERE uid = ?', [options.uid]);
  270. else if(options.id)
  271. app = await db.read('SELECT * FROM apps WHERE id = ?', [options.id]);
  272. else {
  273. log.error('invalid options to refresh_apps_cache');
  274. throw new Error('Invalid options provided');
  275. }
  276. if(!app || !app[0]) {
  277. log.error('refresh_apps_cache could not find the app');
  278. return;
  279. } else {
  280. app = app[0];
  281. if ( override ) {
  282. Object.assign(app, override);
  283. }
  284. kv.set('apps:name:' + app.name, app);
  285. kv.set('apps:id:' + app.id, app);
  286. kv.set('apps:uid:' + app.uid, app);
  287. }
  288. }
  289. }
  290. async function refresh_associations_cache(){
  291. /** @type BaseDatabaseAccessService */
  292. const db = services.get('database').get(DB_READ, 'apps');
  293. const log = services.get('log-service').create('refresh_apps_cache');
  294. log.tick('refresh associations cache');
  295. const associations = await db.read('SELECT * FROM app_filetype_association');
  296. const lists = {};
  297. for ( const association of associations ) {
  298. let ext = association.type;
  299. if ( ext.startsWith('.') ) ext = ext.slice(1);
  300. if ( ! lists.hasOwnProperty(ext) ) lists[ext] = [];
  301. lists[ext].push(association.app_id);
  302. }
  303. for ( const k in lists ) {
  304. kv.set(`assocs:${k}:apps`, lists[k]);
  305. }
  306. }
  307. /**
  308. * Get App by a variety of IDs
  309. *
  310. * @param {string} options - `options`
  311. * @returns {Promise}
  312. */
  313. async function get_app(options){
  314. /** @type BaseDatabaseAccessService */
  315. const db = services.get('database').get(DB_READ, 'apps');
  316. const log = services.get('log-service').create('get_app');
  317. let app = [];
  318. if(options.uid){
  319. // try cache first
  320. app[0] = kv.get(`apps:uid:${options.uid}`);
  321. // not in cache, try db
  322. if(!app[0]) {
  323. log.cache(false, 'apps:uid:' + options.uid);
  324. app = await db.read("SELECT * FROM `apps` WHERE `uid` = ? LIMIT 1", [options.uid]);
  325. }
  326. }else if(options.name){
  327. // try cache first
  328. app[0] = kv.get(`apps:name:${options.name}`);
  329. // not in cache, try db
  330. if(!app[0]) {
  331. log.cache(false, 'apps:name:' + options.name);
  332. app = await db.read("SELECT * FROM `apps` WHERE `name` = ? LIMIT 1", [options.name]);
  333. }
  334. }
  335. else if(options.id){
  336. // try cache first
  337. app[0] = kv.get(`apps:id:${options.id}`);
  338. // not in cache, try db
  339. if(!app[0]) {
  340. log.cache(false, 'apps:id:' + options.id);
  341. app = await db.read("SELECT * FROM `apps` WHERE `id` = ? LIMIT 1", [options.id]);
  342. }
  343. }
  344. app = app && app[0] ? app[0] : null;
  345. if ( app === null ) return null;
  346. // shallow clone because we use the `delete` operator
  347. // and it corrupts the cache otherwise
  348. app = { ...app };
  349. return app;
  350. }
  351. /**
  352. * Checks to see if an app exists
  353. *
  354. * @param {string} options - `options`
  355. * @returns {Promise}
  356. */
  357. async function app_exists(options){
  358. /** @type BaseDatabaseAccessService */
  359. const db = services.get('database').get(DB_READ, 'apps');
  360. let app;
  361. if(options.uid)
  362. app = await db.read("SELECT `id` FROM `apps` WHERE `uid` = ? LIMIT 1", [options.uid]);
  363. else if(options.name)
  364. app = await db.read("SELECT `id` FROM `apps` WHERE `name` = ? LIMIT 1", [options.name]);
  365. else if(options.id)
  366. app = await db.read("SELECT `id` FROM `apps` WHERE `id` = ? LIMIT 1", [options.id]);
  367. return app[0];
  368. }
  369. /**
  370. * change username
  371. *
  372. * @param {string} options - `options`
  373. * @returns {Promise}
  374. */
  375. async function change_username(user_id, new_username){
  376. /** @type BaseDatabaseAccessService */
  377. const db = services.get('database').get(DB_WRITE, 'auth');
  378. const old_username = (await get_user({id: user_id})).username;
  379. // update username
  380. await db.write("UPDATE `user` SET username = ? WHERE `id` = ? LIMIT 1", [new_username, user_id]);
  381. // update root directory name for this user
  382. await db.write("UPDATE `fsentries` SET `name` = ? WHERE `user_id` = ? AND parent_uid IS NULL LIMIT 1", [new_username, user_id]);
  383. const log = services.get('log-service').create('change_username');
  384. log.noticeme(`User ${old_username} changed username to ${new_username}`);
  385. await services.get('filesystem').update_child_paths(`/${old_username}`, `/${new_username}`, user_id);
  386. invalidate_cached_user_by_id(user_id);
  387. }
  388. /**
  389. * Find a FSEntry by its uuid
  390. *
  391. * @param {integer} id - `id` of FSEntry
  392. * @returns {Promise} Promise object represents the UUID of the FileSystem Entry
  393. * @deprecated Use fs middleware instead
  394. */
  395. async function uuid2fsentry(uuid, return_thumbnail){
  396. /** @type BaseDatabaseAccessService */
  397. const db = services.get('database').get(DB_READ, 'filesystem');
  398. // todo optim, check if uuid is not exactly 36 characters long, if not it's invalid
  399. // and we can avoid one unnecessary DB lookup
  400. let fsentry = await db.requireRead(
  401. `SELECT
  402. id,
  403. associated_app_id,
  404. uuid,
  405. public_token,
  406. bucket,
  407. bucket_region,
  408. file_request_token,
  409. user_id,
  410. parent_uid,
  411. is_dir,
  412. is_public,
  413. is_shortcut,
  414. shortcut_to,
  415. sort_by,
  416. ${return_thumbnail ? 'thumbnail,' : ''}
  417. immutable,
  418. name,
  419. metadata,
  420. modified,
  421. created,
  422. accessed,
  423. size
  424. FROM fsentries WHERE uuid = ? LIMIT 1`,
  425. [uuid]
  426. );
  427. if(!fsentry[0])
  428. return false;
  429. else
  430. return fsentry[0];
  431. }
  432. /**
  433. * Find a FSEntry by its id
  434. *
  435. * @param {integer} id - `id` of FSEntry
  436. * @returns {Promise} Promise object represents the UUID of the FileSystem Entry
  437. */
  438. async function id2fsentry(id, return_thumbnail){
  439. /** @type BaseDatabaseAccessService */
  440. const db = services.get('database').get(DB_READ, 'filesystem');
  441. // todo optim, check if uuid is not exactly 36 characters long, if not it's invalid
  442. // and we can avoid one unnecessary DB lookup
  443. let fsentry = await db.requireRead(
  444. `SELECT
  445. id,
  446. uuid,
  447. public_token,
  448. file_request_token,
  449. associated_app_id,
  450. user_id,
  451. parent_uid,
  452. is_dir,
  453. is_public,
  454. is_shortcut,
  455. shortcut_to,
  456. sort_by,
  457. ${return_thumbnail ? 'thumbnail,' : ''}
  458. immutable,
  459. name,
  460. metadata,
  461. modified,
  462. created,
  463. accessed,
  464. size
  465. FROM fsentries WHERE id = ? LIMIT 1`,
  466. [id]
  467. );
  468. if(!fsentry[0]){
  469. return false;
  470. }else
  471. return fsentry[0];
  472. }
  473. /**
  474. * Takes a an absolute path and returns its corresponding FSEntry.
  475. *
  476. * @param {string} path - absolute path of the filesystem entry to be resolved
  477. * @param {boolean} return_content - if FSEntry is a file, determines whether its content should be returned
  478. * @returns {false|object} - `false` if path could not be resolved, otherwise an object representing the FSEntry
  479. * @deprecated Use fs middleware instead
  480. */
  481. async function convert_path_to_fsentry(path){
  482. // todo optim, check if path is valid (e.g. contaisn valid characters)
  483. // if syntactical errors are found we can potentially avoid some expensive db lookups
  484. // '/' means that parent_uid is null
  485. // TODO: facade fsentry for root (devlog:2023-06-01)
  486. if(path === '/')
  487. return null;
  488. //first slash is redundant
  489. path = path.substr(path.indexOf('/') + 1)
  490. //last slash, if existing is redundant
  491. if(path[path.length - 1] === '/')
  492. path = path.slice(0, -1);
  493. //split path into parts
  494. const fsentry_names = path.split('/');
  495. // if no parts, return false
  496. if(fsentry_names.length === 0)
  497. return false;
  498. let parent_uid = null;
  499. let final_res = null;
  500. let is_public = false
  501. let result
  502. /** @type BaseDatabaseAccessService */
  503. const db = services.get('database').get(DB_READ, 'filesystem');
  504. // Try stored path first
  505. result = await db.read(
  506. `SELECT * FROM fsentries WHERE path=? LIMIT 1`,
  507. ['/' + path],
  508. );
  509. if ( result[0] ) {
  510. return result[0];
  511. }
  512. for(let i=0; i < fsentry_names.length; i++){
  513. if(parent_uid === null){
  514. result = await db.read(
  515. `SELECT * FROM fsentries WHERE parent_uid IS NULL AND name=? LIMIT 1`,
  516. [fsentry_names[i]]
  517. );
  518. }
  519. else{
  520. result = await db.read(
  521. `SELECT * FROM fsentries WHERE parent_uid = ? AND name=? LIMIT 1`,
  522. [parent_uid, fsentry_names[i]]
  523. );
  524. }
  525. if(result[0] ){
  526. parent_uid = result[0].uuid;
  527. // is_public is either directly specified or inherited from parent dir
  528. if(result[0].is_public === null)
  529. result[0].is_public = is_public
  530. else
  531. is_public = result[0].is_public
  532. }else{
  533. return false;
  534. }
  535. final_res = result
  536. }
  537. return final_res[0];
  538. }
  539. /**
  540. *
  541. * @param {integer} bytes - size in bytes
  542. * @returns {string} bytes in human-readable format
  543. */
  544. function byte_format(bytes){
  545. // calculate and return bytes in human-readable format
  546. const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
  547. if (typeof bytes !== "number" || bytes < 1) {
  548. return '0 B';
  549. }
  550. const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
  551. return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
  552. };
  553. const get_dir_size = async (path, user)=>{
  554. let size = 0;
  555. const descendants = await get_descendants(path, user);
  556. for(let i=0; i < descendants.length; i++){
  557. if(!descendants[i].is_dir){
  558. size += descendants[i].size;
  559. }
  560. }
  561. return size;
  562. }
  563. /**
  564. * Recursively retrieve all files, directories, and subdirectories under `path`.
  565. * Optionally the `depth` can be set.
  566. *
  567. * @param {string} path
  568. * @param {object} user
  569. * @param {integer} depth
  570. * @returns
  571. */
  572. const get_descendants_0 = async (path, user, depth, return_thumbnail = false) => {
  573. const log = services.get('log-service').create('get_descendants');
  574. log.called();
  575. // decrement depth if it's set
  576. depth !== undefined && depth--;
  577. // turn path into absolute form
  578. path = _path.resolve('/', path)
  579. // get parent dir
  580. const parent = await convert_path_to_fsentry(path);
  581. // holds array that will be returned
  582. const ret = [];
  583. // holds immediate children of this path
  584. let children;
  585. // try to extract username from path
  586. let username;
  587. let split_path = path.split('/');
  588. if(split_path.length === 2 && split_path[0] === '')
  589. username = split_path[1];
  590. /** @type BaseDatabaseAccessService */
  591. const db = services.get('database').get(DB_READ, 'filesystem');
  592. // -------------------------------------
  593. // parent is root ('/')
  594. // -------------------------------------
  595. if(parent === null){
  596. path = '';
  597. // direct children under root
  598. children = await db.read(
  599. `SELECT
  600. id, uuid, parent_uid, name, metadata, is_dir, bucket, bucket_region,
  601. modified, created, immutable, shortcut_to, is_shortcut, sort_by, associated_app_id,
  602. ${return_thumbnail ? 'thumbnail, ' : ''}
  603. accessed, size
  604. FROM fsentries
  605. WHERE user_id = ? AND parent_uid IS NULL`,
  606. [user.id]
  607. );
  608. // users that have shared files/dirs with this user
  609. const sharing_users = await db.read(
  610. `SELECT DISTINCT(owner_user_id), user.username
  611. FROM share
  612. INNER JOIN user ON user.id = share.owner_user_id
  613. WHERE share.recipient_user_id = ?`,
  614. [user.id]
  615. );
  616. if(sharing_users.length>0){
  617. for(let i=0; i<sharing_users.length; i++){
  618. let dir = {};
  619. dir.id = null;
  620. dir.uuid = null;
  621. dir.parent_uid = null;
  622. dir.name = sharing_users[i].username;
  623. dir.is_dir = true;
  624. dir.immutable = true;
  625. children.push(dir)
  626. }
  627. }
  628. }
  629. // -------------------------------------
  630. // parent doesn't exist
  631. // -------------------------------------
  632. else if(parent === false){
  633. return [];
  634. }
  635. // -------------------------------------
  636. // Parent is a shared-user directory: /[some_username](/)
  637. // but make sure `[some_username]` is not the same as the requester's username
  638. // -------------------------------------
  639. else if(username && username !== user.username){
  640. children = [];
  641. let sharing_user;
  642. sharing_user = await get_user({username: username});
  643. if(!sharing_user)
  644. return [];
  645. // shared files/dirs with this user
  646. const shared_fsentries = await db.read(
  647. `SELECT
  648. fsentries.id, fsentries.user_id, fsentries.uuid, fsentries.parent_uid, fsentries.bucket, fsentries.bucket_region,
  649. fsentries.name, fsentries.shortcut_to, fsentries.is_shortcut, fsentries.metadata, fsentries.is_dir, fsentries.modified,
  650. fsentries.created, fsentries.accessed, fsentries.size, fsentries.sort_by, fsentries.associated_app_id,
  651. fsentries.is_symlink, fsentries.symlink_path,
  652. fsentries.immutable ${return_thumbnail ? ', fsentries.thumbnail' : ''}
  653. FROM share
  654. INNER JOIN fsentries ON fsentries.id = share.fsentry_id
  655. WHERE share.recipient_user_id = ? AND owner_user_id = ?`,
  656. [user.id, sharing_user.id]
  657. );
  658. // merge `children` and `shared_fsentries`
  659. if(shared_fsentries.length>0){
  660. for(let i=0; i<shared_fsentries.length; i++){
  661. shared_fsentries[i].path = await id2path(shared_fsentries[i].id);
  662. children.push(shared_fsentries[i])
  663. }
  664. }
  665. }
  666. // -------------------------------------
  667. // All other cases
  668. // -------------------------------------
  669. else{
  670. children = [];
  671. let temp_children = await db.read(
  672. `SELECT
  673. id, user_id, uuid, parent_uid, name, metadata, is_shortcut,
  674. shortcut_to, is_dir, modified, created, accessed, size, sort_by, associated_app_id,
  675. is_symlink, symlink_path,
  676. immutable ${return_thumbnail ? ', thumbnail' : ''}
  677. FROM fsentries
  678. WHERE parent_uid = ?`,
  679. [parent.uuid]
  680. );
  681. // check if user has access to each file, if yes add it
  682. if(temp_children.length>0){
  683. for(let i=0; i<temp_children.length; i++){
  684. const tchild = temp_children[i];
  685. if(await chkperm(tchild, user.id))
  686. children.push(tchild);
  687. }
  688. }
  689. }
  690. // shortcut on empty result set
  691. if ( children.length === 0 ) return [];
  692. const ids = children.map(child => child.id);
  693. const qmarks = ids.map(() => '?').join(',');
  694. let rows = await db.read(
  695. `SELECT root_dir_id FROM subdomains WHERE root_dir_id IN (${qmarks}) AND user_id=?`,
  696. [...ids, user.id]);
  697. log.debug('rows???', rows);
  698. const websiteMap = {};
  699. for ( const row of rows ) websiteMap[row.root_dir_id] = true;
  700. for(let i=0; i<children.length; i++){
  701. const contentType = mime.contentType(children[i].name)
  702. // has_website
  703. let has_website = false;
  704. if(children[i].is_dir){
  705. has_website = websiteMap[children[i].id];
  706. }
  707. // object to return
  708. // TODO: DRY creation of response fsentry from db fsentry
  709. ret.push({
  710. path: children[i].path ?? (path + '/' + children[i].name),
  711. name: children[i].name,
  712. metadata: children[i].metadata,
  713. _id: children[i].id,
  714. id: children[i].uuid,
  715. uid: children[i].uuid,
  716. is_shortcut: children[i].is_shortcut,
  717. shortcut_to: (children[i].shortcut_to ? await id2uuid(children[i].shortcut_to) : undefined),
  718. shortcut_to_path: (children[i].shortcut_to ? await id2path(children[i].shortcut_to) : undefined),
  719. is_symlink: children[i].is_symlink,
  720. symlink_path: children[i].symlink_path,
  721. immutable: children[i].immutable,
  722. is_dir: children[i].is_dir,
  723. modified: children[i].modified,
  724. created: children[i].created,
  725. accessed: children[i].accessed,
  726. size: children[i].size,
  727. sort_by: children[i].sort_by,
  728. thumbnail: children[i].thumbnail,
  729. associated_app_id: children[i].associated_app_id,
  730. type: contentType ? contentType : null,
  731. has_website: has_website,
  732. })
  733. if( children[i].is_dir &&
  734. (depth === undefined || (depth !== undefined && depth > 0))
  735. ){
  736. ret.push(await get_descendants(path + '/' + children[i].name, user, depth))
  737. }
  738. }
  739. return ret.flat();
  740. }
  741. const get_descendants = async (...args) => {
  742. const tracer = services.get('traceService').tracer;
  743. let ret;
  744. await tracer.startActiveSpan('get_descendants', async span => {
  745. ret = await get_descendants_0(...args);
  746. span.end();
  747. });
  748. return ret;
  749. }
  750. /**
  751. *
  752. * @param {integer} entry_id
  753. * @returns
  754. */
  755. const id2path = async (entry_uid)=>{
  756. if ( entry_uid == null ) {
  757. throw new Error('got null or undefined entry id');
  758. }
  759. /** @type BaseDatabaseAccessService */
  760. const db = services.get('database').get(DB_READ, 'filesystem');
  761. const traces = services.get('traceService');
  762. const log = services.get('log-service').create('helpers.id2path');
  763. log.traceOn();
  764. const errors = services.get('error-service').create(log);
  765. log.called();
  766. let result;
  767. return await traces.spanify(`helpers:id2path`, async () => {
  768. log.debug(`entry id: ${entry_uid}`)
  769. if ( typeof entry_uid === 'number' ) {
  770. const old = entry_uid;
  771. entry_uid = await id2uuid(entry_uid);
  772. log.debug(`entry id resolved: resolved ${old} ${entry_uid}`)
  773. }
  774. try {
  775. result = await db.read(`
  776. WITH RECURSIVE cte AS (
  777. SELECT uuid, parent_uid, name, name AS path
  778. FROM fsentries
  779. WHERE uuid = ?
  780. UNION ALL
  781. SELECT e.uuid, e.parent_uid, e.name, ${
  782. db.case({
  783. sqlite: `e.name || '/' || cte.path`,
  784. otherwise: `CONCAT(e.name, '/', cte.path)`,
  785. })
  786. }
  787. FROM fsentries e
  788. INNER JOIN cte ON cte.parent_uid = e.uuid
  789. )
  790. SELECT *
  791. FROM cte
  792. WHERE parent_uid IS NULL
  793. `, [entry_uid]);
  794. } catch (e) {
  795. errors.report('id2path.select', {
  796. alarm: true,
  797. source: e,
  798. message: `error while resolving path for ${entry_uid}: ${e.message}`,
  799. extra: {
  800. entry_uid,
  801. }
  802. });
  803. throw new ManagedError(`cannot create path for ${entry_uid}`);
  804. }
  805. if ( ! result || ! result[0] ) {
  806. errors.report('id2path.select', {
  807. alarm: true,
  808. message: `no result for ${entry_uid}`,
  809. extra: {
  810. entry_uid,
  811. }
  812. });
  813. throw new ManagedError(`cannot create path for ${entry_uid}`);
  814. }
  815. return '/' + result[0].path;
  816. })
  817. }
  818. /**
  819. *
  820. * @param {string} glob
  821. * @param {object} user
  822. * @returns
  823. */
  824. async function resolve_glob(glob, user){
  825. //turn glob into abs path
  826. glob = _path.resolve('/', glob)
  827. //get base of glob
  828. const base = micromatch.scan(glob).base
  829. //estimate needed depth
  830. let depth = 1
  831. const dirs = glob.split('/')
  832. for(let i=0; i< dirs.length; i++){
  833. if(dirs[i].includes('**')){
  834. depth = undefined
  835. break
  836. }else{
  837. depth++
  838. }
  839. }
  840. const descendants = await get_descendants(base, user, depth)
  841. return descendants.filter((fsentry) => {
  842. return fsentry.path && micromatch.isMatch(fsentry.path, glob)
  843. })
  844. }
  845. /**
  846. * Copies a FSEntry represented by `source_path` to `dest_path`.
  847. *
  848. * @param {string} source_path
  849. * @param {string} dest_path
  850. * @param {object} user
  851. * @returns
  852. */
  853. function cp(source_path, dest_path, user, overwrite, change_name, check_perms = true){
  854. throw new Error(`legacy copy function called`);
  855. }
  856. function isString(variable) {
  857. return typeof variable === 'string' || variable instanceof String;
  858. }
  859. // checks to see if given variable is an object
  860. function isObject(variable) {
  861. return variable !== null && typeof variable === 'object';
  862. }
  863. /**
  864. * Recusrively deletes all files under `path`
  865. *
  866. * @param {string} source_path
  867. * @param {object} user
  868. * @returns
  869. */
  870. function rm(source_path, user, descendants_only = false){
  871. throw new Error(`legacy remove function called`);
  872. }
  873. const body_parser_error_handler = (err, req, res, next) => {
  874. if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
  875. return res.status(400).send(err); // Bad request
  876. }
  877. next();
  878. }
  879. async function is_ancestor_of(ancestor_uid, descendant_uid){
  880. /** @type BaseDatabaseAccessService */
  881. const db = services.get('database').get(DB_READ, 'filesystem');
  882. // root is an ancestor to all FSEntries
  883. if(ancestor_uid === null)
  884. return true;
  885. // root is never a descendant to any FSEntries
  886. if(descendant_uid === null)
  887. return false;
  888. if ( typeof ancestor_uid === 'number' ) {
  889. ancestor_uid = await id2uuid(ancestor_uid);
  890. }
  891. if ( typeof descendant_uid === 'number' ) {
  892. descendant_uid = await id2uuid(descendant_uid);
  893. }
  894. let parent = await db.read("SELECT `uuid`, `parent_uid` FROM `fsentries` WHERE `uuid` = ? LIMIT 1", [descendant_uid]);
  895. if(parent[0] === undefined)
  896. parent = await db.pread("SELECT `uuid`, `parent_uid` FROM `fsentries` WHERE `uuid` = ? LIMIT 1", [descendant_uid]);
  897. if(parent[0].uuid === ancestor_uid || parent[0].parent_uid === ancestor_uid){
  898. return true;
  899. }
  900. // keep checking as long as parent of parent is not root
  901. while(parent[0].parent_uid !== null){
  902. parent = await db.read("SELECT `uuid`, `parent_uid` FROM `fsentries` WHERE `uuid` = ? LIMIT 1", [parent[0].parent_uid]);
  903. if(parent[0] === undefined) {
  904. parent = await db.pread("SELECT `uuid`, `parent_uid` FROM `fsentries` WHERE `uuid` = ? LIMIT 1", [descendant_uid]);
  905. }
  906. if(parent[0].uuid === ancestor_uid || parent[0].parent_uid === ancestor_uid){
  907. return true;
  908. }
  909. }
  910. return false;
  911. }
  912. async function sign_file(fsentry, action){
  913. const sha256 = require('js-sha256').sha256;
  914. // fsentry not found
  915. if(fsentry === false){
  916. throw {message: 'No entry found with this uid'};
  917. }
  918. const uid = fsentry.uuid ?? (fsentry.uid ?? fsentry._id);
  919. const ttl = 9999999999999;
  920. const secret = config.url_signature_secret;
  921. const expires = Math.ceil(Date.now() / 1000) + ttl;
  922. const signature = sha256(`${uid}/${action}/${secret}/${expires}`);
  923. const contentType = mime.contentType(fsentry.name);
  924. // return
  925. return {
  926. uid: uid,
  927. expires: expires,
  928. signature: signature,
  929. url: `${config.api_base_url}/file?uid=${uid}&expires=${expires}&signature=${signature}`,
  930. read_url: `${config.api_base_url}/file?uid=${uid}&expires=${expires}&signature=${signature}`,
  931. write_url: `${config.api_base_url}/writeFile?uid=${uid}&expires=${expires}&signature=${signature}`,
  932. metadata_url: `${config.api_base_url}/itemMetadata?uid=${uid}&expires=${expires}&signature=${signature}`,
  933. fsentry_type: contentType,
  934. fsentry_is_dir: !! fsentry.is_dir,
  935. fsentry_name: fsentry.name,
  936. fsentry_size: fsentry.size,
  937. fsentry_accessed: fsentry.accessed,
  938. fsentry_modified: fsentry.modified,
  939. fsentry_created: fsentry.created,
  940. }
  941. }
  942. async function gen_public_token(file_uuid, ttl = 24 * 60 * 60){
  943. const { v4: uuidv4 } = require('uuid');
  944. // get fsentry
  945. let fsentry = await uuid2fsentry(file_uuid);
  946. // fsentry not found
  947. if(fsentry === false){
  948. throw {message: 'No entry found with this uid'};
  949. }
  950. const uid = fsentry.uuid;
  951. const expires = Math.ceil(Date.now() / 1000) + ttl;
  952. const token = uuidv4();
  953. const contentType = mime.contentType(fsentry.name);
  954. /** @type BaseDatabaseAccessService */
  955. const db = services.get('database').get(DB_WRITE, 'filesystem');
  956. // insert into DB
  957. try{
  958. await db.write(
  959. `UPDATE fsentries SET public_token = ? WHERE id = ?`,
  960. [
  961. //token
  962. token,
  963. //fsentry_id
  964. fsentry.id,
  965. ]);
  966. }catch(e){
  967. console.log(e);
  968. return false;
  969. }
  970. // return
  971. return {
  972. uid: uid,
  973. token: token,
  974. url: `${config.api_base_url}/pubfile?token=${token}`,
  975. fsentry_type: contentType,
  976. fsentry_is_dir: fsentry.is_dir,
  977. fsentry_name: fsentry.name,
  978. }
  979. }
  980. async function deleteUser(user_id){
  981. console.log('THIS IS deleteUser ---');
  982. /** @type BaseDatabaseAccessService */
  983. const db = services.get('database').get(DB_READ, 'filesystem');
  984. // get a list of all files owned by this user
  985. let files = await db.read(
  986. `SELECT uuid, bucket, bucket_region FROM fsentries WHERE user_id = ? AND is_dir = 0`,
  987. [user_id]
  988. );
  989. // delete all files from S3
  990. if(files !== null && files.length > 0){
  991. for(let i=0; i<files.length; i++){
  992. // init S3 SDK
  993. const svc_fs = Context.get('services').get('filesystem');
  994. const storage = Context.get('storage');
  995. const op_delete = storage.create_delete();
  996. await op_delete.run({
  997. node: await svc_fs.node(new NodeUIDSelector(files[i].uuid))
  998. });
  999. }
  1000. }
  1001. // delete all fsentries from DB
  1002. await db.write(`DELETE FROM fsentries WHERE user_id = ?`,[user_id]);
  1003. // delete user
  1004. await db.write(`DELETE FROM user WHERE id = ?`,[user_id]);
  1005. }
  1006. function subdomain(req){
  1007. if ( config.experimental_no_subdomain ) return 'api';
  1008. return req.hostname.slice(0, -1 * (config.domain.length + 1));
  1009. }
  1010. async function jwt_auth(req){
  1011. let token;
  1012. // HTTML Auth header
  1013. if(req.header && req.header('Authorization'))
  1014. token = req.header('Authorization');
  1015. // Cookie
  1016. else if(req.cookies && req.cookies[config.cookie_name])
  1017. token = req.cookies[config.cookie_name];
  1018. // Auth token in URL
  1019. else if(req.query && req.query.auth_token)
  1020. token = req.query.auth_token;
  1021. // Socket
  1022. else if(req.handshake && req.handshake.auth && req.handshake.auth.auth_token)
  1023. token = req.handshake.auth.auth_token;
  1024. if(!token || token === 'null')
  1025. throw('No auth token found');
  1026. else if (typeof token !== 'string')
  1027. throw('token must be a string.')
  1028. else
  1029. token = token.replace('Bearer ', '')
  1030. try{
  1031. const svc_auth = Context.get('services').get('auth');
  1032. const actor = await svc_auth.authenticate_from_token(token);
  1033. if ( ! actor.type?.constructor?.name === 'UserActorType' ) {
  1034. throw({
  1035. message: APIError.create('token_unsupported')
  1036. .serialize(),
  1037. });
  1038. }
  1039. return {
  1040. user: actor.type.user,
  1041. token: token,
  1042. };
  1043. }catch(e){
  1044. console.log('ERROR', e);
  1045. throw(e.message);
  1046. }
  1047. }
  1048. /**
  1049. * returns all ancestors of an fsentry
  1050. *
  1051. * @param {*} fsentry_id
  1052. */
  1053. async function ancestors(fsentry_id){
  1054. /** @type BaseDatabaseAccessService */
  1055. const db = services.get('database').get(DB_READ, 'filesystem');
  1056. const ancestors = [];
  1057. // first parent
  1058. let parent = await db.read("SELECT * FROM `fsentries` WHERE `id` = ? LIMIT 1", [fsentry_id]);
  1059. if(parent.length === 0){
  1060. return ancestors;
  1061. }
  1062. // get all subsequent parents
  1063. while(parent[0].parent_uid !== null){
  1064. const parent_fsentry = await uuid2fsentry(parent[0].parent_uid);
  1065. parent = await db.read("SELECT * FROM `fsentries` WHERE `id` = ? LIMIT 1", [parent_fsentry.id]);
  1066. if(parent[0].length !== 0){
  1067. ancestors.push(parent[0])
  1068. }
  1069. }
  1070. return ancestors;
  1071. }
  1072. // THIS LEGACY FUNCTION IS STILL IN USE
  1073. // by: generate_system_fsentries
  1074. // TODO: migrate generate_system_fsentries to use QuickMkdir
  1075. async function mkdir(options){
  1076. const fs = systemfs;
  1077. const dirpath = _path.dirname(_path.resolve('/', options.path));
  1078. let target_name = _path.basename(_path.resolve('/', options.path));
  1079. const overwrite = options.overwrite ?? false;
  1080. const dedupe_name = options.dedupe_name ?? false;
  1081. const immutable = options.immutable ?? false;
  1082. const return_id = options.return_id ?? false;
  1083. const no_perm_check = options.no_perm_check ?? false;
  1084. // make parent directories as needed
  1085. const create_missing_parents = options.create_missing_parents ?? false;
  1086. // hold a list of all parent directories created in the process
  1087. let parent_dirs_created = [];
  1088. let overwritten_uid;
  1089. // target_name validation
  1090. try{
  1091. validate_fsentry_name(target_name)
  1092. }catch(e){
  1093. throw e.message;
  1094. }
  1095. // resolve dirpath to its fsentry
  1096. let parent = await convert_path_to_fsentry(dirpath);
  1097. // dirpath not found
  1098. if(parent === false && !create_missing_parents)
  1099. throw "Target path not found";
  1100. // create missing parent directories
  1101. else if(parent === false && create_missing_parents){
  1102. const dirs = _path.resolve('/', dirpath).split('/');
  1103. let cur_path = '';
  1104. for(let j=0; j < dirs.length; j++){
  1105. if(dirs[j] === '')
  1106. continue;
  1107. cur_path += '/'+dirs[j];
  1108. // skip creating '/[username]'
  1109. if(j === 1)
  1110. continue;
  1111. try{
  1112. let d = await mkdir(fs, {path: cur_path, user: options.user});
  1113. d.path = cur_path;
  1114. parent_dirs_created.push(d);
  1115. }catch(e){
  1116. console.log(`Skipped mkdir ${cur_path}`);
  1117. }
  1118. }
  1119. // try setting parent again
  1120. parent = await convert_path_to_fsentry(dirpath);
  1121. if(parent === false)
  1122. throw "Target path not found";
  1123. }
  1124. // check permission
  1125. if(!no_perm_check && !await chkperm(parent, options.user.id, 'write'))
  1126. throw { code:`forbidden`, message: `permission denied.`};
  1127. // check if a fsentry with the same name exists under this path
  1128. const existing_fsentry = await convert_path_to_fsentry(_path.resolve('/', dirpath + '/' + target_name ));
  1129. /** @type BaseDatabaseAccessService */
  1130. const db = services.get('database').get(DB_WRITE, 'filesystem');
  1131. // if trying to create a directory with an existing path and overwrite==false, throw an error
  1132. if(!overwrite && !dedupe_name && existing_fsentry !== false){
  1133. throw {
  1134. code: 'path_exists',
  1135. message:"A file/directory with the same path already exists.",
  1136. entry_name: existing_fsentry.name,
  1137. existing_fsentry: {
  1138. name: existing_fsentry.name,
  1139. uid: existing_fsentry.uuid,
  1140. }
  1141. };
  1142. }
  1143. else if(overwrite && existing_fsentry){
  1144. overwritten_uid = existing_fsentry.uuid;
  1145. // check permission
  1146. if(!await chkperm(existing_fsentry, options.user.id, 'write'))
  1147. throw {code:`forbidden`, message: `permission denied.`};
  1148. // delete existing dir
  1149. await db.write(
  1150. `DELETE FROM fsentries WHERE id = ? AND user_id = ?`,
  1151. [
  1152. //parent_uid
  1153. existing_fsentry.uuid,
  1154. //user_id
  1155. options.user.id,
  1156. ]);
  1157. }
  1158. // dedupe name, generate a new name until its unique
  1159. else if(dedupe_name && existing_fsentry !== false){
  1160. for( let i = 1; ; i++){
  1161. let try_new_name = existing_fsentry.name + ' (' + i + ')';
  1162. let check_dupe = await db.read(
  1163. "SELECT `id` FROM `fsentries` WHERE `parent_uid` = ? AND name = ? LIMIT 1",
  1164. [existing_fsentry.parent_uid, try_new_name]
  1165. );
  1166. if(check_dupe[0] === undefined){
  1167. target_name = try_new_name;
  1168. break;
  1169. }
  1170. }
  1171. }
  1172. // shrotcut?
  1173. let shortcut_fsentry;
  1174. if(options.shortcut_to){
  1175. shortcut_fsentry = await uuid2fsentry(options.shortcut_to);
  1176. if(shortcut_fsentry === false){
  1177. throw ({ code:`not_found`, message: `shortcut_to not found.`})
  1178. }else if(!parent.is_dir){
  1179. throw ({ code:`not_dir`, message: `parent of shortcut_to must be a directory`})
  1180. }else if(!await chkperm(shortcut_fsentry, options.user.id, 'read')){
  1181. throw ({ code:`forbidden`, message: `shortcut_to permission denied.`})
  1182. }
  1183. }
  1184. // current epoch
  1185. const ts = Math.round(Date.now() / 1000)
  1186. const uid = uuidv4();
  1187. // record in db
  1188. let user_id = (parent === null ? options.user.id : parent.user_id);
  1189. const { insertId: mkdir_db_id } = await db.write(
  1190. `INSERT INTO fsentries
  1191. (uuid, parent_uid, user_id, name, is_dir, created, modified, immutable, shortcut_to, is_shortcut) VALUES
  1192. ( ?, ?, ?, ?, true, ?, ?, ?, ?, ?)`,
  1193. [
  1194. //uuid
  1195. uid,
  1196. //parent_uid
  1197. (parent === null) ? null : parent.uuid,
  1198. //user_id
  1199. user_id,
  1200. //name
  1201. target_name,
  1202. //created
  1203. ts,
  1204. //modified
  1205. ts,
  1206. //immutable
  1207. immutable,
  1208. //shortcut_to,
  1209. shortcut_fsentry ? shortcut_fsentry.id : null,
  1210. //is_shortcut,
  1211. shortcut_fsentry ? 1 : 0,
  1212. ]
  1213. );
  1214. const ret_obj = {
  1215. uid : uid,
  1216. name: target_name,
  1217. immutable: immutable,
  1218. is_dir: true,
  1219. path: options.path ?? false,
  1220. dirpath: dirpath,
  1221. is_shared: await is_shared_with_anyone(mkdir_db_id),
  1222. overwritten_uid: overwritten_uid,
  1223. shortcut_to: shortcut_fsentry ? shortcut_fsentry.uuid : null,
  1224. shortcut_to_path: shortcut_fsentry ? await id2path(shortcut_fsentry.id) : null,
  1225. parent_dirs_created: parent_dirs_created,
  1226. original_client_socket_id: options.original_client_socket_id,
  1227. };
  1228. // add existing_fsentry if exists
  1229. if(existing_fsentry){
  1230. ret_obj.existing_fsentry ={
  1231. name: existing_fsentry.name,
  1232. uid: existing_fsentry.uuid,
  1233. }
  1234. }
  1235. if(return_id)
  1236. ret_obj.id = mkdir_db_id;
  1237. // send realtime success msg to client
  1238. let socketio = require('./socketio.js').getio();
  1239. if(socketio){
  1240. socketio.to(user_id).emit('item.added', ret_obj)
  1241. }
  1242. return ret_obj;
  1243. }
  1244. function is_valid_uuid ( uuid ) {
  1245. let s = "" + uuid;
  1246. 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);
  1247. return !! s;
  1248. }
  1249. function is_valid_uuid4 ( uuid ) {
  1250. return is_valid_uuid(uuid);
  1251. }
  1252. function is_specifically_uuidv4 ( uuid ) {
  1253. let s = "" + uuid;
  1254. 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);
  1255. if (!s) {
  1256. return false;
  1257. }
  1258. return true;
  1259. }
  1260. function is_valid_url ( url ) {
  1261. let s = "" + url;
  1262. try {
  1263. new URL(s);
  1264. return true;
  1265. } catch (e) {
  1266. return false;
  1267. }
  1268. }
  1269. function hyphenize_confirm_code(email_confirm_code){
  1270. email_confirm_code = email_confirm_code.toString();
  1271. email_confirm_code =
  1272. email_confirm_code[0] +
  1273. email_confirm_code[1] +
  1274. email_confirm_code[2] +
  1275. '-' +
  1276. email_confirm_code[3] +
  1277. email_confirm_code[4] +
  1278. email_confirm_code[5];
  1279. return email_confirm_code;
  1280. }
  1281. async function username_exists(username){
  1282. /** @type BaseDatabaseAccessService */
  1283. const db = services.get('database').get(DB_READ, 'filesystem');
  1284. let rows = await db.read(`SELECT EXISTS(SELECT 1 FROM user WHERE username=?) AS username_exists`, [username]);
  1285. if(rows[0].username_exists)
  1286. return true;
  1287. }
  1288. async function app_name_exists(name){
  1289. /** @type BaseDatabaseAccessService */
  1290. const db = services.get('database').get(DB_READ, 'filesystem');
  1291. let rows = await db.read(`SELECT EXISTS(SELECT 1 FROM apps WHERE apps.name=?) AS app_name_exists`, [name]);
  1292. if(rows[0].app_name_exists)
  1293. return true;
  1294. }
  1295. // generates all the default files and directories a user needs,
  1296. // generally used for a brand new account
  1297. async function generate_system_fsentries(user){
  1298. /** @type BaseDatabaseAccessService */
  1299. const db = services.get('database').get(DB_WRITE, 'filesystem');
  1300. //-------------------------------------------------------------
  1301. // create root `/[username]/`
  1302. //-------------------------------------------------------------
  1303. const root_dir = await mkdir({
  1304. path: '/' + user.username,
  1305. user: user,
  1306. immutable: true,
  1307. no_perm_check: true,
  1308. return_id: true,
  1309. });
  1310. // Normally, it is recommended to use mkdir() to create new folders,
  1311. // but during signup this could result in multiple queries to the DB server
  1312. // and for servers in remote regions such as Asia this could result in a
  1313. // very long time for /signup to finish, sometimes up to 30-40 seconds!
  1314. // by combining as many queries as we can into one and avoiding multiple back-and-forth
  1315. // with the DB server, we can speed this process up significantly.
  1316. const ts = Date.now()/1000;
  1317. // Generate UUIDs for all the default folders and files
  1318. let trash_uuid = uuidv4();
  1319. let appdata_uuid = uuidv4();
  1320. let desktop_uuid = uuidv4();
  1321. let documents_uuid = uuidv4();
  1322. let pictures_uuid = uuidv4();
  1323. let videos_uuid = uuidv4();
  1324. const insert_res = await db.write(
  1325. `INSERT INTO fsentries
  1326. (uuid, parent_uid, user_id, name, path, is_dir, created, modified, immutable) VALUES
  1327. ( ?, ?, ?, ?, ?, true, ?, ?, true),
  1328. ( ?, ?, ?, ?, ?, true, ?, ?, true),
  1329. ( ?, ?, ?, ?, ?, true, ?, ?, true),
  1330. ( ?, ?, ?, ?, ?, true, ?, ?, true),
  1331. ( ?, ?, ?, ?, ?, true, ?, ?, true),
  1332. ( ?, ?, ?, ?, ?, true, ?, ?, true)
  1333. `,
  1334. [
  1335. // Trash
  1336. trash_uuid, root_dir.uid, user.id, 'Trash', `/${user.username}/Trash`, ts, ts,
  1337. // AppData
  1338. appdata_uuid, root_dir.uid, user.id, 'AppData', `/${user.username}/AppData`, ts, ts,
  1339. // Desktop
  1340. desktop_uuid, root_dir.uid, user.id, 'Desktop', `/${user.username}/Desktop`, ts, ts,
  1341. // Documents
  1342. documents_uuid, root_dir.uid, user.id, 'Documents', `/${user.username}/Documents`, ts, ts,
  1343. // Pictures
  1344. pictures_uuid, root_dir.uid, user.id, 'Pictures', `/${user.username}/Pictures`, ts, ts,
  1345. // Videos
  1346. videos_uuid, root_dir.uid, user.id, 'Videos', `/${user.username}/Videos`, ts, ts,
  1347. ]
  1348. );
  1349. // https://stackoverflow.com/a/50103616
  1350. let trash_id = insert_res.insertId;
  1351. let appdata_id = insert_res.insertId + 1;
  1352. let desktop_id = insert_res.insertId + 2;
  1353. let documents_id = insert_res.insertId + 3;
  1354. let pictures_id = insert_res.insertId + 4;
  1355. let videos_id = insert_res.insertId + 5;
  1356. // Asynchronously set the user's system folders uuids in database
  1357. // This is for caching purposes, so we don't have to query the DB every time we need to access these folders
  1358. // This is also possible because we know the user's system folders uuids will never change
  1359. // TODO: pass to IIAFE manager to avoid unhandled promise rejection
  1360. // (IIAFE manager doesn't exist yet, hence this is a TODO)
  1361. db.write(
  1362. `UPDATE user SET
  1363. trash_uuid=?, appdata_uuid=?, desktop_uuid=?, documents_uuid=?, pictures_uuid=?, videos_uuid=?,
  1364. trash_id=?, appdata_id=?, desktop_id=?, documents_id=?, pictures_id=?, videos_id=?
  1365. WHERE id=?`,
  1366. [
  1367. trash_uuid, appdata_uuid, desktop_uuid, documents_uuid, pictures_uuid, videos_uuid,
  1368. trash_id, appdata_id, desktop_id, documents_id, pictures_id, videos_id,
  1369. user.id
  1370. ]
  1371. );
  1372. invalidate_cached_user(user);
  1373. }
  1374. function send_email_verification_code(email_confirm_code, email){
  1375. const svc_email = Context.get('services').get('email');
  1376. svc_email.send_email({ email }, 'email_verification_code', {
  1377. code: hyphenize_confirm_code(email_confirm_code),
  1378. })
  1379. }
  1380. function send_email_verification_token(email_confirm_token, email, user_uuid){
  1381. const svc_email = Context.get('services').get('email');
  1382. const link = `${config.origin}/confirm-email-by-token?user_uuid=${user_uuid}&token=${email_confirm_token}`;
  1383. svc_email.send_email({ email }, 'email_verification_link', { link });
  1384. }
  1385. async function generate_random_username(){
  1386. let username;
  1387. do {
  1388. username = generate_identifier();
  1389. } while (await username_exists(username));
  1390. return username;
  1391. }
  1392. function generate_random_str(length) {
  1393. var result = '';
  1394. var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  1395. var charactersLength = characters.length;
  1396. for ( var i = 0; i < length; i++ ) {
  1397. result += characters.charAt(Math.floor(Math.random() *
  1398. charactersLength));
  1399. }
  1400. return result;
  1401. }
  1402. /**
  1403. * Converts a given number of seconds into a human-readable string format.
  1404. *
  1405. * @param {number} seconds - The number of seconds to be converted.
  1406. * @returns {string} The time represented in the format: 'X years Y days Z hours A minutes B seconds'.
  1407. * @throws {TypeError} If the `seconds` parameter is not a number.
  1408. */
  1409. function seconds_to_string(seconds) {
  1410. var numyears = Math.floor(seconds / 31536000);
  1411. var numdays = Math.floor((seconds % 31536000) / 86400);
  1412. var numhours = Math.floor(((seconds % 31536000) % 86400) / 3600);
  1413. var numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60);
  1414. var numseconds = (((seconds % 31536000) % 86400) % 3600) % 60;
  1415. return numyears + " years " + numdays + " days " + numhours + " hours " + numminutes + " minutes " + numseconds + " seconds";
  1416. }
  1417. /**
  1418. * returns a list of apps that could open the fsentry, ranked by relevance
  1419. * @param {*} fsentry
  1420. * @param {*} options
  1421. */
  1422. async function suggest_app_for_fsentry(fsentry, options){
  1423. const monitor = PerformanceMonitor.createContext("suggest_app_for_fsentry");
  1424. const suggested_apps = [];
  1425. let content_type = mime.contentType(fsentry.name);
  1426. if(content_type === null || content_type === undefined || content_type === false)
  1427. content_type = '';
  1428. // IIFE just so fsname can stay `const`
  1429. const fsname = (() => {
  1430. if ( ! fsentry.name ) {
  1431. const fs = require('fs');
  1432. fs.writeFileSync('/tmp/missing-fsentry-name.txt', JSON.stringify(fsentry, null, 2));
  1433. return 'missing-fsentry-name';
  1434. }
  1435. let fsname = fsentry.name.toLowerCase();
  1436. // We add `.directory` so that this works as a file association
  1437. if ( fsentry.is_dir ) fsname += '.directory';
  1438. return fsname;
  1439. })();
  1440. const file_extension = _path.extname(fsname).toLowerCase();
  1441. //---------------------------------------------
  1442. // Code
  1443. //---------------------------------------------
  1444. if(
  1445. fsname.endsWith('.asm') ||
  1446. fsname.endsWith('.asp') ||
  1447. fsname.endsWith('.aspx') ||
  1448. fsname.endsWith('.bash') ||
  1449. fsname.endsWith('.c') ||
  1450. fsname.endsWith('.cpp') ||
  1451. fsname.endsWith('.css') ||
  1452. fsname.endsWith('.csv') ||
  1453. fsname.endsWith('.dhtml') ||
  1454. fsname.endsWith('.f') ||
  1455. fsname.endsWith('.go') ||
  1456. fsname.endsWith('.h') ||
  1457. fsname.endsWith('.htm') ||
  1458. fsname.endsWith('.html') ||
  1459. fsname.endsWith('.html5') ||
  1460. fsname.endsWith('.java') ||
  1461. fsname.endsWith('.jl') ||
  1462. fsname.endsWith('.js') ||
  1463. fsname.endsWith('.jsa') ||
  1464. fsname.endsWith('.json') ||
  1465. fsname.endsWith('.jsonld') ||
  1466. fsname.endsWith('.jsf') ||
  1467. fsname.endsWith('.jsp') ||
  1468. fsname.endsWith('.kt') ||
  1469. fsname.endsWith('.log') ||
  1470. fsname.endsWith('.lock') ||
  1471. fsname.endsWith('.lua') ||
  1472. fsname.endsWith('.md') ||
  1473. fsname.endsWith('.perl') ||
  1474. fsname.endsWith('.phar') ||
  1475. fsname.endsWith('.php') ||
  1476. fsname.endsWith('.pl') ||
  1477. fsname.endsWith('.py') ||
  1478. fsname.endsWith('.r') ||
  1479. fsname.endsWith('.rb') ||
  1480. fsname.endsWith('.rdata') ||
  1481. fsname.endsWith('.rda') ||
  1482. fsname.endsWith('.rdf') ||
  1483. fsname.endsWith('.rds') ||
  1484. fsname.endsWith('.rs') ||
  1485. fsname.endsWith('.rlib') ||
  1486. fsname.endsWith('.rpy') ||
  1487. fsname.endsWith('.scala') ||
  1488. fsname.endsWith('.sc') ||
  1489. fsname.endsWith('.scm') ||
  1490. fsname.endsWith('.sh') ||
  1491. fsname.endsWith('.sol') ||
  1492. fsname.endsWith('.sql') ||
  1493. fsname.endsWith('.ss') ||
  1494. fsname.endsWith('.svg') ||
  1495. fsname.endsWith('.swift') ||
  1496. fsname.endsWith('.toml') ||
  1497. fsname.endsWith('.ts') ||
  1498. fsname.endsWith('.wasm') ||
  1499. fsname.endsWith('.xhtml') ||
  1500. fsname.endsWith('.xml') ||
  1501. fsname.endsWith('.yaml') ||
  1502. // files with no extension
  1503. !fsname.includes('.')
  1504. ){
  1505. suggested_apps.push(await get_app({name: 'code'}))
  1506. suggested_apps.push(await get_app({name: 'editor'}))
  1507. }
  1508. //---------------------------------------------
  1509. // Editor
  1510. //---------------------------------------------
  1511. if(
  1512. fsname.endsWith('.txt') ||
  1513. // files with no extension
  1514. !fsname.includes('.')
  1515. ){
  1516. suggested_apps.push(await get_app({name: 'editor'}))
  1517. suggested_apps.push(await get_app({name: 'code'}))
  1518. }
  1519. //---------------------------------------------
  1520. // Markus
  1521. //---------------------------------------------
  1522. if(fsname.endsWith('.md')){
  1523. suggested_apps.push(await get_app({name: 'markus'}))
  1524. }
  1525. //---------------------------------------------
  1526. // Viewer
  1527. //---------------------------------------------
  1528. if(
  1529. fsname.endsWith('.jpg') ||
  1530. fsname.endsWith('.png') ||
  1531. fsname.endsWith('.webp') ||
  1532. fsname.endsWith('.svg') ||
  1533. fsname.endsWith('.bmp') ||
  1534. fsname.endsWith('.jpeg')
  1535. ){
  1536. suggested_apps.push(await get_app({name: 'viewer'}));
  1537. }
  1538. //---------------------------------------------
  1539. // Draw
  1540. //---------------------------------------------
  1541. if(
  1542. fsname.endsWith('.bmp') ||
  1543. content_type.startsWith('image/')
  1544. ){
  1545. suggested_apps.push(await get_app({name: 'draw'}));
  1546. }
  1547. //---------------------------------------------
  1548. // PDF
  1549. //---------------------------------------------
  1550. if(fsname.endsWith('.pdf')){
  1551. suggested_apps.push(await get_app({name: 'pdf'}));
  1552. }
  1553. //---------------------------------------------
  1554. // Player
  1555. //---------------------------------------------
  1556. if(
  1557. fsname.endsWith('.mp4') ||
  1558. fsname.endsWith('.webm') ||
  1559. fsname.endsWith('.mpg') ||
  1560. fsname.endsWith('.mpv') ||
  1561. fsname.endsWith('.mp3') ||
  1562. fsname.endsWith('.m4a') ||
  1563. fsname.endsWith('.ogg')
  1564. ){
  1565. suggested_apps.push(await get_app({name: 'player'}));
  1566. }
  1567. //---------------------------------------------
  1568. // 3rd-party apps
  1569. //---------------------------------------------
  1570. const apps = kv.get(`assocs:${file_extension.slice(1)}:apps`)
  1571. monitor.label("third party associations");
  1572. if(apps && apps.length > 0){
  1573. for (let index = 0; index < apps.length; index++) {
  1574. // retrieve app from DB
  1575. const third_party_app = await get_app({id: apps[index]})
  1576. if ( ! third_party_app ) continue;
  1577. // only add if the app is approved for opening items or the app is owned by this user
  1578. if( third_party_app.approved_for_opening_items ||
  1579. (options !== undefined && options.user !== undefined && options.user.id === third_party_app.owner_user_id))
  1580. suggested_apps.push(third_party_app)
  1581. }
  1582. }
  1583. monitor.stamp();
  1584. monitor.end();
  1585. // return list
  1586. return suggested_apps;
  1587. }
  1588. function build_item_object(item){
  1589. }
  1590. async function get_taskbar_items(user) {
  1591. /** @type BaseDatabaseAccessService */
  1592. const db = services.get('database').get(DB_WRITE, 'filesystem');
  1593. let taskbar_items_from_db = [];
  1594. // If taskbar items don't exist (specifically NULL)
  1595. // add default apps.
  1596. if(!user.taskbar_items){
  1597. taskbar_items_from_db = [
  1598. {name: 'editor', type: 'app'},
  1599. {name: 'dev-center', type: 'app'},
  1600. {name: 'draw', type: 'app'},
  1601. {name: 'code', type: 'app'},
  1602. {name: 'camera', type: 'app'},
  1603. {name: 'recorder', type: 'app'},
  1604. {name: 'terminal', type: 'app'},
  1605. {name: 'about', type: 'app'},
  1606. ];
  1607. await db.write(
  1608. `UPDATE user SET taskbar_items = ? WHERE id = ?`,
  1609. [
  1610. JSON.stringify(taskbar_items_from_db),
  1611. user.id,
  1612. ]
  1613. );
  1614. invalidate_cached_user(user);
  1615. }
  1616. // there are items from before
  1617. else{
  1618. try {
  1619. taskbar_items_from_db = JSON.parse(user.taskbar_items);
  1620. }catch(e){
  1621. // ignore errors
  1622. }
  1623. }
  1624. // get apps that these taskbar items represent
  1625. let taskbar_items = [];
  1626. for (let index = 0; index < taskbar_items_from_db.length; index++) {
  1627. const taskbar_item_from_db = taskbar_items_from_db[index];
  1628. if(taskbar_item_from_db.type === 'app' && taskbar_item_from_db.name !== 'explorer'){
  1629. let item = {};
  1630. if(taskbar_item_from_db.name)
  1631. item = await get_app({name: taskbar_item_from_db.name});
  1632. else if(taskbar_item_from_db.id)
  1633. item = await get_app({id: taskbar_item_from_db.id});
  1634. else if(taskbar_item_from_db.uid)
  1635. item = await get_app({uid: taskbar_item_from_db.uid});
  1636. // if item not found, skip it
  1637. if(!item) continue;
  1638. // delete sensitive attributes
  1639. delete item.id;
  1640. delete item.owner_user_id;
  1641. delete item.timestamp;
  1642. // delete item.godmode;
  1643. delete item.approved_for_listing;
  1644. delete item.approved_for_opening_items;
  1645. // add to final object
  1646. taskbar_items.push(item)
  1647. }
  1648. }
  1649. return taskbar_items;
  1650. }
  1651. function validate_signature_auth(url, action) {
  1652. const query = new URL(url).searchParams;
  1653. if(!query.get('uid'))
  1654. throw {message: '`uid` is required for signature-based authentication.'}
  1655. else if(!action)
  1656. throw {message: '`action` is required for signature-based authentication.'}
  1657. else if(!query.get('expires'))
  1658. throw {message: '`expires` is required for signature-based authentication.'}
  1659. else if(!query.get('signature'))
  1660. throw {message: '`signature` is required for signature-based authentication.'}
  1661. const expired = query.get('expires') && (query.get('expires') < Date.now() / 1000);
  1662. // expired?
  1663. if(expired)
  1664. throw {message: 'Authentication failed. Signature expired.'}
  1665. const uid = query.get('uid');
  1666. const secret = config.url_signature_secret;
  1667. const sha256 = require('js-sha256').sha256;
  1668. // before doing anything, see if this signature is valid for 'write' action, if yes that means every action is allowed
  1669. if(!expired && query.get('signature') === sha256(`${uid}/write/${secret}/${query.get('expires')}`))
  1670. return true;
  1671. // if not, check specific actions
  1672. else if(!expired && query.get('signature') === sha256(`${uid}/${action}/${secret}/${query.get('expires')}`))
  1673. return true;
  1674. // auth failed
  1675. else
  1676. throw {message: 'Authentication failed'}
  1677. }
  1678. function get_url_from_req(req) {
  1679. return req.protocol + '://' + req.get('host') + req.originalUrl;
  1680. }
  1681. async function mv(options){
  1682. throw new Error('legacy mv function called');
  1683. }
  1684. /**
  1685. * Formats a number with grouped thousands.
  1686. *
  1687. * @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).
  1688. * @param {number} decimals - The number of decimal points. If a non-finite number is provided, it defaults to 0.
  1689. * @param {string} [dec_point='.'] - The character used for the decimal point. Defaults to '.' if not provided.
  1690. * @param {string} [thousands_sep=','] - The character used for the thousands separator. Defaults to ',' if not provided.
  1691. * @returns {string} The formatted number with grouped thousands, using the specified decimal point and thousands separator characters.
  1692. * @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.
  1693. */
  1694. function number_format (number, decimals, dec_point, thousands_sep) {
  1695. // Strip all characters but numerical ones.
  1696. number = (number + '').replace(/[^0-9+\-Ee.]/g, '');
  1697. var n = !isFinite(+number) ? 0 : +number,
  1698. prec = !isFinite(+decimals) ? 0 : Math.abs(decimals),
  1699. sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep,
  1700. dec = (typeof dec_point === 'undefined') ? '.' : dec_point,
  1701. s = '',
  1702. toFixedFix = function (n, prec) {
  1703. var k = Math.pow(10, prec);
  1704. return '' + Math.round(n * k) / k;
  1705. };
  1706. // Fix for IE parseFloat(0.55).toFixed(0) = 0;
  1707. s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
  1708. if (s[0].length > 3) {
  1709. s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep);
  1710. }
  1711. if ((s[1] || '').length < prec) {
  1712. s[1] = s[1] || '';
  1713. s[1] += new Array(prec - s[1].length + 1).join('0');
  1714. }
  1715. return s.join(dec);
  1716. }
  1717. module.exports = {
  1718. ancestors,
  1719. app_name_exists,
  1720. app_exists,
  1721. body_parser_error_handler,
  1722. build_item_object,
  1723. byte_format,
  1724. change_username,
  1725. chkperm,
  1726. convert_path_to_fsentry,
  1727. cp,
  1728. deleteUser,
  1729. get_descendants,
  1730. get_dir_size,
  1731. gen_public_token,
  1732. get_taskbar_items,
  1733. get_url_from_req,
  1734. generate_system_fsentries,
  1735. generate_random_str,
  1736. generate_random_username,
  1737. get_app,
  1738. get_user,
  1739. invalidate_cached_user,
  1740. invalidate_cached_user_by_id,
  1741. has_shared_with,
  1742. hyphenize_confirm_code,
  1743. id2fsentry,
  1744. id2path,
  1745. id2uuid,
  1746. is_ancestor_of,
  1747. is_empty,
  1748. is_shared_with,
  1749. is_shared_with_anyone,
  1750. is_valid_uuid4,
  1751. is_valid_uuid,
  1752. is_specifically_uuidv4,
  1753. is_valid_url,
  1754. jwt_auth,
  1755. mkdir,
  1756. mv,
  1757. number_format,
  1758. refresh_apps_cache,
  1759. refresh_associations_cache,
  1760. resolve_glob,
  1761. rm,
  1762. seconds_to_string,
  1763. send_email_verification_code,
  1764. send_email_verification_token,
  1765. sign_file,
  1766. subdomain,
  1767. suggest_app_for_fsentry,
  1768. df,
  1769. username_exists,
  1770. uuid2fsentry,
  1771. validate_fsentry_name,
  1772. validate_signature_auth,
  1773. tmp_provide_services,
  1774. };