diff --git a/packages/common/index.ts b/packages/common/index.ts index a5ca5b5..b8c15e2 100644 --- a/packages/common/index.ts +++ b/packages/common/index.ts @@ -1,12 +1,11 @@ -import { File } from "@prisma/client"; +import { File, Link } from "@prisma/client"; type FileType = "file" | "dir" | "symlink"; -type Result = - | { +type Result = + | ({ status: "ok"; - file: T; - } + } & { [P in K]: T }) | { status: "not_found"; }; @@ -14,9 +13,10 @@ type Result = // TODO: bump this based on the latest state of the actual backend! export interface Backend { - getFiles: (dir: string) => Promise; - getFileRaw: (filepath: string) => Promise>; - getFileResolved: (filepath: string) => Promise>; + // TODO: use resule + getLinks: (dir: string) => Promise; + getLink: (dir: string) => Promise>; + getFile: (filepath: string) => Promise>; createFile: ( filepath: string, @@ -27,13 +27,10 @@ export interface Backend { targetPath: string ) => Promise>; - writeFile: ( - filepath: string, - uid: number, - gid: number - ) => Promise>; - - deleteFile: (filepath: string) => Promise>; - renameFile: (srcPath: string, destPath: 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 aabac67..aa760fc 100644 --- a/packages/fuse-client/syscalls/getattr.ts +++ b/packages/fuse-client/syscalls/getattr.ts @@ -7,7 +7,7 @@ export const getattr: (backend: SQLiteBackend) => MountOptions["getattr"] = ( ) => { return async (path, cb) => { console.info("getattr(%s)", path); - const r = await backend.getFileResolved(path); + const r = await backend.getFile(path); await match(r) .with({ status: "ok" }, async (r) => { const rSize = await backend.getFileSize(path); diff --git a/packages/fuse-client/syscalls/init.ts b/packages/fuse-client/syscalls/init.ts index b941752..2c6d8e4 100644 --- a/packages/fuse-client/syscalls/init.ts +++ b/packages/fuse-client/syscalls/init.ts @@ -12,7 +12,7 @@ export const init: (backend: SQLiteBackend) => MountOptions["init"] = ( const context = fuse.context(); const { uid, gid } = context; - const rootFolder = await backend.getFileResolved("/"); + const rootFolder = await backend.getFile("/"); 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 4861ee7..6b75985 100644 --- a/packages/fuse-client/syscalls/link.ts +++ b/packages/fuse-client/syscalls/link.ts @@ -10,21 +10,9 @@ export const link: (backend: SQLiteBackend) => MountOptions["link"] = ( // TODO: throw if destination doesn't exist - //@ts-expect-error fix types - 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 }); + const r = await backend.createLink(srcPath, destPath); match(r) .with({ status: "ok" }, () => { cb(0); diff --git a/packages/fuse-client/syscalls/open.ts b/packages/fuse-client/syscalls/open.ts index a5eff57..d8965a1 100644 --- a/packages/fuse-client/syscalls/open.ts +++ b/packages/fuse-client/syscalls/open.ts @@ -7,7 +7,7 @@ export const open: (backend: SQLiteBackend) => MountOptions["open"] = ( ) => { return async (path, flags, cb) => { console.info("open(%s, %d)", path, flags); - const r = await backend.getFileResolved(path); + const r = await backend.getFile(path); match(r) .with({ status: "ok" }, (r) => { diff --git a/packages/fuse-client/syscalls/opendir.ts b/packages/fuse-client/syscalls/opendir.ts index 536bba3..367ad57 100644 --- a/packages/fuse-client/syscalls/opendir.ts +++ b/packages/fuse-client/syscalls/opendir.ts @@ -13,7 +13,7 @@ export const opendir: (backend: SQLiteBackend) => MountOptions["opendir"] = ( return; } - const r = await backend.getFileResolved(path); + const r = await backend.getFile(path); match(r) .with({ status: "ok" }, (r) => { cb(0, r.file.id); diff --git a/packages/fuse-client/syscalls/readdir.ts b/packages/fuse-client/syscalls/readdir.ts index f840c4c..42770e2 100644 --- a/packages/fuse-client/syscalls/readdir.ts +++ b/packages/fuse-client/syscalls/readdir.ts @@ -10,8 +10,8 @@ export const readdir: (backend: SQLiteBackend) => MountOptions["readdir"] = ( // TODO: figure out how are these directories in output of ls -la const dotDirs = [".", ".."]; - const files = await backend.getFiles(path); - const fileNames = dotDirs.concat(files.map((file) => file.name)); + const links = await backend.getLinks(path); + const fileNames = dotDirs.concat(links.map((link) => link.name)); return cb(0, fileNames); }; diff --git a/packages/fuse-client/syscalls/readlink.ts b/packages/fuse-client/syscalls/readlink.ts index 706bcfb..823ecc2 100644 --- a/packages/fuse-client/syscalls/readlink.ts +++ b/packages/fuse-client/syscalls/readlink.ts @@ -7,15 +7,22 @@ export const readlink: (backend: SQLiteBackend) => MountOptions["readlink"] = ( ) => { return async (path, cb) => { console.info("readlink(%s)", path); - const r = await backend.getFileRaw(path); - match(r) - .with({ status: "ok" }, (r) => { - cb(0, r.file.targetPath); - }) - .with({ status: "not_found" }, () => { - //@ts-expect-error fix types, what to do if readlink fails? - cb(fuse.ENOENT); - }) - .exhaustive(); + try { + const r = await backend.getLink(path); + match(r) + .with({ status: "ok" }, (r) => { + cb(0, r.link.targetPath); + }) + .with({ status: "not_found" }, () => { + //@ts-expect-error fix types, what to do if readlink fails? + cb(fuse.ENOENT); + }) + .exhaustive(); + } catch (e) { + console.error(e); + return { + status: "not_found" as const, + }; + } }; }; diff --git a/packages/fuse-client/syscalls/symlink.ts b/packages/fuse-client/syscalls/symlink.ts index ba2ac55..ab80bc1 100644 --- a/packages/fuse-client/syscalls/symlink.ts +++ b/packages/fuse-client/syscalls/symlink.ts @@ -1,6 +1,8 @@ import { SQLiteBackend } from "@zoid-fs/sqlite-backend"; import fuse, { MountOptions } from "@zoid-fs/node-fuse-bindings"; import { match } from "ts-pattern"; +import { constants } from "fs"; +import path from "path"; export const symlink: (backend: SQLiteBackend) => MountOptions["symlink"] = ( backend @@ -8,6 +10,19 @@ export const symlink: (backend: SQLiteBackend) => MountOptions["symlink"] = ( return async (srcPath, destPath, cb) => { console.info("symlink(%s, %s)", srcPath, destPath); + const parsedDestPath = path.parse(destPath); + // Note: actually 255 as per the spec but we get an extra / in the dest path + if (parsedDestPath.base.length > 255) { + cb(fuse.ENAMETOOLONG); + return; + } + + // Note: actually 1023 as per the spec but we get an extra / in the dest path + if (srcPath.length > 1023 || destPath.length > 1023) { + cb(fuse.ENAMETOOLONG); + return; + } + //@ts-expect-error fix types const context = fuse.context(); const { uid, gid } = context; @@ -17,7 +32,7 @@ export const symlink: (backend: SQLiteBackend) => MountOptions["symlink"] = ( const r = await backend.createFile( destPath, "symlink", - 33188, + constants.S_IFLNK, uid, gid, srcPath diff --git a/packages/sqlite-backend/SQLiteBackend.ts b/packages/sqlite-backend/SQLiteBackend.ts index d01d388..b434dba 100644 --- a/packages/sqlite-backend/SQLiteBackend.ts +++ b/packages/sqlite-backend/SQLiteBackend.ts @@ -57,8 +57,8 @@ export class SQLiteBackend implements Backend { await writeBuffer?.flush(); } - async getFiles(dir: string) { - const files = await this.prisma.file.findMany({ + async getLinks(dir: string) { + const files = await this.prisma.link.findMany({ where: { AND: [ { @@ -75,9 +75,9 @@ export class SQLiteBackend implements Backend { return files; } - async getFileRaw(filepath: string) { + async getLink(filepath: string) { try { - const file = await this.prisma.file.findFirstOrThrow({ + const link = await this.prisma.link.findFirstOrThrow({ where: { path: filepath, }, @@ -85,7 +85,7 @@ export class SQLiteBackend implements Backend { return { status: "ok" as const, - file: file, + link, }; } catch (e) { console.error(e); @@ -95,40 +95,22 @@ export class SQLiteBackend implements Backend { } } - async getFileResolved(filepath: string) { + async getFile(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 { - // TODO: error handling - ...targetFile.file!, - mode: constants.S_IFLNK, - }; - }) - .with("link", async () => { - const targetFile = await this.getFileRaw( - fileOrSymlink.file.targetPath - ); - return { - // TODO: error handling - ...targetFile.file!, - }; - }) - .otherwise(() => fileOrSymlink.file); + const link = await this.prisma.link.findFirstOrThrow({ + where: { + path: filepath, + }, + }); + const file = await this.prisma.file.findFirstOrThrow({ + where: { + id: link.fileId, + }, + }); return { status: "ok" as const, - file: file, + file, }; } catch (e) { console.error(e); @@ -175,13 +157,11 @@ export class SQLiteBackend implements Backend { async getFileSize(filepath: string) { try { - const file = await this.getFileResolved(filepath); + const file = await this.getFile(filepath); // TODO: error handling const chunks = await this.prisma.content.findMany({ where: { - file: { - path: file.file?.path, - }, + fileId: file.file?.id, }, }); const bufChunk = Buffer.concat(chunks.map((chunk) => chunk.content)); @@ -199,19 +179,12 @@ export class SQLiteBackend implements Backend { async getFileNLinks(filepath: string) { try { - const file = await this.getFileResolved(filepath); + const file = await this.getFile(filepath); // TODO: error handling - const nLinks = await this.prisma.file.findMany({ + const nLinks = await this.prisma.link.findMany({ where: { - OR: [ - { - path: file.file?.path, - }, - { - targetPath: file.file?.path, - }, - ], + fileId: file.file?.id, }, }); return { @@ -226,34 +199,25 @@ export class SQLiteBackend implements Backend { } } - async createFile( - filepath: string, - type = "file", - mode = 16877, // dir (for default to be file, use 33188) - uid: number, - gid: number, - targetPath: string = "" - ) { + async createLink(filepath: string, destinationPath: string) { try { - const parsedPath = path.parse(filepath); - const file = await this.prisma.file.create({ + // Note: destination is new link, filepath is the existing file + const file = await this.getFile(filepath); + const parsedPath = path.parse(destinationPath); + + const link = await this.prisma.link.create({ data: { name: parsedPath.base, dir: parsedPath.dir, - path: filepath, - type, - mode: type === "dir" ? 16877 : mode, - atime: new Date(), - mtime: new Date(), - ctime: new Date(), - uid, - gid, - targetPath, + path: destinationPath, + type: "file", // Note: hard link is a regular file + fileId: file.file!.id, }, }); + return { status: "ok" as const, - file: file, + file: link, }; } catch (e) { console.error(e); @@ -263,27 +227,43 @@ export class SQLiteBackend implements Backend { } } - async writeFile(filepath: string, uid: number, gid: number) { + async createFile( + filepath: string, + type = "file", + mode = 16877, // dir (for default to be file, use 33188) + uid: number, + gid: number, + targetPath: string = "" + ) { try { const parsedPath = path.parse(filepath); - const file = await this.prisma.file.upsert({ - where: { - path: filepath, - }, - update: {}, - create: { - type: "file", - name: parsedPath.base, - dir: parsedPath.dir, - path: filepath, - mode: 755, - atime: new Date(), - mtime: new Date(), - ctime: new Date(), - uid, - gid, - }, + + const { file, link } = await this.prisma.$transaction(async (tx) => { + const file = await tx.file.create({ + data: { + mode: type === "dir" ? 16877 : mode, + atime: new Date(), + mtime: new Date(), + ctime: new Date(), + uid, + gid, + }, + }); + + const link = await tx.link.create({ + data: { + name: parsedPath.base, + type, + dir: parsedPath.dir, + path: filepath, + targetPath, + fileId: file.id, + }, + }); + + return { file, link }; }); + return { status: "ok" as const, file: file, @@ -305,7 +285,7 @@ export class SQLiteBackend implements Backend { } try { - const rFile = await this.getFileResolved(filepath); + const rFile = await this.getFile(filepath); const file = rFile.file; /** @@ -353,11 +333,14 @@ export class SQLiteBackend implements Backend { async truncateFile(filepath: string, size: number) { try { + const link = await this.prisma.link.findFirstOrThrow({ + where: { + path: filepath, + }, + }); const file = await this.prisma.content.deleteMany({ where: { - file: { - path: filepath, - }, + fileId: link.fileId, offset: { gte: size, }, @@ -377,20 +360,20 @@ export class SQLiteBackend implements Backend { async deleteFile(filepath: string) { try { + const link = await this.prisma.link.findFirstOrThrow({ + where: { + path: filepath, + }, + }); const file = await this.prisma.file.deleteMany({ where: { - OR: [ - { - path: filepath, - }, - { AND: [{ type: "link", targetPath: filepath }] }, - ], + id: link.fileId, }, }); return { status: "ok" as const, - file: file.count, + count: file.count, }; } catch (e) { console.error(e); @@ -405,28 +388,42 @@ export class SQLiteBackend implements Backend { const parsedSrcPath = path.parse(srcPath); const parsedDestPath = path.parse(destPath); - // Note: Delete if the destiantion path already exists - await this.prisma.file.deleteMany({ - where: { - path: destPath, - }, - }); + const { updatedLink: link } = await this.prisma.$transaction( + async (tx) => { + // Note: Delete if the destiantion path already exists + const link = await tx.link.findFirstOrThrow({ + where: { + path: destPath, + }, + }); + + // Note: deleting file should delete link and content as cascade delete is enabled + const fileDeleteMany = await tx.file.deleteMany({ + where: { + id: link.id, + }, + }); + + const updatedLink = await tx.link.update({ + where: { + name: parsedSrcPath.base, + dir: parsedSrcPath.dir, + path: srcPath, + }, + data: { + name: parsedDestPath.base, + dir: parsedDestPath.dir, + path: destPath, + }, + }); + + return { updatedLink }; + } + ); - const file = await this.prisma.file.update({ - where: { - name: parsedSrcPath.base, - dir: parsedSrcPath.dir, - path: srcPath, - }, - data: { - name: parsedDestPath.base, - dir: parsedDestPath.dir, - path: destPath, - }, - }); return { status: "ok" as const, - file: file, + link, }; } catch (e) { console.error(e); @@ -439,14 +436,23 @@ export class SQLiteBackend implements Backend { async updateMode(filepath: string, mode: number) { try { - const file = await this.prisma.file.update({ - where: { - path: filepath, - }, - data: { - mode, - }, + const { file, link } = await this.prisma.$transaction(async (tx) => { + const link = await tx.link.findFirstOrThrow({ + where: { + path: filepath, + }, + }); + const file = await tx.file.update({ + where: { + id: link.id, + }, + data: { + mode, + }, + }); + return { file, link }; }); + return { status: "ok" as const, file: file, @@ -461,15 +467,24 @@ export class SQLiteBackend implements Backend { async updateTimes(filepath: string, atime: number, mtime: number) { try { - const file = await this.prisma.file.update({ - where: { - path: filepath, - }, - data: { - atime: new Date(atime), - mtime: new Date(mtime), - }, + const { file, link } = await this.prisma.$transaction(async (tx) => { + const link = await tx.link.findFirstOrThrow({ + where: { + path: filepath, + }, + }); + const file = await tx.file.update({ + where: { + id: link.fileId, + }, + data: { + atime: new Date(atime), + mtime: new Date(mtime), + }, + }); + return { file, link }; }); + return { status: "ok" as const, file: file, diff --git a/packages/sqlite-backend/prisma/schema.prisma b/packages/sqlite-backend/prisma/schema.prisma index 2eda7c0..b064968 100644 --- a/packages/sqlite-backend/prisma/schema.prisma +++ b/packages/sqlite-backend/prisma/schema.prisma @@ -13,20 +13,27 @@ datasource db { } model File { - 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 + id Int @id @default(autoincrement()) + mode Int + // fileType String // text or binary + uid Int + gid Int + atime DateTime + mtime DateTime @updatedAt + ctime DateTime @default(now()) + Content Content[] + Link Link[] +} + +model Link { + id Int @id @default(autoincrement()) name String + type String // dir or file or symlink 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[] + path String @unique + targetPath String @default("") // Only relevant for symlink, target path + file File @relation(fields: [fileId], references: [id], onDelete: Cascade) + fileId Int } model Content {