Finilazed file rest api
This commit is contained in:
1 parent
8dcd8cc425
commit
872f841d74
4 files changed
+96
-38
No files matched your search
+58
-12
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))!),
|
||||
|
||||
Reference in new issue
Block a user