Skip to content
This repository has been archived by the owner on Apr 13, 2024. It is now read-only.

Implement filesystem functions for node.js #19

Merged
merged 8 commits into from
Feb 15, 2024
Merged
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
137 changes: 136 additions & 1 deletion src/platform/node/filesystem.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
/*
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Phoenix Shell.
*
* Phoenix Shell 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/>.
*/
import fs from 'fs';
import path_ from 'path';

import modeString from 'fs-mode-to-string';

import { DestinationIsDirectoryError, DestinationIsNotDirectoryError } from "../definitions.js";

export const CreateFilesystemProvider = () => {
return {
Expand Down Expand Up @@ -37,5 +55,122 @@ export const CreateFilesystemProvider = () => {

return items;
},
stat: async (path) => {
const stat = await fs.promises.stat(path);
const fullPath = await fs.promises.realpath(path);
const parsedPath = path_.parse(fullPath);
// TODO: Fill in more of these?
return {
id: stat.ino,
associated_app_id: null,
public_token: null,
file_request_token: null,
uid: stat.uid,
parent_id: null,
parent_uid: null,
is_dir: stat.isDirectory(),
is_public: null,
is_shortcut: null,
is_symlink: stat.isSymbolicLink(),
symlink_path: null,
sort_by: null,
sort_order: null,
immutable: null,
name: parsedPath.base,
path: fullPath,
dirname: parsedPath.dir,
dirpath: parsedPath.dir,
metadata: null,
modified: stat.mtime,
created: stat.birthtime,
accessed: stat.atime,
size: stat.size,
layout: null,
owner: null,
type: null,
is_empty: await (async (stat) => {
if (!stat.isDirectory())
return null;
const children = await fs.promises.readdir(path);
return children.length === 0;
})(stat),
};
},
mkdir: async (path, options = { createMissingParents: false }) => {
const createMissingParents = options['createMissingParents'] || false;
return await fs.promises.mkdir(path, { recursive: createMissingParents });
},
read: async (path) => {
return await fs.promises.readFile(path);
},
write: async (path, data) => {
if (data instanceof Blob) {
return await fs.promises.writeFile(path, data.stream());
}
return await fs.promises.writeFile(path, data);
},
rm: async (path, options = { recursive: false }) => {
const recursive = options['recursive'] || false;
const stat = await fs.promises.stat(path);

if ( stat.isDirectory() && ! recursive ) {
throw new DestinationIsDirectoryError(path);
}

return await fs.promises.rm(path, { recursive });
},
rmdir: async (path) => {
const stat = await fs.promises.stat(path);

if ( !stat.isDirectory() ) {
throw new DestinationIsNotDirectoryError(path);
}

return await fs.promises.rmdir(path);
},
move: async (oldPath, newPath) => {
let destStat = null;
try {
destStat = await fs.promises.stat(newPath);
} catch (e) {
if ( e.code !== 'ENOENT' ) throw e;
}

// fs.promises.rename() expects the new path to include the filename.
// So, if newPath is a directory, append the old filename to it to produce the target path and name.
if ( destStat && destStat.isDirectory() ) {
if ( ! newPath.endsWith('/') ) newPath += '/';
newPath += path_.basename(oldPath);
}

return await fs.promises.rename(oldPath, newPath);
},
copy: async (oldPath, newPath) => {
const srcStat = await fs.promises.stat(oldPath);
const srcIsDir = srcStat.isDirectory();

let destStat = null;
try {
destStat = await fs.promises.stat(newPath);
} catch (e) {
if ( e.code !== 'ENOENT' ) throw e;
}
const destIsDir = destStat && destStat.isDirectory();

// fs.promises.cp() is experimental, but does everything we want. Maybe implement this manually if needed.

// `dir -> file`: invalid
if ( srcIsDir && ! destIsDir ) {
throw Error('Cannot copy a directory into a file');
}

// `file -> dir`: fs.promises.cp() expects the new path to include the filename.
if ( ! srcIsDir && destIsDir ) {
if ( ! newPath.endsWith('/') ) newPath += '/';
newPath += path_.basename(oldPath);
}

return await fs.promises.cp(oldPath, newPath, { recursive: srcIsDir });
AtkinsSJ marked this conversation as resolved.
Show resolved Hide resolved
}
};
};
4 changes: 2 additions & 2 deletions src/puter-shell/coreutils/cp.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ export default {
const { filesystem } = ctx.platform;

if ( positionals.length < 1 ) {
err.write('mv: missing file operand\n');
err.write('cp: missing file operand\n');
return;
}

const srcRelPath = positionals.shift();

if ( positionals.length < 1 ) {
const aft = positionals[0];
err.write(`mv: missing destination file operand after '${aft}'\n`);
err.write(`cp: missing destination file operand after '${aft}'\n`);
return;
}

Expand Down
4 changes: 2 additions & 2 deletions src/puter-shell/coreutils/mkdir.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ export default {
target = path.resolve(ctx.vars.pwd, target);
}

const result = await filesystem.mkdir(target);
const result = await filesystem.mkdir(target, { createMissingParents: values.parents });

if ( result.$ === 'error' ) {
if ( result && result.$ === 'error' ) {
throw new Error(result.message);
}
}
Expand Down
7 changes: 6 additions & 1 deletion src/puter-shell/coreutils/touch.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default {
execute: async ctx => {
const { positionals } = ctx.locals;
const { filesystem } = ctx.platform;
const POSIX = filesystem.capabilities['readdir.posix-mode'];

if ( positionals.length === 0 ) {
await ctx.externs.err.write('touch: missing file operand');
Expand All @@ -48,7 +49,11 @@ export default {
try {
stat = await filesystem.stat(path);
} catch (e) {
if ( e.code !== 'subject_does_not_exist' ) throw e;
if ( POSIX ) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This gives me an idea; we should probably translate errors inside platform support. filesystem.capabilities is meant to represent more significant filesystem differences (ex: puter directories can be associated with subdomains, ACL vs POSIX-style permissions, etc)

I like the idea of having an object called ERR_NO_ENTITY (could have properties like posixLabel: 'ENOENT', description: '...') - then it's just an object reference comparison instead of a string comparison here too.

I might merge this first and then we can change this after.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense! The less the builtins have to know/care about the platform, the better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can't find any documentation for Puter SDK errors. I'd need to know their codes to be able to translate them - and generally it would be useful for API users if https://docs.puter.com listed the errors for each API call.

if ( e.code !== 'ENOENT' ) throw e;
} else {
if ( e.code !== 'subject_does_not_exist' ) throw e;
}
}

if ( stat ) continue;
Expand Down