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.
*