From 20ef5ec6ed8753604cac55635f3ed36f348a08f1 Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 16:00:54 +0700 Subject: [PATCH 01/18] fix: rename var --- lib/command/index.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/command/index.ts b/lib/command/index.ts index 73e3c1f..224e203 100644 --- a/lib/command/index.ts +++ b/lib/command/index.ts @@ -23,47 +23,47 @@ export async function execute(command: string): Promise { if (!args.length) { return [[{ content: ' ', color: Color.WHITE }]]; } - let res; + let output; switch (args[0] as Command) { case Command.ECHO: - res = echo(...(args as any)); + output = echo(...(args as any)); break; case Command.HELP: - res = help(...args as any); + output = help(...args as any); break; case Command.CD: - res = await cd(...args as any); + output = await cd(...args as any); break; case Command.SU: - res = await su(...args as any); + output = await su(...args as any); break; case Command.LS: - res = await ls(...args as any); + output = await ls(...args as any); break; case Command.USERADD: - res = await useradd(...args as any); + output = await useradd(...args as any); break; case Command.TOUCH: - res = await touch(...args as any); + output = await touch(...args as any); break; case Command.MKDIR: - res = await mkdir(...args as any); + output = await mkdir(...args as any); break; case Command.UMASK: - res = await umask(...args as any); + output = await umask(...args as any); break; case Command.CP: - res = await cp(...args as any); + output = await cp(...args as any); break; case Command.MV: - res = await mv(...args as any); + output = await mv(...args as any); break; case Command.RM: - res = await rm(...args as any); + output = await rm(...args as any); break; default: - res = echo('echo', ' ', `Unknown command:\\u001b[31m ${args[0]}`); + output = echo('echo', ' ', `Unknown command:\\u001b[31m ${args[0]}`); break; } - return interpretAnsiEscapeColor(res); + return interpretAnsiEscapeColor(output); } From 72a90175ef471b4f1c4bde5868a5a611eb3be7cd Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 16:05:47 +0700 Subject: [PATCH 02/18] fix: split CommandFunc into AsyncCommandFunc and CommandFunc --- lib/command/impls/cd.ts | 4 ++-- lib/command/impls/cp.ts | 4 ++-- lib/command/impls/ls.ts | 4 ++-- lib/command/impls/mkdir.ts | 4 ++-- lib/command/impls/mv.ts | 4 ++-- lib/command/impls/rm.ts | 4 ++-- lib/command/impls/su.ts | 4 ++-- lib/command/impls/touch.ts | 4 ++-- lib/command/impls/types.ts | 3 ++- lib/command/impls/umask.ts | 4 ++-- lib/command/impls/useradd.ts | 4 ++-- 11 files changed, 22 insertions(+), 21 deletions(-) diff --git a/lib/command/impls/cd.ts b/lib/command/impls/cd.ts index c634205..0253f57 100644 --- a/lib/command/impls/cd.ts +++ b/lib/command/impls/cd.ts @@ -1,8 +1,8 @@ import { fileService } from '~/services'; import { formatArg } from '../utils'; -import type { CommandFunc } from './types'; +import type { AsyncCommandFunc } from './types'; -export const cd: CommandFunc = async function(...args) { +export const cd: AsyncCommandFunc = async function(...args) { // discard `cd` args.shift(); // discard first space diff --git a/lib/command/impls/cp.ts b/lib/command/impls/cp.ts index f6fd602..ed41193 100644 --- a/lib/command/impls/cp.ts +++ b/lib/command/impls/cp.ts @@ -1,8 +1,8 @@ import { fileService } from '~/services'; import { formatArg } from '../utils'; -import type { CommandFunc } from './types'; +import type { AsyncCommandFunc } from './types'; -export const cp: CommandFunc = async function(...args) { +export const cp: AsyncCommandFunc = async function(...args) { // discard `cp` args.shift(); // discard first space diff --git a/lib/command/impls/ls.ts b/lib/command/impls/ls.ts index dd510b0..def7baf 100644 --- a/lib/command/impls/ls.ts +++ b/lib/command/impls/ls.ts @@ -1,10 +1,10 @@ import { userService } from '~/services/users'; import { formatArg } from '../utils'; -import type { CommandFunc } from './types'; +import type { AsyncCommandFunc } from './types'; import { fileService } from '~/services/files'; import { groupService } from '~/services/groups'; -export const ls: CommandFunc = async function(...args) { +export const ls: AsyncCommandFunc = async function(...args) { // discard `ls` args.shift(); // discard first space diff --git a/lib/command/impls/mkdir.ts b/lib/command/impls/mkdir.ts index dbc408f..d00fa8a 100644 --- a/lib/command/impls/mkdir.ts +++ b/lib/command/impls/mkdir.ts @@ -1,8 +1,8 @@ import { fileService } from '~/services'; import { formatArg } from '../utils'; -import type { CommandFunc } from './types'; +import type { AsyncCommandFunc } from './types'; -export const mkdir: CommandFunc = async function(...args) { +export const mkdir: AsyncCommandFunc = async function(...args) { // discard `mkdir` args.shift(); // discard first space diff --git a/lib/command/impls/mv.ts b/lib/command/impls/mv.ts index 8711d1d..b128b1e 100644 --- a/lib/command/impls/mv.ts +++ b/lib/command/impls/mv.ts @@ -1,8 +1,8 @@ import { fileService } from '~/services'; import { formatArg } from '../utils'; -import type { CommandFunc } from './types'; +import type { AsyncCommandFunc } from './types'; -export const mv: CommandFunc = async function(...args) { +export const mv: AsyncCommandFunc = async function(...args) { // discard `mv` args.shift(); // discard first space diff --git a/lib/command/impls/rm.ts b/lib/command/impls/rm.ts index cd9acb2..cb1ada5 100644 --- a/lib/command/impls/rm.ts +++ b/lib/command/impls/rm.ts @@ -1,9 +1,9 @@ import { uniq } from 'lodash-es'; import { fileService } from '~/services'; import { formatArg } from '../utils'; -import type { CommandFunc } from './types'; +import type { AsyncCommandFunc } from './types'; -export const rm: CommandFunc = async function(...args) { +export const rm: AsyncCommandFunc = async function(...args) { // discard `rm` args.shift(); // discard first space diff --git a/lib/command/impls/su.ts b/lib/command/impls/su.ts index 545185f..546154c 100644 --- a/lib/command/impls/su.ts +++ b/lib/command/impls/su.ts @@ -1,8 +1,8 @@ import { formatArg } from '../utils'; -import type { CommandFunc } from './types'; +import type { AsyncCommandFunc } from './types'; import { userService } from '~/services/users'; -export const su: CommandFunc = async function(...args) { +export const su: AsyncCommandFunc = async function(...args) { // discard `su` args.shift(); // discard first space diff --git a/lib/command/impls/touch.ts b/lib/command/impls/touch.ts index f250e47..3147db1 100644 --- a/lib/command/impls/touch.ts +++ b/lib/command/impls/touch.ts @@ -1,8 +1,8 @@ import { fileService } from '~/services'; import { formatArg } from '../utils'; -import type { CommandFunc } from './types'; +import type { AsyncCommandFunc } from './types'; -export const touch: CommandFunc = async function(...args) { +export const touch: AsyncCommandFunc = async function(...args) { // discard `touch` args.shift(); // discard first space diff --git a/lib/command/impls/types.ts b/lib/command/impls/types.ts index dd0b472..93e0cf8 100644 --- a/lib/command/impls/types.ts +++ b/lib/command/impls/types.ts @@ -1,4 +1,5 @@ -export type CommandFunc = (...args: string[]) => Promise | string[]; +export type CommandFunc = (...args: string[]) => string[]; +export type AsyncCommandFunc = (...args: string[]) => Promise; export enum Command { ECHO = 'echo', diff --git a/lib/command/impls/umask.ts b/lib/command/impls/umask.ts index bea6b5e..c2c2e29 100644 --- a/lib/command/impls/umask.ts +++ b/lib/command/impls/umask.ts @@ -1,7 +1,7 @@ import { formatArg } from '../utils'; -import type { CommandFunc } from './types'; +import type { AsyncCommandFunc } from './types'; -export const umask: CommandFunc = async function(...args) { +export const umask: AsyncCommandFunc = async function(...args) { // discard `umask` args.shift(); // discard first space diff --git a/lib/command/impls/useradd.ts b/lib/command/impls/useradd.ts index 2a886ae..3def1a3 100644 --- a/lib/command/impls/useradd.ts +++ b/lib/command/impls/useradd.ts @@ -1,8 +1,8 @@ import { formatArg } from '../utils'; -import type { CommandFunc } from './types'; +import type { AsyncCommandFunc } from './types'; import { userService } from '~/services/users'; -export const useradd: CommandFunc = async function(...args) { +export const useradd: AsyncCommandFunc = async function(...args) { // discard `useradd` args.shift(); // discard first space From d9515c0fd41468c01a78d8467a7af59299fe425b Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 16:08:23 +0700 Subject: [PATCH 03/18] fix: loosen eslint to warn on any --- eslint.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 9f397c7..668b0e6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -6,5 +6,6 @@ export default withNuxt({ semi: ['error', 'always'], indent: ['error', 2], quotes: ['error', 'single'], + '@typescript-eslint/no-explicit-any': 'warn', }, }); From 80768eb4b8ba9d87bd3cdcfb2a0666d7a7bf3c37 Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 16:14:52 +0700 Subject: [PATCH 04/18] fix: refactor - unnecessary any assertions & move commandDispatch section to another function --- lib/command/index.ts | 46 +++++++++++++++++--------------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/lib/command/index.ts b/lib/command/index.ts index 224e203..f4ba0dc 100644 --- a/lib/command/index.ts +++ b/lib/command/index.ts @@ -23,47 +23,37 @@ export async function execute(command: string): Promise { if (!args.length) { return [[{ content: ' ', color: Color.WHITE }]]; } - let output; + const output = await commandDispatch(args); + return interpretAnsiEscapeColor(output); +} + +async function commandDispatch(args: string[]): Promise { switch (args[0] as Command) { case Command.ECHO: - output = echo(...(args as any)); - break; + return echo(...args); case Command.HELP: - output = help(...args as any); - break; + return help(...args); case Command.CD: - output = await cd(...args as any); - break; + return await cd(...args); case Command.SU: - output = await su(...args as any); - break; + return await su(...args); case Command.LS: - output = await ls(...args as any); - break; + return await ls(...args); case Command.USERADD: - output = await useradd(...args as any); - break; + return await useradd(...args); case Command.TOUCH: - output = await touch(...args as any); - break; + return await touch(...args); case Command.MKDIR: - output = await mkdir(...args as any); - break; + return await mkdir(...args); case Command.UMASK: - output = await umask(...args as any); - break; + return await umask(...args); case Command.CP: - output = await cp(...args as any); - break; + return await cp(...args); case Command.MV: - output = await mv(...args as any); - break; + return await mv(...args); case Command.RM: - output = await rm(...args as any); - break; + return await rm(...args); default: - output = echo('echo', ' ', `Unknown command:\\u001b[31m ${args[0]}`); - break; + return echo('echo', ' ', `Unknown command:\\u001b[31m ${args[0]}`); } - return interpretAnsiEscapeColor(output); } From 940a1fe9d25583527632217de00e6c80ce4cc7a3 Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 16:15:17 +0700 Subject: [PATCH 05/18] fix: unnecessary white space --- lib/command/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/command/index.ts b/lib/command/index.ts index f4ba0dc..e4138d0 100644 --- a/lib/command/index.ts +++ b/lib/command/index.ts @@ -23,7 +23,7 @@ export async function execute(command: string): Promise { if (!args.length) { return [[{ content: ' ', color: Color.WHITE }]]; } - const output = await commandDispatch(args); + const output = await commandDispatch(args); return interpretAnsiEscapeColor(output); } From 4dc5374690b9d2b0067652dc383003ba4f6368ca Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 16:21:21 +0700 Subject: [PATCH 06/18] fix: mandate a space before function paren --- components/Terminal/EditableLine.vue | 3 ++- composables/cwd.ts | 2 +- composables/umask.ts | 4 ++-- composables/user.ts | 2 +- eslint.config.mjs | 1 + lib/command/impls/cd.ts | 2 +- lib/command/impls/cp.ts | 2 +- lib/command/impls/echo.ts | 2 +- lib/command/impls/help.ts | 4 ++-- lib/command/impls/ls.ts | 6 +++--- lib/command/impls/mkdir.ts | 2 +- lib/command/impls/mv.ts | 2 +- lib/command/impls/rm.ts | 2 +- lib/command/impls/su.ts | 2 +- lib/command/impls/touch.ts | 2 +- lib/command/impls/umask.ts | 8 ++++---- lib/command/impls/useradd.ts | 2 +- lib/command/index.ts | 11 +++++++++-- lib/path/index.ts | 26 +++++++++++++------------- lib/services/parse.ts | 16 ++++++++-------- server/api/files/index.patch.ts | 6 +++--- services/files.ts | 20 ++++++++++---------- services/groups.ts | 2 +- services/users.ts | 8 ++++---- 24 files changed, 73 insertions(+), 64 deletions(-) diff --git a/components/Terminal/EditableLine.vue b/components/Terminal/EditableLine.vue index 0d31456..f55185f 100644 --- a/components/Terminal/EditableLine.vue +++ b/components/Terminal/EditableLine.vue @@ -40,6 +40,7 @@ function getCharPosition (offset: number): { top: number, left: number } { let characterCount = 0; const walker = document.createTreeWalker(inputBox.value, NodeFilter.SHOW_TEXT, null, false); let node: Node | null = null; + // eslint-disable-next-line while (node = walker.nextNode()) { const textLength = node.textContent!.length; if (characterCount + textLength > offset) { @@ -106,7 +107,7 @@ async function onKeydown (e: KeyboardEvent) { } } -async function handleControlKey(key: string) { +async function handleControlKey (key: string) { const { content } = props; switch (key) { case 'v': diff --git a/composables/cwd.ts b/composables/cwd.ts index 85db3fe..659db09 100644 --- a/composables/cwd.ts +++ b/composables/cwd.ts @@ -4,7 +4,7 @@ import { VirtualPath } from '~/lib/path'; export const useCwdStore = createGlobalState(() => { const homeDir = VirtualPath.homeDir(window?.localStorage.getItem('username') || 'guest'); const cwd = ref(homeDir); - function switchCwd(newDir: string) { + function switchCwd (newDir: string) { cwd.value = cwd.value.resolve(newDir); } return { diff --git a/composables/umask.ts b/composables/umask.ts index ac8313b..759a8fc 100644 --- a/composables/umask.ts +++ b/composables/umask.ts @@ -2,11 +2,11 @@ import { createGlobalState } from '@vueuse/core'; export const useUmaskStore = createGlobalState(() => { const umask = ref(window?.localStorage?.getItem('umask') || '000111111010'); - function changeUmask(newUmask: string & { length: 12 } & { [index: number]: '0' | '1' }) { + function changeUmask (newUmask: string & { length: 12 } & { [index: number]: '0' | '1' }) { umask.value = newUmask; localStorage.setItem('umask', newUmask); } - function clearUmask() { + function clearUmask () { umask.value = '000111111010'; localStorage.setItem('umask', umask.value); } diff --git a/composables/user.ts b/composables/user.ts index c8de507..aa84be6 100644 --- a/composables/user.ts +++ b/composables/user.ts @@ -17,7 +17,7 @@ export const useUserStore = createGlobalState(() => { const userId = ref(null); const groupId = ref(null); const createdAt = ref(null); - function switchUser(name: string) { + function switchUser (name: string) { username.value = name; localStorage.setItem('username', name); clearUmask(); diff --git a/eslint.config.mjs b/eslint.config.mjs index 668b0e6..d84dbbd 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -7,5 +7,6 @@ export default withNuxt({ indent: ['error', 2], quotes: ['error', 'single'], '@typescript-eslint/no-explicit-any': 'warn', + 'space-before-function-paren': ['error', 'always'], }, }); diff --git a/lib/command/impls/cd.ts b/lib/command/impls/cd.ts index 0253f57..281aa1f 100644 --- a/lib/command/impls/cd.ts +++ b/lib/command/impls/cd.ts @@ -2,7 +2,7 @@ import { fileService } from '~/services'; import { formatArg } from '../utils'; import type { AsyncCommandFunc } from './types'; -export const cd: AsyncCommandFunc = async function(...args) { +export const cd: AsyncCommandFunc = async function (...args) { // discard `cd` args.shift(); // discard first space diff --git a/lib/command/impls/cp.ts b/lib/command/impls/cp.ts index ed41193..5a6c9ea 100644 --- a/lib/command/impls/cp.ts +++ b/lib/command/impls/cp.ts @@ -2,7 +2,7 @@ import { fileService } from '~/services'; import { formatArg } from '../utils'; import type { AsyncCommandFunc } from './types'; -export const cp: AsyncCommandFunc = async function(...args) { +export const cp: AsyncCommandFunc = async function (...args) { // discard `cp` args.shift(); // discard first space diff --git a/lib/command/impls/echo.ts b/lib/command/impls/echo.ts index a759f95..1e36046 100644 --- a/lib/command/impls/echo.ts +++ b/lib/command/impls/echo.ts @@ -1,7 +1,7 @@ import { formatArg } from '../utils'; import type { CommandFunc } from './types'; -export const echo: CommandFunc = function(...args) { +export const echo: CommandFunc = function (...args) { // discard `echo` args.shift(); // discard first space diff --git a/lib/command/impls/help.ts b/lib/command/impls/help.ts index 731d434..f22a01d 100644 --- a/lib/command/impls/help.ts +++ b/lib/command/impls/help.ts @@ -83,7 +83,7 @@ const commandDescriptions: Record = { }, }; -function getDescription(commandName: string): string[] { +function getDescription (commandName: string): string[] { const commandDescription = commandDescriptions[commandName as Command]; if (commandDescription !== undefined) { return [ @@ -96,7 +96,7 @@ function getDescription(commandName: string): string[] { } } -export const help: CommandFunc = function(...args) { +export const help: CommandFunc = function (...args) { args.shift(); args.shift(); diff --git a/lib/command/impls/ls.ts b/lib/command/impls/ls.ts index def7baf..62739d3 100644 --- a/lib/command/impls/ls.ts +++ b/lib/command/impls/ls.ts @@ -4,7 +4,7 @@ import type { AsyncCommandFunc } from './types'; import { fileService } from '~/services/files'; import { groupService } from '~/services/groups'; -export const ls: AsyncCommandFunc = async function(...args) { +export const ls: AsyncCommandFunc = async function (...args) { // discard `ls` args.shift(); // discard first space @@ -38,7 +38,7 @@ export const ls: AsyncCommandFunc = async function(...args) { ]; }; -function formatFileType(fileType: string): string { +function formatFileType (fileType: string): string { switch (fileType) { case 'file': return '-'; case 'directory': return 'd'; @@ -47,7 +47,7 @@ function formatFileType(fileType: string): string { } } -function formatPermissionBits(permissionBits: string): string { +function formatPermissionBits (permissionBits: string): string { const ownerRead = Number.parseInt(permissionBits[3]); const ownerWrite = Number.parseInt(permissionBits[4]); const ownerExecute = Number.parseInt(permissionBits[5]); diff --git a/lib/command/impls/mkdir.ts b/lib/command/impls/mkdir.ts index d00fa8a..e07a837 100644 --- a/lib/command/impls/mkdir.ts +++ b/lib/command/impls/mkdir.ts @@ -2,7 +2,7 @@ import { fileService } from '~/services'; import { formatArg } from '../utils'; import type { AsyncCommandFunc } from './types'; -export const mkdir: AsyncCommandFunc = async function(...args) { +export const mkdir: AsyncCommandFunc = async function (...args) { // discard `mkdir` args.shift(); // discard first space diff --git a/lib/command/impls/mv.ts b/lib/command/impls/mv.ts index b128b1e..ccd78c0 100644 --- a/lib/command/impls/mv.ts +++ b/lib/command/impls/mv.ts @@ -2,7 +2,7 @@ import { fileService } from '~/services'; import { formatArg } from '../utils'; import type { AsyncCommandFunc } from './types'; -export const mv: AsyncCommandFunc = async function(...args) { +export const mv: AsyncCommandFunc = async function (...args) { // discard `mv` args.shift(); // discard first space diff --git a/lib/command/impls/rm.ts b/lib/command/impls/rm.ts index cb1ada5..ab1443f 100644 --- a/lib/command/impls/rm.ts +++ b/lib/command/impls/rm.ts @@ -3,7 +3,7 @@ import { fileService } from '~/services'; import { formatArg } from '../utils'; import type { AsyncCommandFunc } from './types'; -export const rm: AsyncCommandFunc = async function(...args) { +export const rm: AsyncCommandFunc = async function (...args) { // discard `rm` args.shift(); // discard first space diff --git a/lib/command/impls/su.ts b/lib/command/impls/su.ts index 546154c..20c55dd 100644 --- a/lib/command/impls/su.ts +++ b/lib/command/impls/su.ts @@ -2,7 +2,7 @@ import { formatArg } from '../utils'; import type { AsyncCommandFunc } from './types'; import { userService } from '~/services/users'; -export const su: AsyncCommandFunc = async function(...args) { +export const su: AsyncCommandFunc = async function (...args) { // discard `su` args.shift(); // discard first space diff --git a/lib/command/impls/touch.ts b/lib/command/impls/touch.ts index 3147db1..a043633 100644 --- a/lib/command/impls/touch.ts +++ b/lib/command/impls/touch.ts @@ -2,7 +2,7 @@ import { fileService } from '~/services'; import { formatArg } from '../utils'; import type { AsyncCommandFunc } from './types'; -export const touch: AsyncCommandFunc = async function(...args) { +export const touch: AsyncCommandFunc = async function (...args) { // discard `touch` args.shift(); // discard first space diff --git a/lib/command/impls/umask.ts b/lib/command/impls/umask.ts index c2c2e29..15cb377 100644 --- a/lib/command/impls/umask.ts +++ b/lib/command/impls/umask.ts @@ -1,7 +1,7 @@ import { formatArg } from '../utils'; import type { AsyncCommandFunc } from './types'; -export const umask: AsyncCommandFunc = async function(...args) { +export const umask: AsyncCommandFunc = async function (...args) { // discard `umask` args.shift(); // discard first space @@ -29,12 +29,12 @@ export const umask: AsyncCommandFunc = async function(...args) { ]; }; -function isOctDigit(c: string): boolean { +function isOctDigit (c: string): boolean { const n = Number.parseInt(c); return c.length === 1 && 0 <= n && n <= 7; } -function umaskToOct(umask: string): string { +function umaskToOct (umask: string): string { const ownerRead = Number.parseInt(umask[3]); const ownerWrite = Number.parseInt(umask[4]); const ownerExecute = Number.parseInt(umask[5]); @@ -50,7 +50,7 @@ function umaskToOct(umask: string): string { return `${ownerOct}${groupOct}${otherOct}`; } -function umaskFromOct(octs: string): string { +function umaskFromOct (octs: string): string { const ownerOct = Number.parseInt(octs[0]); const groupOct = Number.parseInt(octs[1]); const otherOct = Number.parseInt(octs[2]); diff --git a/lib/command/impls/useradd.ts b/lib/command/impls/useradd.ts index 3def1a3..2f971ff 100644 --- a/lib/command/impls/useradd.ts +++ b/lib/command/impls/useradd.ts @@ -2,7 +2,7 @@ import { formatArg } from '../utils'; import type { AsyncCommandFunc } from './types'; import { userService } from '~/services/users'; -export const useradd: AsyncCommandFunc = async function(...args) { +export const useradd: AsyncCommandFunc = async function (...args) { // discard `useradd` args.shift(); // discard first space diff --git a/lib/command/index.ts b/lib/command/index.ts index e4138d0..11c92df 100644 --- a/lib/command/index.ts +++ b/lib/command/index.ts @@ -15,7 +15,7 @@ import { rm } from './impls/rm'; import { cp } from './impls/cp'; import { mv } from './impls/mv'; -export async function execute(command: string): Promise { +export async function execute (command: string): Promise { const args = parse(command); if (!args[0]?.trim()) { args.shift(); @@ -27,7 +27,7 @@ export async function execute(command: string): Promise { return interpretAnsiEscapeColor(output); } -async function commandDispatch(args: string[]): Promise { +async function commandDispatch (args: string[]): Promise { switch (args[0] as Command) { case Command.ECHO: return echo(...args); @@ -57,3 +57,10 @@ async function commandDispatch(args: string[]): Promise { return echo('echo', ' ', `Unknown command:\\u001b[31m ${args[0]}`); } } + +enum ShellRedirection { + Output, + Append, +} + +function extractShellRedirection() diff --git a/lib/path/index.ts b/lib/path/index.ts index 77c4558..06fa93d 100644 --- a/lib/path/index.ts +++ b/lib/path/index.ts @@ -3,7 +3,7 @@ import path from 'path-browserify'; export class VirtualPath { private path: string; - private constructor(path: string) { + private constructor (path: string) { if (path[path.length - 1] === '/') { this.path = path.slice(0, path.length - 1); } else { @@ -11,45 +11,45 @@ export class VirtualPath { } } - static create(path: string): VirtualPath { + static create (path: string): VirtualPath { return new VirtualPath(path); } - static createAndCheck(path: string): VirtualPath { + static createAndCheck (path: string): VirtualPath { if (path.match(/^[a-zA-Z \-0-9._/]+$/g) === null) { throw new Error('Invalid path pattern'); } return new VirtualPath(path); } - isValid(): boolean { + isValid (): boolean { return this.path.match(/^[a-zA-Z \-0-9._/]+$/g) !== null; } - parent(): VirtualPath { + parent (): VirtualPath { if (this.isRoot()) { return this; } return VirtualPath.create(path.dirname(this.path)); } - isRoot(): boolean { + isRoot (): boolean { return this.path === ''; } - isHome(username: string): boolean { + isHome (username: string): boolean { return this.equals(VirtualPath.homeDir(username)); } - equals(other: VirtualPath): boolean { + equals (other: VirtualPath): boolean { return this.path === other.path; } - toString(): string { + toString (): string { return this.path; } - toFormattedString(username: string): string { + toFormattedString (username: string): string { const homeDir = VirtualPath.homeDir(username); if (this.path === '') { return '/'; @@ -60,7 +60,7 @@ export class VirtualPath { return this.path; } - resolve(newDir: string): VirtualPath { + resolve (newDir: string): VirtualPath { if (path.isAbsolute(newDir)) { return VirtualPath.create(newDir); } @@ -71,11 +71,11 @@ export class VirtualPath { return VirtualPath.create(path.resolve(this.path, newDir)); } - basename(): string { + basename (): string { return path.basename(this.path); } - static homeDir(username: string): VirtualPath { + static homeDir (username: string): VirtualPath { return VirtualPath.create(`/home/${username}`); } } diff --git a/lib/services/parse.ts b/lib/services/parse.ts index d99b1ad..ce5bd9d 100644 --- a/lib/services/parse.ts +++ b/lib/services/parse.ts @@ -60,14 +60,14 @@ function getNextToken (command: string): { token: string, remaining: string } | i += 1; const nextChar = command[i]; switch (nextChar) { - case '\'': - token += '\''; - break; - case '"': - token += '"'; - break; - default: - token += nextChar; + case '\'': + token += '\''; + break; + case '"': + token += '"'; + break; + default: + token += nextChar; } } break; diff --git a/server/api/files/index.patch.ts b/server/api/files/index.patch.ts index 97c921d..a5c8b05 100644 --- a/server/api/files/index.patch.ts +++ b/server/api/files/index.patch.ts @@ -46,7 +46,7 @@ export default defineEventHandler(async (event) => { }); -async function handleNameChange(dbClient: db.TxnClient, event: H3Event, newFileName: string) { +async function handleNameChange (dbClient: db.TxnClient, event: H3Event, newFileName: string) { const { name } = getQuery(event); const oldFilepath = VirtualPath.create(trimQuote(name as string)); const oldContainerPath = oldFilepath.parent(); @@ -99,7 +99,7 @@ async function handleNameChange(dbClient: db.TxnCli } } -async function handleOwnerChange(dbClient: db.TxnClient, event: H3Event, ownerId: number) { +async function handleOwnerChange (dbClient: db.TxnClient, event: H3Event, ownerId: number) { const { name } = getQuery(event); const filepath = VirtualPath.create(trimQuote(name as string)); @@ -118,7 +118,7 @@ async function handleOwnerChange(dbClient: db.TxnCl await db.update('files', { owner_id: ownerId }, { name: filepath.toString(), deleted_at: db.conditions.isNull }).run(dbClient); } -async function handlePermissionChange(dbClient: db.TxnClient, event: H3Event, permissionBits: string) { +async function handlePermissionChange (dbClient: db.TxnClient, event: H3Event, permissionBits: string) { const { name } = getQuery(event); const filepath = VirtualPath.create(trimQuote(name as string)); diff --git a/services/files.ts b/services/files.ts index 1e28667..124f59e 100644 --- a/services/files.ts +++ b/services/files.ts @@ -35,13 +35,13 @@ export interface FileMeta { } export const fileService = { - async getMetaOfFile(filename: string): Promise> { + async getMetaOfFile (filename: string): Promise> { }, - async getFileContent(filename: string): Promise> { + async getFileContent (filename: string): Promise> { }, - async updateFileContent(filename: string, content: Uint8Array): Promise> { + async updateFileContent (filename: string, content: Uint8Array): Promise> { }, - async getFolderContent(filename: string): Promise> { + async getFolderContent (filename: string): Promise> { const { cwd } = useCwdStore(); const meta = await $fetch('/api/files/ls', { method: 'get', @@ -65,7 +65,7 @@ export const fileService = { fileType: file.fileType, }))); }, - async removeFile(filename: string): Promise> { + async removeFile (filename: string): Promise> { const { cwd } = useCwdStore(); const res = await $fetch('/api/files', { method: 'delete', @@ -78,7 +78,7 @@ export const fileService = { return new Ok(null); }, - async createFile(filename: string, content: string, permissionBits: string): Promise> { + async createFile (filename: string, content: string, permissionBits: string): Promise> { const { cwd } = useCwdStore(); const res = await $fetch('/api/files', { method: 'post', @@ -94,7 +94,7 @@ export const fileService = { } return new Ok(null); }, - async createFolder(filename: string, permissionBits: string): Promise> { + async createFolder (filename: string, permissionBits: string): Promise> { const { cwd } = useCwdStore(); const res = await $fetch('/api/files', { method: 'post', @@ -109,7 +109,7 @@ export const fileService = { } return new Ok(null); }, - async changeDirectory(filename: string): Promise> { + async changeDirectory (filename: string): Promise> { try { const { cwd, switchCwd } = useCwdStore(); const meta = await $fetch('/api/files', { @@ -132,7 +132,7 @@ export const fileService = { return new Err({ code: 500, message: 'Network connection error' }); } }, - async moveFile(src: string, dest: string, umask: string): Promise> { + async moveFile (src: string, dest: string, umask: string): Promise> { const { cwd } = useCwdStore(); const res = await $fetch('/api/files/mv', { method: 'post', @@ -149,7 +149,7 @@ export const fileService = { return new Ok(null); }, - async copyFile(src: string, dest: string, umask: string): Promise> { + async copyFile (src: string, dest: string, umask: string): Promise> { const { cwd } = useCwdStore(); const res = await $fetch('/api/files/cp', { method: 'post', diff --git a/services/groups.ts b/services/groups.ts index 5995385..0c728c0 100644 --- a/services/groups.ts +++ b/services/groups.ts @@ -9,7 +9,7 @@ export interface GroupMeta { const groupMetaCache = new Map(); export const groupService = { - async getMetaOfGroup(id: number): Promise> { + async getMetaOfGroup (id: number): Promise> { if (!groupMetaCache.has(id)) { const res = await $fetch('/api/groups', { method: 'get', diff --git a/services/users.ts b/services/users.ts index 212d2e5..b4fc838 100644 --- a/services/users.ts +++ b/services/users.ts @@ -10,7 +10,7 @@ export interface UserMeta { const userMetaCache = new Map(); export const userService = { - async getMetaOfUser(id: number): Promise> { + async getMetaOfUser (id: number): Promise> { if (!userMetaCache.has(id)) { const res = await $fetch('/api/users', { method: 'get', @@ -28,9 +28,9 @@ export const userService = { const { ok: { data } } = res; return new Ok({ name: data.name, userId: data.userId, groupId: data.groupId, createdAt: data.createdAt }); }, - async getHomeDirectory(id: number): Promise> { + async getHomeDirectory (id: number): Promise> { }, - async switchUser(name: string, password: string | undefined): Promise> { + async switchUser (name: string, password: string | undefined): Promise> { const res = await $fetch('/api/auth/login', { method: 'post', body: { @@ -47,7 +47,7 @@ export const userService = { switchUser(data.username); return new Ok({ name: data.username, userId: data.userId, groupId: data.groupId, createdAt: data.createdAt }); }, - async addUser(name: string, password: string | undefined): Promise> { + async addUser (name: string, password: string | undefined): Promise> { const res = await $fetch('/api/auth/register', { method: 'post', body: { From 7bafd4bdb57af59e755d2b2743d8c5db8d22bf62 Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 16:48:53 +0700 Subject: [PATCH 07/18] fix: filter out whitespaces in execute tokens --- lib/command/impls/cd.ts | 2 -- lib/command/impls/cp.ts | 4 ---- lib/command/impls/echo.ts | 2 -- lib/command/impls/ls.ts | 2 -- lib/command/impls/mkdir.ts | 4 +--- lib/command/impls/mv.ts | 4 ---- lib/command/impls/rm.ts | 2 -- lib/command/impls/su.ts | 4 ---- lib/command/impls/touch.ts | 4 +--- lib/command/impls/umask.ts | 4 +--- lib/command/impls/useradd.ts | 4 ---- lib/command/index.ts | 31 +++++++++++++++++++++++++++---- 12 files changed, 30 insertions(+), 37 deletions(-) diff --git a/lib/command/impls/cd.ts b/lib/command/impls/cd.ts index 281aa1f..d01b5fc 100644 --- a/lib/command/impls/cd.ts +++ b/lib/command/impls/cd.ts @@ -5,8 +5,6 @@ import type { AsyncCommandFunc } from './types'; export const cd: AsyncCommandFunc = async function (...args) { // discard `cd` args.shift(); - // discard first space - args.shift(); if (args.length !== 1 || !args[0].trim()) { return [ diff --git a/lib/command/impls/cp.ts b/lib/command/impls/cp.ts index 5a6c9ea..31b9192 100644 --- a/lib/command/impls/cp.ts +++ b/lib/command/impls/cp.ts @@ -5,13 +5,9 @@ import type { AsyncCommandFunc } from './types'; export const cp: AsyncCommandFunc = async function (...args) { // discard `cp` args.shift(); - // discard first space - args.shift(); const src = formatArg(args.shift()); - args.shift(); const dest = formatArg(args.shift()); - args.shift(); if (args.length > 0 || !src || !dest) { return [ 'Invalid use of cp. Run \'help cp\'', diff --git a/lib/command/impls/echo.ts b/lib/command/impls/echo.ts index 1e36046..e2c4a32 100644 --- a/lib/command/impls/echo.ts +++ b/lib/command/impls/echo.ts @@ -4,8 +4,6 @@ import type { CommandFunc } from './types'; export const echo: CommandFunc = function (...args) { // discard `echo` args.shift(); - // discard first space - args.shift(); return [ args.map((arg) => { diff --git a/lib/command/impls/ls.ts b/lib/command/impls/ls.ts index 62739d3..a7a26e9 100644 --- a/lib/command/impls/ls.ts +++ b/lib/command/impls/ls.ts @@ -7,8 +7,6 @@ import { groupService } from '~/services/groups'; export const ls: AsyncCommandFunc = async function (...args) { // discard `ls` args.shift(); - // discard first space - args.shift(); if (args.length > 1) { return ['Expect an optional dirname as argument.']; diff --git a/lib/command/impls/mkdir.ts b/lib/command/impls/mkdir.ts index e07a837..6420aa4 100644 --- a/lib/command/impls/mkdir.ts +++ b/lib/command/impls/mkdir.ts @@ -5,10 +5,8 @@ import type { AsyncCommandFunc } from './types'; export const mkdir: AsyncCommandFunc = async function (...args) { // discard `mkdir` args.shift(); - // discard first space - args.shift(); - if (args.length > 1 || args.length === 0) { + if (args.length !== 0) { return ['Invalid use of mkdir. Run \'help mkdir\'']; } diff --git a/lib/command/impls/mv.ts b/lib/command/impls/mv.ts index ccd78c0..522517e 100644 --- a/lib/command/impls/mv.ts +++ b/lib/command/impls/mv.ts @@ -5,13 +5,9 @@ import type { AsyncCommandFunc } from './types'; export const mv: AsyncCommandFunc = async function (...args) { // discard `mv` args.shift(); - // discard first space - args.shift(); const src = formatArg(args.shift()); - args.shift(); const dest = formatArg(args.shift()); - args.shift(); if (args.length > 0 || !src || !dest) { return [ 'Invalid use of mv. Run \'help mv\'', diff --git a/lib/command/impls/rm.ts b/lib/command/impls/rm.ts index ab1443f..6c7518d 100644 --- a/lib/command/impls/rm.ts +++ b/lib/command/impls/rm.ts @@ -6,8 +6,6 @@ import type { AsyncCommandFunc } from './types'; export const rm: AsyncCommandFunc = async function (...args) { // discard `rm` args.shift(); - // discard first space - args.shift(); const { cwd } = useCwdStore(); const filenames = uniq(args.filter((arg) => arg.trim()).map((arg) => cwd.value.resolve(formatArg(arg)!).toString())); diff --git a/lib/command/impls/su.ts b/lib/command/impls/su.ts index 20c55dd..c1acb08 100644 --- a/lib/command/impls/su.ts +++ b/lib/command/impls/su.ts @@ -5,15 +5,12 @@ import { userService } from '~/services/users'; export const su: AsyncCommandFunc = async function (...args) { // discard `su` args.shift(); - // discard first space - args.shift(); let username; let password; while (args.length) { const opt = args.shift(); - args.shift(); if (args.length === 0) return ['Invalid use of su. Run \'help su\'']; switch (opt) { case '-u': @@ -25,7 +22,6 @@ export const su: AsyncCommandFunc = async function (...args) { default: return ['Invalid use of su. Run \'help su\'']; } - args.shift(); } if (username === undefined) { diff --git a/lib/command/impls/touch.ts b/lib/command/impls/touch.ts index a043633..e45ccaf 100644 --- a/lib/command/impls/touch.ts +++ b/lib/command/impls/touch.ts @@ -5,10 +5,8 @@ import type { AsyncCommandFunc } from './types'; export const touch: AsyncCommandFunc = async function (...args) { // discard `touch` args.shift(); - // discard first space - args.shift(); - if (args.length > 1 || args.length === 0) { + if (args.length !== 1) { return ['Invalid use of touch. Run \'help touch\'']; } diff --git a/lib/command/impls/umask.ts b/lib/command/impls/umask.ts index 15cb377..dfa5d1f 100644 --- a/lib/command/impls/umask.ts +++ b/lib/command/impls/umask.ts @@ -4,8 +4,6 @@ import type { AsyncCommandFunc } from './types'; export const umask: AsyncCommandFunc = async function (...args) { // discard `umask` args.shift(); - // discard first space - args.shift(); if (args.length === 0) { const { umask } = useUmaskStore(); @@ -23,7 +21,7 @@ export const umask: AsyncCommandFunc = async function (...args) { return ['Invalid umask']; } const { changeUmask } = useUmaskStore(); - changeUmask(umaskFromOct(umask as any)); + changeUmask(umaskFromOct(umask as any) as any); return [ 'Change umask successfully', ]; diff --git a/lib/command/impls/useradd.ts b/lib/command/impls/useradd.ts index 2f971ff..49caa83 100644 --- a/lib/command/impls/useradd.ts +++ b/lib/command/impls/useradd.ts @@ -5,15 +5,12 @@ import { userService } from '~/services/users'; export const useradd: AsyncCommandFunc = async function (...args) { // discard `useradd` args.shift(); - // discard first space - args.shift(); let username; let password; while (args.length) { const opt = args.shift(); - args.shift(); if (args.length === 0) return ['Invalid use of useradd. Run \'help useradd\'']; switch (opt) { case '-u': @@ -25,7 +22,6 @@ export const useradd: AsyncCommandFunc = async function (...args) { default: return ['Invalid use of useradd. Run \'help useradd\'']; } - args.shift(); } if (username === undefined) { diff --git a/lib/command/index.ts b/lib/command/index.ts index 11c92df..c37913a 100644 --- a/lib/command/index.ts +++ b/lib/command/index.ts @@ -14,20 +14,26 @@ import { umask } from './impls/umask'; import { rm } from './impls/rm'; import { cp } from './impls/cp'; import { mv } from './impls/mv'; +import { Err, Ok, type Result } from '~/services'; export async function execute (command: string): Promise { - const args = parse(command); + const shellDirRes = extractShellRedirection(...parse(command).filter((arg) => arg.trim())); + if (!shellDirRes.isOk()) { + return interpretAnsiEscapeColor(shellDirRes.error()!); + } + const shellDir = shellDirRes.unwrap(); + const { args, redirections } = shellDir; if (!args[0]?.trim()) { args.shift(); } if (!args.length) { return [[{ content: ' ', color: Color.WHITE }]]; } - const output = await commandDispatch(args); + const output = await commandDispatch(...args); return interpretAnsiEscapeColor(output); } -async function commandDispatch (args: string[]): Promise { +async function commandDispatch (...args: string[]): Promise { switch (args[0] as Command) { case Command.ECHO: return echo(...args); @@ -63,4 +69,21 @@ enum ShellRedirection { Append, } -function extractShellRedirection() +function extractShellRedirection (...args: string[]): Result<{ + redirections: { mode: ShellRedirection; name: string }[]; + args: string[]; +}, string[]> { + const redirections = []; + + for (let i = args.length - 1; i > 0; --i) { + const arg = args[i]; + if (['>', '>>'].includes(arg)) { + const mode = arg === '>' ? ShellRedirection.Output : ShellRedirection.Append; + const pathname = args[i + 1]; + if (pathname === undefined) return new Err([`Parse error: no pathname found after '${arg}'`]); + redirections.push({ mode, name: pathname }); + args.splice(i, 2); + } + } + return new Ok({ redirections, args }); +} From 48c6192421533bcd6bbede66adf79046eab7bfe0 Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 16:50:15 +0700 Subject: [PATCH 08/18] fix: simplify logic in execute dealing with whitespaces --- lib/command/index.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/command/index.ts b/lib/command/index.ts index c37913a..85fc343 100644 --- a/lib/command/index.ts +++ b/lib/command/index.ts @@ -23,13 +23,7 @@ export async function execute (command: string): Promise { } const shellDir = shellDirRes.unwrap(); const { args, redirections } = shellDir; - if (!args[0]?.trim()) { - args.shift(); - } - if (!args.length) { - return [[{ content: ' ', color: Color.WHITE }]]; - } - const output = await commandDispatch(...args); + const output = args.length ? await commandDispatch(...args) : []; return interpretAnsiEscapeColor(output); } From 8520350a79a3cf40a01da5b9f95b1a9ad885ea79 Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 17:51:17 +0700 Subject: [PATCH 09/18] feat: basic redirect output frame --- lib/command/index.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/command/index.ts b/lib/command/index.ts index 85fc343..1c80b40 100644 --- a/lib/command/index.ts +++ b/lib/command/index.ts @@ -1,4 +1,4 @@ -import { type ColoredContent, Color } from '~/lib'; +import type { ColoredContent } from '~/lib'; import { echo } from './impls/echo'; import { help } from './impls/help'; import { Command } from './impls/types'; @@ -22,8 +22,16 @@ export async function execute (command: string): Promise { return interpretAnsiEscapeColor(shellDirRes.error()!); } const shellDir = shellDirRes.unwrap(); - const { args, redirections } = shellDir; + const { args, redirections } = shellDir; const output = args.length ? await commandDispatch(...args) : []; + if (redirections.length > 0) { + const redirectionOutput = (await Promise.all( + redirections.map( + (redirection) => redirectOutput(output, redirection), + ), + )).flatMap((arg) => arg); + return interpretAnsiEscapeColor(redirectionOutput); + } return interpretAnsiEscapeColor(output); } @@ -58,13 +66,13 @@ async function commandDispatch (...args: string[]): Promise { } } -enum ShellRedirection { +enum RedirectionMode { Output, Append, } function extractShellRedirection (...args: string[]): Result<{ - redirections: { mode: ShellRedirection; name: string }[]; + redirections: { mode: RedirectionMode; name: string }[]; args: string[]; }, string[]> { const redirections = []; @@ -72,7 +80,7 @@ function extractShellRedirection (...args: string[]): Result<{ for (let i = args.length - 1; i > 0; --i) { const arg = args[i]; if (['>', '>>'].includes(arg)) { - const mode = arg === '>' ? ShellRedirection.Output : ShellRedirection.Append; + const mode = arg === '>' ? RedirectionMode.Output : RedirectionMode.Append; const pathname = args[i + 1]; if (pathname === undefined) return new Err([`Parse error: no pathname found after '${arg}'`]); redirections.push({ mode, name: pathname }); @@ -81,3 +89,7 @@ function extractShellRedirection (...args: string[]): Result<{ } return new Ok({ redirections, args }); } + +async function redirectOutput (output: string[], { mode, name }: { mode: RedirectionMode, name: string }): Promise { + return []; +} From d57b0685e4ffdbcbd95c045184c40d729d33d19e Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 17:58:09 +0700 Subject: [PATCH 10/18] feat: allow content patch api to append content --- server/api/files/content/index.patch.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/server/api/files/content/index.patch.ts b/server/api/files/content/index.patch.ts index 6ace054..e94ff33 100644 --- a/server/api/files/content/index.patch.ts +++ b/server/api/files/content/index.patch.ts @@ -16,15 +16,15 @@ export default defineEventHandler(async (event) => { return { error: { code: FileContentPatchErrorCode.INVALID_PARAM, message: 'Expect the "name" query param to be string' } }; } const body = await readBody(event); - if (typeof body !== 'object' || typeof body.content !== 'string') { - return { error: { code: FileContentPatchErrorCode.INVALID_BODY, message: 'Invalid body. Expected "content" to be a string.' } }; + if (typeof body !== 'object' || typeof body.content !== 'string' || typeof body.shouldAppend !== 'boolean') { + return { error: { code: FileContentPatchErrorCode.INVALID_BODY, message: 'Invalid body. Expected "content" to be a string and "shouldAppend" to be a boolean.' } }; } if (!event.context.auth) { return { error: { code: FileContentPatchErrorCode.NOT_ENOUGH_PRIVILEGE, message: 'Should be logged in as a user with enough privilege' } }; } const filepath = VirtualPath.create(trimQuote(name)); try { - const { permission_bits: filePermissionBits, owner_id: fileOwnerId, group_id: fileGroupId } = await db.selectExactlyOne('files', { name: filepath.toString(), file_type: 'file', deleted_at: db.conditions.isNull }).run(dbPool); + const { permission_bits: filePermissionBits, owner_id: fileOwnerId, group_id: fileGroupId, content } = await db.selectExactlyOne('files', { name: filepath.toString(), file_type: 'file', deleted_at: db.conditions.isNull }).run(dbPool); if ( !canAccess( { userId: event.context.auth.userId as number, groupId: event.context.auth.groupId as number }, @@ -35,7 +35,8 @@ export default defineEventHandler(async (event) => { return { error: { code: FileContentPatchErrorCode.NOT_ENOUGH_PRIVILEGE, message: 'Should be logged in as a user with enough privilege' } }; } - await db.update('files', { content: body.content }, { name: filepath.toString(), deleted_at: db.conditions.isNull }).run(dbPool); + const newContent = body.shouldAppend ? content + body.content : body.content; + await db.update('files', { content: newContent }, { name: filepath.toString(), deleted_at: db.conditions.isNull }).run(dbPool); return { ok: { message: 'Update file content successfully' } }; } catch { From e742895a68bb1ce672063199276b795571f6253f Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 18:20:06 +0700 Subject: [PATCH 11/18] fix: move responseCode to lib --- lib/index.ts | 1 + lib/responseCodes.ts | 101 ++++++++++++++++++++++++ server/api/auth/login.post.ts | 7 +- server/api/auth/register.post.ts | 9 +-- server/api/files/content/index.get.ts | 7 +- server/api/files/content/index.patch.ts | 8 +- server/api/files/cp.post.ts | 10 +-- server/api/files/index.delete.ts | 9 +-- server/api/files/index.get.ts | 7 +- server/api/files/index.patch.ts | 10 +-- server/api/files/index.post.ts | 9 +-- server/api/files/ls.get.ts | 7 +- server/api/files/mv.post.ts | 10 +-- server/api/groups/index.get.ts | 9 +-- server/api/users/index.delete.ts | 11 +-- server/api/users/index.get.ts | 9 +-- 16 files changed, 121 insertions(+), 103 deletions(-) create mode 100644 lib/responseCodes.ts diff --git a/lib/index.ts b/lib/index.ts index c90aea7..f4fbe73 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,3 +1,4 @@ export * from './command'; export * from './services'; export * from './types'; +export * from './responseCodes'; diff --git a/lib/responseCodes.ts b/lib/responseCodes.ts new file mode 100644 index 0000000..385e421 --- /dev/null +++ b/lib/responseCodes.ts @@ -0,0 +1,101 @@ +// Initially, each response code is local to each api +// However, have to move it here as Nuxt disallows +// importing code from `server` to `Vue` part. + +export enum LoginErrorCode { + INVALID_BODY = 1000, + INVALID_CRED = 1001, + UNKNOWN_ERROR = 2000, +} + +export enum RegisterErrorCode { + INVALID_BODY = 1000, + USER_ALREADY_EXISTS = 1001, + PASSWORD_TOO_SHORT = 1002, + INVALID_USER_NAME = 1003, + UNKNOWN_ERROR = 2000, +} + +export enum FileContentGetErrorCode { + INVALID_PARAM = 1000, + NOT_ENOUGH_PRIVILEGE = 2000, + FILE_NOT_FOUND = 3000, +} + +export enum FileContentPatchErrorCode { + INVALID_PARAM = 1000, + INVALID_BODY = 1001, + NOT_ENOUGH_PRIVILEGE = 2000, + FILE_NOT_FOUND = 3000, +} + +export enum FileCpErrorCode { + INVALID_PARAM = 1000, + INVALID_BODY = 1001, + NOT_ENOUGH_PRIVILEGE = 2000, + SRC_NOT_FOUND = 3000, + DEST_NOT_FOUND = 3001, + INVALID_COPY_FOLDER_TO_FILE = 3002, +} + +export enum FileDeleteErrorCode { + INVALID_PARAM = 1000, + NOT_ENOUGH_PRIVILEGE = 2000, + FILE_NOT_FOUND = 3000, +} + +export enum FileMetaGetErrorCode { + INVALID_PARAM = 1000, + NOT_ENOUGH_PRIVILEGE = 2000, + FILE_NOT_FOUND = 3000, +} + +export enum FileMetaPatchErrorCode { + INVALID_PARAM = 1000, + INVALID_BODY = 1001, + NOT_ENOUGH_PRIVILEGE = 2000, + FILE_NOT_FOUND = 3000, + DESTINATION_NOT_EXIST = 3001, + SOURCE_NOT_EXIST = 3002, +} + +export enum FilePostErrorCode { + INVALID_PARAM = 1000, + INVALID_BODY = 1001, + NOT_ENOUGH_PRIVILEGE = 2000, + INVALID_FOLDER = 3000, + FILE_ALREADY_EXISTS = 3001, +} + +export enum FileLsErrorCode { + INVALID_PARAM = 1000, + NOT_ENOUGH_PRIVILEGE = 2000, + FILE_NOT_FOUND = 3000, +} + +export enum FileMvErrorCode { + INVALID_PARAM = 1000, + INVALID_BODY = 1001, + NOT_ENOUGH_PRIVILEGE = 2000, + SRC_NOT_FOUND = 3000, + DEST_NOT_FOUND = 3001, + INVALID_MV_FOLDER_TO_FILE = 3002, +} + +export enum GroupGetErrorCode { + INVALID_PARAM = 1000, + NOT_ENOUGH_PRIVILEGE = 2000, + GROUP_NOT_FOUND = 3000, +} + +export enum UserDeleteErrorCode { + INVALID_PARAM = 1000, + NOT_ENOUGH_PRIVILEGE = 2000, + UNDELETABLE_USER = 2001, +} + +export enum UserGetErrorCode { + INVALID_PARAM = 1000, + NOT_ENOUGH_PRIVILEGE = 2000, + USER_NOT_FOUND = 3000, +} diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts index 0910cc1..0f264cf 100644 --- a/server/api/auth/login.post.ts +++ b/server/api/auth/login.post.ts @@ -3,12 +3,7 @@ import jwt from 'jsonwebtoken'; import { defineEventHandler } from 'h3'; import * as db from 'zapatos/db'; import { dbPool } from '~/db/connection'; - -export enum LoginErrorCode { - INVALID_BODY = 1000, - INVALID_CRED = 1001, - UNKNOWN_ERROR = 2000, -} +import { LoginErrorCode } from '~/lib'; export default defineEventHandler(async (event) => { const isProduction = process.env.NODE_ENV === 'production'; diff --git a/server/api/auth/register.post.ts b/server/api/auth/register.post.ts index 41bbac6..02cb081 100644 --- a/server/api/auth/register.post.ts +++ b/server/api/auth/register.post.ts @@ -2,14 +2,7 @@ import bcrypt from 'bcrypt'; import { defineEventHandler } from 'h3'; import * as db from 'zapatos/db'; import { dbPool } from '~/db/connection'; - -export enum RegisterErrorCode { - INVALID_BODY = 1000, - USER_ALREADY_EXISTS = 1001, - PASSWORD_TOO_SHORT = 1002, - INVALID_USER_NAME = 1003, - UNKNOWN_ERROR = 2000, -} +import { RegisterErrorCode } from '~/lib'; export default defineEventHandler(async (event) => { const body = await readBody(event); diff --git a/server/api/files/content/index.get.ts b/server/api/files/content/index.get.ts index d88964a..b1caa79 100644 --- a/server/api/files/content/index.get.ts +++ b/server/api/files/content/index.get.ts @@ -1,14 +1,9 @@ import * as db from 'zapatos/db'; import { dbPool } from '~/db/connection'; +import { FileContentGetErrorCode } from '~/lib'; import { VirtualPath } from '~/lib/path'; import { AccessType, canAccess, FileType, trimQuote } from '~/server/utils'; -export enum FileContentGetErrorCode { - INVALID_PARAM = 1000, - NOT_ENOUGH_PRIVILEGE = 2000, - FILE_NOT_FOUND = 3000, -} - export default defineEventHandler(async (event) => { const { name } = getQuery(event); if (typeof name !== 'string') { diff --git a/server/api/files/content/index.patch.ts b/server/api/files/content/index.patch.ts index e94ff33..83133c8 100644 --- a/server/api/files/content/index.patch.ts +++ b/server/api/files/content/index.patch.ts @@ -1,15 +1,9 @@ import * as db from 'zapatos/db'; import { dbPool } from '~/db/connection'; +import { FileContentPatchErrorCode } from '~/lib'; import { VirtualPath } from '~/lib/path'; import { AccessType, canAccess, FileType, trimQuote } from '~/server/utils'; -export enum FileContentPatchErrorCode { - INVALID_PARAM = 1000, - INVALID_BODY = 1001, - NOT_ENOUGH_PRIVILEGE = 2000, - FILE_NOT_FOUND = 3000, -} - export default defineEventHandler(async (event) => { const { name } = getQuery(event); if (typeof name !== 'string') { diff --git a/server/api/files/cp.post.ts b/server/api/files/cp.post.ts index e302797..f1096ab 100644 --- a/server/api/files/cp.post.ts +++ b/server/api/files/cp.post.ts @@ -1,17 +1,9 @@ import * as db from 'zapatos/db'; import { dbPool } from '~/db/connection'; +import { FileCpErrorCode } from '~/lib'; import { VirtualPath } from '~/lib/path'; import { FileType, AccessType, canAccess } from '~/server/utils'; -export enum FileCpErrorCode { - INVALID_PARAM = 1000, - INVALID_BODY = 1001, - NOT_ENOUGH_PRIVILEGE = 2000, - SRC_NOT_FOUND = 3000, - DEST_NOT_FOUND = 3001, - INVALID_COPY_FOLDER_TO_FILE = 3002, -} - export default defineEventHandler(async (event) => { if (!event.context.auth) { return { error: { code: FileCpErrorCode.NOT_ENOUGH_PRIVILEGE, message: 'Should be logged in as a user with enough privilege' } }; diff --git a/server/api/files/index.delete.ts b/server/api/files/index.delete.ts index ddce96d..f28e3b5 100644 --- a/server/api/files/index.delete.ts +++ b/server/api/files/index.delete.ts @@ -1,14 +1,9 @@ import * as db from 'zapatos/db'; import { dbPool } from '~/db/connection'; +import { FileDeleteErrorCode } from '~/lib'; import { VirtualPath } from '~/lib/path'; import { AccessType, canAccess, FileType, trimQuote } from '~/server/utils'; -export enum FileDeleteErrorCode { - INVALID_PARAM = 1000, - NOT_ENOUGH_PRIVILEGE = 2000, - FILE_NOT_FOUND = 3000, -} - export default defineEventHandler(async (event) => { const { name } = getQuery(event); if (typeof name !== 'string') { @@ -33,7 +28,7 @@ export default defineEventHandler(async (event) => { ) { return { error: { code: FileDeleteErrorCode.NOT_ENOUGH_PRIVILEGE, message: 'Should be logged in as a user with enough privilege' } }; } - + if (!(await db.selectOne('files', { name: filepath.toString(), deleted_at: db.conditions.isNull }).run(dbPool))) { return { error: { code: FileDeleteErrorCode.FILE_NOT_FOUND, message: 'File not found' } }; } diff --git a/server/api/files/index.get.ts b/server/api/files/index.get.ts index abd2b27..41da60b 100644 --- a/server/api/files/index.get.ts +++ b/server/api/files/index.get.ts @@ -1,14 +1,9 @@ import * as db from 'zapatos/db'; import { dbPool } from '~/db/connection'; +import { FileMetaGetErrorCode } from '~/lib'; import { VirtualPath } from '~/lib/path'; import { AccessType, canAccess, FileType, trimQuote } from '~/server/utils'; -export enum FileMetaGetErrorCode { - INVALID_PARAM = 1000, - NOT_ENOUGH_PRIVILEGE = 2000, - FILE_NOT_FOUND = 3000, -} - export default defineEventHandler(async (event) => { const { name } = getQuery(event); if (typeof name !== 'string') { diff --git a/server/api/files/index.patch.ts b/server/api/files/index.patch.ts index a5c8b05..161b601 100644 --- a/server/api/files/index.patch.ts +++ b/server/api/files/index.patch.ts @@ -1,18 +1,10 @@ import type { EventHandlerRequest, H3Event } from 'h3'; import * as db from 'zapatos/db'; import { dbPool } from '~/db/connection'; +import { FileMetaPatchErrorCode } from '~/lib'; import { VirtualPath } from '~/lib/path'; import { AccessType, canAccess, FileType, trimQuote } from '~/server/utils'; -export enum FileMetaPatchErrorCode { - INVALID_PARAM = 1000, - INVALID_BODY = 1001, - NOT_ENOUGH_PRIVILEGE = 2000, - FILE_NOT_FOUND = 3000, - DESTINATION_NOT_EXIST = 3001, - SOURCE_NOT_EXIST = 3002, -} - export default defineEventHandler(async (event) => { const { name } = getQuery(event); if (typeof name !== 'string') { diff --git a/server/api/files/index.post.ts b/server/api/files/index.post.ts index 3b50d8b..2006fd4 100644 --- a/server/api/files/index.post.ts +++ b/server/api/files/index.post.ts @@ -1,16 +1,9 @@ import * as db from 'zapatos/db'; import { dbPool } from '~/db/connection'; +import { FilePostErrorCode } from '~/lib'; import { VirtualPath } from '~/lib/path'; import { AccessType, canAccess, FileType, trimQuote } from '~/server/utils'; -export enum FilePostErrorCode { - INVALID_PARAM = 1000, - INVALID_BODY = 1001, - NOT_ENOUGH_PRIVILEGE = 2000, - INVALID_FOLDER = 3000, - FILE_ALREADY_EXISTS = 3001, -} - export default defineEventHandler(async (event) => { const { name } = getQuery(event); if (typeof name !== 'string') { diff --git a/server/api/files/ls.get.ts b/server/api/files/ls.get.ts index 44d7491..ccadc6b 100644 --- a/server/api/files/ls.get.ts +++ b/server/api/files/ls.get.ts @@ -1,14 +1,9 @@ import * as db from 'zapatos/db'; import { dbPool } from '~/db/connection'; +import { FileLsErrorCode } from '~/lib'; import { VirtualPath } from '~/lib/path'; import { AccessType, canAccess, FileType, trimQuote } from '~/server/utils'; -export enum FileLsErrorCode { - INVALID_PARAM = 1000, - NOT_ENOUGH_PRIVILEGE = 2000, - FILE_NOT_FOUND = 3000, -} - export default defineEventHandler(async (event) => { const { name } = getQuery(event); if (typeof name !== 'string') { diff --git a/server/api/files/mv.post.ts b/server/api/files/mv.post.ts index ef129e1..c110b6b 100644 --- a/server/api/files/mv.post.ts +++ b/server/api/files/mv.post.ts @@ -1,17 +1,9 @@ import * as db from 'zapatos/db'; import { dbPool } from '~/db/connection'; +import { FileMvErrorCode } from '~/lib'; import { VirtualPath } from '~/lib/path'; import { FileType, AccessType, canAccess } from '~/server/utils'; -export enum FileMvErrorCode { - INVALID_PARAM = 1000, - INVALID_BODY = 1001, - NOT_ENOUGH_PRIVILEGE = 2000, - SRC_NOT_FOUND = 3000, - DEST_NOT_FOUND = 3001, - INVALID_MV_FOLDER_TO_FILE = 3002, -} - export default defineEventHandler(async (event) => { if (!event.context.auth) { return { error: { code: FileMvErrorCode.NOT_ENOUGH_PRIVILEGE, message: 'Should be logged in as a user with enough privilege' } }; diff --git a/server/api/groups/index.get.ts b/server/api/groups/index.get.ts index 8bdcb75..c3dda16 100644 --- a/server/api/groups/index.get.ts +++ b/server/api/groups/index.get.ts @@ -1,19 +1,14 @@ import { formatArg } from '~/lib/command/utils'; import * as db from 'zapatos/db'; import { dbPool } from '~/db/connection'; - -export enum GroupGetErrorCode { - INVALID_PARAM = 1000, - NOT_ENOUGH_PRIVILEGE = 2000, - GROUP_NOT_FOUND = 3000, -} +import { GroupGetErrorCode } from '~/lib'; export default defineEventHandler(async (event) => { const { id } = getQuery(event); if (typeof id !== 'string') { return { error: { code: GroupGetErrorCode.INVALID_PARAM, message: 'Expect the "id" query param to be string' } }; } - const formattedId = Number.parseInt(formatArg(id)); + const formattedId = Number.parseInt(formatArg(id)!); try { const { name, created_at: createdAt } = await db.selectExactlyOne('groups', { id: formattedId, deleted_at: db.conditions.isNull }).run(dbPool); diff --git a/server/api/users/index.delete.ts b/server/api/users/index.delete.ts index ff9d3b0..e62f5f5 100644 --- a/server/api/users/index.delete.ts +++ b/server/api/users/index.delete.ts @@ -1,12 +1,7 @@ import { formatArg } from '~/lib/command/utils'; import * as db from 'zapatos/db'; import { dbPool } from '~/db/connection'; - -export enum UserDeleteErrorCode { - INVALID_PARAM = 1000, - NOT_ENOUGH_PRIVILEGE = 2000, - UNDELETABLE_USER = 2001, -} +import { UserDeleteErrorCode } from '~/lib'; export default defineEventHandler(async (event) => { const { name } = getQuery(event); @@ -16,7 +11,7 @@ export default defineEventHandler(async (event) => { if (!event.context.auth) { return { error: { code: UserDeleteErrorCode.NOT_ENOUGH_PRIVILEGE, message: 'Should be logged in as a user with enough privilege' } }; } - const formattedName = formatArg(name); + const formattedName = formatArg(name)!; // Only allow a user to remove itself currently if (formattedName !== event.context.auth.username) { return { error: { code: UserDeleteErrorCode.NOT_ENOUGH_PRIVILEGE, message: 'Should be logged in as a user with enough privilege' } }; @@ -27,6 +22,6 @@ export default defineEventHandler(async (event) => { } await db.update('users', { name: formattedName }, { deleted_at: new Date(Date.now()) }).run(dbPool); - + return { ok: { message: 'Delete user successfully' } }; }); diff --git a/server/api/users/index.get.ts b/server/api/users/index.get.ts index 3b1530c..3b4c495 100644 --- a/server/api/users/index.get.ts +++ b/server/api/users/index.get.ts @@ -1,19 +1,14 @@ import { formatArg } from '~/lib/command/utils'; import * as db from 'zapatos/db'; import { dbPool } from '~/db/connection'; - -export enum UserGetErrorCode { - INVALID_PARAM = 1000, - NOT_ENOUGH_PRIVILEGE = 2000, - USER_NOT_FOUND = 3000, -} +import { UserGetErrorCode } from '~/lib'; export default defineEventHandler(async (event) => { const { id } = getQuery(event); if (typeof id !== 'string') { return { error: { code: UserGetErrorCode.INVALID_PARAM, message: 'Expect the "id" query param to be string' } }; } - const formattedId = Number.parseInt(formatArg(id)); + const formattedId = Number.parseInt(formatArg(id)!); try { const { name: username, created_at, id, group_id } = await db.selectExactlyOne('users', { id: formattedId, deleted_at: db.conditions.isNull }).run(dbPool); From 91c91bf067d3d2c7e021ff0b6efc800973f03f4a Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 18:21:15 +0700 Subject: [PATCH 12/18] feat: implement writeFileContent and appendFileContent file services --- services/files.ts | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/services/files.ts b/services/files.ts index 124f59e..2789605 100644 --- a/services/files.ts +++ b/services/files.ts @@ -1,5 +1,6 @@ import path from 'path-browserify'; import { Err, Ok, type Diagnostic, type Result } from './types'; +import { FilePostErrorCode } from '~/lib'; export enum UserKind { OWNER = 'owner', @@ -37,9 +38,51 @@ export interface FileMeta { export const fileService = { async getMetaOfFile (filename: string): Promise> { }, - async getFileContent (filename: string): Promise> { + async getFileContent (filename: string): Promise> { }, - async updateFileContent (filename: string, content: Uint8Array): Promise> { + // FIXME: Possible race condition if multiple modifications happen on a file + async writeFileContent (filename: string, content: string): Promise> { + const { umask } = useUmaskStore(); + const createRes = await fileService.createFile(filename, '', umask.value); + if (!createRes.isOk() && createRes.error()!.code === FilePostErrorCode.INVALID_FOLDER) { + return createRes; + } + const { cwd } = useCwdStore(); + const res = await $fetch('/api/files/content', { + method: 'patch', + query: { name: cwd.value.resolve(filename) }, + body: { + content, + shouldAppend: false, + }, + credentials: 'include', + }); + if (res.error) { + return new Err({ code: res.error.code, message: res.error.message }); + } + return new Ok(null); + }, + // FIXME: Possible race condition if multiple modifications happen on a file + async appendFileCOntent (filename: string, content: string): Promise> { + const { umask } = useUmaskStore(); + const createRes = await fileService.createFile(filename, '', umask.value); + if (!createRes.isOk() && createRes.error()!.code === FilePostErrorCode.INVALID_FOLDER) { + return createRes; + } + const { cwd } = useCwdStore(); + const res = await $fetch('/api/files/content', { + method: 'patch', + query: { name: cwd.value.resolve(filename) }, + body: { + content, + shouldAppend: true, + }, + credentials: 'include', + }); + if (res.error) { + return new Err({ code: res.error.code, message: res.error.message }); + } + return new Ok(null); }, async getFolderContent (filename: string): Promise> { const { cwd } = useCwdStore(); From 7a0bf6eb44725acc4bdcffa9802c11b6a5dc5461 Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 18:26:45 +0700 Subject: [PATCH 13/18] feat: basic implementation of redirect output --- lib/command/index.ts | 15 +++++++++++++-- services/files.ts | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/command/index.ts b/lib/command/index.ts index 1c80b40..43097b8 100644 --- a/lib/command/index.ts +++ b/lib/command/index.ts @@ -14,7 +14,7 @@ import { umask } from './impls/umask'; import { rm } from './impls/rm'; import { cp } from './impls/cp'; import { mv } from './impls/mv'; -import { Err, Ok, type Result } from '~/services'; +import { Err, fileService, Ok, type Result } from '~/services'; export async function execute (command: string): Promise { const shellDirRes = extractShellRedirection(...parse(command).filter((arg) => arg.trim())); @@ -91,5 +91,16 @@ function extractShellRedirection (...args: string[]): Result<{ } async function redirectOutput (output: string[], { mode, name }: { mode: RedirectionMode, name: string }): Promise { - return []; + const aggOutput = output.join('\n') + '\n'; + let res; + switch (mode) { + case RedirectionMode.Append: + res = await fileService.appendFileContent(name, aggOutput); + break; + case RedirectionMode.Output: + res = await fileService.writeFileContent(name, aggOutput); + break; + } + if (res.isOk()) return []; + return [res.error()!.message]; } diff --git a/services/files.ts b/services/files.ts index 2789605..aa28fa1 100644 --- a/services/files.ts +++ b/services/files.ts @@ -63,7 +63,7 @@ export const fileService = { return new Ok(null); }, // FIXME: Possible race condition if multiple modifications happen on a file - async appendFileCOntent (filename: string, content: string): Promise> { + async appendFileContent (filename: string, content: string): Promise> { const { umask } = useUmaskStore(); const createRes = await fileService.createFile(filename, '', umask.value); if (!createRes.isOk() && createRes.error()!.code === FilePostErrorCode.INVALID_FOLDER) { From cfd372072032010ae3ec50f3baae86e2bd9ffd77 Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 18:27:28 +0700 Subject: [PATCH 14/18] fix: typo --- services/files.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/files.ts b/services/files.ts index aa28fa1..fc631a2 100644 --- a/services/files.ts +++ b/services/files.ts @@ -63,7 +63,7 @@ export const fileService = { return new Ok(null); }, // FIXME: Possible race condition if multiple modifications happen on a file - async appendFileContent (filename: string, content: string): Promise> { + async appendFileContent (filename: string, content: string): Promise> { const { umask } = useUmaskStore(); const createRes = await fileService.createFile(filename, '', umask.value); if (!createRes.isOk() && createRes.error()!.code === FilePostErrorCode.INVALID_FOLDER) { From d74ee85f04a71b8c25b6846662b53f2a0db1bfe7 Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 18:35:37 +0700 Subject: [PATCH 15/18] fix: forget to call toString on filepath before passing to fetch --- server/api/files/content/index.patch.ts | 3 ++- services/files.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/server/api/files/content/index.patch.ts b/server/api/files/content/index.patch.ts index 83133c8..a51feb7 100644 --- a/server/api/files/content/index.patch.ts +++ b/server/api/files/content/index.patch.ts @@ -33,7 +33,8 @@ export default defineEventHandler(async (event) => { await db.update('files', { content: newContent }, { name: filepath.toString(), deleted_at: db.conditions.isNull }).run(dbPool); return { ok: { message: 'Update file content successfully' } }; - } catch { + } catch (e) { + console.log(e); return { error: { code: FileContentPatchErrorCode.FILE_NOT_FOUND, message: 'File not found' } }; } }); diff --git a/services/files.ts b/services/files.ts index fc631a2..71c1549 100644 --- a/services/files.ts +++ b/services/files.ts @@ -50,7 +50,7 @@ export const fileService = { const { cwd } = useCwdStore(); const res = await $fetch('/api/files/content', { method: 'patch', - query: { name: cwd.value.resolve(filename) }, + query: { name: cwd.value.resolve(filename).toString() }, body: { content, shouldAppend: false, @@ -72,7 +72,7 @@ export const fileService = { const { cwd } = useCwdStore(); const res = await $fetch('/api/files/content', { method: 'patch', - query: { name: cwd.value.resolve(filename) }, + query: { name: cwd.value.resolve(filename).toString() }, body: { content, shouldAppend: true, From 616f79590ddcb02104d4721ebfa87f82c2c8fb76 Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 18:41:41 +0700 Subject: [PATCH 16/18] fix: add isValid check for filepath in api --- server/api/files/content/index.get.ts | 3 +++ server/api/files/content/index.patch.ts | 3 +++ server/api/files/cp.post.ts | 7 +++++++ server/api/files/index.delete.ts | 2 +- server/api/files/index.get.ts | 3 +++ server/api/files/index.patch.ts | 6 ++++++ server/api/files/index.post.ts | 3 +++ server/api/files/ls.get.ts | 3 +++ server/api/files/mv.post.ts | 6 ++++++ 9 files changed, 35 insertions(+), 1 deletion(-) diff --git a/server/api/files/content/index.get.ts b/server/api/files/content/index.get.ts index b1caa79..dcd0a45 100644 --- a/server/api/files/content/index.get.ts +++ b/server/api/files/content/index.get.ts @@ -13,6 +13,9 @@ export default defineEventHandler(async (event) => { return { error: { code: FileContentGetErrorCode.NOT_ENOUGH_PRIVILEGE, message: 'Should be logged in as a user with enough privilege' } }; } const filepath = VirtualPath.create(trimQuote(name)); + if (!filepath.isValid()) { + return { error: { code: FileContentGetErrorCode.INVALID_PARAM, message: 'Expect the "name" query param to be valid path' } }; + } try { const { permission_bits: filePermissionBits, owner_id: fileOwnerId, group_id: fileGroupId, content } = await db.selectExactlyOne('files', { name: filepath.toString(), file_type: 'file', deleted_at: db.conditions.isNull }).run(dbPool); if ( diff --git a/server/api/files/content/index.patch.ts b/server/api/files/content/index.patch.ts index a51feb7..91a7cee 100644 --- a/server/api/files/content/index.patch.ts +++ b/server/api/files/content/index.patch.ts @@ -17,6 +17,9 @@ export default defineEventHandler(async (event) => { return { error: { code: FileContentPatchErrorCode.NOT_ENOUGH_PRIVILEGE, message: 'Should be logged in as a user with enough privilege' } }; } const filepath = VirtualPath.create(trimQuote(name)); + if (!filepath.isValid()) { + return { error: { code: FileContentPatchErrorCode.INVALID_PARAM, message: 'Expect the "name" query param to be valid path' } }; + } try { const { permission_bits: filePermissionBits, owner_id: fileOwnerId, group_id: fileGroupId, content } = await db.selectExactlyOne('files', { name: filepath.toString(), file_type: 'file', deleted_at: db.conditions.isNull }).run(dbPool); if ( diff --git a/server/api/files/cp.post.ts b/server/api/files/cp.post.ts index f1096ab..7136100 100644 --- a/server/api/files/cp.post.ts +++ b/server/api/files/cp.post.ts @@ -17,8 +17,15 @@ export default defineEventHandler(async (event) => { typeof body.permission_bits === 'string' && (body.permission_bits.length !== 12 || !body.permission_bits.split('').every((bit: string) => ['0', '1'].includes(bit)))) { return { error: { code: FileCpErrorCode.INVALID_BODY, message: 'Invalid body. Expected "src" and "dest" to be strings and permission_bits to be a bit string.' } }; } + const src = VirtualPath.create(body.src); + if (!src.isValid()) { + return { error: { code: FileCpErrorCode.INVALID_BODY, message: 'Expect the "src" param to be valid path' } }; + } const dest = VirtualPath.create(body.dest); + if (!dest.isValid()) { + return { error: { code: FileCpErrorCode.INVALID_BODY, message: 'Expect the "dest" param to be valid path' } }; + } try { await db.serializable(dbPool, async (dbClient) => { diff --git a/server/api/files/index.delete.ts b/server/api/files/index.delete.ts index f28e3b5..f5e5128 100644 --- a/server/api/files/index.delete.ts +++ b/server/api/files/index.delete.ts @@ -14,7 +14,7 @@ export default defineEventHandler(async (event) => { } const filepath = VirtualPath.create(trimQuote(name)); if (!filepath.isValid()) { - return { error: { code: FileDeleteErrorCode.INVALID_PARAM, message: 'Invalid filename' } }; + return { error: { code: FileDeleteErrorCode.INVALID_PARAM, message: 'Expect the "name" query param to be valid path' } }; } const containerPath = filepath.parent(); try { diff --git a/server/api/files/index.get.ts b/server/api/files/index.get.ts index 41da60b..f15af4c 100644 --- a/server/api/files/index.get.ts +++ b/server/api/files/index.get.ts @@ -13,6 +13,9 @@ export default defineEventHandler(async (event) => { return { error: { code: FileMetaGetErrorCode.NOT_ENOUGH_PRIVILEGE, message: 'Should be logged in as a user with enough privilege' } }; } const filepath = VirtualPath.create(trimQuote(name)); + if (!filepath.isValid()) { + return { error: { code: FileMetaGetErrorCode.INVALID_PARAM, message: 'Expect the "name" query param to be valid path' } }; + } const containerPath = filepath.parent(); try { const { permission_bits: containerDirPermissionBits, owner_id: containerDirOwnerId, group_id: containerDirGroupId } = await db.selectExactlyOne('files', { name: containerPath.toString(), file_type: 'directory', deleted_at: db.conditions.isNull }).run(dbPool); diff --git a/server/api/files/index.patch.ts b/server/api/files/index.patch.ts index 161b601..1765561 100644 --- a/server/api/files/index.patch.ts +++ b/server/api/files/index.patch.ts @@ -94,6 +94,9 @@ async function handleNameChange (dbClient: db.TxnCl async function handleOwnerChange (dbClient: db.TxnClient, event: H3Event, ownerId: number) { const { name } = getQuery(event); const filepath = VirtualPath.create(trimQuote(name as string)); + if (!filepath.isValid()) { + throw { error: { code: FileMetaPatchErrorCode.INVALID_PARAM, message: 'Expect the "name" query param to be valid path' } }; + } let oldOwnerId; try { @@ -113,6 +116,9 @@ async function handleOwnerChange (dbClient: db.TxnC async function handlePermissionChange (dbClient: db.TxnClient, event: H3Event, permissionBits: string) { const { name } = getQuery(event); const filepath = VirtualPath.create(trimQuote(name as string)); + if (!filepath.isValid()) { + throw { error: { code: FileMetaPatchErrorCode.INVALID_PARAM, message: 'Expect the "name" query param to be valid path' } }; + } let ownerId; try { diff --git a/server/api/files/index.post.ts b/server/api/files/index.post.ts index 2006fd4..067273a 100644 --- a/server/api/files/index.post.ts +++ b/server/api/files/index.post.ts @@ -18,6 +18,9 @@ export default defineEventHandler(async (event) => { } const { content, permission_bits } = body; const filepath = VirtualPath.create(trimQuote(name)); + if (!filepath.isValid()) { + return { error: { code: FilePostErrorCode.INVALID_PARAM, message: 'Expect the "name" query param to be valid path' } }; + } const containerPath = filepath.parent(); try { const { permission_bits: containerDirPermissionBits, owner_id: containerDirOwnerId, group_id: containerDirGroupId } = await db.selectExactlyOne('files', { name: containerPath.toString(), file_type: 'directory', deleted_at: db.conditions.isNull }).run(dbPool); diff --git a/server/api/files/ls.get.ts b/server/api/files/ls.get.ts index ccadc6b..a690c77 100644 --- a/server/api/files/ls.get.ts +++ b/server/api/files/ls.get.ts @@ -13,6 +13,9 @@ export default defineEventHandler(async (event) => { return { error: { code: FileLsErrorCode.NOT_ENOUGH_PRIVILEGE, message: 'Should be logged in as a user with enough privilege' } }; } const filepath = VirtualPath.create(trimQuote(name)); + if (!filepath.isValid()) { + return { error: { code: FileLsErrorCode.INVALID_PARAM, message: 'Expect the "name" query param to be valid path' } }; + } try { const { permission_bits: filePermissionBits, owner_id: fileOwnerId, group_id: fileGroupId, file_type: fileType, created_at: createdAt, updated_at: updatedAt } = await db.selectExactlyOne('files', { name: filepath.toString(), deleted_at: db.conditions.isNull }).run(dbPool); if ( diff --git a/server/api/files/mv.post.ts b/server/api/files/mv.post.ts index c110b6b..fb9f993 100644 --- a/server/api/files/mv.post.ts +++ b/server/api/files/mv.post.ts @@ -18,7 +18,13 @@ export default defineEventHandler(async (event) => { return { error: { code: FileMvErrorCode.INVALID_BODY, message: 'Invalid body. Expected "src" and "dest" to be strings and permission_bits to be a bit string.' } }; } const src = VirtualPath.create(body.src); + if (!src.isValid()) { + return { error: { code: FileMvErrorCode.INVALID_BODY, message: 'Expect the "src" param to be valid path' } }; + } const dest = VirtualPath.create(body.dest); + if (!dest.isValid()) { + return { error: { code: FileMvErrorCode.INVALID_BODY, message: 'Expect the "dest" param to be valid path' } }; + } try { await db.serializable(dbPool, async (dbClient) => { From af42ba220df300e6b7451e71c80df29b7c0452c1 Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 18:44:19 +0700 Subject: [PATCH 17/18] fix: remove unnecessary console.log --- server/api/files/content/index.patch.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/api/files/content/index.patch.ts b/server/api/files/content/index.patch.ts index 91a7cee..8954983 100644 --- a/server/api/files/content/index.patch.ts +++ b/server/api/files/content/index.patch.ts @@ -36,8 +36,7 @@ export default defineEventHandler(async (event) => { await db.update('files', { content: newContent }, { name: filepath.toString(), deleted_at: db.conditions.isNull }).run(dbPool); return { ok: { message: 'Update file content successfully' } }; - } catch (e) { - console.log(e); + } catch { return { error: { code: FileContentPatchErrorCode.FILE_NOT_FOUND, message: 'File not found' } }; } }); From fae0db0ded0e293837872343ccad1ff54ef09955 Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Tue, 12 Nov 2024 18:45:22 +0700 Subject: [PATCH 18/18] fix: unshift redirection instead of push --- lib/command/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/command/index.ts b/lib/command/index.ts index 43097b8..6331711 100644 --- a/lib/command/index.ts +++ b/lib/command/index.ts @@ -83,7 +83,7 @@ function extractShellRedirection (...args: string[]): Result<{ const mode = arg === '>' ? RedirectionMode.Output : RedirectionMode.Append; const pathname = args[i + 1]; if (pathname === undefined) return new Err([`Parse error: no pathname found after '${arg}'`]); - redirections.push({ mode, name: pathname }); + redirections.unshift({ mode, name: pathname }); args.splice(i, 2); } }