Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 101 additions & 35 deletions extensions/puterfs/PuterFSProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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,
Expand All @@ -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.
*
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -564,6 +614,17 @@ export default class PuterFSProvider {
return child_uuids;
}

async directory_has_name ({ parent, name }) {
const uid = await parent.get('uid');
/* eslint-disable */
let check_dupe = await db.read(
'SELECT `id` FROM `fsentries` WHERE `parent_uid` = ? AND name = ? LIMIT 1',
[uid, name],
);
/* eslint-enable */
return !!check_dupe[0];
}

/**
* Write a new file to the filesystem. Throws an error if the destination
* already exists.
Expand Down Expand Up @@ -799,6 +860,10 @@ export default class PuterFSProvider {
return rows[0].total_size;
}

// #endregion

// #region internal

/**
* @param {Object} param
* @param {File} param.file: The file to write.
Expand Down Expand Up @@ -941,4 +1006,5 @@ export default class PuterFSProvider {

await tasks.awaitAll();
}
// #endregion
}
33 changes: 21 additions & 12 deletions src/backend/src/api/APIError.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
},
};

Expand All @@ -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];
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -604,7 +613,7 @@ module.exports = class APIError {
...this.fields,
});
}
serialize() {
serialize () {
return {
...this.fields,
$: 'heyputer:api/APIError',
Expand All @@ -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];
Expand All @@ -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})`;
}
};
8 changes: 8 additions & 0 deletions src/backend/src/filesystem/FSNodeContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,10 @@ module.exports = class FSNodeContext {
}

if ( key === 'uid' ) {
const uidSelector = this.get_selector_of_type(NodeUIDSelector);
if ( uidSelector ) {
return uidSelector.value;
}
await this.fetchEntry();
return this.uid;
}
Expand Down Expand Up @@ -737,6 +741,10 @@ module.exports = class FSNodeContext {

return await this.fs.node(new NodeChildSelector(this.selector, name));
}

async hasChild(name) {
return await this.provider.directory_has_name({ parent: this, name });
}

async getTarget() {
await this.fetchEntry();
Expand Down
62 changes: 11 additions & 51 deletions src/backend/src/filesystem/FilesystemService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -165,59 +166,18 @@ class FilesystemService extends BaseService {
throw APIError.create('shortcut_to_does_not_exist');
}

await target.fetchEntry({ thumbnail: true });

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,
});

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));
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 svc_event = this.services.get('event');
svc_event.emit('fs.create.shortcut', {
node,
context: Context.get(),
return await parent.provider.puter_shortcut({
parent, name, user, target,
});

return node;
}

async mklink ({ parent, name, user, target }) {
Expand Down
1 change: 1 addition & 0 deletions src/backend/src/filesystem/definitions/capabilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const capabilityNames = [
'operation-trace',
'readdir-uuid-mode',
'update-thumbnail',
'puter-shortcut',

// Standard Capabilities
'read',
Expand Down
Loading
Loading