-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ac98eaa
commit 306c1ff
Showing
18 changed files
with
489 additions
and
127 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { downloadOwnModelFile } from "@/features/models"; | ||
import mime from "mime"; | ||
import { Readable } from "stream"; | ||
|
||
export async function GET( | ||
req: Request, | ||
{ params }: { params: { model: string; file: string } } | ||
) { | ||
const fileStream: ReadableStream = Readable.toWeb( | ||
await downloadOwnModelFile(parseInt(params.model), params.file) | ||
) as ReadableStream; | ||
|
||
return new Response(fileStream, { | ||
headers: { | ||
"Content-Type": mime.getType(params.model) ?? "application/octet-stream", | ||
}, | ||
}); | ||
} |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,30 @@ | ||
import { saveModel } from "@/features/models"; | ||
import { Readable } from "node:stream"; | ||
import { ReadableStream } from "stream/web"; | ||
import { createOwnModel } from "@/features/models"; | ||
import { z } from "zod"; | ||
import { zfd } from "zod-form-data"; | ||
|
||
const postSchema = zfd.formData({ | ||
file: zfd.file(), | ||
file: zfd.repeatableOfType(zfd.file()), | ||
name: zfd.text(), | ||
coordinateSystem: zfd.text().optional(), | ||
}); | ||
|
||
export async function POST(req: Request) { | ||
const data = postSchema.parse(await req.formData()); | ||
try { | ||
const data = postSchema.parse(await req.formData()); | ||
|
||
const fileStream = Readable.fromWeb(data.file.stream() as ReadableStream); | ||
const model = await createOwnModel( | ||
{ | ||
name: data.name, | ||
coordinateSystem: data.coordinateSystem, | ||
}, | ||
data.file | ||
); | ||
|
||
await saveModel(fileStream); | ||
return Response.json(model, { status: 201 }); | ||
} catch (e) { | ||
if (e instanceof z.ZodError) { | ||
return new Response(e.message, { status: 400 }); | ||
} | ||
throw e; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { | ||
Column, | ||
CreateDateColumn, | ||
Entity, | ||
ManyToOne, | ||
PrimaryGeneratedColumn, | ||
UpdateDateColumn, | ||
} from "typeorm"; | ||
import { User } from "./user"; | ||
|
||
@Entity("models") | ||
export class Model { | ||
@PrimaryGeneratedColumn() id!: number; | ||
|
||
@Column() name!: string; | ||
@Column("varchar", { nullable: true }) coordinateSystem?: string | null; | ||
|
||
@ManyToOne(() => User, (user) => user.models, { | ||
nullable: false, | ||
onDelete: "RESTRICT", | ||
onUpdate: "CASCADE", | ||
}) | ||
user?: User; | ||
|
||
@CreateDateColumn() createdAt!: Date; | ||
@UpdateDateColumn() updatedAt!: Date; | ||
|
||
// get from file system | ||
files?: string[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,40 @@ | ||
import { Readable } from "stream"; | ||
import { describe, expect, test } from "vitest"; | ||
import { | ||
checkFileExists, | ||
deleteFile, | ||
ensureDirectory, | ||
readFile, | ||
saveFileStream, | ||
} from "../storage"; | ||
import { createOwnModel, deleteOwnModel, getOwnModel } from "."; | ||
import { Model } from "../db/entities/model"; | ||
|
||
describe("model actions", () => { | ||
const model = new Readable(); | ||
model.push("test"); | ||
model.push(null); | ||
const modelFile = { | ||
stream: () => | ||
new ReadableStream({ | ||
start(controller) { | ||
controller.enqueue(new TextEncoder().encode("test")); | ||
controller.close(); | ||
}, | ||
}), | ||
name: "test.txt", | ||
} as File; | ||
|
||
test("saveModel", async () => { | ||
// Save the model file | ||
await ensureDirectory("models"); | ||
await saveFileStream("model", "models", model); | ||
const modelMetadata: Pick<Model, "name" | "coordinateSystem"> = { | ||
name: "My best model", | ||
coordinateSystem: "WGS84", | ||
}; | ||
|
||
// Check if the file exists | ||
const data = await readFile("model", "models"); | ||
expect(data.toString()).toBe("test"); | ||
}); | ||
test("model CRD", async () => { | ||
let model: Model | null; | ||
|
||
// CREATE | ||
model = await createOwnModel(modelMetadata, [modelFile]); | ||
expect(model).toMatchObject(modelMetadata); | ||
|
||
test("deleteModel", async () => { | ||
// Delete the model file | ||
await deleteFile("model", "models"); | ||
// READ | ||
model = await getOwnModel(model.id); | ||
expect(model).toMatchObject(modelMetadata); | ||
expect(model?.files).toHaveLength(1); | ||
expect(model?.files?.[0]).toBe(modelFile.name); | ||
|
||
// Check if the file exists | ||
const exists = await checkFileExists("model", "models"); | ||
expect(exists).toBe(false); | ||
// DELETE | ||
await deleteOwnModel(model!.id); | ||
model = await getOwnModel(model!.id); | ||
expect(model).toBe(null); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,124 @@ | ||
import { Readable } from "stream"; | ||
import { canCreateModel } from "../auth/acl"; | ||
import { saveFileStream } from "../storage"; | ||
import { ReadableStream } from "stream/web"; | ||
import { canCreateModel, canReadOwnModels } from "../auth/acl"; | ||
import { getUserToken } from "../auth/user"; | ||
import { Model } from "../db/entities/model"; | ||
import { injectRepository } from "../db/helpers"; | ||
import { | ||
deleteFile, | ||
ensureDirectory, | ||
getUserModelDirectory, | ||
listFilesInDirectory, | ||
readFileStream, | ||
saveFileStream, | ||
} from "../storage"; | ||
|
||
export async function saveModel(model: Readable) { | ||
export async function createOwnModel( | ||
metadata: Partial<Pick<Model, "name" | "coordinateSystem">>, | ||
files: File[] | ||
) { | ||
if (!(await canCreateModel())) throw new Error("Unauthorized"); | ||
|
||
await saveFileStream("model", "models", model); | ||
const user = (await getUserToken())!; | ||
|
||
const modelRepository = await injectRepository(Model); | ||
|
||
const model = await modelRepository.save({ | ||
...metadata, | ||
user: { id: user.id }, | ||
}); | ||
|
||
try { | ||
// save files | ||
const dir = getUserModelDirectory(user.id, model.id); | ||
|
||
await ensureDirectory(dir); | ||
|
||
for (const file of files) { | ||
const fileStream = Readable.fromWeb(file.stream() as ReadableStream); | ||
await saveFileStream(file.name, dir, fileStream); | ||
} | ||
} catch (e) { | ||
await modelRepository.remove(model); | ||
throw e; | ||
} | ||
|
||
return { | ||
...model, | ||
files: files.map((f) => f.name), | ||
}; | ||
} | ||
|
||
export async function downloadOwnModelFile(modelId: number, fileName: string) { | ||
if (!(await canReadOwnModels())) throw new Error("Unauthorized"); | ||
|
||
const user = (await getUserToken())!; | ||
|
||
const modelRepository = await injectRepository(Model); | ||
|
||
const model = await modelRepository.findOne({ | ||
where: { id: modelId, user: { id: user.id } }, | ||
}); | ||
if (!model) throw new Error("Not found"); | ||
|
||
const dir = getUserModelDirectory(user.id, model.id); | ||
|
||
return await readFileStream(fileName, dir); | ||
} | ||
|
||
export async function deleteOwnModel(modelId: number) { | ||
if (!(await canReadOwnModels())) throw new Error("Unauthorized"); | ||
|
||
const user = (await getUserToken())!; | ||
|
||
const modelRepository = await injectRepository(Model); | ||
|
||
const model = await modelRepository.findOne({ | ||
where: { id: modelId, user: { id: user.id } }, | ||
}); | ||
if (!model) throw new Error("Not found"); | ||
|
||
// delete files | ||
const dir = getUserModelDirectory(user.id, model.id); | ||
|
||
const files = await listFilesInDirectory(dir); | ||
for (const file of files) { | ||
await deleteFile(file, dir); | ||
} | ||
|
||
// delete model | ||
await modelRepository.remove(model); | ||
} | ||
|
||
export async function listOwnModels() { | ||
if (!(await canReadOwnModels())) throw new Error("Unauthorized"); | ||
|
||
const user = (await getUserToken())!; | ||
|
||
const modelRepository = await injectRepository(Model); | ||
|
||
return await modelRepository.find({ | ||
where: { user: { id: user.id } }, | ||
}); | ||
} | ||
|
||
export async function getOwnModel(modelId: number) { | ||
if (!(await canReadOwnModels())) throw new Error("Unauthorized"); | ||
|
||
const user = (await getUserToken())!; | ||
|
||
const modelRepository = await injectRepository(Model); | ||
|
||
const model = await modelRepository.findOne({ | ||
where: { id: modelId, user: { id: user.id } }, | ||
}); | ||
if (!model) return null; | ||
|
||
const dir = getUserModelDirectory(user.id, model.id); | ||
const files = await listFilesInDirectory(dir); | ||
|
||
return { | ||
...model, | ||
files, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.