Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/ls command #16

Merged
merged 8 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading