helpers.js 68 KB

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