diff --git a/src/backend/src/filesystem/FSNodeContext.js b/src/backend/src/filesystem/FSNodeContext.js index e86b2a90..83dab94e 100644 --- a/src/backend/src/filesystem/FSNodeContext.js +++ b/src/backend/src/filesystem/FSNodeContext.js @@ -71,11 +71,17 @@ module.exports = class FSNodeContext { * @param {*} opt_identifier.id please pass mysql_id instead * @param {*} opt_identifier.mysql_id a MySQL ID of the filesystem entry */ - constructor ({ services, selector, fs }) { + constructor ({ + services, + selector, + provider, + fs + }) { this.log = services.get('log-service').create('fsnode-context'); this.selector_ = null; this.selectors_ = []; this.selector = selector; + this.provider = provider; this.entry = {}; this.found = undefined; this.found_thumbnail = undefined; @@ -264,106 +270,46 @@ module.exports = class FSNodeContext { return; } + const controls = { + log: this.log, + provide_selector: selector => { + this.selector = selector; + }, + }; + this.log.info('fetching entry: ' + this.selector.describe()); - // All services at the top (DEVLOG-401) - const { - traceService, - fsEntryService, - fsEntryFetcher, - resourceService, - } = Context.get('services').values; - if ( fetch_entry_options.tracer == null ) { - fetch_entry_options.tracer = traceService.tracer; - } - - if ( fetch_entry_options.op ) { - fetch_entry_options.trace_options = { - parent: fetch_entry_options.op.span, - }; - } - - let entry; - - await new Promise (rslv => { - const detachables = new MultiDetachable(); - - const callback = (resolver) => { - detachables.as(TDetachable).detach(); - rslv(); - } - - // either the resource is free - { - // no detachale because waitForResource returns a - // Promise that will be resolved when the resource - // is free no matter what, and then it will be - // garbage collected. - resourceService.waitForResource( - this.selector - ).then(callback.bind(null, 'resourceService')); - } - - // or pending information about the resource - // becomes available - { - // detachable is needed here because waitForEntry keeps - // a map of listeners in memory, and this event may - // never occur. If this never occurs, waitForResource - // is guaranteed to resolve eventually, and then this - // detachable will be detached by `callback` so the - // listener can be garbage collected. - const det = fsEntryService.waitForEntry( - this, callback.bind(null, 'fsEntryService')); - if ( det ) detachables.add(det); - } + const entry = await this.provider.stat({ + selector: this.selector, + options: fetch_entry_options, + node: this, + controls, }); - if ( resourceService.getResourceInfo(this.uid) ) { - entry = await fsEntryService.get(this.uid, fetch_entry_options); - this.log.debug('got an entry from the future'); - } else { - entry = await fsEntryFetcher.find( - this.selector, fetch_entry_options); - } - - if ( ! entry ) { - this.log.info(`entry not found: ${this.selector.describe(true)}`); - } - - if ( entry === null || typeof entry !== 'object' ) { - // TODO: this property shouldn't be set to false - - // this is set to false to avoid regressions with - // existing code. - this.entry = false; - + if ( entry === null ) { this.found = false; - return; + this.entry = false; + } else { + this.found = true; + + if ( ! this.uid && entry.uuid ) { + this.uid = entry.uuid; + } + + if ( ! this.mysql_id && entry.id ) { + this.mysql_id = entry.id; + } + + if ( ! this.path && entry.path ) { + this.path = entry.path; + } + + if ( ! this.name && entry.name ) { + this.name = entry.name; + } + + Object.assign(this.entry, entry); } - - this.found = true; - - if ( entry.id ) { - this.selector = new NodeInternalIDSelector('mysql', entry.id, { - source: 'FSNodeContext optimization' - }); - } - - if ( ! this.uid && entry.uuid ) { - this.uid = entry.uuid; - } - - if ( ! this.mysql_id && entry.id ) { - this.mysql_id = entry.id; - } - - if ( ! this.path && entry.path ) { - this.path = entry.path; - } - - if ( ! this.name && entry.name ) this.name = entry.name; - - Object.assign(this.entry, entry); } /** diff --git a/src/backend/src/filesystem/FilesystemService.js b/src/backend/src/filesystem/FilesystemService.js index 03a2ad54..b7453f44 100644 --- a/src/backend/src/filesystem/FilesystemService.js +++ b/src/backend/src/filesystem/FilesystemService.js @@ -34,6 +34,7 @@ const { DB_WRITE } = require("../services/database/consts"); const { UserActorType } = require('../services/auth/Actor'); const { get_user } = require('../helpers'); const BaseService = require('../services/BaseService'); +const { PuterFSProvider } = require('./puterfs/PuterFSProvider.js'); class FilesystemService extends BaseService { static MODULES = { @@ -344,6 +345,7 @@ class FilesystemService extends BaseService { } let fsNode = new FSNodeContext({ + provider: new PuterFSProvider(), services: this.services, selector, fs: this diff --git a/src/backend/src/filesystem/definitions/capabilities.js b/src/backend/src/filesystem/definitions/capabilities.js new file mode 100644 index 00000000..84e99fd0 --- /dev/null +++ b/src/backend/src/filesystem/definitions/capabilities.js @@ -0,0 +1,20 @@ +const capabilityNames = [ + 'thumbnail', + 'uuid', + 'operation-trace', + + 'read', + 'write', + 'case-sensitive', + 'symlink', + 'unix-perms', + 'trash', +]; + +const fsCapabilities = {}; +for ( const capabilityName of capabilityNames ) { + const key = capabilityName.toUpperCase().replace(/-/g, '_'); + fsCapabilities[key] = Symbol(capabilityName); +} + +module.exports = fsCapabilities; diff --git a/src/backend/src/filesystem/puterfs/PuterFSProvider.js b/src/backend/src/filesystem/puterfs/PuterFSProvider.js new file mode 100644 index 00000000..1c402330 --- /dev/null +++ b/src/backend/src/filesystem/puterfs/PuterFSProvider.js @@ -0,0 +1,117 @@ +const putility = require('@heyputer/putility'); +const { MultiDetachable } = putility.libs.listener; +const { TDetachable } = putility.traits; + +const { NodeInternalIDSelector, NodeChildSelector, NodeUIDSelector, RootNodeSelector, NodePathSelector } = require("../node/selectors"); +const { Context } = require("../../util/context"); +const fsCapabilities = require('../definitions/capabilities'); + +class PuterFSProvider { + get_capabilities () { + return new Set([ + fsCapabilities.THUMBNAIL, + fsCapabilities.UUID, + fsCapabilities.OPERATION_TRACE, + + fsCapabilities.READ, + fsCapabilities.WRITE, + fsCapabilities.CASE_SENSITIVE, + fsCapabilities.SYMLINK, + fsCapabilities.TRASH, + ]); + } + + async stat ({ + selector, + options, + controls, + node, + }) { + // For Puter FS nodes, we assume we will obtain all properties from + // fsEntryService/fsEntryFetcher, except for 'thumbnail' unless it's + // explicitly requested. + + const { + traceService, + fsEntryService, + fsEntryFetcher, + resourceService, + } = Context.get('services').values; + + if ( options.tracer == null ) { + options.tracer = traceService.tracer; + } + + if ( options.op ) { + options.trace_options = { + parent: options.op.span, + }; + } + + let entry; + + await new Promise (rslv => { + const detachables = new MultiDetachable(); + + const callback = (resolver) => { + detachables.as(TDetachable).detach(); + rslv(); + } + + // either the resource is free + { + // no detachale because waitForResource returns a + // Promise that will be resolved when the resource + // is free no matter what, and then it will be + // garbage collected. + resourceService.waitForResource( + selector + ).then(callback.bind(null, 'resourceService')); + } + + // or pending information about the resource + // becomes available + { + // detachable is needed here because waitForEntry keeps + // a map of listeners in memory, and this event may + // never occur. If this never occurs, waitForResource + // is guaranteed to resolve eventually, and then this + // detachable will be detached by `callback` so the + // listener can be garbage collected. + const det = fsEntryService.waitForEntry( + node, callback.bind(null, 'fsEntryService')); + if ( det ) detachables.add(det); + } + }); + + if ( resourceService.getResourceInfo(this.uid) ) { + entry = await fsEntryService.get(this.uid, options); + controls.log.debug('got an entry from the future'); + } else { + entry = await fsEntryFetcher.find( + selector, options); + } + + if ( ! entry ) { + controls.log.info(`entry not found: ${selector.describe(true)}`); + } + + if ( entry === null || typeof entry !== 'object' ) { + return null; + } + + if ( entry.id ) { + controls.provide_selector( + new NodeInternalIDSelector('mysql', entry.id, { + source: 'FSNodeContext optimization' + }) + ); + } + + return entry; + } +} + +module.exports = { + PuterFSProvider, +};