Skip to content

Commit

Permalink
Merge pull request #16 from Huy-DNA/feat/ls-command
Browse files Browse the repository at this point in the history
Feat/ls command
  • Loading branch information
Huy-DNA authored Nov 4, 2024
2 parents b841f16 + 9c8e066 commit 53978c2
Show file tree
Hide file tree
Showing 14 changed files with 193 additions and 37 deletions.
6 changes: 6 additions & 0 deletions lib/command/impls/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ const commandDescriptions: Record<Command, CommandDescription> = {
{ args: ['-u <username>', '-p <password>'] },
],
},
[Command.LS]: {
description: 'List directory\'s content',
usages: [
{ args: ['<dirname>?'] },
],
},
[Command.USERADD]: {
description: 'Register user',
usages: [
Expand Down
61 changes: 61 additions & 0 deletions lib/command/impls/ls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { userService } from '~/services/users';
import { formatArg } from '../utils';
import type { CommandFunc } from './types';
import { fileService } from '~/services/files';
import { groupService } from '~/services/groups';

export const ls: CommandFunc = async function(...args) {
// discard `ls`
args.shift();
// discard first space
args.shift();

if (args.length > 1) {
return ['Expect an optional dirname as argument.'];
}

const dirname = args.length ? formatArg(args[0]) : '.';
const res = await fileService.getFolderContent(dirname);

if (res.isOk()) {
const files = res.unwrap();
const fileLines = [];
for (const file of files) {
const fileType = formatFileType(file.fileType as string);
const filePermissionBits = formatPermissionBits(file.permission as unknown as string);
const fileOwner = (await userService.getMetaOfUser(file.ownerId)).unwrap().name;
const fileGroup = (await groupService.getMetaOfGroup(file.groupId)).unwrap().name;
fileLines.push(`${fileType}${filePermissionBits} ${fileOwner} ${fileGroup} ${file.name}`);
}
return [
`total ${files.length}`,
...fileLines,
];
}

return [
res.error()!.message,
];
};

function formatFileType(fileType: string): string {
switch (fileType) {
case 'file': return '-';
case 'directory': return 'd';
case 'symlink': return 'l';
default: throw new Error('Unreachable');
}
}

function formatPermissionBits(permissionBits: string): string {
const ownerRead = permissionBits[3];
const ownerWrite = permissionBits[4];
const ownerExecute = permissionBits[5];
const groupRead = permissionBits[6];
const groupWrite = permissionBits[7];
const groupExecute = permissionBits[8];
const otherRead = permissionBits[9];
const otherWrite = permissionBits[10];
const otherExecute = permissionBits[11];
return `${ownerRead ? 'r' : '-'}${ownerWrite ? 'w' : '-'}${ownerExecute ? 'x' : '-'}${groupRead ? 'r' : '-'}${groupWrite ? 'w' : '-'}${groupExecute ? 'x' : '-'}${otherRead ? 'r' : '-'}${otherWrite ? 'w' : '-'}${otherExecute ? 'x' : '-'}`;
}
1 change: 1 addition & 0 deletions lib/command/impls/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export enum Command {
HELP = 'help',
CD = 'cd',
SU = 'su',
LS = 'ls',
USERADD = 'useradd',
}
4 changes: 4 additions & 0 deletions lib/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { interpretAnsiEscapeColor } from './utils';
import { parse } from '../services/parse';
import { cd } from './impls/cd';
import { su } from './impls/su';
import { ls } from './impls/ls';
import { useradd } from './impls/useradd';

export async function execute(command: string): Promise<ColoredContent> {
Expand All @@ -30,6 +31,9 @@ export async function execute(command: string): Promise<ColoredContent> {
case Command.SU:
res = await su(...args as any);
break;
case Command.LS:
res = await ls(...args as any);
break;
case Command.USERADD:
res = await useradd(...args as any);
break;
Expand Down
2 changes: 1 addition & 1 deletion server/api/files/content/index.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default defineEventHandler(async (event) => {
if (
!canAccess(
{ userId: event.context.auth.userId as number, groupId: event.context.auth.groupId as number },
{ fileType: FileType.REGULAR_FILE, ownerId: fileOwnerId, groupId: fileGroupId, permissionBits: filePermissionBits },
{ fileType: FileType.UNKNOWN, ownerId: fileOwnerId, groupId: fileGroupId, permissionBits: filePermissionBits },
AccessType.READ,
)
) {
Expand Down
2 changes: 1 addition & 1 deletion server/api/files/content/index.patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default defineEventHandler(async (event) => {
if (
!canAccess(
{ userId: event.context.auth.userId as number, groupId: event.context.auth.groupId as number },
{ fileType: FileType.REGULAR_FILE, ownerId: fileOwnerId, groupId: fileGroupId, permissionBits: filePermissionBits },
{ fileType: FileType.UNKNOWN, ownerId: fileOwnerId, groupId: fileGroupId, permissionBits: filePermissionBits },
AccessType.WRITE,
)
) {
Expand Down
5 changes: 4 additions & 1 deletion server/api/files/index.delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ export default defineEventHandler(async (event) => {
return { error: { code: FileDeleteErrorCode.FILE_NOT_FOUND, message: 'File not found' } };
}

await db.update('files', { deleted_at: new Date(Date.now()) }, { name: db.conditions.like(`${filepath.toString()}%`) }).run(dbPool);
await db.readCommitted(dbPool, async (dbClient) => {
await db.update('files', { deleted_at: new Date(Date.now()) }, { name: db.conditions.like(`${filepath.toString()}/%`) }).run(dbClient);
await db.update('files', { deleted_at: new Date(Date.now()) }, { name: filepath.toString() }).run(dbClient);
});

return { ok: { message: 'Delete file successfully' } };
} catch {
Expand Down
43 changes: 43 additions & 0 deletions server/api/files/ls.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as db from 'zapatos/db';
import { dbPool } from '~/db/connection';
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') {
return { error: { code: FileLsErrorCode.INVALID_PARAM, message: 'Expect the "name" query param to be string' } };
}
if (!event.context.auth) {
return { error: { code: FileLsErrorCode.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, file_type: fileType, created_at: createdAt, updated_at: updatedAt } = await db.selectExactlyOne('files', { name: filepath.toString() }).run(dbPool);
if (
!canAccess(
{ userId: event.context.auth.userId as number, groupId: event.context.auth.groupId as number },
{ fileType: FileType.UNKNOWN, ownerId: fileOwnerId, groupId: fileGroupId, permissionBits: filePermissionBits },
AccessType.EXECUTE,
)
) {
return { error: { code: FileLsErrorCode.NOT_ENOUGH_PRIVILEGE, message: 'Should be logged in as a user with enough privilege' } };
}

if (fileType === 'file') {
return { ok: { message: 'Fetch file meta successfully', data: { files: [{ name: filepath.toString(), ownerId: fileOwnerId, groupId: fileGroupId, fileType: fileType, createdAt, updatedAt, permissionBits: filePermissionBits }] } } };
}

const files = await db.select('files', { name: db.conditions.and(db.conditions.like(`${filepath.toString()}/%`), db.conditions.notLike(`${filepath.toString()}/%/%`)) }).run(dbPool);

return { ok: { message: 'Fetch folder\'s content successfully', data: { files: files.map(({ permission_bits, updated_at, name, file_type, created_at, owner_id, group_id }) => ({ name, fileType: file_type, createdAt: created_at, ownerId: owner_id, groupId: group_id, permissionBits: permission_bits, updatedAt: updated_at })) } } };
} catch {
return { error: { code: FileLsErrorCode.FILE_NOT_FOUND, message: 'File not found' } };
}
});
19 changes: 6 additions & 13 deletions server/api/groups/index.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,15 @@ export enum GroupGetErrorCode {
}

export default defineEventHandler(async (event) => {
const { name } = getQuery(event);
if (typeof name !== 'string') {
return { error: { code: GroupGetErrorCode.INVALID_PARAM, message: 'Expect the "name" query param to be string' } };
}
const formattedName = formatArg(name);
if (formattedName !== 'guest' && !event.context.auth) {
return { error: { code: GroupGetErrorCode.NOT_ENOUGH_PRIVILEGE, message: 'Should be logged in as a user with enough privilege' } };
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));

try {
const { group_id: groupId } = await db.selectExactlyOne('users', { name: event.context.auth.username, deleted_at: db.conditions.isNull }).run(dbPool);
const { name: trueGroupName, created_at: createdAt } = await db.selectExactlyOne('groups', { id: groupId, deleted_at: db.conditions.isNull }).run(dbPool);
if (formattedName !== 'guest' && formattedName !== trueGroupName?.trim()) {
return { error: { code: GroupGetErrorCode.NOT_ENOUGH_PRIVILEGE, message: 'Should be logged in as a user with enough privilege' } };
}
return { ok: { data: { name, groupId, createdAt }, message: 'Get group successfully' } };
const { name, created_at: createdAt } = await db.selectExactlyOne('groups', { id: formattedId, deleted_at: db.conditions.isNull }).run(dbPool);
return { ok: { data: { name, groupId: formattedId, createdAt }, message: 'Get group successfully' } };
} catch {
return { error: { code: GroupGetErrorCode.GROUP_NOT_FOUND, message: 'Group not found' } };
}
Expand Down
15 changes: 6 additions & 9 deletions server/api/users/index.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,15 @@ export enum UserGetErrorCode {
}

export default defineEventHandler(async (event) => {
const { name } = getQuery(event);
if (typeof name !== 'string') {
return { error: { code: UserGetErrorCode.INVALID_PARAM, message: 'Expect the "name" query param to be string' } };
}
const formattedName = formatArg(name);
if (formattedName !== 'guest' && (!event.context.auth || formattedName !== event.context.auth.username)) {
return { error: { code: UserGetErrorCode.NOT_ENOUGH_PRIVILEGE, message: 'Should be logged in as a user with enough privilege' } };
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));

try {
const { name: username, created_at, id, group_id } = await db.selectExactlyOne('users', { name: formattedName, deleted_at: db.conditions.isNull }).run(dbPool);
return { ok: { data: { name: username?.trim(), userId: id, groupId: group_id, createdAt: created_at }, message: 'Get user successfully' } };
const { name: username, created_at, id, group_id } = await db.selectExactlyOne('users', { id: formattedId, deleted_at: db.conditions.isNull }).run(dbPool);
return { ok: { data: { name: username, userId: id, groupId: group_id, createdAt: created_at }, message: 'Get user successfully' } };
} catch {
return { error: { code: UserGetErrorCode.USER_NOT_FOUND, message: 'User not found' } };
}
Expand Down
1 change: 1 addition & 0 deletions server/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const enum AccessType {
}

export const enum FileType {
UNKNOWN,
REGULAR_FILE,
DIRECTORY,
}
Expand Down
44 changes: 34 additions & 10 deletions services/files.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import path from 'path-browserify';
import { Err, Ok, type Diagnostic, type Result } from './types';

export enum UserKind {
Expand Down Expand Up @@ -30,22 +31,45 @@ export interface FileMeta {
groupId: number;
createdAt: Date;
updatedAt: Date;
fileType: string;
}

export const fileService = {
async getMetaOfFile (filename: string): Promise<Result<FileMeta, Diagnostic>> {
async getMetaOfFile(filename: string): Promise<Result<FileMeta, Diagnostic>> {
},
async getFileContent (filename: string): Promise<Result<Uint8Array, Diagnostic>> {
async getFileContent(filename: string): Promise<Result<Uint8Array, Diagnostic>> {
},
async updateFileContent (filename: string, content: Uint8Array): Promise<Result<null, Diagnostic>> {
async updateFileContent(filename: string, content: Uint8Array): Promise<Result<null, Diagnostic>> {
},
async getFolderContent (filename: string): Promise<Result<FileMeta[], Diagnostic>> {
async getFolderContent(filename: string): Promise<Result<FileMeta[], Diagnostic>> {
const { cwd } = useCwdStore();
const meta = await $fetch('/api/files/ls', {
method: 'get',
query: {
name: cwd.value.resolve(filename).toString(),
},
credentials: 'include',
});
if (meta.error) {
return new Err({ code: meta.error.code, message: meta.error.message });
}
const { ok: { data } } = meta;
return new Ok(data.files.map((file) => ({
name: path.basename(file.name),
fullname: file.name,
permission: file.permissionBits,
ownerId: file.ownerId,
groupId: file.groupId,
createdAt: file.createdAt,
updatedAt: file.updatedAt,
fileType: file.fileType,
})));
},
async removeFile (filename: string): Promise<Result<null, Diagnostic>> {
async removeFile(filename: string): Promise<Result<null, Diagnostic>> {
},
async createFile (filename: string): Promise<Result<null, Diagnostic>> {
async createFile(filename: string): Promise<Result<null, Diagnostic>> {
},
async changeDirectory (filename: string): Promise<Result<null, Diagnostic>> {
async changeDirectory(filename: string): Promise<Result<null, Diagnostic>> {
try {
const { cwd, switchCwd } = useCwdStore();
const meta = await $fetch('/api/files', {
Expand All @@ -68,10 +92,10 @@ export const fileService = {
return new Err({ code: 500, message: 'Network connection error' });
}
},
async moveFile (filename: string, destination: string): Promise<Result<null, Diagnostic>> {
async moveFile(filename: string, destination: string): Promise<Result<null, Diagnostic>> {
},
async copyFile (filename: string, destination: string): Promise<Result<null, Diagnostic>> {
async copyFile(filename: string, destination: string): Promise<Result<null, Diagnostic>> {
},
async currentDirectory (): Promise<Result<string, Diagnostic>> {
async currentDirectory(): Promise<Result<string, Diagnostic>> {
},
};
16 changes: 14 additions & 2 deletions services/groups.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import type { Diagnostic, Result } from './types';
import { Err, Ok, type Diagnostic, type Result } from './types';

export interface GroupMeta {
name: string;
groupId: number;
id: number;
createdAt: Date;
}

export const groupService = {
async getMetaOfGroup (id: number): Promise<Result<GroupMeta, Diagnostic>> {
const res = await $fetch('/api/groups', {
method: 'get',
query: {
id,
},
});
if (res.error) {
return new Err({ code: res.error.code, message: res.error.message });
}
const { ok: { data } } = res;
return new Ok({ name: data.name, id: data.id, createdAt: data.createdAt });

},
};
11 changes: 11 additions & 0 deletions services/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ export interface UserMeta {

export const userService = {
async getMetaOfUser(id: number): Promise<Result<UserMeta, Diagnostic>> {
const res = await $fetch('/api/users', {
method: 'get',
query: {
id,
},
});
if (res.error) {
return new Err({ code: res.error.code, message: res.error.message });
}
const { ok: { data } } = res;
return new Ok({ name: data.name, userId: data.userId, groupId: data.groupId, createdAt: data.createdAt });
},
async getHomeDirectory(id: number): Promise<Result<string, Diagnostic>> {
},
Expand Down

0 comments on commit 53978c2

Please sign in to comment.