diff --git a/.env.example b/.env.example index d7c2f47..545f578 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ BASE=/coli-rich/app/ LOGIN=http://localhost:3004 +ENRICHMENTS_PATH=./enrichments diff --git a/.gitignore b/.gitignore index 91987b0..9affdee 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ dist-ssr .env .docker/data + +enrichments/ diff --git a/README.md b/README.md index b42ae0d..86443a1 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ BASE=/ VITE_LOGIN_SERVER=http://localhost:3004 # Hardcoded list of allow user URIs that can perform enrichments in the backend VITE_ALLOWED_USERS=uri1,uri2 +# Local file path where submitted enrichments will be temporarily stored +ENRICHMENTS_PATH=./enrichments ``` ## To-Dos diff --git a/src/client/App.vue b/src/client/App.vue index 244dd10..34e3c7f 100644 --- a/src/client/App.vue +++ b/src/client/App.vue @@ -223,7 +223,7 @@ async function submitEnrichments(ppn, suggestions) { } // TODO: Improve error handling further. try { - const response = await fetch(baseUrl + "submit", options) + const response = await fetch(baseUrl + "enrichment", options) const data = await response.json() if (response.status === 201) { alert("Success") diff --git a/src/config.js b/src/config.js index be72c69..d45f800 100644 --- a/src/config.js +++ b/src/config.js @@ -15,6 +15,20 @@ if (login && !login.endsWith("/")) { login += "/" } +const enrichmentsPath = env.ENRICHMENTS_PATH || "./enrichments" + +// Create folder for enrichments path if necessary +import fs from "node:fs" +try { + if (!fs.existsSync(enrichmentsPath)) { + fs.mkdirSync(enrichmentsPath) + } +} catch (err) { + console.error(`Error when trying to access/create enrichtments path ${enrichmentsPath}:`, err) + console.error(`Make sure ${enrichmentsPath} is writable and restart the application.`) + process.exit(1) +} + export default { env: NODE_ENV, isProduction: NODE_ENV === "production", @@ -22,6 +36,7 @@ export default { port: parseInt(env.PORT) || 3454, login, allowedUsers: (env.VITE_ALLOWED_USERS || "").split(",").filter(Boolean).map(uri => uri.trim()), + enrichmentsPath, // methods log, warn: logger("warn"), diff --git a/src/server/enrichment-router.js b/src/server/enrichment-router.js new file mode 100644 index 0000000..11c40ac --- /dev/null +++ b/src/server/enrichment-router.js @@ -0,0 +1,66 @@ +import express from "express" +import path from "node:path" +import fs from "node:fs" +import { createHash } from "node:crypto" + +import * as auth from "./auth.js" +import * as errors from "./errors.js" +import config from "../config.js" + +const router = express.Router() +export default router + +function getBase(req) { + const url = new URL(`${req.protocol}://${req.get("host")}${req.originalUrl}`) + let base = `${url.origin}${url.pathname}` + if (!base.endsWith("/")) { + base += "/" + } + return base +} + +router.post("/", auth.main, (req, res) => { + // Create hash of PICA patch content as id + const id = createHash("sha1").update(req.body).digest("hex") + const uri = `${getBase(req)}${id}` + res.set("Location", uri) + try { + fs.writeFileSync(path.join(config.enrichmentsPath, id), req.body) + res.status(201).json({ + id, + uri, + ok: 1, + }) + } catch (error) { + config.error(error) + // ? Should we differentiate between errors? + throw new errors.BackendError() + } +}) + +router.get("/", (req, res) => { + let base = getBase(req), enrichments = "" + for (const id of fs.readdirSync(config.enrichmentsPath)) { + const stats = fs.lstatSync(path.join(config.enrichmentsPath, id)) + // Make sure it is a file + if (stats.isFile()) { + enrichments += `${base}${id} ${stats.birthtime.toISOString()}\n` + } + } + res.type("txt").send(enrichments) +}) + +router.get("/:id", (req, res) => { + const id = req.params.id + try { + const file = path.join(config.enrichmentsPath, id) + const created = fs.lstatSync(file).birthtime + const enrichment = fs.readFileSync(file, "utf-8") + res.set("Date", created) + res.type("txt") + res.send(enrichment) + } catch (error) { + // ? Should we differentiate between errors? + throw new errors.EntityNotFoundError(null, id) + } +}) diff --git a/src/server/errors.js b/src/server/errors.js index 9000714..30bf2d7 100644 --- a/src/server/errors.js +++ b/src/server/errors.js @@ -25,6 +25,32 @@ export class ForbiddenAccessError extends Error { } } +export class EntityNotFoundError extends Error { + constructor(message, id) { + const prefLabel = message ? { en: message } : { + en: `The requested entity ${id} could not be found.`, + de: `Die abgefragte Entität ${id} konnte nicht gefunden werden.`, + } + message = message || prefLabel.en + super(message) + this.statusCode = 404 + this.prefLabel = prefLabel + } +} + +export class BackendError extends Error { + constructor(message) { + const prefLabel = message ? { en: message } : { + en: "There was an unknown error with the backend.", + de: "Es gab einen unbekannten Backend-Fehler.", + } + message = message || prefLabel.en + super(message) + this.statusCode = 500 + this.prefLabel = prefLabel + } +} + export class NotImplementedError extends Error { constructor(message) { const prefLabel = message ? { en: message } : { diff --git a/src/server/server.js b/src/server/server.js index 7f5d499..ca442f7 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -2,9 +2,9 @@ import express from "express" import ViteExpress from "vite-express" import path from "node:path" -import * as auth from "./auth.js" import * as errors from "./errors.js" import config from "../config.js" +import enrichmentRouter from "./enrichment-router.js" import * as jskos from "jskos-tools" @@ -16,12 +16,9 @@ import bodyParser from "body-parser" app.use(bodyParser.urlencoded({ extended: true })) app.use(bodyParser.text()) -// TODO: Authenticated test endpoint, replace with actual enrichment endpoint -app.post(path.join(config.base, "/submit"), auth.main, (req) => { - // PICA data in req.body - config.log(req.body) - throw new errors.NotImplementedError() -}) +const enrichmentServerPath = path.join(config.base, "/enrichment") +app.use(enrichmentServerPath, enrichmentRouter) + // Error handling app.use((error, req, res, next) => {