diff --git a/src/restapi.ts b/src/restapi.ts index 3015190..de40742 100644 --- a/src/restapi.ts +++ b/src/restapi.ts @@ -2,23 +2,26 @@ import { Context, Router } from './deps.ts'; import { FileMetaData, FileService } from './services/mod.ts'; import { readerFromStreamReader } from './deps.ts'; import { asyncIterableToArray } from './utils.ts'; +import { createMockNext } from '../vendor/deno.land/x/oak@v12.1.0/testing.ts'; +import { Status } from "../vendor/deno.land/std@0.178.0/http/http_status.ts"; type Handler = (ctx: Context) => void; class FileDTO { readonly path: string; readonly id: string; - readonly size: number; + readonly contentSize: number; + readonly contentURL: string; constructor(path: string, id: string, size: number){ this.path = path; this.id = id; - this.size = size; + this.contentSize = size; + this.contentURL = `/v1/content/${id}/raw`; } static fromFileMetaData(fm: FileMetaData): FileDTO{ - return new FileDTO(); - + return new FileDTO(fm.path, fm.id, fm.contentSize); } } @@ -32,20 +35,60 @@ function listFileHandlerFactory(fileService: FileService): Handler { function createFileHandlerFactory(fileService: FileService): Handler { return async (ctx: Context) => { const untypedCtx: any = ctx; - ctx.response.body = 'This is a post'; const path = untypedCtx.request.url.searchParams.get('path'); - + if(!path){ + ctx.response.status = Status.BadRequest; + return; + } const reqBody = await ctx.request.body({ type: 'stream' }).value; const reader = readerFromStreamReader(reqBody.getReader()); await fileService.createFromReader(path, reader); + ctx.response.status = Status.Created; + }; +} + +function renameFileHandlerFactory(fileService: FileService): Handler { + return async (ctx: Context) => { + const untypedCtx: any = ctx; + const fileId: string = untypedCtx.params.fileId; + const reqBody = await ctx.request.body().value; + const newPath = reqBody.path; + console.log("> " + reqBody) + if(!reqBody.path){ + return; + } + console.log(">>> " + fileId); + console.log(">>> " + newPath); + const ret = await fileService.renameById(fileId, newPath); + if(ret === null){ + ctx.response.status = Status.NotFound; + return + } + + ctx.response.status = Status.OK; + ctx.response.body = FileDTO.fromFileMetaData(ret); }; } function getFileHandlerFactory(fileService: FileService): Handler { return async (ctx: Context) => { const untypedCtx: any = ctx; - const fileId = untypedCtx.params.path; - const reader = await fileService.getContentAsReader(fileId); + const fileId = untypedCtx.params.fileId; + const fm = await fileService.getMetaDataByID(fileId); + if (fm == null) { + ctx.response.status = 404; + return; + } + ctx.response.status = 200; + ctx.response.body = FileDTO.fromFileMetaData(fm); + }; +} + +function getDownloadFileHandlerFactory(fileService: FileService): Handler { + return async (ctx: Context) => { + const untypedCtx: any = ctx; + const fileId = untypedCtx.params.fileId; + const reader = await fileService.getContentById(fileId); if (reader == null) { ctx.response.status = 404; return; @@ -58,8 +101,8 @@ function getFileHandlerFactory(fileService: FileService): Handler { function deleteFileHandlerFactory(fileService: FileService): Handler { return async (ctx: Context) => { const untypedCtx: any = ctx; - const path = untypedCtx.params.path; - const isDeleted = await fileService.deleteByPath(path); + const path = untypedCtx.params.fileId; + const isDeleted = await fileService.deleteById(path); if (!isDeleted) { ctx.response.status = 404; return; @@ -70,9 +113,12 @@ function deleteFileHandlerFactory(fileService: FileService): Handler { export function buildAPI(router: Router, fileService: FileService): Router { router.get('/files', listFileHandlerFactory(fileService)); - router.get('/files/:path', getFileHandlerFactory(fileService)); - router.delete('/file/:path', deleteFileHandlerFactory(fileService)); + router.get('/files/:fileId', getFileHandlerFactory(fileService)); + router.delete('/files/:fileId', deleteFileHandlerFactory(fileService)); + router.put('/files/:fileId', renameFileHandlerFactory(fileService)); router.post('/files', createFileHandlerFactory(fileService)); + router.get('/content/:fileId/raw', getDownloadFileHandlerFactory(fileService)); + return router; } diff --git a/src/services/denokv.ts b/src/services/denokv.ts index 73db8ee..e9f0781 100644 --- a/src/services/denokv.ts +++ b/src/services/denokv.ts @@ -151,10 +151,15 @@ export class DenoKVFileService implements FileService { } } - async deleteByPath(path: string): Promise { + async deleteById(id: string): Promise { + const file = await this.kv.get(['file', id]) + if(file.value == null){ + return false; + } + const idx = await this.kv.get([ 'file_index_path', - path, + file.value.path, ]); if (idx.value === null) { return false; @@ -162,7 +167,8 @@ export class DenoKVFileService implements FileService { // todo, this will fail silently if transaction fails. we might want to put that into a loop. await this.kv.atomic() .check(idx) - .delete(['file_index_path', path]) + .check(file) + .delete(file.key) .delete(idx.key) .commit(); return true; @@ -184,11 +190,11 @@ export class DenoKVFileService implements FileService { } async getRawContentByPath(path: string): Promise { - const ret = await this.getContentAsReader(path); + const ret = await this.getContentByPath(path); return ret === null ? null : readAll(ret); } - async getContentAsReader(path: string): Promise { + async getContentByPath(path: string): Promise { const idx = await this.kv.get([ 'file_index_path', path, @@ -196,11 +202,15 @@ export class DenoKVFileService implements FileService { if (idx.value === null) { return null; } - const ser = await this.kv.get(['file', idx.value.fileId]); + return this.getContentById(idx.value.fileId); + } + + async getContentById(id: string): Promise{ + const ser = await this.kv.get(['file', id]); if (ser.value === null) { return null; } - return this.decompressContent(ser.value, this.blobService.getBlob(idx.value.fileId)); + return this.decompressContent(ser.value, this.blobService.getBlob(id)); } async getMetaDataByID(id: string): Promise { @@ -215,6 +225,20 @@ export class DenoKVFileService implements FileService { }; } + async renameByPath( + originalPath: string, + newPath: string, + ): Promise { + const idx = await this.kv.get([ + 'file_index_path', + originalPath, + ]); + if (idx.value == null) { + return null; + } + return this.renameById(idx.value.fileId, newPath); + } + async renameById(id: string, newPath: string): Promise { const fileObject = await this.kv.get(['file', id]); if (fileObject.value === null) { @@ -239,18 +263,4 @@ export class DenoKVFileService implements FileService { .commit(); return this.getMetaDataByID(id); } - - async renameByPath( - originalPath: string, - newPath: string, - ): Promise { - const idx = await this.kv.get([ - 'file_index_path', - originalPath, - ]); - if (idx.value == null) { - return null; - } - return this.renameById(idx.value.fileId, newPath); - } } diff --git a/src/services/interfaces.ts b/src/services/interfaces.ts index 6bd8cfd..fbd96a1 100644 --- a/src/services/interfaces.ts +++ b/src/services/interfaces.ts @@ -16,7 +16,9 @@ export interface FileService { getRawContentByPath(path: string): Promise; - getContentAsReader(path: string): Promise; + getContentByPath(path: string): Promise; + + getContentById(id: string): Promise; getMetaDataByID(id: string): Promise; @@ -27,7 +29,7 @@ export interface FileService { newPath: string, ): Promise; - deleteByPath(path: string): Promise; + deleteById(id: string): Promise; } export interface BlobService { diff --git a/tests/fileservice_test.ts b/tests/fileservice_test.ts index 19004fb..a029b9a 100644 --- a/tests/fileservice_test.ts +++ b/tests/fileservice_test.ts @@ -83,8 +83,8 @@ Deno.test('DenoKVFileService happy path xxx ', async (t) => { }); await t.step('Delete File', async () => { - await fs.deleteByPath(fileName1); - await fs.deleteByPath('404 not found'); + await fs.deleteById(file1!.id); + await fs.deleteById('404 not found'); }); await t.step('List after delete', async () => { @@ -106,7 +106,7 @@ Deno.test('DenoKVFileService happy path xxx ', async (t) => { await t.step('Rename file', async () => { const newPath = 'boris/file2.txt'; const ret = await fs.renameById(file2!.id, newPath); - assertEquals(ret?.path, newPath); + assertEquals(ret!.path, newPath); assertEquals( new TextDecoder().decode((await fs.getRawContentByPath(newPath))!),