Persist resources on disk
This commit is contained in:
parent
7ba432faa9
commit
4eed204572
3
.gitignore
vendored
3
.gitignore
vendored
@ -104,5 +104,8 @@ dist
|
|||||||
# TernJS port file
|
# TernJS port file
|
||||||
.tern-port
|
.tern-port
|
||||||
|
|
||||||
|
# Default data directory
|
||||||
|
data
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
|
|||||||
10
src/index.ts
10
src/index.ts
@ -4,6 +4,7 @@ import http from "http";
|
|||||||
import { Server as SocketIO } from "socket.io";
|
import { Server as SocketIO } from "socket.io";
|
||||||
import { restHandler } from "./restHandler";
|
import { restHandler } from "./restHandler";
|
||||||
import bodyParser from "body-parser";
|
import bodyParser from "body-parser";
|
||||||
|
import { FileStorage } from "./repository";
|
||||||
|
|
||||||
type UserToFollow = {
|
type UserToFollow = {
|
||||||
socketId: string;
|
socketId: string;
|
||||||
@ -32,8 +33,13 @@ app.use(express.static("public"));
|
|||||||
|
|
||||||
app.use(bodyParser.raw({ limit: "10mb" }));
|
app.use(bodyParser.raw({ limit: "10mb" }));
|
||||||
|
|
||||||
app.use("/scene/*", restHandler("scene"));
|
const dataDir = process.env.DATA_DIR || "./data";
|
||||||
app.use("/file/*", restHandler("file"));
|
|
||||||
|
const scenes = new FileStorage(`${dataDir}/scenes`);
|
||||||
|
app.use("/scene/*", restHandler(scenes));
|
||||||
|
|
||||||
|
const files = new FileStorage(`${dataDir}/files`);
|
||||||
|
app.use("/file/*", restHandler(files));
|
||||||
|
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
res.send("Excalidraw collaboration server is up :)");
|
res.send("Excalidraw collaboration server is up :)");
|
||||||
|
|||||||
51
src/repository.ts
Normal file
51
src/repository.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import * as fs from "node:fs/promises";
|
||||||
|
import * as path from "node:path";
|
||||||
|
|
||||||
|
export interface Resource {
|
||||||
|
hash: string;
|
||||||
|
data: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Repository<T = Resource> {
|
||||||
|
get(id: string): Promise<T | null>;
|
||||||
|
put(id: string, resource: T): Promise<void>;
|
||||||
|
delete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileStorage implements Repository<Resource> {
|
||||||
|
|
||||||
|
constructor(public dataPath: string) { }
|
||||||
|
|
||||||
|
private resolvePaths(id: string) {
|
||||||
|
const baseFile = path.resolve(this.dataPath, "./" + id);
|
||||||
|
return {
|
||||||
|
dataFile: `${baseFile}.data`,
|
||||||
|
hashFile: `${baseFile}.hash`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get(id: string): Promise<Resource | null> {
|
||||||
|
const { dataFile, hashFile } = this.resolvePaths(id);
|
||||||
|
if (!existsSync(dataFile) || !existsSync(hashFile)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data: await fs.readFile(dataFile),
|
||||||
|
hash: (await fs.readFile(hashFile)).toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async put(id: string, resource: Resource): Promise<void> {
|
||||||
|
const { dataFile, hashFile } = this.resolvePaths(id);
|
||||||
|
await fs.mkdir(path.dirname(dataFile), { recursive: true });
|
||||||
|
await fs.writeFile(dataFile, new Uint8Array(resource.data));
|
||||||
|
await fs.writeFile(hashFile, resource.hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete(id: string): Promise<void> {
|
||||||
|
const { dataFile, hashFile } = this.resolvePaths(id);
|
||||||
|
await fs.rm(dataFile);
|
||||||
|
await fs.rm(hashFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,14 +1,11 @@
|
|||||||
import type { RequestHandler } from "express";
|
import type { Request, RequestHandler, Response } from "express";
|
||||||
|
import { Repository } from "./repository";
|
||||||
|
|
||||||
interface Resource {
|
type AsyncHandler = (req: Request, res: Response) => Promise<void>;
|
||||||
hash: string;
|
|
||||||
data: ArrayBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function restHandler(_resName: string): RequestHandler {
|
export function restHandler(repo: Repository): RequestHandler {
|
||||||
const resources: Record<string, Resource> = {};
|
|
||||||
|
|
||||||
const _put: RequestHandler = (req, res, next) => {
|
const _put: AsyncHandler = async (req, res) => {
|
||||||
const resId = req.params[0];
|
const resId = req.params[0];
|
||||||
const newData = req.body as ArrayBuffer;
|
const newData = req.body as ArrayBuffer;
|
||||||
const newHash = req.header("ETag");
|
const newHash = req.header("ETag");
|
||||||
@ -19,75 +16,72 @@ export function restHandler(_resName: string): RequestHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resources[resId] && resources[resId].hash != oldHash) {
|
const resource = await repo.get(resId);
|
||||||
|
if (resource && resource.hash != oldHash) {
|
||||||
res.status(409).send("Hash mismatch. Update denied.");
|
res.status(409).send("Hash mismatch. Update denied.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
resources[resId] = {
|
await repo.put(resId, {
|
||||||
hash: newHash,
|
hash: newHash,
|
||||||
data: newData,
|
data: newData,
|
||||||
};
|
});
|
||||||
|
|
||||||
res.status(200).send(`Resource with ID ${resId} has been upserted.`);
|
res.status(200).send(`Resource with ID ${resId} has been upserted.`);
|
||||||
next();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const _get: RequestHandler = (req, res, next) => {
|
const _get: AsyncHandler = async (req, res) => {
|
||||||
const resId = req.params[0];
|
const resId = req.params[0];
|
||||||
const oldHash = req.header("If-None-Match");
|
const oldHash = req.header("If-None-Match");
|
||||||
|
|
||||||
if (!resources[resId]) {
|
const resource = await repo.get(resId);
|
||||||
|
if (!resource) {
|
||||||
res.status(404).send("Not found.");
|
res.status(404).send("Not found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resources[resId] && resources[resId].hash == oldHash) {
|
if (resource.hash == oldHash) {
|
||||||
res.status(304).send("Not modified.");
|
res.status(304).send("Not modified.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Respect If-None-Match header
|
|
||||||
|
|
||||||
const resource = resources[resId];
|
|
||||||
res.header("ETag", resource.hash).header("Content-Type", "application/octet-stream");
|
res.header("ETag", resource.hash).header("Content-Type", "application/octet-stream");
|
||||||
res.status(200).send(resource.data);
|
res.status(200).send(resource.data);
|
||||||
next();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const _options: RequestHandler = (req, res, next) => {
|
const _options: AsyncHandler = async (_req, res) => {
|
||||||
const resId = req.params[0];
|
|
||||||
delete resources[resId];
|
|
||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
next();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const _delete: RequestHandler = (req, res, next) => {
|
const _delete: AsyncHandler = async (req, res) => {
|
||||||
const resId = req.params[0];
|
const resId = req.params[0];
|
||||||
delete resources[resId];
|
await repo.delete(resId);
|
||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
next();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (req, res, next) => {
|
return async (req, res, next) => {
|
||||||
res.header("Access-Control-Allow-Origin", process.env.CORS_ORIGIN || "*");
|
try {
|
||||||
res.header("Access-Control-Allow-Methods", "GET, PUT, DELETE, OPTIONS");
|
res.header("Access-Control-Allow-Origin", process.env.CORS_ORIGIN || "*");
|
||||||
res.header("Access-Control-Allow-Headers", "Content-Type, ETag, If-Match, If-None-Match");
|
res.header("Access-Control-Allow-Methods", "GET, PUT, DELETE, OPTIONS");
|
||||||
switch (req.method) {
|
res.header("Access-Control-Allow-Headers", "Content-Type, ETag, If-Match, If-None-Match");
|
||||||
case "GET":
|
switch (req.method) {
|
||||||
_get(req, res, next);
|
case "GET":
|
||||||
break;
|
await _get(req, res);
|
||||||
case "PUT":
|
break;
|
||||||
_put(req, res, next);
|
case "PUT":
|
||||||
break;
|
await _put(req, res);
|
||||||
case "OPTIONS":
|
break;
|
||||||
_options(req, res, next);
|
case "OPTIONS":
|
||||||
break;
|
await _options(req, res);
|
||||||
case "DELETE":
|
break;
|
||||||
_delete(req, res, next);
|
case "DELETE":
|
||||||
break;
|
await _delete(req, res);
|
||||||
default:
|
break;
|
||||||
next();
|
default:
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
next(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user