Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1dd8fc604 | |||
| 24395a1ddd | |||
| 02a3396b5f | |||
| 64ae78ad7a | |||
| 00000344bb |
@@ -1,10 +0,0 @@
|
||||
node_modules/
|
||||
*.code-workspace
|
||||
.idea/
|
||||
dist/
|
||||
android/
|
||||
ios/
|
||||
releases/
|
||||
.vscode/
|
||||
.env.local
|
||||
/server-node/jobs
|
||||
@@ -1,35 +0,0 @@
|
||||
# Contribute
|
||||
|
||||
This file should introduce you with the concepts and tools used in this project.
|
||||
|
||||
## Basic Setup
|
||||
|
||||
- Install/Update **Node (v22.2.0, [nvm](https://github.com/coreybutler/nvm-windows))** & NPM(>10.2.1)
|
||||
- To install all dependecies `npm run update-all-dependencies` (in [root](/))
|
||||
- To test your current setup and boot a complete install of spdf v2 run `npm run dev-all` (in [root](/))
|
||||
|
||||
## Nomenclature
|
||||
|
||||
- API - Probably refers to the “normal“ API of spdf v2 without workflows unless otherwise noted.
|
||||
- Workflow - Either the express-endpoint for running workflows or the user defined workflow-json
|
||||
- Action - A sub-element of a workflow describing what operation should run on the inputted file / output of the last action.
|
||||
- Operation - The actual function that will run on the pdf, including parameters.
|
||||
- Operator - The actual code/implementation of the Operation (e.g. impose.ts) OR The parent class of every Operator.
|
||||
- Validator - A function that makes sure things are as they should be. Every Operator must have one.
|
||||
- Decorator - Explanations and Human Readable names of fields, these will be displayed in the frontend and used to provide better errors for the (workflow-)API
|
||||
|
||||
## Folder structure
|
||||
|
||||
- client-tauri - The frontend - Can be built to web and to a desktop app (with extra functions) using tauri.
|
||||
- server-node - The backend - Provides extra functionality for the web client.
|
||||
- shared-operatons - Components (e.g. Operators) that are shared between frontend and backend.
|
||||
|
||||
## Adding a PDF Operator
|
||||
|
||||
An Operator is either shared by the server and the client or it might have different implementations based on if its executed by the client, desktop-backend or web-backend. The current structure allows us to define where the Operator can be run.
|
||||
|
||||
## PDF Library Docs
|
||||
- [pdf-lib](https://pdf-lib.js.org) - js
|
||||
- [mozilla's pdfjs-dist/pdf.js](https://www.npmjs.com/package/pdfjs-dist) - js
|
||||
- [pdfcpu](https://pdfcpu.io) - go-wasm
|
||||
- [opencv-wasm](https://www.npmjs.com/package/opencv-wasm) - c++-wasm
|
||||
@@ -1,159 +1,20 @@
|
||||
# StirlingPDF rewrite
|
||||
|
||||
This is the development repository for the new StirlingPDF backend. With the power of JS, WASM & GO this will provide almost all functionality SPDF can do currently directly on the client. For automation purposes this will still provide an API to automate your workflows.
|
||||
|
||||

|
||||
This image is here to reflect current progress and will be updated accordingly.
|
||||
|
||||
## Try the new API!
|
||||
|
||||
[](https://documenter.getpostman.com/view/30633786/2s9YRB1Wto)
|
||||
|
||||
## Understanding Workflows
|
||||
|
||||
Workflows define how to apply operations to a PDF, including their order and relations with eachother.
|
||||
|
||||
Workflows can be created via the web-ui and then exported or, if you want to brag a bit, you can create the JSON object yourself.
|
||||
|
||||
### Basics
|
||||
|
||||
To create your own, you have to understand a few key features first. You can also look at more examples our github repository.
|
||||
|
||||
```json
|
||||
{
|
||||
"outputOptions": {
|
||||
"zip": false
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "extract",
|
||||
"values": {
|
||||
"pageIndexes": [0, 2]
|
||||
},
|
||||
"actions": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The workflow above will extract the first (p\[0\]) and third (p\[2\]) page of the document.
|
||||
|
||||
You can also nest workflows like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"outputOptions": {
|
||||
"zip": false
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "extract",
|
||||
"values": {
|
||||
"pageIndexes": [0, 2]
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "impose",
|
||||
"values": {
|
||||
"nup": 2, // 2 pages of the input document will be put on one page of the output document.
|
||||
"format": "A4L" // A4L -> The page size of the Ouput will be an A4 in Landscape. You can also use other paper formats and "P" for portrait output.
|
||||
},
|
||||
"actions": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If you look at it closely, you will see that the extract operation has another nested operation of the type impose. This workflow will produce a PDF with the 1st and 2nd page of the input on one single page.
|
||||
|
||||
### Advanced
|
||||
|
||||
If that is not enought for you usecase, there is also the possibility to connect operations with eachother.
|
||||
|
||||
You can also do different operations to produce two different output PDFs from one input.
|
||||
|
||||
If you are interested in learning about this, take a look at the Example workflows provided in the repository, ask on the discord, or wait for me to finish this documentation.
|
||||
|
||||
## Features
|
||||
|
||||
### Rewrite Roadmap
|
||||
|
||||
* [X] Client side PDF-Manipulation
|
||||
* [X] Workflows
|
||||
* [X] passportjs backend (auth)
|
||||
* [ ] Auth in frontend
|
||||
* [ ] Feature equivalent with S-PDF v1
|
||||
* [ ] Stateful UI
|
||||
* [ ] Node based editing of Workflows
|
||||
|
||||
### Functions
|
||||
|
||||
Current functions of spdf and their progress in this repo.
|
||||
|
||||
#### PDF Functions
|
||||
|
||||
| Status | Feature | Description |
|
||||
| ------ | -------------------------------------------------- | ----------- |
|
||||
| ✔️ | arrange | |
|
||||
| ✔️ | extract | |
|
||||
| ✔️ | impose | |
|
||||
| ✔️ | merge | |
|
||||
| ✔️ | remove blank | |
|
||||
| ✔️ | remove | |
|
||||
| ✔️ | rotate pages | |
|
||||
| ✔️ | scale content | |
|
||||
| ✔️ | scale pages | |
|
||||
| ✔️ | split by preset | |
|
||||
| ✔️ | split by index | |
|
||||
| ✔️ | update metadata | |
|
||||
| ✔️ | pdf to single large page | |
|
||||
| 🚧 | remove annotations | |
|
||||
| 🚧 | flatten | |
|
||||
| 🚧 | overlay pdfs | |
|
||||
| 🚧 | compress | |
|
||||
| 🚧 | change permissions | |
|
||||
| 🚧 | pdf to pdf/a | |
|
||||
| 🚧 | add page numbers | |
|
||||
| 🚧 | add image | |
|
||||
| 🚧 | add watermark | |
|
||||
| 🚧 | auto rename | |
|
||||
| 🚧 | add stamp | |
|
||||
| ❌ | repair | |
|
||||
| ❌ | sign with cert | |
|
||||
| ❌ | ocr | |
|
||||
| ❌ | auto split by size/count (+split by preset) | |
|
||||
| ❌ | split pdfs by sections/chapters (+split by preset) | |
|
||||
| ❌ | adjust colors/contrast | |
|
||||
| ❌ | adjust colors/contrast | |
|
||||
| ❌ | sanitize | |
|
||||
| ❌ | sign | |
|
||||
| ❌ | basic text editing | |
|
||||
| ❌ | auto redact | |
|
||||
|
||||
#### Generic Filetype (Filetypes are not supported by workflows yet. Coming Soon™)
|
||||
|
||||
| Status | Feature | Description |
|
||||
| ------ | ------------------- | ----------- |
|
||||
| 🚧 | image to pdf | |
|
||||
| 🚧 | pdf to image | |
|
||||
| 🚧 | extract images | |
|
||||
| 🚧 | show javascript | |
|
||||
| ❌ | convert file to pdf | |
|
||||
| ❌ | pdf to word | |
|
||||
| ❌ | pdf to presentation | |
|
||||
| ❌ | pdf to rtf | |
|
||||
| ❌ | pdf to html | |
|
||||
| ❌ | pdf to xml | |
|
||||
| ❌ | url/website to pdf | |
|
||||
| ❌ | markdown to pdf | |
|
||||
| ❌ | pdf to csv | |
|
||||
| ❌ | get all info | |
|
||||
| ❌ | compare | |
|
||||
|
||||
✔️: Done, 🚧: Possible with current Libraries, ❌: Planned Feature
|
||||
|
||||
## Contribute
|
||||
|
||||
For initial instructions look at [CONTRIBUTE.md](./CONTRIBUTE.md)
|
||||
## Usage
|
||||
|
||||
[Helm](https://helm.sh) must be installed to use the charts. Please refer to
|
||||
Helm's [documentation](https://helm.sh/docs) to get started.
|
||||
|
||||
Once Helm has been set up correctly, add the repo as follows:
|
||||
|
||||
`helm repo add <alias> https://docs.stirlingpdf.com/Stirling-PDF`
|
||||
|
||||
If you had already added this repo earlier, run `helm repo update` to retrieve
|
||||
the latest versions of the packages. You can then run `helm search repo <alias>` to see the charts.
|
||||
|
||||
To install the <chart-name> chart:
|
||||
|
||||
helm install <chart-name> <alias>/<chart-name>
|
||||
|
||||
To uninstall the chart:
|
||||
|
||||
helm delete <chart-name>
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
VITE_USE_AUTH=False
|
||||
VITE_BACKEND=""
|
||||
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# Use an existing image as a base
|
||||
FROM node:22.2.0-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --legacy-peer-deps
|
||||
|
||||
RUN npm i -g serve
|
||||
|
||||
COPY ./dist ./dist
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD [ "serve", "-s", "dist", "-p", "3000" ]
|
||||
@@ -1,13 +0,0 @@
|
||||
vite-build:
|
||||
npx tsc
|
||||
npx vite build
|
||||
|
||||
vite-dockerize:
|
||||
make vite-build
|
||||
bash -c "cp ../package-lock.json ./"
|
||||
docker build . -t stirling-pdf:latest
|
||||
bash -c "rm ./package-lock.json"
|
||||
docker image prune
|
||||
|
||||
tauri-build:
|
||||
npx tauri build
|
||||
@@ -1,11 +0,0 @@
|
||||
# Stirling-PDF frontend
|
||||
|
||||
Tauri + Vite + React + Typescript
|
||||
|
||||
## Development
|
||||
|
||||
Use the package scripts to start developement
|
||||
|
||||
## Production
|
||||
|
||||
Use the make file to build for the correct platform
|
||||
@@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/stirling-pdf-logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri + React + TS</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "@stirling-pdf/client-tauri",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"vite:dev": "vite",
|
||||
"vite:preview-prod": "make vite-build && vite preview",
|
||||
"tauri:build-debug": "npx tauri build --debug",
|
||||
"tauri:dev": "tauri dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stirling-pdf/shared-operations": "^0.0.0",
|
||||
"@tauri-apps/api": "^1.5.1",
|
||||
"@types/semver": "^7.5.8",
|
||||
"archiver": "^6.0.1",
|
||||
"i18next": "^23.6.0",
|
||||
"i18next-browser-languagedetector": "^7.1.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pdfjs-dist": "^4.0.189",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-material-symbols": "^4.4.0",
|
||||
"react-router-bootstrap": "^0.26.2",
|
||||
"react-router-dom": "^6.18.0",
|
||||
"vite": "^5.4.2",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"vite-plugin-top-level-await": "^1.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.5.0",
|
||||
"@types/archiver": "^6.0.2",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-router-bootstrap": "^0.26.5",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"typescript": "^5.0.2",
|
||||
"vite-plugin-compile-time": "^0.2.1",
|
||||
"vite-plugin-dynamic-import": "^1.5.0"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 132 KiB |
@@ -1,298 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="99.537987mm"
|
||||
height="95.209366mm"
|
||||
viewBox="0 0 99.537987 95.209366"
|
||||
version="1.1"
|
||||
id="svg745"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="stirling-transparent.svg"
|
||||
inkscape:export-filename="stirling.png"
|
||||
inkscape:export-xdpi="80"
|
||||
inkscape:export-ydpi="80"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview747"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.914906"
|
||||
inkscape:cx="175.42786"
|
||||
inkscape:cy="510.43495"
|
||||
inkscape:window-width="2256"
|
||||
inkscape:window-height="1428"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="40"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg745" /><defs
|
||||
id="defs742"><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient72198"><stop
|
||||
style="stop-color:#ccd6d7;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop72194" /><stop
|
||||
style="stop-color:#0f3a3f;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop72196" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient71928"><stop
|
||||
style="stop-color:#4b0005;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop71924" /><stop
|
||||
style="stop-color:#8f000c;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop71926" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient71920"><stop
|
||||
style="stop-color:#4b0005;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop71916" /><stop
|
||||
style="stop-color:#8f000c;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop71918" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient69598"><stop
|
||||
style="stop-color:#6a0007;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop69594" /><stop
|
||||
style="stop-color:#b8000f;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop69596" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient46361"><stop
|
||||
style="stop-color:#f7f6f8;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop46359" /><stop
|
||||
style="stop-color:#b7b7b5;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop46357" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient40554"><stop
|
||||
style="stop-color:#f4f2f4;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop40550" /><stop
|
||||
style="stop-color:#57767b;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop40552" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient39095"><stop
|
||||
style="stop-color:#285459;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop39093" /><stop
|
||||
style="stop-color:#a6b6af;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop39091" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient36672"><stop
|
||||
style="stop-color:#da453f;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop36668" /><stop
|
||||
style="stop-color:#a60008;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop36670" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient19524"><stop
|
||||
style="stop-color:#3a4b4f;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop19522" /><stop
|
||||
style="stop-color:#617979;stop-opacity:0.97461927;"
|
||||
offset="1"
|
||||
id="stop19520" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient17996"><stop
|
||||
style="stop-color:#401016;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop17994" /><stop
|
||||
style="stop-color:#761f19;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop17992" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient15569"><stop
|
||||
style="stop-color:#8ea8ad;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop15565" /><stop
|
||||
style="stop-color:#e9e7eb;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop15567" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient15557"><stop
|
||||
style="stop-color:#9b0e11;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop15553" /><stop
|
||||
style="stop-color:#370707;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop15555" /></linearGradient><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient15557"
|
||||
id="linearGradient15559"
|
||||
x1="120.06672"
|
||||
y1="63.25761"
|
||||
x2="135.16347"
|
||||
y2="78.078682"
|
||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient15569"
|
||||
id="linearGradient15571"
|
||||
x1="127.97037"
|
||||
y1="101.66144"
|
||||
x2="133.88971"
|
||||
y2="104.77026"
|
||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient17996"
|
||||
id="linearGradient17998"
|
||||
x1="117.9284"
|
||||
y1="86.055084"
|
||||
x2="130.67392"
|
||||
y2="76.945541"
|
||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient19524"
|
||||
id="linearGradient19528"
|
||||
x1="130.98172"
|
||||
y1="82.402977"
|
||||
x2="135.72115"
|
||||
y2="86.45166"
|
||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient36672"
|
||||
id="linearGradient36674"
|
||||
x1="63.797714"
|
||||
y1="74.81752"
|
||||
x2="96.636673"
|
||||
y2="120.29293"
|
||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient39095"
|
||||
id="linearGradient39097"
|
||||
x1="120.54506"
|
||||
y1="124.76902"
|
||||
x2="128.04152"
|
||||
y2="126.0704"
|
||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient40554"
|
||||
id="linearGradient40556"
|
||||
x1="113.84585"
|
||||
y1="114.04285"
|
||||
x2="119.65858"
|
||||
y2="128.50244"
|
||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient46361"
|
||||
id="linearGradient46363"
|
||||
x1="73.993439"
|
||||
y1="114.13906"
|
||||
x2="94.845322"
|
||||
y2="71.247383"
|
||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient69598"
|
||||
id="linearGradient69600"
|
||||
x1="95.854446"
|
||||
y1="114.66749"
|
||||
x2="103.77842"
|
||||
y2="120.1887"
|
||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient71920"
|
||||
id="linearGradient71922"
|
||||
x1="98.580376"
|
||||
y1="87.186958"
|
||||
x2="118.09738"
|
||||
y2="101.19449"
|
||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient71928"
|
||||
id="linearGradient71930"
|
||||
x1="78.278267"
|
||||
y1="97.433273"
|
||||
x2="92.313202"
|
||||
y2="104.33479"
|
||||
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient72198"
|
||||
id="linearGradient72200"
|
||||
x1="125.76636"
|
||||
y1="138.46817"
|
||||
x2="123.3327"
|
||||
y2="126.03291"
|
||||
gradientUnits="userSpaceOnUse" /></defs><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer5"
|
||||
inkscape:label="shadow"
|
||||
style="display:inline"
|
||||
sodipodi:insensitive="true"
|
||||
transform="translate(-51.420144,-44.470286)"><path
|
||||
style="display:inline;fill:url(#linearGradient72200);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 84.146049,134.73858 c 0,0 11.721038,2.48294 17.938661,2.91673 6.21763,0.43378 14.75251,0.59994 22.41237,-0.43379 8.01008,-1.081 13.19907,-2.22733 14.50043,-2.66112 1.30136,-0.43379 4.00784,-1.24297 4.15244,-2.25514 0.1446,-1.01217 -3.4703,-2.74733 -6.21763,-3.32571 -2.74732,-0.57838 -12.72444,-1.44596 -14.89337,-1.44596 -2.16894,0 -37.892901,7.20499 -37.892901,7.20499 z"
|
||||
id="path72192"
|
||||
sodipodi:nodetypes="cssssssc" /></g><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Origami"
|
||||
style="display:inline"
|
||||
sodipodi:insensitive="true"
|
||||
transform="translate(-51.420144,-44.470286)"><path
|
||||
style="display:inline;fill:url(#linearGradient40556);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 84.460552,134.86721 35.165798,-6.85679 16.15467,-42.7383 z"
|
||||
id="path960"
|
||||
sodipodi:nodetypes="cccc" /><path
|
||||
style="fill:url(#linearGradient15571);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 135.71493,85.428056 0.3984,45.049024 -9.66213,-20.46173 z"
|
||||
id="path964"
|
||||
sodipodi:nodetypes="cccc" /><path
|
||||
style="display:inline;fill:url(#linearGradient39097);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 119.60518,128.00293 16.5337,2.48693 -9.68769,-20.5512 z"
|
||||
id="path966"
|
||||
sodipodi:nodetypes="cccc" /><path
|
||||
style="display:inline;fill:url(#linearGradient15559);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 118.42209,57.022622 12.70423,-2.766809 -0.0785,25.087175 -12.43878,-13.376518 z"
|
||||
id="path968"
|
||||
sodipodi:nodetypes="ccccc" /><path
|
||||
style="display:inline;fill:url(#linearGradient19528);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 135.72114,85.386768 -4.84219,-6.459493 0.0305,11.126604 z"
|
||||
id="path970"
|
||||
sodipodi:nodetypes="cccc" /><path
|
||||
style="display:inline;fill:url(#linearGradient17998);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 119.10273,65.682415 11.96883,13.44935 -0.0899,10.819868 -11.88577,11.430427 z"
|
||||
id="path972"
|
||||
sodipodi:nodetypes="ccccc" /><path
|
||||
style="display:inline;fill:url(#linearGradient36674);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 62.145635,130.15618 62.043392,65.435258 c 0,0 0.179321,-2.778132 1.89516,-4.036097 1.874923,-1.374597 4.341768,-1.894096 4.341768,-1.894096 l 50.91788,-11.349167 -0.0113,53.144272 -34.733274,33.56547 z"
|
||||
id="path958"
|
||||
sodipodi:nodetypes="ccsccccc" /></g><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Letter"
|
||||
style="display:inline"
|
||||
sodipodi:insensitive="true"
|
||||
transform="translate(-51.420144,-44.470286)"><path
|
||||
style="display:inline;fill:url(#linearGradient69600);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 94.780881,91.406803 16.870379,17.074877 -23.723345,23.00249 -18.122131,-17.99816 5.497473,-2.80607 18.404054,-0.0511 2.35163,-8.23071 z"
|
||||
id="path54894"
|
||||
sodipodi:nodetypes="cccccccc" /><path
|
||||
style="display:inline;fill:url(#linearGradient71930);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 72.440405,92.224764 16.15467,15.745686 4.089788,-6.79927 z"
|
||||
id="path54892" /><path
|
||||
style="display:inline;fill:url(#linearGradient71922);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 95.138739,84.965385 1.124691,-14.109776 22.92453,22.286787 0.008,8.164604 -3.28863,3.16649 z"
|
||||
id="path54890"
|
||||
sodipodi:nodetypes="cccccc"
|
||||
inkscape:label="path54890" /><path
|
||||
style="display:inline;fill:url(#linearGradient46363);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 95.138739,84.965385 h 1.226936 l -0.05112,-14.109776 c 0,0 -5.776827,-3.220709 -12.167126,-2.40275 -6.390296,0.817957 -8.151582,2.1248 -10.58233,4.396523 -1.90229,1.777838 -2.913974,3.527446 -3.987546,7.157132 -0.512646,1.733226 -0.281963,5.988892 0.613471,8.537436 0.664591,1.891528 2.453873,4.294281 4.958868,6.134686 2.662335,1.956002 8.281825,3.527443 8.281825,3.527443 0,0 5.134614,1.887351 5.572338,4.294281 0.308137,1.69437 -0.102243,3.22071 -1.635914,4.95887 -1.258314,1.42609 -3.62969,1.99377 -6.288054,1.07357 -2.658364,-0.92021 -6.139514,-3.85065 -7.106009,-4.90775 -0.817958,-0.89464 -2.820665,-3.06173 -2.890231,-4.294021 -0.01209,-0.214205 -1.229505,-0.0963 -1.229505,-0.0963 l -0.0723,14.256941 5.879073,2.24938 c 0,0 5.214483,1.78929 8.946415,1.43143 3.731934,-0.35786 7.617235,-0.51122 11.604778,-5.16336 3.987542,-4.65213 3.680812,-12.831715 2.147141,-15.899056 -1.533673,-3.067344 -3.561212,-6.138812 -10.480087,-8.281826 -5.776829,-1.789283 -7.872846,-3.01622 -8.128458,-4.396524 -0.255611,-1.380305 0.0091,-4.253646 2.760607,-5.214481 3.220711,-1.124693 5.623462,-0.05112 7.05489,1.12469 1.431425,1.175817 5.572339,5.623462 5.572339,5.623462 z"
|
||||
id="path46355"
|
||||
sodipodi:nodetypes="cccssssscssssscccssssssscc" /></g></svg>
|
||||
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,4 +0,0 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
[package]
|
||||
name = "StirlingPDF"
|
||||
version = "2.0.0" # needed for dev build?
|
||||
description = "Selfhosted PDF processing"
|
||||
authors = ["LaserKaspar, SaudF"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.5", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.5", features = [ "fs-remove-dir", "fs-create-dir", "shell-all", "fs-write-file", "fs-read-file", "dialog-save", "dialog-open"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||
# DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 87 KiB |
@@ -1,15 +0,0 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![greet])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
{
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run vite:dev",
|
||||
"beforeBuildCommand": "make vite-build",
|
||||
"devPath": "http://localhost:1420",
|
||||
"distDir": "../dist",
|
||||
"withGlobalTauri": false
|
||||
},
|
||||
"package": {
|
||||
"productName": "StirlingPDF",
|
||||
"version": "2.0.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"shell": {
|
||||
"all": true,
|
||||
"open": true,
|
||||
"scope": [
|
||||
{
|
||||
"name": "libreoffice-version",
|
||||
"cmd": "libreoffice",
|
||||
"args": ["--version"]
|
||||
},{
|
||||
"name": "libreoffice-convert",
|
||||
"cmd": "libreoffice",
|
||||
"args": ["--headless","--convert-to",{ "validator": "\\S+" },{ "validator": "\\S+" },"--outdir",{ "validator": "\\S+" }]
|
||||
}
|
||||
]
|
||||
},
|
||||
"dialog": {
|
||||
"all": false,
|
||||
"ask": false,
|
||||
"confirm": false,
|
||||
"message": false,
|
||||
"open": true,
|
||||
"save": true
|
||||
},
|
||||
"fs": {
|
||||
"all": false,
|
||||
"readFile": true,
|
||||
"writeFile": true,
|
||||
"readDir": false,
|
||||
"copyFile": false,
|
||||
"createDir": true,
|
||||
"removeDir": true,
|
||||
"removeFile": false,
|
||||
"renameFile": false,
|
||||
"exists": false
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "com.stirlingtools.pdf",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"category": "Business"
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
"resizable": true,
|
||||
"title": "StirlingPDF",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { Fragment, Suspense } from "react";
|
||||
|
||||
import { Routes, Route, Outlet, Navigate } from "react-router-dom";
|
||||
import Home from "./pages/Home";
|
||||
import Operators from "./pages/Operators";
|
||||
import NoMatch from "./pages/NoMatch";
|
||||
import NavBar from "./components/NavBar";
|
||||
|
||||
import { useTranslation, initReactI18next } from "react-i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
import i18next from "i18next";
|
||||
import resourcesToBackend from "i18next-resources-to-backend";
|
||||
import { listOperatorNames } from "@stirling-pdf/shared-operations/src/workflow/operatorAccessor";
|
||||
import AuthenticatedRoute from "./components/AuthenticatedRoute";
|
||||
import Login from "./pages/Auth/Login";
|
||||
import Logout from "./pages/Auth/Logout";
|
||||
import Register from "./pages/Auth/Register";
|
||||
|
||||
i18next.use(LanguageDetector).use(initReactI18next).use(resourcesToBackend((language: string, namespace: string) => import(`@stirling-pdf/shared-operations/public/locales/${namespace}/${language}.json`).catch((e) => console.warn("some component tried to render with an unsupported language, falling back to en", e))))
|
||||
.init({
|
||||
debug: false,
|
||||
ns: ["common"], // Preload this namespace, no need to add the others, they will load once their module is loaded
|
||||
defaultNS: "common",
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
initImmediate: false // Makes loading blocking but sync
|
||||
}); // TODO: use i18next.config.ts instead
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Suspense fallback={<Loading/>}>
|
||||
{/* Routes nest inside one another. Nested route paths build upon
|
||||
parent route paths, and nested route elements render inside
|
||||
parent route elements. See the note about <Outlet> below. */}
|
||||
<Routes>
|
||||
<Route path="/auth" element={<Layout />}>
|
||||
<Route index element={<Navigate to="/auth/login" />}/>
|
||||
<Route path="login" element={<Login />}></Route>
|
||||
<Route path="logout" element={<Logout />}></Route>
|
||||
<Route path="register" element={<Register />}></Route>
|
||||
<Route path="*" element={<NoMatch />} />
|
||||
</Route>
|
||||
|
||||
<Route element={<AuthenticatedRoute />}>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Home />} />
|
||||
<Route path="*" element={<NoMatch />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/operators" element={<Layout />}>
|
||||
<Route index element={<NoMatch />} />
|
||||
{listOperatorNames().map((name) => {
|
||||
return <Route key={name} path={name} element={<Operators/>} />;
|
||||
})}
|
||||
<Route path="*" element={<NoMatch />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
return "Loading";
|
||||
}
|
||||
|
||||
function Layout() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div lang-direction={t("language.direction")}>
|
||||
<NavBar/>
|
||||
|
||||
{/* An <Outlet> renders whatever child route is currently active,
|
||||
so you can think about this <Outlet> as a placeholder for
|
||||
the child routes we defined above. */}
|
||||
<Outlet/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 512 512"
|
||||
style="enable-background:new 0 0 512 512;"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="favicon.svg"
|
||||
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||
inkscape:export-filename="favicon.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs173">
|
||||
|
||||
|
||||
<linearGradient
|
||||
id="XMLID_5_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="304.496"
|
||||
y1="422.9102"
|
||||
x2="316.036"
|
||||
y2="326.2626">
|
||||
<stop
|
||||
offset="0"
|
||||
style="stop-color:#DCF1F3"
|
||||
id="stop156" />
|
||||
<stop
|
||||
offset="1"
|
||||
style="stop-color:#C2C2C9"
|
||||
id="stop158" />
|
||||
</linearGradient>
|
||||
|
||||
</defs><sodipodi:namedview
|
||||
id="namedview171"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.4142136"
|
||||
inkscape:cx="219.91021"
|
||||
inkscape:cy="232.63813"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2054"
|
||||
inkscape:window-x="2869"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="XMLID_4_" />
|
||||
<style
|
||||
type="text/css"
|
||||
id="style150">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#C02223;}
|
||||
.st2{fill:#882425;}
|
||||
.st3{fill:url(#XMLID_5_);}
|
||||
.st4{fill:url(#XMLID_7_);}
|
||||
</style>
|
||||
|
||||
<g
|
||||
id="XMLID_4_">
|
||||
<path
|
||||
id="XMLID_131_"
|
||||
class="st1"
|
||||
d="M 347.01402,14.355825 98.978019,69.02261 C 73.825483,74.547445 55.942464,96.792175 55.942464,122.52628 v 315.06096 c 0,22.39012 16.719895,41.14548 38.819234,43.76251 L 224.8861,498.36042 339.48636,384.26465 455.76603,265.15425 453.73057,84.870162 C 453.43979,62.916214 433.08513,46.632491 411.71274,51.284984 l -28.78729,6.251786 0.14539,-13.666697 C 383.36162,24.678542 365.62399,10.284894 347.01402,14.355825 Z"
|
||||
sodipodi:nodetypes="ccssccccccccc"
|
||||
style="stroke-width:1.45391" /><path
|
||||
id="XMLID_117_"
|
||||
class="st2"
|
||||
d="m 383.21622,57.53677 v 285.8375 L 456.05681,265.00885 454.02135,78.763767 C 453.87595,59.863016 436.28372,45.905539 417.81914,49.97647 Z"
|
||||
style="stroke-width:1.45391" /><polygon
|
||||
id="XMLID_18_"
|
||||
class="st3"
|
||||
points="234.7,422.6 368.5,387.7 393.5,262.2 "
|
||||
style="fill:url(#XMLID_5_)"
|
||||
transform="matrix(1.4556308,0,0,1.4548265,-116.73161,-116.45231)" />
|
||||
<linearGradient
|
||||
id="XMLID_7_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="223.0838"
|
||||
y1="372.7559"
|
||||
x2="241.4174"
|
||||
y2="114.557"
|
||||
gradientTransform="matrix(1.4539039,0,0,1.4539039,-116.19976,-116.20474)">
|
||||
<stop
|
||||
offset="0"
|
||||
style="stop-color:#DCF1F3"
|
||||
id="stop163" />
|
||||
<stop
|
||||
offset="1"
|
||||
style="stop-color:#C2C2C9"
|
||||
id="stop165" />
|
||||
</linearGradient>
|
||||
<path
|
||||
id="XMLID_6_"
|
||||
class="st4"
|
||||
d="m 282.89686,214.84917 c 0,0 -22.24473,-28.93269 -38.67384,-36.78377 -10.46811,-4.94327 -26.02489,-6.83335 -38.23768,-0.72695 -18.02841,9.0142 -19.91848,34.31213 -3.34397,44.34406 3.92553,2.47165 9.15959,4.50711 15.99294,6.10641 36.63838,8.43264 97.12077,25.87949 89.70587,96.10304 0,0 -4.21633,65.86185 -73.56753,73.42215 -12.2128,1.30851 -24.57098,0.43617 -36.493,-2.32625 -16.42911,-3.63476 -45.50719,-11.04967 -59.75545,-19.91849 l -2.61703,-75.16682 h 6.97875 c 0,0 13.81208,33.43978 53.06749,49.57812 7.26952,2.90781 15.26599,4.07093 22.97168,2.90781 9.74116,-1.45391 21.22699,-6.68796 25.87949,-22.53551 0,0 7.85108,-23.11707 -32.85823,-35.76604 -32.56744,-10.17733 -63.24481,-20.64543 -75.89378,-54.95757 -5.961,-16.28371 -6.97874,-34.31212 -2.90781,-51.61358 5.37944,-22.53551 20.79082,-54.23062 64.40794,-67.89732 0,0 57.28381,-15.55677 96.53922,5.52484 l -1.74468,89.70587 z"
|
||||
style="fill:url(#XMLID_7_);stroke-width:1.45391" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -1,22 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
|
||||
interface AuthenticatedRouteProps {
|
||||
isAuthenticated: boolean;
|
||||
children?: ReactNode; // Accepting children
|
||||
}
|
||||
|
||||
|
||||
function isAuthenticated() {
|
||||
if (import.meta.env.VITE_USE_AUTH == "True") {
|
||||
// TODO: if user is set in localstorage and is valid (either by time or by checking online) return true
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function AuthenticatedRoute({}: {}): JSX.Element {
|
||||
return isAuthenticated() ? <Outlet /> : <Navigate to="/auth/login" />;
|
||||
};
|
||||
|
||||
export default AuthenticatedRoute;
|
||||
@@ -1,22 +0,0 @@
|
||||
.fields {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.fields input, select {
|
||||
width: 100%;
|
||||
padding: 12px 20px;
|
||||
margin: 8px 0;
|
||||
display: inline-block;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.submit {
|
||||
background-color: var(--md-sys-color-primary);;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 16px 32px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import Joi from "@stirling-tools/joi";
|
||||
import { GenericField } from "./fields/GenericField";
|
||||
import React from "react";
|
||||
|
||||
import styles from "./BuildForm.module.css";
|
||||
|
||||
interface BuildFormProps {
|
||||
/** The text to display inside the button */
|
||||
schemaDescription: Joi.Description | undefined;
|
||||
onSubmit: React.FormEventHandler<HTMLFormElement>;
|
||||
}
|
||||
|
||||
export function BuildForm({ schemaDescription, onSubmit }: BuildFormProps) {
|
||||
console.log("Render Build Fields", schemaDescription);
|
||||
const values = (schemaDescription?.keys as any)?.values.keys as { [key: string]: Joi.Description};
|
||||
return (
|
||||
<form onSubmit={(e) => { onSubmit(e); e.preventDefault(); }}>
|
||||
<div className={styles.fields}>
|
||||
{
|
||||
values ? Object.keys(values).map((key) => {
|
||||
return (<GenericField key={key} fieldName={key} joiDefinition={values[key]} />)
|
||||
}) : undefined
|
||||
}
|
||||
</div>
|
||||
<input className={styles.submit} type="submit" value="Submit" />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
.navbar_brand {
|
||||
color: var(--md-sys-color-on-surface) !important;
|
||||
}
|
||||
|
||||
.main_icon {
|
||||
width: 36px;
|
||||
height: 42px;
|
||||
vertical-align: middle;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.icon_text {
|
||||
margin-left: 8px;
|
||||
margin-right: 4px;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import StirlingLogo from "../assets/favicon.svg";
|
||||
import NavBarStyles from "./NavBar.module.css";
|
||||
|
||||
function NavBar() {
|
||||
return (
|
||||
<nav>
|
||||
<a className={NavBarStyles.navbar_brand} href="/">
|
||||
<img className={NavBarStyles.main_icon} src={StirlingLogo} alt="icon"/>
|
||||
<span className={NavBarStyles.icon_text}>Stirling PDF</span>
|
||||
</a>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export default NavBar;
|
||||
@@ -1,22 +0,0 @@
|
||||
.operator_card {
|
||||
border: 1px solid var(--md-sys-color-surface-5);
|
||||
border-radius: 1.75rem;
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background: var(--md-sys-color-surface-5);
|
||||
transition: transform 0.3s, border 0.3s;
|
||||
transform-origin: center center;
|
||||
outline: 0px solid transparent;
|
||||
}
|
||||
|
||||
.operator_card h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.operator_card:hover {
|
||||
cursor: pointer;
|
||||
transform: scale(1.08);
|
||||
box-shadow: var(--md-sys-elevation-2);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { getSchemaByName } from "@stirling-pdf/shared-operations/src/workflow/operatorAccessor";
|
||||
|
||||
import styles from './OperatorCard.module.css';
|
||||
import { MaterialSymbol, MaterialSymbolProps } from 'react-material-symbols';
|
||||
|
||||
interface OperatorCardProps {
|
||||
/** The text to display inside the button */
|
||||
operatorInternalName: string;
|
||||
}
|
||||
|
||||
export function OperatorCard({ operatorInternalName }: OperatorCardProps) {
|
||||
const [schema, setSchema] = useState<any>(undefined); // TODO: Type as joi type
|
||||
const [materialSymbolName, setMaterialSymbolName] = useState<MaterialSymbolProps["icon"]>("error");
|
||||
|
||||
useEffect(() => {
|
||||
getSchemaByName(operatorInternalName).then(schema => {
|
||||
if(schema) {
|
||||
setSchema(schema.schema);
|
||||
setMaterialSymbolName(schema.materialSymbolName || "error");
|
||||
}
|
||||
});
|
||||
}, [operatorInternalName]);
|
||||
|
||||
return (
|
||||
<a key={operatorInternalName} href={"/operators/" + operatorInternalName}>
|
||||
<div className={styles.operator_card}>
|
||||
<h3><MaterialSymbol icon={materialSymbolName} size={30} fill grade={-25} color='black'></MaterialSymbol> { schema?.describe().flags.label }</h3>
|
||||
{ schema?.describe().flags.description }
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
import Joi from "@stirling-tools/joi";
|
||||
import { Fragment } from "react";
|
||||
|
||||
interface GenericFieldProps {
|
||||
fieldName: string,
|
||||
joiDefinition: Joi.Description
|
||||
}
|
||||
|
||||
interface Flags {
|
||||
label: string,
|
||||
description: string,
|
||||
}
|
||||
|
||||
export function GenericField({ fieldName, joiDefinition }: GenericFieldProps) {
|
||||
const flags = joiDefinition.flags as Flags;
|
||||
|
||||
switch (joiDefinition.type) {
|
||||
case "number":
|
||||
var validValues = joiDefinition.allow;
|
||||
if(validValues) { // Restrained number input
|
||||
return (
|
||||
<Fragment>
|
||||
<label htmlFor={fieldName}>{flags.label}:</label>
|
||||
<input type="number" list={fieldName} name={fieldName}/>
|
||||
<datalist id={fieldName}>
|
||||
{joiDefinition.allow.map((e: string) => {
|
||||
return (<option key={e} value={e}/>)
|
||||
})}
|
||||
</datalist>
|
||||
<br/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
else { // Unrestrained number input
|
||||
// TODO: Check if integer or not.
|
||||
return (
|
||||
<Fragment>
|
||||
<label htmlFor={fieldName}>{flags.label}:</label>
|
||||
<input type="number" list={fieldName} name={fieldName}/>
|
||||
<br/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
if(joiDefinition.allow) { // Restrained text input
|
||||
return (
|
||||
<Fragment>
|
||||
<label htmlFor={fieldName}>{flags.label}:</label>
|
||||
<input type="text" list={fieldName} name={fieldName}/>
|
||||
<datalist id={fieldName}>
|
||||
{joiDefinition.allow.map((e: string) => {
|
||||
return (<option key={e} value={e}/>)
|
||||
})}
|
||||
</datalist>
|
||||
<br/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<Fragment>
|
||||
<label htmlFor={fieldName}>{flags.label}:</label>
|
||||
<input type="text" list={fieldName} name={fieldName}/>
|
||||
<br/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
break;
|
||||
case "comma_array":
|
||||
if(joiDefinition.items.length == 1) {
|
||||
const item: Joi.Description = joiDefinition.items[0];
|
||||
|
||||
if(item.type == "number") {
|
||||
const props: any = {};
|
||||
|
||||
item.rules.forEach((rule: { args: any, name: string}) => {
|
||||
|
||||
switch (rule.name) {
|
||||
case "integer":
|
||||
if(props.pattern) {
|
||||
return (<div>props.pattern was already set, this is not implemented.</div>);
|
||||
}
|
||||
props.pattern = `(\\d+)(,\\s*\\d+)*`;
|
||||
break;
|
||||
case "min":
|
||||
// TODO: Could validate this in frontend first.
|
||||
break;
|
||||
case "max":
|
||||
// TODO: Could validate this in frontend first.
|
||||
break;
|
||||
default:
|
||||
return (<div>comma_array, item rule {rule.name} is not implemented.</div>);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<label htmlFor={fieldName}>{flags.label}:</label>
|
||||
<input type="text" pattern={props.pattern} list={fieldName} name={fieldName}/>
|
||||
<br/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (<div>comma_array, other types than numbers are not implemented yet.</div>);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// TODO: Implement multiple items if necessary
|
||||
return (<div>comma_array, joi items are empty or bigger than one, this is not implemented</div>);
|
||||
}
|
||||
break;
|
||||
case "alternatives": // TODO: Better support this. It is currently used by ScaleContent (working) and SplitByPreset (incompatible, but with that operator it isn't even considered a field so we need a different schema for that)
|
||||
return (
|
||||
<Fragment>
|
||||
<label htmlFor={fieldName}>{flags.label}:</label>
|
||||
<input type="text" list={fieldName} name={fieldName}/>
|
||||
<br/>
|
||||
</Fragment>
|
||||
);
|
||||
case "boolean":
|
||||
return (
|
||||
<Fragment>
|
||||
<label htmlFor={fieldName}>{flags.label}:</label>
|
||||
<input type="checkbox" list={fieldName} name={fieldName}/>
|
||||
<br/>
|
||||
</Fragment>
|
||||
);
|
||||
case "date":
|
||||
return (
|
||||
<Fragment>
|
||||
<label htmlFor={fieldName}>{flags.label}:</label>
|
||||
<input type="date" list={fieldName} name={fieldName}/>
|
||||
<br/>
|
||||
</Fragment>
|
||||
);
|
||||
default:
|
||||
console.log(joiDefinition);
|
||||
return (<div>GenericField.tsx: <br/> "{fieldName}": requested type "{joiDefinition.type}" not found. Check console for further info.</div>)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
.custom_file_upload input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.custom_file_upload {
|
||||
border: 1px solid #ccc;
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { forwardRef, ForwardedRef, FormEvent } from 'react';
|
||||
|
||||
import styles from "./InputField.module.css";
|
||||
|
||||
function InputField(_props: {}, inputRef: ForwardedRef<HTMLInputElement>) {
|
||||
function onChange(e: FormEvent<HTMLInputElement>) {
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
if(files) {
|
||||
const filesArray: File[] = Array.from(files as any);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = filesArray[i];
|
||||
if(file) {
|
||||
console.log(file.name);
|
||||
}
|
||||
else
|
||||
throw new Error("This should not happen. Contact maintainers.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<label className={styles.custom_file_upload}>
|
||||
<input onChange={onChange} type="file" id="pdfFile" accept=".pdf" multiple ref={inputRef}/>
|
||||
Upload your PDF(s)!
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
export default forwardRef(InputField);
|
||||
@@ -1,30 +0,0 @@
|
||||
|
||||
// import NavDropdown from "react-bootstrap/NavDropdown";
|
||||
// import { useTranslation } from "react-i18next";
|
||||
// import { BsGlobe2 } from "react-icons/bs";
|
||||
|
||||
// function generateSublist() {
|
||||
// const { i18n } = useTranslation();
|
||||
// const out: JSX.Element[] = [];
|
||||
// const languages = i18n.options.resources;
|
||||
|
||||
// for (const key in languages) {
|
||||
// const lang: any = languages[key].translation;
|
||||
// const staticKey = key;
|
||||
// out.push((
|
||||
// <NavDropdown.Item key={"language-"+key} className="nav-icon" onClick={()=>i18n.changeLanguage(staticKey)}>
|
||||
// <span>{lang.language?.flag}</span>
|
||||
// <span>{lang.language?.name}</span>
|
||||
// </NavDropdown.Item>
|
||||
// ));
|
||||
// }
|
||||
// return <>{out}</>;
|
||||
// }
|
||||
|
||||
// export default function LanguagePicker() {
|
||||
// return (
|
||||
// <NavDropdown id="languages-dropdown" title={<><span className="nav-icon"><BsGlobe2/></span></>}>
|
||||
// {generateSublist()}
|
||||
// </NavDropdown>
|
||||
// );
|
||||
// }
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import App from "./App";
|
||||
import "./root.css";
|
||||
import 'react-material-symbols/rounded';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -1,13 +0,0 @@
|
||||
// TODO: Store user info in localstorage. Session cookie will be set automatically
|
||||
|
||||
// TODO: Check if user login is enabled on this server
|
||||
|
||||
import styles from './Auth.module.css';
|
||||
|
||||
function Login() {
|
||||
return (
|
||||
"Login Test Page"
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
||||
@@ -1,13 +0,0 @@
|
||||
// TODO: Delete user info in localstorage. Send request to logout endpoint -> session cookie will be removed automatically.
|
||||
|
||||
// TODO: Check if user login is enabled on this server
|
||||
|
||||
import styles from './Auth.module.css';
|
||||
|
||||
function Logout() {
|
||||
return (
|
||||
"Logout Test Page"
|
||||
);
|
||||
}
|
||||
|
||||
export default Logout;
|
||||
@@ -1,13 +0,0 @@
|
||||
// TODO: Register user and login in the same request.
|
||||
|
||||
// TODO: Check if user registration & login is enabled on this server
|
||||
|
||||
import styles from './Auth.module.css';
|
||||
|
||||
function Register() {
|
||||
return (
|
||||
"Register Test Page"
|
||||
);
|
||||
}
|
||||
|
||||
export default Register;
|
||||
@@ -1,32 +0,0 @@
|
||||
import { listOperatorNames } from "@stirling-pdf/shared-operations/src/workflow/operatorAccessor";
|
||||
import { OperatorCard } from "../components/OperatorCard";
|
||||
|
||||
|
||||
import styles from './Home.module.css';
|
||||
|
||||
function Home() {
|
||||
const operators = listOperatorNames();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>
|
||||
Stirling PDF
|
||||
</h1>
|
||||
<h2>
|
||||
Your locally hosted one-stop-shop for all your PDF needs
|
||||
</h2>
|
||||
|
||||
{/**TODO: Search bar */}
|
||||
|
||||
<div className={styles.operator_container}>
|
||||
{
|
||||
operators.map((operator) => {
|
||||
return (<OperatorCard key={operator} operatorInternalName={operator}></OperatorCard>)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Fragment } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
function NoMatch() {
|
||||
return (
|
||||
<Fragment>
|
||||
<h2>The Page you are trying to access does not exist.</h2>
|
||||
<p>
|
||||
<Link to="/">Go back home...</Link>
|
||||
</p>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default NoMatch;
|
||||
@@ -1,98 +0,0 @@
|
||||
import { Fragment, useEffect, useRef } from "react";
|
||||
|
||||
import { BaseSyntheticEvent, useState } from "react";
|
||||
import { BuildForm } from "../components/BuildForm";
|
||||
import { getOperatorByName, getSchemaByName } from "@stirling-pdf/shared-operations/src/workflow/operatorAccessor";
|
||||
import { PdfFile, RepresentationType } from "@stirling-pdf/shared-operations/src/wrappers/PdfFile";
|
||||
import { Action } from "@stirling-pdf/shared-operations/declarations/Action";
|
||||
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import InputField from "../components/fields/InputField";
|
||||
|
||||
|
||||
function Operators() {
|
||||
const [schema, setSchema] = useState<any>(undefined); // TODO: Type as joi type
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const operatorInternalName = location.pathname.split("/")[2]; // /operators/<operatorInternalName>
|
||||
|
||||
useEffect(() => {
|
||||
getSchemaByName(operatorInternalName).then(schema => {
|
||||
if(schema) {
|
||||
setSchema(schema.schema);
|
||||
}
|
||||
});
|
||||
}, [location]);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
async function handleSubmit(e: BaseSyntheticEvent) {
|
||||
const formData = new FormData(e.target);
|
||||
const values = Object.fromEntries(formData.entries());
|
||||
let action: Action = {type: operatorInternalName, values: values};
|
||||
|
||||
|
||||
// Validate PDF File
|
||||
|
||||
// Createing the pdffile before validation because joi cant handle it for some reason and I can't fix the underlying issue / I want to make progress, wasted like 3 hours on this already. TODO: The casting should be done in JoiPDFFileSchema.ts if done correctly...
|
||||
const files = inputRef.current?.files;
|
||||
const inputs: PdfFile[] = [];
|
||||
|
||||
if(files) {
|
||||
const filesArray: File[] = Array.from(files as any);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = filesArray[i];
|
||||
if(file) {
|
||||
inputs.push(new PdfFile(
|
||||
file.name.replace(/\.[^/.]+$/, ""), // Strip Extension
|
||||
new Uint8Array(await file.arrayBuffer()),
|
||||
RepresentationType.Uint8Array
|
||||
));
|
||||
}
|
||||
else
|
||||
throw new Error("This should not happen. Contact maintainers.");
|
||||
}
|
||||
}
|
||||
|
||||
const validationResults = schema.validate({input: inputs, values: action.values});
|
||||
|
||||
if(validationResults.error) {
|
||||
console.error({error: "Validation failed", details: validationResults.error.message}, validationResults.error.stack);
|
||||
}
|
||||
else {
|
||||
action.values = validationResults.value.values;
|
||||
const Operator = (await getOperatorByName(operatorInternalName))!;
|
||||
|
||||
const operation = new Operator(action);
|
||||
operation.run(validationResults.value.input, (progress) => {
|
||||
console.log("OperationProgress: " + progress.operationProgress, "CurFileProgress: " + progress.curFileProgress);
|
||||
}).then(async pdfFiles => {
|
||||
console.log("Result", pdfFiles);
|
||||
|
||||
for await (const pdfFile of (pdfFiles as PdfFile[])) {
|
||||
var blob = new Blob([await pdfFile.uint8Array], {type: "application/pdf"});
|
||||
var objectUrl = URL.createObjectURL(blob);
|
||||
window.open(objectUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<h1>{ schema?.describe().flags.label }</h1>
|
||||
<h2>{ schema?.describe().flags.description }</h2>
|
||||
|
||||
<InputField ref={inputRef} />
|
||||
|
||||
<div id="values">
|
||||
<BuildForm schemaDescription={schema?.describe()} onSubmit={handleSubmit}></BuildForm>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default Operators;
|
||||
@@ -1,5 +0,0 @@
|
||||
.operator_container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 3fr));
|
||||
gap: 30px 30px;
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
:root {
|
||||
--md-sys-color-primary: rgb(0 96 170);
|
||||
--md-sys-color-surface-tint: rgb(0 96 170);
|
||||
--md-sys-color-on-primary: rgb(255 255 255);
|
||||
--md-sys-color-primary-container: rgb(80 163 255);
|
||||
--md-sys-color-on-primary-container: rgb(0 20 43);
|
||||
--md-sys-color-secondary: rgb(65 96 136);
|
||||
--md-sys-color-on-secondary: rgb(255 255 255);
|
||||
--md-sys-color-secondary-container: rgb(188 215 255);
|
||||
--md-sys-color-on-secondary-container: rgb(32 65 103);
|
||||
--md-sys-color-tertiary: rgb(88 90 138);
|
||||
--md-sys-color-on-tertiary: rgb(255 255 255);
|
||||
--md-sys-color-tertiary-container: rgb(151 153 205);
|
||||
--md-sys-color-on-tertiary-container: rgb(7 9 55);
|
||||
--md-sys-color-error: rgb(186 26 26);
|
||||
--md-sys-color-on-error: rgb(255 255 255);
|
||||
--md-sys-color-error-container: rgb(255 218 214);
|
||||
--md-sys-color-on-error-container: rgb(65 0 2);
|
||||
--md-sys-color-background: rgb(248 249 255);
|
||||
--md-sys-color-on-background: rgb(24 28 34);
|
||||
--md-sys-color-surface: rgb(248 249 255);
|
||||
--md-sys-color-on-surface: rgb(24 28 34);
|
||||
--md-sys-color-surface-variant: rgb(220 227 241);
|
||||
--md-sys-color-on-surface-variant: rgb(64 71 83);
|
||||
--md-sys-color-outline: rgb(112 119 132);
|
||||
--md-sys-color-outline-variant: rgb(192 199 213);
|
||||
--md-sys-color-shadow: rgb(0 0 0);
|
||||
--md-sys-color-scrim: rgb(0 0 0);
|
||||
--md-sys-color-inverse-surface: rgb(45 49 55);
|
||||
--md-sys-color-inverse-on-surface: rgb(238 241 250);
|
||||
--md-sys-color-inverse-primary: rgb(162 201 255);
|
||||
--md-sys-color-primary-fixed: rgb(211 228 255);
|
||||
--md-sys-color-on-primary-fixed: rgb(0 28 56);
|
||||
--md-sys-color-primary-fixed-dim: rgb(162 201 255);
|
||||
--md-sys-color-on-primary-fixed-variant: rgb(0 72 130);
|
||||
--md-sys-color-secondary-fixed: rgb(211 228 255);
|
||||
--md-sys-color-on-secondary-fixed: rgb(0 28 56);
|
||||
--md-sys-color-secondary-fixed-dim: rgb(169 201 246);
|
||||
--md-sys-color-on-secondary-fixed-variant: rgb(40 72 111);
|
||||
--md-sys-color-tertiary-fixed: rgb(225 224 255);
|
||||
--md-sys-color-on-tertiary-fixed: rgb(20 22 66);
|
||||
--md-sys-color-tertiary-fixed-dim: rgb(193 194 248);
|
||||
--md-sys-color-on-tertiary-fixed-variant: rgb(64 67 112);
|
||||
--md-sys-color-surface-dim: rgb(215 218 227);
|
||||
--md-sys-color-surface-bright: rgb(248 249 255);
|
||||
--md-sys-color-surface-container-lowest: rgb(255 255 255);
|
||||
--md-sys-color-surface-container-low: rgb(241 243 253);
|
||||
--md-sys-color-surface-container: rgb(235 238 247);
|
||||
--md-sys-color-surface-container-high: rgb(229 232 241);
|
||||
--md-sys-color-surface-container-highest: rgb(223 226 235);
|
||||
--md-nav-section-color-opacity: 1;
|
||||
--md-nav-on-section-color-opacity: 1;
|
||||
--md-nav-section-color-sign: rgba(25, 101, 212, var(--md-nav-section-color-opacity));
|
||||
--md-nav-on-section-color-sign: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
||||
--md-nav-section-color-organize: rgba(120, 130, 255, var(--md-nav-section-color-opacity));
|
||||
--md-nav-on-section-color-organize: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
||||
--md-nav-section-color-convert: rgba(25, 177, 212, var(--md-nav-section-color-opacity));
|
||||
--md-nav-on-section-color-convert: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
||||
--md-nav-section-color-security: rgba(255, 120, 146, var(--md-nav-section-color-opacity));
|
||||
--md-nav-on-section-color-security: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
||||
--md-nav-section-color-other: rgba(72, 189, 84, var(--md-nav-section-color-opacity));
|
||||
--md-nav-on-section-color-other: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
||||
--md-nav-section-color-advance: rgba(245, 84, 84, var(--md-nav-section-color-opacity));
|
||||
--md-nav-on-section-color-advance: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
||||
--md-nav-section-color-image: rgba(212, 172, 25, var(--md-nav-section-color-opacity));
|
||||
--md-nav-on-section-color-image: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
||||
--md-nav-section-color-word: rgba(61, 153, 245, var(--md-nav-section-color-opacity));
|
||||
--md-nav-on-section-color-word: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
||||
--md-nav-section-color-ppt: rgba(255, 128, 0, var(--md-nav-section-color-opacity));
|
||||
--md-nav-on-section-color-ppt: rgba(255, 251, 254, var(--md-nav-on-section-color-opacity));
|
||||
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
}
|
||||
|
||||
:where(html, .light-theme, .dark-theme), .tokens, :host {
|
||||
--md-sys-color-surface-1:
|
||||
color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 0, 0.05) 5%);
|
||||
--md-sys-color-surface-2:
|
||||
color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 0, 0.08) 5%);
|
||||
--md-sys-color-surface-3:
|
||||
color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 0, 0.11) 5%);
|
||||
--md-sys-color-surface-4:
|
||||
color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 0, 0.12) 5%);
|
||||
--md-sys-color-surface-5:
|
||||
color-mix(in srgb, var(--md-sys-color-primary) 13%, rgba(0, 0, 0, 0.14) 5%);
|
||||
--md-sys-icon-fill-0: 'FILL' 0, 'wght' 500;
|
||||
--md-sys-icon-fill-1: 'FILL' 1, 'wght' 500;
|
||||
--md-sys-state-hover-opacity:
|
||||
color-mix(in srgb, var(--md-sys-color-primary), rgba(0, 0, 0, 0) 80%);
|
||||
--md-sys-color-shadow: #000000;
|
||||
--md-elevation-shadow-color-rgb: 0, 0, 0;
|
||||
--md-elevation-shadow-color: var(--md-elevation-shadow-color-rgb);
|
||||
--md-sys-elevation-0: 0px 0px 0px 0px rgb(var(--md-elevation-shadow-color), 0.2), 0px 0px 0px 0px rgb(var(--md-elevation-shadow-color), 0.14), 0px 0px 0px 0px rgb(var(--md-elevation-shadow-color), 0.12);
|
||||
--md-sys-elevation-1: 0px 3px 1px -2px rgb(var(--md-elevation-shadow-color), 0.2), 0px 2px 2px 0px rgb(var(--md-elevation-shadow-color), 0.14), 0px 1px 5px 0px rgb(var(--md-elevation-shadow-color), 0.12);
|
||||
--md-sys-elevation-2: 0px 2px 4px -1px rgb(var(--md-elevation-shadow-color), 0.2), 0px 4px 5px 0px rgb(var(--md-elevation-shadow-color), 0.14), 0px 1px 10px 0px rgb(var(--md-elevation-shadow-color), 0.12);
|
||||
--md-sys-elevation-3: 0px 5px 5px -3px rgb(var(--md-elevation-shadow-color), 0.2), 0px 8px 10px 1px rgb(var(--md-elevation-shadow-color), 0.14), 0px 3px 14px 2px rgb(var(--md-elevation-shadow-color), 0.12);
|
||||
--md-sys-elevation-4: 0px 5px 5px -3px rgb(0, 0, 0 / 0.2), 0px 8px 10px 1px rgb(var(--md-elevation-shadow-color), 0.14), 0px 3px 14px 2px rgb(var(--md-elevation-shadow-color), 0.12);
|
||||
--md-sys-elevation-5: 0px 8px 10px -6px rgb(var(--md-elevation-shadow-color), 0.2), 0px 16px 24px 2px rgb(var(--md-elevation-shadow-color), 0.14), 0px 6px 30px 5px rgb(var(--md-elevation-shadow-color), 0.12);
|
||||
}
|
||||
|
||||
body, select, textarea {
|
||||
background-color: var(--md-sys-color-surface);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 10px auto;
|
||||
padding: 0 20px;
|
||||
font-family: var(--bs-font-sans-serif);
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
background-color: #fff;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.material-symbols {
|
||||
vertical-align: top;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
|
||||
|
||||
export function appendToFilename(inputPath: string, toAppend: string) {
|
||||
const parts = inputPath.split(".");
|
||||
if (parts.length > 1) {
|
||||
parts[parts.length-2] = parts[parts.length-2] + toAppend;
|
||||
}
|
||||
return parts.join(".");
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
|
||||
import { readBinaryFile, writeBinaryFile, removeDir, BaseDirectory } from "@tauri-apps/api/fs";
|
||||
import { PdfFile,RepresentationType } from "@stirling-pdf/shared-operations/src/wrappers/PdfFile";
|
||||
import { runShell, isTauriAvailable } from "./tauri-wrapper";
|
||||
|
||||
export async function fileToPdf(byteArray: Uint8Array, filename: string): Promise<PdfFile> {
|
||||
const randUuid = crypto.randomUUID();
|
||||
const tempDir = "StirlingPDF/"+randUuid;
|
||||
const srcFile = tempDir+"/"+filename;
|
||||
|
||||
await writeBinaryFile(srcFile, byteArray);
|
||||
await writeBinaryFile(srcFile, new Uint8Array([]), { dir: BaseDirectory.Temp });
|
||||
|
||||
const messageList: string[] = [];
|
||||
await runShell("libreoffice-convert", ["--headless","--convert-to","pdf",srcFile,"--outdir",tempDir], (message, stream) => {
|
||||
if (stream === "stdout") {
|
||||
messageList.push(message);
|
||||
}
|
||||
console.debug(`${stream}, ${randUuid}: ${message}`);
|
||||
});
|
||||
const lastMessage = messageList[messageList.length-1];
|
||||
const outputFilePath = lastMessage.split(" -> ")[1].split(".pdf")[0]+".pdf";
|
||||
const outputFilePathSplit = outputFilePath.toString().split("[\\/]");
|
||||
const outputFileName = outputFilePathSplit[outputFilePathSplit.length-1];
|
||||
const outputBytes = await readBinaryFile(outputFilePath);
|
||||
|
||||
await removeDir(tempDir);
|
||||
|
||||
return new PdfFile(outputFileName, outputBytes, RepresentationType.Uint8Array);
|
||||
}
|
||||
|
||||
export async function isLibreOfficeInstalled() {
|
||||
if (!isTauriAvailable()) return false;
|
||||
|
||||
const messageList: string[] = [];
|
||||
try {
|
||||
await runShell("libreoffice-version", ["--version"], (message, stream) => {
|
||||
if (stream === "stdout") {
|
||||
messageList.push(message);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
console.log("messageList", messageList);
|
||||
const result = messageList[0].match("LibreOffice ([0-9]+\.){4}.*");
|
||||
return result ? true : false;
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
|
||||
import { open, save } from "@tauri-apps/api/dialog";
|
||||
import { readBinaryFile, writeBinaryFile } from "@tauri-apps/api/fs";
|
||||
import { Command } from "@tauri-apps/api/shell";
|
||||
|
||||
export interface TauriBrowserFile {
|
||||
name: string,
|
||||
relativePath?: string,
|
||||
data: Uint8Array,
|
||||
getPath: ()=>string
|
||||
}
|
||||
function byteArrayToFile(byteArray: Uint8Array, filePath: string): TauriBrowserFile | null {
|
||||
const separator = filePath.includes("\\") ? "\\" : "/";
|
||||
|
||||
const split = filePath.split(separator);
|
||||
const fileName = split.pop();
|
||||
const path = split.join(separator);
|
||||
if (!fileName) return null;
|
||||
return {
|
||||
name: fileName,
|
||||
data: byteArray,
|
||||
relativePath: path?path:undefined,
|
||||
getPath: ()=> (path?path:undefined) + separator + fileName,
|
||||
};
|
||||
}
|
||||
|
||||
export function isTauriAvailable() {
|
||||
return (window as any).__TAURI_IPC__ ? true : false;
|
||||
}
|
||||
|
||||
// [*] = Not available in browser
|
||||
interface SelectFilesDialogOptions {
|
||||
defaultPath?: string, // [*] the default path to open the dialog on
|
||||
directory?: boolean, // should the dialog be a directory dialog
|
||||
filters?: { // list of file type filters
|
||||
name: string, // category name eg. 'Images'
|
||||
extensions: string[] // list of extensions eg ['png', 'jpeg', 'jpg']
|
||||
}[],
|
||||
multiple?: boolean, // allow multiple selections
|
||||
recursive?: boolean, // [*] If directory is true, indicates that it will be read recursively later. Defines whether subdirectories will be allowed on the scope or not.
|
||||
title?: string // [*] the title of the dialog
|
||||
}
|
||||
export function openFiles(options: SelectFilesDialogOptions): Promise<TauriBrowserFile[] | null> {
|
||||
return new Promise(async (resolve) => {
|
||||
if (isTauriAvailable()) {
|
||||
let selected = await open(options);
|
||||
if (!selected) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(selected)) {
|
||||
selected = [selected];
|
||||
}
|
||||
|
||||
const files: TauriBrowserFile[] = [];
|
||||
for (const s of selected) {
|
||||
const contents = await readBinaryFile(s);
|
||||
const res = byteArrayToFile(contents, s);
|
||||
if (res) {
|
||||
files.push(res);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(files);
|
||||
return;
|
||||
} else {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
if (options.directory) input.setAttribute("webkitdirectory", "");
|
||||
if (options.filters) input.setAttribute("accept", options.filters.flatMap(f => f.extensions).map(ext => "."+ext).join(", "));
|
||||
if (options.multiple) input.setAttribute("multiple", "");
|
||||
|
||||
input.onchange = async () => {
|
||||
if (input.files && input.files.length) {
|
||||
console.log("input.files", input.files);
|
||||
const files: TauriBrowserFile[] = [];
|
||||
for (const f of input.files) {
|
||||
const contents = new Uint8Array(await f.arrayBuffer());
|
||||
const res = byteArrayToFile(contents, f.name);
|
||||
if (res) {
|
||||
files.push(res);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(files);
|
||||
}
|
||||
input.onchange = null;
|
||||
document.body.onfocus = null;
|
||||
};
|
||||
|
||||
// detect the user clicking cancel
|
||||
document.body.onfocus = () => {
|
||||
setTimeout(()=>{ resolve(null) }, 200); // the timeout is needed because 'document.body.onfocus' is called before 'input.onchange'
|
||||
};
|
||||
|
||||
input.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// [*] = Not available in browser
|
||||
interface DownloadFilesDialogOptions {
|
||||
defaultPath?: string, // the default path to open the dialog on
|
||||
filters?: { // [*] list of file type filters
|
||||
name: string, // category name eg. 'Images'
|
||||
extensions: string[] // list of extensions eg ['png', 'jpeg', 'jpg']
|
||||
}[],
|
||||
title?: string // [*] the title of the dialog
|
||||
}
|
||||
export async function downloadFile(fileData: Uint8Array, options: DownloadFilesDialogOptions): Promise<undefined> {
|
||||
if (isTauriAvailable()) {
|
||||
const pathToSave = await save(options);
|
||||
console.log("pathToSave", pathToSave);
|
||||
if (pathToSave) {
|
||||
await writeBinaryFile(pathToSave, fileData);
|
||||
}
|
||||
} else {
|
||||
const pdfBlob = new Blob([fileData], { type: "application/pdf" });
|
||||
const url = URL.createObjectURL(pdfBlob);
|
||||
const downloadOption = localStorage.getItem("downloadOption");
|
||||
|
||||
// ensure filename is not a path
|
||||
const separator = options.defaultPath?.includes("\\") ? "\\" : "/";
|
||||
const filename = options.defaultPath?.split(separator).pop();
|
||||
const filenameToUse = filename ? filename : "edited.pdf";
|
||||
|
||||
if (downloadOption === "sameWindow") {
|
||||
// Open the file in the same window
|
||||
window.location.href = url;
|
||||
} else if (downloadOption === "newWindow") {
|
||||
// Open the file in a new window
|
||||
window.open(url, "_blank");
|
||||
} else {
|
||||
// Download the file
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.href = url;
|
||||
downloadLink.download = filenameToUse;
|
||||
downloadLink.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dont forget to whitelist the Command in src-tauri/tauri.conf.json! (tauri.allowlist.shell.scope)
|
||||
* @param commandName The name of the command to run. Must be defined in tauri.allowlist.shell.scope[].name
|
||||
* @param args The args to pass into the command
|
||||
* @param callback A callback function that is called when output is logged
|
||||
* @returns A log of all the outputs logged
|
||||
*/
|
||||
export function runShell(commandName: string, args: string[], callback: (message: any, stream:"stdout"|"stderr"|"error") => void): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
const comm = new Command(commandName, args);
|
||||
comm.on("close", data => {
|
||||
if (data.code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Command failed with exit code ${data.code} and signal ${data.signal}`));
|
||||
}
|
||||
});
|
||||
comm.on("error", error => { callback(error, "error") });
|
||||
comm.stdout.on("data", line => { callback(line, "stdout") });
|
||||
comm.stderr.on("data", line => { callback(line, "stderr") });
|
||||
|
||||
const child = await comm.spawn();
|
||||
console.debug(`Started child process with pid: ${child.pid}`);
|
||||
});
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"declarations/*.d.ts",
|
||||
"../shared-operations/declarations/*.d.ts"
|
||||
],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||
import react from "@vitejs/plugin-react";
|
||||
import topLevelAwait from "vite-plugin-top-level-await";
|
||||
import dynamicImport from 'vite-plugin-dynamic-import';
|
||||
import compileTime from "vite-plugin-compile-time";
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [
|
||||
// Thanks: https://stackoverflow.com/questions/74417822/how-can-i-use-buffer-process-in-vite-app
|
||||
nodePolyfills({
|
||||
include: [],
|
||||
|
||||
globals: {
|
||||
Buffer: true, // can also be 'build', 'dev', or false
|
||||
global: false,
|
||||
process: true,
|
||||
},
|
||||
// Whether to polyfill `node:` protocol imports.
|
||||
protocolImports: false,
|
||||
}),
|
||||
react(),
|
||||
topLevelAwait({
|
||||
// The export name of top-level await promise for each chunk module
|
||||
promiseExportName: "__tla",
|
||||
// The function to generate import names of top-level await promise in each chunk module
|
||||
promiseImportName: i => `__tla_${i}`
|
||||
}),
|
||||
compileTime(),
|
||||
dynamicImport(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'#pdfcpu': fileURLToPath(new URL("../shared-operations/src/wasm/pdfcpu/pdfcpu-wrapper.client", import.meta.url))
|
||||
}
|
||||
},
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
// 1. prevent vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
},
|
||||
// 3. to make use of `TAURI_DEBUG` and other env variables
|
||||
// https://tauri.app/v1/api/config#buildconfig.beforedevcommand
|
||||
envPrefix: ["VITE_"],
|
||||
base: '/', // relative paths sadly don't work with react router dom sub-dirs for some reason...
|
||||
}));
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: v1
|
||||
entries:
|
||||
stirling-pdf-chart:
|
||||
- apiVersion: v2
|
||||
appVersion: 0.31.1
|
||||
created: "2024-11-04T20:13:48.298110043Z"
|
||||
description: locally hosted web application that allows you to perform various
|
||||
operations on PDF files
|
||||
digest: 49a6e968f0203e7390094ae7fa20bc3fb0542ebdf426501c170641e98d2a45e2
|
||||
home: https://github.com/Stirling-Tools/Stirling-PDF
|
||||
keywords:
|
||||
- stirling-pdf
|
||||
- helm
|
||||
- charts repo
|
||||
maintainers:
|
||||
- name: Stirling-Tools
|
||||
url: https://github.com/Stirling-Tools/Stirling-PDF
|
||||
name: stirling-pdf-chart
|
||||
sources:
|
||||
- https://github.com/Stirling-Tools/Stirling-PDF
|
||||
urls:
|
||||
- https://github.com/Stirling-Tools/Stirling-PDF/releases/download/stirling-pdf-chart-1.0.1/stirling-pdf-chart-1.0.1.tgz
|
||||
version: 1.0.1
|
||||
generated: "2024-11-04T20:13:48.29811941Z"
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "stirling-pdf",
|
||||
"private": "true",
|
||||
"workspaces": [
|
||||
"client-tauri",
|
||||
"server-node",
|
||||
"shared-operations"
|
||||
],
|
||||
"scripts": {
|
||||
"dev-all": "concurrently --names \"node,tauri\" -c \"red.bold,cyan.bold\" --kill-others \"npm run -w server-node dev\" \"npm run -w client-tauri dev\"",
|
||||
"update-all-dependencies": "npm i --workspace=stirling-pdf --workspace=server-node --workspace=client-tauri --workspace=shared-operation",
|
||||
"update-backend-dependencies": "npm i --workspace=server-node --workspace=shared-operation",
|
||||
"update-frontend-dependencies": "npm i --workspace=client-tauri --workspace=shared-operation"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^8.56.0"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=7.24.2"
|
||||
},
|
||||
"engineStrict": true
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
VITE_JOBS_ENABLED=True
|
||||
VITE_JOBS_DIR="./jobs"
|
||||
|
||||
VITE_AUTH_ENABLED=True
|
||||
VITE_AUTH_SESSION_SECRET="default-secret"
|
||||
|
||||
VITE_SEQUELIZE_LOGGING=False
|
||||
@@ -1,19 +0,0 @@
|
||||
# Use an existing image as a base
|
||||
FROM node:22.2.0-alpine
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --legacy-peer-deps
|
||||
|
||||
COPY ./dist ./dist
|
||||
|
||||
RUN apk del git
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD [ "node", "./dist/index.js" ]
|
||||
@@ -1,10 +0,0 @@
|
||||
vite-build:
|
||||
npx tsc
|
||||
npx vite build
|
||||
|
||||
vite-dockerize:
|
||||
make vite-build
|
||||
bash -c "cp ../package-lock.json ./"
|
||||
docker build . -t stirling-pdf-backend:latest
|
||||
bash -c "rm ./package-lock.json"
|
||||
docker image prune
|
||||
@@ -1,7 +0,0 @@
|
||||
type UserModel = import("../src/auth/user/user-model").User;
|
||||
|
||||
declare namespace Express {
|
||||
interface User extends UserModel {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
declare namespace NodeJS {
|
||||
export interface ProcessEnv {
|
||||
JOBS_ENABLED: "True" | "False",
|
||||
JOBS_DIR: string,
|
||||
AUTH_ENABLED: "True" | "False",
|
||||
AUTH_SESSION_SECRET: string,
|
||||
SEQUELIZE_LOGGING: "True" | "False"
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
{
|
||||
"name": "@stirling-pdf/server-node",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
"imports": {
|
||||
"#pdfcpu": "@stirling-pdf/shared-operations/src/wasm/pdfcpu/pdfcpu-wrapper.server.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvas": "^2.11.2"
|
||||
},
|
||||
"scripts": {
|
||||
"vite:dev": "vite",
|
||||
"vite:preview-prod": "make vite-build && node ./dist/index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@stirling-tools/joi": "github:stirling-tools/joi",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/multer": "^1.4.10",
|
||||
"@wasmer/wasmfs": "^0.12.0",
|
||||
"archiver": "^6.0.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.2",
|
||||
"express-fileupload": "^1.4.2",
|
||||
"express-session": "^1.18.0",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"joi": "^17.11.0",
|
||||
"jsqr": "^1.4.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-headerapikey": "^1.2.2",
|
||||
"passport-local": "^1.0.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^4.5.136",
|
||||
"react-material-symbols": "^4.4.0",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"rollup-plugin-dynamic-import-variables": "^1.1.0",
|
||||
"sequelize": "^6.37.3",
|
||||
"sqlite3": "^5.1.7",
|
||||
"toml": "^3.0.0",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"vite": "^5.4.2",
|
||||
"vite-plugin-compile-time": "^0.2.1",
|
||||
"vite-plugin-dynamic-import": "^1.5.0",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"vite-plugin-top-level-await": "^1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-dynamic-import-vars": "^2.1.2",
|
||||
"@rollup/plugin-run": "^3.0.2",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/archiver": "^6.0.2",
|
||||
"@types/express-session": "^1.18.0",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"copyfiles": "^2.4.1",
|
||||
"pkgroll": "^2.0.1",
|
||||
"rimraf": "^5.0.5",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.5.4",
|
||||
"vite-plugin-node": "^3.1.0"
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Error as SequelizeError } from "sequelize";
|
||||
import { User } from "../user/user-model";
|
||||
import { APIKey } from "./apikey-model";
|
||||
|
||||
export function findOne(params: {apikey?: string}, cb: (err: Error | null, apikey?: APIKey | undefined, info?: Object | undefined) => void): undefined {
|
||||
const query: any = params;
|
||||
|
||||
for (let key in query) {
|
||||
if (query[key] === undefined) {
|
||||
delete query[key];
|
||||
}
|
||||
}
|
||||
|
||||
if(Object.keys(query).length == 0) {
|
||||
cb(new Error("You need to provide at least one argument."), undefined)
|
||||
}
|
||||
|
||||
APIKey.findOne({
|
||||
where: query,
|
||||
include: APIKey.associations.User
|
||||
}).then(apikey => {
|
||||
if(apikey)
|
||||
cb(null, apikey);
|
||||
else
|
||||
cb(null, undefined, { message: "The requested apikey was not found."});
|
||||
}).catch(e =>
|
||||
cb(e, undefined)
|
||||
);
|
||||
}
|
||||
|
||||
export async function createAPIKey(user: User | undefined): Promise<APIKey | undefined> {
|
||||
if(!user) throw new Error("User was undefined");
|
||||
|
||||
const apikey = crypto.randomUUID(); // TODO: Is this secure enough?
|
||||
const apikeyEntry = await user.createAPIKey({ apikey: apikey });
|
||||
|
||||
return apikeyEntry;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { User } from '../user/user-model';
|
||||
import {
|
||||
Model, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute, ForeignKey,
|
||||
} from 'sequelize';
|
||||
|
||||
export class APIKey extends Model<InferAttributes<APIKey>, InferCreationAttributes<APIKey>> {
|
||||
declare id: CreationOptional<number>;
|
||||
declare apikey: string;
|
||||
|
||||
declare UserId: ForeignKey<User['id']>;
|
||||
|
||||
// `User` is an eagerly-loaded association.
|
||||
declare User?: NonAttribute<User>;
|
||||
|
||||
declare createdAt: CreationOptional<Date>;
|
||||
declare updatedAt: CreationOptional<Date>;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import passport from "passport";
|
||||
import session from "express-session";
|
||||
import { initialize } from "./passport-config";
|
||||
import auth from "../routes/auth/auth-controller";
|
||||
import { Express } from "express";
|
||||
|
||||
export function connect(app: Express) {
|
||||
app.use(session({
|
||||
secret: import.meta.env.VITE_SESSION_SECRET || "default-secret",
|
||||
resave: false,
|
||||
saveUninitialized: false
|
||||
}));
|
||||
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.authenticate(['headerapikey', 'session'], {
|
||||
session: false, // Only set a session on the login request.
|
||||
}));
|
||||
|
||||
initialize(passport);
|
||||
|
||||
app.use("/auth", auth);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
export function isAuthorized(req: Request, res: Response, next: NextFunction) {
|
||||
if(import.meta.env.VITE_AUTH_ENABLED === "False" || req.user) {
|
||||
return next();
|
||||
}
|
||||
return res.status(403).json({"Error": "Authentication failed."});
|
||||
}
|
||||
|
||||
export function whenAuthIsEnabled(req: Request, res: Response, next: NextFunction) {
|
||||
if(import.meta.env.VITE_AUTH_ENABLED === "True") {
|
||||
return next();
|
||||
}
|
||||
return res.status(403).json({"Error": "Authentication is not enabled."});
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Strategy as LocalStrategy} from "passport-local";
|
||||
import { HeaderAPIKeyStrategy as HeaderAPIKeyStrategy } from "passport-headerapikey";
|
||||
|
||||
import * as User from "./user/user-controller";
|
||||
import * as APIKey from "./apikey/apikey-controller";
|
||||
|
||||
export function initialize(passport: typeof import("passport")) {
|
||||
passport.use("local", new LocalStrategy(
|
||||
function(username, password, done) {
|
||||
User.findOne({username: username}, function (err, user) {
|
||||
if (err) {
|
||||
return done(err, false);
|
||||
}
|
||||
if (!user) {
|
||||
return done(null, false);
|
||||
}
|
||||
|
||||
User.verifyPassword(user, password, (error, success) => {
|
||||
if(error) return done(error, false);
|
||||
|
||||
if(!success) return done(null, false);
|
||||
|
||||
return done(null, user)
|
||||
});
|
||||
});
|
||||
}
|
||||
));
|
||||
|
||||
passport.use(new HeaderAPIKeyStrategy(
|
||||
{ header: 'Authorization', prefix: 'Bearer ' },
|
||||
false,
|
||||
function(apikey, done) {
|
||||
APIKey.findOne({ apikey: apikey }, function (err, apikey, info) {
|
||||
if (err) {
|
||||
return done(err, false);
|
||||
}
|
||||
if (!apikey) {
|
||||
return done(null, false, info);
|
||||
}
|
||||
return done(null, apikey.User);
|
||||
});
|
||||
}
|
||||
));
|
||||
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user.id)
|
||||
});
|
||||
|
||||
passport.deserializeUser((id: number, done) => {
|
||||
User.findOne({ id: id }, function (err, user) {
|
||||
done(err, user);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { Error as SequelizeError, Op } from "sequelize";
|
||||
import { Password, User } from "./user-model";
|
||||
import crypto from "crypto";
|
||||
|
||||
type PickOne<T, F extends keyof T> = Pick<T, F> & { [K in keyof Omit<T, F>]?: never };
|
||||
|
||||
export function findOne(params: {id?: number, username?: string}, cb: (err: Error | null, apikey?: User | undefined, info?: Object | undefined) => void): undefined {
|
||||
const query: any = params;
|
||||
|
||||
for (let key in query) {
|
||||
if (query[key] === undefined) {
|
||||
delete query[key];
|
||||
}
|
||||
}
|
||||
|
||||
if(Object.keys(query).length == 0) {
|
||||
cb(new Error("You need to provide at least one argument."), undefined)
|
||||
}
|
||||
|
||||
User.findOne({
|
||||
where: query
|
||||
}).then(user => {
|
||||
if(user)
|
||||
cb(null, user);
|
||||
else
|
||||
cb(null, undefined, { message: "The requested user was not found."});
|
||||
}).catch(e =>
|
||||
cb(e, undefined)
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Allow other authentication methods
|
||||
export function createUser(params: { username: string, password: string }, cb: (err: SequelizeError | null, user: User | null) => void ) {
|
||||
User.create({ username: params.username }).then(async (user) => {
|
||||
const salt = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
hashPassword(params.password, salt, async (err, derivedKey) => {
|
||||
if(err || !derivedKey) {
|
||||
return cb(err, null);
|
||||
}
|
||||
|
||||
user.setPassword(await Password.create({
|
||||
password: derivedKey,
|
||||
salt: salt
|
||||
})).then(password => {
|
||||
cb(null, user as any as User);
|
||||
}).catch(e => {
|
||||
cb(e, null);
|
||||
});
|
||||
})
|
||||
}).catch(e =>
|
||||
cb(e, null)
|
||||
);
|
||||
}
|
||||
|
||||
export async function verifyPassword(user: User, password: string, cb: (error: Error | null, success: boolean | null) => void) {
|
||||
const passwordRecord = await user.getPassword();
|
||||
if(!passwordRecord) {
|
||||
return cb(new Error("This user does not have a password set!"), null);
|
||||
}
|
||||
|
||||
hashPassword(password, passwordRecord.salt, (err, derivedKey) => {
|
||||
if(err) return cb(err, null);
|
||||
return cb(null, passwordRecord.password == derivedKey);
|
||||
});
|
||||
}
|
||||
|
||||
function hashPassword(password: string, salt: string, cb: (err: Error | null, derivedKey: string | null) => void) {
|
||||
crypto.pbkdf2(password, salt, 100000, 64, 'sha512', (err, derivedKey) => {
|
||||
if (err) return cb(err, null);
|
||||
cb(null, derivedKey.toString('hex'));
|
||||
});
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import {
|
||||
Association, DataTypes, Model, ModelDefined, Optional,
|
||||
Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute, ForeignKey,
|
||||
|
||||
HasManyAddAssociationMixin, HasManyCountAssociationsMixin,
|
||||
HasManyCreateAssociationMixin, HasManyGetAssociationsMixin, HasManyHasAssociationMixin,
|
||||
HasManySetAssociationsMixin, HasManyAddAssociationsMixin, HasManyHasAssociationsMixin,
|
||||
HasManyRemoveAssociationMixin, HasManyRemoveAssociationsMixin,
|
||||
|
||||
HasOneGetAssociationMixin, HasOneSetAssociationMixin, HasOneCreateAssociationMixin,
|
||||
} from 'sequelize';
|
||||
|
||||
import { APIKey } from '../apikey/apikey-model';
|
||||
|
||||
export class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
|
||||
declare id: CreationOptional<number>;
|
||||
declare username: string;
|
||||
declare mail?: string;
|
||||
|
||||
declare getPassword: HasOneGetAssociationMixin<Password | undefined>; // Note the null assertions!
|
||||
declare setPassword: HasOneSetAssociationMixin<Password | undefined, number>;
|
||||
declare createPassword: HasOneCreateAssociationMixin<Password>;
|
||||
|
||||
declare getAccessRules: HasManyGetAssociationsMixin<AccessRule | undefined>; // Note the null assertions!
|
||||
declare addAccessRule: HasManyAddAssociationMixin<AccessRule | undefined, number>;
|
||||
declare addAccessRules: HasManyAddAssociationsMixin<AccessRule | undefined, number>;
|
||||
declare setAccessRules: HasManySetAssociationsMixin<AccessRule | undefined, number>;
|
||||
declare removeAccessRule: HasManyRemoveAssociationMixin<AccessRule | undefined, number>;
|
||||
declare removeAccessRules: HasManyRemoveAssociationsMixin<AccessRule | undefined, number>;
|
||||
declare hasAccessRule: HasManyHasAssociationMixin<AccessRule | undefined, number>;
|
||||
declare hasAccessRules: HasManyHasAssociationsMixin<AccessRule | undefined, number>;
|
||||
declare countAccessRules: HasManyCountAssociationsMixin;
|
||||
declare createAccessRule: HasManyCreateAssociationMixin<AccessRule, 'UserId'>;
|
||||
|
||||
declare getAPIKeys: HasManyGetAssociationsMixin<APIKey | undefined>; // Note the null assertions!
|
||||
declare addAPIKey: HasManyAddAssociationMixin<APIKey | undefined, number>;
|
||||
declare addAPIKeys: HasManyAddAssociationsMixin<APIKey | undefined, number>;
|
||||
declare setAPIKeys: HasManySetAssociationsMixin<APIKey | undefined, number>;
|
||||
declare removeAPIKey: HasManyRemoveAssociationMixin<APIKey | undefined, number>;
|
||||
declare removeAPIKeys: HasManyRemoveAssociationsMixin<APIKey | undefined, number>;
|
||||
declare hasAPIKey: HasManyHasAssociationMixin<APIKey | undefined, number>;
|
||||
declare hasAPIKeys: HasManyHasAssociationsMixin<APIKey | undefined, number>;
|
||||
declare countAPIKeys: HasManyCountAssociationsMixin;
|
||||
declare createAPIKey: HasManyCreateAssociationMixin<APIKey, 'UserId'>;
|
||||
|
||||
// You can also pre-declare possible inclusions, these will only be populated if you
|
||||
// actively include a relation.
|
||||
declare apikeys?: NonAttribute<APIKey[]>;
|
||||
|
||||
declare createdAt: CreationOptional<Date>;
|
||||
declare updatedAt: CreationOptional<Date>;
|
||||
}
|
||||
|
||||
export class Password extends Model<InferAttributes<Password>, InferCreationAttributes<Password>> {
|
||||
declare id: CreationOptional<number>;
|
||||
declare password: string;
|
||||
declare salt: string;
|
||||
|
||||
declare ownerId: ForeignKey<User['id']>;
|
||||
declare owner?: NonAttribute<User>;
|
||||
|
||||
declare createdAt: CreationOptional<Date>;
|
||||
declare updatedAt: CreationOptional<Date>;
|
||||
}
|
||||
|
||||
export class AccessRule extends Model<InferAttributes<AccessRule>, InferCreationAttributes<AccessRule>> {
|
||||
declare id: CreationOptional<number>;
|
||||
declare grants: string;
|
||||
|
||||
declare UserId: ForeignKey<User['id']>;
|
||||
declare User?: NonAttribute<User>;
|
||||
|
||||
declare createdAt: CreationOptional<Date>;
|
||||
declare updatedAt: CreationOptional<Date>;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { Sequelize, DataTypes } from "sequelize";
|
||||
|
||||
//TODO: Make this configurable
|
||||
const sequelize = new Sequelize("sqlite::memory:", {
|
||||
logging: import.meta.env.VITE_SEQUELIZE_LOGGING === "True" ? console.log : false
|
||||
});
|
||||
|
||||
import { User, AccessRule, Password } from "../auth/user/user-model";
|
||||
import { APIKey } from "../auth/apikey/apikey-model";
|
||||
|
||||
User.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
mail: {
|
||||
type: DataTypes.STRING,
|
||||
unique: true,
|
||||
},
|
||||
createdAt: DataTypes.DATE,
|
||||
updatedAt: DataTypes.DATE,
|
||||
},
|
||||
{ sequelize },
|
||||
);
|
||||
|
||||
Password.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
password: DataTypes.STRING,
|
||||
salt: DataTypes.STRING,
|
||||
createdAt: DataTypes.DATE,
|
||||
updatedAt: DataTypes.DATE,
|
||||
},
|
||||
{ sequelize },
|
||||
);
|
||||
User.hasOne(Password);
|
||||
Password.belongsTo(User);
|
||||
|
||||
AccessRule.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
grants: DataTypes.STRING,
|
||||
createdAt: DataTypes.DATE,
|
||||
updatedAt: DataTypes.DATE,
|
||||
},
|
||||
{ sequelize },
|
||||
);
|
||||
User.hasMany(AccessRule);
|
||||
AccessRule.belongsTo(User);
|
||||
|
||||
APIKey.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER.UNSIGNED,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
apikey: DataTypes.STRING,
|
||||
createdAt: DataTypes.DATE,
|
||||
updatedAt: DataTypes.DATE,
|
||||
},
|
||||
{ sequelize },
|
||||
);
|
||||
User.hasMany(APIKey);
|
||||
APIKey.belongsTo(User);
|
||||
|
||||
export default sequelize;
|
||||
|
||||
(async () => {
|
||||
await sequelize.sync({ force: true });
|
||||
console.log("Database synced!")
|
||||
})();
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* translation
|
||||
*/
|
||||
|
||||
import i18next from "i18next";
|
||||
import resourcesToBackend from "i18next-resources-to-backend";
|
||||
|
||||
i18next.use(resourcesToBackend((language: string, namespace: string) => import(`../../shared-operations/public/locales/${namespace}/${language}.json`)))
|
||||
.init({
|
||||
debug: false,
|
||||
ns: ["common"], // Preload this namespace, no need to add the others, they will load once their module is loaded
|
||||
defaultNS: "common",
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
initImmediate: false // Makes loading blocking but sync
|
||||
});
|
||||
|
||||
// list available modules
|
||||
import { listOperatorNames } from "@stirling-pdf/shared-operations/src/workflow/operatorAccessor";
|
||||
console.log("Available Modules: ", listOperatorNames());
|
||||
|
||||
/*
|
||||
* jobs
|
||||
*/
|
||||
|
||||
if(import.meta.env.VITE_JOBS_ENABLED === "True")
|
||||
import("./jobs/jobs-controller");
|
||||
|
||||
/**
|
||||
* database
|
||||
*/
|
||||
|
||||
if(import.meta.env.VITE_AUTH_ENABLED === "True")
|
||||
import("./data/sequelize-relations");
|
||||
|
||||
/*
|
||||
* EXPRESS
|
||||
*/
|
||||
|
||||
import express from "express";
|
||||
const app = express();
|
||||
const PORT = 8000;
|
||||
|
||||
import api from "./routes/api/api-controller";
|
||||
|
||||
/*
|
||||
* auth
|
||||
*/
|
||||
|
||||
console.log("env", import.meta.env)
|
||||
|
||||
if(import.meta.env.VITE_AUTH_ENABLED === "True") {
|
||||
console.log("Attatching Auth")
|
||||
import("./auth/auth-controller").then(router => router.connect(app)).finally(() => {
|
||||
/*
|
||||
* api
|
||||
*/
|
||||
|
||||
app.use("/api", api);
|
||||
});
|
||||
}
|
||||
else {
|
||||
app.use("/api", api);
|
||||
}
|
||||
|
||||
// viteNode
|
||||
if (import.meta.env.PROD) {
|
||||
app.listen(PORT, () => {
|
||||
console.log(`http://localhost:${PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
});
|
||||
|
||||
export const viteNodeApp = app;
|
||||
@@ -1,197 +0,0 @@
|
||||
import { traverseOperations } from '@stirling-pdf/shared-operations/src/workflow/traverseOperations';
|
||||
import { PdfFile, RepresentationType } from '@stirling-pdf/shared-operations/src/wrappers/PdfFile';
|
||||
import fs from 'fs';
|
||||
import path from "path";
|
||||
import toml from 'toml';
|
||||
|
||||
const jobsDir = import.meta.env.VITE_JOBS_DIR;
|
||||
|
||||
// TODO: Also remove watched folders
|
||||
const watchedFolders: {
|
||||
[folderName: string]: Job
|
||||
} = {};
|
||||
|
||||
if(jobsDir)
|
||||
setupJobs(jobsDir);
|
||||
|
||||
function setupJobs(jobsDir: string) {
|
||||
if(!fs.existsSync(jobsDir)) {
|
||||
console.log("jobs dir does not exist. creating one...");
|
||||
fs.mkdirSync(jobsDir);
|
||||
}
|
||||
|
||||
fs.watch(jobsDir, {}, (e, f) => {
|
||||
if(f === null) return;
|
||||
|
||||
if(f === "jobs.toml") {
|
||||
handleJobsToml("jobs.toml", jobsDir);
|
||||
}
|
||||
})
|
||||
|
||||
fs.readdir(jobsDir, (err, files) => {
|
||||
if (files.includes("jobs.toml")) {
|
||||
handleJobsToml("jobs.toml", jobsDir);
|
||||
}
|
||||
else {
|
||||
console.log("jobs.toml is not present, if you want to use jobs please configure it");
|
||||
}
|
||||
// TODO: Cleanup the folder?
|
||||
});
|
||||
}
|
||||
|
||||
interface Job {
|
||||
type: string
|
||||
}
|
||||
|
||||
type cronString = string;
|
||||
|
||||
interface FolderJob extends Job {
|
||||
trigger: "FILE_CHANGE" | "START_FILE_DELETION" | cronString,
|
||||
delay: number | undefined,
|
||||
respectFolderStructure: boolean | undefined,
|
||||
enableLogsDir: boolean | undefined,
|
||||
keepOriginals: boolean | undefined,
|
||||
indicateStatus: boolean | undefined,
|
||||
}
|
||||
|
||||
function handleJobsToml(jobsFile: string, jobsDir: string) {
|
||||
console.log("jobs.toml was updated.");
|
||||
fs.readFile(path.join(jobsDir, jobsFile), (err, data) => {
|
||||
const jobsConfig = toml.parse(data.toString());
|
||||
const jobs: { [key: string]: Job} = jobsConfig.jobs;
|
||||
|
||||
for (const jobName in jobs) {
|
||||
const job = jobs[jobName];
|
||||
switch (job.type) {
|
||||
case "folder":
|
||||
setupFolderJob(jobName, job as FolderJob, jobsDir);
|
||||
break;
|
||||
default:
|
||||
console.error(`job-type ${job.type} of ${jobName} is not implemented`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const watchedWritingFiles: { [path: string]: NodeJS.Timeout } = {};
|
||||
|
||||
function setupFolderJob(jobName: string, job: FolderJob, jobsDir: string) {
|
||||
const jobFolder = path.join(jobsDir, jobName, "/");
|
||||
|
||||
if(watchedFolders[path.join(jobFolder, "in/")]) {
|
||||
return;
|
||||
}
|
||||
|
||||
watchedFolders[path.join(jobFolder, "in/")] = job;
|
||||
|
||||
if(!fs.existsSync(jobFolder)) {
|
||||
fs.mkdirSync(jobFolder);
|
||||
|
||||
if(!fs.existsSync(path.join(jobFolder, "workflow.json"))) {
|
||||
fs.writeFileSync(path.join(jobFolder, "workflow.json"), "{}");
|
||||
}
|
||||
|
||||
if(!fs.existsSync(path.join(jobFolder, "in/"))) {
|
||||
fs.mkdirSync(path.join(jobFolder, "in"));
|
||||
}
|
||||
|
||||
if(!fs.existsSync(path.join(jobFolder, "out/"))) {
|
||||
fs.mkdirSync(path.join(jobFolder, "out"));
|
||||
}
|
||||
}
|
||||
|
||||
// trigger
|
||||
|
||||
switch (job.trigger) {
|
||||
case "FILE_CHANGE":
|
||||
// TODO: Process files that are already in there
|
||||
fs.watch(path.join(jobFolder, "in/"), async (e, f) => {
|
||||
if(!f || f == "") return;
|
||||
|
||||
const file = path.parse(f);
|
||||
const filePath = path.join(jobFolder, "in/", f);
|
||||
|
||||
if(file.ext != ".pdf") {
|
||||
if(file.ext == ".processing-pdf") {
|
||||
return;
|
||||
}
|
||||
console.log("Non-pdf files aren't supported at the moment.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(watchedWritingFiles[filePath]) {
|
||||
clearTimeout(watchedWritingFiles[filePath]);
|
||||
}
|
||||
|
||||
console.log("in/", e, f)
|
||||
watchedWritingFiles[filePath] = setTimeout(async () => {
|
||||
processSingleFile(file, filePath, jobFolder);
|
||||
}, (job.delay || 5) * 1000)
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`The trigger ${job.trigger} for ${jobName} could not be setup.`)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function processSingleFile(file: path.ParsedPath, filePath: string, jobFolder: string) {
|
||||
console.log("Processing file ", file.base);
|
||||
|
||||
try {
|
||||
var workflow = JSON.parse(fs.readFileSync(path.join(jobFolder, "workflow.json")).toString());
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
console.error("malformed workflow-json was provided", err.message);
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if(!workflow.actions) {
|
||||
console.error("The provided workflow does not contain any actions.");
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Reading File");
|
||||
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
const input: PdfFile = new PdfFile(file.name, new Uint8Array(data), RepresentationType.Uint8Array, file.name);
|
||||
|
||||
if(fs.existsSync(filePath))
|
||||
fs.renameSync(filePath, filePath + ".processing-pdf");
|
||||
else {
|
||||
console.log(`${filePath} does not exist anymore. Either it was already processed or it was deleted by the user.`);
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Check if file type == inputType for operator
|
||||
|
||||
traverseOperations(workflow.actions, [input], (state) => {
|
||||
console.log("State: ", state);
|
||||
}).then(async (pdfResults) => {
|
||||
console.log("Download");
|
||||
//TODO: Write files to fs
|
||||
pdfResults.forEach(async pdfResult => {
|
||||
fs.writeFile(path.join(jobFolder, "out/", pdfResult.filename + ".pdf"), await pdfResult.uint8Array, (err) => {
|
||||
if(err) console.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
fs.rmSync(filePath + ".processing-pdf");
|
||||
}).catch((err) => {
|
||||
if(err.validationError) {
|
||||
// Bad Request
|
||||
console.log(err);
|
||||
}
|
||||
else if (err instanceof Error) {
|
||||
console.error("Internal Server Error", err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import express, { Request, Response } from "express";
|
||||
|
||||
import { isAuthorized } from "../../auth/authenticationMiddleware";
|
||||
|
||||
import workflow from "./workflow-controller";
|
||||
import dynamicOperations from "./dynamic-operations-controller";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(isAuthorized);
|
||||
|
||||
router.get("/", (req: Request, res: Response) => {
|
||||
// TODO: Implement root api endpoint
|
||||
res.status(501).json({"Error": "Unfinished Endpoint. This sould probably send some api docs?"});
|
||||
});
|
||||
|
||||
router.use("/workflow", workflow);
|
||||
router.use("/", dynamicOperations);
|
||||
|
||||
export default router;
|
||||
@@ -1,63 +0,0 @@
|
||||
import express, { Request, Response } from "express";
|
||||
const router = express.Router();
|
||||
import multer from "multer";
|
||||
const upload = multer();
|
||||
import { getOperatorByName, getSchemaByName } from "@stirling-pdf/shared-operations/src/workflow/operatorAccessor";
|
||||
|
||||
import { PdfFile } from "@stirling-pdf/shared-operations/src/wrappers/PdfFile";
|
||||
import { respondWithPdfFiles } from "../../utils/response-utils";
|
||||
import { Action } from "@stirling-pdf/shared-operations/declarations/Action";
|
||||
import { JoiPDFFileSchema } from "@stirling-pdf/shared-operations/src/wrappers/PdfFileJoi";
|
||||
|
||||
router.post("/:func", upload.array("file"), async function(req: Request, res: Response) {
|
||||
await handleEndpoint(req, res);
|
||||
});
|
||||
|
||||
router.post("/:dir/:func", upload.array("file"), async function(req: Request, res: Response) {
|
||||
await handleEndpoint(req, res);
|
||||
});
|
||||
|
||||
async function handleEndpoint(req: Request, res: Response) {
|
||||
if(!req.files || req.files.length == 0) {
|
||||
res.status(400).json({error: "no input file(s) were provided"});
|
||||
return;
|
||||
}
|
||||
|
||||
const validationResults = await JoiPDFFileSchema.validateAsync(req.files);
|
||||
if(validationResults.error) {
|
||||
res.status(400).json({error: "PDF validation failed", details: validationResults.error.message});
|
||||
return;
|
||||
}
|
||||
const pdfFiles: PdfFile[] = validationResults.value;
|
||||
|
||||
const schema = await getSchemaByName(req.params.func);
|
||||
|
||||
if(schema) {
|
||||
const action: Action = {type: req.params.func, values: req.body};
|
||||
|
||||
const validationResults = schema.schema.validate({input: pdfFiles, values: action.values});
|
||||
|
||||
if(validationResults.error) {
|
||||
res.status(400).json({error: "Value validation failed", details: validationResults.error.message});
|
||||
}
|
||||
else {
|
||||
action.values = validationResults.value.values;
|
||||
|
||||
const Operator = await getOperatorByName(req.params.func);
|
||||
if(!Operator) {
|
||||
res.status(400).json({error: `the operator of type ${req.params.func} does not exist`});
|
||||
return
|
||||
}
|
||||
|
||||
const operation = new Operator(action);
|
||||
operation.run(validationResults.value.input, (progress) => {}).then(pdfFiles => {
|
||||
respondWithPdfFiles(res, pdfFiles, req.params.func + "_result");
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
res.status(400).json({error: `the operator of type ${req.params.func} does not exist`});
|
||||
}
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -1,237 +0,0 @@
|
||||
import express, { Request, Response } from "express";
|
||||
import crypto from "crypto";
|
||||
import multer from "multer";
|
||||
const upload = multer();
|
||||
|
||||
import { traverseOperations } from "@stirling-pdf/shared-operations/src/workflow/traverseOperations";
|
||||
import { PdfFile, RepresentationType } from "@stirling-pdf/shared-operations/src/wrappers/PdfFile";
|
||||
import { respondWithPdfFiles } from "../../utils/response-utils";
|
||||
import { JoiPDFFileSchema } from "@stirling-pdf/shared-operations/src/wrappers/PdfFileJoi";
|
||||
|
||||
interface Workflow {
|
||||
eventStream?: express.Response,
|
||||
result?: PdfFile[],
|
||||
finished: boolean,
|
||||
createdAt: EpochTimeStamp,
|
||||
finishedAt?: EpochTimeStamp,
|
||||
error?: { type: number, error: string, stack?: string }
|
||||
// TODO: When auth is implemented: owner
|
||||
}
|
||||
|
||||
const activeWorkflows: Record<string, Workflow> = {};
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post("/:workflowUuid?", [
|
||||
upload.array("files"),
|
||||
async (req: Request, res: Response) => {
|
||||
// TODO: Maybe replace with another validator
|
||||
if(req.files?.length == 0) {
|
||||
res.status(400).json({"error": "No files were uploaded."});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var workflow = JSON.parse(req.body.workflow);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
console.error("malformed workflow-json was provided", err.message);
|
||||
res.status(400).json({error: "Malformed workflow-JSON was provided. See Server-Logs for more info", details: err.message});
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if(!workflow.actions) {
|
||||
res.status(400).json({error: "The provided workflow does not contain any actions."});
|
||||
return
|
||||
}
|
||||
|
||||
const validationResults = await JoiPDFFileSchema.validateAsync(req.files);
|
||||
if(validationResults.error) {
|
||||
res.status(400).json({error: "PDF validation failed", details: validationResults.error.message});
|
||||
return;
|
||||
}
|
||||
const inputs: PdfFile[] = validationResults;
|
||||
|
||||
// Allow option to do it synchronously and just make a long request
|
||||
if(req.body.async === "false") {
|
||||
console.log("Don't do async");
|
||||
|
||||
// TODO: Check if file type == inputType for operator
|
||||
|
||||
traverseOperations(workflow.actions, inputs, (state) => {
|
||||
console.log("State: ", state);
|
||||
}).then(async (pdfResults) => {
|
||||
console.log("Download");
|
||||
await respondWithPdfFiles(res, pdfResults, "workflow-results");
|
||||
}).catch((err) => {
|
||||
if(err.validationError) {
|
||||
// Bad Request
|
||||
res.status(400).json({error: err});
|
||||
}
|
||||
else if (err instanceof Error) {
|
||||
console.error("Internal Server Error", err);
|
||||
// Internal Server Error
|
||||
res.status(500).json({error: err.message, stack: err.stack});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
console.log("Start Async Workflow");
|
||||
// TODO: UUID collision checks
|
||||
let workflowID = req.params.workflowUuid;
|
||||
if(!workflowID)
|
||||
workflowID = generateWorkflowID();
|
||||
|
||||
activeWorkflows[workflowID] = {
|
||||
createdAt: Date.now(),
|
||||
finished: false
|
||||
};
|
||||
const activeWorkflow = activeWorkflows[workflowID];
|
||||
|
||||
res.status(200).json({
|
||||
"workflowID": workflowID,
|
||||
"data-recieved": {
|
||||
"fileCount": inputs.length,
|
||||
"workflow": workflow
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Check if file type == inputType for operator
|
||||
|
||||
traverseOperations(workflow.actions, inputs, (state) => {
|
||||
console.log("State: ", state);
|
||||
if(activeWorkflow.eventStream)
|
||||
activeWorkflow.eventStream.write(`data: ${state}\n\n`);
|
||||
}).then(async (pdfResults) => {
|
||||
if(activeWorkflow.eventStream) {
|
||||
activeWorkflow.eventStream.write("data: processing done\n\n");
|
||||
activeWorkflow.eventStream.end();
|
||||
}
|
||||
|
||||
activeWorkflow.result = pdfResults;
|
||||
activeWorkflow.finished = true;
|
||||
activeWorkflow.finishedAt = Date.now();
|
||||
}).catch((err) => {
|
||||
if(err.validationError) {
|
||||
activeWorkflow.error = {type: 500, error: err};
|
||||
activeWorkflow.finished = true;
|
||||
activeWorkflow.finishedAt = Date.now();
|
||||
|
||||
// Bad Request
|
||||
if(activeWorkflow.eventStream) {
|
||||
activeWorkflow.eventStream.write(`data: ${activeWorkflow.error}\n\n`);
|
||||
activeWorkflow.eventStream.end();
|
||||
}
|
||||
}
|
||||
else if (err instanceof Error) {
|
||||
console.error("Internal Server Error", err);
|
||||
activeWorkflow.error = {type: 400, error: err.message, stack: err.stack};
|
||||
activeWorkflow.finished = true;
|
||||
activeWorkflow.finishedAt = Date.now();
|
||||
|
||||
// Internal Server Error
|
||||
if(activeWorkflow.eventStream) {
|
||||
activeWorkflow.eventStream.write(`data: ${activeWorkflow.error}\n\n`);
|
||||
activeWorkflow.eventStream.end();
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
router.get("/progress/:workflowUuid", (req: Request, res: Response) => {
|
||||
if(!req.params.workflowUuid) {
|
||||
res.status(400).json({"error": "No workflowUuid weres provided."});
|
||||
return;
|
||||
}
|
||||
if(!activeWorkflows.hasOwnProperty(req.params.workflowUuid)) {
|
||||
res.status(400).json({"error": `No workflow with workflowUuid "${req.params.workflowUuid}" was found.`});
|
||||
return;
|
||||
}
|
||||
|
||||
// Return current progress
|
||||
const workflow = activeWorkflows[req.params.workflowUuid];
|
||||
res.status(200).json({ createdAt: workflow.createdAt, finished: workflow.finished, finishedAt: workflow.finishedAt, error: workflow.error });
|
||||
});
|
||||
|
||||
router.get("/progress-stream/:workflowUuid", (req: Request, res: Response) => {
|
||||
if(!req.params.workflowUuid) {
|
||||
res.status(400).json({"error": "No workflowUuid weres provided."});
|
||||
return;
|
||||
}
|
||||
if(!activeWorkflows.hasOwnProperty(req.params.workflowUuid)) {
|
||||
res.status(400).json({"error": `No workflow with workflowUuid "${req.params.workflowUuid}" was found.`});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Check if already done
|
||||
|
||||
// Send realtime updates
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
res.flushHeaders(); // flush the headers to establish SSE with client
|
||||
|
||||
const workflow = activeWorkflows[req.params.workflowUuid];
|
||||
workflow.eventStream = res;
|
||||
|
||||
res.on("close", () => {
|
||||
res.end();
|
||||
// TODO: Abort if not already done?
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/result/:workflowUuid", async (req: Request, res: Response) => {
|
||||
if(!req.params.workflowUuid) {
|
||||
res.status(400).json({"error": "No workflowUuid weres provided."});
|
||||
return;
|
||||
}
|
||||
if(!activeWorkflows.hasOwnProperty(req.params.workflowUuid)) {
|
||||
res.status(400).json({"error": `No workflow with workflowUuid "${req.params.workflowUuid}" was found.`});
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* If workflow isn't done return error
|
||||
* Send file, if there are multiple outputs return as zip
|
||||
* If download is done, delete results / allow deletion within the next 5-60 mins
|
||||
*/
|
||||
const workflow = activeWorkflows[req.params.workflowUuid];
|
||||
if(!workflow.finished) {
|
||||
res.status(202).json({ message: "Workflow hasn't finished yet. Check progress or connect to progress-steam to get notified when its done." });
|
||||
return;
|
||||
}
|
||||
|
||||
await respondWithPdfFiles(res, workflow.result, "workflow-results");
|
||||
// Delete workflow / results when done.
|
||||
delete activeWorkflows[req.params.workflowUuid];
|
||||
});
|
||||
|
||||
router.post("/abort/:workflowUuid", (req: Request, res: Response) => {
|
||||
if(!req.params.workflowUuid) {
|
||||
res.status(400).json({"error": "No workflowUuid weres provided."});
|
||||
return;
|
||||
}
|
||||
if(!activeWorkflows.hasOwnProperty(req.params.workflowUuid)) {
|
||||
res.status(400).json({"error": `No workflow with workflowUuid "${req.params.workflowUuid}" was found.`});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Abort workflow
|
||||
res.status(501).json({"warning": "Abortion has not been implemented yet."});
|
||||
});
|
||||
|
||||
function generateWorkflowID() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -1,13 +0,0 @@
|
||||
import express, { Request, Response } from "express";
|
||||
|
||||
import login from "./login-controller";
|
||||
import logout from "./logout-controller";
|
||||
import register from "./register-controller";
|
||||
import status from "./status-controller";
|
||||
import createAPIKey from "./create-api-key-controller"
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use("/", [createAPIKey, login, logout, register, status]);
|
||||
|
||||
export default router;
|
||||
@@ -1,10 +0,0 @@
|
||||
import * as APIKey from "../../auth/apikey/apikey-controller";
|
||||
import { whenAuthIsEnabled, isAuthorized } from "../../auth/authenticationMiddleware";
|
||||
import express, { Request, Response } from "express";
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/create-api-key', whenAuthIsEnabled, isAuthorized, async function(req: Request, res: Response) {
|
||||
res.json({apikey: await APIKey.createAPIKey(req.user)});
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,16 +0,0 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
|
||||
import passport from "passport";
|
||||
|
||||
router.post("/login", passport.authenticate(['local'], {
|
||||
successRedirect: '/auth/status',
|
||||
failureRedirect: '/auth/login/failure'
|
||||
}));
|
||||
|
||||
router.post('/login/password', passport.authenticate('local', {
|
||||
successRedirect: '/auth/status',
|
||||
failureRedirect: '/auth/login/failure'
|
||||
}));
|
||||
|
||||
export default router;
|
||||
@@ -1,11 +0,0 @@
|
||||
import express, { Request, Response } from "express";
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/logout', function(req, res, next) {
|
||||
req.logout(function(err) {
|
||||
if (err) { return next(err); }
|
||||
res.redirect('/');
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { error } from "pdf-lib";
|
||||
import * as User from "../../auth/user/user-controller";
|
||||
import express, { Request, Response } from "express";
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/register', async function(req: Request, res: Response) {
|
||||
//TODO: Register new user
|
||||
|
||||
});
|
||||
|
||||
router.post('/register/password', async function(req: Request, res: Response) {
|
||||
if(req.query) {
|
||||
if(!req.query.username) {
|
||||
res.status(400).json({error: "no username was provided"});
|
||||
return;
|
||||
}
|
||||
if(!req.query.password) {
|
||||
res.status(400).json({error: "no password was provided"});
|
||||
return;
|
||||
}
|
||||
|
||||
User.createUser({username: req.query.username as string, password: req.query.password as string}, async (err, user) => {
|
||||
if(err) {
|
||||
res.status(500).json(err);
|
||||
return;
|
||||
}
|
||||
res.json(user);
|
||||
});
|
||||
}
|
||||
else {
|
||||
res.status(400).json({error: "no params were provided"})
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,9 +0,0 @@
|
||||
import { isAuthorized } from "../../auth/authenticationMiddleware";
|
||||
import express, { Request, Response } from "express";
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/status', isAuthorized, async function(req: Request, res: Response) {
|
||||
res.json({user: req.user});
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,60 +0,0 @@
|
||||
|
||||
import { Response } from "express";
|
||||
import { PdfFile } from "@stirling-pdf/shared-operations/src/wrappers/PdfFile";
|
||||
import Archiver from "archiver";
|
||||
|
||||
async function respondWithFile(res: Response, uint8Array: Uint8Array, filename: string, mimeType: string): Promise<void> {
|
||||
res.writeHead(200, {
|
||||
"Content-Type": mimeType,
|
||||
"Content-disposition": `attachment; filename="${filename}"`,
|
||||
"Content-Length": uint8Array.length
|
||||
});
|
||||
res.end(uint8Array);
|
||||
}
|
||||
|
||||
async function respondWithPdfFile(res: Response, file: PdfFile): Promise<void> {
|
||||
const byteArray = await file.uint8Array;
|
||||
respondWithFile(res, byteArray, file.filename+".pdf", "application/pdf");
|
||||
}
|
||||
|
||||
async function respondWithZip(res: Response, filename: string, files: {uint8Array: Uint8Array, filename: string}[]): Promise<void> {
|
||||
if (files.length == 0) {
|
||||
res.status(500).json({"warning": "The workflow had no outputs."});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(filename);
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "application/zip",
|
||||
"Content-disposition": `attachment; filename="${filename}.zip"`,
|
||||
});
|
||||
|
||||
// TODO: Also allow changing the compression level
|
||||
const zip = Archiver("zip");
|
||||
|
||||
// Stream the file to the user.
|
||||
zip.pipe(res);
|
||||
|
||||
console.log("Adding Files to ZIP...");
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
zip.append(Buffer.from(files[i].uint8Array), { name: files[i].filename });
|
||||
}
|
||||
|
||||
zip.finalize();
|
||||
console.log("Sent");
|
||||
}
|
||||
|
||||
export async function respondWithPdfFiles(res: Response, pdfFiles: PdfFile[] | undefined, filename: string) {
|
||||
if(!pdfFiles || pdfFiles.length == 0) {
|
||||
res.status(500).json({"warning": "The workflow had no outputs."});
|
||||
}
|
||||
else if (pdfFiles.length == 1) {
|
||||
respondWithPdfFile(res, pdfFiles[0]);
|
||||
}
|
||||
else {
|
||||
const promises = pdfFiles.map(async (pdf) => {return{uint8Array: await pdf.uint8Array, filename: pdf.filename + ".pdf"}});
|
||||
const files = await Promise.all(promises);
|
||||
respondWithZip(res, filename, files);
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
// "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "ES2020", /* Specify what module code is generated. */
|
||||
"rootDir": "../", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "Node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
"baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */
|
||||
"paths": {
|
||||
"#pdfcpu": ["../shared-operations/src/wasm/pdfcpu/pdfcpu-wrapper.server.js"],
|
||||
"@stirling-pdf/*": [ "../../*" ]
|
||||
}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"types": [
|
||||
"vite/client"
|
||||
], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"./declarations/",
|
||||
"../shared-operations/declarations/"
|
||||
],
|
||||
"ts-node": {
|
||||
"experimentalSpecifierResolution": "node",
|
||||
"transpileOnly": true,
|
||||
"esm": true
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import topLevelAwait from "vite-plugin-top-level-await";
|
||||
import dynamicImport from 'vite-plugin-dynamic-import'
|
||||
import compileTime from "vite-plugin-compile-time"
|
||||
import { VitePluginNode } from 'vite-plugin-node';
|
||||
|
||||
export default defineConfig({
|
||||
// ...vite configures
|
||||
server: {
|
||||
// vite server configs, for details see [vite doc](https://vitejs.dev/config/#server-host)
|
||||
port: 8000
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
"pg-hstore" // sequelize
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
...VitePluginNode({
|
||||
// Nodejs native Request adapter
|
||||
// currently this plugin support 'express', 'nest', 'koa' and 'fastify' out of box,
|
||||
// you can also pass a function if you are using other frameworks, see Custom Adapter section
|
||||
adapter: 'express',
|
||||
|
||||
// tell the plugin where is your project entry
|
||||
appPath: './src/index.ts',
|
||||
|
||||
// Optional, default: false
|
||||
// if you want to init your app on boot, set this to true
|
||||
initAppOnBoot: true,
|
||||
}),
|
||||
topLevelAwait({
|
||||
// The export name of top-level await promise for each chunk module
|
||||
promiseExportName: "__tla",
|
||||
// The function to generate import names of top-level await promise in each chunk module
|
||||
promiseImportName: i => `__tla_${i}`
|
||||
}),
|
||||
compileTime(),
|
||||
dynamicImport(),
|
||||
],
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
// TODO: This file can probably be removed by now
|
||||
|
||||
export interface Action {
|
||||
values: any;
|
||||
type: "wait" | "done" | "impose" | string;
|
||||
actions?: Action[];
|
||||
}
|
||||
|
||||
export interface WaitAction extends Action {
|
||||
values: { id: number }
|
||||
}
|
||||
|
||||
export interface ExtractAction extends Action {
|
||||
values: { indexes: string | number[] }
|
||||
}
|
||||
|
||||
export interface ImposeAction extends Action {
|
||||
values: { nup: number, format: string }
|
||||
}
|
||||
|
||||
export interface WaitAction extends Action {
|
||||
values: { id: number }
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
declare interface ImportMeta {
|
||||
compileTime: <T>(file: string) => T
|
||||
}
|
||||