mirror of
https://github.com/HeyPuter/puter.git
synced 2025-12-30 17:50:00 -06:00
refactor: merge FSEntryFetcher with FSEntryController
This commit is contained in:
@@ -38,7 +38,6 @@ const svc_acl = extension.import('service:acl');
|
||||
|
||||
// TODO: these services ought to be part of this extension
|
||||
const svc_size = extension.import('service:sizeService');
|
||||
const svc_fsEntryFetcher = extension.import('service:fsEntryFetcher');
|
||||
const svc_resource = extension.import('service:resourceService');
|
||||
|
||||
// Not sure where these really belong yet
|
||||
@@ -219,25 +218,25 @@ export default class PuterFSProvider {
|
||||
}) {
|
||||
// shortcut: has full path
|
||||
if ( selector?.path ) {
|
||||
const entry = await svc_fsEntryFetcher.findByPath(selector.path);
|
||||
const entry = await this.fsEntryController.findByPath(selector.path);
|
||||
return Boolean(entry);
|
||||
}
|
||||
|
||||
// shortcut: has uid
|
||||
if ( selector?.uid ) {
|
||||
const entry = await svc_fsEntryFetcher.findByUID(selector.uid);
|
||||
const entry = await this.fsEntryController.findByUID(selector.uid);
|
||||
return Boolean(entry);
|
||||
}
|
||||
|
||||
// shortcut: parent uid + child name
|
||||
if ( selector instanceof NodeChildSelector && selector.parent instanceof NodeUIDSelector ) {
|
||||
return await svc_fsEntryFetcher.nameExistsUnderParent(selector.parent.uid,
|
||||
return await this.fsEntryController.nameExistsUnderParent(selector.parent.uid,
|
||||
selector.name);
|
||||
}
|
||||
|
||||
// shortcut: parent id + child name
|
||||
if ( selector instanceof NodeChildSelector && selector.parent instanceof NodeInternalIDSelector ) {
|
||||
return await svc_fsEntryFetcher.nameExistsUnderParentID(selector.parent.id,
|
||||
return await this.fsEntryController.nameExistsUnderParentID(selector.parent.id,
|
||||
selector.name);
|
||||
}
|
||||
|
||||
@@ -358,7 +357,7 @@ export default class PuterFSProvider {
|
||||
node,
|
||||
}) {
|
||||
// For Puter FS nodes, we assume we will obtain all properties from
|
||||
// fsEntryService/fsEntryFetcher, except for 'thumbnail' unless it's
|
||||
// fsEntryController, except for 'thumbnail' unless it's
|
||||
// explicitly requested.
|
||||
|
||||
if ( options.tracer == null ) {
|
||||
@@ -409,7 +408,7 @@ export default class PuterFSProvider {
|
||||
entry = await this.fsEntryController.get(maybe_uid, options);
|
||||
controls.log.debug('got an entry from the future');
|
||||
} else {
|
||||
entry = await svc_fsEntryFetcher.find(selector, options);
|
||||
entry = await this.fsEntryController.find(selector, options);
|
||||
}
|
||||
|
||||
if ( ! entry ) {
|
||||
|
||||
@@ -12,7 +12,11 @@ const { PuterPath } = extension.import('fs');
|
||||
const { Context } = extension.import('core');
|
||||
|
||||
const {
|
||||
RootNodeSelector,
|
||||
NodeChildSelector,
|
||||
NodeUIDSelector,
|
||||
NodePathSelector,
|
||||
NodeInternalIDSelector,
|
||||
} = extension.import('core').fs.selectors;
|
||||
|
||||
export default class {
|
||||
@@ -36,6 +40,37 @@ export default class {
|
||||
this.entryListeners_ = {};
|
||||
|
||||
this.mkPromiseForQueueSize_();
|
||||
|
||||
// this list of properties is for read operations
|
||||
// (originally in FSEntryFetcher)
|
||||
this.defaultProperties = [
|
||||
'id',
|
||||
'associated_app_id',
|
||||
'uuid',
|
||||
'public_token',
|
||||
'bucket',
|
||||
'bucket_region',
|
||||
'file_request_token',
|
||||
'user_id',
|
||||
'parent_uid',
|
||||
'is_dir',
|
||||
'is_public',
|
||||
'is_shortcut',
|
||||
'is_symlink',
|
||||
'symlink_path',
|
||||
'shortcut_to',
|
||||
'sort_by',
|
||||
'sort_order',
|
||||
'immutable',
|
||||
'name',
|
||||
'metadata',
|
||||
'modified',
|
||||
'created',
|
||||
'accessed',
|
||||
'size',
|
||||
'layout',
|
||||
'path',
|
||||
];
|
||||
}
|
||||
|
||||
init () {
|
||||
@@ -67,6 +102,7 @@ export default class {
|
||||
});
|
||||
}
|
||||
|
||||
// #region write operations
|
||||
async insert (entry) {
|
||||
const op = new Insert(entry);
|
||||
await this.enqueue_(op);
|
||||
@@ -84,7 +120,9 @@ export default class {
|
||||
await this.enqueue_(op);
|
||||
return op;
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region read operations
|
||||
async fast_get_descendants (uuid) {
|
||||
return (await db.read(`
|
||||
WITH RECURSIVE descendant_cte AS (
|
||||
@@ -160,8 +198,7 @@ export default class {
|
||||
op.apply(answer);
|
||||
}
|
||||
if ( answer.is_diff ) {
|
||||
const fsEntryFetcher = Context.get('services').get('fsEntryFetcher');
|
||||
const base_entry = await fsEntryFetcher.find(new NodeUIDSelector(uuid),
|
||||
const base_entry = await this.find(new NodeUIDSelector(uuid),
|
||||
fetch_entry_options);
|
||||
answer.entry = { ...base_entry, ...answer.entry };
|
||||
}
|
||||
@@ -197,6 +234,203 @@ export default class {
|
||||
return rows[0].total_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a filesystem entry using the provided selector.
|
||||
* @param {Object} selector - The selector object specifying how to find the entry
|
||||
* @param {Object} fetch_entry_options - Options for fetching the entry
|
||||
* @returns {Promise<Object|null>} The filesystem entry or null if not found
|
||||
*/
|
||||
async find (selector, fetch_entry_options) {
|
||||
if ( selector instanceof RootNodeSelector ) {
|
||||
return selector.entry;
|
||||
}
|
||||
if ( selector instanceof NodePathSelector ) {
|
||||
return await this.findByPath(selector.value, fetch_entry_options);
|
||||
}
|
||||
if ( selector instanceof NodeUIDSelector ) {
|
||||
return await this.findByUID(selector.value, fetch_entry_options);
|
||||
}
|
||||
if ( selector instanceof NodeInternalIDSelector ) {
|
||||
return await this.findByID(selector.id, fetch_entry_options);
|
||||
}
|
||||
if ( selector instanceof NodeChildSelector ) {
|
||||
let id;
|
||||
|
||||
if ( selector.parent instanceof RootNodeSelector ) {
|
||||
id = await this.findNameInRoot(selector.name);
|
||||
} else {
|
||||
const parentEntry = await this.find(selector.parent);
|
||||
if ( ! parentEntry ) return null;
|
||||
id = await this.findNameInParent(parentEntry.uuid, selector.name);
|
||||
}
|
||||
|
||||
if ( id === undefined ) return null;
|
||||
if ( typeof id !== 'number' ) {
|
||||
throw new Error('unexpected type for id value',
|
||||
typeof id,
|
||||
id);
|
||||
}
|
||||
return this.find(new NodeInternalIDSelector('mysql', id));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a filesystem entry by its UUID.
|
||||
* @param {string} uuid - The UUID of the entry to find
|
||||
* @param {Object} fetch_entry_options - Options including thumbnail flag
|
||||
* @returns {Promise<Object|undefined>} The filesystem entry or undefined if not found
|
||||
*/
|
||||
async findByUID (uuid, fetch_entry_options = {}) {
|
||||
const { thumbnail } = fetch_entry_options;
|
||||
|
||||
let fsentry = await db.tryHardRead(`SELECT ${
|
||||
this.defaultProperties.join(', ')
|
||||
}${thumbnail ? ', thumbnail' : ''
|
||||
} FROM fsentries WHERE uuid = ? LIMIT 1`,
|
||||
[uuid]);
|
||||
|
||||
return fsentry[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a filesystem entry by its internal database ID.
|
||||
* @param {number} id - The internal ID of the entry to find
|
||||
* @param {Object} fetch_entry_options - Options including thumbnail flag
|
||||
* @returns {Promise<Object|undefined>} The filesystem entry or undefined if not found
|
||||
*/
|
||||
async findByID (id, fetch_entry_options = {}) {
|
||||
const { thumbnail } = fetch_entry_options;
|
||||
|
||||
let fsentry = await db.tryHardRead(`SELECT ${
|
||||
this.defaultProperties.join(', ')
|
||||
}${thumbnail ? ', thumbnail' : ''
|
||||
} FROM fsentries WHERE id = ? LIMIT 1`,
|
||||
[id]);
|
||||
|
||||
return fsentry[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a filesystem entry by its full path.
|
||||
* @param {string} path - The full path of the entry to find
|
||||
* @param {Object} fetch_entry_options - Options including thumbnail flag and tracer
|
||||
* @returns {Promise<Object|false>} The filesystem entry or false if not found
|
||||
*/
|
||||
async findByPath (path, fetch_entry_options = {}) {
|
||||
const { thumbnail } = fetch_entry_options;
|
||||
|
||||
if ( path === '/' ) {
|
||||
return this.find(new RootNodeSelector());
|
||||
}
|
||||
|
||||
const parts = path.split('/').filter(path => path !== '');
|
||||
if ( parts.length === 0 ) {
|
||||
// TODO: invalid path; this should be an error
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: use a closure table for more efficient path resolving
|
||||
let parent_uid = null;
|
||||
let result;
|
||||
|
||||
const resultColsSql = this.defaultProperties.join(', ') +
|
||||
(thumbnail ? ', thumbnail' : '');
|
||||
|
||||
result = await db.read(`SELECT ${ resultColsSql
|
||||
} FROM fsentries WHERE path=? LIMIT 1`,
|
||||
[path]);
|
||||
|
||||
// using knex instead
|
||||
|
||||
if ( result[0] ) return result[0];
|
||||
|
||||
const loop = async () => {
|
||||
for ( let i = 0 ; i < parts.length ; i++ ) {
|
||||
const part = parts[i];
|
||||
const isLast = i == parts.length - 1;
|
||||
const colsSql = isLast ? resultColsSql : 'uuid';
|
||||
if ( parent_uid === null ) {
|
||||
result = await db.read(`SELECT ${ colsSql
|
||||
} FROM fsentries WHERE parent_uid IS NULL AND name=? LIMIT 1`,
|
||||
[part]);
|
||||
} else {
|
||||
result = await db.read(`SELECT ${ colsSql
|
||||
} FROM fsentries WHERE parent_uid=? AND name=? LIMIT 1`,
|
||||
[parent_uid, part]);
|
||||
}
|
||||
|
||||
if ( ! result[0] ) return false;
|
||||
parent_uid = result[0].uuid;
|
||||
}
|
||||
};
|
||||
|
||||
if ( fetch_entry_options.tracer ) {
|
||||
const tracer = fetch_entry_options.tracer;
|
||||
const options = fetch_entry_options.trace_options;
|
||||
await tracer.startActiveSpan('fs:sql:findByPath',
|
||||
...(options ? [options] : []),
|
||||
async span => {
|
||||
await loop();
|
||||
span.end();
|
||||
});
|
||||
} else {
|
||||
await loop();
|
||||
}
|
||||
|
||||
return result[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the ID of a child entry with the given name in the root directory.
|
||||
* @param {string} name - The name of the child entry to find
|
||||
* @returns {Promise<number|undefined>} The ID of the child entry or undefined if not found
|
||||
*/
|
||||
async findNameInRoot (name) {
|
||||
let child_id = await db.read('SELECT `id` FROM `fsentries` WHERE `parent_uid` IS NULL AND name = ? LIMIT 1',
|
||||
[name]);
|
||||
return child_id[0]?.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the ID of a child entry with the given name under a specific parent.
|
||||
* @param {string} parent_uid - The UUID of the parent directory
|
||||
* @param {string} name - The name of the child entry to find
|
||||
* @returns {Promise<number|undefined>} The ID of the child entry or undefined if not found
|
||||
*/
|
||||
async findNameInParent (parent_uid, name) {
|
||||
let child_id = await db.read('SELECT `id` FROM `fsentries` WHERE `parent_uid` = ? AND name = ? LIMIT 1',
|
||||
[parent_uid, name]);
|
||||
return child_id[0]?.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entry with the given name exists under a specific parent.
|
||||
* @param {string} parent_uid - The UUID of the parent directory
|
||||
* @param {string} name - The name to check for
|
||||
* @returns {Promise<boolean>} True if the name exists under the parent, false otherwise
|
||||
*/
|
||||
async nameExistsUnderParent (parent_uid, name) {
|
||||
let check_dupe = await db.read('SELECT `id` FROM `fsentries` WHERE `parent_uid` = ? AND name = ? LIMIT 1',
|
||||
[parent_uid, name]);
|
||||
return !!check_dupe[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entry with the given name exists under a parent specified by ID.
|
||||
* @param {number} parent_id - The internal ID of the parent directory
|
||||
* @param {string} name - The name to check for
|
||||
* @returns {Promise<boolean>} True if the name exists under the parent, false otherwise
|
||||
*/
|
||||
async nameExistsUnderParentID (parent_id, name) {
|
||||
const parent = await this.findByID(parent_id);
|
||||
if ( ! parent ) {
|
||||
return false;
|
||||
}
|
||||
return this.nameExistsUnderParent(parent.uuid, name);
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region queue logic
|
||||
async enqueue_ (op) {
|
||||
while (
|
||||
this.currentState.queue.length > this.max_queue ||
|
||||
@@ -296,4 +530,5 @@ export default class {
|
||||
this.mkPromiseForQueueSize_();
|
||||
queueSizeResolve();
|
||||
}
|
||||
// #endregion
|
||||
}
|
||||
@@ -16,23 +16,23 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const APIError = require("../../api/APIError");
|
||||
const FSNodeParam = require("../../api/filesystem/FSNodeParam");
|
||||
const FlagParam = require("../../api/filesystem/FlagParam");
|
||||
const StringParam = require("../../api/filesystem/StringParam");
|
||||
const UserParam = require("../../api/filesystem/UserParam");
|
||||
const config = require("../../config");
|
||||
const { chkperm, validate_fsentry_name } = require("../../helpers");
|
||||
const { TeePromise } = require("@heyputer/putility").libs.promise;
|
||||
const { pausing_tee, logging_stream, offset_write_stream, stream_to_the_void } = require("../../util/streamutil");
|
||||
const { TYPE_DIRECTORY } = require("../FSNodeContext");
|
||||
const { LLRead } = require("../ll_operations/ll_read");
|
||||
const { RootNodeSelector, NodePathSelector } = require("../node/selectors");
|
||||
const { is_valid_node_name } = require("../validation");
|
||||
const { HLFilesystemOperation } = require("./definitions");
|
||||
const { MkTree } = require("./hl_mkdir");
|
||||
const { Actor } = require("../../services/auth/Actor");
|
||||
const { LLCWrite, LLOWrite } = require("../ll_operations/ll_write");
|
||||
const APIError = require('../../api/APIError');
|
||||
const FSNodeParam = require('../../api/filesystem/FSNodeParam');
|
||||
const FlagParam = require('../../api/filesystem/FlagParam');
|
||||
const StringParam = require('../../api/filesystem/StringParam');
|
||||
const UserParam = require('../../api/filesystem/UserParam');
|
||||
const config = require('../../config');
|
||||
const { chkperm, validate_fsentry_name } = require('../../helpers');
|
||||
const { TeePromise } = require('@heyputer/putility').libs.promise;
|
||||
const { pausing_tee, logging_stream, offset_write_stream, stream_to_the_void } = require('../../util/streamutil');
|
||||
const { TYPE_DIRECTORY } = require('../FSNodeContext');
|
||||
const { LLRead } = require('../ll_operations/ll_read');
|
||||
const { RootNodeSelector, NodePathSelector } = require('../node/selectors');
|
||||
const { is_valid_node_name } = require('../validation');
|
||||
const { HLFilesystemOperation } = require('./definitions');
|
||||
const { MkTree } = require('./hl_mkdir');
|
||||
const { Actor } = require('../../services/auth/Actor');
|
||||
const { LLCWrite, LLOWrite } = require('../ll_operations/ll_write');
|
||||
|
||||
class WriteCommonFeature {
|
||||
install_in_instance (instance) {
|
||||
@@ -43,7 +43,7 @@ class WriteCommonFeature {
|
||||
) {
|
||||
throw APIError.create('file_too_large', null, {
|
||||
max_size: config.max_file_size,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -52,9 +52,9 @@ class WriteCommonFeature {
|
||||
) {
|
||||
throw APIError.create('thumbnail_too_large', null, {
|
||||
max_size: config.max_thumbnail_size,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
instance._verify_room = async function () {
|
||||
if ( ! this.values.file ) return;
|
||||
@@ -67,10 +67,10 @@ class WriteCommonFeature {
|
||||
|
||||
const usage = await sizeService.get_usage(user.id);
|
||||
const capacity = await sizeService.get_storage_capacity(user.id);
|
||||
if( capacity - usage - file.size < 0 ) {
|
||||
if ( capacity - usage - file.size < 0 ) {
|
||||
throw APIError.create('storage_limit_reached');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,11 +85,11 @@ class HLWrite extends HLFilesystemOperation {
|
||||
- deduplicate files with the same name
|
||||
// - create thumbnails; this will happen in low-level operation for now
|
||||
- create shortcuts
|
||||
`
|
||||
`;
|
||||
|
||||
static FEATURES = [
|
||||
new WriteCommonFeature(),
|
||||
]
|
||||
];
|
||||
|
||||
static PARAMETERS = {
|
||||
// the parent directory, or a filepath that doesn't exist yet
|
||||
@@ -117,7 +117,7 @@ class HLWrite extends HLFilesystemOperation {
|
||||
static MODULES = {
|
||||
_path: require('path'),
|
||||
mime: require('mime-types'),
|
||||
}
|
||||
};
|
||||
|
||||
async _run () {
|
||||
const { context, values } = this;
|
||||
@@ -134,7 +134,7 @@ class HLWrite extends HLFilesystemOperation {
|
||||
|
||||
this.checkpoint('before parent exists check');
|
||||
|
||||
if ( ! await parent.exists() && values.create_missing_parents ) {
|
||||
if ( !await parent.exists() && values.create_missing_parents ) {
|
||||
if ( ! (parent.selector instanceof NodePathSelector) ) {
|
||||
throw APIError.create('dest_does_not_exist', null, {
|
||||
parent: parent.selector,
|
||||
@@ -178,7 +178,7 @@ class HLWrite extends HLFilesystemOperation {
|
||||
|
||||
this.checkpoint('check parent DNE or is not a directory');
|
||||
if (
|
||||
! await parent.exists() ||
|
||||
!await parent.exists() ||
|
||||
await parent.get('type') !== TYPE_DIRECTORY
|
||||
) {
|
||||
destination = parent;
|
||||
@@ -219,16 +219,16 @@ class HLWrite extends HLFilesystemOperation {
|
||||
|
||||
const dest_exists = await destination.exists();
|
||||
|
||||
if ( values.offset !== undefined && ! dest_exists ) {
|
||||
if ( values.offset !== undefined && !dest_exists ) {
|
||||
throw APIError.create('offset_without_existing_file');
|
||||
}
|
||||
|
||||
|
||||
// The correct ACL check here depends on context.
|
||||
// ll_write checks ACL, but we need to shortcut it here
|
||||
// or else we might send the user too much information.
|
||||
{
|
||||
const node_to_check =
|
||||
( dest_exists && overwrite && ! dedupe_name )
|
||||
( dest_exists && overwrite && !dedupe_name )
|
||||
? destination : parent;
|
||||
|
||||
const actor = values.actor ?? Actor.adapt(values.user);
|
||||
@@ -239,17 +239,16 @@ class HLWrite extends HLFilesystemOperation {
|
||||
}
|
||||
|
||||
if ( dest_exists ) {
|
||||
if ( ! overwrite && ! dedupe_name ) {
|
||||
if ( !overwrite && !dedupe_name ) {
|
||||
throw APIError.create('item_with_same_name_exists', null, {
|
||||
entry_name: target_name
|
||||
entry_name: target_name,
|
||||
});
|
||||
}
|
||||
|
||||
if ( dedupe_name ) {
|
||||
const fsEntryFetcher = context.get('services').get('fsEntryFetcher');
|
||||
const target_ext = _path.extname(target_name);
|
||||
const target_noext = _path.basename(target_name, target_ext);
|
||||
for ( let i=1 ;; i++ ) {
|
||||
for ( let i = 1 ;; i++ ) {
|
||||
const try_new_name = `${target_noext} (${i})${target_ext}`;
|
||||
const exists = await parent.hasChild(try_new_name);
|
||||
if ( ! exists ) {
|
||||
@@ -302,53 +301,55 @@ class HLWrite extends HLFilesystemOperation {
|
||||
let thumbnail_promise = new TeePromise();
|
||||
if ( await parent.isAppDataDirectory() || values.no_thumbnail ) {
|
||||
thumbnail_promise.resolve(undefined);
|
||||
} else (async () => {
|
||||
const reason = await (async () => {
|
||||
const { mime } = this.modules;
|
||||
const thumbnails = context.get('services').get('thumbnails');
|
||||
if ( values.thumbnail ) return 'already thumbnail';
|
||||
} else {
|
||||
(async () => {
|
||||
const reason = await (async () => {
|
||||
const { mime } = this.modules;
|
||||
const thumbnails = context.get('services').get('thumbnails');
|
||||
if ( values.thumbnail ) return 'already thumbnail';
|
||||
|
||||
const content_type = mime.contentType(target_name);
|
||||
this.log.debug('CONTENT TYPE', content_type);
|
||||
if ( ! content_type ) return 'no content type';
|
||||
if ( ! thumbnails.is_supported_mimetype(content_type) ) return 'unsupported content type';
|
||||
if ( ! thumbnails.is_supported_size(values.file.size) ) return 'too large';
|
||||
const content_type = mime.contentType(target_name);
|
||||
this.log.debug('CONTENT TYPE', content_type);
|
||||
if ( ! content_type ) return 'no content type';
|
||||
if ( ! thumbnails.is_supported_mimetype(content_type) ) return 'unsupported content type';
|
||||
if ( ! thumbnails.is_supported_size(values.file.size) ) return 'too large';
|
||||
|
||||
// Create file object for thumbnail by either using an existing
|
||||
// buffer (ex: /download endpoint) or by forking a stream
|
||||
// (ex: /write and /batch endpoints).
|
||||
const thumb_file = (() => {
|
||||
if ( values.file.buffer ) return values.file;
|
||||
// Create file object for thumbnail by either using an existing
|
||||
// buffer (ex: /download endpoint) or by forking a stream
|
||||
// (ex: /write and /batch endpoints).
|
||||
const thumb_file = (() => {
|
||||
if ( values.file.buffer ) return values.file;
|
||||
|
||||
const [replace_stream, thumbnail_stream] =
|
||||
pausing_tee(values.file.stream, 2);
|
||||
const [replace_stream, thumbnail_stream] =
|
||||
pausing_tee(values.file.stream, 2);
|
||||
|
||||
values.file.stream = replace_stream;
|
||||
return { ...values.file, stream: thumbnail_stream };
|
||||
values.file.stream = replace_stream;
|
||||
return { ...values.file, stream: thumbnail_stream };
|
||||
})();
|
||||
|
||||
let thumbnail;
|
||||
try {
|
||||
thumbnail = await thumbnails.thumbify(thumb_file);
|
||||
} catch (e) {
|
||||
stream_to_the_void(thumb_file.stream);
|
||||
return `thumbnail error: ${ e.message}`;
|
||||
}
|
||||
|
||||
const thumbnailData = { url: thumbnail };
|
||||
if ( thumbnailData.url ) {
|
||||
await svc_event.emit('thumbnail.created', thumbnailData); // An extension can modify where this thumbnail is stored
|
||||
}
|
||||
|
||||
thumbnail_promise.resolve(thumbnailData.url);
|
||||
})();
|
||||
|
||||
let thumbnail;
|
||||
try {
|
||||
thumbnail = await thumbnails.thumbify(thumb_file);
|
||||
} catch (e) {
|
||||
stream_to_the_void(thumb_file.stream);
|
||||
return 'thumbnail error: ' + e.message;
|
||||
}
|
||||
|
||||
const thumbnailData = { url: thumbnail }
|
||||
if (thumbnailData.url) {
|
||||
await svc_event.emit('thumbnail.created', thumbnailData); // An extension can modify where this thumbnail is stored
|
||||
}
|
||||
|
||||
thumbnail_promise.resolve(thumbnailData.url);
|
||||
})();
|
||||
if ( reason ) {
|
||||
this.log.debug('REASON', reason);
|
||||
thumbnail_promise.resolve(undefined);
|
||||
if ( reason ) {
|
||||
this.log.debug('REASON', reason);
|
||||
thumbnail_promise.resolve(undefined);
|
||||
|
||||
// values.file.stream = logging_stream(values.file.stream);
|
||||
}
|
||||
})();
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
this.checkpoint('before delegate');
|
||||
|
||||
|
||||
@@ -1,291 +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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const { DB_READ } = require("../../services/database/consts");
|
||||
const { NodePathSelector, NodeUIDSelector, NodeInternalIDSelector, NodeChildSelector, RootNodeSelector } = require("../../filesystem/node/selectors");
|
||||
const BaseService = require("../../services/BaseService");
|
||||
|
||||
/**
|
||||
* Service for fetching filesystem entries from the database using various selector types.
|
||||
* Handles different methods of locating files and directories in the filesystem.
|
||||
*/
|
||||
module.exports = class DatabaseFSEntryFetcher extends BaseService {
|
||||
static CONCERN = 'filesystem';
|
||||
|
||||
/**
|
||||
* Initializes the default properties that will be selected from the database.
|
||||
*/
|
||||
_construct () {
|
||||
this.defaultProperties = [
|
||||
'id',
|
||||
'associated_app_id',
|
||||
'uuid',
|
||||
'public_token',
|
||||
'bucket',
|
||||
'bucket_region',
|
||||
'file_request_token',
|
||||
'user_id',
|
||||
'parent_uid',
|
||||
'is_dir',
|
||||
'is_public',
|
||||
'is_shortcut',
|
||||
'is_symlink',
|
||||
'symlink_path',
|
||||
'shortcut_to',
|
||||
'sort_by',
|
||||
'sort_order',
|
||||
'immutable',
|
||||
'name',
|
||||
'metadata',
|
||||
'modified',
|
||||
'created',
|
||||
'accessed',
|
||||
'size',
|
||||
'layout',
|
||||
'path',
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the database connection for filesystem operations.
|
||||
*/
|
||||
_init () {
|
||||
this.db = this.services.get('database').get(DB_READ, 'filesystem');
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a filesystem entry using the provided selector.
|
||||
* @param {Object} selector - The selector object specifying how to find the entry
|
||||
* @param {Object} fetch_entry_options - Options for fetching the entry
|
||||
* @returns {Promise<Object|null>} The filesystem entry or null if not found
|
||||
*/
|
||||
async find (selector, fetch_entry_options) {
|
||||
if ( selector instanceof RootNodeSelector ) {
|
||||
return selector.entry;
|
||||
}
|
||||
if ( selector instanceof NodePathSelector ) {
|
||||
return await this.findByPath(
|
||||
selector.value, fetch_entry_options);
|
||||
}
|
||||
if ( selector instanceof NodeUIDSelector ) {
|
||||
return await this.findByUID(
|
||||
selector.value, fetch_entry_options);
|
||||
}
|
||||
if ( selector instanceof NodeInternalIDSelector ) {
|
||||
return await this.findByID(
|
||||
selector.id, fetch_entry_options);
|
||||
}
|
||||
if ( selector instanceof NodeChildSelector ) {
|
||||
let id;
|
||||
|
||||
if ( selector.parent instanceof RootNodeSelector ) {
|
||||
id = await this.findNameInRoot(selector.name);
|
||||
} else {
|
||||
const parentEntry = await this.find(selector.parent);
|
||||
if ( ! parentEntry ) return null;
|
||||
id = await this.findNameInParent(
|
||||
parentEntry.uuid, selector.name
|
||||
);
|
||||
}
|
||||
|
||||
if ( id === undefined ) return null;
|
||||
if ( typeof id !== 'number' ) {
|
||||
throw new Error(
|
||||
'unexpected type for id value',
|
||||
typeof id,
|
||||
id
|
||||
);
|
||||
}
|
||||
return this.find(new NodeInternalIDSelector('mysql', id));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a filesystem entry by its UUID.
|
||||
* @param {string} uuid - The UUID of the entry to find
|
||||
* @param {Object} fetch_entry_options - Options including thumbnail flag
|
||||
* @returns {Promise<Object|undefined>} The filesystem entry or undefined if not found
|
||||
*/
|
||||
async findByUID(uuid, fetch_entry_options = {}) {
|
||||
const { thumbnail } = fetch_entry_options;
|
||||
|
||||
let fsentry = await this.db.tryHardRead(
|
||||
`SELECT ` +
|
||||
this.defaultProperties.join(', ') +
|
||||
(thumbnail ? `, thumbnail` : '') +
|
||||
` FROM fsentries WHERE uuid = ? LIMIT 1`,
|
||||
[uuid]
|
||||
);
|
||||
|
||||
return fsentry[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a filesystem entry by its internal database ID.
|
||||
* @param {number} id - The internal ID of the entry to find
|
||||
* @param {Object} fetch_entry_options - Options including thumbnail flag
|
||||
* @returns {Promise<Object|undefined>} The filesystem entry or undefined if not found
|
||||
*/
|
||||
async findByID(id, fetch_entry_options = {}) {
|
||||
const { thumbnail } = fetch_entry_options;
|
||||
|
||||
let fsentry = await this.db.tryHardRead(
|
||||
`SELECT ` +
|
||||
this.defaultProperties.join(', ') +
|
||||
(thumbnail ? `, thumbnail` : '') +
|
||||
` FROM fsentries WHERE id = ? LIMIT 1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
return fsentry[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a filesystem entry by its full path.
|
||||
* @param {string} path - The full path of the entry to find
|
||||
* @param {Object} fetch_entry_options - Options including thumbnail flag and tracer
|
||||
* @returns {Promise<Object|false>} The filesystem entry or false if not found
|
||||
*/
|
||||
async findByPath(path, fetch_entry_options = {}) {
|
||||
const { thumbnail } = fetch_entry_options;
|
||||
|
||||
if ( path === '/' ) {
|
||||
return this.find(new RootNodeSelector());
|
||||
}
|
||||
|
||||
const parts = path.split('/').filter(path => path !== '');
|
||||
if ( parts.length === 0 ) {
|
||||
// TODO: invalid path; this should be an error
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// TODO: use a closure table for more efficient path resolving
|
||||
let parent_uid = null;
|
||||
let result;
|
||||
|
||||
const resultColsSql = this.defaultProperties.join(', ') +
|
||||
(thumbnail ? `, thumbnail` : '');
|
||||
|
||||
result = await this.db.read(
|
||||
`SELECT ` + resultColsSql +
|
||||
` FROM fsentries WHERE path=? LIMIT 1`,
|
||||
[path]
|
||||
);
|
||||
|
||||
// using knex instead
|
||||
|
||||
if ( result[0] ) return result[0];
|
||||
|
||||
this.log.debug(`findByPath (not cached): ${path}`)
|
||||
|
||||
const loop = async () => {
|
||||
for ( let i=0 ; i < parts.length ; i++ ) {
|
||||
const part = parts[i];
|
||||
const isLast = i == parts.length - 1;
|
||||
const colsSql = isLast ? resultColsSql : 'uuid';
|
||||
if ( parent_uid === null ) {
|
||||
result = await this.db.read(
|
||||
`SELECT ` + colsSql +
|
||||
` FROM fsentries WHERE parent_uid IS NULL AND name=? LIMIT 1`,
|
||||
[part]
|
||||
);
|
||||
} else {
|
||||
result = await this.db.read(
|
||||
`SELECT ` + colsSql +
|
||||
` FROM fsentries WHERE parent_uid=? AND name=? LIMIT 1`,
|
||||
[parent_uid, part]
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! result[0] ) return false;
|
||||
parent_uid = result[0].uuid;
|
||||
}
|
||||
}
|
||||
|
||||
if ( fetch_entry_options.tracer ) {
|
||||
const tracer = fetch_entry_options.tracer;
|
||||
const options = fetch_entry_options.trace_options;
|
||||
await tracer.startActiveSpan(`fs:sql:findByPath`,
|
||||
...(options ? [options] : []),
|
||||
async span => {
|
||||
await loop();
|
||||
span.end();
|
||||
});
|
||||
} else {
|
||||
await loop();
|
||||
}
|
||||
|
||||
return result[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the ID of a child entry with the given name in the root directory.
|
||||
* @param {string} name - The name of the child entry to find
|
||||
* @returns {Promise<number|undefined>} The ID of the child entry or undefined if not found
|
||||
*/
|
||||
async findNameInRoot (name) {
|
||||
let child_id = await this.db.read(
|
||||
"SELECT `id` FROM `fsentries` WHERE `parent_uid` IS NULL AND name = ? LIMIT 1",
|
||||
[name]
|
||||
);
|
||||
return child_id[0]?.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the ID of a child entry with the given name under a specific parent.
|
||||
* @param {string} parent_uid - The UUID of the parent directory
|
||||
* @param {string} name - The name of the child entry to find
|
||||
* @returns {Promise<number|undefined>} The ID of the child entry or undefined if not found
|
||||
*/
|
||||
async findNameInParent (parent_uid, name) {
|
||||
let child_id = await this.db.read(
|
||||
"SELECT `id` FROM `fsentries` WHERE `parent_uid` = ? AND name = ? LIMIT 1",
|
||||
[parent_uid, name]
|
||||
);
|
||||
return child_id[0]?.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entry with the given name exists under a specific parent.
|
||||
* @param {string} parent_uid - The UUID of the parent directory
|
||||
* @param {string} name - The name to check for
|
||||
* @returns {Promise<boolean>} True if the name exists under the parent, false otherwise
|
||||
*/
|
||||
async nameExistsUnderParent (parent_uid, name) {
|
||||
let check_dupe = await this.db.read(
|
||||
"SELECT `id` FROM `fsentries` WHERE `parent_uid` = ? AND name = ? LIMIT 1",
|
||||
[parent_uid, name]
|
||||
);
|
||||
return !! check_dupe[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entry with the given name exists under a parent specified by ID.
|
||||
* @param {number} parent_id - The internal ID of the parent directory
|
||||
* @param {string} name - The name to check for
|
||||
* @returns {Promise<boolean>} True if the name exists under the parent, false otherwise
|
||||
*/
|
||||
async nameExistsUnderParentID (parent_id, name) {
|
||||
const parent = await this.findByID(parent_id);
|
||||
if ( ! parent ) {
|
||||
return false;
|
||||
}
|
||||
return this.nameExistsUnderParent(parent.uuid, name);
|
||||
}
|
||||
}
|
||||
@@ -63,9 +63,6 @@ class PuterFSModule extends AdvancedBase {
|
||||
const { MountpointService } = require('./MountpointService');
|
||||
services.registerService('mountpoint', MountpointService);
|
||||
|
||||
const DatabaseFSEntryFetcher = require('./DatabaseFSEntryFetcher');
|
||||
services.registerService('fsEntryFetcher', DatabaseFSEntryFetcher);
|
||||
|
||||
const { MemoryFSService } = require('./customfs/MemoryFSService');
|
||||
services.registerService('memoryfs', MemoryFSService);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user