dev(puterfs): move mkshortcut, make ll_rmdir...

...use readdir from the provider instead of calling
fast_get_direct_descendants directly on fsEntryService.

This change is prerequisite to removing FSEntryService from core.
This commit is contained in:
KernelDeimos
2025-11-13 13:45:31 -05:00
committed by Eric Dubé
parent 33a8814feb
commit 35d32f7fc8
5 changed files with 126 additions and 102 deletions
+90 -35
View File
@@ -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
}
+21 -12
View File
@@ -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})`;
}
};
+11 -51
View File
@@ -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 }) {
@@ -24,6 +24,7 @@ const capabilityNames = [
'operation-trace',
'readdir-uuid-mode',
'update-thumbnail',
'puter-shortcut',
// Standard Capabilities
'read',
@@ -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');