diff --git a/extensions/puterfs/main.js b/extensions/puterfs/main.js index 4e3fd0b269..5613e68df4 100644 --- a/extensions/puterfs/main.js +++ b/extensions/puterfs/main.js @@ -79,6 +79,7 @@ const { const { FSNodeContext, + capabilities, } = extension.import('fs'); const { @@ -99,6 +100,25 @@ const { } = extension.import('fs').util; class PuterFSProvider { + // TODO: should this be a static member instead? + get_capabilities () { + return new Set([ + capabilities.THUMBNAIL, + capabilities.UPDATE_THUMBNAIL, + capabilities.UUID, + capabilities.OPERATION_TRACE, + capabilities.READDIR_UUID_MODE, + + capabilities.COPY_TREE, + + capabilities.READ, + capabilities.WRITE, + capabilities.CASE_SENSITIVE, + capabilities.SYMLINK, + capabilities.TRASH, + ]); + } + /** * Check if a given node exists. * @@ -226,6 +246,41 @@ 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 svc_fsEntry.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); @@ -863,12 +918,10 @@ class PuterFSProvider { } } -const { TmpProxyFSProvider } = extension.import('fs'); - extension.on('create.filesystem-types', event => { event.createFilesystemType('puterfs', { mount ({ path }) { - return new TmpProxyFSProvider(path, new PuterFSProvider(path)); + return new PuterFSProvider(path); }, }); }); diff --git a/src/backend/src/filesystem/FilesystemService.js b/src/backend/src/filesystem/FilesystemService.js index aca09e758d..a27b17733a 100644 --- a/src/backend/src/filesystem/FilesystemService.js +++ b/src/backend/src/filesystem/FilesystemService.js @@ -28,7 +28,6 @@ const { UserActorType } = require('../services/auth/Actor'); const { get_user } = require('../helpers'); const BaseService = require('../services/BaseService'); const { MANAGE_PERM_PREFIX } = require('../services/auth/permissionConts.mjs'); -const { PuterFSProvider } = require('../modules/puterfs/lib/PuterFSProvider.js'); const { quot } = require('@heyputer/putility/src/libs/string.js'); class FilesystemService extends BaseService { @@ -95,7 +94,7 @@ class FilesystemService extends BaseService { || permission.startsWith(`${MANAGE_PERM_PREFIX}:${MANAGE_PERM_PREFIX}:fs:`); // owner has implicit rule to give others manage access; }, checker: async ({ actor, permission }) => { - if ( !(actor.type instanceof UserActorType) ) { + if ( ! (actor.type instanceof UserActorType) ) { return undefined; } @@ -109,7 +108,7 @@ class FilesystemService extends BaseService { const owner_id = await node.get('user_id'); // These conditions should never happen - if ( ! owner_id || ! actor.type.user.id ) { + if ( !owner_id || !actor.type.user.id ) { throw new Error('something unexpected happened'); } @@ -330,8 +329,8 @@ class FilesystemService extends BaseService { } if ( ! (selector instanceof NodeSelector) ) { - throw new Error('FileSystemService could not resolve the specified node value ' + - quot('' + selector) + ` (type: ${typeof selector}) ` + + throw new Error(`FileSystemService could not resolve the specified node value ${ + quot(`${ selector}`) } (type: ${typeof selector}) ` + 'to a filesystem node selector'); } diff --git a/src/backend/src/modules/puterfs/PuterFSModule.js b/src/backend/src/modules/puterfs/PuterFSModule.js index fb9b2faf7a..ebdbc935fa 100644 --- a/src/backend/src/modules/puterfs/PuterFSModule.js +++ b/src/backend/src/modules/puterfs/PuterFSModule.js @@ -22,7 +22,6 @@ const FSNodeContext = require('../../filesystem/FSNodeContext'); const capabilities = require('../../filesystem/definitions/capabilities'); const selectors = require('../../filesystem/node/selectors'); const { RuntimeModule } = require('../../extension/RuntimeModule'); -const { TmpProxyFSProvider } = require('./TmpProxyFSProvider'); const { MODE_READ, MODE_WRITE } = require('../../services/fs/FSLockService'); const { UploadProgressTracker } = require('../../filesystem/storage/UploadProgressTracker'); @@ -39,7 +38,6 @@ class PuterFSModule extends AdvancedBase { capabilities, selectors, FSNodeContext, - TmpProxyFSProvider, lock: { MODE_READ, MODE_WRITE, diff --git a/src/backend/src/modules/puterfs/PuterFSService.js b/src/backend/src/modules/puterfs/PuterFSService.js deleted file mode 100644 index 268accc5c6..0000000000 --- a/src/backend/src/modules/puterfs/PuterFSService.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -const BaseService = require("../../services/BaseService"); -const { PuterFSProvider } = require("./lib/PuterFSProvider"); - -class PuterFSService extends BaseService { - async _init () { - const svc_mountpoint = this.services.get('mountpoint'); - svc_mountpoint.register_mounter('puterfs', this.as('mounter')); - } - - static IMPLEMENTS = { - mounter: { - async mount ({ path, options }) { - const provider = new PuterFSProvider(); - return provider; - } - } - } -} - -module.exports = { - PuterFSService, -}; diff --git a/src/backend/src/modules/puterfs/TmpProxyFSProvider.js b/src/backend/src/modules/puterfs/TmpProxyFSProvider.js deleted file mode 100644 index 91ab8a42e9..0000000000 --- a/src/backend/src/modules/puterfs/TmpProxyFSProvider.js +++ /dev/null @@ -1,40 +0,0 @@ -const { PuterFSProvider } = require("./lib/PuterFSProvider"); - -/** - * This is a temporary filesystem provider implementation that will - * proxy calls to either the new PuterFS implementation from the - * `puterfs` extension (if the method is implemented), or to the - * soon-to-be-legacy implementation in Puter's core otherwise. - * - * Once all of the methods for PuterFS have been moved to the - * extension, this temporary proxy provider FS Provider will be - * removed. - */ -class TmpProxyFSProvider { - constructor (path, puterfs) { - this.puterfs = puterfs; - this.legacyfs = new PuterFSProvider(); - - return new Proxy(this, { - get (target, prop, _receiver) { - if ( prop in target.puterfs ) { - const value = target.puterfs[prop]; - if ( typeof value === 'function' ) { - return value.bind(target.puterfs); - } - return value; - } - - const value = target.legacyfs[prop]; - if ( typeof value === 'function' ) { - return value.bind(target.legacyfs); - } - return value; - }, - }) - } -} - -module.exports = { - TmpProxyFSProvider, -}; diff --git a/src/backend/src/modules/puterfs/lib/PuterFSProvider.js b/src/backend/src/modules/puterfs/lib/PuterFSProvider.js deleted file mode 100644 index 644da93ec7..0000000000 --- a/src/backend/src/modules/puterfs/lib/PuterFSProvider.js +++ /dev/null @@ -1,333 +0,0 @@ -/* - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -const putility = require('@heyputer/putility'); -const { MultiDetachable } = putility.libs.listener; -const { TDetachable } = putility.traits; -const { NodeInternalIDSelector, NodeChildSelector, NodeUIDSelector } = require('../../../filesystem/node/selectors'); -const { Context } = require('../../../util/context'); -const fsCapabilities = require('../../../filesystem/definitions/capabilities'); -const { UploadProgressTracker } = require('../../../filesystem/storage/UploadProgressTracker'); -const FSNodeContext = require('../../../filesystem/FSNodeContext'); -const { RESOURCE_STATUS_PENDING_CREATE } = require('../ResourceService'); -const { ParallelTasks } = require('../../../util/otelutil'); -const { TYPE_DIRECTORY } = require('../../../filesystem/FSNodeContext'); -const APIError = require('../../../api/APIError'); -const { MODE_WRITE } = require('../../../services/fs/FSLockService'); -const { DB_WRITE } = require('../../../services/database/consts'); -const { stuck_detector_stream, hashing_stream } = require('../../../util/streamutil'); -const crypto = require('crypto'); -const { OperationFrame } = require('../../../services/OperationTraceService'); -const path = require('path'); -const uuidv4 = require('uuid').v4; -const config = require('../../../config.js'); -const { Actor } = require('../../../services/auth/Actor.js'); -const { UserActorType } = require('../../../services/auth/Actor.js'); -const { get_user } = require('../../../helpers.js'); - -const STUCK_STATUS_TIMEOUT = 10 * 1000; -const STUCK_ALARM_TIMEOUT = 20 * 1000; - -class PuterFSProvider extends putility.AdvancedBase { - - get #services () { // we really should just pass services in constructor, global state is a bit messy - return Context.get('services'); - } - - /** @type {import('../../../services/MeteringService/MeteringService.js').MeteringService} */ - get #meteringService () { - return this.#services.get('meteringService').meteringService; - } - - constructor (...a) { - super(...a); - this.log_fsentriesNotFound = (config.logging ?? []) - .includes('fsentries-not-found'); - } - - get_capabilities () { - return new Set([ - fsCapabilities.THUMBNAIL, - fsCapabilities.UPDATE_THUMBNAIL, - fsCapabilities.UUID, - fsCapabilities.OPERATION_TRACE, - fsCapabilities.READDIR_UUID_MODE, - - fsCapabilities.COPY_TREE, - - fsCapabilities.READ, - fsCapabilities.WRITE, - fsCapabilities.CASE_SENSITIVE, - fsCapabilities.SYMLINK, - fsCapabilities.TRASH, - ]); - } - - /** - * Check if a given node exists. - * - * @param {Object} param - * @param {NodeSelector} param.selector - The selector used for checking. - * @returns {Promise} - True if the node exists, false otherwise. - */ - async quick_check ({ - selector, - }) { - console.error('This .quick_check should not be called!'); - throw new Error('This .quick_check should not be called!'); - } - - async stat ({ - selector, - options, - controls, - node, - }) { - console.error('This .stat should not be called!'); - throw new Error('This .stat should not be called!'); - } - - async readdir ({ node }) { - console.error('This .readdir should not be called!'); - throw new Error('This .readdir should not be called!'); - } - - async move ({ context, node, new_parent, new_name, metadata }) { - console.error('This .move should not be called!'); - throw new Error('This .move should not be called!'); - } - - async copy_tree ({ context, node, options = {} }) { - console.error('This .copy_tree should not be called!'); - throw new Error('This .copy_tree should not be called!'); - } - - async unlink ({ context, node, options = {} }) { - console.error('This .unlink should not be called!'); - throw new Error('This .unlink should not be called!'); - } - - async rmdir ({ context, node, options = {} }) { - console.error('This .rmdir should not be called!'); - throw new Error('This .rmdir should not be called!'); - } - - /** - * Create a new directory. - * - * @param {Object} param - * @param {Context} param.context - * @param {FSNode} param.parent - * @param {string} param.name - * @param {boolean} param.immutable - * @returns {Promise} - */ - async mkdir ({ context, parent, name, immutable }) { - console.error('This .mkdir should not be called!'); - throw new Error('This .mkdir should not be called!'); - } - - 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'); - - const svc_fsEntry = services.get('fsEntryService'); - const svc_event = services.get('event'); - - 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 svc_fsEntry.update(uid, { - thumbnail, - }); - - (async () => { - await entryOp.awaitDone(); - svc_event.emit('fs.write.file', { - node, - context, - }); - })(); - - return node; - } - - /** - * Write a new file to the filesystem. Throws an error if the destination - * already exists. - * - * @param {Object} param - * @param {Context} param.context - * @param {FSNode} param.parent: The parent directory of the file. - * @param {string} param.name: The name of the file. - * @param {File} param.file: The file to write. - * @returns {Promise} - */ - async write_new ({ context, parent, name, file }) { - console.error('This .write_new should not be called!'); - throw new Error('This .write_new should not be called!'); - } - - /** - * Overwrite an existing file. Throws an error if the destination does not - * exist. - * - * @param {Object} param - * @param {Context} param.context - * @param {FSNodeContext} param.node: The node to write to. - * @param {File} param.file: The file to write. - * @returns {Promise} - */ - async write_overwrite ({ context, node, file }) { - console.error('This .write_overwrite should not be called!'); - throw new Error('This .write_overwrite should not be called!'); - } - - /** - * @param {Object} param - * @param {File} param.file: The file to write. - * @returns - */ - async #storage_upload ({ - uuid, - bucket, - bucket_region, - file, - tmp, - }) { - const log = this.#services.get('log-service').create('fs.#storage_upload'); - const errors = this.#services.get('error-service').create(log); - const svc_event = this.#services.get('event'); - - const svc_mountpoint = this.#services.get('mountpoint'); - const storage = svc_mountpoint.get_storage(this.constructor.name); - - bucket ??= config.s3_bucket; - bucket_region ??= config.s3_region ?? config.region; - - let upload_tracker = new UploadProgressTracker(); - - svc_event.emit('fs.storage.upload-progress', { - upload_tracker, - context: Context.get(), - meta: { - item_uid: uuid, - item_path: tmp.path, - }, - }); - - if ( ! file.buffer ) { - let stream = file.stream; - let alarm_timeout = null; - stream = stuck_detector_stream(stream, { - timeout: STUCK_STATUS_TIMEOUT, - on_stuck: () => { - this.frame.status = OperationFrame.FRAME_STATUS_STUCK; - log.warn('Upload stream stuck might be stuck', { - bucket_region, - bucket, - uuid, - }); - alarm_timeout = setTimeout(() => { - errors.report('fs.write.s3-upload', { - message: 'Upload stream stuck for too long', - alarm: true, - extra: { - bucket_region, - bucket, - uuid, - }, - }); - }, STUCK_ALARM_TIMEOUT); - }, - on_unstuck: () => { - clearTimeout(alarm_timeout); - this.frame.status = OperationFrame.FRAME_STATUS_WORKING; - }, - }); - file = { ...file, stream }; - } - - let hashPromise; - if ( file.buffer ) { - const hash = crypto.createHash('sha256'); - hash.update(file.buffer); - hashPromise = Promise.resolve(hash.digest('hex')); - } else { - const hs = hashing_stream(file.stream); - file.stream = hs.stream; - hashPromise = hs.hashPromise; - } - - hashPromise.then(hash => { - const svc_event = this.#services.get('event'); - svc_event.emit('outer.fs.write-hash', { - hash, uuid, - }); - }); - - const state_upload = storage.create_upload(); - - try { - await state_upload.run({ - uid: uuid, - file, - storage_meta: { bucket, bucket_region }, - storage_api: { progress_tracker: upload_tracker }, - }); - } catch (e) { - errors.report('fs.write.storage-upload', { - source: e || new Error('unknown'), - trace: true, - alarm: true, - extra: { - bucket_region, - bucket, - uuid, - }, - }); - throw APIError.create('upload_failed'); - } - - return state_upload; - } - - async read ({ - context, - node, - version_id, - range, - }) { - console.error('This .read should not be called!'); - throw new Error('This .read should not be called!'); - } -} - -module.exports = { - PuterFSProvider, -}; diff --git a/src/backend/src/routers/filesystem_api/update.js b/src/backend/src/routers/filesystem_api/update.js index 7eac77de50..59d698b7e1 100644 --- a/src/backend/src/routers/filesystem_api/update.js +++ b/src/backend/src/routers/filesystem_api/update.js @@ -1,10 +1,9 @@ -const APIError = require("../../api/APIError"); -const eggspress = require("../../api/eggspress"); -const FSNodeParam = require("../../api/filesystem/FSNodeParam"); -const StringParam = require("../../api/filesystem/StringParam"); -const { is_valid_url } = require("../../helpers"); -const { PuterFSProvider } = require("../../modules/puterfs/lib/PuterFSProvider"); -const { Context } = require("../../util/context"); +const APIError = require('../../api/APIError'); +const eggspress = require('../../api/eggspress'); +const FSNodeParam = require('../../api/filesystem/FSNodeParam'); +const StringParam = require('../../api/filesystem/StringParam'); +const { is_valid_url } = require('../../helpers'); +const { Context } = require('../../util/context'); module.exports = eggspress('/update-fsentry-thumbnail', { subdomain: 'api', @@ -25,22 +24,22 @@ module.exports = eggspress('/update-fsentry-thumbnail', { got: typeof req.values.thumbnail, }); } - + if ( ! await req.values.fsNode.exists() ) { throw new APIError.create('subject_does_not_exist'); } - + const svc = Context.get('services'); - + const svc_mountpoint = svc.get('mountpoint'); const provider = await svc_mountpoint.get_provider(req.values.fsNode.selector); - + provider.update_thumbnail({ context: Context.get(), node: req.values.fsNode, thumbnail: req.body.thumbnail, }); - + res.json({}); }); diff --git a/src/backend/src/services/LocalDiskStorageService.js b/src/backend/src/services/LocalDiskStorageService.js index 3f9085dcb0..1ef1bb81ad 100644 --- a/src/backend/src/services/LocalDiskStorageService.js +++ b/src/backend/src/services/LocalDiskStorageService.js @@ -17,12 +17,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -const { LocalDiskStorageStrategy } = require("../filesystem/strategies/storage_a/LocalDiskStorageStrategy"); -const { PuterFSProvider } = require("../modules/puterfs/lib/PuterFSProvider"); +const { LocalDiskStorageStrategy } = require('../filesystem/strategies/storage_a/LocalDiskStorageStrategy'); const { TeePromise } = require('@heyputer/putility').libs.promise; -const { progress_stream, size_limit_stream } = require("../util/streamutil"); -const BaseService = require("./BaseService"); - +const { progress_stream, size_limit_stream } = require('../util/streamutil'); +const BaseService = require('./BaseService'); /** * @class LocalDiskStorageService @@ -36,8 +34,7 @@ class LocalDiskStorageService extends BaseService { static MODULES = { fs: require('fs'), path: require('path'), - } - + }; /** * Initializes the context for the storage service. @@ -51,12 +48,13 @@ class LocalDiskStorageService extends BaseService { const svc_contextInit = this.services.get('context-init'); const storage = new LocalDiskStorageStrategy({ services: this.services }); svc_contextInit.register_value('storage', storage); - + + // TODO: this is rather silly and can be removed once the storage + // implementation is moved into the extension as part of puterfs const svc_mountpoint = this.services.get('mountpoint'); - svc_mountpoint.set_storage(PuterFSProvider.name, storage); + svc_mountpoint.set_storage('PuterFSProvider', storage); } - /** * Initializes the local disk storage service. * @@ -81,7 +79,6 @@ class LocalDiskStorageService extends BaseService { return path.join(this.path, key); } - /** * Stores a stream to local disk storage. * @@ -105,7 +102,7 @@ class LocalDiskStorageService extends BaseService { total: size, progress_callback: on_progress, }); - + stream = size_limit_stream(stream, { limit: size, }); @@ -122,7 +119,6 @@ class LocalDiskStorageService extends BaseService { return await writePromise; } - /** * Stores a buffer to the local disk. * @@ -141,7 +137,6 @@ class LocalDiskStorageService extends BaseService { await fs.promises.writeFile(path, buffer); } - /** * Creates a read stream for a given key. * @@ -155,31 +150,30 @@ class LocalDiskStorageService extends BaseService { const fs = require('fs'); const path = this._get_path(uid); - + // Handle range requests for partial content const { range } = options; - if (range) { + if ( range ) { const rangeMatch = range.match(/bytes=(\d+)-(\d*)/); - if (rangeMatch) { + if ( rangeMatch ) { const start = parseInt(rangeMatch[1], 10); const endStr = rangeMatch[2]; - + const streamOptions = { start }; - + // If end is specified, set it (fs.createReadStream end is inclusive) - if (endStr) { + if ( endStr ) { streamOptions.end = parseInt(endStr, 10); } - + return fs.createReadStream(path, streamOptions); } } - + // Default: create stream for entire file return fs.createReadStream(path); } - /** * Copies a file from one key to another within the local disk storage. * @@ -198,7 +192,6 @@ class LocalDiskStorageService extends BaseService { await fs.promises.copyFile(src_path, dst_path); } - /** * Deletes a file from the local disk storage. *