diff --git a/extensions/puterfs/PuterFSProvider.js b/extensions/puterfs/PuterFSProvider.js index 1134201b2..ddcf622a2 100644 --- a/extensions/puterfs/PuterFSProvider.js +++ b/extensions/puterfs/PuterFSProvider.js @@ -100,6 +100,7 @@ const { export default class PuterFSProvider { constructor ({ fsEntryController }) { this.fsEntryController = fsEntryController; + this.name = 'puterfs'; } // TODO: should this be a static member instead? @@ -110,6 +111,7 @@ export default class PuterFSProvider { capabilities.UUID, capabilities.OPERATION_TRACE, capabilities.READDIR_UUID_MODE, + capabilities.PUTER_SHORTCUT, capabilities.COPY_TREE, capabilities.GET_RECURSIVE_SIZE, @@ -122,6 +124,89 @@ export default class PuterFSProvider { ]); } + // #region PuterOnly + async update_thumbnail ({ context, node, thumbnail }) { + const { + actor: inputActor, + } = context.values; + const actor = inputActor ?? Context.get('actor'); + + context = context ?? Context.get(); + const services = context.get('services'); + + // TODO: this ACL check should not be here, but there's no LL method yet + // and it's possible we will never implement the thumbnail + // capability for any other filesystem type + + const svc_acl = services.get('acl'); + if ( ! await svc_acl.check(actor, node, 'write') ) { + throw await svc_acl.get_safe_acl_error(actor, node, 'write'); + } + + const uid = await node.get('uid'); + + const entryOp = await this.fsEntryController.update(uid, { + thumbnail, + }); + + (async () => { + await entryOp.awaitDone(); + svc_event.emit('fs.write.file', { + node, + context, + }); + })(); + + return node; + } + + async puter_shortcut ({ parent, name, user, target }) { + await target.fetchEntry({ thumbnail: true }); + + const ts = Math.round(Date.now() / 1000); + const uid = uuidv4(); + + svc_resource.register({ + uid, + status: RESOURCE_STATUS_PENDING_CREATE, + }); + + const raw_fsentry = { + is_shortcut: 1, + shortcut_to: target.mysql_id, + is_dir: target.entry.is_dir, + thumbnail: target.entry.thumbnail, + uuid: uid, + parent_uid: await parent.get('uid'), + path: path_.join(await parent.get('path'), name), + user_id: user.id, + name, + created: ts, + updated: ts, + modified: ts, + immutable: false, + }; + + const entryOp = await this.fsEntryController.insert(raw_fsentry); + + (async () => { + await entryOp.awaitDone(); + svc_resource.free(uid); + })(); + + const node = await svc_fs.node(new NodeUIDSelector(uid)); + + svc_event.emit('fs.create.shortcut', { + node, + context: Context.get(), + }); + + return node; + } + // #endregion + + // #region Standard FS + /** * Check if a given node exists. * @@ -250,41 +335,6 @@ export default class PuterFSProvider { return node; } - async update_thumbnail ({ context, node, thumbnail }) { - const { - actor: inputActor, - } = context.values; - const actor = inputActor ?? Context.get('actor'); - - context = context ?? Context.get(); - const services = context.get('services'); - - // TODO: this ACL check should not be here, but there's no LL method yet - // and it's possible we will never implement the thumbnail - // capability for any other filesystem type - - const svc_acl = services.get('acl'); - if ( ! await svc_acl.check(actor, node, 'write') ) { - throw await svc_acl.get_safe_acl_error(actor, node, 'write'); - } - - const uid = await node.get('uid'); - - const entryOp = await this.fsEntryController.update(uid, { - thumbnail, - }); - - (async () => { - await entryOp.awaitDone(); - svc_event.emit('fs.write.file', { - node, - context, - }); - })(); - - return node; - } - async read ({ context, node, version_id, range }) { const svc_mountpoint = context.get('services').get('mountpoint'); const storage = svc_mountpoint.get_storage(this.constructor.name); @@ -799,6 +849,10 @@ export default class PuterFSProvider { return rows[0].total_size; } + // #endregion + + // #region internal + /** * @param {Object} param * @param {File} param.file: The file to write. @@ -941,4 +995,5 @@ export default class PuterFSProvider { await tasks.awaitAll(); } + // #endregion } diff --git a/src/backend/src/api/APIError.js b/src/backend/src/api/APIError.js index 9c533a402..53ea4cfae 100644 --- a/src/backend/src/api/APIError.js +++ b/src/backend/src/api/APIError.js @@ -286,7 +286,16 @@ module.exports = class APIError { 'unresolved_relative_path': { status: 400, message: ({ path }) => `Unresolved relative path: ${quot(path)}. ` + - "You may need to specify a full path starting with '/'.", + "You may need to specify a full path starting with '/'.", + }, + 'missing_filesystem_capability': { + status: 422, + message: ({ action, subjectName, providerName, capability }) => { + return `Cannot perform action ${quot(action)} on ` + + `${quot(subjectName)} because it is inside a filesystem ` + + `of type ${providerName}, which does not implement the ` + + `required capability called ${quot(capability)}.`; + }, }, // Open @@ -514,11 +523,11 @@ module.exports = class APIError { status: 400, message: ({ engine, valid_engines }) => `Invalid engine: ${quot(engine)}. Valid engines are: ${valid_engines.map(quot).join(', ')}.`, }, - + // Abuse prevention 'moderation_failed': { status: 422, - message: `Content moderation failed`, + message: 'Content moderation failed', }, }; @@ -540,7 +549,7 @@ module.exports = class APIError { * - an object with a message property to use as the error message * @returns */ - static create(status, source, fields = {}) { + static create (status, source, fields = {}) { // Just the error code if ( typeof status === 'string' ) { const code = this.codes[status]; @@ -578,12 +587,12 @@ module.exports = class APIError { console.error('Invalid APIError source:', source); return new APIError(500, 'Internal Server Error', null, {}); } - static adapt(err) { + static adapt (err) { if ( err instanceof APIError ) return err; return APIError.create('internal_error'); } - constructor(status, message, source, fields = {}) { + constructor (status, message, source, fields = {}) { this.codes = this.constructor.codes; this.status = status; this._message = message; @@ -595,7 +604,7 @@ module.exports = class APIError { this._message = this.codes[message].message; } } - write(res) { + write (res) { const message = typeof this.message === 'function' ? this.message(this.fields) : this.message; @@ -604,7 +613,7 @@ module.exports = class APIError { ...this.fields, }); } - serialize() { + serialize () { return { ...this.fields, $: 'heyputer:api/APIError', @@ -613,11 +622,11 @@ module.exports = class APIError { }; } - querystringize(extra) { + querystringize (extra) { return new URLSearchParams(this.querystringize_(extra)); } - querystringize_(extra) { + querystringize_ (extra) { const fields = {}; for ( const k in this.fields ) { fields[`field_${k}`] = this.fields[k]; @@ -631,14 +640,14 @@ module.exports = class APIError { }; } - get message() { + get message () { const message = typeof this._message === 'function' ? this._message(this.fields) : this._message; return message; } - toString() { + toString () { return `APIError(${this.status}, ${this.message})`; } }; diff --git a/src/backend/src/filesystem/FilesystemService.js b/src/backend/src/filesystem/FilesystemService.js index a27b17733..f7857f5b5 100644 --- a/src/backend/src/filesystem/FilesystemService.js +++ b/src/backend/src/filesystem/FilesystemService.js @@ -29,6 +29,7 @@ const { get_user } = require('../helpers'); const BaseService = require('../services/BaseService'); const { MANAGE_PERM_PREFIX } = require('../services/auth/permissionConts.mjs'); const { quot } = require('@heyputer/putility/src/libs/string.js'); +const fsCapabilities = require('./definitions/capabilities.js'); class FilesystemService extends BaseService { static MODULES = { @@ -165,59 +166,18 @@ class FilesystemService extends BaseService { throw APIError.create('shortcut_to_does_not_exist'); } - await target.fetchEntry({ thumbnail: true }); + if ( ! parent.provider.get_capabilities().has(fsCapabilities.PUTER_SHORTCUT) ) { + throw APIError.create('missing_filesystem_capability', null, { + action: 'make shortcut', + subjectName: parent.path ?? parent.uid, + providerName: parent.provider.name, + capability: 'PUTER_SHORTCUT', + }); + } - const { _path, uuidv4 } = this.modules; - const svc_fsEntry = this.services.get('fsEntryService'); - const resourceService = this.services.get('resourceService'); - - const ts = Math.round(Date.now() / 1000); - const uid = uuidv4(); - - resourceService.register({ - uid, - status: RESOURCE_STATUS_PENDING_CREATE, + return await parent.provider.puter_shortcut({ + parent, name, user, target, }); - - console.log('registered entry'); - - const raw_fsentry = { - is_shortcut: 1, - shortcut_to: target.mysql_id, - is_dir: target.entry.is_dir, - thumbnail: target.entry.thumbnail, - uuid: uid, - parent_uid: await parent.get('uid'), - path: _path.join(await parent.get('path'), name), - user_id: user.id, - name, - created: ts, - updated: ts, - modified: ts, - immutable: false, - }; - - this.log.debug('creating fsentry', { fsentry: raw_fsentry }); - - const entryOp = await svc_fsEntry.insert(raw_fsentry); - - console.log('entry op', entryOp); - - (async () => { - await entryOp.awaitDone(); - this.log.debug('finished creating fsentry', { uid }); - resourceService.free(uid); - })(); - - const node = await this.node(new NodeUIDSelector(uid)); - - const svc_event = this.services.get('event'); - svc_event.emit('fs.create.shortcut', { - node, - context: Context.get(), - }); - - return node; } async mklink ({ parent, name, user, target }) { diff --git a/src/backend/src/filesystem/definitions/capabilities.js b/src/backend/src/filesystem/definitions/capabilities.js index 695d49d84..241184c61 100644 --- a/src/backend/src/filesystem/definitions/capabilities.js +++ b/src/backend/src/filesystem/definitions/capabilities.js @@ -24,6 +24,7 @@ const capabilityNames = [ 'operation-trace', 'readdir-uuid-mode', 'update-thumbnail', + 'puter-shortcut', // Standard Capabilities 'read', diff --git a/src/backend/src/filesystem/ll_operations/ll_rmdir.js b/src/backend/src/filesystem/ll_operations/ll_rmdir.js index 302070745..e8c7674b6 100644 --- a/src/backend/src/filesystem/ll_operations/ll_rmdir.js +++ b/src/backend/src/filesystem/ll_operations/ll_rmdir.js @@ -58,12 +58,11 @@ class LLRmDir extends LLFilesystemOperation { throw APIError.create('immutable'); } - const svc_fsEntry = svc.get('fsEntryService'); const fs = svc.get('filesystem'); - const children = await svc_fsEntry.fast_get_direct_descendants( - await target.get('uid') - ); + const children = await target.provider.readdir({ + node: target, + }); if ( children.length > 0 && ! recursive && ! ignore_not_empty ) { throw APIError.create('not_empty');