mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-26 07:57:10 +00:00
perf: batch readdir suggested apps into single big query (#2332)
* perf: batch readdir suggested apps into single big query * fix: cleanup fucntions to go through single entry
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const { get_user, id2path, id2uuid, is_empty, suggest_app_for_fsentry, get_app } = require('../helpers');
|
||||
const { get_user, id2path, id2uuid, is_empty, suggestedAppForFsEntry, get_app } = require('../helpers');
|
||||
|
||||
const putility = require('@heyputer/putility');
|
||||
const config = require('../config');
|
||||
@@ -548,6 +548,7 @@ module.exports = class FSNodeContext {
|
||||
return this.entry.size;
|
||||
}
|
||||
|
||||
/** Avoid using if fetching directory items */
|
||||
async fetchSuggestedApps (user, force) {
|
||||
if ( this.entry.suggested_apps && !force ) return;
|
||||
|
||||
@@ -555,7 +556,7 @@ module.exports = class FSNodeContext {
|
||||
if ( ! this.entry ) return;
|
||||
|
||||
this.entry.suggested_apps =
|
||||
await suggest_app_for_fsentry(this.entry, { user });
|
||||
await suggestedAppForFsEntry(this.entry, { user });
|
||||
}
|
||||
|
||||
async fetchIsEmpty () {
|
||||
|
||||
@@ -19,13 +19,15 @@
|
||||
const APIError = require('../../api/APIError');
|
||||
const { Context } = require('../../util/context');
|
||||
const { stream_to_buffer } = require('../../util/streamutil');
|
||||
const { get_apps } = require('../../helpers');
|
||||
const { get_apps, suggestedAppsForFsEntries } = require('../../helpers');
|
||||
const { ECMAP } = require('../ECMAP');
|
||||
const { TYPE_DIRECTORY, TYPE_SYMLINK } = require('../FSNodeContext');
|
||||
const { LLListUsers } = require('../ll_operations/ll_listusers');
|
||||
const { LLReadDir } = require('../ll_operations/ll_readdir');
|
||||
const { LLReadShares } = require('../ll_operations/ll_readshares');
|
||||
const { HLFilesystemOperation } = require('./definitions');
|
||||
const { DB_READ } = require('../../services/database/consts');
|
||||
const config = require('../../config');
|
||||
|
||||
class HLReadDir extends HLFilesystemOperation {
|
||||
static CONCERN = 'filesystem';
|
||||
@@ -117,13 +119,14 @@ class HLReadDir extends HLFilesystemOperation {
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! no_assocs ) {
|
||||
await Promise.all([
|
||||
this.#batchFetchSuggestedApps(children, user),
|
||||
this.#batchFetchSubdomains(children, user),
|
||||
]);
|
||||
}
|
||||
|
||||
return Promise.all(children.map(async child => {
|
||||
if ( ! no_assocs ) {
|
||||
await Promise.all([
|
||||
child.fetchSuggestedApps(user),
|
||||
child.fetchSubdomains(user),
|
||||
]);
|
||||
}
|
||||
const entry = await child.getSafeEntry();
|
||||
if ( !no_thumbs && entry.associated_app ) {
|
||||
const svc_appIcon = this.context.get('services').get('app-icon');
|
||||
@@ -151,6 +154,59 @@ class HLReadDir extends HLFilesystemOperation {
|
||||
return entry;
|
||||
}));
|
||||
}
|
||||
|
||||
async #batchFetchSubdomains (children, user) {
|
||||
const dirChildren = [];
|
||||
const childById = new Map();
|
||||
|
||||
for ( const child of children ) {
|
||||
const entry = child.entry;
|
||||
if ( ! entry?.is_dir ) continue;
|
||||
entry.subdomains = [];
|
||||
if ( entry.id == null ) continue;
|
||||
dirChildren.push(entry.id);
|
||||
childById.set(entry.id, child);
|
||||
}
|
||||
|
||||
if ( dirChildren.length === 0 ) return;
|
||||
|
||||
const placeholders = dirChildren.map(() => '?').join(',');
|
||||
const db = this.context.get('services').get('database').get(DB_READ, 'filesystem');
|
||||
const rows = await db.read(`SELECT root_dir_id, subdomain, uuid
|
||||
FROM subdomains
|
||||
WHERE root_dir_id IN (${placeholders}) AND user_id = ?`,
|
||||
[...dirChildren, user.id]);
|
||||
|
||||
for ( const row of rows ) {
|
||||
const child = childById.get(row.root_dir_id);
|
||||
if ( ! child ) continue;
|
||||
child.entry.subdomains.push({
|
||||
subdomain: row.subdomain,
|
||||
address: `${config.protocol }://${ row.subdomain }.puter.site`,
|
||||
uuid: row.uuid,
|
||||
});
|
||||
child.entry.has_website = true;
|
||||
}
|
||||
}
|
||||
|
||||
async #batchFetchSuggestedApps (children, user) {
|
||||
const entries = [];
|
||||
const targets = [];
|
||||
|
||||
for ( const child of children ) {
|
||||
const entry = child.entry;
|
||||
if ( !entry || entry.suggested_apps ) continue;
|
||||
entries.push(entry);
|
||||
targets.push(entry);
|
||||
}
|
||||
|
||||
if ( entries.length === 0 ) return;
|
||||
|
||||
const suggestedLists = await suggestedAppsForFsEntries(entries, { user });
|
||||
for ( let index = 0; index < targets.length; index++ ) {
|
||||
targets[index].suggested_apps = suggestedLists[index] ?? [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
+334
-256
@@ -359,198 +359,207 @@ async function get_app (options) {
|
||||
* @param {Object} [options]
|
||||
* @returns {Promise<Array<object|null>>}
|
||||
*/
|
||||
async function get_apps (specifiers, options = {}) {
|
||||
if ( ! Array.isArray(specifiers) ) {
|
||||
specifiers = [specifiers];
|
||||
}
|
||||
const get_apps = spanify('get_apps', async (specifiers, options = {}) => {
|
||||
const start = Date.now();
|
||||
console.log('Entering Get apps at: ', start);
|
||||
try {
|
||||
if ( ! Array.isArray(specifiers) ) {
|
||||
specifiers = [specifiers];
|
||||
}
|
||||
|
||||
const cacheApp = (app) => {
|
||||
if ( ! app ) return;
|
||||
app = { ...app };
|
||||
kv.set(`apps:uid:${app.uid}`, app, { EX: 30 });
|
||||
kv.set(`apps:name:${app.name}`, app, { EX: 30 });
|
||||
kv.set(`apps:id:${app.id}`, app, { EX: 30 });
|
||||
};
|
||||
const cacheApp = (app) => {
|
||||
if ( ! app ) return;
|
||||
app = { ...app };
|
||||
kv.set(`apps:uid:${app.uid}`, app, { EX: 30 });
|
||||
kv.set(`apps:name:${app.name}`, app, { EX: 30 });
|
||||
kv.set(`apps:id:${app.id}`, app, { EX: 30 });
|
||||
};
|
||||
|
||||
const normalized = specifiers.map(spec => spec ? { ...spec } : {});
|
||||
const normalized = specifiers.map(spec => spec ? { ...spec } : {});
|
||||
|
||||
if ( options.follow_old_names ) {
|
||||
const svc_oldAppName = _servicesHolder.services.get('old-app-name');
|
||||
for ( const spec of normalized ) {
|
||||
if ( spec.uid || !spec.name ) continue;
|
||||
const old_name = await svc_oldAppName.check_app_name(spec.name);
|
||||
if ( old_name ) {
|
||||
spec.uid = old_name.app_uid;
|
||||
delete spec.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const appByUid = new Map();
|
||||
const appByName = new Map();
|
||||
const appById = new Map();
|
||||
|
||||
const addApp = (app) => {
|
||||
if ( ! app ) return;
|
||||
appByUid.set(app.uid, app);
|
||||
appByName.set(app.name, app);
|
||||
appById.set(app.id, app);
|
||||
};
|
||||
|
||||
const pendingLookups = new Map();
|
||||
const pendingToResolve = new Map();
|
||||
const queryUids = new Set();
|
||||
const queryNames = new Set();
|
||||
const queryIds = new Set();
|
||||
|
||||
const queueMissing = (type, value) => {
|
||||
const queryKey = `${type}:${value}`;
|
||||
if ( pendingToResolve.has(queryKey) || pendingLookups.has(queryKey) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingKey = `pending_app:${queryKey}`;
|
||||
const pending = kv.get(pendingKey);
|
||||
if ( pending ) {
|
||||
pendingLookups.set(queryKey, pending);
|
||||
return;
|
||||
}
|
||||
|
||||
let resolveQuery;
|
||||
let rejectQuery;
|
||||
const queryPromise = new Promise((resolve, reject) => {
|
||||
resolveQuery = resolve;
|
||||
rejectQuery = reject;
|
||||
});
|
||||
kv.set(pendingKey, queryPromise, { EX: PENDING_QUERY_TTL });
|
||||
pendingToResolve.set(queryKey, { resolveQuery, rejectQuery, pendingKey });
|
||||
|
||||
if ( type === 'uid' ) {
|
||||
queryUids.add(value);
|
||||
} else if ( type === 'name' ) {
|
||||
queryNames.add(value);
|
||||
} else if ( type === 'id' ) {
|
||||
queryIds.add(value);
|
||||
}
|
||||
};
|
||||
|
||||
if ( options.follow_old_names ) {
|
||||
const svc_oldAppName = _servicesHolder.services.get('old-app-name');
|
||||
for ( const spec of normalized ) {
|
||||
if ( spec.uid || !spec.name ) continue;
|
||||
const old_name = await svc_oldAppName.check_app_name(spec.name);
|
||||
if ( old_name ) {
|
||||
spec.uid = old_name.app_uid;
|
||||
delete spec.name;
|
||||
if ( spec.uid ) {
|
||||
const cached = kv.get(`apps:uid:${spec.uid}`);
|
||||
if ( cached ) {
|
||||
addApp(cached);
|
||||
} else {
|
||||
queueMissing('uid', spec.uid);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ( spec.name ) {
|
||||
const cached = kv.get(`apps:name:${spec.name}`);
|
||||
if ( cached ) {
|
||||
addApp(cached);
|
||||
} else {
|
||||
queueMissing('name', spec.name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ( spec.id ) {
|
||||
const cached = kv.get(`apps:id:${spec.id}`);
|
||||
if ( cached ) {
|
||||
addApp(cached);
|
||||
} else {
|
||||
queueMissing('id', spec.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const appByUid = new Map();
|
||||
const appByName = new Map();
|
||||
const appById = new Map();
|
||||
const pendingResultsPromise = pendingLookups.size
|
||||
? Promise.all(Array.from(pendingLookups.values()))
|
||||
: Promise.resolve([]);
|
||||
|
||||
const addApp = (app) => {
|
||||
if ( ! app ) return;
|
||||
appByUid.set(app.uid, app);
|
||||
appByName.set(app.name, app);
|
||||
appById.set(app.id, app);
|
||||
};
|
||||
|
||||
const pendingLookups = new Map();
|
||||
const pendingToResolve = new Map();
|
||||
const queryUids = new Set();
|
||||
const queryNames = new Set();
|
||||
const queryIds = new Set();
|
||||
|
||||
const queueMissing = (type, value) => {
|
||||
const queryKey = `${type}:${value}`;
|
||||
if ( pendingToResolve.has(queryKey) || pendingLookups.has(queryKey) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingKey = `pending_app:${queryKey}`;
|
||||
const pending = kv.get(pendingKey);
|
||||
if ( pending ) {
|
||||
pendingLookups.set(queryKey, pending);
|
||||
return;
|
||||
}
|
||||
|
||||
let resolveQuery;
|
||||
let rejectQuery;
|
||||
const queryPromise = new Promise((resolve, reject) => {
|
||||
resolveQuery = resolve;
|
||||
rejectQuery = reject;
|
||||
});
|
||||
kv.set(pendingKey, queryPromise, { EX: PENDING_QUERY_TTL });
|
||||
pendingToResolve.set(queryKey, { resolveQuery, rejectQuery, pendingKey });
|
||||
|
||||
if ( type === 'uid' ) {
|
||||
queryUids.add(value);
|
||||
} else if ( type === 'name' ) {
|
||||
queryNames.add(value);
|
||||
} else if ( type === 'id' ) {
|
||||
queryIds.add(value);
|
||||
}
|
||||
};
|
||||
|
||||
for ( const spec of normalized ) {
|
||||
if ( spec.uid ) {
|
||||
const cached = kv.get(`apps:uid:${spec.uid}`);
|
||||
if ( cached ) {
|
||||
addApp(cached);
|
||||
} else {
|
||||
queueMissing('uid', spec.uid);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ( spec.name ) {
|
||||
const cached = kv.get(`apps:name:${spec.name}`);
|
||||
if ( cached ) {
|
||||
addApp(cached);
|
||||
} else {
|
||||
queueMissing('name', spec.name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ( spec.id ) {
|
||||
const cached = kv.get(`apps:id:${spec.id}`);
|
||||
if ( cached ) {
|
||||
addApp(cached);
|
||||
} else {
|
||||
queueMissing('id', spec.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pendingResultsPromise = pendingLookups.size
|
||||
? Promise.all(Array.from(pendingLookups.values()))
|
||||
: Promise.resolve([]);
|
||||
|
||||
if ( queryUids.size || queryNames.size || queryIds.size ) {
|
||||
if ( queryUids.size || queryNames.size || queryIds.size ) {
|
||||
/** @type BaseDatabaseAccessService */
|
||||
const db = _servicesHolder.services.get('database').get(DB_READ, 'apps');
|
||||
const db = _servicesHolder.services.get('database').get(DB_READ, 'apps');
|
||||
|
||||
const clauses = [];
|
||||
const params = [];
|
||||
const clauses = [];
|
||||
const params = [];
|
||||
|
||||
if ( queryUids.size ) {
|
||||
const uids = Array.from(queryUids);
|
||||
clauses.push(`uid IN (${uids.map(() => '?').join(', ')})`);
|
||||
params.push(...uids);
|
||||
}
|
||||
if ( queryNames.size ) {
|
||||
const names = Array.from(queryNames);
|
||||
clauses.push(`name IN (${names.map(() => '?').join(', ')})`);
|
||||
params.push(...names);
|
||||
}
|
||||
if ( queryIds.size ) {
|
||||
const ids = Array.from(queryIds);
|
||||
clauses.push(`id IN (${ids.map(() => '?').join(', ')})`);
|
||||
params.push(...ids);
|
||||
}
|
||||
if ( queryUids.size ) {
|
||||
const uids = Array.from(queryUids);
|
||||
clauses.push(`uid IN (${uids.map(() => '?').join(', ')})`);
|
||||
params.push(...uids);
|
||||
}
|
||||
if ( queryNames.size ) {
|
||||
const names = Array.from(queryNames);
|
||||
clauses.push(`name IN (${names.map(() => '?').join(', ')})`);
|
||||
params.push(...names);
|
||||
}
|
||||
if ( queryIds.size ) {
|
||||
const ids = Array.from(queryIds);
|
||||
clauses.push(`id IN (${ids.map(() => '?').join(', ')})`);
|
||||
params.push(...ids);
|
||||
}
|
||||
|
||||
let rows = [];
|
||||
const resolvedKeys = new Set();
|
||||
try {
|
||||
rows = await db.read(`SELECT * FROM \`apps\` WHERE ${clauses.join(' OR ')}`,
|
||||
params);
|
||||
for ( const app of rows ) {
|
||||
cacheApp(app);
|
||||
addApp(app);
|
||||
let rows = [];
|
||||
const resolvedKeys = new Set();
|
||||
try {
|
||||
rows = await db.read(`SELECT * FROM \`apps\` WHERE ${clauses.join(' OR ')}`,
|
||||
params);
|
||||
for ( const app of rows ) {
|
||||
cacheApp(app);
|
||||
addApp(app);
|
||||
|
||||
const uidKey = `uid:${app.uid}`;
|
||||
const nameKey = `name:${app.name}`;
|
||||
const idKey = `id:${app.id}`;
|
||||
const uidKey = `uid:${app.uid}`;
|
||||
const nameKey = `name:${app.name}`;
|
||||
const idKey = `id:${app.id}`;
|
||||
|
||||
if ( pendingToResolve.has(uidKey) ) {
|
||||
pendingToResolve.get(uidKey).resolveQuery(app);
|
||||
resolvedKeys.add(uidKey);
|
||||
if ( pendingToResolve.has(uidKey) ) {
|
||||
pendingToResolve.get(uidKey).resolveQuery(app);
|
||||
resolvedKeys.add(uidKey);
|
||||
}
|
||||
if ( pendingToResolve.has(nameKey) ) {
|
||||
pendingToResolve.get(nameKey).resolveQuery(app);
|
||||
resolvedKeys.add(nameKey);
|
||||
}
|
||||
if ( pendingToResolve.has(idKey) ) {
|
||||
pendingToResolve.get(idKey).resolveQuery(app);
|
||||
resolvedKeys.add(idKey);
|
||||
}
|
||||
}
|
||||
if ( pendingToResolve.has(nameKey) ) {
|
||||
pendingToResolve.get(nameKey).resolveQuery(app);
|
||||
resolvedKeys.add(nameKey);
|
||||
|
||||
for ( const [key, { resolveQuery }] of pendingToResolve.entries() ) {
|
||||
if ( ! resolvedKeys.has(key) ) {
|
||||
resolveQuery(null);
|
||||
}
|
||||
}
|
||||
if ( pendingToResolve.has(idKey) ) {
|
||||
pendingToResolve.get(idKey).resolveQuery(app);
|
||||
resolvedKeys.add(idKey);
|
||||
} catch ( err ) {
|
||||
for ( const { rejectQuery } of pendingToResolve.values() ) {
|
||||
rejectQuery(err);
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
for ( const { pendingKey } of pendingToResolve.values() ) {
|
||||
kv.del(pendingKey);
|
||||
}
|
||||
}
|
||||
|
||||
for ( const [key, { resolveQuery }] of pendingToResolve.entries() ) {
|
||||
if ( ! resolvedKeys.has(key) ) {
|
||||
resolveQuery(null);
|
||||
}
|
||||
}
|
||||
} catch ( err ) {
|
||||
for ( const { rejectQuery } of pendingToResolve.values() ) {
|
||||
rejectQuery(err);
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
for ( const { pendingKey } of pendingToResolve.values() ) {
|
||||
kv.del(pendingKey);
|
||||
}
|
||||
}
|
||||
|
||||
const pendingResults = await pendingResultsPromise;
|
||||
for ( const app of pendingResults ) {
|
||||
addApp(app);
|
||||
}
|
||||
|
||||
return normalized.map(spec => {
|
||||
let app;
|
||||
if ( spec.uid ) {
|
||||
app = appByUid.get(spec.uid);
|
||||
} else if ( spec.name ) {
|
||||
app = appByName.get(spec.name);
|
||||
} else if ( spec.id ) {
|
||||
app = appById.get(spec.id);
|
||||
}
|
||||
return app ? { ...app } : null;
|
||||
});
|
||||
} finally {
|
||||
const end = Date.now();
|
||||
console.log('Exiting at: ', end);
|
||||
console.log('Total time taken for get_apps(): ', end - start);
|
||||
}
|
||||
|
||||
const pendingResults = await pendingResultsPromise;
|
||||
for ( const app of pendingResults ) {
|
||||
addApp(app);
|
||||
}
|
||||
|
||||
return normalized.map(spec => {
|
||||
let app;
|
||||
if ( spec.uid ) {
|
||||
app = appByUid.get(spec.uid);
|
||||
} else if ( spec.name ) {
|
||||
app = appByName.get(spec.name);
|
||||
} else if ( spec.id ) {
|
||||
app = appById.get(spec.id);
|
||||
}
|
||||
return app ? { ...app } : null;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks to see if an app exists
|
||||
@@ -1482,8 +1491,67 @@ function seconds_to_string (seconds) {
|
||||
* @param {*} fsentry
|
||||
* @param {*} options
|
||||
*/
|
||||
async function suggest_app_for_fsentry (fsentry, options) {
|
||||
const suggested_apps = [];
|
||||
const SUGGEST_APP_CODE_EXTS = [
|
||||
'.asm',
|
||||
'.asp',
|
||||
'.aspx',
|
||||
'.bash',
|
||||
'.c',
|
||||
'.cpp',
|
||||
'.css',
|
||||
'.csv',
|
||||
'.dhtml',
|
||||
'.f',
|
||||
'.go',
|
||||
'.h',
|
||||
'.htm',
|
||||
'.html',
|
||||
'.html5',
|
||||
'.java',
|
||||
'.jl',
|
||||
'.js',
|
||||
'.jsa',
|
||||
'.json',
|
||||
'.jsonld',
|
||||
'.jsf',
|
||||
'.jsp',
|
||||
'.kt',
|
||||
'.log',
|
||||
'.lock',
|
||||
'.lua',
|
||||
'.md',
|
||||
'.perl',
|
||||
'.phar',
|
||||
'.php',
|
||||
'.pl',
|
||||
'.py',
|
||||
'.r',
|
||||
'.rb',
|
||||
'.rdata',
|
||||
'.rda',
|
||||
'.rdf',
|
||||
'.rds',
|
||||
'.rs',
|
||||
'.rlib',
|
||||
'.rpy',
|
||||
'.scala',
|
||||
'.sc',
|
||||
'.scm',
|
||||
'.sh',
|
||||
'.sol',
|
||||
'.sql',
|
||||
'.ss',
|
||||
'.svg',
|
||||
'.swift',
|
||||
'.toml',
|
||||
'.ts',
|
||||
'.wasm',
|
||||
'.xhtml',
|
||||
'.xml',
|
||||
'.yaml',
|
||||
];
|
||||
|
||||
const buildSuggestedAppSpecifiers = (fsentry) => {
|
||||
const name_specifiers = [];
|
||||
|
||||
let content_type = mime.contentType(fsentry.name);
|
||||
@@ -1501,74 +1569,12 @@ async function suggest_app_for_fsentry (fsentry, options) {
|
||||
})();
|
||||
const file_extension = _path.extname(fsname).toLowerCase();
|
||||
|
||||
const any_of = (list, name) => {
|
||||
return list.some(v => name.endsWith(v));
|
||||
};
|
||||
const any_of = (list, name) => list.some(v => name.endsWith(v));
|
||||
|
||||
//---------------------------------------------
|
||||
// Code
|
||||
//---------------------------------------------
|
||||
const exts_code = [
|
||||
'.asm',
|
||||
'.asp',
|
||||
'.aspx',
|
||||
'.bash',
|
||||
'.c',
|
||||
'.cpp',
|
||||
'.css',
|
||||
'.csv',
|
||||
'.dhtml',
|
||||
'.f',
|
||||
'.go',
|
||||
'.h',
|
||||
'.htm',
|
||||
'.html',
|
||||
'.html5',
|
||||
'.java',
|
||||
'.jl',
|
||||
'.js',
|
||||
'.jsa',
|
||||
'.json',
|
||||
'.jsonld',
|
||||
'.jsf',
|
||||
'.jsp',
|
||||
'.kt',
|
||||
'.log',
|
||||
'.lock',
|
||||
'.lua',
|
||||
'.md',
|
||||
'.perl',
|
||||
'.phar',
|
||||
'.php',
|
||||
'.pl',
|
||||
'.py',
|
||||
'.r',
|
||||
'.rb',
|
||||
'.rdata',
|
||||
'.rda',
|
||||
'.rdf',
|
||||
'.rds',
|
||||
'.rs',
|
||||
'.rlib',
|
||||
'.rpy',
|
||||
'.scala',
|
||||
'.sc',
|
||||
'.scm',
|
||||
'.sh',
|
||||
'.sol',
|
||||
'.sql',
|
||||
'.ss',
|
||||
'.svg',
|
||||
'.swift',
|
||||
'.toml',
|
||||
'.ts',
|
||||
'.wasm',
|
||||
'.xhtml',
|
||||
'.xml',
|
||||
'.yaml',
|
||||
];
|
||||
|
||||
if ( any_of(exts_code, fsname) || !fsname.includes('.') ) {
|
||||
if ( any_of(SUGGEST_APP_CODE_EXTS, fsname) || !fsname.includes('.') ) {
|
||||
name_specifiers.push({ name: 'code' });
|
||||
name_specifiers.push({ name: 'editor' });
|
||||
}
|
||||
@@ -1639,42 +1645,113 @@ async function suggest_app_for_fsentry (fsentry, options) {
|
||||
const apps = kv.get(`assocs:${file_extension.slice(1)}:apps`) ?? [];
|
||||
const id_specifiers = apps.map(app_id => ({ id: app_id }));
|
||||
|
||||
const specifiers = [...name_specifiers, ...id_specifiers];
|
||||
const resolved = specifiers.length > 0
|
||||
? await get_apps(specifiers)
|
||||
: [];
|
||||
return { name_specifiers, id_specifiers };
|
||||
};
|
||||
|
||||
const name_apps = resolved.slice(0, name_specifiers.length);
|
||||
const buildSuggestedAppsFromResolved = (resolved, name_specifier_count, options) => {
|
||||
const suggested_apps = [];
|
||||
|
||||
const name_apps = resolved.slice(0, name_specifier_count);
|
||||
suggested_apps.push(...name_apps);
|
||||
|
||||
const third_party_apps = resolved.slice(name_specifiers.length);
|
||||
const third_party_apps = resolved.slice(name_specifier_count);
|
||||
for ( const third_party_app of third_party_apps ) {
|
||||
if ( ! third_party_app ) continue;
|
||||
if ( third_party_app.approved_for_opening_items ||
|
||||
(options !== undefined && options.user !== undefined && options.user.id === third_party_app.owner_user_id) )
|
||||
(options?.user && options.user.id === third_party_app.owner_user_id) )
|
||||
{
|
||||
suggested_apps.push(third_party_app);
|
||||
}
|
||||
}
|
||||
|
||||
// return list
|
||||
if ( suggested_apps.some(app => app && app.name === 'editor') ) {
|
||||
const [codeapp] = await get_apps([{ name: 'codeapp' }]);
|
||||
if ( codeapp ) {
|
||||
suggested_apps.push(codeapp);
|
||||
}
|
||||
}
|
||||
return suggested_apps.filter((suggested_app, pos, self) => {
|
||||
const needs_codeapp = suggested_apps.some(app => app && app.name === 'editor');
|
||||
return { suggested_apps, needs_codeapp };
|
||||
};
|
||||
|
||||
const normalizeSuggestedApps = (suggested_apps) => (
|
||||
suggested_apps.filter((suggested_app, pos, self) => {
|
||||
// Remove any null values caused by calling `get_app()` for apps that don't exist.
|
||||
// This happens on self-host because we don't include `code`, among others.
|
||||
if ( ! suggested_app )
|
||||
{
|
||||
if ( ! suggested_app ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove any duplicate entries
|
||||
return self.indexOf(suggested_app) === pos;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
async function suggestedAppsForFsEntries (fsentries, options) {
|
||||
if ( ! Array.isArray(fsentries) ) {
|
||||
fsentries = [fsentries];
|
||||
}
|
||||
|
||||
const batches = [];
|
||||
const specifiers = [];
|
||||
const results = new Array(fsentries.length);
|
||||
|
||||
for ( let index = 0; index < fsentries.length; index++ ) {
|
||||
const fsentry = fsentries[index];
|
||||
if ( ! fsentry ) {
|
||||
results[index] = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
const { name_specifiers, id_specifiers } = buildSuggestedAppSpecifiers(fsentry);
|
||||
const entry_specifiers = [...name_specifiers, ...id_specifiers];
|
||||
|
||||
if ( entry_specifiers.length === 0 ) {
|
||||
results[index] = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
const offset = specifiers.length;
|
||||
specifiers.push(...entry_specifiers);
|
||||
batches.push({
|
||||
index,
|
||||
offset,
|
||||
count: entry_specifiers.length,
|
||||
name_count: name_specifiers.length,
|
||||
suggested_apps: [],
|
||||
needs_codeapp: false,
|
||||
});
|
||||
}
|
||||
|
||||
let resolved = [];
|
||||
if ( specifiers.length > 0 ) {
|
||||
resolved = await get_apps(specifiers);
|
||||
}
|
||||
|
||||
let any_needs_codeapp = false;
|
||||
for ( const batch of batches ) {
|
||||
const slice = resolved.slice(batch.offset, batch.offset + batch.count);
|
||||
const { suggested_apps, needs_codeapp } = buildSuggestedAppsFromResolved(slice,
|
||||
batch.name_count,
|
||||
options);
|
||||
batch.suggested_apps = suggested_apps;
|
||||
batch.needs_codeapp = needs_codeapp;
|
||||
if ( needs_codeapp ) any_needs_codeapp = true;
|
||||
}
|
||||
|
||||
let codeapp;
|
||||
if ( any_needs_codeapp ) {
|
||||
[codeapp] = await get_apps([{ name: 'codeapp' }]);
|
||||
}
|
||||
|
||||
for ( const batch of batches ) {
|
||||
let suggested_apps = batch.suggested_apps;
|
||||
if ( batch.needs_codeapp && codeapp ) {
|
||||
suggested_apps = [...suggested_apps, codeapp];
|
||||
}
|
||||
results[batch.index] = normalizeSuggestedApps(suggested_apps);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function suggestedAppForFsEntry (fsentry, options) {
|
||||
const [result] = await suggestedAppsForFsEntries([fsentry], options);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function get_taskbar_items (user, { icon_size, no_icons } = {}) {
|
||||
@@ -1898,7 +1975,8 @@ module.exports = {
|
||||
send_email_verification_token,
|
||||
sign_file,
|
||||
subdomain,
|
||||
suggest_app_for_fsentry,
|
||||
suggestedAppsForFsEntries,
|
||||
suggestedAppForFsEntry,
|
||||
df,
|
||||
username_exists,
|
||||
uuid2fsentry,
|
||||
|
||||
Reference in New Issue
Block a user