diff --git a/extensions/puterfs/PuterFSProvider.js b/extensions/puterfs/PuterFSProvider.js index e99d31697e..1c1a52ff5a 100644 --- a/extensions/puterfs/PuterFSProvider.js +++ b/extensions/puterfs/PuterFSProvider.js @@ -335,19 +335,14 @@ export default class PuterFSProvider { 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); + async read ({ node, version_id, range }) { const location = await node.get('s3:location') ?? {}; - const stream = (await storage.create_read_stream(await node.get('uid'), { - // TODO: fs:decouple-s3 - bucket: location.bucket, - bucket_region: location.bucket_region, + const stream = this.storageController.read({ + uid: await node.get('uid'), + location, + range, version_id, - key: location.key, - memory_file: node.entry, - ...(range ? { range } : {}), - })); + }); return stream; } @@ -491,10 +486,7 @@ export default class PuterFSProvider { }, }); - // const storage = new PuterS3StorageStrategy({ services: svc }); - const storage = context.get('storage'); - const state_copy = storage.create_copy(); - await state_copy.run({ + await this.storageController.copy({ src_node: source, dst_storage: { key: uuid, @@ -995,10 +987,7 @@ export default class PuterFSProvider { if ( await node.get('has-s3') ) { tasks.add('remove-from-s3', async () => { - // const storage = new PuterS3StorageStrategy({ services: svc }); - const storage = Context.get('storage'); - const state_delete = storage.create_delete(); - await state_delete.run({ + await this.storageController.delete({ node: node, }); }); diff --git a/extensions/puterfs/storage/LocalDiskStorageController.js b/extensions/puterfs/storage/LocalDiskStorageController.js index 8fbf827cfa..69df371592 100644 --- a/extensions/puterfs/storage/LocalDiskStorageController.js +++ b/extensions/puterfs/storage/LocalDiskStorageController.js @@ -52,11 +52,45 @@ export default class LocalDiskStorageController { // @ts-ignore (it's wrong about this) await writePromise; } - copy () { + async copy ({ src_node, dst_storage, storage_api }) { + const { progress_tracker } = storage_api; + + const src_path = this.#getPath(await src_node.get('uid')); + const dst_path = this.#getPath(dst_storage.key); + + await fs.promises.copyFile(src_path, dst_path); + + // for now we just copy the file, we don't care about the progress + progress_tracker.set_total(1); + progress_tracker.set(1); } - delete () { + async delete ({ node }) { + const path = this.#getPath(await node.get('uid')); + await fs.promises.unlink(path); } - read () { + async read ({ uid, range }) { + const path = this.#getPath(uid); + + // Handle range requests for partial content + if ( range ) { + const rangeMatch = range.match(/bytes=(\d+)-(\d*)/); + 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 ) { + streamOptions.end = parseInt(endStr, 10); + } + + return fs.createReadStream(path, streamOptions); + } + } + + // Default: create stream for entire file + return fs.createReadStream(path); } #getPath (key) { diff --git a/src/backend/src/filesystem/FilesystemService.js b/src/backend/src/filesystem/FilesystemService.js index f7857f5b51..4eadc0bc55 100644 --- a/src/backend/src/filesystem/FilesystemService.js +++ b/src/backend/src/filesystem/FilesystemService.js @@ -18,14 +18,14 @@ */ // TODO: database access can be a service const { RESOURCE_STATUS_PENDING_CREATE } = require('../modules/puterfs/ResourceService.js'); -const { NodePathSelector, NodeUIDSelector, NodeInternalIDSelector, NodeSelector } = require('./node/selectors.js'); +const { NodePathSelector, NodeUIDSelector, NodeInternalIDSelector, NodeSelector, NodeChildSelector } = require('./node/selectors.js'); const FSNodeContext = require('./FSNodeContext.js'); const { Context } = require('../util/context.js'); const APIError = require('../api/APIError.js'); const { PermissionUtil, PermissionRewriter, PermissionImplicator, PermissionExploder } = require('../services/auth/permissionUtils.mjs'); const { DB_WRITE } = require('../services/database/consts'); const { UserActorType } = require('../services/auth/Actor'); -const { get_user } = require('../helpers'); +const { get_user, is_valid_uuid4 } = 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'); @@ -334,6 +334,68 @@ class FilesystemService extends BaseService { return fsNode; } + // #region Simplified API + async read (selector, options = {}) { + const node = this.#coerceToNode(selector); + const ll_read = new LLRead(); + const stream = await ll_read.run({ + ...options, + fsNode: node, + }); + return stream; + } + + #coerceToNode (stringOrSelectorOrNode, { creatable } = {}) { + if ( stringOrSelectorOrNode instanceof FSNodeContext ) { + if ( creatable ) { + throw new Error('cannot specify a file/directory to create with an FSNodeContext'); + } + return stringOrSelectorOrNode; + } + + if ( stringOrSelectorOrNode instanceof NodeSelector ) { + if ( creatable && (stringOrSelectorOrNode instanceof NodeUIDSelector) ) { + throw new Error('cannot specify a file/directory to create by UUID'); + } + return this.node(stringOrSelectorOrNode); + } + + if ( typeof stringOrSelectorOrNode !== 'string' ) { + throw new Error('expected string, NodeSelector, or FSNodeContext'); + } + const string = stringOrSelectorOrNode; + + if ( string.startsWith('./') ) { + throw new Error('relative paths are not supported here'); + } + + // This will be coerced by `this.node` to a NodePathSelector + if ( string.startsWith('/') ) { + return this.node(string); + } + + // UUID followed by path component + if ( string.includes('/') ) { + const uuidPart = string.slice(0, string.indexOf('/')); + if ( ! is_valid_uuidv4(uuidPart) ) { + throw new Error('expected file/directory identifier to begin with UUID or /'); + } + + throw new Error('"UUID/then/path" form is not yet supported'); + } + + if ( ! is_valid_uuid4(string) ) { + throw new Error('string is not a valid file/directory specifier'); + } + + if ( creatable ) { + throw new Error('cannot specify a file/directory to create by UID'); + } + + return this.node(new NodeUIDSelector(string)); + } + // #endregion + /** * get_entry() returns a filesystem entry using * path, uid, or id associated with a filesystem