diff --git a/packages/common/index.ts b/packages/common/index.ts index a0ffbd6..a5ca5b5 100644 --- a/packages/common/index.ts +++ b/packages/common/index.ts @@ -15,7 +15,8 @@ type Result = export interface Backend { getFiles: (dir: string) => Promise; - getFile: (filepath: string) => Promise>; + getFileRaw: (filepath: string) => Promise>; + getFileResolved: (filepath: string) => Promise>; createFile: ( filepath: string, @@ -23,7 +24,7 @@ export interface Backend { mode: number, uid: number, gid: number, - targetId: number + targetPath: string ) => Promise>; writeFile: ( @@ -32,7 +33,7 @@ export interface Backend { gid: number ) => Promise>; - deleteFile: (filepath: string) => Promise>; + deleteFile: (filepath: string) => Promise>; renameFile: (srcPath: string, destPath: string) => Promise>; updateMode: (filepath: string, mode: number) => Promise>; } diff --git a/packages/fuse-client/syscalls/getattr.ts b/packages/fuse-client/syscalls/getattr.ts index 269d1f3..aabac67 100644 --- a/packages/fuse-client/syscalls/getattr.ts +++ b/packages/fuse-client/syscalls/getattr.ts @@ -6,9 +6,8 @@ export const getattr: (backend: SQLiteBackend) => MountOptions["getattr"] = ( backend ) => { return async (path, cb) => { - console.log("getattr(%s)", path); - const r = await backend.getFile(path); - + console.info("getattr(%s)", path); + const r = await backend.getFileResolved(path); await match(r) .with({ status: "ok" }, async (r) => { const rSize = await backend.getFileSize(path); @@ -16,13 +15,15 @@ export const getattr: (backend: SQLiteBackend) => MountOptions["getattr"] = ( cb(fuse.ENOENT); return; } - + const rNlinks = await backend.getFileNLinks(path); const { mtime, atime, ctime, mode } = r.file; cb(0, { mtime, atime, ctime, - nlink: 1, + blocks: 1, + ino: r.file.id, + nlink: rNlinks.nLinks?.length || 1, size: rSize.size, mode: mode, // TODO: enable posix mode where real uid/gid are returned diff --git a/packages/fuse-client/syscalls/init.ts b/packages/fuse-client/syscalls/init.ts index 94fd556..b941752 100644 --- a/packages/fuse-client/syscalls/init.ts +++ b/packages/fuse-client/syscalls/init.ts @@ -6,13 +6,13 @@ export const init: (backend: SQLiteBackend) => MountOptions["init"] = ( backend ) => { return async (cb) => { - console.log("init"); + console.info("init"); //@ts-expect-error fix types const context = fuse.context(); const { uid, gid } = context; - const rootFolder = await backend.getFile("/"); + const rootFolder = await backend.getFileResolved("/"); match(rootFolder) .with({ status: "ok" }, () => {}) .with({ status: "not_found" }, async () => { diff --git a/packages/fuse-client/syscalls/link.ts b/packages/fuse-client/syscalls/link.ts index 73294d2..4861ee7 100644 --- a/packages/fuse-client/syscalls/link.ts +++ b/packages/fuse-client/syscalls/link.ts @@ -1,14 +1,37 @@ import { SQLiteBackend } from "@zoid-fs/sqlite-backend"; -import { MountOptions } from "@zoid-fs/node-fuse-bindings"; -import { symlink } from "./symlink"; +import fuse, { MountOptions } from "@zoid-fs/node-fuse-bindings"; +import { match } from "ts-pattern"; export const link: (backend: SQLiteBackend) => MountOptions["link"] = ( backend ) => { - return async (src, dest, cb) => { - console.log("link(%s, %s)", src, dest); + return async (srcPath, destPath, cb) => { + console.info("link(%s, %s)", srcPath, destPath); + + // TODO: throw if destination doesn't exist + //@ts-expect-error fix types - // TODO: implement link properly - symlink(backend)(src, dest, cb); + const context = fuse.context(); + const { uid, gid } = context; + + // TODO: double check if mode for link is correct + // https://unix.stackexchange.com/questions/193465/what-file-mode-is-a-link + const r = await backend.createFile( + destPath, + "link", + 41453, // Link's mode??? from node-fuse-binding source, why though? + uid, + gid, + srcPath + ); + console.log({ r }); + match(r) + .with({ status: "ok" }, () => { + cb(0); + }) + .with({ status: "not_found" }, () => { + cb(fuse.ENOENT); + }) + .exhaustive(); }; }; diff --git a/packages/fuse-client/syscalls/open.ts b/packages/fuse-client/syscalls/open.ts index a921e86..a5eff57 100644 --- a/packages/fuse-client/syscalls/open.ts +++ b/packages/fuse-client/syscalls/open.ts @@ -6,8 +6,8 @@ export const open: (backend: SQLiteBackend) => MountOptions["open"] = ( backend ) => { return async (path, flags, cb) => { - console.log("open(%s, %d)", path, flags); - const r = await backend.getFile(path); + console.info("open(%s, %d)", path, flags); + const r = await backend.getFileResolved(path); match(r) .with({ status: "ok" }, (r) => { diff --git a/packages/fuse-client/syscalls/opendir.ts b/packages/fuse-client/syscalls/opendir.ts index 78de909..536bba3 100644 --- a/packages/fuse-client/syscalls/opendir.ts +++ b/packages/fuse-client/syscalls/opendir.ts @@ -6,14 +6,14 @@ export const opendir: (backend: SQLiteBackend) => MountOptions["opendir"] = ( backend ) => { return async (path, flags, cb) => { - console.log("opendir(%s, %d)", path, flags); + console.info("opendir(%s, %d)", path, flags); if (path === "/") { cb(0, 42); // TODO: Universal FD for root dir, it should probably be in the database as bootstrap return; } - const r = await backend.getFile(path); + const r = await backend.getFileResolved(path); match(r) .with({ status: "ok" }, (r) => { cb(0, r.file.id); diff --git a/packages/fuse-client/syscalls/readlink.ts b/packages/fuse-client/syscalls/readlink.ts index fe32b72..706bcfb 100644 --- a/packages/fuse-client/syscalls/readlink.ts +++ b/packages/fuse-client/syscalls/readlink.ts @@ -6,11 +6,11 @@ export const readlink: (backend: SQLiteBackend) => MountOptions["readlink"] = ( backend ) => { return async (path, cb) => { - console.log("readlink(%s)", path); - const r = await backend.getFile(path); + console.info("readlink(%s)", path); + const r = await backend.getFileRaw(path); match(r) .with({ status: "ok" }, (r) => { - cb(0, r.file.name); + cb(0, r.file.targetPath); }) .with({ status: "not_found" }, () => { //@ts-expect-error fix types, what to do if readlink fails? diff --git a/packages/fuse-client/syscalls/symlink.ts b/packages/fuse-client/syscalls/symlink.ts index 7400cda..ba2ac55 100644 --- a/packages/fuse-client/syscalls/symlink.ts +++ b/packages/fuse-client/syscalls/symlink.ts @@ -6,14 +6,7 @@ export const symlink: (backend: SQLiteBackend) => MountOptions["symlink"] = ( backend ) => { return async (srcPath, destPath, cb) => { - console.log("symlink(%s, %s)", srcPath, destPath); - - const targetFile = await backend.getFile(srcPath); - console.log({ targetFile }); - if (targetFile.status === "not_found") { - cb(fuse.ENOENT); - return; - } + console.info("symlink(%s, %s)", srcPath, destPath); //@ts-expect-error fix types const context = fuse.context(); @@ -27,7 +20,7 @@ export const symlink: (backend: SQLiteBackend) => MountOptions["symlink"] = ( 33188, uid, gid, - targetFile.file.id + srcPath ); match(r) .with({ status: "ok" }, () => { diff --git a/packages/sqlite-backend/SQLiteBackend.ts b/packages/sqlite-backend/SQLiteBackend.ts index 02a1407..d01d388 100644 --- a/packages/sqlite-backend/SQLiteBackend.ts +++ b/packages/sqlite-backend/SQLiteBackend.ts @@ -12,7 +12,7 @@ export type ContentChunk = { size: number; }; -const WRITE_BUFFER_SIZE = 10; +const WRITE_BUFFER_SIZE = 1000; export class SQLiteBackend implements Backend { private readonly writeBuffers: Map> = @@ -60,39 +60,78 @@ export class SQLiteBackend implements Backend { async getFiles(dir: string) { const files = await this.prisma.file.findMany({ where: { - dir, + AND: [ + { + dir, + }, + { + path: { + not: "/", + }, + }, + ], }, }); return files; } - async getFile(filepath: string) { + async getFileRaw(filepath: string) { try { - const fileOrSymlink = await this.prisma.file.findFirstOrThrow({ + const file = await this.prisma.file.findFirstOrThrow({ where: { path: filepath, }, }); - const file = await match(fileOrSymlink.type === "symlink") - .with(true, async () => { - const targetFile = await this.prisma.file.findFirstOrThrow({ - where: { - id: fileOrSymlink.targetId, - }, - }); + return { + status: "ok" as const, + file: file, + }; + } catch (e) { + console.error(e); + return { + status: "not_found" as const, + }; + } + } + + async getFileResolved(filepath: string) { + try { + const fileOrSymlink = await this.getFileRaw(filepath); + if (fileOrSymlink.status === "not_found") { + return { + status: "not_found" as const, + }; + } + + const file = await match(fileOrSymlink.file.type) + .with("symlink", async () => { + const targetFile = await this.getFileRaw( + fileOrSymlink.file.targetPath + ); return { - ...targetFile, + // TODO: error handling + ...targetFile.file!, mode: constants.S_IFLNK, }; }) - .otherwise(() => fileOrSymlink); + .with("link", async () => { + const targetFile = await this.getFileRaw( + fileOrSymlink.file.targetPath + ); + return { + // TODO: error handling + ...targetFile.file!, + }; + }) + .otherwise(() => fileOrSymlink.file); return { status: "ok" as const, file: file, }; } catch (e) { + console.error(e); return { status: "not_found" as const, }; @@ -127,6 +166,7 @@ export class SQLiteBackend implements Backend { chunks, }; } catch (e) { + console.error(e); return { status: "not_found" as const, }; @@ -135,10 +175,12 @@ export class SQLiteBackend implements Backend { async getFileSize(filepath: string) { try { + const file = await this.getFileResolved(filepath); + // TODO: error handling const chunks = await this.prisma.content.findMany({ where: { file: { - path: filepath, + path: file.file?.path, }, }, }); @@ -148,6 +190,36 @@ export class SQLiteBackend implements Backend { size: Buffer.byteLength(bufChunk), }; } catch (e) { + console.error(e); + return { + status: "not_found" as const, + }; + } + } + + async getFileNLinks(filepath: string) { + try { + const file = await this.getFileResolved(filepath); + + // TODO: error handling + const nLinks = await this.prisma.file.findMany({ + where: { + OR: [ + { + path: file.file?.path, + }, + { + targetPath: file.file?.path, + }, + ], + }, + }); + return { + status: "ok" as const, + nLinks, + }; + } catch (e) { + console.error(e); return { status: "not_found" as const, }; @@ -160,7 +232,7 @@ export class SQLiteBackend implements Backend { mode = 16877, // dir (for default to be file, use 33188) uid: number, gid: number, - targetId: number = 0 + targetPath: string = "" ) { try { const parsedPath = path.parse(filepath); @@ -176,7 +248,7 @@ export class SQLiteBackend implements Backend { ctime: new Date(), uid, gid, - targetId, + targetPath, }, }); return { @@ -184,6 +256,7 @@ export class SQLiteBackend implements Backend { file: file, }; } catch (e) { + console.error(e); return { status: "not_found" as const, }; @@ -216,6 +289,7 @@ export class SQLiteBackend implements Backend { file: file, }; } catch (e) { + console.error(e); return { status: "not_found" as const, }; @@ -231,7 +305,7 @@ export class SQLiteBackend implements Backend { } try { - const rFile = await this.getFile(filepath); + const rFile = await this.getFileResolved(filepath); const file = rFile.file; /** @@ -270,6 +344,7 @@ export class SQLiteBackend implements Backend { chunks, }; } catch (e) { + console.error(e); return { status: "not_found" as const, }; @@ -293,6 +368,7 @@ export class SQLiteBackend implements Backend { file: file, }; } catch (e) { + console.error(e); return { status: "not_found" as const, }; @@ -301,17 +377,23 @@ export class SQLiteBackend implements Backend { async deleteFile(filepath: string) { try { - const file = await this.prisma.file.delete({ + const file = await this.prisma.file.deleteMany({ where: { - path: filepath, + OR: [ + { + path: filepath, + }, + { AND: [{ type: "link", targetPath: filepath }] }, + ], }, }); return { status: "ok" as const, - file: file, + file: file.count, }; } catch (e) { + console.error(e); return { status: "not_found" as const, }; @@ -370,6 +452,7 @@ export class SQLiteBackend implements Backend { file: file, }; } catch (e) { + console.error(e); return { status: "not_found" as const, }; @@ -392,6 +475,7 @@ export class SQLiteBackend implements Backend { file: file, }; } catch (e) { + console.error(e); return { status: "not_found" as const, }; diff --git a/packages/sqlite-backend/prisma/schema.prisma b/packages/sqlite-backend/prisma/schema.prisma index d6c03b2..2eda7c0 100644 --- a/packages/sqlite-backend/prisma/schema.prisma +++ b/packages/sqlite-backend/prisma/schema.prisma @@ -13,20 +13,20 @@ datasource db { } model File { - id Int @id @default(autoincrement()) - type String // dir or file or symlink - targetId Int @default(0) // only relevant for symlink otherwise 0 - mode Int - name String - dir String - path String @unique + id Int @id @default(autoincrement()) + type String // dir or file or symlink or link + targetPath String @default("") // only relevant for symlink otherwise empty + mode Int + name String + dir String + path String @unique // fileType String // text or binary - uid Int - gid Int - atime DateTime - mtime DateTime @updatedAt - ctime DateTime @default(now()) - Content Content[] + uid Int + gid Int + atime DateTime + mtime DateTime @updatedAt + ctime DateTime @default(now()) + Content Content[] } model Content { diff --git a/packages/zoid-fs-client/package.json b/packages/zoid-fs-client/package.json index 4d65706..6525036 100644 --- a/packages/zoid-fs-client/package.json +++ b/packages/zoid-fs-client/package.json @@ -4,7 +4,7 @@ "license": "MIT", "type": "module", "scripts": { - "start": "vite-node --watch index.ts /home/divyendusingh/zoid/vfs/1", + "start": "vite-node --watch index.ts /home/divyenduz/Documents/zoid/vfs/1", "test:prepare": "vite-node --watch index.ts /home/div/code/vfs/test-fs --tenant test", "ci:setup-fuse": "vite-node --watch index.ts", "test": "vitest",