Finilazed file rest api

This commit is contained in:
didier amyot committed 2023-06-13 09:49:20 -04:00
1 parent 8dcd8cc425
commit 872f841d74
4 files changed
+96 -38

No files matched your search

+58 -12
View File
@@ -2,23 +2,26 @@ import { Context, Router } from './deps.ts';
import { FileMetaData, FileService } from './services/mod.ts'; import { FileMetaData, FileService } from './services/mod.ts';
import { readerFromStreamReader } from './deps.ts'; import { readerFromStreamReader } from './deps.ts';
import { asyncIterableToArray } from './utils.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; type Handler = (ctx: Context) => void;
class FileDTO { class FileDTO {
readonly path: string; readonly path: string;
readonly id: string; readonly id: string;
readonly size: number; readonly contentSize: number;
readonly contentURL: string;
constructor(path: string, id: string, size: number){ constructor(path: string, id: string, size: number){
this.path = path; this.path = path;
this.id = id; this.id = id;
this.size = size; this.contentSize = size;
this.contentURL = `/v1/content/${id}/raw`;
} }
static fromFileMetaData(fm: FileMetaData): FileDTO{ 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 { function createFileHandlerFactory(fileService: FileService): Handler {
return async (ctx: Context) => { return async (ctx: Context) => {
const untypedCtx: any = ctx; const untypedCtx: any = ctx;
ctx.response.body = 'This is a post';
const path = untypedCtx.request.url.searchParams.get('path'); 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 reqBody = await ctx.request.body({ type: 'stream' }).value;
const reader = readerFromStreamReader(reqBody.getReader()); const reader = readerFromStreamReader(reqBody.getReader());
await fileService.createFromReader(path, reader); 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 { function getFileHandlerFactory(fileService: FileService): Handler {
return async (ctx: Context) => { return async (ctx: Context) => {
const untypedCtx: any = ctx; const untypedCtx: any = ctx;
const fileId = untypedCtx.params.path; const fileId = untypedCtx.params.fileId;
const reader = await fileService.getContentAsReader(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) { if (reader == null) {
ctx.response.status = 404; ctx.response.status = 404;
return; return;
@@ -58,8 +101,8 @@ function getFileHandlerFactory(fileService: FileService): Handler {
function deleteFileHandlerFactory(fileService: FileService): Handler { function deleteFileHandlerFactory(fileService: FileService): Handler {
return async (ctx: Context) => { return async (ctx: Context) => {
const untypedCtx: any = ctx; const untypedCtx: any = ctx;
const path = untypedCtx.params.path; const path = untypedCtx.params.fileId;
const isDeleted = await fileService.deleteByPath(path); const isDeleted = await fileService.deleteById(path);
if (!isDeleted) { if (!isDeleted) {
ctx.response.status = 404; ctx.response.status = 404;
return; return;
@@ -70,9 +113,12 @@ function deleteFileHandlerFactory(fileService: FileService): Handler {
export function buildAPI(router: Router, fileService: FileService): Router { export function buildAPI(router: Router, fileService: FileService): Router {
router.get('/files', listFileHandlerFactory(fileService)); router.get('/files', listFileHandlerFactory(fileService));
router.get('/files/:path', getFileHandlerFactory(fileService)); router.get('/files/:fileId', getFileHandlerFactory(fileService));
router.delete('/file/:path', deleteFileHandlerFactory(fileService)); router.delete('/files/:fileId', deleteFileHandlerFactory(fileService));
router.put('/files/:fileId', renameFileHandlerFactory(fileService));
router.post('/files', createFileHandlerFactory(fileService)); router.post('/files', createFileHandlerFactory(fileService));
router.get('/content/:fileId/raw', getDownloadFileHandlerFactory(fileService));
return router; return router;
} }
+31 -21
View File
@@ -151,10 +151,15 @@ export class DenoKVFileService implements FileService {
} }
} }
async deleteByPath(path: string): Promise<boolean> { async deleteById(id: string): Promise<boolean> {
const file = await this.kv.get<SerializedFile>(['file', id])
if(file.value == null){
return false;
}
const idx = await this.kv.get<SerializedFilePathIndex>([ const idx = await this.kv.get<SerializedFilePathIndex>([
'file_index_path', 'file_index_path',
path, file.value.path,
]); ]);
if (idx.value === null) { if (idx.value === null) {
return false; 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. // todo, this will fail silently if transaction fails. we might want to put that into a loop.
await this.kv.atomic() await this.kv.atomic()
.check(idx) .check(idx)
.delete(['file_index_path', path]) .check(file)
.delete(file.key)
.delete(idx.key) .delete(idx.key)
.commit(); .commit();
return true; return true;
@@ -184,11 +190,11 @@ export class DenoKVFileService implements FileService {
} }
async getRawContentByPath(path: string): Promise<Uint8Array | null> { async getRawContentByPath(path: string): Promise<Uint8Array | null> {
const ret = await this.getContentAsReader(path); const ret = await this.getContentByPath(path);
return ret === null ? null : readAll(ret); return ret === null ? null : readAll(ret);
} }
async getContentAsReader(path: string): Promise<Deno.Reader | null> { async getContentByPath(path: string): Promise<Deno.Reader | null> {
const idx = await this.kv.get<SerializedFilePathIndex>([ const idx = await this.kv.get<SerializedFilePathIndex>([
'file_index_path', 'file_index_path',
path, path,
@@ -196,11 +202,15 @@ export class DenoKVFileService implements FileService {
if (idx.value === null) { if (idx.value === null) {
return null; return null;
} }
const ser = await this.kv.get<SerializedFile>(['file', idx.value.fileId]); return this.getContentById(idx.value.fileId);
}
async getContentById(id: string): Promise<Deno.Reader | null>{
const ser = await this.kv.get<SerializedFile>(['file', id]);
if (ser.value === null) { if (ser.value === null) {
return 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<FileMetaData | null> { async getMetaDataByID(id: string): Promise<FileMetaData | null> {
@@ -215,6 +225,20 @@ export class DenoKVFileService implements FileService {
}; };
} }
async renameByPath(
originalPath: string,
newPath: string,
): Promise<FileMetaData | null> {
const idx = await this.kv.get<SerializedFilePathIndex>([
'file_index_path',
originalPath,
]);
if (idx.value == null) {
return null;
}
return this.renameById(idx.value.fileId, newPath);
}
async renameById(id: string, newPath: string): Promise<FileMetaData | null> { async renameById(id: string, newPath: string): Promise<FileMetaData | null> {
const fileObject = await this.kv.get<SerializedFile>(['file', id]); const fileObject = await this.kv.get<SerializedFile>(['file', id]);
if (fileObject.value === null) { if (fileObject.value === null) {
@@ -239,18 +263,4 @@ export class DenoKVFileService implements FileService {
.commit(); .commit();
return this.getMetaDataByID(id); return this.getMetaDataByID(id);
} }
async renameByPath(
originalPath: string,
newPath: string,
): Promise<FileMetaData | null> {
const idx = await this.kv.get<SerializedFilePathIndex>([
'file_index_path',
originalPath,
]);
if (idx.value == null) {
return null;
}
return this.renameById(idx.value.fileId, newPath);
}
} }
+4 -2
View File
@@ -16,7 +16,9 @@ export interface FileService {
getRawContentByPath(path: string): Promise<Uint8Array | null>; getRawContentByPath(path: string): Promise<Uint8Array | null>;
getContentAsReader(path: string): Promise<Deno.Reader | null>; getContentByPath(path: string): Promise<Deno.Reader | null>;
getContentById(id: string): Promise<Deno.Reader | null>;
getMetaDataByID(id: string): Promise<FileMetaData | null>; getMetaDataByID(id: string): Promise<FileMetaData | null>;
@@ -27,7 +29,7 @@ export interface FileService {
newPath: string, newPath: string,
): Promise<FileMetaData | null>; ): Promise<FileMetaData | null>;
deleteByPath(path: string): Promise<boolean>; deleteById(id: string): Promise<boolean>;
} }
export interface BlobService { export interface BlobService {
+3 -3
View File
@@ -83,8 +83,8 @@ Deno.test('DenoKVFileService happy path xxx ', async (t) => {
}); });
await t.step('Delete File', async () => { await t.step('Delete File', async () => {
await fs.deleteByPath(fileName1); await fs.deleteById(file1!.id);
await fs.deleteByPath('404 not found'); await fs.deleteById('404 not found');
}); });
await t.step('List after delete', async () => { 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 () => { await t.step('Rename file', async () => {
const newPath = 'boris/file2.txt'; const newPath = 'boris/file2.txt';
const ret = await fs.renameById(file2!.id, newPath); const ret = await fs.renameById(file2!.id, newPath);
assertEquals(ret?.path, newPath); assertEquals(ret!.path, newPath);
assertEquals( assertEquals(
new TextDecoder().decode((await fs.getRawContentByPath(newPath))!), new TextDecoder().decode((await fs.getRawContentByPath(newPath))!),