Skip to content
Open
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
4 changes: 4 additions & 0 deletions src/backend/src/api/APIError.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ module.exports = class APIError {
status: 422,
message: 'Cannot write an item to the root directory.',
},
'cannot_create_in_root': {
status: 403,
message: 'Directories cannot be created in the root directory.',
},
'cannot_overwrite_a_directory': {
status: 422,
message: 'Cannot overwrite a directory.',
Expand Down
49 changes: 43 additions & 6 deletions src/backend/src/filesystem/hl_operations/hl_mkdir.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,20 +270,58 @@ class HLMkdir extends HLFilesystemOperation {
}

let parent_node = values.parent || await fs.node(new RootNodeSelector());
console.log('USING PARENT', parent_node.selector.describe());
let target_basename = _path.basename(values.path);

// Check if trying to create a directory directly in the root directory
// This check applies regardless of create_missing_parents flag
const path_dirname = _path.dirname(values.path);
if ( parent_node.isRoot && (path_dirname === '/' || path_dirname === '.' || path_dirname === '') ) {
throw APIError.create('cannot_create_in_root');
}

// Check if create_missing_parents would cause creation in root
// For example: mkdir('/foo/bar', { create_missing_parents: true }) would create /foo in root
// Note: User directories are allowed (they already exist), so we check if the first component
// would be a user directory before blocking. This check occurs BEFORE any directory creation.
if ( values.create_missing_parents && parent_node.isRoot && path_dirname !== '/' && path_dirname !== '.' && path_dirname !== '' ) {
// Extract the first component of the path that would be created in root
const path_parts = values.path.split('/').filter(Boolean);
if ( path_parts.length > 0 ) {
const first_component = path_parts[0];
// Check if the first component is a user directory (user directories already exist)
const first_component_node = await fs.node(new NodePathSelector(`/${first_component}`));
await first_component_node.fetchEntry();

// If the directory doesn't exist, block creation in root
if ( ! await first_component_node.exists() ) {
throw APIError.create('cannot_create_in_root');
}

// Check if it's a user directory (user directories are allowed)
const is_user_dir = await first_component_node.isUserDirectory();

// Only block if it's NOT a user directory (user directories are allowed)
if ( !is_user_dir) {
throw APIError.create('cannot_create_in_root');
}
}
}

const top_parent = values.create_missing_parents
? await this._create_top_parent({ top_parent: parent_node })
: await this._get_existing_top_parent({ top_parent: parent_node })
;

// `parent_node` becomes the parent of the last directory name
// specified under `path`.
parent_node = await this._create_parents({
parent_node: top_parent,
actor: values.actor,
});
parent_node = values.create_missing_parents
? await this._create_parents({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will values.create_missing_parents ever return a falsy value? if it will not, then logic below will never run

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, values.create_missing_parents can be falsy, and the _get_existing_parent branch will run in those cases. Here's why:

  1. FlagParam Default Behavior
    In FlagParam.js (line 26), when a flag is optional: true and no value is provided, it defaults to false:
    this.default = this.options.default ?? false;

  2. Router Handling
    In the mkdir.js router (lines 79-82), boolify() is used which can return false:

create_missing_parents: boolify(
    req.body.create_missing_ancestors ??
    req.body.create_missing_parents
)

If neither create_missing_ancestors nor create_missing_parents is provided in the request body, or if they're explicitly set to false, the value will be false.

  1. When the Branch Runs
    The _get_existing_parent branch will execute when:
  • create_missing_parents is not provided in the request (defaults to false)
  • create_missing_parents is explicitly set to false
  • The client doesn't pass the flag at all

The ternary at lines 317-324 is correct and both branches are reachable. The _get_existing_parent path is used when the caller expects all parent directories to already exist, which is a valid use case.

parent_node: top_parent,
actor: values.actor,
})
: await this._get_existing_parent({
parent_node: top_parent,
});

const user_id = values.actor.type.user.id;

Expand Down Expand Up @@ -413,7 +451,6 @@ class HLMkdir extends HLFilesystemOperation {
const node = await fs.node(current);

if ( ! await node.exists() ) {
console.log('HERE FROM', node.selector.describe(), parent_node.selector.describe());
throw APIError.create('dest_does_not_exist');
}

Expand Down
5 changes: 2 additions & 3 deletions src/puter-js/src/modules/FileSystem/operations/mkdir.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,12 @@ const mkdir = function (...args) {
options.path = getAbsolutePathForApp(options.path);

xhr.send(JSON.stringify({
parent: path.dirname(options.path),
path: path.basename(options.path),
path: options.path,
overwrite: options.overwrite ?? false,
dedupe_name: (options.rename || options.dedupeName) ?? false,
shortcut_to: options.shortcutTo,
original_client_socket_id: this.socket.id,
create_missing_parents: (options.recursive || options.createMissingParents) ?? false,
create_missing_parents: (options.recursive || options.createMissingParents || options.create_missing_parents) ?? false,
}));
})
}
Expand Down