diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..39c4d3a --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,20 @@ +FROM denoland/deno:1.26.2 + +EXPOSE 8000 + +RUN mkdir -p /home/deno +RUN chown -R deno /home/deno +RUN mkdir -p /usr/src/app/src +WORKDIR /usr/src/app + +RUN apt update \ + && apt -y install pdfsandwich tesseract-ocr-deu tesseract-ocr-fra curl git zip unzip +RUN rm /etc/ImageMagick-6/policy.xml + +USER deno +COPY src/deps.ts src/deps.ts +RUN deno cache src/deps.ts + +COPY . . + +CMD [ "/bin/bash", "/usr/src/app/.devcontainer/docker-cmd.sh" ] \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..03ce50f --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,47 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/docker-existing-docker-compose +// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml. +{ + "name": "Existing Docker Compose (Extend)", + + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": [ + "../dev-docker-compose.yml", + "docker-compose.yml" + ], + + "containerEnv": { + "TRIDOC_PWD": "pw123", + }, + + // The 'service' property is the name of the service for the container that VS Code should + // use. Update this value and .devcontainer/docker-compose.yml to the real service name. + "service": "tridoc", + + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/usr/src/app", + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment the next line if you want start specific services in your Docker Compose config. + "runServices": [ "fuseki" ], + + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + // "shutdownAction": "none", + + // Uncomment the next line to run commands after the container is created - for example installing curl. + // "postCreateCommand": "apt-get update && apt-get install -y curl", + + // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "deno", + "customizations": { + "vscode": { + "extensions": [ + "denoland.vscode-deno" + ] + } + } +} diff --git a/.devcontainer/docker-cmd.sh b/.devcontainer/docker-cmd.sh new file mode 100644 index 0000000..6791b5b --- /dev/null +++ b/.devcontainer/docker-cmd.sh @@ -0,0 +1,11 @@ +#!/bin/bash +echo 'Attempting to create Dataset "3DOC"' +curl 'http://fuseki:3030/$/datasets' -H "Authorization: Basic $(echo -n admin:pw123 | base64)" \ + -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' --data 'dbName=3DOC&dbType=tdb' +set -m +deno run --watch --allow-net --allow-read=blobs,rdf.ttl --allow-write=blobs,rdf.ttl --allow-run --allow-env=TRIDOC_PWD,OCR_LANG src/main.ts & +sleep 5 +echo 'Attempting to create Dataset "3DOC"' +curl 'http://fuseki:3030/$/datasets' -H "Authorization: Basic $(echo -n admin:pw123 | base64)" \ + -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' --data 'dbName=3DOC&dbType=tdb' +fg 1 diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..3b042ba --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3' +services: + # Update this to the name of the service you want to work with in your docker-compose.yml file + tridoc: + # If you want add a non-root user to your Dockerfile, you can use the "remoteUser" + # property in devcontainer.json to cause VS Code its sub-processes (terminals, tasks, + # debugging) to execute as the user. Uncomment the next line if you want the entire + # container to run as this user instead. Note that, on Linux, you may need to + # ensure the UID and GID of the container user you create matches your local user. + # See https://aka.ms/vscode-remote/containers/non-root for details. + # + user: deno + + # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer + # folder. Note that the path of the Dockerfile and context is relative to the *primary* + # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile" + # array). The sample below assumes your primary file is in the root of your project. + # + build: + context: . + dockerfile: .devcontainer/Dockerfile + + volumes: + # Update this to wherever you want VS Code to mount the folder of your project + - .:/usr/src/app:cached + + # Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details. + # - /var/run/docker.sock:/var/run/docker.sock + + # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. + # cap_add: + # - SYS_PTRACE + # security_opt: + # - seccomp:unconfined + + # Overrides default command so things don't shut down after the process ends. + # command: "/bin/bash -c \"TRIDOC_PWD=\\\"pw123\\\" deno run --allow-net --allow-read=blobs --allow-write=blobs --allow-run=convert,pdfsandwich --allow-env=TRIDOC_PWD,OCR_LANG src/main.ts &\\\n sleep 5\\\n echo 'Attempting to create Dataset \\\"3DOC\\\"'\\\n curl 'http://fuseki:3030/$/datasets' -H \\\"Authorization: Basic $(echo -n admin:pw123 | base64)\\\" -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' --data 'dbName=3DOC&dbType=tdb'\\\n fg 1\\\n /bin/sh -c \\\"while sleep 1000; do :; done\\\"\"" diff --git a/.dockerignore b/.dockerignore index f145ba1..e911418 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ +old blobs fuseki-base node_modules \ No newline at end of file diff --git a/.gitignore b/.gitignore index c797f99..ccf4377 100644 --- a/.gitignore +++ b/.gitignore @@ -1,65 +1,9 @@ -# Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# next.js build output -.next - +node_modules blobs - fuseki-base \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 01e694c..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "program": "${workspaceFolder}/lib/server", - "env": {"TRIDOC_PWD": "tridoc"} - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 3d41712..1535e13 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,5 @@ { - "npm.packageManager": "yarn" + "deno.enable": true, + "deno.lint": true, + "deno.unstable": true } \ No newline at end of file diff --git a/3doc.config.js b/3doc.config.js deleted file mode 100644 index e69de29..0000000 diff --git a/DEV-README.md b/DEV-README.md index 8ddc74e..cc6a6a2 100644 --- a/DEV-README.md +++ b/DEV-README.md @@ -1,65 +1,20 @@ # tridoc -## Table Of Contents - * [Easy Setup with Docker-Compose](#easy-setup-with-docker-compose) - * [Dev Build](#dev-build) - * [Production Build](#production-build) - * [Setup with Persistent Fuseki](#setup-with-persistent-fuseki) - * [Docker](#docker) - * [Manual](#manual) +## Run "live" -## Developer Guide +Use the vscode-devcontainer: this will start tridoc and fuseki. -This assumes a Unix/Linux/wsl system with bash +It will use TRIDOC_PWD = "pw123". +Access tridoc from http://localhost:8000 and fuseki from http://localhost:8001 -### Easy Setup with Docker-Compose +You might need to `chown deno:deno` blobs/ and fuseki-base (attach bash to docker as root from outside) -This will setup tridoc on port 8000 and fuseki avaliable on port 8001. +Watch the logs from outside of vscode with -Replace `YOUR PASSWORD HERE` in the first command with your choice of password. - -#### Dev Build: - -``` -export TRIDOC_PWD="YOUR PASSWORD HERE" -docker-compose -f dev-docker-compose.yml build -docker-compose -f dev-docker-compose.yml up -``` - -#### Production Build: - -``` -export TRIDOC_PWD="YOUR PASSWORD HERE" -docker-compose build -docker-compose up -``` - -### Setup with Persistent Fuseki - -The following method expect an instance of Fuseki running on http://fuseki:3030/ with user `admin` and password `pw123`. This fuseki instance must have lucene indexing enabled and configured as in [config-tdb.ttl](config-tdb.ttl). - -#### Docker: - -``` -docker build -t tridoc . -docker run -p 8000:8000 -e TRIDOC_PWD="YOUR PASSWORD HERE" tridoc -``` - -#### Manual: - -Install the following dependencies: - -``` -node:12.18 yarn pdfsandwich tesseract-ocr-deu tesseract-ocr-fra +```sh +docker logs -f tridoc-backend_tridoc_1 ``` -And run the following commands - -``` -rm /etc/ImageMagick-6/policy.xml -yarn install -bash docker-cmd.sh -``` ## Tips & Tricks diff --git a/Dockerfile b/Dockerfile index 3fbabeb..680ee6a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,18 @@ -FROM node:lts-buster +FROM denoland/deno:1.26.2 + EXPOSE 8000 + +RUN mkdir -p /usr/src/app/src +WORKDIR /usr/src/app + RUN apt update \ - && apt -y install pdfsandwich tesseract-ocr-deu tesseract-ocr-fra + && apt -y install pdfsandwich tesseract-ocr-deu tesseract-ocr-fra curl zip unzip RUN rm /etc/ImageMagick-6/policy.xml -RUN mkdir -p /usr/src/app -WORKDIR /usr/src/app -COPY . /usr/src/app -RUN yarn install -RUN chmod +x /usr/src/app/docker-cmd.sh -CMD [ "/usr/src/app/docker-cmd.sh" ] \ No newline at end of file + +USER deno +COPY src/deps.ts src/deps.ts +RUN deno cache src/deps.ts + +COPY . . + +CMD [ "/bin/bash", "/usr/src/app/docker-cmd.sh" ] \ No newline at end of file diff --git a/LICENSE b/LICENSE index e2b8549..2ec5ef9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Reto Gmür +Copyright (c) 2022 Noam Bachmann & Reto Gmür Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0965e93..58948b9 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ When getting a comment, a JSON array with objects of the following structure is ## API | Address | Method | Description | Request / Payload | Response | Implemented in Version | -| - | - | - | - | - | - | +| - | - | - | - | - | - | - | | `/count` | GET | Count (matching) documents | [1](#f1) [3](#f3) | Number | 1.1.0 | | `/doc` | POST | Add / Store Document | PDF[5](#f5) | - | 1.1.0 | | `/doc` | GET | Get List of all (matching) documents | [1](#f1) [2](#f2) [3](#f3) | Array of objects with document identifiers and titles (where available) | 1.1.0 | @@ -123,6 +123,7 @@ When getting a comment, a JSON array with objects of the following structure is | `/doc/{id}/title` | DELETE | Reset document title | - | - | 1.1.0 | | `/doc/{id}/meta` | GET | Get various metadata | - | `{"title": "the_Title", "tags":[...], "comments": [...] ... }` | 1.1.0 \| .comments & .created in 1.2.1 | | `/raw/rdf` | GET | Get all metadata as RDF. Useful for Backups | [4](#f4) | RDF, Content-Type defined over request Headers or ?accept. Fallback to text/turtle. | 1.1.0 | +| `/raw/rdf` | DELETE | "Cancel" failed zip upload—use only if certain it’s done & failed | | | (deno only) | | `/raw/zip` or `/raw/tgz` | GET | Get all data. Useful for backups | - | ZIP / TGZ containing blobs/ directory with all pdfs as stored within tridoc and a rdf.ttl file with all metadata. | 1.3.0 | | `/raw/zip` | PUT | Replace all data with backup zip | ZIP | Replaces the metadata and adds the blobs from the zip | 1.3.0 | | `/tag` | POST | Create new tag | See above | - | 1.1.0 | diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 0000000..03da2df --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,12 @@ +{ + "fmt": { + "files": { + "include": ["src/"] + } + }, + "tasks": { + // --allow-run=convert,pdfsandwich,pdftotext,tar,zip,unzip,bash + "run": "deno run --allow-net --allow-read=blobs,rdf.ttl --allow-write=blobs,rdf.ttls --allow-run --allow-env=TRIDOC_PWD,OCR_LANG src/main.ts", + "run-watch": "deno run --watch --allow-net --allow-read=blobs,rdf.ttl --allow-write=blobs,rdf.ttl --allow-run --allow-env=TRIDOC_PWD,OCR_LANG src/main.ts" + } +} diff --git a/docker-cmd.sh b/docker-cmd.sh index 56e8c9d..c707f9c 100644 --- a/docker-cmd.sh +++ b/docker-cmd.sh @@ -1,10 +1,9 @@ #!/bin/bash -sleep 5 echo 'Attempting to create Dataset "3DOC"' curl 'http://fuseki:3030/$/datasets' -H "Authorization: Basic $(echo -n admin:pw123 | base64)" \ -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' --data 'dbName=3DOC&dbType=tdb' set -m -yarn start & +deno run --allow-net --allow-read=blobs,rdf.ttl --allow-write=blobs,rdf.ttl --allow-run --allow-env=TRIDOC_PWD,OCR_LANG src/main.ts & sleep 5 echo 'Attempting to create Dataset "3DOC"' curl 'http://fuseki:3030/$/datasets' -H "Authorization: Basic $(echo -n admin:pw123 | base64)" \ diff --git a/find-draft.txt b/find-draft.txt deleted file mode 100644 index df2b38d..0000000 --- a/find-draft.txt +++ /dev/null @@ -1,2 +0,0 @@ -Encode where necessary: GET /doc?tag=pay&tag=prority(>3)&tag=not(personal)&search=helsinki - /doc?tag=pay&tag=prority(>3)¬tag=personal&text=helsinki diff --git a/lib/datastore.js b/old/lib/datastore.js similarity index 100% rename from lib/datastore.js rename to old/lib/datastore.js diff --git a/lib/metadeleter.js b/old/lib/metadeleter.js similarity index 100% rename from lib/metadeleter.js rename to old/lib/metadeleter.js diff --git a/lib/metafinder.js b/old/lib/metafinder.js similarity index 100% rename from lib/metafinder.js rename to old/lib/metafinder.js diff --git a/lib/metastorer.js b/old/lib/metastorer.js similarity index 100% rename from lib/metastorer.js rename to old/lib/metastorer.js diff --git a/lib/pdfprocessor.js b/old/lib/pdfprocessor.js similarity index 100% rename from lib/pdfprocessor.js rename to old/lib/pdfprocessor.js diff --git a/lib/server.js b/old/lib/server.js similarity index 100% rename from lib/server.js rename to old/lib/server.js diff --git a/tdt.fish b/old/tdt.fish similarity index 100% rename from tdt.fish rename to old/tdt.fish diff --git a/package.json b/package.json deleted file mode 100644 index 5a603ea..0000000 --- a/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "tridoc-backend", - "version": "1.5.2", - "description": "Simple RDF-Based Document Management System", - "main": "lib/server", - "repository": "git@github.com:tridoc/tridoc-backend.git", - "author": "Noam Bachmann ", - "license": "MIT", - "dependencies": { - "adm-zip": "^0.4.16", - "archiver": "^3.1.1", - "hapi": "^17.5.2", - "hapi-auth-basic": "^5.0.0", - "nanoid": "^1.1.0", - "node-fetch": "^2.2.0", - "pdfjs-dist": "^2.0.489" - }, - "scripts": { - "start": "node lib/server.js", - "start-with-pwd": "TRIDOC_PWD='tridoc' node lib/server.js" - } -} diff --git a/src/deps.ts b/src/deps.ts new file mode 100644 index 0000000..85736e4 --- /dev/null +++ b/src/deps.ts @@ -0,0 +1,8 @@ +export const VERSION = "1.6.0-alpha.deno.1"; + +export { encode } from "https://deno.land/std@0.160.0/encoding/base64.ts"; +export { emptyDir, ensureDir } from "https://deno.land/std@0.160.0/fs/mod.ts"; +export { serve } from "https://deno.land/std@0.160.0/http/mod.ts"; +export { writableStreamFromWriter } from "https://deno.land/std@0.160.0/streams/mod.ts"; + +export { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts"; diff --git a/src/handlers/cors.ts b/src/handlers/cors.ts new file mode 100644 index 0000000..d98c74a --- /dev/null +++ b/src/handlers/cors.ts @@ -0,0 +1,12 @@ +import { respond } from "../helpers/cors.ts"; + +export function options( + _request: Request, + _match: URLPatternResult, +): Promise { + return new Promise((resolve) => + resolve( + respond(undefined, { status: 204 }), + ) + ); +} diff --git a/src/handlers/count.ts b/src/handlers/count.ts new file mode 100644 index 0000000..06d0f26 --- /dev/null +++ b/src/handlers/count.ts @@ -0,0 +1,12 @@ +import { respond } from "../helpers/cors.ts"; +import { processParams } from "../helpers/processParams.ts"; +import { getDocumentNumber } from "../meta/finder.ts"; + +export async function count( + request: Request, + _match: URLPatternResult, +): Promise { + const params = await processParams(request); + const count = await getDocumentNumber(params); + return respond("" + count); +} diff --git a/src/handlers/doc.ts b/src/handlers/doc.ts new file mode 100644 index 0000000..db7b454 --- /dev/null +++ b/src/handlers/doc.ts @@ -0,0 +1,301 @@ +import { ensureDir } from "https://deno.land/std@0.160.0/fs/ensure_dir.ts"; +import { nanoid, writableStreamFromWriter } from "../deps.ts"; +import { respond } from "../helpers/cors.ts"; +import { getText } from "../helpers/pdfprocessor.ts"; +import { processParams } from "../helpers/processParams.ts"; +import * as metadelete from "../meta/delete.ts"; +import * as metafinder from "../meta/finder.ts"; +import * as metastore from "../meta/store.ts"; + +type TagAdd = { + label: string; + parameter?: { + type: + | "http://www.w3.org/2001/XMLSchema#decimal" + | "http://www.w3.org/2001/XMLSchema#date"; + value: string; // must be valid xsd:decimal or xsd:date, as specified in property type. + }; // only for parameterizable tags +}; + +function getDir(id: string) { + return "./blobs/" + id.slice(0, 2) + "/" + id.slice(2, 6) + "/" + + id.slice(6, 14); +} + +function getPath(id: string) { + return "./blobs/" + id.slice(0, 2) + "/" + id.slice(2, 6) + "/" + + id.slice(6, 14) + "/" + id; +} + +function datecheck(request: Request) { + const url = new URL(request.url); + const regex = + /^(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-6]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-6]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-6]\d([+-][0-2]\d:[0-5]\d|Z))$/; + const date = url.searchParams.get("date"); + return date ? (regex.test(date) ? date : undefined) : undefined; +} + +export async function deleteDoc( + _request: Request, + match: URLPatternResult, +): Promise { + const id = match.pathname.groups.id; + await metadelete.deleteFile(id); + return respond(undefined, { status: 204 }); +} + +export async function deleteTag( + _request: Request, + match: URLPatternResult, +) { + await metadelete.deleteTag( + decodeURIComponent(match.pathname.groups.tagLabel), + match.pathname.groups.id, + ); + return respond(undefined, { status: 204 }); +} +export async function deleteTitle( + request: Request, + match: URLPatternResult, +): Promise { + const id = match.pathname.groups.id; + await metadelete.deleteTitle(id); + return respond(undefined, { status: 201 }); +} + +export async function getComments( + _request: Request, + match: URLPatternResult, +): Promise { + const id = match.pathname.groups.id; + const response = await metafinder.getComments(id); + return respond(JSON.stringify(response), { + headers: { + "content-type": "application/json; charset=utf-8", + }, + }); +} + +export async function getPDF( + _request: Request, + match: URLPatternResult, +): Promise { + const id = match.pathname.groups.id; + const path = getPath(id); + try { + const fileName = await metafinder.getBasicMeta(id).then(( + { title, created }, + ) => title || created || "document"); + const file = await Deno.open(path, { read: true }); + // Build a readable stream so the file doesn't have to be fully loaded into memory while we send it + const readableStream = file.readable; + return respond(readableStream, { + headers: { + "content-disposition": `inline; filename="${encodeURI(fileName)}.pdf"`, + "content-type": "application/pdf", + }, + }); + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + return respond("404 Not Found", { status: 404 }); + } + throw error; + } +} + +export async function getMeta( + _request: Request, + match: URLPatternResult, +): Promise { + const id = match.pathname.groups.id; + return respond( + JSON.stringify({ + ...(await metafinder.getBasicMeta(id)), + comments: await metafinder.getComments(id), + tags: await metafinder.getTags(id), + }), + { + headers: { + "content-type": "application/json; charset=utf-8", + }, + }, + ); +} + +export async function getTags( + _request: Request, + match: URLPatternResult, +): Promise { + const id = match.pathname.groups.id; + return respond(JSON.stringify(await metafinder.getTags(id)), { + headers: { + "content-type": "application/json; charset=utf-8", + }, + }); +} + +export async function getThumb( + _request: Request, + match: URLPatternResult, +): Promise { + const id = match.pathname.groups.id; + const path = getPath(id); + const fileName = await metafinder.getBasicMeta(id).then(( + { title, created }, + ) => title || created || "thumbnail"); + let thumb: Deno.FsFile; + try { + thumb = await Deno.open(path + ".png", { read: true }); + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + try { + await Deno.stat(path); // Check if PDF exists → 404 otherwise + const p = Deno.run({ + cmd: [ + "convert", + "-thumbnail", + "300x", + "-alpha", + "remove", + `${path}[0]`, + `${path}.png`, + ], + }); + const { success, code } = await p.status(); + if (!success) throw new Error("convert failed with code " + code); + thumb = await Deno.open(path + ".png", { read: true }); + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + return respond("404 Not Found", { status: 404 }); + } + throw error; + } + } else { + throw error; + } + } + // Build a readable stream so the file doesn't have to be fully loaded into memory while we send it + const readableStream = thumb.readable; + return respond(readableStream, { + headers: { + "content-disposition": `inline; filename="${encodeURI(fileName)}.png"`, + "content-type": "image/png", + }, + }); +} + +export async function getTitle( + _request: Request, + match: URLPatternResult, +): Promise { + const id = match.pathname.groups.id; + return respond( + JSON.stringify({ title: (await metafinder.getBasicMeta(id)).title }), + { + headers: { + "content-type": "application/json; charset=utf-8", + }, + }, + ); +} + +export async function list( + request: Request, + _match: URLPatternResult, +): Promise { + const params = await processParams(request); + const response = await metafinder.getDocumentList(params); + return respond(JSON.stringify(response), { + headers: { + "content-type": "application/json; charset=utf-8", + }, + }); +} + +export async function postComment( + request: Request, + match: URLPatternResult, +): Promise { + const id = match.pathname.groups.id; + await metastore.addComment(id, (await request.json()).text); + return respond(undefined, { status: 201 }); +} + +export async function postPDF( + request: Request, + _match: URLPatternResult, +): Promise { + const id = nanoid(); + const path = getPath(id); + await ensureDir(getDir(id)); + const pdf = await Deno.open(path, { write: true, create: true }); + const writableStream = writableStreamFromWriter(pdf); + await request.body?.pipeTo(writableStream); + console.log((new Date()).toISOString(), "Document created with id", id); + let text = await getText(path); + if (text.length < 4) { + // run OCR + const lang = Deno.env.get("OCR_LANG") || "fra+deu+eng"; + const p = Deno.run({ cmd: ["pdfsandwich", "-rgb", "-lang", lang, path] }); + const { success, code } = await p.status(); + if (!success) throw new Error("pdfsandwich failed with code " + code); + // pdfsandwich generates a file with the same name + _ocr + await Deno.rename(path + "_ocr", path); + text = await getText(path); + console.log((new Date()).toISOString(), id, ": OCR finished"); + } + // no await as we don’t care for the result - if it fails, the thumbnail will be created upon request. + Deno.run({ + cmd: [ + "convert", + "-thumbnail", + "300x", + "-alpha", + "remove", + `${path}[0]`, + `${path}.png`, + ], + }); + const date = datecheck(request); + await metastore.storeDocument({ id, text, date }); + return respond(undefined, { + headers: { + "Location": "/doc/" + id, + "Access-Control-Expose-Headers": "Location", + }, + }); +} + +export async function postTag( + request: Request, + match: URLPatternResult, +): Promise { + const id = match.pathname.groups.id; + const tagObject: TagAdd = await request.json(); + const [label, type] = + (await metafinder.getTagTypes([tagObject.label]))?.[0] ?? + [undefined, undefined]; + if (!label) { + return respond("Tag must exist before adding to a document", { + status: 400, + }); + } + if (tagObject.parameter?.type !== type) { + return respond("Type provided does not match", { status: 400 }); + } + if (tagObject.parameter?.type && !tagObject.parameter?.value) { + return respond("No value provided", { status: 400 }); + } + await metastore.addTag(id, tagObject.label, tagObject.parameter?.value, type); + return respond(undefined, { status: 201 }); +} + +export async function putTitle( + request: Request, + match: URLPatternResult, +): Promise { + const id = match.pathname.groups.id; + const title: string = (await request.json())?.title; + await metastore.addTitle(id, title); + return respond(undefined, { status: 201 }); +} diff --git a/src/handlers/notImplemented.ts b/src/handlers/notImplemented.ts new file mode 100644 index 0000000..10da909 --- /dev/null +++ b/src/handlers/notImplemented.ts @@ -0,0 +1,6 @@ +export function notImplemented( + _request: Request, + _match: URLPatternResult, +): Promise { + throw new Error("not implemented"); +} diff --git a/src/handlers/raw.ts b/src/handlers/raw.ts new file mode 100644 index 0000000..a680a83 --- /dev/null +++ b/src/handlers/raw.ts @@ -0,0 +1,142 @@ +import { ensureDir } from "https://deno.land/std@0.160.0/fs/ensure_dir.ts"; +import { emptyDir, writableStreamFromWriter } from "../deps.ts"; +import { respond } from "../helpers/cors.ts"; +import { dump } from "../meta/fusekiFetch.ts"; +import { restore } from "../meta/store.ts"; + +const decoder = new TextDecoder("utf-8"); + +export async function deleteRDFFile( + _request: Request, + _match: URLPatternResult, +): Promise { + await Deno.remove("rdf.ttl"); + return respond(undefined, { status: 204 }); +} + +export async function getRDF( + request: Request, + _match: URLPatternResult, +): Promise { + const url = new URL(request.url); + const accept = url.searchParams.has("accept") + ? decodeURIComponent(url.searchParams.get("accept")!) + : request.headers.get("Accept") || "text/turtle"; + return await dump(accept); +} + +export async function getTGZ( + _request: Request, + _match: URLPatternResult, +): Promise { + const timestamp = "" + Date.now(); + const tarPath = "blobs/tgz-" + timestamp; + const rdfName = "rdf-" + timestamp; + const rdfPath = "blobs/rdf/" + rdfName; + await ensureDir("blobs/rdf"); + const rdf = await Deno.open(rdfPath, { + create: true, + write: true, + truncate: true, + }); + const writableStream = writableStreamFromWriter(rdf); + await (await dump()).body?.pipeTo(writableStream); + const p = Deno.run({ + cmd: [ + "bash", + "-c", + `tar --transform="s|${rdfPath}|rdf.ttl|" --exclude-tag="${rdfName}" -czvf ${tarPath} blobs/*/`, + ], + }); + const { success, code } = await p.status(); + if (!success) throw new Error("tar -czf failed with code " + code); + await Deno.remove(rdfPath); + const tar = await Deno.open(tarPath); + // Build a readable stream so the file doesn't have to be fully loaded into memory while we send it + const readableStream = tar.readable; + return respond(readableStream, { + headers: { + "content-disposition": + `inline; filename="tridoc_backup_${timestamp}.tar.gz"`, + "content-type": "application/gzip", + }, + }); + // TODO: Figure out how to delete these files +} + +export async function getZIP( + _request: Request, + _match: URLPatternResult, +): Promise { + const timestamp = "" + Date.now(); + const zipPath = `blobs/zip-${timestamp}.zip`; + const rdfPath = "blobs/rdf-" + timestamp; + const rdf = await Deno.open(rdfPath, { + create: true, + write: true, + truncate: true, + }); + const writableStream = writableStreamFromWriter(rdf); + await (await dump()).body?.pipeTo(writableStream); + // Create zip + const p_1 = Deno.run({ + cmd: [ + "bash", + "-c", + `zip -r ${zipPath} blobs/*/ ${rdfPath} -x "blobs/rdf/*"`, + ], + }); + const r_1 = await p_1.status(); + if (!r_1.success) throw new Error("zip failed with code " + r_1.code); + // move rdf-??? to rdf.zip + const p_2 = Deno.run({ + cmd: [ + "bash", + "-c", + `printf "@ ${rdfPath}\\n@=rdf.ttl\\n" | zipnote -w ${zipPath}`, + ], + }); + const r_2 = await p_2.status(); + if (!r_2.success) throw new Error("zipnote failed with code " + r_2.code); + await Deno.remove(rdfPath); + const zip = await Deno.open(zipPath); + // Build a readable stream so the file doesn't have to be fully loaded into memory while we send it + const readableStream = zip.readable; + return respond(readableStream, { + headers: { + "content-disposition": + `inline; filename="tridoc_backup_${timestamp}.zip"`, + "content-type": "application/zip", + }, + }); + // TODO: Figure out how to delete these files +} + +export async function putZIP( + request: Request, + _match: URLPatternResult, +): Promise { + try { + await Deno.stat("rdf.ttl"); + throw new Error( + "Can't unzip concurrently: rdf.ttl already exists. If you know what you are doing, clear this message with HTTP DELETE /raw/rdf", + ); + } catch (error) { + if (!(error instanceof Deno.errors.NotFound)) { + throw error; + } + } + await emptyDir("blobs"); + const zipPath = "blobs/zip-" + Date.now(); + const zip = await Deno.open(zipPath, { write: true, create: true }); + const writableStream = writableStreamFromWriter(zip); + await request.body?.pipeTo(writableStream); + const p = Deno.run({ cmd: ["unzip", zipPath] }); + const { success, code } = await p.status(); + if (!success) throw new Error("unzip failed with code " + code); + await Deno.remove(zipPath); + const turtleData = decoder.decode(await Deno.readFile("rdf.ttl")); + await Deno.remove("rdf.ttl"); + await restore(turtleData); + return respond(undefined, { status: 204 }); +} diff --git a/src/handlers/tag.ts b/src/handlers/tag.ts new file mode 100644 index 0000000..27cf821 --- /dev/null +++ b/src/handlers/tag.ts @@ -0,0 +1,75 @@ +import { respond } from "../helpers/cors.ts"; +import { processParams } from "../helpers/processParams.ts"; +import * as metadelete from "../meta/delete.ts"; +import * as metafinder from "../meta/finder.ts"; +import * as metastore from "../meta/store.ts"; + +type TagCreate = { + label: string; + parameter?: { + type: + | "http://www.w3.org/2001/XMLSchema#decimal" + | "http://www.w3.org/2001/XMLSchema#date"; + }; // only for parameterizable tags +}; + +export async function createTag( + request: Request, + _match: URLPatternResult, +): Promise { + const tagObject: TagCreate = await request.json(); + if (!tagObject?.label) return respond("No label provided", { status: 400 }); + if ( + tagObject?.parameter && + tagObject.parameter.type !== "http://www.w3.org/2001/XMLSchema#decimal" && + tagObject.parameter.type !== "http://www.w3.org/2001/XMLSchema#date" + ) { + return respond("Invalid type", { status: 400 }); + } + const tagList = await metafinder.getTagList(); + if (tagList.some((e) => e.label === tagObject.label)) { + return respond("Tag already exists", { status: 400 }); + } + const regex = /\s|^[.]{1,2}$|\/|\\|#|"|'|,|;|:|\?/; + if (regex.test(tagObject.label)) { + return respond("Label contains forbidden characters", { status: 400 }); + } + await metastore.createTag(tagObject.label, tagObject.parameter?.type); + return respond(undefined, { status: 201 }); +} + +export async function deleteTag( + _request: Request, + match: URLPatternResult, +) { + await metadelete.deleteTag( + decodeURIComponent(match.pathname.groups.tagLabel), + ); + return respond(undefined, { status: 204 }); +} + +export async function getDocs( + request: Request, + match: URLPatternResult, +): Promise { + const params = await processParams(request, { + tags: [[match.pathname.groups.tagLabel]], + }); + const response = await metafinder.getDocumentList(params); + return respond(JSON.stringify(response), { + headers: { + "content-type": "application/json; charset=utf-8", + }, + }); +} + +export async function getTagList( + _request: Request, + _match: URLPatternResult, +): Promise { + return respond(JSON.stringify(await metafinder.getTagList()), { + headers: { + "content-type": "application/json; charset=utf-8", + }, + }); +} diff --git a/src/handlers/version.ts b/src/handlers/version.ts new file mode 100644 index 0000000..2864385 --- /dev/null +++ b/src/handlers/version.ts @@ -0,0 +1,9 @@ +import { VERSION } from "../deps.ts"; +import { respond } from "../helpers/cors.ts"; + +export function version( + _request: Request, + _match: URLPatternResult, +): Promise { + return new Promise((resolve) => resolve(respond(VERSION))); +} diff --git a/src/helpers/cors.ts b/src/helpers/cors.ts new file mode 100644 index 0000000..907f2ad --- /dev/null +++ b/src/helpers/cors.ts @@ -0,0 +1,11 @@ +export function respond(body?: BodyInit, init?: ResponseInit) { + return new Response(body, { + ...init, + headers: { + ...init?.headers, + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, PUT, DELETE, GET, OPTIONS", + "Access-Control-Allow-Headers": "Authorization, Content-Type", + }, + }); +} diff --git a/src/helpers/pdfprocessor.ts b/src/helpers/pdfprocessor.ts new file mode 100644 index 0000000..1e267d2 --- /dev/null +++ b/src/helpers/pdfprocessor.ts @@ -0,0 +1,9 @@ +const decoder = new TextDecoder("utf-8"); + +export async function getText(path: string) { + const p = Deno.run({ cmd: ["pdftotext", path, "-"], stdout: "piped" }); + const output = decoder.decode(await p.output()); + const { success, code } = await p.status(); + if (!success) throw new Error("pdftotext failed with code " + code); + return output; +} diff --git a/src/helpers/processParams.ts b/src/helpers/processParams.ts new file mode 100644 index 0000000..d03525a --- /dev/null +++ b/src/helpers/processParams.ts @@ -0,0 +1,98 @@ +import { getTagTypes } from "../meta/finder.ts"; + +function extractQuery(request: Request) { + const url = new URL(request.url); + const query: Record = {}; + for (const param of url.searchParams) { + if (query[param[0]]) { + query[param[0]].push(param[1]); + } else query[param[0]] = new Array(param[1]); + } + return query; +} + +type ParamTag = { + label: string; // [0] + min?: string; // [1] + max?: string; // [2] + type?: string; // [3] + maxIsExclusive?: boolean; //[5] +}; + +export type queryOverrides = { + tags?: string[][]; + nottags?: string[][]; +}; + +export type Params = { + tags?: ParamTag[]; + nottags?: ParamTag[]; + text?: string; + limit?: number; + offset?: number; +}; + +export async function processParams( + request: Request, + queryOverrides?: queryOverrides, +): Promise { + const query = extractQuery(request); + const result: Params = {}; + const tags = query.tag?.map((t) => t.split(";")) ?? []; + if (queryOverrides?.tags) tags.push(...queryOverrides.tags); + const nottags = query.nottag?.map((t) => t.split(";")) ?? []; + if (queryOverrides?.nottags) tags.push(...queryOverrides.nottags); + result.text = query.text?.[0]; + result.limit = parseInt(query.limit?.[0], 10) > 0 + ? parseInt(query.limit[0]) + : undefined; + result.offset = parseInt(query.offset?.[0], 10) >= 0 + ? parseInt(query.offset[0]) + : undefined; + return await getTagTypes( + tags.map((e) => e[0]).concat(nottags.map((e) => e[0])), + ).then((types) => { + function tagMap(t: string[]): ParamTag { + const label = t[0]; + const type = types.find((e) => e[0] === t[0])?.[1]; + let min = t[1]; + let max = t[2]; + let maxIsExclusive; + if (type === "http://www.w3.org/2001/XMLSchema#date") { + if (min) { + switch (min.length) { + case 4: + min += "-01-01"; + break; + case 7: + min += "-01"; + break; + } + } + if (max) { + switch (max.length) { + case 4: + max += "-12-31"; + break; + case 7: { + const month = parseInt(max.substring(5), 10) + 1; + if (month < 13) { + max = max.substring(0, 5) + "-" + + month.toString().padStart(2, "0") + "-01"; + maxIsExclusive = true; + } else { + max += "-31"; + } + break; + } + } + } + } + return { label, min, max, type, maxIsExclusive }; + } + result.tags = tags.map(tagMap); + result.nottags = nottags.map(tagMap); + console.log("eh??", result); + return result; + }); +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..e75b254 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,12 @@ +import { serve } from "./server/server.ts"; + +console.log("Starting tridoc backend server"); + +// TODO Check external dependencies + +if (!Deno.env.get("TRIDOC_PWD")) { + throw new Error("No password set"); +} + +serve(); +console.log("Tridoc backend server is listening on port 8000"); diff --git a/src/meta/delete.ts b/src/meta/delete.ts new file mode 100644 index 0000000..be9e4bc --- /dev/null +++ b/src/meta/delete.ts @@ -0,0 +1,58 @@ +import { fusekiUpdate } from "./fusekiFetch.ts"; + +export function deleteFile(id: string) { + return fusekiUpdate(` +WITH +DELETE { ?p ?o } +WHERE { ?p ?o }`); +} + +export async function deleteTag(label: string, id?: string) { + await Promise.allSettled([ + fusekiUpdate(` +PREFIX rdf: +PREFIX s: +PREFIX tridoc: +WITH +DELETE { + ${ + id ? ` tridoc:tag ?ptag + ` : `?ptag ?p ?o . + ?s ?p1 ?ptag` + } +} +WHERE { + ?ptag tridoc:parameterizableTag ?tag. + ?tag tridoc:label "${label}" . + OPTIONAL { ?ptag ?p ?o } + OPTIONAL { + ${id ? ` tridoc:tag ?ptag` : "?s ?p1 ?ptag"} + } +}`), + fusekiUpdate(` +PREFIX rdf: +PREFIX s: +PREFIX tridoc: +WITH +DELETE { + ${ + id ? ` tridoc:tag ?tag` : `?tag ?p ?o . + ?s ?p1 ?tag` + } +} +WHERE { + ?tag tridoc:label "${label}" . + OPTIONAL { ?tag ?p ?o } + OPTIONAL { + ${id ? ` ?p1 ?tag` : "?s ?p1 ?tag"} + } +}`), + ]); +} + +export function deleteTitle(id: string) { + return fusekiUpdate(` +PREFIX s: +WITH +DELETE { s:name ?o } +WHERE { s:name ?o }`); +} diff --git a/src/meta/finder.ts b/src/meta/finder.ts new file mode 100644 index 0000000..ad3ed14 --- /dev/null +++ b/src/meta/finder.ts @@ -0,0 +1,257 @@ +import { Params } from "../helpers/processParams.ts"; +import { fusekiFetch } from "./fusekiFetch.ts"; + +export async function getComments(id: string) { + const query = `PREFIX rdf: +PREFIX xsd: +PREFIX tridoc: +PREFIX s: +SELECT DISTINCT ?d ?t WHERE { + GRAPH { + s:comment [ + a s:Comment ; + s:dateCreated ?d ; + s:text ?t + ] . + } +}`; + return await fusekiFetch(query).then((json) => + json.results.bindings.map((binding) => { + return { text: binding.t.value, created: binding.d.value }; + }) + ); +} + +export async function getDocumentList( + { tags = [], nottags = [], text, limit, offset }: Params, +) { + let tagQuery = ""; + for (let i = 0; i < tags.length; i++) { + if (tags[i].type) { + tagQuery += `{ ?s tridoc:tag ?ptag${i} . + ?ptag${i} tridoc:parameterizableTag ?atag${i} . + ?ptag${i} tridoc:value ?v${i} . + ?atag${i} tridoc:label "${tags[i].label}" . + ${ + tags[i].min + ? `FILTER (?v${i} >= "${tags[i].min}"^^<${tags[i].type}> )` + : "" + } + ${ + tags[i].max + ? `FILTER (?v${i} ${tags[i].maxIsExclusive ? "<" : "<="} "${ + tags[i].max + }"^^<${tags[i].type}> )` + : "" + } }`; + } else { + tagQuery += `{ ?s tridoc:tag ?tag${i} . + ?tag${i} tridoc:label "${tags[i].label}" . }`; + } + } + for (let i = 0; i < nottags.length; i++) { + if (nottags[i].type) { + tagQuery += `FILTER NOT EXISTS { ?s tridoc:tag ?ptag${i} . + ?ptag${i} tridoc:parameterizableTag ?atag${i} . + ?ptag${i} tridoc:value ?v${i} . + ?atag${i} tridoc:label "${nottags[i].label}" . + ${ + nottags[i].min + ? `FILTER (?v${i} >= "${nottags[i].min}"^^<${nottags[i].type}> )` + : "" + } + ${ + nottags[i].max + ? `FILTER (?v${i} ${nottags[i].maxIsExclusive ? "<" : "<="} "${ + nottags[i].max + }"^^<${nottags[i].type}> )` + : "" + } }`; + } else { + tagQuery += `FILTER NOT EXISTS { ?s tridoc:tag ?tag${i} . + ?tag${i} tridoc:label "${nottags[i].label}" . }`; + } + } + const body = "PREFIX rdf: \n" + + "PREFIX s: \n" + + "PREFIX tridoc: \n" + + "PREFIX text: \n" + + "SELECT DISTINCT ?s ?identifier ?title ?date\n" + + "WHERE {\n" + + " ?s s:identifier ?identifier .\n" + + " ?s s:dateCreated ?date .\n" + + tagQuery + + " OPTIONAL { ?s s:name ?title . }\n" + + (text + ? '{ { ?s text:query (s:name "' + text + + '") } UNION { ?s text:query (s:text "' + text + '")} } .\n' + : "") + + "}\n" + + "ORDER BY desc(?date)\n" + + (limit ? "LIMIT " + limit + "\n" : "") + + (offset ? "OFFSET " + offset : ""); + return await fusekiFetch(body).then((json) => + json.results.bindings.map((binding) => { + const result: Record = {}; + result.identifier = binding.identifier.value; + if (binding.title) { + result.title = binding.title.value; + } + if (binding.date) { + result.created = binding.date.value; + } + return result; + }) + ); +} + +export async function getDocumentNumber( + { tags = [], nottags = [], text }: Params, +) { + let tagQuery = ""; + for (let i = 0; i < tags.length; i++) { + if (tags[i].type) { + tagQuery += `{ ?s tridoc:tag ?ptag${i} . + ?ptag${i} tridoc:parameterizableTag ?atag${i} . + ?ptag${i} tridoc:value ?v${i} . + ?atag${i} tridoc:label "${tags[i].label}" . + ${ + tags[i].min + ? `FILTER (?v${i} >= "${tags[i].min}"^^<${tags[i].type}> )` + : "" + } + ${ + tags[i].max + ? `FILTER (?v${i} ${tags[i].maxIsExclusive ? "<" : "<="} "${ + tags[i].max + }"^^<${tags[i].type}> )` + : "" + } }`; + } else { + tagQuery += `{ ?s tridoc:tag ?tag${i} . + ?tag${i} tridoc:label "${tags[i].label}" . }`; + } + } + for (let i = 0; i < nottags.length; i++) { + if (nottags[i].type) { + tagQuery += `FILTER NOT EXISTS { ?s tridoc:tag ?ptag${i} . + ?ptag${i} tridoc:parameterizableTag ?atag${i} . + ?ptag${i} tridoc:value ?v${i} . + ?atag${i} tridoc:label "${nottags[i].label}" . + ${ + nottags[i].min + ? `FILTER (?v${i} >= "${nottags[i].min}"^^<${nottags[i].type}> )` + : "" + } + ${ + nottags[i].max + ? `FILTER (?v${i} ${nottags[i].maxIsExclusive ? "<" : "<="} "${ + nottags[i].max + }"^^<${nottags[i].type}> )` + : "" + } }`; + } else { + tagQuery += `FILTER NOT EXISTS { ?s tridoc:tag ?tag${i} . + ?tag${i} tridoc:label "${nottags[i].label}" . }`; + } + } + return await fusekiFetch(` +PREFIX rdf: +PREFIX s: +PREFIX tridoc: +PREFIX text: +SELECT (COUNT(DISTINCT ?s) as ?count) +WHERE { + ?s s:identifier ?identifier . + ${tagQuery} + ${ + text + ? `{ { ?s text:query (s:name "${text}") } UNION { ?s text:query (s:text "${text}")} } .\n` + : "" + }}`).then((json) => parseInt(json.results.bindings[0].count.value, 10)); +} + +export async function getBasicMeta(id: string) { + return await fusekiFetch(` +PREFIX rdf: +PREFIX s: +SELECT ?title ?date +WHERE { + ?s s:identifier "${id}" . + ?s s:dateCreated ?date . + OPTIONAL { ?s s:name ?title . } +}`).then((json) => { + return { + title: json.results.bindings[0]?.title?.value, + created: json.results.bindings[0]?.date?.value, + }; + }); +} + +export async function getTagList() { + const query = ` +PREFIX tridoc: +SELECT DISTINCT ?s ?label ?type +WHERE { + ?s tridoc:label ?label . + OPTIONAL { ?s tridoc:valueType ?type . } +}`; + return await fusekiFetch(query).then((json) => + json.results.bindings.map((binding) => { + return { + label: binding.label.value, + parameter: binding.type ? { type: binding.type.value } : undefined, + }; + }) + ); +} + +export async function getTags(id: string) { + const query = ` +PREFIX tridoc: +SELECT DISTINCT ?label ?type ?v + WHERE { + GRAPH { + tridoc:tag ?tag . + { + ?tag tridoc:label ?label . + } + UNION + { + ?tag tridoc:value ?v ; + tridoc:parameterizableTag ?ptag . + ?ptag tridoc:label ?label ; + tridoc:valueType ?type . + } + } +}`; + return await fusekiFetch(query).then((json) => + json.results.bindings.map((binding) => { + return { + label: binding.label.value, + parameter: binding.type + ? { type: binding.type.value, value: binding.v.value } + : undefined, + }; + }) + ); +} + +// => [label, type?][] +export async function getTagTypes(labels: string[]) { + const json = await fusekiFetch(` +PREFIX tridoc: +SELECT DISTINCT ?l ?t WHERE { VALUES ?l { "${ + labels.join('" "') + }" } ?s tridoc:label ?l . OPTIONAL { ?s tridoc:valueType ?t . } }`); + return json.results.bindings.map( + (binding) => { + const result_1 = []; + result_1[0] = binding.l.value; + if (binding.t) { + result_1[1] = binding.t.value; + } + return result_1; + }, + ); +} diff --git a/src/meta/fusekiFetch.ts b/src/meta/fusekiFetch.ts new file mode 100644 index 0000000..afbaf5a --- /dev/null +++ b/src/meta/fusekiFetch.ts @@ -0,0 +1,56 @@ +type SparqlJson = { + head: { + vars: string[]; + }; + results: { + bindings: { [key: string]: { type: string; value: string } }[]; + }; +}; + +export function dump(accept = "text/turtle") { + const query = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }"; + console.log((new Date()).toISOString(), "→ FUSEKI QUERY", query, "\n"); + return fetch("http://fuseki:3030/3DOC/query", { + method: "POST", + headers: { + "Authorization": "Basic " + btoa("admin:pw123"), + "Content-Type": "application/sparql-query", + "Accept": accept, + }, + body: query, + }); +} + +export async function fusekiFetch(query: string): Promise { + console.log((new Date()).toISOString(), "→ FUSEKI QUERY", query, "\n"); + return await fetch("http://fuseki:3030/3DOC/query", { + method: "POST", + headers: { + "Authorization": "Basic " + btoa("admin:pw123"), + "Content-Type": "application/sparql-query", + }, + body: query, + }).then(async (response) => { + if (response.ok) { + return response.json(); + } else { + throw new Error("Fuseki Error: " + await response.text()); + } + }); +} + +export async function fusekiUpdate(query: string): Promise { + console.log((new Date()).toISOString(), "→ FUSEKI UPDATE", query, "\n"); + return await fetch("http://fuseki:3030/3DOC/update", { + method: "POST", + headers: { + "Authorization": "Basic " + btoa("admin:pw123"), + "Content-Type": "application/sparql-update", + }, + body: query, + }).then(async (response) => { + if (!response.ok) { + throw new Error("Fuseki Error: " + await response.text()); + } + }); +} diff --git a/src/meta/store.ts b/src/meta/store.ts new file mode 100644 index 0000000..ed5826c --- /dev/null +++ b/src/meta/store.ts @@ -0,0 +1,116 @@ +import { fusekiUpdate } from "./fusekiFetch.ts"; + +function escapeLiteral(string: string) { + return string.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace( + /\r/g, + "\\r", + ).replace(/'/g, "\\'").replace(/"/g, '\\"'); +} + +export async function addComment(id: string, text: string) { + const now = new Date(); + const query = ` +PREFIX rdf: +PREFIX xsd: +PREFIX tridoc: +PREFIX s: +INSERT DATA { + GRAPH { + s:comment [ + a s:Comment ; + s:dateCreated "${now.toISOString()}"^^xsd:dateTime ; + s:text "${escapeLiteral(text)}" + ] . + } +}`; + return await fusekiUpdate(query); +} + +export async function addTag( + id: string, + label: string, + value: string, + type: string, +) { + const tag = value + ? encodeURIComponent(label) + "/" + value + : encodeURIComponent(label); + const query = ` +PREFIX rdf: +PREFIX xsd: +PREFIX tridoc: +PREFIX s: +INSERT DATA { + GRAPH { + tridoc:tag .${ + value + ? ` + a tridoc:ParameterizedTag ; + tridoc:parameterizableTag ; + tridoc:value "${value}"^^<${type}> .` + : "" + } + } +}`; + return await fusekiUpdate(query); +} + +export async function addTitle(id: string, title: string) { + const query = ` +PREFIX rdf: +PREFIX s: +WITH +DELETE { s:name ?o } +INSERT { s:name "${escapeLiteral(title)}" } +WHERE { OPTIONAL { s:name ?o } }`; + return await fusekiUpdate(query); +} + +export async function createTag( + label: string, + type?: + | "http://www.w3.org/2001/XMLSchema#decimal" + | "http://www.w3.org/2001/XMLSchema#date", +) { + const tagType = type ? "ParameterizableTag" : "Tag"; + const valueType = type ? "tridoc:valueType <" + type + ">;\n" : ""; + const query = ` +PREFIX rdf: +PREFIX xsd: +PREFIX tridoc: +PREFIX s: +INSERT DATA { + GRAPH { + rdf:type tridoc:${tagType} ; + ${valueType} tridoc:label "${escapeLiteral(label)}" . + } +}`; + return await fusekiUpdate(query); +} + +export function restore(turtleData: string) { + return fusekiUpdate(` +CLEAR GRAPH ; +INSERT DATA { + GRAPH { ${turtleData} } +}`); +} + +export async function storeDocument( + { id, text, date }: { id: string; text: string; date?: string }, +) { + const created = (date ? new Date(date) : new Date()).toISOString(); + const query = ` +PREFIX rdf: +PREFIX xsd: +PREFIX s: +INSERT DATA { + GRAPH { + rdf:type s:DigitalDocument ; + s:dateCreated "${created}"^^xsd:dateTime ; + s:identifier "${id}" ; + s:text "${escapeLiteral(text)}" . + } +}`; + return await fusekiUpdate(query); +} diff --git a/src/server/routes.ts b/src/server/routes.ts new file mode 100644 index 0000000..231fd6c --- /dev/null +++ b/src/server/routes.ts @@ -0,0 +1,97 @@ +import { options } from "../handlers/cors.ts"; +import { count } from "../handlers/count.ts"; +import * as doc from "../handlers/doc.ts"; +import * as raw from "../handlers/raw.ts"; +import * as tag from "../handlers/tag.ts"; +import { version } from "../handlers/version.ts"; + +export const routes: { + [method: string]: { + pattern: URLPattern; + handler: (request: Request, match: URLPatternResult) => Promise; + }[]; +} = { + "OPTIONS": [{ + pattern: new URLPattern({ pathname: "*" }), + handler: options, + }], + "GET": [{ + pattern: new URLPattern({ pathname: "/count" }), + handler: count, + }, { + pattern: new URLPattern({ pathname: "/doc" }), + handler: doc.list, + }, { + pattern: new URLPattern({ pathname: "/doc/:id" }), + handler: doc.getPDF, + }, { + pattern: new URLPattern({ pathname: "/doc/:id/comment" }), + handler: doc.getComments, + }, { + pattern: new URLPattern({ pathname: "/doc/:id/tag" }), + handler: doc.getTags, + }, { + pattern: new URLPattern({ pathname: "/doc/:id/thumb" }), + handler: doc.getThumb, + }, { + pattern: new URLPattern({ pathname: "/doc/:id/title" }), + handler: doc.getTitle, + }, { + pattern: new URLPattern({ pathname: "/doc/:id/meta" }), + handler: doc.getMeta, + }, { + pattern: new URLPattern({ pathname: "/raw/rdf" }), + handler: raw.getRDF, + }, { + pattern: new URLPattern({ pathname: "/raw/zip" }), + handler: raw.getZIP, + }, { + pattern: new URLPattern({ pathname: "/raw/tgz" }), + handler: raw.getTGZ, + }, { + pattern: new URLPattern({ pathname: "/tag" }), + handler: tag.getTagList, + }, { + pattern: new URLPattern({ pathname: "/tag/:tagLabel" }), + handler: tag.getDocs, + }, { + pattern: new URLPattern({ pathname: "/version" }), + handler: version, + }], + "POST": [{ + pattern: new URLPattern({ pathname: "/doc" }), + handler: doc.postPDF, + }, { + pattern: new URLPattern({ pathname: "/doc/:id/comment" }), + handler: doc.postComment, + }, { + pattern: new URLPattern({ pathname: "/doc/:id/tag" }), + handler: doc.postTag, + }, { + pattern: new URLPattern({ pathname: "/tag" }), + handler: tag.createTag, + }], + "PUT": [{ + pattern: new URLPattern({ pathname: "/doc/:id/title" }), + handler: doc.putTitle, + }, { + pattern: new URLPattern({ pathname: "/raw/zip" }), + handler: raw.putZIP, + }], + "DELETE": [{ + pattern: new URLPattern({ pathname: "/doc/:id" }), + handler: doc.deleteDoc, + }, { + pattern: new URLPattern({ pathname: "/doc/:id/tag/:tagLabel" }), + handler: doc.deleteTag, + }, { + pattern: new URLPattern({ pathname: "/doc/:id/title" }), + handler: doc.deleteTitle, + }, { + pattern: new URLPattern({ pathname: "/tag/:tagLabel" }), + handler: tag.deleteTag, + }, { + pattern: new URLPattern({ pathname: "/raw/rdf" }), + handler: raw.deleteRDFFile, + }], +}; diff --git a/src/server/server.ts b/src/server/server.ts new file mode 100644 index 0000000..f810f0a --- /dev/null +++ b/src/server/server.ts @@ -0,0 +1,58 @@ +import { encode, serve as stdServe } from "../deps.ts"; +import { respond } from "../helpers/cors.ts"; +import { routes } from "./routes.ts"; + +const isAuthenticated = (request: Request) => { + return (request.method === "OPTIONS") || + request.headers.get("Authorization") === + "Basic " + encode("tridoc:" + Deno.env.get("TRIDOC_PWD")); +}; + +const handler = async (request: Request): Promise => { + const path = request.url.slice(request.url.indexOf("/", "https://".length)); + console.log((new Date()).toISOString(), request.method, path); + try { + if (!isAuthenticated(request)) { + console.log( + (new Date()).toISOString(), + request.method, + path, + "→ 401: Not Authenticated", + ); + return respond("401 Not Authenticated", { + status: 401, + headers: { "WWW-Authenticate": "Basic" }, + }); + } + + const route = routes[request.method]?.find(({ pattern }) => + pattern.test(request.url) + ); + if (route) { + return await route.handler(request, route.pattern.exec(request.url)!); + } + + console.log( + (new Date()).toISOString(), + request.method, + path, + "→ 404: Path not found", + ); + return respond("404 Path not found", { status: 404 }); + } catch (error) { + let message; + if (error instanceof Deno.errors.PermissionDenied) { + message = "Got “Permission Denied” trying to access the file on disk.\n\n Please run ```docker exec -u 0 [name of backend-container] chmod -R a+r ./blobs/ rdf.ttl``` on the host server to fix this and similar issues for the future." + } + console.log( + (new Date()).toISOString(), + request.method, + path, + "→ 500:", + error, + ); + return respond("500 " + (message || error), { status: 500 }); + } +}; + +export const serve = () => stdServe(handler, { onListen: undefined }); diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 1ad8e52..0000000 --- a/yarn.lock +++ /dev/null @@ -1,624 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -accept@3.x.x: - version "3.0.2" - resolved "https://registry.yarnpkg.com/accept/-/accept-3.0.2.tgz#83e41cec7e1149f3fd474880423873db6c6cc9ac" - dependencies: - boom "7.x.x" - hoek "5.x.x" - -adm-zip@^0.4.16: - version "0.4.16" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365" - integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg== - -ajv-keywords@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" - -ajv@^6.1.0: - version "6.5.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.2.tgz#678495f9b82f7cca6be248dd92f59bff5e1f4360" - dependencies: - fast-deep-equal "^2.0.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.1" - -ammo@3.x.x: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ammo/-/ammo-3.0.1.tgz#c79ceeac36fb4e55085ea3fe0c2f42bfa5f7c914" - dependencies: - hoek "5.x.x" - -archiver-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" - integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== - dependencies: - glob "^7.1.4" - graceful-fs "^4.2.0" - lazystream "^1.0.0" - lodash.defaults "^4.2.0" - lodash.difference "^4.5.0" - lodash.flatten "^4.4.0" - lodash.isplainobject "^4.0.6" - lodash.union "^4.6.0" - normalize-path "^3.0.0" - readable-stream "^2.0.0" - -archiver@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-3.1.1.tgz#9db7819d4daf60aec10fe86b16cb9258ced66ea0" - integrity sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg== - dependencies: - archiver-utils "^2.1.0" - async "^2.6.3" - buffer-crc32 "^0.2.1" - glob "^7.1.4" - readable-stream "^3.4.0" - tar-stream "^2.1.0" - zip-stream "^2.1.2" - -async@^2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== - dependencies: - lodash "^4.17.14" - -b64@4.x.x: - version "4.0.0" - resolved "https://registry.yarnpkg.com/b64/-/b64-4.0.0.tgz#c37f587f0a383c7019e821120e8c3f58f0d22772" - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -base64-js@^1.0.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" - integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== - -big-time@2.x.x: - version "2.0.1" - resolved "https://registry.yarnpkg.com/big-time/-/big-time-2.0.1.tgz#68c7df8dc30f97e953f25a67a76ac9713c16c9de" - -big.js@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" - -bl@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88" - integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A== - dependencies: - readable-stream "^3.0.1" - -boom@7.x.x: - version "7.2.0" - resolved "https://registry.yarnpkg.com/boom/-/boom-7.2.0.tgz#2bff24a55565767fde869ec808317eb10c48e966" - dependencies: - hoek "5.x.x" - -bounce@1.x.x: - version "1.2.0" - resolved "https://registry.yarnpkg.com/bounce/-/bounce-1.2.0.tgz#e3bac68c73fd256e38096551efc09f504873c8c8" - dependencies: - boom "7.x.x" - hoek "5.x.x" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= - -buffer@^5.1.0: - version "5.4.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115" - integrity sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - -call@5.x.x: - version "5.0.1" - resolved "https://registry.yarnpkg.com/call/-/call-5.0.1.tgz#ac1b5c106d9edc2a17af2a4a4f74dd4f0c06e910" - dependencies: - boom "7.x.x" - hoek "5.x.x" - -catbox-memory@3.x.x: - version "3.1.2" - resolved "https://registry.yarnpkg.com/catbox-memory/-/catbox-memory-3.1.2.tgz#4aeec1bc994419c0f7e60087f172aaedd9b4911c" - dependencies: - big-time "2.x.x" - boom "7.x.x" - hoek "5.x.x" - -catbox@10.x.x: - version "10.0.2" - resolved "https://registry.yarnpkg.com/catbox/-/catbox-10.0.2.tgz#e6ac1f35102d1a9bd07915b82e508d12b50a8bfa" - dependencies: - boom "7.x.x" - bounce "1.x.x" - hoek "5.x.x" - joi "13.x.x" - -compress-commons@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-2.1.1.tgz#9410d9a534cf8435e3fbbb7c6ce48de2dc2f0610" - integrity sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q== - dependencies: - buffer-crc32 "^0.2.13" - crc32-stream "^3.0.1" - normalize-path "^3.0.0" - readable-stream "^2.3.6" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -content@4.x.x: - version "4.0.5" - resolved "https://registry.yarnpkg.com/content/-/content-4.0.5.tgz#bc547deabc889ab69bce17faf3585c29f4c41bf2" - dependencies: - boom "7.x.x" - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -crc32-stream@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-3.0.1.tgz#cae6eeed003b0e44d739d279de5ae63b171b4e85" - integrity sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w== - dependencies: - crc "^3.4.4" - readable-stream "^3.4.0" - -crc@^3.4.4: - version "3.8.0" - resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6" - integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ== - dependencies: - buffer "^5.1.0" - -cryptiles@4.x.x: - version "4.1.2" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-4.1.2.tgz#363c9ab5c859da9d2d6fb901b64d980966181184" - dependencies: - boom "7.x.x" - -emojis-list@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" - -end-of-stream@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" - integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== - dependencies: - once "^1.4.0" - -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - -fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -glob@^7.1.4: - version "7.1.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" - integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -graceful-fs@^4.2.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" - integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q== - -hapi-auth-basic@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/hapi-auth-basic/-/hapi-auth-basic-5.0.0.tgz#0438b00225e4f7baccd7f29e04b4fc5037c012b0" - integrity sha1-BDiwAiXk97rM1/KeBLT8UDfAErA= - dependencies: - boom "7.x.x" - hoek "5.x.x" - -hapi@^17.5.2: - version "17.5.2" - resolved "https://registry.yarnpkg.com/hapi/-/hapi-17.5.2.tgz#9c5823cdcdd17e5621ebc8928aefb144d033caac" - dependencies: - accept "3.x.x" - ammo "3.x.x" - boom "7.x.x" - bounce "1.x.x" - call "5.x.x" - catbox "10.x.x" - catbox-memory "3.x.x" - heavy "6.x.x" - hoek "5.x.x" - joi "13.x.x" - mimos "4.x.x" - podium "3.x.x" - shot "4.x.x" - statehood "6.x.x" - subtext "6.x.x" - teamwork "3.x.x" - topo "3.x.x" - -heavy@6.x.x: - version "6.1.0" - resolved "https://registry.yarnpkg.com/heavy/-/heavy-6.1.0.tgz#1bbfa43dc61dd4b543ede3ff87db8306b7967274" - dependencies: - boom "7.x.x" - hoek "5.x.x" - joi "13.x.x" - -hoek@5.x.x: - version "5.0.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.3.tgz#b71d40d943d0a95da01956b547f83c4a5b4a34ac" - -ieee754@^1.1.4: - version "1.1.13" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.3, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -iron@5.x.x: - version "5.0.4" - resolved "https://registry.yarnpkg.com/iron/-/iron-5.0.4.tgz#003ed822f656f07c2b62762815f5de3947326867" - dependencies: - boom "7.x.x" - cryptiles "4.x.x" - hoek "5.x.x" - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isemail@3.x.x: - version "3.1.3" - resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.1.3.tgz#64f37fc113579ea12523165c3ebe3a71a56ce571" - dependencies: - punycode "2.x.x" - -joi@13.x.x: - version "13.4.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-13.4.0.tgz#afc359ee3d8bc5f9b9ba6cdc31b46d44af14cecc" - dependencies: - hoek "5.x.x" - isemail "3.x.x" - topo "3.x.x" - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - -json5@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - -lazystream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" - integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ= - dependencies: - readable-stream "^2.0.5" - -loader-utils@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" - dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - -lodash.defaults@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" - integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= - -lodash.difference@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" - integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw= - -lodash.flatten@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - -lodash.union@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" - integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= - -lodash@^4.17.14: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== - -mime-db@1.x.x: - version "1.35.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.35.0.tgz#0569d657466491283709663ad379a99b90d9ab47" - -mimos@4.x.x: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mimos/-/mimos-4.0.0.tgz#76e3d27128431cb6482fd15b20475719ad626a5a" - dependencies: - hoek "5.x.x" - mime-db "1.x.x" - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -nanoid@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-1.1.0.tgz#b18e806e1cdbfdbe030374d5cf08a48cbc80b474" - -nigel@3.x.x: - version "3.0.1" - resolved "https://registry.yarnpkg.com/nigel/-/nigel-3.0.1.tgz#48a08859d65177312f1c25af7252c1e07bb07c2a" - dependencies: - hoek "5.x.x" - vise "3.x.x" - -node-ensure@^0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7" - -node-fetch@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.2.0.tgz#4ee79bde909262f9775f731e3656d0db55ced5b5" - -normalize-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -once@^1.3.0, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -pdfjs-dist@^2.0.489: - version "2.0.489" - resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.0.489.tgz#63e54b292a86790a454697eb44d4347b8fbfad27" - dependencies: - node-ensure "^0.0.0" - worker-loader "^1.1.1" - -pez@4.x.x: - version "4.0.2" - resolved "https://registry.yarnpkg.com/pez/-/pez-4.0.2.tgz#0a7c81b64968e90b0e9562b398f390939e9c4b53" - dependencies: - b64 "4.x.x" - boom "7.x.x" - content "4.x.x" - hoek "5.x.x" - nigel "3.x.x" - -podium@3.x.x: - version "3.1.2" - resolved "https://registry.yarnpkg.com/podium/-/podium-3.1.2.tgz#b701429739cf6bdde6b3015ae6b48d400817ce9e" - dependencies: - hoek "5.x.x" - joi "13.x.x" - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -punycode@2.x.x, punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - -readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.1, readable-stream@^3.1.1, readable-stream@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" - integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@~5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== - -schema-utils@^0.4.0: - version "0.4.5" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e" - dependencies: - ajv "^6.1.0" - ajv-keywords "^3.1.0" - -shot@4.x.x: - version "4.0.5" - resolved "https://registry.yarnpkg.com/shot/-/shot-4.0.5.tgz#c7e7455d11d60f6b6cd3c43e15a3b431c17e5566" - dependencies: - hoek "5.x.x" - joi "13.x.x" - -statehood@6.x.x: - version "6.0.6" - resolved "https://registry.yarnpkg.com/statehood/-/statehood-6.0.6.tgz#0dbd7c50774d3f61a24e42b0673093bbc81fa5f0" - dependencies: - boom "7.x.x" - bounce "1.x.x" - cryptiles "4.x.x" - hoek "5.x.x" - iron "5.x.x" - joi "13.x.x" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -subtext@6.x.x: - version "6.0.7" - resolved "https://registry.yarnpkg.com/subtext/-/subtext-6.0.7.tgz#8e40a67901a734d598142665c90e398369b885f9" - dependencies: - boom "7.x.x" - content "4.x.x" - hoek "5.x.x" - pez "4.x.x" - wreck "14.x.x" - -tar-stream@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3" - integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw== - dependencies: - bl "^3.0.0" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -teamwork@3.x.x: - version "3.0.1" - resolved "https://registry.yarnpkg.com/teamwork/-/teamwork-3.0.1.tgz#ff38c7161f41f8070b7813716eb6154036ece196" - -topo@3.x.x: - version "3.0.0" - resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.0.tgz#37e48c330efeac784538e0acd3e62ca5e231fe7a" - dependencies: - hoek "5.x.x" - -uri-js@^4.2.1: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - dependencies: - punycode "^2.1.0" - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -vise@3.x.x: - version "3.0.0" - resolved "https://registry.yarnpkg.com/vise/-/vise-3.0.0.tgz#76ad14ab31669c50fbb0817bc0e72fedcbb3bf4c" - dependencies: - hoek "5.x.x" - -worker-loader@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-1.1.1.tgz#920d74ddac6816fc635392653ed8b4af1929fd92" - dependencies: - loader-utils "^1.0.0" - schema-utils "^0.4.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -wreck@14.x.x: - version "14.0.2" - resolved "https://registry.yarnpkg.com/wreck/-/wreck-14.0.2.tgz#89c17a9061c745ed1c3aebcb66ea181dbaab454c" - dependencies: - boom "7.x.x" - hoek "5.x.x" - -zip-stream@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-2.1.2.tgz#841efd23214b602ff49c497cba1a85d8b5fbc39c" - integrity sha512-ykebHGa2+uzth/R4HZLkZh3XFJzivhVsjJt8bN3GvBzLaqqrUdRacu+c4QtnUgjkkQfsOuNE1JgLKMCPNmkKgg== - dependencies: - archiver-utils "^2.1.0" - compress-commons "^2.1.1" - readable-stream "^3.4.0"