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 { 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;
}
+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>([
'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<Uint8Array | null> {
const ret = await this.getContentAsReader(path);
const ret = await this.getContentByPath(path);
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>([
'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<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) {
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> {
@@ -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> {
const fileObject = await this.kv.get<SerializedFile>(['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<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>;
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>;
@@ -27,7 +29,7 @@ export interface FileService {
newPath: string,
): Promise<FileMetaData | null>;
deleteByPath(path: string): Promise<boolean>;
deleteById(id: string): Promise<boolean>;
}
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 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))!),