mirror of
https://github.com/TheAnachronism/docspell.git
synced 2024-11-13 02:31:10 +00:00
Compare commits
No commits in common. "32cc994b30629dc71c2f74bda9dbbfb97bb930cb" and "e3faff7acfa6451be85e01836a3579b4c9b519d5" have entirely different histories.
32cc994b30
...
e3faff7acf
@ -1,2 +0,0 @@
|
|||||||
# Scala Steward: Reformat with scalafmt 3.8.2
|
|
||||||
1c566cd5182d41f4cc06040fc347ddb4be617779
|
|
42
.github/release-drafter.yml
vendored
42
.github/release-drafter.yml
vendored
@ -1,42 +0,0 @@
|
|||||||
name-template: "$RESOLVED_VERSION"
|
|
||||||
tag-template: "$RESOLVED_VERSION"
|
|
||||||
template: |
|
|
||||||
## What’s Changed
|
|
||||||
|
|
||||||
$CHANGES
|
|
||||||
|
|
||||||
categories:
|
|
||||||
- title: "🚀 Features"
|
|
||||||
labels:
|
|
||||||
- 'feature'
|
|
||||||
- 'enhancement'
|
|
||||||
- title: "🐛 Bug Fixes"
|
|
||||||
labels:
|
|
||||||
- 'fix'
|
|
||||||
- 'bug'
|
|
||||||
- title: "💚 Maintenance"
|
|
||||||
labels:
|
|
||||||
- 'chore'
|
|
||||||
- 'documentation'
|
|
||||||
- title: "🧱 Dependencies"
|
|
||||||
labels:
|
|
||||||
- 'dependencies'
|
|
||||||
- 'type: dependencies'
|
|
||||||
|
|
||||||
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
|
||||||
version-resolver:
|
|
||||||
major:
|
|
||||||
labels:
|
|
||||||
- 'breaking'
|
|
||||||
minor:
|
|
||||||
labels:
|
|
||||||
- 'feature'
|
|
||||||
- 'enhancement'
|
|
||||||
patch:
|
|
||||||
labels:
|
|
||||||
- 'chore'
|
|
||||||
- 'documentation'
|
|
||||||
- 'dependencies'
|
|
||||||
default: patch
|
|
||||||
exclude-labels:
|
|
||||||
- 'skip-changelog'
|
|
2
.github/renovate.json
vendored
2
.github/renovate.json
vendored
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"automerge": true,
|
"automerge": true,
|
||||||
"labels": ["dependencies"],
|
"labels": ["type: dependencies"],
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"matchManagers": [
|
"matchManagers": [
|
||||||
|
16
.github/stale.yml
vendored
Normal file
16
.github/stale.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Number of days of inactivity before an issue becomes stale
|
||||||
|
daysUntilStale: 30
|
||||||
|
# Number of days of inactivity before a stale issue is closed
|
||||||
|
daysUntilClose: 7
|
||||||
|
onlyLabels:
|
||||||
|
- question
|
||||||
|
# Label to use when marking an issue as stale
|
||||||
|
staleLabel: stale
|
||||||
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
|
markComment: >
|
||||||
|
This issue has been automatically marked as stale because it has not
|
||||||
|
had recent activity. It will be closed if no further activity
|
||||||
|
occurs. This only applies to 'question' issues. Always feel free to
|
||||||
|
reopen or create new issues. Thank you!
|
||||||
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
|
closeComment: false
|
15
.github/workflows/ci-docs.yml
vendored
15
.github/workflows/ci-docs.yml
vendored
@ -6,13 +6,20 @@ on:
|
|||||||
- "master"
|
- "master"
|
||||||
jobs:
|
jobs:
|
||||||
check-website:
|
check-website:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.7
|
- uses: actions/checkout@v4.1.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: cachix/install-nix-action@v27
|
|
||||||
- name: Set current version
|
- name: Set current version
|
||||||
run: echo "DOCSPELL_VERSION=$(cat version.sbt | grep version | cut -d= -f2 | xargs)" >> $GITHUB_ENV
|
run: echo "DOCSPELL_VERSION=$(cat version.sbt | grep version | cut -d= -f2 | xargs)" >> $GITHUB_ENV
|
||||||
|
- uses: jorelali/setup-elm@v5
|
||||||
|
with:
|
||||||
|
elm-version: 0.19.1
|
||||||
|
- uses: cachix/install-nix-action@v25
|
||||||
|
with:
|
||||||
|
nix_path: nixpkgs=channel:nixos-23.05
|
||||||
|
- name: Print nixpkgs version
|
||||||
|
run: nix-instantiate --eval -E '(import <nixpkgs> {}).lib.version'
|
||||||
- name: Build website (${{ env.DOCSPELL_VERSION }})
|
- name: Build website (${{ env.DOCSPELL_VERSION }})
|
||||||
run: nix develop .#ci --command sbt make-website
|
run: nix-shell website/shell.nix --run "sbt make-website"
|
||||||
|
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@ -5,18 +5,30 @@ on:
|
|||||||
- master
|
- master
|
||||||
jobs:
|
jobs:
|
||||||
ci-matrix:
|
ci-matrix:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
java: [ 'openjdk@1.17' ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.7
|
- uses: actions/checkout@v4.1.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 100
|
fetch-depth: 100
|
||||||
|
- uses: jorelali/setup-elm@v5
|
||||||
|
with:
|
||||||
|
elm-version: 0.19.1
|
||||||
|
- uses: bahmutov/npm-install@v1
|
||||||
|
with:
|
||||||
|
working-directory: modules/webapp
|
||||||
- name: Fetch tags
|
- name: Fetch tags
|
||||||
run: git fetch --depth=100 origin +refs/tags/*:refs/tags/*
|
run: git fetch --depth=100 origin +refs/tags/*:refs/tags/*
|
||||||
- uses: cachix/install-nix-action@v27
|
- uses: olafurpg/setup-scala@v14
|
||||||
|
with:
|
||||||
|
java-version: ${{ matrix.java }}
|
||||||
|
# - name: Coursier cache
|
||||||
|
# uses: coursier/cache-action@v6
|
||||||
- name: sbt ci ${{ github.ref }}
|
- name: sbt ci ${{ github.ref }}
|
||||||
run: nix develop .#ci --command sbt ci
|
run: sbt ci
|
||||||
ci:
|
ci:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: [ci-matrix]
|
needs: [ci-matrix]
|
||||||
|
4
.github/workflows/docker-image.yml
vendored
4
.github/workflows/docker-image.yml
vendored
@ -4,9 +4,9 @@ on:
|
|||||||
types: [ published ]
|
types: [ published ]
|
||||||
jobs:
|
jobs:
|
||||||
docker-images:
|
docker-images:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.7
|
- uses: actions/checkout@v4.1.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set current version
|
- name: Set current version
|
||||||
|
14
.github/workflows/release-drafter.yml
vendored
14
.github/workflows/release-drafter.yml
vendored
@ -1,14 +0,0 @@
|
|||||||
name: Release Drafter
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update_release_draft:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: release-drafter/release-drafter@v6
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
22
.github/workflows/release-nightly.yml
vendored
22
.github/workflows/release-nightly.yml
vendored
@ -5,20 +5,32 @@ on:
|
|||||||
- "master"
|
- "master"
|
||||||
jobs:
|
jobs:
|
||||||
release-nightly:
|
release-nightly:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
java: [ 'openjdk@1.17' ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.7
|
- uses: actions/checkout@v4.1.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: cachix/install-nix-action@v27
|
- uses: olafurpg/setup-scala@v14
|
||||||
|
with:
|
||||||
|
java-version: ${{ matrix.java }}
|
||||||
|
- uses: jorelali/setup-elm@v5
|
||||||
|
with:
|
||||||
|
elm-version: 0.19.1
|
||||||
|
- uses: bahmutov/npm-install@v1
|
||||||
|
with:
|
||||||
|
working-directory: modules/webapp
|
||||||
|
# - name: Coursier cache
|
||||||
|
# uses: coursier/cache-action@v6
|
||||||
- name: Set current version
|
- name: Set current version
|
||||||
run: echo "DOCSPELL_VERSION=$(cat version.sbt | grep version | cut -d= -f2 | xargs)" >> $GITHUB_ENV
|
run: echo "DOCSPELL_VERSION=$(cat version.sbt | grep version | cut -d= -f2 | xargs)" >> $GITHUB_ENV
|
||||||
- name: sbt ci ${{ github.ref }}
|
- name: sbt ci ${{ github.ref }}
|
||||||
run: nix develop .#ci --command sbt ci
|
run: sbt ci
|
||||||
- name: sbt make-pkg (${{ env.DOCSPELL_VERSION }})
|
- name: sbt make-pkg (${{ env.DOCSPELL_VERSION }})
|
||||||
run: nix develop .#ci --command sbt make-pkg
|
run: sbt make-pkg
|
||||||
- uses: "marvinpinto/action-automatic-releases@latest"
|
- uses: "marvinpinto/action-automatic-releases@latest"
|
||||||
with:
|
with:
|
||||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@ -5,18 +5,30 @@ on:
|
|||||||
- 'v*'
|
- 'v*'
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
java: [ 'openjdk@1.17' ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.7
|
- uses: actions/checkout@v4.1.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: cachix/install-nix-action@v27
|
- uses: olafurpg/setup-scala@v14
|
||||||
|
with:
|
||||||
|
java-version: ${{ matrix.java }}
|
||||||
|
- uses: jorelali/setup-elm@v5
|
||||||
|
with:
|
||||||
|
elm-version: 0.19.1
|
||||||
|
- uses: bahmutov/npm-install@v1
|
||||||
|
with:
|
||||||
|
working-directory: modules/webapp
|
||||||
|
# - name: Coursier cache
|
||||||
|
# uses: coursier/cache-action@v6
|
||||||
- name: Set current version
|
- name: Set current version
|
||||||
run: echo "DOCSPELL_VERSION=$(cat version.sbt | grep version | cut -d= -f2 | xargs)" >> $GITHUB_ENV
|
run: echo "DOCSPELL_VERSION=$(cat version.sbt | grep version | cut -d= -f2 | xargs)" >> $GITHUB_ENV
|
||||||
- name: sbt make-pkg (${{ env.DOCSPELL_VERSION }})
|
- name: sbt make-pkg (${{ env.DOCSPELL_VERSION }})
|
||||||
run: nix develop .#ci --command sbt make-pkg
|
run: sbt make-pkg
|
||||||
- uses: meeDamian/github-release@2.0
|
- uses: meeDamian/github-release@2.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
21
.github/workflows/stale.yml
vendored
21
.github/workflows/stale.yml
vendored
@ -1,21 +0,0 @@
|
|||||||
name: 'Handle stale issues'
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '30 1 * * *'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
stale:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# https://github.com/actions/stale
|
|
||||||
- uses: actions/stale@v9
|
|
||||||
with:
|
|
||||||
days-before-stale: 30
|
|
||||||
days-before-close: 7
|
|
||||||
only-labels: question
|
|
||||||
stale-issue-label: stale
|
|
||||||
stale-issue-message: >
|
|
||||||
This issue has been automatically marked as stale because it has not
|
|
||||||
had recent activity. It will be closed if no further activity
|
|
||||||
occurs. This only applies to 'question' issues. Always feel free to
|
|
||||||
reopen or create new issues. Thank you!
|
|
17
.github/workflows/website.yml
vendored
17
.github/workflows/website.yml
vendored
@ -5,17 +5,24 @@ on:
|
|||||||
- "current-docs"
|
- "current-docs"
|
||||||
jobs:
|
jobs:
|
||||||
publish-website:
|
publish-website:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.7
|
- uses: actions/checkout@v4.1.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: cachix/install-nix-action@v27
|
|
||||||
- name: Set current version
|
- name: Set current version
|
||||||
run: echo "DOCSPELL_VERSION=$(cat version.sbt | grep version | cut -d= -f2 | xargs)" >> $GITHUB_ENV
|
run: echo "DOCSPELL_VERSION=$(cat version.sbt | grep version | cut -d= -f2 | xargs)" >> $GITHUB_ENV
|
||||||
|
- uses: jorelali/setup-elm@v5
|
||||||
|
with:
|
||||||
|
elm-version: 0.19.1
|
||||||
|
- uses: cachix/install-nix-action@v25
|
||||||
|
with:
|
||||||
|
nix_path: nixpkgs=channel:nixos-23.05
|
||||||
|
- name: Print nixpkgs version
|
||||||
|
run: nix-instantiate --eval -E '(import <nixpkgs> {}).lib.version'
|
||||||
- name: Build website (${{ env.DOCSPELL_VERSION }})
|
- name: Build website (${{ env.DOCSPELL_VERSION }})
|
||||||
run: nix develop .#ci --command sbt make-website
|
run: nix-shell website/shell.nix --run "sbt make-website"
|
||||||
- name: Publish website (${{ env.DOCSPELL_VERSION }})
|
- name: Publish website (${{ env.DOCSPELL_VERSION }})
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: nix develop .#ci --command sbt publish-website
|
run: sbt publish-website
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,4 @@
|
|||||||
#artwork/*.png
|
#artwork/*.png
|
||||||
.envrc
|
|
||||||
target/
|
target/
|
||||||
local/
|
local/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
@ -6,7 +6,7 @@ pull_request_rules:
|
|||||||
assign:
|
assign:
|
||||||
users: [eikek]
|
users: [eikek]
|
||||||
label:
|
label:
|
||||||
add: ["dependencies"]
|
add: ["type: dependencies"]
|
||||||
- name: automatically merge Scala Steward PRs on CI success
|
- name: automatically merge Scala Steward PRs on CI success
|
||||||
conditions:
|
conditions:
|
||||||
- author=eikek-scala-steward[bot]
|
- author=eikek-scala-steward[bot]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
version = "3.8.2"
|
version = "3.7.17"
|
||||||
|
|
||||||
preset = default
|
preset = default
|
||||||
align.preset = some
|
align.preset = some
|
||||||
|
@ -1020,7 +1020,7 @@ Additionally there are some other minor features and bug fixes.
|
|||||||
to be able to add a request header. Check [this for
|
to be able to add a request header. Check [this for
|
||||||
firefox](https://addons.mozilla.org/en-US/firefox/addon/modheader-firefox/)
|
firefox](https://addons.mozilla.org/en-US/firefox/addon/modheader-firefox/)
|
||||||
or [this for
|
or [this for
|
||||||
chromium](https://chromewebstore.google.com/detail/modheader-modify-http-hea/idgpnmonknjnojddfkpgkljpfnnfcklj)
|
chromium](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj)
|
||||||
- then add the request header `Docspell-Ui` with value `1`.
|
- then add the request header `Docspell-Ui` with value `1`.
|
||||||
Reloading the page gets you back the old ui.
|
Reloading the page gets you back the old ui.
|
||||||
- With new Web-UI, certain features and fixes were realized, but not
|
- With new Web-UI, certain features and fixes were realized, but not
|
||||||
|
23
build.sbt
23
build.sbt
@ -15,14 +15,11 @@ val scalafixSettings = Seq(
|
|||||||
|
|
||||||
val sharedSettings = Seq(
|
val sharedSettings = Seq(
|
||||||
organization := "com.github.eikek",
|
organization := "com.github.eikek",
|
||||||
scalaVersion := "2.13.14",
|
scalaVersion := "2.13.12",
|
||||||
organizationName := "Eike K. & Contributors",
|
organizationName := "Eike K. & Contributors",
|
||||||
licenses += (
|
licenses += ("AGPL-3.0-or-later", url(
|
||||||
"AGPL-3.0-or-later",
|
"https://spdx.org/licenses/AGPL-3.0-or-later.html"
|
||||||
url(
|
)),
|
||||||
"https://spdx.org/licenses/AGPL-3.0-or-later.html"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
startYear := Some(2020),
|
startYear := Some(2020),
|
||||||
headerLicenseStyle := HeaderLicenseStyle.SpdxSyntax,
|
headerLicenseStyle := HeaderLicenseStyle.SpdxSyntax,
|
||||||
headerSources / excludeFilter := HiddenFileFilter || "*.java" || "StringUtil.scala",
|
headerSources / excludeFilter := HiddenFileFilter || "*.java" || "StringUtil.scala",
|
||||||
@ -680,11 +677,7 @@ val restapi = project
|
|||||||
openapiTargetLanguage := Language.Scala,
|
openapiTargetLanguage := Language.Scala,
|
||||||
openapiPackage := Pkg("docspell.restapi.model"),
|
openapiPackage := Pkg("docspell.restapi.model"),
|
||||||
openapiSpec := (Compile / resourceDirectory).value / "docspell-openapi.yml",
|
openapiSpec := (Compile / resourceDirectory).value / "docspell-openapi.yml",
|
||||||
openapiStaticGen := OpenApiDocGenerator.Redoc,
|
openapiStaticGen := OpenApiDocGenerator.Redoc
|
||||||
openapiRedoclyCmd := Seq("redocly-cli"),
|
|
||||||
openapiRedoclyConfig := Some(
|
|
||||||
(LocalRootProject / baseDirectory).value / "project" / "redocly.yml"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.dependsOn(common, query.jvm, notificationApi, jsonminiq, addonlib)
|
.dependsOn(common, query.jvm, notificationApi, jsonminiq, addonlib)
|
||||||
|
|
||||||
@ -704,11 +697,7 @@ val joexapi = project
|
|||||||
openapiTargetLanguage := Language.Scala,
|
openapiTargetLanguage := Language.Scala,
|
||||||
openapiPackage := Pkg("docspell.joexapi.model"),
|
openapiPackage := Pkg("docspell.joexapi.model"),
|
||||||
openapiSpec := (Compile / resourceDirectory).value / "joex-openapi.yml",
|
openapiSpec := (Compile / resourceDirectory).value / "joex-openapi.yml",
|
||||||
openapiStaticGen := OpenApiDocGenerator.Redoc,
|
openapiStaticGen := OpenApiDocGenerator.Redoc
|
||||||
openapiRedoclyCmd := Seq("redocly-cli"),
|
|
||||||
openapiRedoclyConfig := Some(
|
|
||||||
(LocalRootProject / baseDirectory).value / "project" / "redocly.yml"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.dependsOn(common, loggingScribe, addonlib)
|
.dependsOn(common, loggingScribe, addonlib)
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ services:
|
|||||||
- restserver
|
- restserver
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:16.3
|
image: postgres:16.1
|
||||||
container_name: postgres_db
|
container_name: postgres_db
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.20.2
|
FROM alpine:20231219
|
||||||
|
|
||||||
ARG version=
|
ARG version=
|
||||||
ARG joex_url=
|
ARG joex_url=
|
||||||
@ -77,7 +77,7 @@ RUN \
|
|||||||
wget https://github.com/tesseract-ocr/tessdata/raw/main/khm.traineddata && \
|
wget https://github.com/tesseract-ocr/tessdata/raw/main/khm.traineddata && \
|
||||||
mv khm.traineddata /usr/share/tessdata
|
mv khm.traineddata /usr/share/tessdata
|
||||||
|
|
||||||
# Using these data files for japanese, because they work better. Includes vertical data. See #973 and #2445.
|
# Using these data files for japanese, because they work better. See #973
|
||||||
RUN \
|
RUN \
|
||||||
wget https://raw.githubusercontent.com/tesseract-ocr/tessdata_fast/master/jpn_vert.traineddata && \
|
wget https://raw.githubusercontent.com/tesseract-ocr/tessdata_fast/master/jpn_vert.traineddata && \
|
||||||
wget https://raw.githubusercontent.com/tesseract-ocr/tessdata_fast/master/jpn.traineddata && \
|
wget https://raw.githubusercontent.com/tesseract-ocr/tessdata_fast/master/jpn.traineddata && \
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.20.2
|
FROM alpine:20231219
|
||||||
|
|
||||||
ARG version=
|
ARG version=
|
||||||
ARG restserver_url=
|
ARG restserver_url=
|
||||||
|
130
flake.lock
130
flake.lock
@ -1,130 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"devshell-tools": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1710099997,
|
|
||||||
"narHash": "sha256-WmBKTLdth6I/D+0//9enbIXohGsBjepbjIAm9pCYj0U=",
|
|
||||||
"owner": "eikek",
|
|
||||||
"repo": "devshell-tools",
|
|
||||||
"rev": "e82faf976d318b3829f6f7f6785db6f3c7b65267",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "eikek",
|
|
||||||
"repo": "devshell-tools",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1709126324,
|
|
||||||
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils_2": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems_2"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1709126324,
|
|
||||||
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1709309926,
|
|
||||||
"narHash": "sha256-VZFBtXGVD9LWTecGi6eXrE0hJ/mVB3zGUlHImUs2Qak=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "79baff8812a0d68e24a836df0a364c678089e2c7",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-23.11",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1709677081,
|
|
||||||
"narHash": "sha256-tix36Y7u0rkn6mTm0lA45b45oab2cFLqAzDbJxeXS+c=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "880992dcc006a5e00dd0591446fdf723e6a51a64",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-23.11",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"devshell-tools": "devshell-tools",
|
|
||||||
"flake-utils": "flake-utils_2",
|
|
||||||
"nixpkgs": "nixpkgs_2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
193
flake.nix
193
flake.nix
@ -1,193 +0,0 @@
|
|||||||
{
|
|
||||||
description = "Docspell";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
|
|
||||||
devshell-tools.url = "github:eikek/devshell-tools";
|
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = {
|
|
||||||
self,
|
|
||||||
nixpkgs,
|
|
||||||
flake-utils,
|
|
||||||
devshell-tools,
|
|
||||||
}:
|
|
||||||
flake-utils.lib.eachDefaultSystem (system: let
|
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
|
||||||
sbt17 = pkgs.sbt.override {jre = pkgs.jdk17;};
|
|
||||||
ciPkgs = with pkgs; [
|
|
||||||
sbt17
|
|
||||||
jdk17
|
|
||||||
dpkg
|
|
||||||
elmPackages.elm
|
|
||||||
fakeroot
|
|
||||||
zola
|
|
||||||
yarn
|
|
||||||
nodejs
|
|
||||||
redocly-cli
|
|
||||||
tailwindcss
|
|
||||||
];
|
|
||||||
devshellPkgs =
|
|
||||||
ciPkgs
|
|
||||||
++ (with pkgs; [
|
|
||||||
jq
|
|
||||||
scala-cli
|
|
||||||
netcat
|
|
||||||
wget
|
|
||||||
which
|
|
||||||
inotifyTools
|
|
||||||
]);
|
|
||||||
docspellPkgs = pkgs.callPackage (import ./nix/pkg.nix) {};
|
|
||||||
dockerAmd64 = pkgs.pkgsCross.gnu64.callPackage (import ./nix/docker.nix) {
|
|
||||||
inherit (docspellPkgs) docspell-restserver docspell-joex;
|
|
||||||
};
|
|
||||||
dockerArm64 = pkgs.pkgsCross.aarch64-multiplatform.callPackage (import ./nix/docker.nix) {
|
|
||||||
inherit (docspellPkgs) docspell-restserver docspell-joex;
|
|
||||||
};
|
|
||||||
in {
|
|
||||||
formatter = pkgs.alejandra;
|
|
||||||
|
|
||||||
packages = {
|
|
||||||
inherit (docspellPkgs) docspell-restserver docspell-joex;
|
|
||||||
};
|
|
||||||
|
|
||||||
legacyPackages = {
|
|
||||||
docker = {
|
|
||||||
amd64 = {
|
|
||||||
inherit (dockerAmd64) docspell-restserver docspell-joex;
|
|
||||||
};
|
|
||||||
arm64 = {
|
|
||||||
inherit (dockerArm64) docspell-restserver docspell-joex;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
checks = {
|
|
||||||
build-server = self.packages.${system}.docspell-restserver;
|
|
||||||
build-joex = self.packages.${system}.docspell-joex;
|
|
||||||
|
|
||||||
test = with import (nixpkgs + "/nixos/lib/testing-python.nix")
|
|
||||||
{
|
|
||||||
inherit system;
|
|
||||||
};
|
|
||||||
makeTest {
|
|
||||||
name = "docspell";
|
|
||||||
nodes = {
|
|
||||||
machine = {...}: {
|
|
||||||
nixpkgs.overlays = [self.overlays.default];
|
|
||||||
imports = [
|
|
||||||
self.nixosModules.default
|
|
||||||
./nix/checks
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
testScript = builtins.readFile ./nix/checks/testScript.py;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
devShells = {
|
|
||||||
dev-cnt = pkgs.mkShellNoCC {
|
|
||||||
buildInputs =
|
|
||||||
(builtins.attrValues devshell-tools.legacyPackages.${system}.cnt-scripts)
|
|
||||||
++ devshellPkgs;
|
|
||||||
|
|
||||||
DOCSPELL_ENV = "dev";
|
|
||||||
DEV_CONTAINER = "docsp-dev";
|
|
||||||
SBT_OPTS = "-Xmx2G -Xss4m";
|
|
||||||
};
|
|
||||||
dev-vm = pkgs.mkShellNoCC {
|
|
||||||
buildInputs =
|
|
||||||
(builtins.attrValues devshell-tools.legacyPackages.${system}.vm-scripts)
|
|
||||||
++ devshellPkgs;
|
|
||||||
|
|
||||||
DOCSPELL_ENV = "dev";
|
|
||||||
SBT_OPTS = "-Xmx2G -Xss4m";
|
|
||||||
DEV_VM = "dev-vm";
|
|
||||||
VM_SSH_PORT = "10022";
|
|
||||||
};
|
|
||||||
ci = pkgs.mkShellNoCC {
|
|
||||||
buildInputs = ciPkgs;
|
|
||||||
SBT_OPTS = "-Xmx2G -Xss4m";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
// {
|
|
||||||
nixosModules = {
|
|
||||||
default = {...}: {
|
|
||||||
imports = [
|
|
||||||
./nix/modules/server.nix
|
|
||||||
./nix/modules/joex.nix
|
|
||||||
];
|
|
||||||
};
|
|
||||||
server = import ./nix/modules/server.nix;
|
|
||||||
joex = import ./nix/modules/joex.nix;
|
|
||||||
};
|
|
||||||
|
|
||||||
overlays.default = final: prev: let
|
|
||||||
docspellPkgs = final.callPackage (import ./nix/pkg.nix) {};
|
|
||||||
in {
|
|
||||||
inherit (docspellPkgs) docspell-restserver docspell-joex;
|
|
||||||
};
|
|
||||||
|
|
||||||
nixosConfigurations = {
|
|
||||||
test-vm = devshell-tools.lib.mkVm {
|
|
||||||
system = "x86_64-linux";
|
|
||||||
modules = [
|
|
||||||
self.nixosModules.default
|
|
||||||
{
|
|
||||||
nixpkgs.overlays = [self.overlays.default];
|
|
||||||
}
|
|
||||||
./nix/test-vm.nix
|
|
||||||
];
|
|
||||||
};
|
|
||||||
docsp-dev = devshell-tools.lib.mkContainer {
|
|
||||||
system = "x86_64-linux";
|
|
||||||
modules = [
|
|
||||||
{
|
|
||||||
services.dev-postgres = {
|
|
||||||
enable = true;
|
|
||||||
databases = ["docspell"];
|
|
||||||
};
|
|
||||||
services.dev-email.enable = true;
|
|
||||||
services.dev-minio.enable = true;
|
|
||||||
services.dev-solr = {
|
|
||||||
enable = true;
|
|
||||||
cores = ["docspell"];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
dev-vm = devshell-tools.lib.mkVm {
|
|
||||||
system = "x86_64-linux";
|
|
||||||
modules = [
|
|
||||||
{
|
|
||||||
networking.hostName = "dev-vm";
|
|
||||||
virtualisation.memorySize = 2048;
|
|
||||||
|
|
||||||
services.dev-postgres = {
|
|
||||||
enable = true;
|
|
||||||
databases = ["docspell"];
|
|
||||||
};
|
|
||||||
services.dev-email.enable = true;
|
|
||||||
services.dev-minio.enable = true;
|
|
||||||
services.dev-solr = {
|
|
||||||
enable = true;
|
|
||||||
cores = ["docspell"];
|
|
||||||
heap = 512;
|
|
||||||
};
|
|
||||||
port-forward.ssh = 10022;
|
|
||||||
port-forward.dev-postgres = 6534;
|
|
||||||
port-forward.dev-smtp = 10025;
|
|
||||||
port-forward.dev-imap = 10143;
|
|
||||||
port-forward.dev-webmail = 8080;
|
|
||||||
port-forward.dev-minio-api = 9000;
|
|
||||||
port-forward.dev-minio-console = 9001;
|
|
||||||
port-forward.dev-solr = 8983;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
@ -38,9 +38,9 @@ final case class AddonArchive(url: LenientUri, name: String, version: String) {
|
|||||||
Files[F].createDirectories(target) *>
|
Files[F].createDirectories(target) *>
|
||||||
reader(url)
|
reader(url)
|
||||||
.through(Zip[F](logger.some).unzip(glob = glob, targetDir = target.some))
|
.through(Zip[F](logger.some).unzip(glob = glob, targetDir = target.some))
|
||||||
|
.evalTap(_ => Directory.unwrapSingle[F](logger, target))
|
||||||
.compile
|
.compile
|
||||||
.drain
|
.drain
|
||||||
.flatTap(_ => Directory.unwrapSingle[F](logger, target))
|
|
||||||
.as(target)
|
.as(target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ private[addons] object RunnerUtil {
|
|||||||
): F[AddonResult] =
|
): F[AddonResult] =
|
||||||
for {
|
for {
|
||||||
stdout <-
|
stdout <-
|
||||||
if (ctx.meta.parseResult) CollectOut.buffer[F]
|
if (ctx.meta.options.exists(_.collectOutput)) CollectOut.buffer[F]
|
||||||
else CollectOut.none[F].pure[F]
|
else CollectOut.none[F].pure[F]
|
||||||
cmdResult <- SysExec(cmd, logger, ctx.baseDir.some)
|
cmdResult <- SysExec(cmd, logger, ctx.baseDir.some)
|
||||||
.flatMap(
|
.flatMap(
|
||||||
@ -135,7 +135,7 @@ private[addons] object RunnerUtil {
|
|||||||
out <- stdout.get
|
out <- stdout.get
|
||||||
_ <- logger.debug(s"Addon stdout: $out")
|
_ <- logger.debug(s"Addon stdout: $out")
|
||||||
result = Option
|
result = Option
|
||||||
.when(ctx.meta.parseResult && out.nonEmpty)(
|
.when(ctx.meta.options.exists(_.collectOutput) && out.nonEmpty)(
|
||||||
JsonParser
|
JsonParser
|
||||||
.decode[AddonOutput](out)
|
.decode[AddonOutput](out)
|
||||||
.fold(AddonResult.decodingError, AddonResult.success)
|
.fold(AddonResult.decodingError, AddonResult.success)
|
||||||
|
Binary file not shown.
@ -9,7 +9,7 @@ package docspell.addons
|
|||||||
import cats.effect._
|
import cats.effect._
|
||||||
import cats.syntax.option._
|
import cats.syntax.option._
|
||||||
|
|
||||||
import docspell.common._
|
import docspell.common.UrlReader
|
||||||
import docspell.logging.TestLoggingConfig
|
import docspell.logging.TestLoggingConfig
|
||||||
|
|
||||||
import munit._
|
import munit._
|
||||||
@ -42,20 +42,10 @@ class AddonArchiveTest extends CatsEffectSuite with TestLoggingConfig with Fixtu
|
|||||||
} yield ()
|
} yield ()
|
||||||
}
|
}
|
||||||
|
|
||||||
tempDir.test("read archive from zip with yaml only") { dir =>
|
|
||||||
for {
|
|
||||||
aa <- AddonArchive.read[IO](singleFileAddonUrl, UrlReader.defaultReader[IO], None)
|
|
||||||
_ = assertEquals(aa.version, "0.7.0")
|
|
||||||
path <- aa.extractTo(UrlReader.defaultReader[IO], dir)
|
|
||||||
read <- AddonArchive.read[IO](aa.url, UrlReader.defaultReader[IO], path.some)
|
|
||||||
_ = assertEquals(aa, read)
|
|
||||||
} yield ()
|
|
||||||
}
|
|
||||||
|
|
||||||
tempDir.test("Read generated addon from path") { dir =>
|
tempDir.test("Read generated addon from path") { dir =>
|
||||||
AddonGenerator.successAddon("mini-addon").use { addon =>
|
AddonGenerator.successAddon("mini-addon").use { addon =>
|
||||||
for {
|
for {
|
||||||
archive <- IO(AddonArchive(addon.url, "test-addon", "0.1.0"))
|
archive <- IO(AddonArchive(addon.url, "", ""))
|
||||||
path <- archive.extractTo[IO](UrlReader.defaultReader[IO], dir)
|
path <- archive.extractTo[IO](UrlReader.defaultReader[IO], dir)
|
||||||
|
|
||||||
read <- AddonArchive.read[IO](addon.url, UrlReader.defaultReader[IO], path.some)
|
read <- AddonArchive.read[IO](addon.url, UrlReader.defaultReader[IO], path.some)
|
||||||
|
@ -142,7 +142,7 @@ class AddonExecutorTest extends CatsEffectSuite with Fixtures with TestLoggingCo
|
|||||||
AddonExecutionResult.executionResultMonoid
|
AddonExecutionResult.executionResultMonoid
|
||||||
.combine(
|
.combine(
|
||||||
AddonExecutionResult.empty,
|
AddonExecutionResult.empty,
|
||||||
AddonExecutionResult(Nil, pure = true)
|
AddonExecutionResult(Nil, true)
|
||||||
)
|
)
|
||||||
.pure
|
.pure
|
||||||
)
|
)
|
||||||
|
@ -27,9 +27,9 @@ object AddonGenerator {
|
|||||||
): Resource[IO, AddonArchive] =
|
): Resource[IO, AddonArchive] =
|
||||||
output match {
|
output match {
|
||||||
case None =>
|
case None =>
|
||||||
generate(name, version, collectOutput = false)("exit 0")
|
generate(name, version, false)("exit 0")
|
||||||
case Some(out) =>
|
case Some(out) =>
|
||||||
generate(name, version, collectOutput = true)(
|
generate(name, version, true)(
|
||||||
s"""
|
s"""
|
||||||
|cat <<-EOF
|
|cat <<-EOF
|
||||||
|${out.asJson.noSpaces}
|
|${out.asJson.noSpaces}
|
||||||
@ -77,9 +77,8 @@ object AddonGenerator {
|
|||||||
meta = AddonMeta.Meta(name, version, None),
|
meta = AddonMeta.Meta(name, version, None),
|
||||||
triggers = Set(AddonTriggerType.ExistingItem: AddonTriggerType).some,
|
triggers = Set(AddonTriggerType.ExistingItem: AddonTriggerType).some,
|
||||||
args = None,
|
args = None,
|
||||||
runner = AddonMeta
|
runner =
|
||||||
.Runner(None, None, AddonMeta.TrivialRunner(enable = true, "addon.sh").some)
|
AddonMeta.Runner(None, None, AddonMeta.TrivialRunner(true, "addon.sh").some).some,
|
||||||
.some,
|
|
||||||
options =
|
options =
|
||||||
AddonMeta.Options(networking = !collectOutput, collectOutput = collectOutput).some
|
AddonMeta.Options(networking = !collectOutput, collectOutput = collectOutput).some
|
||||||
)
|
)
|
||||||
|
@ -35,13 +35,4 @@ class AddonMetaTest extends CatsEffectSuite with TestLoggingConfig with Fixtures
|
|||||||
_ = assertEquals(meta, dummyAddonMeta)
|
_ = assertEquals(meta, dummyAddonMeta)
|
||||||
} yield ()
|
} yield ()
|
||||||
}
|
}
|
||||||
|
|
||||||
test("parse yaml with defaults") {
|
|
||||||
val yamlStr = """meta:
|
|
||||||
| name: "test"
|
|
||||||
| version: "0.1.0"
|
|
||||||
|""".stripMargin
|
|
||||||
val meta = AddonMeta.fromYamlString(yamlStr).fold(throw _, identity)
|
|
||||||
assert(meta.parseResult)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -31,9 +31,6 @@ trait Fixtures extends TestLoggingConfig { self: CatsEffectSuite =>
|
|||||||
val miniAddonUrl =
|
val miniAddonUrl =
|
||||||
LenientUri.fromJava(getClass.getResource("/minimal-addon.zip"))
|
LenientUri.fromJava(getClass.getResource("/minimal-addon.zip"))
|
||||||
|
|
||||||
val singleFileAddonUrl =
|
|
||||||
LenientUri.fromJava(getClass.getResource("/docspell-addon-single-file.zip"))
|
|
||||||
|
|
||||||
val dummyAddonMeta =
|
val dummyAddonMeta =
|
||||||
AddonMeta(
|
AddonMeta(
|
||||||
meta =
|
meta =
|
||||||
@ -43,13 +40,13 @@ trait Fixtures extends TestLoggingConfig { self: CatsEffectSuite =>
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
runner = Runner(
|
runner = Runner(
|
||||||
nix = NixRunner(enable = true).some,
|
nix = NixRunner(true).some,
|
||||||
docker = DockerRunner(
|
docker = DockerRunner(
|
||||||
enable = true,
|
enable = true,
|
||||||
image = None,
|
image = None,
|
||||||
build = "Dockerfile".some
|
build = "Dockerfile".some
|
||||||
).some,
|
).some,
|
||||||
trivial = TrivialRunner(enable = true, "src/addon.sh").some
|
trivial = TrivialRunner(true, "src/addon.sh").some
|
||||||
).some,
|
).some,
|
||||||
options = Options(networking = true, collectOutput = true).some
|
options = Options(networking = true, collectOutput = true).some
|
||||||
)
|
)
|
||||||
@ -58,7 +55,7 @@ trait Fixtures extends TestLoggingConfig { self: CatsEffectSuite =>
|
|||||||
Path(s"/tmp/target/test-temp")
|
Path(s"/tmp/target/test-temp")
|
||||||
|
|
||||||
val tempDir =
|
val tempDir =
|
||||||
ResourceFunFixture[Path](
|
ResourceFixture[Path](
|
||||||
Resource.eval(Files[IO].createDirectories(baseTempDir)) *>
|
Resource.eval(Files[IO].createDirectories(baseTempDir)) *>
|
||||||
Files[IO]
|
Files[IO]
|
||||||
.tempDirectory(baseTempDir.some, "run-", PosixPermissions.fromOctal("777"))
|
.tempDirectory(baseTempDir.some, "run-", PosixPermissions.fromOctal("777"))
|
||||||
@ -68,7 +65,7 @@ trait Fixtures extends TestLoggingConfig { self: CatsEffectSuite =>
|
|||||||
runner: RunnerType,
|
runner: RunnerType,
|
||||||
runners: RunnerType*
|
runners: RunnerType*
|
||||||
): AddonExecutorConfig = {
|
): AddonExecutorConfig = {
|
||||||
val nspawn = NSpawn(enabled = false, "sudo", "systemd-nspawn", Duration.millis(100))
|
val nspawn = NSpawn(false, "sudo", "systemd-nspawn", Duration.millis(100))
|
||||||
AddonExecutorConfig(
|
AddonExecutorConfig(
|
||||||
runner = runner :: runners.toList,
|
runner = runner :: runners.toList,
|
||||||
runTimeout = Duration.minutes(2),
|
runTimeout = Duration.minutes(2),
|
||||||
|
@ -125,7 +125,6 @@ object DateFind {
|
|||||||
case Language.Dutch => dmy.or(ymd).or(mdy)
|
case Language.Dutch => dmy.or(ymd).or(mdy)
|
||||||
case Language.Latvian => dmy.or(lavLong).or(ymd)
|
case Language.Latvian => dmy.or(lavLong).or(ymd)
|
||||||
case Language.Japanese => ymd
|
case Language.Japanese => ymd
|
||||||
case Language.JpnVert => ymd
|
|
||||||
case Language.Hebrew => dmy
|
case Language.Hebrew => dmy
|
||||||
case Language.Lithuanian => ymd
|
case Language.Lithuanian => ymd
|
||||||
case Language.Polish => dmy
|
case Language.Polish => dmy
|
||||||
|
@ -54,8 +54,6 @@ object MonthName {
|
|||||||
latvian
|
latvian
|
||||||
case Language.Japanese =>
|
case Language.Japanese =>
|
||||||
japanese
|
japanese
|
||||||
case Language.JpnVert =>
|
|
||||||
japanese
|
|
||||||
case Language.Hebrew =>
|
case Language.Hebrew =>
|
||||||
hebrew
|
hebrew
|
||||||
case Language.Lithuanian =>
|
case Language.Lithuanian =>
|
||||||
|
@ -22,7 +22,7 @@ import munit._
|
|||||||
|
|
||||||
class StanfordNerAnnotatorSuite extends FunSuite with TestLoggingConfig {
|
class StanfordNerAnnotatorSuite extends FunSuite with TestLoggingConfig {
|
||||||
lazy val germanClassifier =
|
lazy val germanClassifier =
|
||||||
new StanfordCoreNLP(Properties.nerGerman(None, highRecall = false))
|
new StanfordCoreNLP(Properties.nerGerman(None, false))
|
||||||
lazy val englishClassifier =
|
lazy val englishClassifier =
|
||||||
new StanfordCoreNLP(Properties.nerEnglish(None))
|
new StanfordCoreNLP(Properties.nerEnglish(None))
|
||||||
|
|
||||||
|
@ -90,6 +90,6 @@ object Config {
|
|||||||
}
|
}
|
||||||
object Addons {
|
object Addons {
|
||||||
val disabled: Addons =
|
val disabled: Addons =
|
||||||
Addons(enabled = false, allowImpure = false, UrlMatcher.False, UrlMatcher.True)
|
Addons(false, false, UrlMatcher.False, UrlMatcher.True)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,7 +127,7 @@ object Login {
|
|||||||
_ <- logF.trace(s"Account lookup: $data")
|
_ <- logF.trace(s"Account lookup: $data")
|
||||||
res <- data match {
|
res <- data match {
|
||||||
case Some(d) if checkNoPassword(d, Set(AccountSource.OpenId)) =>
|
case Some(d) if checkNoPassword(d, Set(AccountSource.OpenId)) =>
|
||||||
doLogin(config, d.account, rememberMe = false)
|
doLogin(config, d.account, false)
|
||||||
case Some(d) if checkNoPassword(d, Set(AccountSource.Local)) =>
|
case Some(d) if checkNoPassword(d, Set(AccountSource.Local)) =>
|
||||||
config.onAccountSourceConflict match {
|
config.onAccountSourceConflict match {
|
||||||
case OnAccountSourceConflict.Fail =>
|
case OnAccountSourceConflict.Fail =>
|
||||||
@ -145,7 +145,7 @@ object Login {
|
|||||||
AccountSource.OpenId
|
AccountSource.OpenId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
res <- doLogin(config, d.account, rememberMe = false)
|
res <- doLogin(config, d.account, false)
|
||||||
} yield res
|
} yield res
|
||||||
}
|
}
|
||||||
case _ =>
|
case _ =>
|
||||||
@ -212,12 +212,7 @@ object Login {
|
|||||||
val okResult: F[Result] =
|
val okResult: F[Result] =
|
||||||
for {
|
for {
|
||||||
_ <- store.transact(RUser.updateLogin(sf.token.account))
|
_ <- store.transact(RUser.updateLogin(sf.token.account))
|
||||||
newToken <- AuthToken.user(
|
newToken <- AuthToken.user(sf.token.account, false, config.serverSecret, None)
|
||||||
sf.token.account,
|
|
||||||
requireSecondFactor = false,
|
|
||||||
config.serverSecret,
|
|
||||||
None
|
|
||||||
)
|
|
||||||
rem <- OptionT
|
rem <- OptionT
|
||||||
.whenF(sf.rememberMe && config.rememberMe.enabled)(
|
.whenF(sf.rememberMe && config.rememberMe.enabled)(
|
||||||
insertRememberToken(store, sf.token.account, config)
|
insertRememberToken(store, sf.token.account, config)
|
||||||
@ -244,9 +239,7 @@ object Login {
|
|||||||
(for {
|
(for {
|
||||||
_ <- validateToken
|
_ <- validateToken
|
||||||
key <- EitherT.fromOptionF(
|
key <- EitherT.fromOptionF(
|
||||||
store.transact(
|
store.transact(RTotp.findEnabledByUserId(sf.token.account.userId, true)),
|
||||||
RTotp.findEnabledByUserId(sf.token.account.userId, enabled = true)
|
|
||||||
),
|
|
||||||
Result.invalidAuth
|
Result.invalidAuth
|
||||||
)
|
)
|
||||||
now <- EitherT.right[Result](Timestamp.current[F])
|
now <- EitherT.right[Result](Timestamp.current[F])
|
||||||
@ -262,12 +255,7 @@ object Login {
|
|||||||
def okResult(acc: AccountInfo) =
|
def okResult(acc: AccountInfo) =
|
||||||
for {
|
for {
|
||||||
_ <- store.transact(RUser.updateLogin(acc))
|
_ <- store.transact(RUser.updateLogin(acc))
|
||||||
token <- AuthToken.user(
|
token <- AuthToken.user(acc, false, config.serverSecret, None)
|
||||||
acc,
|
|
||||||
requireSecondFactor = false,
|
|
||||||
config.serverSecret,
|
|
||||||
None
|
|
||||||
)
|
|
||||||
} yield Result.ok(token, None)
|
} yield Result.ok(token, None)
|
||||||
|
|
||||||
def rememberedLogin(rid: Ident) =
|
def rememberedLogin(rid: Ident) =
|
||||||
|
@ -93,7 +93,7 @@ object AddonOps {
|
|||||||
AddonResult.executionFailed(
|
AddonResult.executionFailed(
|
||||||
new Exception(s"Addon run config ${id.id} not found.")
|
new Exception(s"Addon run config ${id.id} not found.")
|
||||||
) :: Nil,
|
) :: Nil,
|
||||||
pure = false
|
false
|
||||||
) :: Nil,
|
) :: Nil,
|
||||||
Nil
|
Nil
|
||||||
)
|
)
|
||||||
|
@ -72,7 +72,7 @@ private[joex] class AddonPrepare[F[_]: Sync](store: Store[F]) extends LoggerExte
|
|||||||
|
|
||||||
token <- AuthToken.user(
|
token <- AuthToken.user(
|
||||||
account,
|
account,
|
||||||
requireSecondFactor = false,
|
false,
|
||||||
secret.getOrElse(ByteVector.empty),
|
secret.getOrElse(ByteVector.empty),
|
||||||
tokenValidity.some
|
tokenValidity.some
|
||||||
)
|
)
|
||||||
|
@ -194,14 +194,7 @@ object OCollective {
|
|||||||
id <- Ident.randomId[F]
|
id <- Ident.randomId[F]
|
||||||
settings = sett.emptyTrash.getOrElse(EmptyTrash.default)
|
settings = sett.emptyTrash.getOrElse(EmptyTrash.default)
|
||||||
args = EmptyTrashArgs(cid, settings.minAge)
|
args = EmptyTrashArgs(cid, settings.minAge)
|
||||||
ut = UserTask(
|
ut = UserTask(id, EmptyTrashArgs.taskName, true, settings.schedule, None, args)
|
||||||
id,
|
|
||||||
EmptyTrashArgs.taskName,
|
|
||||||
enabled = true,
|
|
||||||
settings.schedule,
|
|
||||||
None,
|
|
||||||
args
|
|
||||||
)
|
|
||||||
_ <- uts.updateOneTask(UserTaskScope.collective(cid), args.makeSubject.some, ut)
|
_ <- uts.updateOneTask(UserTaskScope.collective(cid), args.makeSubject.some, ut)
|
||||||
_ <- joex.notifyAllNodes
|
_ <- joex.notifyAllNodes
|
||||||
} yield ()
|
} yield ()
|
||||||
@ -227,7 +220,7 @@ object OCollective {
|
|||||||
ut = UserTask(
|
ut = UserTask(
|
||||||
id,
|
id,
|
||||||
LearnClassifierArgs.taskName,
|
LearnClassifierArgs.taskName,
|
||||||
enabled = true,
|
true,
|
||||||
CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All),
|
CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All),
|
||||||
None,
|
None,
|
||||||
args
|
args
|
||||||
@ -246,7 +239,7 @@ object OCollective {
|
|||||||
ut = UserTask(
|
ut = UserTask(
|
||||||
id,
|
id,
|
||||||
EmptyTrashArgs.taskName,
|
EmptyTrashArgs.taskName,
|
||||||
enabled = true,
|
true,
|
||||||
CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All),
|
CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All),
|
||||||
None,
|
None,
|
||||||
args
|
args
|
||||||
|
@ -114,14 +114,14 @@ object ONotification {
|
|||||||
)
|
)
|
||||||
_ <- notMod.send(logbuf._2.andThen(log), ev, ch)
|
_ <- notMod.send(logbuf._2.andThen(log), ev, ch)
|
||||||
logs <- logbuf._1.get
|
logs <- logbuf._1.get
|
||||||
res = SendTestResult(success = true, logs)
|
res = SendTestResult(true, logs)
|
||||||
} yield res).attempt
|
} yield res).attempt
|
||||||
.map {
|
.map {
|
||||||
case Right(res) => res
|
case Right(res) => res
|
||||||
case Left(ex) =>
|
case Left(ex) =>
|
||||||
val ev =
|
val ev =
|
||||||
LogEvent.of(Level.Error, "Failed sending sample event").addError(ex)
|
LogEvent.of(Level.Error, "Failed sending sample event").addError(ex)
|
||||||
SendTestResult(success = false, Vector(ev))
|
SendTestResult(false, Vector(ev))
|
||||||
}
|
}
|
||||||
|
|
||||||
def listChannels(userId: Ident): F[Vector[Channel]] =
|
def listChannels(userId: Ident): F[Vector[Channel]] =
|
||||||
|
@ -120,9 +120,7 @@ object OTotp {
|
|||||||
def confirmInit(accountId: AccountInfo, otp: OnetimePassword): F[ConfirmResult] =
|
def confirmInit(accountId: AccountInfo, otp: OnetimePassword): F[ConfirmResult] =
|
||||||
for {
|
for {
|
||||||
_ <- log.info(s"Confirm TOTP setup for account ${accountId.asString}")
|
_ <- log.info(s"Confirm TOTP setup for account ${accountId.asString}")
|
||||||
key <- store.transact(
|
key <- store.transact(RTotp.findEnabledByUserId(accountId.userId, false))
|
||||||
RTotp.findEnabledByUserId(accountId.userId, enabled = false)
|
|
||||||
)
|
|
||||||
now <- Timestamp.current[F]
|
now <- Timestamp.current[F]
|
||||||
res <- key match {
|
res <- key match {
|
||||||
case None =>
|
case None =>
|
||||||
@ -131,7 +129,7 @@ object OTotp {
|
|||||||
val check = totp.checkPassword(r.secret, otp, now.value)
|
val check = totp.checkPassword(r.secret, otp, now.value)
|
||||||
if (check)
|
if (check)
|
||||||
store
|
store
|
||||||
.transact(RTotp.setEnabled(accountId.userId, enabled = true))
|
.transact(RTotp.setEnabled(accountId.userId, true))
|
||||||
.map(_ => ConfirmResult.Success)
|
.map(_ => ConfirmResult.Success)
|
||||||
else ConfirmResult.Failed.pure[F]
|
else ConfirmResult.Failed.pure[F]
|
||||||
}
|
}
|
||||||
@ -142,7 +140,7 @@ object OTotp {
|
|||||||
case Some(pw) =>
|
case Some(pw) =>
|
||||||
for {
|
for {
|
||||||
_ <- log.info(s"Validating TOTP, because it is requested to disable it.")
|
_ <- log.info(s"Validating TOTP, because it is requested to disable it.")
|
||||||
key <- store.transact(RTotp.findEnabledByLogin(accountId, enabled = true))
|
key <- store.transact(RTotp.findEnabledByLogin(accountId, true))
|
||||||
now <- Timestamp.current[F]
|
now <- Timestamp.current[F]
|
||||||
res <- key match {
|
res <- key match {
|
||||||
case None =>
|
case None =>
|
||||||
@ -151,7 +149,7 @@ object OTotp {
|
|||||||
val check = totp.checkPassword(r.secret, pw, now.value)
|
val check = totp.checkPassword(r.secret, pw, now.value)
|
||||||
if (check)
|
if (check)
|
||||||
UpdateResult.fromUpdate(
|
UpdateResult.fromUpdate(
|
||||||
store.transact(RTotp.setEnabled(r.userId, enabled = false))
|
store.transact(RTotp.setEnabled(r.userId, false))
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
log.info(s"TOTP code was invalid. Not disabling it.") *> UpdateResult
|
log.info(s"TOTP code was invalid. Not disabling it.") *> UpdateResult
|
||||||
@ -162,15 +160,15 @@ object OTotp {
|
|||||||
case None =>
|
case None =>
|
||||||
UpdateResult.fromUpdate {
|
UpdateResult.fromUpdate {
|
||||||
(for {
|
(for {
|
||||||
key <- OptionT(RTotp.findEnabledByLogin(accountId, enabled = true))
|
key <- OptionT(RTotp.findEnabledByLogin(accountId, true))
|
||||||
n <- OptionT.liftF(RTotp.setEnabled(key.userId, enabled = false))
|
n <- OptionT.liftF(RTotp.setEnabled(key.userId, false))
|
||||||
} yield n).mapK(store.transform).getOrElse(0)
|
} yield n).mapK(store.transform).getOrElse(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def state(acc: AccountInfo): F[OtpState] =
|
def state(acc: AccountInfo): F[OtpState] =
|
||||||
for {
|
for {
|
||||||
record <- store.transact(RTotp.findEnabledByUserId(acc.userId, enabled = true))
|
record <- store.transact(RTotp.findEnabledByUserId(acc.userId, true))
|
||||||
result = record match {
|
result = record match {
|
||||||
case Some(r) =>
|
case Some(r) =>
|
||||||
OtpState.Enabled(r.created)
|
OtpState.Enabled(r.created)
|
||||||
|
@ -159,7 +159,7 @@ object OUpload {
|
|||||||
data.meta.skipDuplicates,
|
data.meta.skipDuplicates,
|
||||||
data.meta.fileFilter.some,
|
data.meta.fileFilter.some,
|
||||||
data.meta.tags.some,
|
data.meta.tags.some,
|
||||||
reprocess = false,
|
false,
|
||||||
data.meta.attachmentsOnly,
|
data.meta.attachmentsOnly,
|
||||||
data.meta.customData
|
data.meta.customData
|
||||||
)
|
)
|
||||||
|
@ -32,12 +32,9 @@ class AuthTokenTest extends CatsEffectSuite {
|
|||||||
val otherSecret = ByteVector.fromValidHex("16bad")
|
val otherSecret = ByteVector.fromValidHex("16bad")
|
||||||
|
|
||||||
test("validate") {
|
test("validate") {
|
||||||
val token1 =
|
val token1 = AuthToken.user[IO](user, false, secret, None).unsafeRunSync()
|
||||||
AuthToken.user[IO](user, requireSecondFactor = false, secret, None).unsafeRunSync()
|
|
||||||
val token2 =
|
val token2 =
|
||||||
AuthToken
|
AuthToken.user[IO](user, false, secret, Duration.seconds(10).some).unsafeRunSync()
|
||||||
.user[IO](user, requireSecondFactor = false, secret, Duration.seconds(10).some)
|
|
||||||
.unsafeRunSync()
|
|
||||||
assert(token1.validate(secret, Duration.seconds(5)))
|
assert(token1.validate(secret, Duration.seconds(5)))
|
||||||
assert(!token1.validate(otherSecret, Duration.seconds(5)))
|
assert(!token1.validate(otherSecret, Duration.seconds(5)))
|
||||||
assert(!token1.copy(account = john).validate(secret, Duration.seconds(5)))
|
assert(!token1.copy(account = john).validate(secret, Duration.seconds(5)))
|
||||||
@ -49,12 +46,9 @@ class AuthTokenTest extends CatsEffectSuite {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test("signature") {
|
test("signature") {
|
||||||
val token1 =
|
val token1 = AuthToken.user[IO](user, false, secret, None).unsafeRunSync()
|
||||||
AuthToken.user[IO](user, requireSecondFactor = false, secret, None).unsafeRunSync()
|
|
||||||
val token2 =
|
val token2 =
|
||||||
AuthToken
|
AuthToken.user[IO](user, false, secret, Duration.seconds(10).some).unsafeRunSync()
|
||||||
.user[IO](user, requireSecondFactor = false, secret, Duration.seconds(10).some)
|
|
||||||
.unsafeRunSync()
|
|
||||||
|
|
||||||
assert(token1.sigValid(secret))
|
assert(token1.sigValid(secret))
|
||||||
assert(token1.sigInvalid(otherSecret))
|
assert(token1.sigInvalid(otherSecret))
|
||||||
|
@ -123,11 +123,6 @@ object Language {
|
|||||||
val iso3 = "jpn"
|
val iso3 = "jpn"
|
||||||
}
|
}
|
||||||
|
|
||||||
/*It's not an ISO value, but this needs to be unique and tesseract will need jpn_vert for it's scan from the config of /etc/docspell-joex/docspell-joex.conf.*/
|
|
||||||
case object JpnVert extends Language {
|
|
||||||
val iso2 = "ja_vert"
|
|
||||||
val iso3 = "jpn_vert"
|
|
||||||
}
|
|
||||||
case object Hebrew extends Language {
|
case object Hebrew extends Language {
|
||||||
val iso2 = "he"
|
val iso2 = "he"
|
||||||
val iso3 = "heb"
|
val iso3 = "heb"
|
||||||
@ -177,7 +172,6 @@ object Language {
|
|||||||
Romanian,
|
Romanian,
|
||||||
Latvian,
|
Latvian,
|
||||||
Japanese,
|
Japanese,
|
||||||
JpnVert,
|
|
||||||
Hebrew,
|
Hebrew,
|
||||||
Lithuanian,
|
Lithuanian,
|
||||||
Polish,
|
Polish,
|
||||||
|
@ -78,11 +78,7 @@ case class LenientUri(
|
|||||||
.covary[F]
|
.covary[F]
|
||||||
.rethrow
|
.rethrow
|
||||||
.flatMap(url =>
|
.flatMap(url =>
|
||||||
fs2.io.readInputStream(
|
fs2.io.readInputStream(Sync[F].delay(url.openStream()), chunkSize, true)
|
||||||
Sync[F].delay(url.openStream()),
|
|
||||||
chunkSize,
|
|
||||||
closeAfterUse = true
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def readText[F[_]: Sync](chunkSize: Int): F[String] =
|
def readText[F[_]: Sync](chunkSize: Int): F[String] =
|
||||||
@ -125,7 +121,7 @@ object LenientUri {
|
|||||||
val isRoot = true
|
val isRoot = true
|
||||||
val isEmpty = false
|
val isEmpty = false
|
||||||
def /(seg: String): Path =
|
def /(seg: String): Path =
|
||||||
NonEmptyPath(NonEmptyList.of(seg), trailingSlash = false)
|
NonEmptyPath(NonEmptyList.of(seg), false)
|
||||||
def asString = "/"
|
def asString = "/"
|
||||||
}
|
}
|
||||||
case object EmptyPath extends Path {
|
case object EmptyPath extends Path {
|
||||||
@ -133,7 +129,7 @@ object LenientUri {
|
|||||||
val isRoot = false
|
val isRoot = false
|
||||||
val isEmpty = true
|
val isEmpty = true
|
||||||
def /(seg: String): Path =
|
def /(seg: String): Path =
|
||||||
NonEmptyPath(NonEmptyList.of(seg), trailingSlash = false)
|
NonEmptyPath(NonEmptyList.of(seg), false)
|
||||||
def asString = ""
|
def asString = ""
|
||||||
}
|
}
|
||||||
case class NonEmptyPath(segs: NonEmptyList[String], trailingSlash: Boolean)
|
case class NonEmptyPath(segs: NonEmptyList[String], trailingSlash: Boolean)
|
||||||
|
@ -194,7 +194,7 @@ object MimeType {
|
|||||||
val csValueStart = in.substring(n + "charset=".length).trim
|
val csValueStart = in.substring(n + "charset=".length).trim
|
||||||
val csName = csValueStart.indexOf(';') match {
|
val csName = csValueStart.indexOf(';') match {
|
||||||
case -1 => unquote(csValueStart).trim
|
case -1 => unquote(csValueStart).trim
|
||||||
case n2 => unquote(csValueStart.substring(0, n2)).trim
|
case n => unquote(csValueStart.substring(0, n)).trim
|
||||||
}
|
}
|
||||||
if (Charset.isSupported(csName)) Right((Some(Charset.forName(csName)), ""))
|
if (Charset.isSupported(csName)) Right((Some(Charset.forName(csName)), ""))
|
||||||
else Right((None, ""))
|
else Right((None, ""))
|
||||||
|
@ -0,0 +1,212 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 Eike K. & Contributors
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package docspell.common
|
||||||
|
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.lang.ProcessBuilder.Redirect
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
import scala.jdk.CollectionConverters._
|
||||||
|
|
||||||
|
import cats.effect._
|
||||||
|
import cats.implicits._
|
||||||
|
import fs2.io.file.Path
|
||||||
|
import fs2.{Stream, io, text}
|
||||||
|
|
||||||
|
import docspell.common.{exec => newExec}
|
||||||
|
import docspell.logging.Logger
|
||||||
|
|
||||||
|
// better use `SysCmd` and `SysExec`
|
||||||
|
object SystemCommand {
|
||||||
|
|
||||||
|
final case class Config(
|
||||||
|
program: String,
|
||||||
|
args: Seq[String],
|
||||||
|
timeout: Duration,
|
||||||
|
env: Map[String, String] = Map.empty
|
||||||
|
) {
|
||||||
|
|
||||||
|
def toSysCmd = newExec
|
||||||
|
.SysCmd(program, newExec.Args(args))
|
||||||
|
.withTimeout(timeout)
|
||||||
|
.addEnv(newExec.Env(env))
|
||||||
|
|
||||||
|
def mapArgs(f: String => String): Config =
|
||||||
|
Config(program, args.map(f), timeout)
|
||||||
|
|
||||||
|
def replace(repl: Map[String, String]): Config =
|
||||||
|
mapArgs(s =>
|
||||||
|
repl.foldLeft(s) { case (res, (k, v)) =>
|
||||||
|
res.replace(k, v)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def withEnv(key: String, value: String): Config =
|
||||||
|
copy(env = env.updated(key, value))
|
||||||
|
|
||||||
|
def addEnv(moreEnv: Map[String, String]): Config =
|
||||||
|
copy(env = env ++ moreEnv)
|
||||||
|
|
||||||
|
def appendArgs(extraArgs: Args): Config =
|
||||||
|
copy(args = args ++ extraArgs.args)
|
||||||
|
|
||||||
|
def appendArgs(extraArgs: Seq[String]): Config =
|
||||||
|
copy(args = args ++ extraArgs)
|
||||||
|
|
||||||
|
def toCmd: List[String] =
|
||||||
|
program :: args.toList
|
||||||
|
|
||||||
|
lazy val cmdString: String =
|
||||||
|
toCmd.mkString(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class Args(args: Vector[String]) extends Iterable[String] {
|
||||||
|
override def iterator = args.iterator
|
||||||
|
|
||||||
|
def prepend(a: String): Args = Args(a +: args)
|
||||||
|
|
||||||
|
def prependWhen(flag: Boolean)(a: String): Args =
|
||||||
|
prependOption(Option.when(flag)(a))
|
||||||
|
|
||||||
|
def prependOption(value: Option[String]): Args =
|
||||||
|
value.map(prepend).getOrElse(this)
|
||||||
|
|
||||||
|
def append(a: String, as: String*): Args =
|
||||||
|
Args(args ++ (a +: as.toVector))
|
||||||
|
|
||||||
|
def appendOption(value: Option[String]): Args =
|
||||||
|
value.map(append(_)).getOrElse(this)
|
||||||
|
|
||||||
|
def appendOptionVal(first: String, second: Option[String]): Args =
|
||||||
|
second.map(b => append(first, b)).getOrElse(this)
|
||||||
|
|
||||||
|
def appendWhen(flag: Boolean)(a: String, as: String*): Args =
|
||||||
|
if (flag) append(a, as: _*) else this
|
||||||
|
|
||||||
|
def appendWhenNot(flag: Boolean)(a: String, as: String*): Args =
|
||||||
|
if (!flag) append(a, as: _*) else this
|
||||||
|
|
||||||
|
def append(p: Path): Args =
|
||||||
|
append(p.toString)
|
||||||
|
|
||||||
|
def append(as: Iterable[String]): Args =
|
||||||
|
Args(args ++ as.toVector)
|
||||||
|
}
|
||||||
|
object Args {
|
||||||
|
val empty: Args = Args()
|
||||||
|
|
||||||
|
def apply(as: String*): Args =
|
||||||
|
Args(as.toVector)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class Result(rc: Int, stdout: String, stderr: String)
|
||||||
|
|
||||||
|
def exec[F[_]: Sync](
|
||||||
|
cmd: Config,
|
||||||
|
logger: Logger[F],
|
||||||
|
wd: Option[Path] = None,
|
||||||
|
stdin: Stream[F, Byte] = Stream.empty
|
||||||
|
): Stream[F, Result] =
|
||||||
|
startProcess(cmd, wd, logger, stdin) { proc =>
|
||||||
|
Stream.eval {
|
||||||
|
for {
|
||||||
|
_ <- writeToProcess(stdin, proc)
|
||||||
|
term <- Sync[F].blocking(proc.waitFor(cmd.timeout.seconds, TimeUnit.SECONDS))
|
||||||
|
_ <-
|
||||||
|
if (term)
|
||||||
|
logger.debug(s"Command `${cmd.cmdString}` finished: ${proc.exitValue}")
|
||||||
|
else
|
||||||
|
logger.warn(
|
||||||
|
s"Command `${cmd.cmdString}` did not finish in ${cmd.timeout.formatExact}!"
|
||||||
|
)
|
||||||
|
_ <- if (!term) timeoutError(proc, cmd) else Sync[F].pure(())
|
||||||
|
out <-
|
||||||
|
if (term) inputStreamToString(proc.getInputStream)
|
||||||
|
else Sync[F].pure("")
|
||||||
|
err <-
|
||||||
|
if (term) inputStreamToString(proc.getErrorStream)
|
||||||
|
else Sync[F].pure("")
|
||||||
|
} yield Result(proc.exitValue, out, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def execSuccess[F[_]: Sync](
|
||||||
|
cmd: Config,
|
||||||
|
logger: Logger[F],
|
||||||
|
wd: Option[Path] = None,
|
||||||
|
stdin: Stream[F, Byte] = Stream.empty
|
||||||
|
): Stream[F, Result] =
|
||||||
|
exec(cmd, logger, wd, stdin).flatMap { r =>
|
||||||
|
if (r.rc != 0)
|
||||||
|
Stream.raiseError[F](
|
||||||
|
new Exception(
|
||||||
|
s"Command `${cmd.cmdString}` returned non-zero exit code ${r.rc}. Stderr: ${r.stderr}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else Stream.emit(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def startProcess[F[_]: Sync, A](
|
||||||
|
cmd: Config,
|
||||||
|
wd: Option[Path],
|
||||||
|
logger: Logger[F],
|
||||||
|
stdin: Stream[F, Byte]
|
||||||
|
)(
|
||||||
|
f: Process => Stream[F, A]
|
||||||
|
): Stream[F, A] = {
|
||||||
|
val log = logger.debug(s"Running external command: ${cmd.cmdString}")
|
||||||
|
val hasStdin = stdin.take(1).compile.last.map(_.isDefined)
|
||||||
|
val proc = log *> hasStdin.flatMap(flag =>
|
||||||
|
Sync[F].blocking {
|
||||||
|
val pb = new ProcessBuilder(cmd.toCmd.asJava)
|
||||||
|
.redirectInput(if (flag) Redirect.PIPE else Redirect.INHERIT)
|
||||||
|
.redirectError(Redirect.PIPE)
|
||||||
|
.redirectOutput(Redirect.PIPE)
|
||||||
|
|
||||||
|
val pbEnv = pb.environment()
|
||||||
|
cmd.env.foreach { case (key, value) =>
|
||||||
|
pbEnv.put(key, value)
|
||||||
|
}
|
||||||
|
wd.map(_.toNioPath.toFile).foreach(pb.directory)
|
||||||
|
pb.start()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Stream
|
||||||
|
.bracket(proc)(p =>
|
||||||
|
logger.debug(s"Closing process: `${cmd.cmdString}`").map(_ => p.destroy())
|
||||||
|
)
|
||||||
|
.flatMap(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def inputStreamToString[F[_]: Sync](in: InputStream): F[String] =
|
||||||
|
io.readInputStream(Sync[F].pure(in), 16 * 1024, closeAfterUse = false)
|
||||||
|
.through(text.utf8.decode)
|
||||||
|
.chunks
|
||||||
|
.map(_.toVector.mkString)
|
||||||
|
.fold1(_ + _)
|
||||||
|
.compile
|
||||||
|
.last
|
||||||
|
.map(_.getOrElse(""))
|
||||||
|
|
||||||
|
private def writeToProcess[F[_]: Sync](
|
||||||
|
data: Stream[F, Byte],
|
||||||
|
proc: Process
|
||||||
|
): F[Unit] =
|
||||||
|
data
|
||||||
|
.through(io.writeOutputStream(Sync[F].blocking(proc.getOutputStream)))
|
||||||
|
.compile
|
||||||
|
.drain
|
||||||
|
|
||||||
|
private def timeoutError[F[_]: Sync](proc: Process, cmd: Config): F[Unit] =
|
||||||
|
Sync[F].blocking(proc.destroyForcibly()).attempt *> {
|
||||||
|
Sync[F].raiseError(
|
||||||
|
new Exception(
|
||||||
|
s"Command `${cmd.cmdString}` timed out (${cmd.timeout.formatExact})"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -62,7 +62,7 @@ object UrlMatcher {
|
|||||||
// strip path to only match prefixes
|
// strip path to only match prefixes
|
||||||
val mPath: LenientUri.Path =
|
val mPath: LenientUri.Path =
|
||||||
NonEmptyList.fromList(url.path.segments.take(pathSegmentCount)) match {
|
NonEmptyList.fromList(url.path.segments.take(pathSegmentCount)) match {
|
||||||
case Some(nel) => LenientUri.NonEmptyPath(nel, trailingSlash = false)
|
case Some(nel) => LenientUri.NonEmptyPath(nel, false)
|
||||||
case None => LenientUri.RootPath
|
case None => LenientUri.RootPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,9 +17,6 @@ case class Env(values: Map[String, String]) {
|
|||||||
def addAll(e: Env): Env =
|
def addAll(e: Env): Env =
|
||||||
Env(values ++ e.values)
|
Env(values ++ e.values)
|
||||||
|
|
||||||
def modifyValue(f: String => String): Env =
|
|
||||||
Env(values.view.mapValues(f).toMap)
|
|
||||||
|
|
||||||
def ++(e: Env) = addAll(e)
|
def ++(e: Env) = addAll(e)
|
||||||
|
|
||||||
def foreach(f: (String, String) => Unit): Unit =
|
def foreach(f: (String, String) => Unit): Unit =
|
||||||
|
@ -1,89 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 Eike K. & Contributors
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
package docspell.common.exec
|
|
||||||
|
|
||||||
import docspell.common.Duration
|
|
||||||
import docspell.common.Ident
|
|
||||||
import docspell.common.exec.Env
|
|
||||||
import docspell.common.exec.ExternalCommand.ArgMapping
|
|
||||||
import docspell.common.exec.SysCmd
|
|
||||||
|
|
||||||
final case class ExternalCommand(
|
|
||||||
program: String,
|
|
||||||
args: Seq[String],
|
|
||||||
timeout: Duration,
|
|
||||||
env: Map[String, String] = Map.empty,
|
|
||||||
argMappings: Map[Ident, ArgMapping] = Map.empty
|
|
||||||
) {
|
|
||||||
def withVars(vars: Map[String, String]): ExternalCommand.WithVars =
|
|
||||||
ExternalCommand.WithVars(this, vars)
|
|
||||||
|
|
||||||
import ExternalCommand.pattern
|
|
||||||
|
|
||||||
def resolve(vars: Map[String, String]): SysCmd = {
|
|
||||||
val replace = ExternalCommand.replaceString(vars) _
|
|
||||||
val resolvedArgMappings =
|
|
||||||
argMappings.view.mapValues(_.resolve(replace).firstMatch).toMap
|
|
||||||
val resolvedArgs = args.map(replace).flatMap { arg =>
|
|
||||||
resolvedArgMappings
|
|
||||||
.find(e => pattern(e._1.id) == arg)
|
|
||||||
.map(_._2)
|
|
||||||
.getOrElse(List(arg))
|
|
||||||
}
|
|
||||||
|
|
||||||
SysCmd(replace(program), resolvedArgs: _*)
|
|
||||||
.withTimeout(timeout)
|
|
||||||
.withEnv(_ => Env(env).modifyValue(replace))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object ExternalCommand {
|
|
||||||
private val openPattern = "{{"
|
|
||||||
private val closePattern = "}}"
|
|
||||||
|
|
||||||
private def pattern(s: String): String = s"${openPattern}${s}${closePattern}"
|
|
||||||
|
|
||||||
def apply(program: String, args: Seq[String], timeout: Duration): ExternalCommand =
|
|
||||||
ExternalCommand(program, args, timeout, Map.empty, Map.empty)
|
|
||||||
|
|
||||||
final case class ArgMapping(
|
|
||||||
value: String,
|
|
||||||
mappings: List[ArgMatch]
|
|
||||||
) {
|
|
||||||
private[exec] def resolve(replace: String => String): ArgMapping =
|
|
||||||
ArgMapping(replace(value), mappings.map(_.resolve(replace)))
|
|
||||||
|
|
||||||
def firstMatch: List[String] =
|
|
||||||
mappings.find(am => value.matches(am.matches)).map(_.args).getOrElse(Nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class ArgMatch(
|
|
||||||
matches: String,
|
|
||||||
args: List[String]
|
|
||||||
) {
|
|
||||||
private[exec] def resolve(replace: String => String): ArgMatch =
|
|
||||||
ArgMatch(replace(matches), args.map(replace))
|
|
||||||
}
|
|
||||||
|
|
||||||
private def replaceString(vars: Map[String, String])(in: String): String =
|
|
||||||
vars.foldLeft(in) { case (result, (name, value)) =>
|
|
||||||
val key = s"{{$name}}"
|
|
||||||
result.replace(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class WithVars(cmd: ExternalCommand, vars: Map[String, String]) {
|
|
||||||
def resolved: SysCmd = cmd.resolve(vars)
|
|
||||||
def append(more: (String, String)*): WithVars =
|
|
||||||
WithVars(cmd, vars ++ more.toMap)
|
|
||||||
|
|
||||||
def withVar(key: String, value: String): WithVars =
|
|
||||||
WithVars(cmd, vars.updated(key, value))
|
|
||||||
|
|
||||||
def withVarOption(key: String, value: Option[String]): WithVars =
|
|
||||||
value.map(withVar(key, _)).getOrElse(this)
|
|
||||||
}
|
|
||||||
}
|
|
@ -38,20 +38,6 @@ trait SysExec[F[_]] {
|
|||||||
|
|
||||||
def waitFor(timeout: Option[Duration] = None): F[Int]
|
def waitFor(timeout: Option[Duration] = None): F[Int]
|
||||||
|
|
||||||
/** Uses `waitFor` and throws when return code is non-zero. Logs stderr and stdout while
|
|
||||||
* waiting.
|
|
||||||
*/
|
|
||||||
def runToSuccess(logger: Logger[F], timeout: Option[Duration] = None)(implicit
|
|
||||||
F: Async[F]
|
|
||||||
): F[Int]
|
|
||||||
|
|
||||||
/** Uses `waitFor` and throws when return code is non-zero. Logs stderr while waiting
|
|
||||||
* and collects stdout once finished successfully.
|
|
||||||
*/
|
|
||||||
def runToSuccessStdout(logger: Logger[F], timeout: Option[Duration] = None)(implicit
|
|
||||||
F: Async[F]
|
|
||||||
): F[String]
|
|
||||||
|
|
||||||
/** Sends a signal to the process to terminate it immediately */
|
/** Sends a signal to the process to terminate it immediately */
|
||||||
def cancel: F[Unit]
|
def cancel: F[Unit]
|
||||||
|
|
||||||
@ -89,12 +75,6 @@ object SysExec {
|
|||||||
proc <- startProcess(logger, cmd, workdir, stdin)
|
proc <- startProcess(logger, cmd, workdir, stdin)
|
||||||
fibers <- Resource.eval(Ref.of[F, List[F[Unit]]](Nil))
|
fibers <- Resource.eval(Ref.of[F, List[F[Unit]]](Nil))
|
||||||
} yield new SysExec[F] {
|
} yield new SysExec[F] {
|
||||||
private lazy val basicName: String =
|
|
||||||
cmd.program.lastIndexOf(java.io.File.separatorChar.toInt) match {
|
|
||||||
case n if n > 0 => cmd.program.drop(n + 1)
|
|
||||||
case _ => cmd.program.takeRight(16)
|
|
||||||
}
|
|
||||||
|
|
||||||
def stdout: Stream[F, Byte] =
|
def stdout: Stream[F, Byte] =
|
||||||
fs2.io.readInputStream(
|
fs2.io.readInputStream(
|
||||||
Sync[F].blocking(proc.getInputStream),
|
Sync[F].blocking(proc.getInputStream),
|
||||||
@ -127,39 +107,6 @@ object SysExec {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def runToSuccess(logger: Logger[F], timeout: Option[Duration])(implicit
|
|
||||||
F: Async[F]
|
|
||||||
): F[Int] =
|
|
||||||
logOutputs(logger, basicName).use(_.waitFor(timeout).flatMap {
|
|
||||||
case rc if rc == 0 => Sync[F].pure(0)
|
|
||||||
case rc =>
|
|
||||||
Sync[F].raiseError(
|
|
||||||
new Exception(s"Command `${cmd.program}` returned non-zero exit code ${rc}")
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
def runToSuccessStdout(logger: Logger[F], timeout: Option[Duration])(implicit
|
|
||||||
F: Async[F]
|
|
||||||
): F[String] =
|
|
||||||
F.background(
|
|
||||||
stderrLines
|
|
||||||
.through(line => Stream.eval(logger.debug(s"[$basicName (err)]: $line")))
|
|
||||||
.compile
|
|
||||||
.drain
|
|
||||||
).use { f1 =>
|
|
||||||
waitFor(timeout)
|
|
||||||
.flatMap {
|
|
||||||
case rc if rc == 0 => stdout.through(fs2.text.utf8.decode).compile.string
|
|
||||||
case rc =>
|
|
||||||
Sync[F].raiseError[String](
|
|
||||||
new Exception(
|
|
||||||
s"Command `${cmd.program}` returned non-zero exit code ${rc}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.flatTap(_ => f1)
|
|
||||||
}
|
|
||||||
|
|
||||||
def consumeOutputs(out: Pipe[F, String, Unit], err: Pipe[F, String, Unit])(implicit
|
def consumeOutputs(out: Pipe[F, String, Unit], err: Pipe[F, String, Unit])(implicit
|
||||||
F: Async[F]
|
F: Async[F]
|
||||||
): Resource[F, SysExec[F]] =
|
): Resource[F, SysExec[F]] =
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
package docspell.common.util
|
package docspell.common.util
|
||||||
|
|
||||||
import cats.data.OptionT
|
|
||||||
import cats.effect._
|
import cats.effect._
|
||||||
import cats.syntax.all._
|
import cats.syntax.all._
|
||||||
import cats.{Applicative, Monad}
|
import cats.{Applicative, Monad}
|
||||||
@ -27,10 +26,10 @@ object Directory {
|
|||||||
(dir :: dirs.toList).traverse_(Files[F].createDirectories(_))
|
(dir :: dirs.toList).traverse_(Files[F].createDirectories(_))
|
||||||
|
|
||||||
def nonEmpty[F[_]: Files: Sync](dir: Path): F[Boolean] =
|
def nonEmpty[F[_]: Files: Sync](dir: Path): F[Boolean] =
|
||||||
OptionT
|
List(
|
||||||
.whenM(Files[F].isDirectory(dir))(Files[F].list(dir).take(1).compile.toList)
|
Files[F].isDirectory(dir),
|
||||||
.map(_.nonEmpty)
|
Files[F].list(dir).take(1).compile.last.map(_.isDefined)
|
||||||
.isDefined
|
).sequence.map(_.forall(identity))
|
||||||
|
|
||||||
def isEmpty[F[_]: Files: Sync](dir: Path): F[Boolean] =
|
def isEmpty[F[_]: Files: Sync](dir: Path): F[Boolean] =
|
||||||
nonEmpty(dir).map(b => !b)
|
nonEmpty(dir).map(b => !b)
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 Eike K. & Contributors
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
package docspell.common.exec
|
|
||||||
|
|
||||||
import docspell.common.Duration
|
|
||||||
import docspell.common.Ident
|
|
||||||
import docspell.common.exec.Args
|
|
||||||
import docspell.common.exec.Env
|
|
||||||
import docspell.common.exec.ExternalCommand._
|
|
||||||
import docspell.common.exec.SysCmd
|
|
||||||
|
|
||||||
import munit.FunSuite
|
|
||||||
|
|
||||||
class ExternalCommandTest extends FunSuite {
|
|
||||||
|
|
||||||
test("resolve") {
|
|
||||||
val cmd = ExternalCommand(
|
|
||||||
program = "tesseract",
|
|
||||||
args = "{{infile}}" :: "{{lang-spec}}" :: "out" :: "pdf" :: "txt" :: Nil,
|
|
||||||
timeout = Duration.minutes(5),
|
|
||||||
env = Map.empty,
|
|
||||||
argMappings = Map(
|
|
||||||
Ident.unsafe("lang-spec") -> ArgMapping(
|
|
||||||
value = "{{lang}}",
|
|
||||||
mappings = List(
|
|
||||||
ArgMatch(
|
|
||||||
matches = "jpn_vert",
|
|
||||||
args = List("-l", "jpn_vert", "-c", "preserve_interword_spaces=1")
|
|
||||||
),
|
|
||||||
ArgMatch(
|
|
||||||
matches = ".*",
|
|
||||||
args = List("-l", "{{lang}}")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val varsDe = Map("lang" -> "de", "encoding" -> "UTF_8", "infile" -> "text.jpg")
|
|
||||||
assertEquals(
|
|
||||||
cmd.resolve(varsDe),
|
|
||||||
SysCmd(
|
|
||||||
"tesseract",
|
|
||||||
Args.of("text.jpg", "-l", "de", "out", "pdf", "txt"),
|
|
||||||
Env.empty,
|
|
||||||
Duration.minutes(5)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val varsJpnVert = varsDe.updated("lang", "jpn_vert")
|
|
||||||
assertEquals(
|
|
||||||
cmd.resolve(varsJpnVert),
|
|
||||||
SysCmd(
|
|
||||||
"tesseract",
|
|
||||||
Args.of(
|
|
||||||
"text.jpg",
|
|
||||||
"-l",
|
|
||||||
"jpn_vert",
|
|
||||||
"-c",
|
|
||||||
"preserve_interword_spaces=1",
|
|
||||||
"out",
|
|
||||||
"pdf",
|
|
||||||
"txt"
|
|
||||||
),
|
|
||||||
Env.empty,
|
|
||||||
Duration.minutes(5)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -16,7 +16,7 @@ import munit.CatsEffectSuite
|
|||||||
|
|
||||||
class DirectoryTest extends CatsEffectSuite with TestLoggingConfig {
|
class DirectoryTest extends CatsEffectSuite with TestLoggingConfig {
|
||||||
val logger = docspell.logging.getLogger[IO]
|
val logger = docspell.logging.getLogger[IO]
|
||||||
val tempDir = ResourceFunFixture(
|
val tempDir = ResourceFixture(
|
||||||
Files[IO].tempDirectory(Path("target").some, "directory-test-", None)
|
Files[IO].tempDirectory(Path("target").some, "directory-test-", None)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -11,8 +11,7 @@ import cats.implicits._
|
|||||||
import fs2.io.file.{Files, Path}
|
import fs2.io.file.{Files, Path}
|
||||||
import fs2.{Pipe, Stream}
|
import fs2.{Pipe, Stream}
|
||||||
|
|
||||||
import docspell.common.exec.ExternalCommand
|
import docspell.common._
|
||||||
import docspell.common.exec.SysExec
|
|
||||||
import docspell.common.util.File
|
import docspell.common.util.File
|
||||||
import docspell.convert.ConversionResult
|
import docspell.convert.ConversionResult
|
||||||
import docspell.convert.ConversionResult.{Handler, successPdf, successPdfTxt}
|
import docspell.convert.ConversionResult.{Handler, successPdf, successPdfTxt}
|
||||||
@ -22,11 +21,11 @@ private[extern] object ExternConv {
|
|||||||
|
|
||||||
def toPDF[F[_]: Async: Files, A](
|
def toPDF[F[_]: Async: Files, A](
|
||||||
name: String,
|
name: String,
|
||||||
cmdCfg: ExternalCommand.WithVars,
|
cmdCfg: SystemCommand.Config,
|
||||||
wd: Path,
|
wd: Path,
|
||||||
useStdin: Boolean,
|
useStdin: Boolean,
|
||||||
logger: Logger[F],
|
logger: Logger[F],
|
||||||
reader: (Path, Int) => F[ConversionResult[F]]
|
reader: (Path, SystemCommand.Result) => F[ConversionResult[F]]
|
||||||
)(in: Stream[F, Byte], handler: Handler[F, A]): F[A] =
|
)(in: Stream[F, Byte], handler: Handler[F, A]): F[A] =
|
||||||
Stream
|
Stream
|
||||||
.resource(File.withTempDir[F](wd, s"docspell-$name"))
|
.resource(File.withTempDir[F](wd, s"docspell-$name"))
|
||||||
@ -34,21 +33,32 @@ private[extern] object ExternConv {
|
|||||||
val inFile = dir.resolve("infile").absolute.normalize
|
val inFile = dir.resolve("infile").absolute.normalize
|
||||||
val out = dir.resolve("out.pdf").absolute.normalize
|
val out = dir.resolve("out.pdf").absolute.normalize
|
||||||
val sysCfg =
|
val sysCfg =
|
||||||
cmdCfg
|
cmdCfg.replace(
|
||||||
.withVar("outfile", out.toString)
|
Map(
|
||||||
.withVarOption("infile", Option.when(!useStdin)(inFile.toString))
|
"{{outfile}}" -> out.toString
|
||||||
.resolved
|
) ++
|
||||||
|
(if (!useStdin) Map("{{infile}}" -> inFile.toString)
|
||||||
|
else Map.empty)
|
||||||
|
)
|
||||||
|
|
||||||
val createInput: Pipe[F, Byte, Unit] =
|
val createInput: Pipe[F, Byte, Unit] =
|
||||||
if (useStdin) _ => Stream.emit(())
|
if (useStdin) _ => Stream.emit(())
|
||||||
else storeDataToFile(name, logger, inFile)
|
else storeDataToFile(name, logger, inFile)
|
||||||
|
|
||||||
in.through(createInput).evalMap { _ =>
|
in.through(createInput).flatMap { _ =>
|
||||||
SysExec(sysCfg, logger, Some(dir), Option.when(useStdin)(in))
|
SystemCommand
|
||||||
.flatMap(_.logOutputs(logger, name))
|
.exec[F](
|
||||||
.use { proc =>
|
sysCfg,
|
||||||
proc.waitFor().flatMap(rc => reader(out, rc).flatMap(handler.run))
|
logger,
|
||||||
}
|
Some(dir),
|
||||||
|
if (useStdin) in
|
||||||
|
else Stream.empty
|
||||||
|
)
|
||||||
|
.evalMap(result =>
|
||||||
|
logResult(name, result, logger)
|
||||||
|
.flatMap(_ => reader(out, result))
|
||||||
|
.flatMap(handler.run)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.compile
|
.compile
|
||||||
@ -64,9 +74,9 @@ private[extern] object ExternConv {
|
|||||||
def readResult[F[_]: Async: Files](
|
def readResult[F[_]: Async: Files](
|
||||||
chunkSize: Int,
|
chunkSize: Int,
|
||||||
logger: Logger[F]
|
logger: Logger[F]
|
||||||
)(out: Path, result: Int): F[ConversionResult[F]] =
|
)(out: Path, result: SystemCommand.Result): F[ConversionResult[F]] =
|
||||||
File.existsNonEmpty[F](out).flatMap {
|
File.existsNonEmpty[F](out).flatMap {
|
||||||
case true if result == 0 =>
|
case true if result.rc == 0 =>
|
||||||
val outTxt = out.resolveSibling(out.fileName.toString + ".txt")
|
val outTxt = out.resolveSibling(out.fileName.toString + ".txt")
|
||||||
File.existsNonEmpty[F](outTxt).flatMap {
|
File.existsNonEmpty[F](outTxt).flatMap {
|
||||||
case true =>
|
case true =>
|
||||||
@ -78,13 +88,13 @@ private[extern] object ExternConv {
|
|||||||
successPdf(File.readAll(out, chunkSize)).pure[F]
|
successPdf(File.readAll(out, chunkSize)).pure[F]
|
||||||
}
|
}
|
||||||
case true =>
|
case true =>
|
||||||
logger.warn(s"Command not successful (rc=${result}), but file exists.") *>
|
logger.warn(s"Command not successful (rc=${result.rc}), but file exists.") *>
|
||||||
successPdf(File.readAll(out, chunkSize)).pure[F]
|
successPdf(File.readAll(out, chunkSize)).pure[F]
|
||||||
|
|
||||||
case false =>
|
case false =>
|
||||||
ConversionResult
|
ConversionResult
|
||||||
.failure[F](
|
.failure[F](
|
||||||
new Exception(s"Command result=${result}. No output file found.")
|
new Exception(s"Command result=${result.rc}. No output file found.")
|
||||||
)
|
)
|
||||||
.pure[F]
|
.pure[F]
|
||||||
}
|
}
|
||||||
@ -93,25 +103,25 @@ private[extern] object ExternConv {
|
|||||||
outPrefix: String,
|
outPrefix: String,
|
||||||
chunkSize: Int,
|
chunkSize: Int,
|
||||||
logger: Logger[F]
|
logger: Logger[F]
|
||||||
)(out: Path, result: Int): F[ConversionResult[F]] = {
|
)(out: Path, result: SystemCommand.Result): F[ConversionResult[F]] = {
|
||||||
val outPdf = out.resolveSibling(s"$outPrefix.pdf")
|
val outPdf = out.resolveSibling(s"$outPrefix.pdf")
|
||||||
File.existsNonEmpty[F](outPdf).flatMap {
|
File.existsNonEmpty[F](outPdf).flatMap {
|
||||||
case true =>
|
case true =>
|
||||||
val outTxt = out.resolveSibling(s"$outPrefix.txt")
|
val outTxt = out.resolveSibling(s"$outPrefix.txt")
|
||||||
File.exists(outTxt).flatMap { txtExists =>
|
File.exists(outTxt).flatMap { txtExists =>
|
||||||
val pdfData = File.readAll(out, chunkSize)
|
val pdfData = File.readAll(out, chunkSize)
|
||||||
if (result == 0)
|
if (result.rc == 0)
|
||||||
if (txtExists) successPdfTxt(pdfData, File.readText(outTxt)).pure[F]
|
if (txtExists) successPdfTxt(pdfData, File.readText(outTxt)).pure[F]
|
||||||
else successPdf(pdfData).pure[F]
|
else successPdf(pdfData).pure[F]
|
||||||
else
|
else
|
||||||
logger.warn(s"Command not successful (rc=${result}), but file exists.") *>
|
logger.warn(s"Command not successful (rc=${result.rc}), but file exists.") *>
|
||||||
successPdf(pdfData).pure[F]
|
successPdf(pdfData).pure[F]
|
||||||
}
|
}
|
||||||
|
|
||||||
case false =>
|
case false =>
|
||||||
ConversionResult
|
ConversionResult
|
||||||
.failure[F](
|
.failure[F](
|
||||||
new Exception(s"Command result=${result}. No output file found.")
|
new Exception(s"Command result=${result.rc}. No output file found.")
|
||||||
)
|
)
|
||||||
.pure[F]
|
.pure[F]
|
||||||
}
|
}
|
||||||
@ -128,6 +138,14 @@ private[extern] object ExternConv {
|
|||||||
.drain ++
|
.drain ++
|
||||||
Stream.eval(storeFile(in, inFile))
|
Stream.eval(storeFile(in, inFile))
|
||||||
|
|
||||||
|
private def logResult[F[_]: Sync](
|
||||||
|
name: String,
|
||||||
|
result: SystemCommand.Result,
|
||||||
|
logger: Logger[F]
|
||||||
|
): F[Unit] =
|
||||||
|
logger.debug(s"$name stdout: ${result.stdout}") *>
|
||||||
|
logger.debug(s"$name stderr: ${result.stderr}")
|
||||||
|
|
||||||
private def storeFile[F[_]: Async: Files](
|
private def storeFile[F[_]: Async: Files](
|
||||||
in: Stream[F, Byte],
|
in: Stream[F, Byte],
|
||||||
target: Path
|
target: Path
|
||||||
|
@ -24,16 +24,14 @@ object OcrMyPdf {
|
|||||||
logger: Logger[F]
|
logger: Logger[F]
|
||||||
)(in: Stream[F, Byte], handler: Handler[F, A]): F[A] =
|
)(in: Stream[F, Byte], handler: Handler[F, A]): F[A] =
|
||||||
if (cfg.enabled) {
|
if (cfg.enabled) {
|
||||||
val reader: (Path, Int) => F[ConversionResult[F]] =
|
val reader: (Path, SystemCommand.Result) => F[ConversionResult[F]] =
|
||||||
ExternConv.readResult[F](chunkSize, logger)
|
ExternConv.readResult[F](chunkSize, logger)
|
||||||
|
|
||||||
val cmd = cfg.command.withVars(Map("lang" -> lang.iso3))
|
|
||||||
|
|
||||||
ExternConv.toPDF[F, A](
|
ExternConv.toPDF[F, A](
|
||||||
"ocrmypdf",
|
"ocrmypdf",
|
||||||
cmd,
|
cfg.command.replace(Map("{{lang}}" -> lang.iso3)),
|
||||||
cfg.workingDir,
|
cfg.workingDir,
|
||||||
useStdin = false,
|
false,
|
||||||
logger,
|
logger,
|
||||||
reader
|
reader
|
||||||
)(in, handler)
|
)(in, handler)
|
||||||
|
@ -8,10 +8,10 @@ package docspell.convert.extern
|
|||||||
|
|
||||||
import fs2.io.file.Path
|
import fs2.io.file.Path
|
||||||
|
|
||||||
import docspell.common.exec.ExternalCommand
|
import docspell.common.SystemCommand
|
||||||
|
|
||||||
case class OcrMyPdfConfig(
|
case class OcrMyPdfConfig(
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
command: ExternalCommand,
|
command: SystemCommand.Config,
|
||||||
workingDir: Path
|
workingDir: Path
|
||||||
)
|
)
|
||||||
|
@ -24,18 +24,17 @@ object Tesseract {
|
|||||||
logger: Logger[F]
|
logger: Logger[F]
|
||||||
)(in: Stream[F, Byte], handler: Handler[F, A]): F[A] = {
|
)(in: Stream[F, Byte], handler: Handler[F, A]): F[A] = {
|
||||||
val outBase = cfg.command.args.tail.headOption.getOrElse("out")
|
val outBase = cfg.command.args.tail.headOption.getOrElse("out")
|
||||||
val reader: (Path, Int) => F[ConversionResult[F]] =
|
val reader: (Path, SystemCommand.Result) => F[ConversionResult[F]] =
|
||||||
ExternConv.readResultTesseract[F](outBase, chunkSize, logger)
|
ExternConv.readResultTesseract[F](outBase, chunkSize, logger)
|
||||||
|
|
||||||
val cmd = cfg.command.withVars(Map("lang" -> lang.iso3))
|
|
||||||
|
|
||||||
ExternConv.toPDF[F, A](
|
ExternConv.toPDF[F, A](
|
||||||
"tesseract",
|
"tesseract",
|
||||||
cmd,
|
cfg.command.replace(Map("{{lang}}" -> lang.iso3)),
|
||||||
cfg.workingDir,
|
cfg.workingDir,
|
||||||
useStdin = false,
|
false,
|
||||||
logger,
|
logger,
|
||||||
reader
|
reader
|
||||||
)(in, handler)
|
)(in, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,6 @@ package docspell.convert.extern
|
|||||||
|
|
||||||
import fs2.io.file.Path
|
import fs2.io.file.Path
|
||||||
|
|
||||||
import docspell.common.exec.ExternalCommand
|
import docspell.common.SystemCommand
|
||||||
|
|
||||||
case class TesseractConfig(command: ExternalCommand, workingDir: Path)
|
case class TesseractConfig(command: SystemCommand.Config, workingDir: Path)
|
||||||
|
@ -10,6 +10,7 @@ import cats.effect._
|
|||||||
import fs2.Stream
|
import fs2.Stream
|
||||||
import fs2.io.file.{Files, Path}
|
import fs2.io.file.{Files, Path}
|
||||||
|
|
||||||
|
import docspell.common._
|
||||||
import docspell.convert.ConversionResult
|
import docspell.convert.ConversionResult
|
||||||
import docspell.convert.ConversionResult.Handler
|
import docspell.convert.ConversionResult.Handler
|
||||||
import docspell.logging.Logger
|
import docspell.logging.Logger
|
||||||
@ -21,15 +22,14 @@ object Unoconv {
|
|||||||
chunkSize: Int,
|
chunkSize: Int,
|
||||||
logger: Logger[F]
|
logger: Logger[F]
|
||||||
)(in: Stream[F, Byte], handler: Handler[F, A]): F[A] = {
|
)(in: Stream[F, Byte], handler: Handler[F, A]): F[A] = {
|
||||||
val reader: (Path, Int) => F[ConversionResult[F]] =
|
val reader: (Path, SystemCommand.Result) => F[ConversionResult[F]] =
|
||||||
ExternConv.readResult[F](chunkSize, logger)
|
ExternConv.readResult[F](chunkSize, logger)
|
||||||
val cmd = cfg.command.withVars(Map.empty)
|
|
||||||
|
|
||||||
ExternConv.toPDF[F, A](
|
ExternConv.toPDF[F, A](
|
||||||
"unoconv",
|
"unoconv",
|
||||||
cmd,
|
cfg.command,
|
||||||
cfg.workingDir,
|
cfg.workingDir,
|
||||||
useStdin = false,
|
false,
|
||||||
logger,
|
logger,
|
||||||
reader
|
reader
|
||||||
)(
|
)(
|
||||||
@ -37,4 +37,5 @@ object Unoconv {
|
|||||||
handler
|
handler
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,6 @@ package docspell.convert.extern
|
|||||||
|
|
||||||
import fs2.io.file.Path
|
import fs2.io.file.Path
|
||||||
|
|
||||||
import docspell.common.exec.ExternalCommand
|
import docspell.common.SystemCommand
|
||||||
|
|
||||||
case class UnoconvConfig(command: ExternalCommand, workingDir: Path)
|
case class UnoconvConfig(command: SystemCommand.Config, workingDir: Path)
|
||||||
|
@ -27,10 +27,10 @@ object Weasyprint {
|
|||||||
sanitizeHtml: SanitizeHtml,
|
sanitizeHtml: SanitizeHtml,
|
||||||
logger: Logger[F]
|
logger: Logger[F]
|
||||||
)(in: Stream[F, Byte], handler: Handler[F, A]): F[A] = {
|
)(in: Stream[F, Byte], handler: Handler[F, A]): F[A] = {
|
||||||
val reader: (Path, Int) => F[ConversionResult[F]] =
|
val reader: (Path, SystemCommand.Result) => F[ConversionResult[F]] =
|
||||||
ExternConv.readResult[F](chunkSize, logger)
|
ExternConv.readResult[F](chunkSize, logger)
|
||||||
|
|
||||||
val cmdCfg = cfg.command.withVars(Map("encoding" -> charset.name()))
|
val cmdCfg = cfg.command.replace(Map("{{encoding}}" -> charset.name()))
|
||||||
|
|
||||||
// html sanitize should (among other) remove links to invalid
|
// html sanitize should (among other) remove links to invalid
|
||||||
// protocols like cid: which is not supported by further
|
// protocols like cid: which is not supported by further
|
||||||
@ -51,4 +51,5 @@ object Weasyprint {
|
|||||||
handler
|
handler
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,6 @@ package docspell.convert.extern
|
|||||||
|
|
||||||
import fs2.io.file.Path
|
import fs2.io.file.Path
|
||||||
|
|
||||||
import docspell.common.exec.ExternalCommand
|
import docspell.common.SystemCommand
|
||||||
|
|
||||||
case class WeasyprintConfig(command: ExternalCommand, workingDir: Path)
|
case class WeasyprintConfig(command: SystemCommand.Config, workingDir: Path)
|
||||||
|
@ -27,10 +27,10 @@ object WkHtmlPdf {
|
|||||||
sanitizeHtml: SanitizeHtml,
|
sanitizeHtml: SanitizeHtml,
|
||||||
logger: Logger[F]
|
logger: Logger[F]
|
||||||
)(in: Stream[F, Byte], handler: Handler[F, A]): F[A] = {
|
)(in: Stream[F, Byte], handler: Handler[F, A]): F[A] = {
|
||||||
val reader: (Path, Int) => F[ConversionResult[F]] =
|
val reader: (Path, SystemCommand.Result) => F[ConversionResult[F]] =
|
||||||
ExternConv.readResult[F](chunkSize, logger)
|
ExternConv.readResult[F](chunkSize, logger)
|
||||||
|
|
||||||
val cmdCfg = cfg.command.withVars(Map("encoding" -> charset.name()))
|
val cmdCfg = cfg.command.replace(Map("{{encoding}}" -> charset.name()))
|
||||||
|
|
||||||
// html sanitize should (among other) remove links to invalid
|
// html sanitize should (among other) remove links to invalid
|
||||||
// protocols like cid: which is not supported by further
|
// protocols like cid: which is not supported by further
|
||||||
@ -58,4 +58,5 @@ object WkHtmlPdf {
|
|||||||
handler
|
handler
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,6 @@ package docspell.convert.extern
|
|||||||
|
|
||||||
import fs2.io.file.Path
|
import fs2.io.file.Path
|
||||||
|
|
||||||
import docspell.common.exec.ExternalCommand
|
import docspell.common.SystemCommand
|
||||||
|
|
||||||
case class WkHtmlPdfConfig(command: ExternalCommand, workingDir: Path)
|
case class WkHtmlPdfConfig(command: SystemCommand.Config, workingDir: Path)
|
||||||
|
@ -15,7 +15,6 @@ import cats.implicits._
|
|||||||
import fs2.Stream
|
import fs2.Stream
|
||||||
|
|
||||||
import docspell.common._
|
import docspell.common._
|
||||||
import docspell.common.exec._
|
|
||||||
import docspell.common.util.File
|
import docspell.common.util.File
|
||||||
import docspell.convert.ConversionResult.Handler
|
import docspell.convert.ConversionResult.Handler
|
||||||
import docspell.convert.ConvertConfig.HtmlConverter
|
import docspell.convert.ConvertConfig.HtmlConverter
|
||||||
@ -37,7 +36,7 @@ class ConversionTest extends FunSuite with FileChecks with TestLoggingConfig {
|
|||||||
3000 * 3000,
|
3000 * 3000,
|
||||||
MarkdownConfig("body { padding: 2em 5em; }"),
|
MarkdownConfig("body { padding: 2em 5em; }"),
|
||||||
WkHtmlPdfConfig(
|
WkHtmlPdfConfig(
|
||||||
ExternalCommand(
|
SystemCommand.Config(
|
||||||
"wkhtmltopdf",
|
"wkhtmltopdf",
|
||||||
Seq("-s", "A4", "--encoding", "UTF-8", "-", "{{outfile}}"),
|
Seq("-s", "A4", "--encoding", "UTF-8", "-", "{{outfile}}"),
|
||||||
Duration.seconds(20)
|
Duration.seconds(20)
|
||||||
@ -45,7 +44,7 @@ class ConversionTest extends FunSuite with FileChecks with TestLoggingConfig {
|
|||||||
target
|
target
|
||||||
),
|
),
|
||||||
WeasyprintConfig(
|
WeasyprintConfig(
|
||||||
ExternalCommand(
|
SystemCommand.Config(
|
||||||
"weasyprint",
|
"weasyprint",
|
||||||
Seq("--encoding", "UTF-8", "-", "{{outfile}}"),
|
Seq("--encoding", "UTF-8", "-", "{{outfile}}"),
|
||||||
Duration.seconds(20)
|
Duration.seconds(20)
|
||||||
@ -54,7 +53,7 @@ class ConversionTest extends FunSuite with FileChecks with TestLoggingConfig {
|
|||||||
),
|
),
|
||||||
HtmlConverter.Wkhtmltopdf,
|
HtmlConverter.Wkhtmltopdf,
|
||||||
TesseractConfig(
|
TesseractConfig(
|
||||||
ExternalCommand(
|
SystemCommand.Config(
|
||||||
"tesseract",
|
"tesseract",
|
||||||
Seq("{{infile}}", "out", "-l", "deu", "pdf", "txt"),
|
Seq("{{infile}}", "out", "-l", "deu", "pdf", "txt"),
|
||||||
Duration.seconds(20)
|
Duration.seconds(20)
|
||||||
@ -62,7 +61,7 @@ class ConversionTest extends FunSuite with FileChecks with TestLoggingConfig {
|
|||||||
target
|
target
|
||||||
),
|
),
|
||||||
UnoconvConfig(
|
UnoconvConfig(
|
||||||
ExternalCommand(
|
SystemCommand.Config(
|
||||||
"unoconv",
|
"unoconv",
|
||||||
Seq("-f", "pdf", "-o", "{{outfile}}", "{{infile}}"),
|
Seq("-f", "pdf", "-o", "{{outfile}}", "{{infile}}"),
|
||||||
Duration.seconds(20)
|
Duration.seconds(20)
|
||||||
@ -70,8 +69,8 @@ class ConversionTest extends FunSuite with FileChecks with TestLoggingConfig {
|
|||||||
target
|
target
|
||||||
),
|
),
|
||||||
OcrMyPdfConfig(
|
OcrMyPdfConfig(
|
||||||
enabled = true,
|
true,
|
||||||
ExternalCommand(
|
SystemCommand.Config(
|
||||||
"ocrmypdf",
|
"ocrmypdf",
|
||||||
Seq(
|
Seq(
|
||||||
"-l",
|
"-l",
|
||||||
@ -87,7 +86,7 @@ class ConversionTest extends FunSuite with FileChecks with TestLoggingConfig {
|
|||||||
),
|
),
|
||||||
target
|
target
|
||||||
),
|
),
|
||||||
ConvertConfig.DecryptPdf(enabled = true, Nil)
|
ConvertConfig.DecryptPdf(true, Nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
val conversion =
|
val conversion =
|
||||||
|
@ -14,7 +14,6 @@ import cats.effect.unsafe.implicits.global
|
|||||||
import fs2.io.file.Path
|
import fs2.io.file.Path
|
||||||
|
|
||||||
import docspell.common._
|
import docspell.common._
|
||||||
import docspell.common.exec._
|
|
||||||
import docspell.common.util.File
|
import docspell.common.util.File
|
||||||
import docspell.convert._
|
import docspell.convert._
|
||||||
import docspell.files.ExampleFiles
|
import docspell.files.ExampleFiles
|
||||||
@ -28,7 +27,7 @@ class ExternConvTest extends FunSuite with FileChecks with TestLoggingConfig {
|
|||||||
val target = File.path(Paths.get("target"))
|
val target = File.path(Paths.get("target"))
|
||||||
|
|
||||||
test("convert html to pdf") {
|
test("convert html to pdf") {
|
||||||
val cfg = ExternalCommand(
|
val cfg = SystemCommand.Config(
|
||||||
"wkhtmltopdf",
|
"wkhtmltopdf",
|
||||||
Seq("-s", "A4", "--encoding", "UTF-8", "-", "{{outfile}}"),
|
Seq("-s", "A4", "--encoding", "UTF-8", "-", "{{outfile}}"),
|
||||||
Duration.seconds(20)
|
Duration.seconds(20)
|
||||||
@ -54,7 +53,7 @@ class ExternConvTest extends FunSuite with FileChecks with TestLoggingConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test("convert office to pdf") {
|
test("convert office to pdf") {
|
||||||
val cfg = ExternalCommand(
|
val cfg = SystemCommand.Config(
|
||||||
"unoconv",
|
"unoconv",
|
||||||
Seq("-f", "pdf", "-o", "{{outfile}}", "{{infile}}"),
|
Seq("-f", "pdf", "-o", "{{outfile}}", "{{infile}}"),
|
||||||
Duration.seconds(20)
|
Duration.seconds(20)
|
||||||
@ -81,7 +80,7 @@ class ExternConvTest extends FunSuite with FileChecks with TestLoggingConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test("convert image to pdf") {
|
test("convert image to pdf") {
|
||||||
val cfg = ExternalCommand(
|
val cfg = SystemCommand.Config(
|
||||||
"tesseract",
|
"tesseract",
|
||||||
Seq("{{infile}}", "out", "-l", "deu", "pdf", "txt"),
|
Seq("{{infile}}", "out", "-l", "deu", "pdf", "txt"),
|
||||||
Duration.seconds(20)
|
Duration.seconds(20)
|
||||||
@ -106,4 +105,5 @@ class ExternConvTest extends FunSuite with FileChecks with TestLoggingConfig {
|
|||||||
)
|
)
|
||||||
.unsafeRunSync()
|
.unsafeRunSync()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,7 @@ import cats.effect._
|
|||||||
import fs2.Stream
|
import fs2.Stream
|
||||||
import fs2.io.file.{Files, Path}
|
import fs2.io.file.{Files, Path}
|
||||||
|
|
||||||
import docspell.common.exec.ExternalCommand
|
import docspell.common._
|
||||||
import docspell.common.exec.SysExec
|
|
||||||
import docspell.common.util.File
|
import docspell.common.util.File
|
||||||
import docspell.logging.Logger
|
import docspell.logging.Logger
|
||||||
|
|
||||||
@ -78,17 +77,14 @@ object Ocr {
|
|||||||
else cfg.ghostscript.command.args
|
else cfg.ghostscript.command.args
|
||||||
val cmd = cfg.ghostscript.command
|
val cmd = cfg.ghostscript.command
|
||||||
.copy(args = xargs)
|
.copy(args = xargs)
|
||||||
.withVars(
|
.replace(
|
||||||
Map(
|
Map(
|
||||||
"infile" -> "-",
|
"{{infile}}" -> "-",
|
||||||
"outfile" -> "%d.tif"
|
"{{outfile}}" -> "%d.tif"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.resolved
|
SystemCommand
|
||||||
|
.execSuccess(cmd, logger, wd = Some(wd), stdin = pdf)
|
||||||
Stream
|
|
||||||
.resource(SysExec(cmd, logger, Some(wd), Some(pdf)))
|
|
||||||
.evalMap(_.runToSuccess(logger))
|
|
||||||
.flatMap(_ => File.listFiles(pathEndsWith(".tif"), wd))
|
.flatMap(_ => File.listFiles(pathEndsWith(".tif"), wd))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,22 +93,18 @@ object Ocr {
|
|||||||
*/
|
*/
|
||||||
private[extract] def runGhostscriptFile[F[_]: Async: Files](
|
private[extract] def runGhostscriptFile[F[_]: Async: Files](
|
||||||
pdf: Path,
|
pdf: Path,
|
||||||
ghostscript: ExternalCommand,
|
ghostscript: SystemCommand.Config,
|
||||||
wd: Path,
|
wd: Path,
|
||||||
logger: Logger[F]
|
logger: Logger[F]
|
||||||
): Stream[F, Path] = {
|
): Stream[F, Path] = {
|
||||||
val cmd = ghostscript
|
val cmd = ghostscript.replace(
|
||||||
.withVars(
|
Map(
|
||||||
Map(
|
"{{infile}}" -> pdf.absolute.toString,
|
||||||
"infile" -> pdf.absolute.toString,
|
"{{outfile}}" -> "%d.tif"
|
||||||
"outfile" -> "%d.tif"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.resolved
|
)
|
||||||
|
SystemCommand
|
||||||
Stream
|
.execSuccess[F](cmd, logger, wd = Some(wd))
|
||||||
.resource(SysExec(cmd, logger, Some(wd)))
|
|
||||||
.evalMap(_.runToSuccess(logger))
|
|
||||||
.flatMap(_ => File.listFiles(pathEndsWith(".tif"), wd))
|
.flatMap(_ => File.listFiles(pathEndsWith(".tif"), wd))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,23 +116,19 @@ object Ocr {
|
|||||||
*/
|
*/
|
||||||
private[extract] def runUnpaperFile[F[_]: Async](
|
private[extract] def runUnpaperFile[F[_]: Async](
|
||||||
img: Path,
|
img: Path,
|
||||||
unpaper: ExternalCommand,
|
unpaper: SystemCommand.Config,
|
||||||
wd: Option[Path],
|
wd: Option[Path],
|
||||||
logger: Logger[F]
|
logger: Logger[F]
|
||||||
): Stream[F, Path] = {
|
): Stream[F, Path] = {
|
||||||
val targetFile = img.resolveSibling("u-" + img.fileName.toString).absolute
|
val targetFile = img.resolveSibling("u-" + img.fileName.toString).absolute
|
||||||
val cmd = unpaper
|
val cmd = unpaper.replace(
|
||||||
.withVars(
|
Map(
|
||||||
Map(
|
"{{infile}}" -> img.absolute.toString,
|
||||||
"infile" -> img.absolute.toString,
|
"{{outfile}}" -> targetFile.toString
|
||||||
"outfile" -> targetFile.toString
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.resolved
|
)
|
||||||
|
SystemCommand
|
||||||
Stream
|
.execSuccess[F](cmd, logger, wd = wd)
|
||||||
.resource(SysExec(cmd, logger, wd))
|
|
||||||
.evalMap(_.runToSuccess(logger))
|
|
||||||
.map(_ => targetFile)
|
.map(_ => targetFile)
|
||||||
.handleErrorWith { th =>
|
.handleErrorWith { th =>
|
||||||
logger
|
logger
|
||||||
@ -162,14 +150,12 @@ object Ocr {
|
|||||||
// so use the parent as working dir
|
// so use the parent as working dir
|
||||||
runUnpaperFile(img, config.unpaper.command, img.parent, logger).flatMap { uimg =>
|
runUnpaperFile(img, config.unpaper.command, img.parent, logger).flatMap { uimg =>
|
||||||
val cmd = config.tesseract.command
|
val cmd = config.tesseract.command
|
||||||
.withVars(
|
.replace(
|
||||||
Map("file" -> uimg.fileName.toString, "lang" -> fixLanguage(lang))
|
Map("{{file}}" -> uimg.fileName.toString, "{{lang}}" -> fixLanguage(lang))
|
||||||
)
|
)
|
||||||
.resolved
|
SystemCommand
|
||||||
|
.execSuccess[F](cmd, logger, wd = uimg.parent)
|
||||||
Stream
|
.map(_.stdout)
|
||||||
.resource(SysExec(cmd, logger, uimg.parent))
|
|
||||||
.evalMap(_.runToSuccessStdout(logger))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Run tesseract on the given image file and return the extracted text. */
|
/** Run tesseract on the given image file and return the extracted text. */
|
||||||
@ -180,12 +166,8 @@ object Ocr {
|
|||||||
config: OcrConfig
|
config: OcrConfig
|
||||||
): Stream[F, String] = {
|
): Stream[F, String] = {
|
||||||
val cmd = config.tesseract.command
|
val cmd = config.tesseract.command
|
||||||
.withVars(Map("file" -> "stdin", "lang" -> fixLanguage(lang)))
|
.replace(Map("{{file}}" -> "stdin", "{{lang}}" -> fixLanguage(lang)))
|
||||||
.resolved
|
SystemCommand.execSuccess(cmd, logger, stdin = img).map(_.stdout)
|
||||||
|
|
||||||
Stream
|
|
||||||
.resource(SysExec(cmd, logger, None, Some(img)))
|
|
||||||
.evalMap(_.runToSuccessStdout(logger))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def fixLanguage(lang: String): String =
|
private def fixLanguage(lang: String): String =
|
||||||
|
@ -6,9 +6,12 @@
|
|||||||
|
|
||||||
package docspell.extract.ocr
|
package docspell.extract.ocr
|
||||||
|
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
import fs2.io.file.Path
|
import fs2.io.file.Path
|
||||||
|
|
||||||
import docspell.common.exec.ExternalCommand
|
import docspell.common._
|
||||||
|
import docspell.common.util.File
|
||||||
|
|
||||||
case class OcrConfig(
|
case class OcrConfig(
|
||||||
maxImageSize: Int,
|
maxImageSize: Int,
|
||||||
@ -22,10 +25,43 @@ object OcrConfig {
|
|||||||
|
|
||||||
case class PageRange(begin: Int)
|
case class PageRange(begin: Int)
|
||||||
|
|
||||||
case class Ghostscript(command: ExternalCommand, workingDir: Path)
|
case class Ghostscript(command: SystemCommand.Config, workingDir: Path)
|
||||||
|
|
||||||
case class Tesseract(command: ExternalCommand)
|
case class Tesseract(command: SystemCommand.Config)
|
||||||
|
|
||||||
case class Unpaper(command: ExternalCommand)
|
case class Unpaper(command: SystemCommand.Config)
|
||||||
|
|
||||||
|
val default = OcrConfig(
|
||||||
|
maxImageSize = 3000 * 3000,
|
||||||
|
pageRange = PageRange(10),
|
||||||
|
ghostscript = Ghostscript(
|
||||||
|
SystemCommand.Config(
|
||||||
|
"gs",
|
||||||
|
Seq(
|
||||||
|
"-dNOPAUSE",
|
||||||
|
"-dBATCH",
|
||||||
|
"-dSAFER",
|
||||||
|
"-sDEVICE=tiffscaled8",
|
||||||
|
"-sOutputFile={{outfile}}",
|
||||||
|
"{{infile}}"
|
||||||
|
),
|
||||||
|
Duration.seconds(30)
|
||||||
|
),
|
||||||
|
File.path(
|
||||||
|
Paths.get(System.getProperty("java.io.tmpdir")).resolve("docspell-extraction")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
unpaper = Unpaper(
|
||||||
|
SystemCommand
|
||||||
|
.Config("unpaper", Seq("{{infile}}", "{{outfile}}"), Duration.seconds(30))
|
||||||
|
),
|
||||||
|
tesseract = Tesseract(
|
||||||
|
SystemCommand
|
||||||
|
.Config(
|
||||||
|
"tesseract",
|
||||||
|
Seq("{{file}}", "stdout", "-l", "{{lang}}"),
|
||||||
|
Duration.minutes(1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -6,14 +6,9 @@
|
|||||||
|
|
||||||
package docspell.extract.ocr
|
package docspell.extract.ocr
|
||||||
|
|
||||||
import java.nio.file.Paths
|
|
||||||
|
|
||||||
import cats.effect.IO
|
import cats.effect.IO
|
||||||
import cats.effect.unsafe.implicits.global
|
import cats.effect.unsafe.implicits.global
|
||||||
|
|
||||||
import docspell.common.Duration
|
|
||||||
import docspell.common.exec.ExternalCommand
|
|
||||||
import docspell.common.util.File
|
|
||||||
import docspell.files.TestFiles
|
import docspell.files.TestFiles
|
||||||
import docspell.logging.TestLoggingConfig
|
import docspell.logging.TestLoggingConfig
|
||||||
|
|
||||||
@ -26,7 +21,7 @@ class TextExtractionSuite extends FunSuite with TestLoggingConfig {
|
|||||||
|
|
||||||
test("extract english pdf".ignore) {
|
test("extract english pdf".ignore) {
|
||||||
val text = TextExtract
|
val text = TextExtract
|
||||||
.extract[IO](letterSourceEN, logger, "eng", TextExtractionSuite.defaultConfig)
|
.extract[IO](letterSourceEN, logger, "eng", OcrConfig.default)
|
||||||
.compile
|
.compile
|
||||||
.lastOrError
|
.lastOrError
|
||||||
.unsafeRunSync()
|
.unsafeRunSync()
|
||||||
@ -36,7 +31,7 @@ class TextExtractionSuite extends FunSuite with TestLoggingConfig {
|
|||||||
test("extract german pdf".ignore) {
|
test("extract german pdf".ignore) {
|
||||||
val expect = TestFiles.letterDEText
|
val expect = TestFiles.letterDEText
|
||||||
val extract = TextExtract
|
val extract = TextExtract
|
||||||
.extract[IO](letterSourceDE, logger, "deu", TextExtractionSuite.defaultConfig)
|
.extract[IO](letterSourceDE, logger, "deu", OcrConfig.default)
|
||||||
.compile
|
.compile
|
||||||
.lastOrError
|
.lastOrError
|
||||||
.unsafeRunSync()
|
.unsafeRunSync()
|
||||||
@ -44,37 +39,3 @@ class TextExtractionSuite extends FunSuite with TestLoggingConfig {
|
|||||||
assertEquals(extract.value, expect)
|
assertEquals(extract.value, expect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object TextExtractionSuite {
|
|
||||||
val defaultConfig = OcrConfig(
|
|
||||||
maxImageSize = 3000 * 3000,
|
|
||||||
pageRange = OcrConfig.PageRange(10),
|
|
||||||
ghostscript = OcrConfig.Ghostscript(
|
|
||||||
ExternalCommand(
|
|
||||||
"gs",
|
|
||||||
Seq(
|
|
||||||
"-dNOPAUSE",
|
|
||||||
"-dBATCH",
|
|
||||||
"-dSAFER",
|
|
||||||
"-sDEVICE=tiffscaled8",
|
|
||||||
"-sOutputFile={{outfile}}",
|
|
||||||
"{{infile}}"
|
|
||||||
),
|
|
||||||
Duration.seconds(30)
|
|
||||||
),
|
|
||||||
File.path(
|
|
||||||
Paths.get(System.getProperty("java.io.tmpdir")).resolve("docspell-extraction")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
unpaper = OcrConfig.Unpaper(
|
|
||||||
ExternalCommand("unpaper", Seq("{{infile}}", "{{outfile}}"), Duration.seconds(30))
|
|
||||||
),
|
|
||||||
tesseract = OcrConfig.Tesseract(
|
|
||||||
ExternalCommand(
|
|
||||||
"tesseract",
|
|
||||||
Seq("{{file}}", "stdout", "-l", "{{lang}}"),
|
|
||||||
Duration.minutes(1)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
@ -19,7 +19,7 @@ import munit._
|
|||||||
|
|
||||||
class ZipTest extends CatsEffectSuite with TestLoggingConfig {
|
class ZipTest extends CatsEffectSuite with TestLoggingConfig {
|
||||||
val logger = docspell.logging.getLogger[IO]
|
val logger = docspell.logging.getLogger[IO]
|
||||||
val tempDir = ResourceFunFixture(
|
val tempDir = ResourceFixture(
|
||||||
Files[IO].tempDirectory(Path("target").some, "zip-test-", None)
|
Files[IO].tempDirectory(Path("target").some, "zip-test-", None)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -201,7 +201,6 @@ object FtsRepository extends DoobieMeta {
|
|||||||
case Language.Czech => "simple"
|
case Language.Czech => "simple"
|
||||||
case Language.Latvian => "simple"
|
case Language.Latvian => "simple"
|
||||||
case Language.Japanese => "simple"
|
case Language.Japanese => "simple"
|
||||||
case Language.JpnVert => "simple"
|
|
||||||
case Language.Hebrew => "simple"
|
case Language.Hebrew => "simple"
|
||||||
case Language.Lithuanian => "simple"
|
case Language.Lithuanian => "simple"
|
||||||
case Language.Polish => "simple"
|
case Language.Polish => "simple"
|
||||||
|
@ -45,7 +45,7 @@ object SolrMigration {
|
|||||||
description,
|
description,
|
||||||
FtsMigration.Result.reIndexAll.pure[F]
|
FtsMigration.Result.reIndexAll.pure[F]
|
||||||
),
|
),
|
||||||
dataChangeOnly = true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
def indexAll[F[_]: Applicative](
|
def indexAll[F[_]: Applicative](
|
||||||
@ -59,7 +59,7 @@ object SolrMigration {
|
|||||||
description,
|
description,
|
||||||
FtsMigration.Result.indexAll.pure[F]
|
FtsMigration.Result.indexAll.pure[F]
|
||||||
),
|
),
|
||||||
dataChangeOnly = true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
def apply[F[_]: Functor](
|
def apply[F[_]: Functor](
|
||||||
@ -74,6 +74,6 @@ object SolrMigration {
|
|||||||
description,
|
description,
|
||||||
task.map(_ => FtsMigration.Result.workDone)
|
task.map(_ => FtsMigration.Result.workDone)
|
||||||
),
|
),
|
||||||
dataChangeOnly = false
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -299,22 +299,14 @@ object SolrSetup {
|
|||||||
Map("add-field" -> body.asJson).asJson
|
Map("add-field" -> body.asJson).asJson
|
||||||
|
|
||||||
def string(field: Field): AddField =
|
def string(field: Field): AddField =
|
||||||
AddField(field, "string", stored = true, indexed = true, multiValued = false)
|
AddField(field, "string", true, true, false)
|
||||||
|
|
||||||
def textGeneral(field: Field): AddField =
|
def textGeneral(field: Field): AddField =
|
||||||
AddField(field, "text_general", stored = true, indexed = true, multiValued = false)
|
AddField(field, "text_general", true, true, false)
|
||||||
|
|
||||||
def textLang(field: Field, lang: Language): AddField =
|
def textLang(field: Field, lang: Language): AddField =
|
||||||
if (lang == Language.Czech)
|
if (lang == Language.Czech) AddField(field, s"text_cz", true, true, false)
|
||||||
AddField(field, s"text_cz", stored = true, indexed = true, multiValued = false)
|
else AddField(field, s"text_${lang.iso2}", true, true, false)
|
||||||
else
|
|
||||||
AddField(
|
|
||||||
field,
|
|
||||||
s"text_${lang.iso2}",
|
|
||||||
stored = true,
|
|
||||||
indexed = true,
|
|
||||||
multiValued = false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case class DeleteField(name: Field)
|
case class DeleteField(name: Field)
|
||||||
|
@ -595,30 +595,11 @@ Docpell Update Check
|
|||||||
tesseract = {
|
tesseract = {
|
||||||
command = {
|
command = {
|
||||||
program = "tesseract"
|
program = "tesseract"
|
||||||
# Custom Language Mappings Below
|
|
||||||
# Japanese Vertical Mapping
|
|
||||||
arg-mappings = {
|
|
||||||
"tesseract_lang" = {
|
|
||||||
value = "{{lang}}"
|
|
||||||
mappings = [
|
|
||||||
{
|
|
||||||
matches = "jpn_vert"
|
|
||||||
args = [ "-l", "jpn_vert", "-c", "preserve_interword_spaces=1" ]
|
|
||||||
},
|
|
||||||
# Start Other Custom Language Mappings Here
|
|
||||||
# Default Mapping Below
|
|
||||||
{
|
|
||||||
matches = ".*"
|
|
||||||
args = [ "-l", "{{lang}}" ]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
# Default arguments for all processing go below.
|
|
||||||
args = [
|
args = [
|
||||||
"{{infile}}",
|
"{{infile}}",
|
||||||
"out",
|
"out",
|
||||||
"{{tesseract_lang}}",
|
"-l",
|
||||||
|
"{{lang}}",
|
||||||
"pdf",
|
"pdf",
|
||||||
"txt"
|
"txt"
|
||||||
]
|
]
|
||||||
@ -670,34 +651,8 @@ Docpell Update Check
|
|||||||
enabled = true
|
enabled = true
|
||||||
command = {
|
command = {
|
||||||
program = "ocrmypdf"
|
program = "ocrmypdf"
|
||||||
# Custom argument mappings for this program.
|
|
||||||
arg-mappings = {
|
|
||||||
"ocr_lang" = {
|
|
||||||
value = "{{lang}}"
|
|
||||||
# Custom Language Mappings Below
|
|
||||||
# Japanese Vertical Mapping
|
|
||||||
mappings = [
|
|
||||||
{
|
|
||||||
matches = "jpn_vert"
|
|
||||||
args = [ "-l", "jpn_vert", "--pdf-renderer", "sandwich", "--tesseract-pagesegmode", "5", "--output-type", "pdf" ]
|
|
||||||
},
|
|
||||||
# Japanese Mapping for OCR Optimization
|
|
||||||
{
|
|
||||||
matches = "jpn"
|
|
||||||
args = [ "-l", "jpn", "--output-type", "pdf" ]
|
|
||||||
},
|
|
||||||
# Start Other Custom Language Mappings Here
|
|
||||||
# Default Mapping Below
|
|
||||||
{
|
|
||||||
matches = ".*"
|
|
||||||
args = [ "-l", "{{lang}}" ]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
# Default arguments for all processing go below.
|
|
||||||
args = [
|
args = [
|
||||||
"{{ocr_lang}}",
|
"-l", "{{lang}}",
|
||||||
"--skip-text",
|
"--skip-text",
|
||||||
"--deskew",
|
"--deskew",
|
||||||
"-j", "1",
|
"-j", "1",
|
||||||
|
@ -30,7 +30,7 @@ object EmptyTrashTask {
|
|||||||
UserTask(
|
UserTask(
|
||||||
args.periodicTaskId,
|
args.periodicTaskId,
|
||||||
EmptyTrashArgs.taskName,
|
EmptyTrashArgs.taskName,
|
||||||
enabled = true,
|
true,
|
||||||
ce,
|
ce,
|
||||||
None,
|
None,
|
||||||
args
|
args
|
||||||
|
@ -29,23 +29,23 @@ object FileCopyTask {
|
|||||||
case class CopyResult(success: Boolean, message: String, counter: List[Counter])
|
case class CopyResult(success: Boolean, message: String, counter: List[Counter])
|
||||||
object CopyResult {
|
object CopyResult {
|
||||||
def noSourceImpl: CopyResult =
|
def noSourceImpl: CopyResult =
|
||||||
CopyResult(success = false, "No source BinaryStore implementation found!", Nil)
|
CopyResult(false, "No source BinaryStore implementation found!", Nil)
|
||||||
|
|
||||||
def noTargetImpl: CopyResult =
|
def noTargetImpl: CopyResult =
|
||||||
CopyResult(success = false, "No target BinaryStore implementation found!", Nil)
|
CopyResult(false, "No target BinaryStore implementation found!", Nil)
|
||||||
|
|
||||||
def noSourceStore(id: Ident): CopyResult =
|
def noSourceStore(id: Ident): CopyResult =
|
||||||
CopyResult(
|
CopyResult(
|
||||||
success = false,
|
false,
|
||||||
s"No source file repo found with id: ${id.id}. Make sure it is present in the config.",
|
s"No source file repo found with id: ${id.id}. Make sure it is present in the config.",
|
||||||
Nil
|
Nil
|
||||||
)
|
)
|
||||||
|
|
||||||
def noTargetStore: CopyResult =
|
def noTargetStore: CopyResult =
|
||||||
CopyResult(success = false, "No target file repositories defined", Nil)
|
CopyResult(false, "No target file repositories defined", Nil)
|
||||||
|
|
||||||
def success(counter: NonEmptyList[Counter]): CopyResult =
|
def success(counter: NonEmptyList[Counter]): CopyResult =
|
||||||
CopyResult(success = true, "Done", counter.toList)
|
CopyResult(true, "Done", counter.toList)
|
||||||
|
|
||||||
implicit val binaryIdCodec: Codec[BinaryId] =
|
implicit val binaryIdCodec: Codec[BinaryId] =
|
||||||
Codec.from(
|
Codec.from(
|
||||||
@ -96,10 +96,8 @@ object FileCopyTask {
|
|||||||
.fromList(targets.filter(_ != srcConfig))
|
.fromList(targets.filter(_ != srcConfig))
|
||||||
.toRight(CopyResult.noTargetStore)
|
.toRight(CopyResult.noTargetStore)
|
||||||
|
|
||||||
srcRepo = store.createFileRepository(srcConfig, withAttributeStore = true)
|
srcRepo = store.createFileRepository(srcConfig, true)
|
||||||
targetRepos = trgConfig.map(
|
targetRepos = trgConfig.map(store.createFileRepository(_, false))
|
||||||
store.createFileRepository(_, withAttributeStore = false)
|
|
||||||
)
|
|
||||||
} yield (srcRepo, targetRepos)
|
} yield (srcRepo, targetRepos)
|
||||||
|
|
||||||
data match {
|
data match {
|
||||||
|
@ -13,8 +13,8 @@ case class CleanupResult(removed: Int, disabled: Boolean) {
|
|||||||
def asString = if (disabled) "disabled" else s"$removed"
|
def asString = if (disabled) "disabled" else s"$removed"
|
||||||
}
|
}
|
||||||
object CleanupResult {
|
object CleanupResult {
|
||||||
def of(n: Int): CleanupResult = CleanupResult(n, disabled = false)
|
def of(n: Int): CleanupResult = CleanupResult(n, false)
|
||||||
def disabled: CleanupResult = CleanupResult(0, disabled = true)
|
def disabled: CleanupResult = CleanupResult(0, true)
|
||||||
|
|
||||||
implicit val jsonEncoder: Encoder[CleanupResult] =
|
implicit val jsonEncoder: Encoder[CleanupResult] =
|
||||||
deriveEncoder
|
deriveEncoder
|
||||||
|
@ -55,7 +55,7 @@ object HouseKeepingTask {
|
|||||||
UserTask(
|
UserTask(
|
||||||
periodicId,
|
periodicId,
|
||||||
taskName,
|
taskName,
|
||||||
enabled = true,
|
true,
|
||||||
ce,
|
ce,
|
||||||
"Docspell house-keeping".some,
|
"Docspell house-keeping".some,
|
||||||
()
|
()
|
||||||
|
@ -222,13 +222,13 @@ object FindProposal {
|
|||||||
def searchExact[F[_]: Sync](ctx: Context[F, Args], store: Store[F]): Finder[F] =
|
def searchExact[F[_]: Sync](ctx: Context[F, Args], store: Store[F]): Finder[F] =
|
||||||
labels =>
|
labels =>
|
||||||
labels.toList
|
labels.toList
|
||||||
.traverse(nl => search(nl, exact = true, ctx, store))
|
.traverse(nl => search(nl, true, ctx, store))
|
||||||
.map(MetaProposalList.flatten)
|
.map(MetaProposalList.flatten)
|
||||||
|
|
||||||
def searchFuzzy[F[_]: Sync](ctx: Context[F, Args], store: Store[F]): Finder[F] =
|
def searchFuzzy[F[_]: Sync](ctx: Context[F, Args], store: Store[F]): Finder[F] =
|
||||||
labels =>
|
labels =>
|
||||||
labels.toList
|
labels.toList
|
||||||
.traverse(nl => search(nl, exact = false, ctx, store))
|
.traverse(nl => search(nl, false, ctx, store))
|
||||||
.map(MetaProposalList.flatten)
|
.map(MetaProposalList.flatten)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,10 +131,10 @@ object ReProcessItem {
|
|||||||
data.item.source, // source-id
|
data.item.source, // source-id
|
||||||
None, // folder
|
None, // folder
|
||||||
Seq.empty,
|
Seq.empty,
|
||||||
skipDuplicate = false,
|
false,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
reprocess = true,
|
true,
|
||||||
None, // attachOnly (not used when reprocessing attachments)
|
None, // attachOnly (not used when reprocessing attachments)
|
||||||
None // cannot retain customData from an already existing item
|
None // cannot retain customData from an already existing item
|
||||||
),
|
),
|
||||||
|
@ -75,7 +75,7 @@ object TextAnalysis {
|
|||||||
analyser: TextAnalyser[F],
|
analyser: TextAnalyser[F],
|
||||||
nerFile: RegexNerFile[F]
|
nerFile: RegexNerFile[F]
|
||||||
)(rm: RAttachmentMeta): F[(RAttachmentMeta, AttachmentDates)] = {
|
)(rm: RAttachmentMeta): F[(RAttachmentMeta, AttachmentDates)] = {
|
||||||
val settings = NlpSettings(ctx.args.meta.language, highRecall = false, None)
|
val settings = NlpSettings(ctx.args.meta.language, false, None)
|
||||||
for {
|
for {
|
||||||
customNer <- nerFile.makeFile(ctx.args.meta.collective)
|
customNer <- nerFile.makeFile(ctx.args.meta.collective)
|
||||||
sett = settings.copy(regexNer = customNer)
|
sett = settings.copy(regexNer = customNer)
|
||||||
|
@ -28,7 +28,7 @@ object JoexRoutes {
|
|||||||
for {
|
for {
|
||||||
_ <- app.scheduler.notifyChange
|
_ <- app.scheduler.notifyChange
|
||||||
_ <- app.periodicScheduler.notifyChange
|
_ <- app.periodicScheduler.notifyChange
|
||||||
resp <- Ok(BasicResult(success = true, "Schedulers notified."))
|
resp <- Ok(BasicResult(true, "Schedulers notified."))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case GET -> Root / "running" =>
|
case GET -> Root / "running" =>
|
||||||
@ -43,7 +43,7 @@ object JoexRoutes {
|
|||||||
_ <- Async[F].start(
|
_ <- Async[F].start(
|
||||||
Temporal[F].sleep(Duration.seconds(1).toScala) *> app.initShutdown
|
Temporal[F].sleep(Duration.seconds(1).toScala) *> app.initShutdown
|
||||||
)
|
)
|
||||||
resp <- Ok(BasicResult(success = true, "Shutdown initiated."))
|
resp <- Ok(BasicResult(true, "Shutdown initiated."))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case GET -> Root / "job" / Ident(id) =>
|
case GET -> Root / "job" / Ident(id) =>
|
||||||
@ -54,9 +54,7 @@ object JoexRoutes {
|
|||||||
job <- optJob
|
job <- optJob
|
||||||
log <- optLog
|
log <- optLog
|
||||||
} yield mkJobLog(job, log)
|
} yield mkJobLog(job, log)
|
||||||
resp <- jAndL
|
resp <- jAndL.map(Ok(_)).getOrElse(NotFound(BasicResult(false, "Not found")))
|
||||||
.map(Ok(_))
|
|
||||||
.getOrElse(NotFound(BasicResult(success = false, "Not found")))
|
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case POST -> Root / "job" / Ident(id) / "cancel" =>
|
case POST -> Root / "job" / Ident(id) / "cancel" =>
|
||||||
|
@ -323,7 +323,7 @@ object ScanMailboxTask {
|
|||||||
s"mailbox-${ctx.args.account.login.id}",
|
s"mailbox-${ctx.args.account.login.id}",
|
||||||
args.itemFolder,
|
args.itemFolder,
|
||||||
Seq.empty,
|
Seq.empty,
|
||||||
skipDuplicates = true,
|
true,
|
||||||
args.fileFilter.getOrElse(Glob.all),
|
args.fileFilter.getOrElse(Glob.all),
|
||||||
args.tags.getOrElse(Nil),
|
args.tags.getOrElse(Nil),
|
||||||
args.language,
|
args.language,
|
||||||
|
@ -18,8 +18,6 @@ servers:
|
|||||||
- url: /api/v1
|
- url: /api/v1
|
||||||
description: Current host
|
description: Current host
|
||||||
|
|
||||||
security: []
|
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/api/info/version:
|
/api/info/version:
|
||||||
get:
|
get:
|
||||||
|
@ -164,7 +164,7 @@ object Event {
|
|||||||
for {
|
for {
|
||||||
id1 <- Ident.randomId[F]
|
id1 <- Ident.randomId[F]
|
||||||
id2 <- Ident.randomId[F]
|
id2 <- Ident.randomId[F]
|
||||||
} yield ItemSelection(account, Nel.of(id1, id2), more = true, baseUrl, None)
|
} yield ItemSelection(account, Nel.of(id1, id2), true, baseUrl, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Event when a new job is added to the queue */
|
/** Event when a new job is added to the queue */
|
||||||
|
@ -20,9 +20,9 @@ import org.typelevel.ci._
|
|||||||
|
|
||||||
trait Fixtures extends HttpClientOps { self: CatsEffectSuite =>
|
trait Fixtures extends HttpClientOps { self: CatsEffectSuite =>
|
||||||
|
|
||||||
val pubsubEnv = ResourceFunFixture(Fixtures.envResource("node-1"))
|
val pubsubEnv = ResourceFixture(Fixtures.envResource("node-1"))
|
||||||
|
|
||||||
val pubsubT = ResourceFunFixture {
|
val pubsubT = ResourceFixture {
|
||||||
Fixtures
|
Fixtures
|
||||||
.envResource("node-1")
|
.envResource("node-1")
|
||||||
.flatMap(_.pubSub)
|
.flatMap(_.pubSub)
|
||||||
|
@ -87,10 +87,10 @@ object ParseFailure {
|
|||||||
SimpleMessage(offset, message)
|
SimpleMessage(offset, message)
|
||||||
|
|
||||||
case InRange(offset, lower, upper) =>
|
case InRange(offset, lower, upper) =>
|
||||||
if (lower == upper) ExpectMessage(offset, List(lower.toString), exhaustive = true)
|
if (lower == upper) ExpectMessage(offset, List(lower.toString), true)
|
||||||
else {
|
else {
|
||||||
val expect = s"$lower-$upper"
|
val expect = s"$lower-$upper"
|
||||||
ExpectMessage(offset, List(expect), exhaustive = true)
|
ExpectMessage(offset, List(expect), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
case Length(offset, expected, actual) =>
|
case Length(offset, expected, actual) =>
|
||||||
@ -110,10 +110,6 @@ object ParseFailure {
|
|||||||
ExpectMessage(offset, options.take(7), options.size < 8)
|
ExpectMessage(offset, options.take(7), options.size < 8)
|
||||||
|
|
||||||
case WithContext(ctx, expect) =>
|
case WithContext(ctx, expect) =>
|
||||||
ExpectMessage(
|
ExpectMessage(expect.offset, s"Failed to parse near: $ctx" :: Nil, true)
|
||||||
expect.offset,
|
|
||||||
s"Failed to parse near: $ctx" :: Nil,
|
|
||||||
exhaustive = true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,6 @@ servers:
|
|||||||
- url: /api/v1
|
- url: /api/v1
|
||||||
description: Current host
|
description: Current host
|
||||||
|
|
||||||
security: []
|
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/api/info/version:
|
/api/info/version:
|
||||||
get:
|
get:
|
||||||
|
@ -329,7 +329,7 @@ trait Conversions {
|
|||||||
sourceName,
|
sourceName,
|
||||||
None,
|
None,
|
||||||
validFileTypes,
|
validFileTypes,
|
||||||
skipDuplicates = false,
|
false,
|
||||||
Glob.all,
|
Glob.all,
|
||||||
Nil,
|
Nil,
|
||||||
None,
|
None,
|
||||||
@ -641,86 +641,82 @@ trait Conversions {
|
|||||||
def basicResult(r: SetValueResult): BasicResult =
|
def basicResult(r: SetValueResult): BasicResult =
|
||||||
r match {
|
r match {
|
||||||
case SetValueResult.FieldNotFound =>
|
case SetValueResult.FieldNotFound =>
|
||||||
BasicResult(success = false, "The given field is unknown")
|
BasicResult(false, "The given field is unknown")
|
||||||
case SetValueResult.ItemNotFound =>
|
case SetValueResult.ItemNotFound =>
|
||||||
BasicResult(success = false, "The given item is unknown")
|
BasicResult(false, "The given item is unknown")
|
||||||
case SetValueResult.ValueInvalid(msg) =>
|
case SetValueResult.ValueInvalid(msg) =>
|
||||||
BasicResult(success = false, s"The value is invalid: $msg")
|
BasicResult(false, s"The value is invalid: $msg")
|
||||||
case SetValueResult.Success =>
|
case SetValueResult.Success =>
|
||||||
BasicResult(success = true, "Custom field value set successfully.")
|
BasicResult(true, "Custom field value set successfully.")
|
||||||
}
|
}
|
||||||
|
|
||||||
def basicResult(cr: JobCancelResult): BasicResult =
|
def basicResult(cr: JobCancelResult): BasicResult =
|
||||||
cr match {
|
cr match {
|
||||||
case JobCancelResult.JobNotFound => BasicResult(success = false, "Job not found")
|
case JobCancelResult.JobNotFound => BasicResult(false, "Job not found")
|
||||||
case JobCancelResult.CancelRequested =>
|
case JobCancelResult.CancelRequested =>
|
||||||
BasicResult(success = true, "Cancel was requested at the job executor")
|
BasicResult(true, "Cancel was requested at the job executor")
|
||||||
case JobCancelResult.Removed =>
|
case JobCancelResult.Removed =>
|
||||||
BasicResult(success = true, "The job has been removed from the queue.")
|
BasicResult(true, "The job has been removed from the queue.")
|
||||||
}
|
}
|
||||||
|
|
||||||
def idResult(ar: AddResult, id: Ident, successMsg: String): IdResult =
|
def idResult(ar: AddResult, id: Ident, successMsg: String): IdResult =
|
||||||
ar match {
|
ar match {
|
||||||
case AddResult.Success => IdResult(success = true, successMsg, id)
|
case AddResult.Success => IdResult(true, successMsg, id)
|
||||||
case AddResult.EntityExists(msg) => IdResult(success = false, msg, Ident.unsafe(""))
|
case AddResult.EntityExists(msg) => IdResult(false, msg, Ident.unsafe(""))
|
||||||
case AddResult.Failure(ex) =>
|
case AddResult.Failure(ex) =>
|
||||||
IdResult(success = false, s"Internal error: ${ex.getMessage}", Ident.unsafe(""))
|
IdResult(false, s"Internal error: ${ex.getMessage}", Ident.unsafe(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
def basicResult(ar: AddResult, successMsg: String): BasicResult =
|
def basicResult(ar: AddResult, successMsg: String): BasicResult =
|
||||||
ar match {
|
ar match {
|
||||||
case AddResult.Success => BasicResult(success = true, successMsg)
|
case AddResult.Success => BasicResult(true, successMsg)
|
||||||
case AddResult.EntityExists(msg) => BasicResult(success = false, msg)
|
case AddResult.EntityExists(msg) => BasicResult(false, msg)
|
||||||
case AddResult.Failure(ex) =>
|
case AddResult.Failure(ex) =>
|
||||||
BasicResult(success = false, s"Internal error: ${ex.getMessage}")
|
BasicResult(false, s"Internal error: ${ex.getMessage}")
|
||||||
}
|
}
|
||||||
|
|
||||||
def basicResult(ar: UpdateResult, successMsg: String): BasicResult =
|
def basicResult(ar: UpdateResult, successMsg: String): BasicResult =
|
||||||
ar match {
|
ar match {
|
||||||
case UpdateResult.Success => BasicResult(success = true, successMsg)
|
case UpdateResult.Success => BasicResult(true, successMsg)
|
||||||
case UpdateResult.NotFound => BasicResult(success = false, "Not found")
|
case UpdateResult.NotFound => BasicResult(false, "Not found")
|
||||||
case UpdateResult.Failure(ex) =>
|
case UpdateResult.Failure(ex) =>
|
||||||
BasicResult(success = false, s"Error: ${ex.getMessage}")
|
BasicResult(false, s"Error: ${ex.getMessage}")
|
||||||
}
|
}
|
||||||
|
|
||||||
def basicResult(ur: OUpload.UploadResult): BasicResult =
|
def basicResult(ur: OUpload.UploadResult): BasicResult =
|
||||||
ur match {
|
ur match {
|
||||||
case UploadResult.Success => BasicResult(success = true, "Files submitted.")
|
case UploadResult.Success => BasicResult(true, "Files submitted.")
|
||||||
case UploadResult.NoFiles =>
|
case UploadResult.NoFiles => BasicResult(false, "There were no files to submit.")
|
||||||
BasicResult(success = false, "There were no files to submit.")
|
case UploadResult.NoSource => BasicResult(false, "The source id is not valid.")
|
||||||
case UploadResult.NoSource =>
|
case UploadResult.NoItem => BasicResult(false, "The item could not be found.")
|
||||||
BasicResult(success = false, "The source id is not valid.")
|
|
||||||
case UploadResult.NoItem =>
|
|
||||||
BasicResult(success = false, "The item could not be found.")
|
|
||||||
case UploadResult.NoCollective =>
|
case UploadResult.NoCollective =>
|
||||||
BasicResult(success = false, "The collective could not be found.")
|
BasicResult(false, "The collective could not be found.")
|
||||||
case UploadResult.StoreFailure(_) =>
|
case UploadResult.StoreFailure(_) =>
|
||||||
BasicResult(
|
BasicResult(
|
||||||
success = false,
|
false,
|
||||||
"There were errors storing a file! See the server logs for details."
|
"There were errors storing a file! See the server logs for details."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def basicResult(cr: PassChangeResult): BasicResult =
|
def basicResult(cr: PassChangeResult): BasicResult =
|
||||||
cr match {
|
cr match {
|
||||||
case PassChangeResult.Success => BasicResult(success = true, "Password changed.")
|
case PassChangeResult.Success => BasicResult(true, "Password changed.")
|
||||||
case PassChangeResult.UpdateFailed =>
|
case PassChangeResult.UpdateFailed =>
|
||||||
BasicResult(success = false, "The database update failed.")
|
BasicResult(false, "The database update failed.")
|
||||||
case PassChangeResult.PasswordMismatch =>
|
case PassChangeResult.PasswordMismatch =>
|
||||||
BasicResult(success = false, "The current password is incorrect.")
|
BasicResult(false, "The current password is incorrect.")
|
||||||
case PassChangeResult.UserNotFound =>
|
case PassChangeResult.UserNotFound => BasicResult(false, "User not found.")
|
||||||
BasicResult(success = false, "User not found.")
|
|
||||||
case PassChangeResult.InvalidSource(source) =>
|
case PassChangeResult.InvalidSource(source) =>
|
||||||
BasicResult(
|
BasicResult(
|
||||||
success = false,
|
false,
|
||||||
s"User has invalid soure: $source. Passwords are managed elsewhere."
|
s"User has invalid soure: $source. Passwords are managed elsewhere."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def basicResult(e: Either[Throwable, _], successMsg: String): BasicResult =
|
def basicResult(e: Either[Throwable, _], successMsg: String): BasicResult =
|
||||||
e match {
|
e match {
|
||||||
case Right(_) => BasicResult(success = true, successMsg)
|
case Right(_) => BasicResult(true, successMsg)
|
||||||
case Left(ex) => BasicResult(success = false, ex.getMessage)
|
case Left(ex) => BasicResult(false, ex.getMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MIME Type
|
// MIME Type
|
||||||
|
@ -38,7 +38,7 @@ object BinaryUtil {
|
|||||||
if (matches) withResponseHeaders(dsl, NotModified())(data)
|
if (matches) withResponseHeaders(dsl, NotModified())(data)
|
||||||
else makeByteResp(dsl)(data)
|
else makeByteResp(dsl)(data)
|
||||||
}
|
}
|
||||||
.getOrElse(NotFound(BasicResult(success = false, "Not found")))
|
.getOrElse(NotFound(BasicResult(false, "Not found")))
|
||||||
}
|
}
|
||||||
|
|
||||||
def respondHead[F[_]: Async](dsl: Http4sDsl[F])(
|
def respondHead[F[_]: Async](dsl: Http4sDsl[F])(
|
||||||
@ -48,7 +48,7 @@ object BinaryUtil {
|
|||||||
|
|
||||||
fileData
|
fileData
|
||||||
.map(data => withResponseHeaders(dsl, Ok())(data))
|
.map(data => withResponseHeaders(dsl, Ok())(data))
|
||||||
.getOrElse(NotFound(BasicResult(success = false, "Not found")))
|
.getOrElse(NotFound(BasicResult(false, "Not found")))
|
||||||
}
|
}
|
||||||
|
|
||||||
def respondPreview[F[_]: Async](dsl: Http4sDsl[F], req: Request[F])(
|
def respondPreview[F[_]: Async](dsl: Http4sDsl[F], req: Request[F])(
|
||||||
@ -56,7 +56,7 @@ object BinaryUtil {
|
|||||||
): F[Response[F]] = {
|
): F[Response[F]] = {
|
||||||
import dsl._
|
import dsl._
|
||||||
def notFound =
|
def notFound =
|
||||||
NotFound(BasicResult(success = false, "Not found"))
|
NotFound(BasicResult(false, "Not found"))
|
||||||
|
|
||||||
QP.WithFallback.unapply(req.multiParams) match {
|
QP.WithFallback.unapply(req.multiParams) match {
|
||||||
case Some(bool) =>
|
case Some(bool) =>
|
||||||
@ -75,7 +75,7 @@ object BinaryUtil {
|
|||||||
)
|
)
|
||||||
|
|
||||||
case None =>
|
case None =>
|
||||||
BadRequest(BasicResult(success = false, "Invalid query parameter 'withFallback'"))
|
BadRequest(BasicResult(false, "Invalid query parameter 'withFallback'"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ object BinaryUtil {
|
|||||||
import dsl._
|
import dsl._
|
||||||
fileData
|
fileData
|
||||||
.map(data => withResponseHeaders(dsl, Ok())(data))
|
.map(data => withResponseHeaders(dsl, Ok())(data))
|
||||||
.getOrElse(NotFound(BasicResult(success = false, "Not found")))
|
.getOrElse(NotFound(BasicResult(false, "Not found")))
|
||||||
}
|
}
|
||||||
|
|
||||||
def withResponseHeaders[F[_]: Sync](dsl: Http4sDsl[F], resp: F[Response[F]])(
|
def withResponseHeaders[F[_]: Sync](dsl: Http4sDsl[F], resp: F[Response[F]])(
|
||||||
|
@ -33,10 +33,10 @@ object ThrowableResponseMapper {
|
|||||||
def toResponse(ex: Throwable): F[Response[F]] =
|
def toResponse(ex: Throwable): F[Response[F]] =
|
||||||
ex match {
|
ex match {
|
||||||
case _: IllegalArgumentException =>
|
case _: IllegalArgumentException =>
|
||||||
BadRequest(BasicResult(success = false, ex.getMessage))
|
BadRequest(BasicResult(false, ex.getMessage))
|
||||||
|
|
||||||
case _ =>
|
case _ =>
|
||||||
InternalServerError(BasicResult(success = false, ex.getMessage))
|
InternalServerError(BasicResult(false, ex.getMessage))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ object AddonArchiveRoutes extends AddonValidationSupport {
|
|||||||
case req @ POST -> Root :? Sync(sync) =>
|
case req @ POST -> Root :? Sync(sync) =>
|
||||||
def create(r: Option[RAddonArchive]) =
|
def create(r: Option[RAddonArchive]) =
|
||||||
IdResult(
|
IdResult(
|
||||||
success = true,
|
true,
|
||||||
r.fold("Addon submitted for installation")(r =>
|
r.fold("Addon submitted for installation")(r =>
|
||||||
s"Addon installed: ${r.id.id}"
|
s"Addon installed: ${r.id.id}"
|
||||||
),
|
),
|
||||||
@ -77,7 +77,7 @@ object AddonArchiveRoutes extends AddonValidationSupport {
|
|||||||
case PUT -> Root / Ident(id) :? Sync(sync) =>
|
case PUT -> Root / Ident(id) :? Sync(sync) =>
|
||||||
def create(r: Option[AddonMeta]) =
|
def create(r: Option[AddonMeta]) =
|
||||||
BasicResult(
|
BasicResult(
|
||||||
success = true,
|
true,
|
||||||
r.fold("Addon updated in background")(m =>
|
r.fold("Addon updated in background")(m =>
|
||||||
s"Addon updated: ${m.nameAndVersion}"
|
s"Addon updated: ${m.nameAndVersion}"
|
||||||
)
|
)
|
||||||
@ -99,8 +99,8 @@ object AddonArchiveRoutes extends AddonValidationSupport {
|
|||||||
for {
|
for {
|
||||||
flag <- backend.addons.deleteAddon(token.account.collectiveId, id)
|
flag <- backend.addons.deleteAddon(token.account.collectiveId, id)
|
||||||
resp <-
|
resp <-
|
||||||
if (flag) Ok(BasicResult(success = true, "Addon deleted"))
|
if (flag) Ok(BasicResult(true, "Addon deleted"))
|
||||||
else NotFound(BasicResult(success = false, "Addon not found"))
|
else NotFound(BasicResult(false, "Addon not found"))
|
||||||
} yield resp
|
} yield resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,11 +112,11 @@ object AddonArchiveRoutes extends AddonValidationSupport {
|
|||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
def failWith(msg: String): F[Response[F]] =
|
def failWith(msg: String): F[Response[F]] =
|
||||||
Ok(IdResult(success = false, msg, Ident.unsafe("")))
|
Ok(IdResult(false, msg, Ident.unsafe("")))
|
||||||
|
|
||||||
e match {
|
e match {
|
||||||
case AddonValidationError.AddonNotFound =>
|
case AddonValidationError.AddonNotFound =>
|
||||||
NotFound(BasicResult(success = false, "Addon not found."))
|
NotFound(BasicResult(false, "Addon not found."))
|
||||||
|
|
||||||
case _ =>
|
case _ =>
|
||||||
failWith(validationErrorToMessage(e))
|
failWith(validationErrorToMessage(e))
|
||||||
|
@ -35,5 +35,5 @@ object AddonRoutes {
|
|||||||
"run" -> AddonRunRoutes(backend, token)
|
"run" -> AddonRunRoutes(backend, token)
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Responses.notFoundRoute(BasicResult(success = false, "Addons disabled"))
|
Responses.notFoundRoute(BasicResult(false, "Addons disabled"))
|
||||||
}
|
}
|
||||||
|
@ -43,8 +43,8 @@ object AddonRunConfigRoutes {
|
|||||||
.map(_.leftMap(_.message))
|
.map(_.leftMap(_.message))
|
||||||
)
|
)
|
||||||
resp <- res.fold(
|
resp <- res.fold(
|
||||||
msg => Ok(BasicResult(success = false, msg)),
|
msg => Ok(BasicResult(false, msg)),
|
||||||
id => Ok(IdResult(success = true, s"Addon run config added", id))
|
id => Ok(IdResult(true, s"Addon run config added", id))
|
||||||
)
|
)
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
@ -58,8 +58,8 @@ object AddonRunConfigRoutes {
|
|||||||
.map(_.leftMap(_.message))
|
.map(_.leftMap(_.message))
|
||||||
)
|
)
|
||||||
resp <- res.fold(
|
resp <- res.fold(
|
||||||
msg => Ok(BasicResult(success = false, msg)),
|
msg => Ok(BasicResult(false, msg)),
|
||||||
id => Ok(IdResult(success = true, s"Addon run config updated", id))
|
id => Ok(IdResult(true, s"Addon run config updated", id))
|
||||||
)
|
)
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
@ -67,8 +67,8 @@ object AddonRunConfigRoutes {
|
|||||||
for {
|
for {
|
||||||
flag <- backend.addons.deleteAddonRunConfig(token.account.collectiveId, id)
|
flag <- backend.addons.deleteAddonRunConfig(token.account.collectiveId, id)
|
||||||
resp <-
|
resp <-
|
||||||
if (flag) Ok(BasicResult(success = true, "Addon task deleted"))
|
if (flag) Ok(BasicResult(true, "Addon task deleted"))
|
||||||
else NotFound(BasicResult(success = false, "Addon task not found"))
|
else NotFound(BasicResult(false, "Addon task not found"))
|
||||||
} yield resp
|
} yield resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ object AddonRunRoutes {
|
|||||||
input.addonRunConfigIds.toSet,
|
input.addonRunConfigIds.toSet,
|
||||||
UserTaskScope(token.account)
|
UserTaskScope(token.account)
|
||||||
)
|
)
|
||||||
resp <- Ok(BasicResult(success = true, "Job for running addons submitted."))
|
resp <- Ok(BasicResult(true, "Job for running addons submitted."))
|
||||||
} yield resp
|
} yield resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ object AttachmentRoutes {
|
|||||||
resp <-
|
resp <-
|
||||||
fileData
|
fileData
|
||||||
.map(data => withResponseHeaders(Ok())(data))
|
.map(data => withResponseHeaders(Ok())(data))
|
||||||
.getOrElse(NotFound(BasicResult(success = false, "Not found")))
|
.getOrElse(NotFound(BasicResult(false, "Not found")))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req @ GET -> Root / Ident(id) / "original" =>
|
case req @ GET -> Root / Ident(id) / "original" =>
|
||||||
@ -83,7 +83,7 @@ object AttachmentRoutes {
|
|||||||
if (matches) withResponseHeaders(NotModified())(data)
|
if (matches) withResponseHeaders(NotModified())(data)
|
||||||
else makeByteResp(data)
|
else makeByteResp(data)
|
||||||
}
|
}
|
||||||
.getOrElse(NotFound(BasicResult(success = false, "Not found")))
|
.getOrElse(NotFound(BasicResult(false, "Not found")))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case HEAD -> Root / Ident(id) / "archive" =>
|
case HEAD -> Root / Ident(id) / "archive" =>
|
||||||
@ -93,7 +93,7 @@ object AttachmentRoutes {
|
|||||||
resp <-
|
resp <-
|
||||||
fileData
|
fileData
|
||||||
.map(data => withResponseHeaders(Ok())(data))
|
.map(data => withResponseHeaders(Ok())(data))
|
||||||
.getOrElse(NotFound(BasicResult(success = false, "Not found")))
|
.getOrElse(NotFound(BasicResult(false, "Not found")))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req @ GET -> Root / Ident(id) / "archive" =>
|
case req @ GET -> Root / Ident(id) / "archive" =>
|
||||||
@ -108,7 +108,7 @@ object AttachmentRoutes {
|
|||||||
if (matches) withResponseHeaders(NotModified())(data)
|
if (matches) withResponseHeaders(NotModified())(data)
|
||||||
else makeByteResp(data)
|
else makeByteResp(data)
|
||||||
}
|
}
|
||||||
.getOrElse(NotFound(BasicResult(success = false, "Not found")))
|
.getOrElse(NotFound(BasicResult(false, "Not found")))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req @ GET -> Root / Ident(id) / "preview" =>
|
case req @ GET -> Root / Ident(id) / "preview" =>
|
||||||
@ -148,9 +148,7 @@ object AttachmentRoutes {
|
|||||||
for {
|
for {
|
||||||
rm <- backend.itemSearch.findAttachmentMeta(id, user.account.collectiveId)
|
rm <- backend.itemSearch.findAttachmentMeta(id, user.account.collectiveId)
|
||||||
md = rm.map(Conversions.mkAttachmentMeta)
|
md = rm.map(Conversions.mkAttachmentMeta)
|
||||||
resp <- md
|
resp <- md.map(Ok(_)).getOrElse(NotFound(BasicResult(false, "Not found.")))
|
||||||
.map(Ok(_))
|
|
||||||
.getOrElse(NotFound(BasicResult(success = false, "Not found.")))
|
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req @ POST -> Root / Ident(id) / "name" =>
|
case req @ POST -> Root / Ident(id) / "name" =>
|
||||||
@ -171,11 +169,8 @@ object AttachmentRoutes {
|
|||||||
backend.attachment
|
backend.attachment
|
||||||
.setExtractedText(user.account.collectiveId, itemId, id, newText)
|
.setExtractedText(user.account.collectiveId, itemId, id, newText)
|
||||||
)
|
)
|
||||||
resp <- OptionT.liftF(
|
resp <- OptionT.liftF(Ok(BasicResult(true, "Extracted text updated.")))
|
||||||
Ok(BasicResult(success = true, "Extracted text updated."))
|
} yield resp).getOrElseF(NotFound(BasicResult(false, "Attachment not found")))
|
||||||
)
|
|
||||||
} yield resp)
|
|
||||||
.getOrElseF(NotFound(BasicResult(success = false, "Attachment not found")))
|
|
||||||
|
|
||||||
case DELETE -> Root / Ident(id) / "extracted-text" =>
|
case DELETE -> Root / Ident(id) / "extracted-text" =>
|
||||||
(for {
|
(for {
|
||||||
@ -186,9 +181,7 @@ object AttachmentRoutes {
|
|||||||
backend.attachment
|
backend.attachment
|
||||||
.setExtractedText(user.account.collectiveId, itemId, id, "".pure[F])
|
.setExtractedText(user.account.collectiveId, itemId, id, "".pure[F])
|
||||||
)
|
)
|
||||||
resp <- OptionT.liftF(
|
resp <- OptionT.liftF(Ok(BasicResult(true, "Extracted text cleared.")))
|
||||||
Ok(BasicResult(success = true, "Extracted text cleared."))
|
|
||||||
)
|
|
||||||
} yield resp).getOrElseF(NotFound())
|
} yield resp).getOrElseF(NotFound())
|
||||||
|
|
||||||
case GET -> Root / Ident(id) / "extracted-text" =>
|
case GET -> Root / Ident(id) / "extracted-text" =>
|
||||||
@ -197,15 +190,14 @@ object AttachmentRoutes {
|
|||||||
backend.itemSearch.findAttachmentMeta(id, user.account.collectiveId)
|
backend.itemSearch.findAttachmentMeta(id, user.account.collectiveId)
|
||||||
)
|
)
|
||||||
resp <- OptionT.liftF(Ok(OptionalText(meta.content)))
|
resp <- OptionT.liftF(Ok(OptionalText(meta.content)))
|
||||||
} yield resp)
|
} yield resp).getOrElseF(NotFound(BasicResult(false, "Attachment not found")))
|
||||||
.getOrElseF(NotFound(BasicResult(success = false, "Attachment not found")))
|
|
||||||
|
|
||||||
case DELETE -> Root / Ident(id) =>
|
case DELETE -> Root / Ident(id) =>
|
||||||
for {
|
for {
|
||||||
n <- backend.item.deleteAttachment(id, user.account.collectiveId)
|
n <- backend.item.deleteAttachment(id, user.account.collectiveId)
|
||||||
res =
|
res =
|
||||||
if (n == 0) BasicResult(success = false, "Attachment not found")
|
if (n == 0) BasicResult(false, "Attachment not found")
|
||||||
else BasicResult(success = true, "Attachment deleted.")
|
else BasicResult(true, "Attachment deleted.")
|
||||||
resp <- Ok(res)
|
resp <- Ok(res)
|
||||||
} yield resp
|
} yield resp
|
||||||
}
|
}
|
||||||
|
@ -40,9 +40,9 @@ object CalEventCheckRoutes {
|
|||||||
val next = ev
|
val next = ev
|
||||||
.nextElapses(now.toUtcDateTime, 2)
|
.nextElapses(now.toUtcDateTime, 2)
|
||||||
.map(Timestamp.atUtc)
|
.map(Timestamp.atUtc)
|
||||||
CalEventCheckResult(success = true, "Valid.", ev.some, next)
|
CalEventCheckResult(true, "Valid.", ev.some, next)
|
||||||
case Left(err) =>
|
case Left(err) =>
|
||||||
CalEventCheckResult(success = false, err, None, Nil)
|
CalEventCheckResult(false, err, None, Nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ object ClientSettingsRoutes {
|
|||||||
for {
|
for {
|
||||||
data <- req.as[Json]
|
data <- req.as[Json]
|
||||||
_ <- backend.clientSettings.saveUser(clientId, user.account.userId, data)
|
_ <- backend.clientSettings.saveUser(clientId, user.account.userId, data)
|
||||||
res <- Ok(BasicResult(success = true, "Settings stored"))
|
res <- Ok(BasicResult(true, "Settings stored"))
|
||||||
} yield res
|
} yield res
|
||||||
|
|
||||||
case GET -> Root / "user" / Ident(clientId) =>
|
case GET -> Root / "user" / Ident(clientId) =>
|
||||||
@ -97,7 +97,7 @@ object ClientSettingsRoutes {
|
|||||||
user.account.collectiveId,
|
user.account.collectiveId,
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
res <- Ok(BasicResult(success = true, "Settings stored"))
|
res <- Ok(BasicResult(true, "Settings stored"))
|
||||||
} yield res
|
} yield res
|
||||||
|
|
||||||
case GET -> Root / "collective" / Ident(clientId) =>
|
case GET -> Root / "collective" / Ident(clientId) =>
|
||||||
|
@ -118,7 +118,7 @@ object CollectiveRoutes {
|
|||||||
case POST -> Root / "classifier" / "startonce" =>
|
case POST -> Root / "classifier" / "startonce" =>
|
||||||
for {
|
for {
|
||||||
_ <- backend.collective.startLearnClassifier(user.account.collectiveId)
|
_ <- backend.collective.startLearnClassifier(user.account.collectiveId)
|
||||||
resp <- Ok(BasicResult(success = true, "Task submitted"))
|
resp <- Ok(BasicResult(true, "Task submitted"))
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
case req @ POST -> Root / "emptytrash" / "startonce" =>
|
case req @ POST -> Root / "emptytrash" / "startonce" =>
|
||||||
@ -127,7 +127,7 @@ object CollectiveRoutes {
|
|||||||
_ <- backend.collective.startEmptyTrash(
|
_ <- backend.collective.startEmptyTrash(
|
||||||
EmptyTrashArgs(user.account.collectiveId, data.minAge)
|
EmptyTrashArgs(user.account.collectiveId, data.minAge)
|
||||||
)
|
)
|
||||||
resp <- Ok(BasicResult(success = true, "Task submitted"))
|
resp <- Ok(BasicResult(true, "Task submitted"))
|
||||||
} yield resp
|
} yield resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ object CustomFieldRoutes {
|
|||||||
(for {
|
(for {
|
||||||
field <- OptionT(backend.customFields.findById(user.account.collectiveId, id))
|
field <- OptionT(backend.customFields.findById(user.account.collectiveId, id))
|
||||||
res <- OptionT.liftF(Ok(convertField(field)))
|
res <- OptionT.liftF(Ok(convertField(field)))
|
||||||
} yield res).getOrElseF(NotFound(BasicResult(success = false, "Not found")))
|
} yield res).getOrElseF(NotFound(BasicResult(false, "Not found")))
|
||||||
|
|
||||||
case req @ PUT -> Root / Ident(id) =>
|
case req @ PUT -> Root / Ident(id) =>
|
||||||
for {
|
for {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user