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 { 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
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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))!),
|
||||||
|
|||||||
Reference in new issue
Block a user