mirror of
https://github.com/simon987/sist2.git
synced 2025-12-12 15:08:53 +00:00
Compare commits
114 Commits
2.11.7
...
process-po
| Author | SHA1 | Date | |
|---|---|---|---|
| 903feb4889 | |||
| 01490d1cbf | |||
| 6182338f29 | |||
| 300c70883d | |||
| fc36f33d52 | |||
| ca973d63a4 | |||
| f8abffba81 | |||
| 8c662bb8f8 | |||
| 9c40dddd41 | |||
| d259b95017 | |||
| 707bac86b3 | |||
| 8b9b067c06 | |||
| b17f3ff924 | |||
| e44fbf741c | |||
| fa14efbeb6 | |||
| c510162dd9 | |||
| f5c664507f | |||
| 2805fd509f | |||
| 20adcce4a9 | |||
| 1e6e24111b | |||
|
|
5a76b855c9 | ||
| 6f759642fc | |||
| 587c9a2c90 | |||
| 821a571ecf | |||
|
|
9020246a01 | ||
|
|
200c000c5a | ||
|
|
a43f930d00 | ||
| abe120197a | |||
| 9e0d7bf992 | |||
|
|
959d4b4386 | ||
|
|
742a50be03 | ||
| 87ecc5ef6d | |||
| 2e3d648796 | |||
| 9972e21fcc | |||
| c625c03552 | |||
| 5863b9cd6e | |||
| 86ca9f1ecb | |||
| b9f008603a | |||
| a074d8cf10 | |||
| 795b6e2e2e | |||
|
|
59fd0f935c | ||
| 3bd1f593b0 | |||
| 8e93d50d9e | |||
|
|
e093a8a05c | ||
| 588b4df164 | |||
|
|
8b1740958b | ||
| fe25ad5459 | |||
|
|
83d9e0fb4b | ||
| 6c8e6ac0b3 | |||
|
|
2de2f87f16 | ||
| 61eb311577 | |||
| f3a4598cfd | |||
| e135efaa4b | |||
| 5d488acd77 | |||
| 79b78a92f8 | |||
| ad18b4d7fd | |||
| d221de5a94 | |||
| 13e7ea188b | |||
| cb4bd9f05a | |||
| c0b8a9c467 | |||
| c18557e360 | |||
| 4ec54c9a32 | |||
|
|
1baf3861f7 | ||
|
|
25fb912f69 | ||
|
|
e7f27cfd13 | ||
|
|
6e38653f2f | ||
| 38fba363f2 | |||
| c7b3d11a6d | |||
| 4e1109c528 | |||
| f87de89275 | |||
| 1205981a11 | |||
| 09613eaaf9 | |||
| a74726be55 | |||
|
|
cb228052d2 | ||
| fe56da95d5 | |||
| 9f2ad58f78 | |||
| 84d9bf4323 | |||
| 90aa90f3f3 | |||
| 3fad07360c | |||
|
|
00c3a640d0 | ||
| 730e495bde | |||
| 54df1dfcf7 | |||
| a75675ecea | |||
| 901035da15 | |||
| ceb7265639 | |||
| 036ed9ea1e | |||
| 779303a2f7 | |||
| 23aee14c07 | |||
| 50b9201be3 | |||
|
|
14cfb15661 | ||
| 125c85d9bb | |||
| 474eb95aff | |||
| acf7453057 | |||
| 9a949d2694 | |||
| dbdc75dcb8 | |||
| c575fca91d | |||
| 0bf4244683 | |||
| eea5ce75f3 | |||
| 9b81856353 | |||
| a10d6952ba | |||
| 2b639bd4ac | |||
| e9f92330fd | |||
| cb37a6e6c1 | |||
| b82c26f0fb | |||
| 16a4fb4874 | |||
| cdc4c0ad3d | |||
| d034851ecb | |||
| ea7dfe7c84 | |||
| 8bfd010f4b | |||
| 499eb2b2e4 | |||
| 25ab883063 | |||
|
|
6ab606203f | ||
| 6ec98046fa | |||
|
|
4fac81ca6a |
@@ -8,13 +8,13 @@ Testing/
|
||||
**/cmake_install.cmake
|
||||
**/CMakeCache.txt
|
||||
**/CMakeFiles/
|
||||
.cmake
|
||||
LICENSE
|
||||
Makefile
|
||||
**/*.md
|
||||
**/*.cbp
|
||||
VERSION
|
||||
**/node_modules/
|
||||
.git/
|
||||
sist2-*-linux-debug
|
||||
sist2-*-linux
|
||||
sist2_debug
|
||||
@@ -28,4 +28,13 @@ sist2
|
||||
**/ext_libwpd
|
||||
**/core
|
||||
*.a
|
||||
tmp_scan/
|
||||
tmp_scan/
|
||||
Dockerfile
|
||||
Dockerfile.arm64
|
||||
docker-compose.yml
|
||||
state.db
|
||||
*-journal
|
||||
build/
|
||||
__pycache__/
|
||||
sist2-vue/dist
|
||||
sist2-admin/frontend/dist
|
||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -10,6 +10,9 @@ Makefile
|
||||
LOG
|
||||
sist2*
|
||||
!sist2-vue/
|
||||
!sist2-admin
|
||||
!sist2_admin
|
||||
!sist2.py
|
||||
*.sist2/
|
||||
bundle*.css
|
||||
bundle.js
|
||||
@@ -25,4 +28,19 @@ test_i
|
||||
test_i_inc
|
||||
node_modules/
|
||||
.cmake/
|
||||
i_inc/
|
||||
i_inc/
|
||||
state.db
|
||||
*.pyc
|
||||
!sist2-admin/frontend/dist
|
||||
*.js.map
|
||||
sist2-vue/dist
|
||||
sist2-admin/frontend/dist
|
||||
.ninja_deps
|
||||
.ninja_log
|
||||
build.ninja
|
||||
src/web/static_generated.c
|
||||
src/magic_generated.c
|
||||
src/index/static_generated.c
|
||||
*.sist2
|
||||
*-shm
|
||||
*-journal
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -7,3 +7,9 @@
|
||||
[submodule "third-party/libscan/third-party/antiword"]
|
||||
path = third-party/libscan/third-party/antiword
|
||||
url = https://github.com/simon987/antiword
|
||||
[submodule "third-party/libscan/third-party/libmobi"]
|
||||
path = third-party/libscan/third-party/libmobi
|
||||
url = https://github.com/bfabiszewski/libmobi
|
||||
[submodule "third-party/libscan/libscan-test-files"]
|
||||
path = third-party/libscan/libscan-test-files
|
||||
url = https://github.com/simon987/libscan-test-files
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.7)
|
||||
|
||||
project(sist2)
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
|
||||
project(sist2 C)
|
||||
|
||||
option(SIST_DEBUG "Build a debug executable" on)
|
||||
option(SIST_FAKE_STORE "Disable IO operations of LMDB stores for debugging purposes" 0)
|
||||
option(SIST_FAST "Enable more optimisation flags" off)
|
||||
|
||||
add_compile_definitions(
|
||||
"SIST_PLATFORM=${SIST_PLATFORM}"
|
||||
@@ -21,39 +21,45 @@ set(ARGPARSE_SHARED off)
|
||||
add_subdirectory(third-party/argparse)
|
||||
|
||||
add_executable(sist2
|
||||
# argparse
|
||||
third-party/argparse/argparse.h third-party/argparse/argparse.c
|
||||
|
||||
src/main.c
|
||||
src/sist.h
|
||||
src/io/walk.h src/io/walk.c
|
||||
src/io/store.h src/io/store.c
|
||||
src/tpool.h src/tpool.c
|
||||
src/parsing/parse.h src/parsing/parse.c
|
||||
src/parsing/magic_util.c src/parsing/magic_util.h
|
||||
src/io/serialize.h src/io/serialize.c
|
||||
src/parsing/mime.h src/parsing/mime.c src/parsing/mime_generated.c
|
||||
src/index/web.c src/index/web.h
|
||||
src/web/serve.c src/web/serve.h
|
||||
src/web/web_util.c src/web/web_util.h
|
||||
src/index/elastic.c src/index/elastic.h
|
||||
src/util.c src/util.h
|
||||
src/ctx.h src/types.h
|
||||
src/ctx.c src/ctx.h
|
||||
src/types.h
|
||||
src/log.c src/log.h
|
||||
src/cli.c src/cli.h
|
||||
src/stats.c src/stats.h src/ctx.c
|
||||
src/parsing/sidecar.c src/parsing/sidecar.h
|
||||
src/database/database.c src/database/database.h
|
||||
src/parsing/fs_util.h
|
||||
|
||||
# argparse
|
||||
third-party/argparse/argparse.h third-party/argparse/argparse.c
|
||||
)
|
||||
src/auth0/auth0_c_api.h src/auth0/auth0_c_api.cpp
|
||||
|
||||
src/database/database_stats.c src/database/database_stats.h src/database/database_schema.c)
|
||||
set_target_properties(sist2 PROPERTIES LINKER_LANGUAGE C)
|
||||
|
||||
target_link_directories(sist2 PRIVATE BEFORE ${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/)
|
||||
set(CMAKE_FIND_LIBRARY_SUFFIXES .a .lib)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
|
||||
pkg_search_module(GLIB REQUIRED glib-2.0)
|
||||
|
||||
find_package(lmdb CONFIG REQUIRED)
|
||||
find_package(cJSON CONFIG REQUIRED)
|
||||
find_package(unofficial-mongoose CONFIG REQUIRED)
|
||||
find_package(CURL CONFIG REQUIRED)
|
||||
find_library(MAGIC_LIB NAMES libmagic.a REQUIRED)
|
||||
find_package(unofficial-sqlite3 CONFIG REQUIRED)
|
||||
|
||||
|
||||
target_include_directories(
|
||||
@@ -62,7 +68,6 @@ target_include_directories(
|
||||
${CMAKE_SOURCE_DIR}/third-party/utf8.h/
|
||||
${CMAKE_SOURCE_DIR}/third-party/libscan/
|
||||
${CMAKE_SOURCE_DIR}/
|
||||
${GLIB_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_compile_options(
|
||||
@@ -93,13 +98,26 @@ if (SIST_DEBUG)
|
||||
PROPERTIES
|
||||
OUTPUT_NAME sist2_debug
|
||||
)
|
||||
elseif (SIST_FAST)
|
||||
target_compile_options(
|
||||
sist2
|
||||
PRIVATE
|
||||
|
||||
-Ofast
|
||||
-march=native
|
||||
-fno-stack-protector
|
||||
-fomit-frame-pointer
|
||||
-freciprocal-math
|
||||
)
|
||||
else ()
|
||||
target_compile_options(
|
||||
sist2
|
||||
PRIVATE
|
||||
|
||||
-Ofast
|
||||
-fno-stack-protector
|
||||
-fomit-frame-pointer
|
||||
-w
|
||||
)
|
||||
endif ()
|
||||
|
||||
@@ -113,19 +131,16 @@ target_link_libraries(
|
||||
sist2
|
||||
|
||||
z
|
||||
lmdb
|
||||
cjson
|
||||
argparse
|
||||
${GLIB_LDFLAGS}
|
||||
unofficial::mongoose::mongoose
|
||||
CURL::libcurl
|
||||
|
||||
pthread
|
||||
magic
|
||||
|
||||
c
|
||||
|
||||
scan
|
||||
|
||||
${MAGIC_LIB}
|
||||
unofficial::sqlite3::sqlite3
|
||||
)
|
||||
|
||||
add_custom_target(
|
||||
|
||||
53
Dockerfile
53
Dockerfile
@@ -1,15 +1,39 @@
|
||||
FROM simon987/sist2-build as build
|
||||
MAINTAINER simon987 <me@simon987.net>
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash
|
||||
RUN apt update -y; apt install -y nodejs && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /build/
|
||||
COPY . .
|
||||
RUN cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake .
|
||||
RUN make -j$(nproc)
|
||||
RUN strip sist2 || mv sist2_debug sist2
|
||||
|
||||
FROM --platform="linux/amd64" ubuntu:21.10
|
||||
COPY scripts scripts
|
||||
COPY schema schema
|
||||
COPY CMakeLists.txt .
|
||||
COPY third-party third-party
|
||||
COPY src src
|
||||
COPY sist2-vue sist2-vue
|
||||
COPY sist2-admin sist2-admin
|
||||
|
||||
RUN apt update && apt install -y curl libasan5 && rm -rf /var/lib/apt/lists/*
|
||||
RUN cd sist2-vue/ && npm install && npm run build
|
||||
RUN cd sist2-admin/frontend/ && npm install && npm run build
|
||||
|
||||
RUN mkdir build && cd build && cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake ..
|
||||
RUN cd build && make -j$(nproc)
|
||||
RUN strip build/sist2 || mv build/sist2_debug build/sist2
|
||||
|
||||
FROM --platform="linux/amd64" ubuntu@sha256:965fbcae990b0467ed5657caceaec165018ef44a4d2d46c7cdea80a9dff0d1ea
|
||||
|
||||
WORKDIR /root
|
||||
|
||||
ENV LANG C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
|
||||
ENTRYPOINT ["/root/sist2"]
|
||||
|
||||
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y curl libasan5 libmagic1 python3 \
|
||||
python3-pip git tesseract-ocr && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /usr/share/tessdata && \
|
||||
cd /usr/share/tessdata/ && \
|
||||
@@ -18,11 +42,16 @@ RUN mkdir -p /usr/share/tessdata && \
|
||||
curl -o /usr/share/tessdata/eng.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/eng.traineddata &&\
|
||||
curl -o /usr/share/tessdata/fra.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/fra.traineddata &&\
|
||||
curl -o /usr/share/tessdata/rus.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/rus.traineddata &&\
|
||||
curl -o /usr/share/tessdata/spa.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/spa.traineddata
|
||||
curl -o /usr/share/tessdata/osd.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/osd.traineddata &&\
|
||||
curl -o /usr/share/tessdata/spa.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/spa.traineddata &&\
|
||||
curl -o /usr/share/tessdata/deu.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/deu.traineddata &&\
|
||||
curl -o /usr/share/tessdata/equ.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/equ.traineddata &&\
|
||||
curl -o /usr/share/tessdata/chi_sim.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/chi_sim.traineddata
|
||||
|
||||
ENTRYPOINT ["/root/sist2"]
|
||||
# sist2
|
||||
COPY --from=build /build/build/sist2 /root/sist2
|
||||
|
||||
ENV LANG C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
|
||||
COPY --from=build /build/sist2 /root/sist2
|
||||
# sist2-admin
|
||||
COPY sist2-admin/requirements.txt sist2-admin/
|
||||
RUN python3 -m pip install --no-cache -r sist2-admin/requirements.txt
|
||||
COPY --from=build /build/sist2-admin/ sist2-admin/
|
||||
|
||||
@@ -3,13 +3,20 @@ MAINTAINER simon987 <me@simon987.net>
|
||||
|
||||
WORKDIR /build/
|
||||
ADD . /build/
|
||||
RUN cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake .
|
||||
RUN make -j$(nproc)
|
||||
RUN strip sist2
|
||||
RUN mkdir build && cd build && cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake ..
|
||||
RUN cd build && make -j$(nproc)
|
||||
RUN strip build/sist2 || mv build/sist2_debug build/sist2
|
||||
|
||||
FROM --platform="linux/arm64/v8" ubuntu:21.10
|
||||
FROM --platform=linux/arm64/v8 ubuntu@sha256:537da24818633b45fcb65e5285a68c3ec1f3db25f5ae5476a7757bc8dfae92a3
|
||||
|
||||
RUN apt update && apt install -y curl libasan5 && rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /root
|
||||
|
||||
ENV LANG C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
|
||||
ENTRYPOINT ["/root/sist2"]
|
||||
|
||||
RUN apt update && apt install -y curl libasan5 libmagic1 tesseract-ocr python3-pip python3 git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /usr/share/tessdata && \
|
||||
cd /usr/share/tessdata/ && \
|
||||
@@ -18,11 +25,16 @@ RUN mkdir -p /usr/share/tessdata && \
|
||||
curl -o /usr/share/tessdata/eng.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/eng.traineddata &&\
|
||||
curl -o /usr/share/tessdata/fra.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/fra.traineddata &&\
|
||||
curl -o /usr/share/tessdata/rus.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/rus.traineddata &&\
|
||||
curl -o /usr/share/tessdata/spa.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/spa.traineddata
|
||||
curl -o /usr/share/tessdata/osd.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/osd.traineddata &&\
|
||||
curl -o /usr/share/tessdata/spa.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/spa.traineddata &&\
|
||||
curl -o /usr/share/tessdata/deu.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/deu.traineddata &&\
|
||||
curl -o /usr/share/tessdata/equ.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/equ.traineddata &&\
|
||||
curl -o /usr/share/tessdata/chi_sim.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/chi_sim.traineddata
|
||||
|
||||
ENV LANG C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
# sist2
|
||||
COPY --from=build /build/build/sist2 /root/sist2
|
||||
|
||||
ENTRYPOINT ["/root/sist2"]
|
||||
|
||||
COPY --from=build /build/sist2 /root/sist2
|
||||
# sist2-admin
|
||||
COPY sist2-admin/requirements.txt sist2-admin/
|
||||
RUN python3 -m pip install --no-cache -r sist2-admin/requirements.txt
|
||||
COPY --from=build /build/sist2-admin/ sist2-admin/
|
||||
|
||||
17
README.md
17
README.md
@@ -37,12 +37,12 @@ sist2 (Simple incremental search tool)
|
||||
1. Download [from official website](https://www.elastic.co/downloads/elasticsearch)
|
||||
1. *(or)* Run using docker:
|
||||
```bash
|
||||
docker run -d -p 9200:9200 -e "discovery.type=single-node" elasticsearch:7.14.0
|
||||
docker run -d -p 9200:9200 -e "discovery.type=single-node" elasticsearch:7.17.9
|
||||
```
|
||||
1. *(or)* Run using docker-compose:
|
||||
```yaml
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:7.14.0
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.9
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- "ES_JAVA_OPTS=-Xms1G -Xmx2G"
|
||||
@@ -52,7 +52,7 @@ sist2 (Simple incremental search tool)
|
||||
Select the file corresponding to your CPU architecture and mark the binary as executable with `chmod +x` *
|
||||
2. *(or)* Download a [development snapshot](https://files.simon987.net/.gate/sist2/simon987_sist2/) *(Not
|
||||
recommended!)*
|
||||
3. *(or)* `docker pull simon987/sist2:2.11.7-x64-linux`
|
||||
3. *(or)* `docker pull simon987/sist2:2.12.1-x64-linux`
|
||||
|
||||
1. See [Usage guide](docs/USAGE.md)
|
||||
|
||||
@@ -81,7 +81,7 @@ See [Usage guide](docs/USAGE.md) for more details
|
||||
| html, xml | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | yes | no | - |
|
||||
| tar, zip, rar, 7z, ar ... | Libarchive | yes\* | - | no |
|
||||
| docx, xlsx, pptx | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | yes | if embedded | creator, modified_by, title |
|
||||
| doc (MS Word 97-2003) | antiword | yes | yes | author, title |
|
||||
| doc (MS Word 97-2003) | antiword | yes | no | author, title |
|
||||
| mobi, azw, azw3 | libmobi | yes | no | author, title |
|
||||
| wpd (WordPerfect) | libwpd | yes | no | *planned* |
|
||||
| json, jsonl, ndjson | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | yes | - | - |
|
||||
@@ -109,7 +109,7 @@ Download the language data files with your package manager (`apt install tessera
|
||||
directly [from Github](https://github.com/tesseract-ocr/tesseract/wiki/Data-Files).
|
||||
|
||||
The `simon987/sist2` image comes with common languages
|
||||
(hin, jpn, eng, fra, rus, spa) pre-installed.
|
||||
(hin, jpn, eng, fra, rus, spa, chi_sim, deu) pre-installed.
|
||||
|
||||
You can use the `+` separator to specify multiple languages. The language
|
||||
name must be identical to the `*.traineddata` file installed on your system
|
||||
@@ -141,7 +141,7 @@ docker run --rm --entrypoint cat my-sist2-image /root/sist2 > sist2-x64-linux
|
||||
1. Install compile-time dependencies
|
||||
|
||||
```bash
|
||||
apt install gcc g++ python3 yasm ragel automake autotools-dev wget libtool libssl-dev curl zip unzip tar xorg-dev libglu1-mesa-dev libxcursor-dev libxml2-dev libxinerama-dev gettext nasm git
|
||||
apt install gcc g++ python3 yasm ragel automake autotools-dev wget libtool libssl-dev curl zip unzip tar xorg-dev libglu1-mesa-dev libxcursor-dev libxml2-dev libxinerama-dev gettext nasm git nodejs
|
||||
```
|
||||
|
||||
1. Apply vcpkg patches, as per [sist2-build](https://github.com/simon987/sist2-build) Dockerfile
|
||||
@@ -149,13 +149,14 @@ docker run --rm --entrypoint cat my-sist2-image /root/sist2 > sist2-x64-linux
|
||||
1. Install vcpkg dependencies
|
||||
|
||||
```bash
|
||||
vcpkg install curl[core,openssl]
|
||||
vcpkg install lmdb cjson glib brotli libarchive[core,bzip2,libxml2,lz4,lzma,lzo] pthread tesseract libxml2 libmupdf gtest mongoose libmagic libraw jasper lcms gumbo
|
||||
vcpkg install curl[core,openssl] sqlite3 cpp-jwt pcre cjson brotli libarchive[core,bzip2,libxml2,lz4,lzma,lzo] pthread tesseract libxml2 libmupdf gtest mongoose libmagic libraw gumbo ffmpeg[core,avcodec,avformat,swscale,swresample]
|
||||
```
|
||||
|
||||
1. Build
|
||||
```bash
|
||||
git clone --recursive https://github.com/simon987/sist2/
|
||||
(cd sist2-vue; npm install; npm run build)
|
||||
(cd sist2-admin/frontend; npm install; npm run build)
|
||||
cmake -DSIST_DEBUG=off -DCMAKE_TOOLCHAIN_FILE=<VCPKG_ROOT>/scripts/buildsystems/vcpkg.cmake .
|
||||
make
|
||||
```
|
||||
|
||||
@@ -12,7 +12,7 @@ REWRITE_URL=""
|
||||
sist2 scan \
|
||||
--threads 14 \
|
||||
--mem-throttle 32768 \
|
||||
--quality 1.0 \
|
||||
--thumbnail-quality 2 \
|
||||
--name $NAME \
|
||||
--ocr-lang=eng+chi_sim \
|
||||
--ocr-ebooks \
|
||||
|
||||
@@ -12,7 +12,7 @@ REWRITE_URL=""
|
||||
sist2 scan \
|
||||
--threads 14 \
|
||||
--mem-throttle 32768 \
|
||||
--quality 1.0 \
|
||||
--thumbnail-quality 2 \
|
||||
--name $NAME \
|
||||
--ocr-lang=eng+chi_sim \
|
||||
--ocr-ebooks \
|
||||
|
||||
24
docker-compose.yml
Normal file
24
docker-compose.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
elasticsearch:
|
||||
image: elasticsearch:7.17.9
|
||||
container_name: sist2-es
|
||||
environment:
|
||||
- "discovery.type=single-node"
|
||||
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
|
||||
sist2-admin:
|
||||
build:
|
||||
context: .
|
||||
container_name: sist2-admin
|
||||
volumes:
|
||||
- /mnt/array/sist2-admin-data/:/sist2-admin/
|
||||
- /:/host
|
||||
ports:
|
||||
- 4090:4090
|
||||
# NOTE: Don't export this port publicly!
|
||||
- 8080:8080
|
||||
working_dir: /root/sist2-admin/
|
||||
entrypoint: python3
|
||||
command:
|
||||
- /root/sist2-admin/sist2_admin/app.py
|
||||
@@ -33,7 +33,7 @@ Lightning-fast file system indexer and search tool.
|
||||
Scan options
|
||||
-t, --threads=<int> Number of threads. DEFAULT=1
|
||||
--mem-throttle=<int> Total memory threshold in MiB for scan throttling. DEFAULT=0
|
||||
-q, --thumbnail-quality=<flt> Thumbnail quality, on a scale of 1.0 to 31.0, 1.0 being the best. DEFAULT=1
|
||||
-q, --thumbnail-quality=<int> Thumbnail quality, on a scale of 2 to 31, 2 being the best. DEFAULT=2
|
||||
--thumbnail-size=<int> Thumbnail size, in pixels. DEFAULT=500
|
||||
--thumbnail-count=<int> Number of thumbnails to generate. Set a value > 1 to create video previews, set to 0 to disable thumbnails. DEFAULT=1
|
||||
--content-size=<int> Number of bytes to be extracted from text documents. Set to 0 to disable. DEFAULT=32768
|
||||
@@ -74,6 +74,10 @@ Web options
|
||||
--es-index=<str> Elasticsearch index name. DEFAULT=sist2
|
||||
--bind=<str> Listen on this address. DEFAULT=localhost:4090
|
||||
--auth=<str> Basic auth in user:password format
|
||||
--auth0-audience=<str> API audience/identifier
|
||||
--auth0-domain=<str> Application domain
|
||||
--auth0-client-id=<str> Application client ID
|
||||
--auth0-public-key-file=<str> Path to Auth0 public key file extracted from <domain>/pem
|
||||
--tag-auth=<str> Basic auth in user:password format for tagging
|
||||
--tagline=<str> Tagline in navbar
|
||||
--dev Serve html & js files from disk (for development)
|
||||
@@ -97,13 +101,13 @@ Made by simon987 <me@simon987.net>. Released under GPL-3.0
|
||||
Total memory threshold in MiB for scan throttling. Worker threads will not start a new parse job
|
||||
until the total memory usage of sist2 is below this threshold. Set to 0 to disable. DEFAULT=0
|
||||
* `-q, --thumbnail-quality`
|
||||
Thumbnail quality, on a scale of 1.0 to 31.0, 1.0 being the best.
|
||||
Thumbnail quality, on a scale of 2 to 32, 2 being the best. See section below for a rough estimate of thumbnail database size
|
||||
* `--thumbnail-size`
|
||||
Thumbnail size in pixels.
|
||||
* `--thumbnail-count`
|
||||
Maximum number of thumbnails to generate. When set to a value >= 2, thumbnails for video previews
|
||||
will be generated. The actual number of thumbnails generated depends on the length of the video (maximum 1 image
|
||||
every ~5s). Set to 0 to completely disable thumbnails.
|
||||
every ~7s). Set to 0 to completely disable thumbnails.
|
||||
* `--content-size`
|
||||
Number of bytes of text to be extracted from the content of files (plain text, PDFs etc.).
|
||||
Repeated whitespace and special characters do not count toward this limit.
|
||||
@@ -150,6 +154,18 @@ Made by simon987 <me@simon987.net>. Released under GPL-3.0
|
||||
operations. Checksums are not calculated for all file types, unless the file is inside an archive. When enabled, duplicate
|
||||
files are hidden in the web UI (this behaviour can be toggled in the Configuration page).
|
||||
|
||||
|
||||
#### Thumbnail database size estimation
|
||||
|
||||
See chart below for rough estimate of thumbnail size vs. thumbnail size & quality arguments:
|
||||
|
||||
For example, `--thumbnail-size=500`, `--thumbnail-quality=2` for a directory with 8 million images will create a thumbnail database
|
||||
that is about `8000000 * 36kB = 288GB`.
|
||||
|
||||

|
||||
|
||||
// TODO: add note about LMDB page size 4096
|
||||
|
||||
### Scan examples
|
||||
|
||||
Simple scan
|
||||
@@ -157,7 +173,7 @@ Simple scan
|
||||
sist2 scan ~/Documents
|
||||
|
||||
sist2 scan \
|
||||
--threads 4 --content-size 16000000 --quality 1.0 --archive shallow \
|
||||
--threads 4 --content-size 16000000 --thumbnail-quality 2 --archive shallow \
|
||||
--name "My Documents" --rewrite-url "http://nas.domain.local/My Documents/" \
|
||||
~/Documents -o ./documents.idx/
|
||||
```
|
||||
@@ -268,6 +284,7 @@ sist2 index --print ./my_index/ | jq | less
|
||||
* `--dev` Serve html & js files from disk (for development, used to modify frontend files without having to recompile)
|
||||
* `--lang=<str>` Set the default web UI language (See #180 for a list of supported languages, default
|
||||
is `en`). The user can change the language in the configuration page
|
||||
* `--auth0-audience`, `--auth0-domain`, `--auth0-client-id`, `--auth0-public-key-file` See [Authentication with Auth0](auth0.md)
|
||||
|
||||
### Web examples
|
||||
|
||||
@@ -292,7 +309,7 @@ Both the `root` and `rewrite_url` fields are safe to manually modify from the
|
||||
|
||||
# Elasticsearch
|
||||
|
||||
Elasticsearch versions >=6.8.0, <8.0.0 are supported by sist2.
|
||||
Elasticsearch versions >=6.8.0, 7.X.X and 8.X.X are supported by sist2.
|
||||
|
||||
Using a version >=7.14.0 is recommended to enable the following features:
|
||||
|
||||
|
||||
19
docs/auth0.md
Normal file
19
docs/auth0.md
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
# Authentication with Auth0
|
||||
|
||||
1. Create a new Auth0 application (Single page app)
|
||||
2. Create a new Auth0 API:
|
||||
1. Choose `RS256` signing algorithm
|
||||
2. Set identifier (audience) to `https://sist2`
|
||||
3. Download the Auth0 certificate from https://<domain>.auth0.com/pem (you can find the domain Applications->Basic information)
|
||||
4. Extract the public key from the certificate using `openssl x509 -pubkey -noout -in cert.pem > pubkey.txt`
|
||||
5. Start the sist2 web server
|
||||
|
||||
Example options:
|
||||
```bash
|
||||
sist2 web \
|
||||
--auth0-client-id XXX \
|
||||
--auth0-audience https://sist2 \
|
||||
--auth0-domain YYY.auth0.com \
|
||||
--auth0-public-key-file /ZZZ/pubkey.txt
|
||||
```
|
||||
BIN
docs/thumbnail_size.png
Normal file
BIN
docs/thumbnail_size.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 180 KiB |
@@ -67,7 +67,8 @@
|
||||
"index": false
|
||||
},
|
||||
"mtime": {
|
||||
"type": "integer"
|
||||
"type": "date",
|
||||
"format": "epoch_millis"
|
||||
},
|
||||
"size": {
|
||||
"type": "long"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"refresh_interval": "30s",
|
||||
"codec": "best_compression",
|
||||
"number_of_replicas": 0,
|
||||
"highlight.max_analyzed_offset": 10000000
|
||||
"highlight.max_analyzed_offset": 1000000
|
||||
},
|
||||
"analysis": {
|
||||
"tokenizer": {
|
||||
@@ -16,7 +16,7 @@
|
||||
"delimiter": "."
|
||||
},
|
||||
"my_nGram_tokenizer": {
|
||||
"type": "nGram",
|
||||
"type": "ngram",
|
||||
"min_gram": 3,
|
||||
"max_gram": 3
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
rm -rf index.sist2/
|
||||
(
|
||||
cd ..
|
||||
rm -rf index.sist2
|
||||
|
||||
python3 scripts/mime.py > src/parsing/mime_generated.c
|
||||
python3 scripts/serve_static.py > src/web/static_generated.c
|
||||
python3 scripts/index_static.py > src/index/static_generated.c
|
||||
python3 scripts/mime.py > src/parsing/mime_generated.c
|
||||
python3 scripts/serve_static.py > src/web/static_generated.c
|
||||
python3 scripts/index_static.py > src/index/static_generated.c
|
||||
python3 scripts/magic_static.py > src/magic_generated.c
|
||||
|
||||
printf "static const char *const Sist2CommitHash = \"%s\";\n" $(git rev-parse HEAD) > src/git_hash.h
|
||||
printf "static const char *const Sist2CommitHash = \"%s\";\n" $(git rev-parse HEAD) > src/git_hash.h
|
||||
)
|
||||
@@ -4,14 +4,20 @@ VCPKG_ROOT="/vcpkg"
|
||||
|
||||
git submodule update --init --recursive
|
||||
|
||||
rm -rf CMakeFiles CMakeCache.txt
|
||||
cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" .
|
||||
make -j $(nproc)
|
||||
strip sist2
|
||||
./sist2 -v > VERSION
|
||||
mv sist2 sist2-x64-linux
|
||||
mkdir build
|
||||
(
|
||||
cd build
|
||||
cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" ..
|
||||
make -j $(nproc)
|
||||
strip sist2
|
||||
./sist2 -v > VERSION
|
||||
)
|
||||
mv build/sist2 sist2-x64-linux
|
||||
|
||||
rm -rf CMakeFiles CMakeCache.txt
|
||||
cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG=on -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" .
|
||||
make -j $(nproc)
|
||||
mv sist2_debug sist2-x64-linux-debug
|
||||
(
|
||||
cd build
|
||||
rm -rf CMakeFiles CMakeCache.txt
|
||||
cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG=on -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" ..
|
||||
make -j $(nproc)
|
||||
)
|
||||
mv build/sist2_debug sist2-x64-linux-debug
|
||||
@@ -4,14 +4,19 @@ VCPKG_ROOT="/vcpkg"
|
||||
|
||||
git submodule update --init --recursive
|
||||
|
||||
rm -rf CMakeFiles CMakeCache.txt
|
||||
cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" .
|
||||
make -j $(nproc)
|
||||
strip sist2
|
||||
mv sist2 sist2-arm64-linux
|
||||
mkdir build
|
||||
(
|
||||
cd build
|
||||
cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" .
|
||||
make -j $(nproc)
|
||||
strip sist2
|
||||
)
|
||||
mv build/sist2 sist2-arm64-linux
|
||||
|
||||
rm -rf CMakeFiles CMakeCache.txt
|
||||
cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG=on -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" .
|
||||
make -j $(nproc)
|
||||
strip sist2
|
||||
mv sist2_debug sist2-arm64-linux-debug
|
||||
(
|
||||
cd build
|
||||
cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG=on -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" .
|
||||
make -j $(nproc)
|
||||
)
|
||||
mv build/sist2_debug sist2-arm64-linux-debug
|
||||
8
scripts/magic_static.py
Normal file
8
scripts/magic_static.py
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
try:
|
||||
with open("/usr/lib/file/magic.mgc", "rb") as f:
|
||||
data = f.read()
|
||||
except:
|
||||
data = bytes([])
|
||||
|
||||
print("char magic_database_buffer[%d] = {%s};" % (len(data), ",".join(str(int(b)) for b in data)))
|
||||
@@ -1,3 +1,4 @@
|
||||
application/x-matlab-data,mat
|
||||
application/arj, arj
|
||||
application/base64, mme
|
||||
application/binhex, hqx
|
||||
@@ -29,7 +30,7 @@ application/mime, aps
|
||||
application/mspowerpoint, ppz
|
||||
application/msword, doc|dot|w6w|wiz|word
|
||||
application/netmc, mcp
|
||||
application/octet-stream, bin|dump|gpg
|
||||
application/octet-stream, bin|dump|gpg|pack|idx
|
||||
application/oda, oda
|
||||
application/ogg, ogv
|
||||
application/pdf, pdf
|
||||
@@ -243,7 +244,7 @@ audio/make, funk|my|pfunk
|
||||
audio/midi, kar
|
||||
audio/mid, rmi
|
||||
audio/mp4, m4b
|
||||
audio/mpeg, m2a|mpa
|
||||
audio/mpeg, m2a|mpa|mpga
|
||||
audio/ogg, ogg
|
||||
audio/s3m, s3m
|
||||
audio/tsp-audio, tsi
|
||||
@@ -346,6 +347,8 @@ text/mcf, mcf
|
||||
text/pascal, pas
|
||||
text/PGP,
|
||||
text/plain, com|cmd|conf|def|g|idc|list|lst|mar|sdml|text|txt|md|groovy|license|properties|desktop|ini|rst|cmake|ipynb|readme|less|lo|go|yml|d|cs|hpp|srt|nfo|sfv|m3u|csv|eml|make|log|markdown|yaml
|
||||
text/x-script.python, pyx
|
||||
text/csv,
|
||||
application/vnd.coffeescript, coffee
|
||||
text/richtext, rt|rtf|rtx
|
||||
text/rtf,
|
||||
@@ -382,7 +385,7 @@ text/x-pascal, p
|
||||
text/x-perl, pl
|
||||
text/x-php, php
|
||||
text/x-po, po
|
||||
text/x-python, py
|
||||
text/x-python, py|pyi
|
||||
text/x-ruby, rb
|
||||
text/x-sass, sass
|
||||
text/x-scss, scss
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import zlib
|
||||
|
||||
mimes = {}
|
||||
noparse = set()
|
||||
ext_in_hash = set()
|
||||
@@ -135,24 +137,40 @@ def clean(t):
|
||||
return t.replace("/", "_").replace(".", "_").replace("+", "_").replace("-", "_")
|
||||
|
||||
|
||||
def crc(s):
|
||||
return zlib.crc32(s.encode()) & 0xffffffff
|
||||
|
||||
|
||||
with open("scripts/mime.csv") as f:
|
||||
for l in f:
|
||||
mime, ext_list = l.split(",")
|
||||
if l.startswith("!"):
|
||||
mime = mime[1:]
|
||||
noparse.add(mime)
|
||||
ext = [x.strip() for x in ext_list.split("|")]
|
||||
ext = [x.strip() for x in ext_list.split("|") if x.strip() != ""]
|
||||
mimes[mime] = ext
|
||||
|
||||
seen_crc = set()
|
||||
for ext in mimes.values():
|
||||
for e in ext:
|
||||
if crc(e) in seen_crc:
|
||||
raise Exception("CRC32 collision")
|
||||
seen_crc.add(crc(e))
|
||||
|
||||
seen_crc = set()
|
||||
for mime in mimes.keys():
|
||||
if crc(mime) in seen_crc:
|
||||
raise Exception("CRC32 collision")
|
||||
seen_crc.add(crc(mime))
|
||||
|
||||
print("// **Generated by mime.py**")
|
||||
print("#ifndef MIME_GENERATED_C")
|
||||
print("#define MIME_GENERATED_C")
|
||||
print("#include <glib.h>\n")
|
||||
print("#include <stdlib.h>\n")
|
||||
# Enum
|
||||
print("enum mime {")
|
||||
for mime, ext in sorted(mimes.items()):
|
||||
print(" " + clean(mime) + "=" + mime_id(mime) + ",")
|
||||
print(f"{clean(mime)}={mime_id(mime)},")
|
||||
print("};")
|
||||
|
||||
# Enum -> string
|
||||
@@ -163,20 +181,20 @@ with open("scripts/mime.csv") as f:
|
||||
print("default: return NULL;}}")
|
||||
|
||||
# Ext -> Enum
|
||||
print("GHashTable *mime_get_ext_table() {"
|
||||
"GHashTable *ext_table = g_hash_table_new(g_str_hash, g_str_equal);")
|
||||
print("unsigned int mime_extension_lookup(unsigned long extension_crc32) {"
|
||||
"switch (extension_crc32) {")
|
||||
for mime, ext in mimes.items():
|
||||
for e in [e for e in ext if e]:
|
||||
print("g_hash_table_insert(ext_table, \"" + e + "\", (gpointer)" + clean(mime) + ");")
|
||||
if e in ext_in_hash:
|
||||
raise Exception("extension already in hash: " + e)
|
||||
ext_in_hash.add(e)
|
||||
print("return ext_table;}")
|
||||
if len(ext) > 0:
|
||||
for e in ext:
|
||||
print(f"case {crc(e)}:", end="")
|
||||
print(f"return {clean(mime)};")
|
||||
print("default: return 0;}}")
|
||||
|
||||
# string -> Enum
|
||||
print("GHashTable *mime_get_mime_table() {"
|
||||
"GHashTable *mime_table = g_hash_table_new(g_str_hash, g_str_equal);")
|
||||
for mime, ext in mimes.items():
|
||||
print("g_hash_table_insert(mime_table, \"" + mime + "\", (gpointer)" + clean(mime) + ");")
|
||||
print("return mime_table;}")
|
||||
print("unsigned int mime_name_lookup(unsigned long mime_crc32) {"
|
||||
"switch (mime_crc32) {")
|
||||
for mime in mimes.keys():
|
||||
print(f"case {crc(mime)}: return {clean(mime)};")
|
||||
|
||||
print("default: return 0;}}")
|
||||
print("#endif")
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
make clean
|
||||
rm -rf CMakeFiles/ CMakeCache.txt Makefile \
|
||||
third-party/libscan/CMakeFiles third-party/libscan/CMakeCache.txt third-party/libscan/third-party/ext_ffmpeg \
|
||||
third-party/libscan/third-party/ext_libmobi third-party/libscan/Makefile
|
||||
@@ -1,2 +1,3 @@
|
||||
docker run --rm -it -p 9200:9200 -e "discovery.type=single-node" \
|
||||
-e "ES_JAVA_OPTS=-Xms8g -Xmx8g" elasticsearch:7.14.0
|
||||
docker run --rm -it --name "sist2-dev-es"\
|
||||
-p 9200:9200 -e "discovery.type=single-node" \
|
||||
-e "ES_JAVA_OPTS=-Xms8g -Xmx8g" elasticsearch:7.17.9
|
||||
|
||||
3
scripts/start_dev_es_6.sh
Executable file
3
scripts/start_dev_es_6.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
docker run --rm -it --name "sist2-dev-es-6"\
|
||||
-p 9202:9200 -e "discovery.type=single-node" \
|
||||
-e "ES_JAVA_OPTS=-Xms8g -Xmx8g" elasticsearch:6.8.0
|
||||
3
scripts/start_dev_es_8.sh
Executable file
3
scripts/start_dev_es_8.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
docker run --rm -it --name "sist2-dev-es"\
|
||||
-p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" \
|
||||
-e "ES_JAVA_OPTS=-Xms8g -Xmx8g" elasticsearch:8.1.2
|
||||
5
sist2-admin/frontend/babel.config.js
Normal file
5
sist2-admin/frontend/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
19086
sist2-admin/frontend/package-lock.json
generated
Normal file
19086
sist2-admin/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
sist2-admin/frontend/package.json
Normal file
49
sist2-admin/frontend/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "sist2-admin-vue",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"watch": "vue-cli-service build --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
"core-js": "^3.6.5",
|
||||
"moment": "^2.29.3",
|
||||
"socket.io-client": "^4.5.1",
|
||||
"vue": "^2.6.14",
|
||||
"vue-i18n": "^8.24.4",
|
||||
"vue-router": "^3.5.4",
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~5.0.8",
|
||||
"@vue/cli-plugin-router": "~5.0.8",
|
||||
"@vue/cli-plugin-vuex": "~5.0.8",
|
||||
"@vue/cli-service": "~5.0.8",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.5.2",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
BIN
sist2-admin/frontend/public/favicon.ico
Normal file
BIN
sist2-admin/frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
17
sist2-admin/frontend/public/index.html
Normal file
17
sist2-admin/frontend/public/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>serve_favicon_ico.ico">
|
||||
<title>sist2-admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
98
sist2-admin/frontend/src/App.vue
Normal file
98
sist2-admin/frontend/src/App.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<NavBar></NavBar>
|
||||
<b-container class="pt-4">
|
||||
<b-alert show dismissible variant="info">
|
||||
This is a beta version of sist2-admin. Please submit bug reports, usability issues and feature requests
|
||||
to the <a href="https://github.com/simon987/sist2/issues/new/choose" target="_blank">issue tracker on Github</a>. Thank you!
|
||||
</b-alert>
|
||||
<router-view/>
|
||||
</b-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavBar from "@/components/NavBar";
|
||||
import Sist2AdminApi from "@/Sist2AdminApi";
|
||||
|
||||
export default {
|
||||
components: {NavBar},
|
||||
data() {
|
||||
return {
|
||||
socket: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
Sist2AdminApi.getSist2AdminInfo()
|
||||
.then(resp => this.$store.commit("setSist2AdminInfo", resp.data));
|
||||
this.$store.dispatch("loadBrowserSettings");
|
||||
this.connectNotifications();
|
||||
// this.socket.onclose = this.connectNotifications;
|
||||
},
|
||||
methods: {
|
||||
connectNotifications() {
|
||||
this.socket = new WebSocket(`ws://${window.location.host}/notifications`);
|
||||
this.socket.onopen = () => {
|
||||
this.socket.send("Hello from client");
|
||||
}
|
||||
|
||||
this.socket.onmessage = e => {
|
||||
const notification = JSON.parse(e.data);
|
||||
if (notification.message) {
|
||||
notification.messageString = this.$t(notification.message).toString();
|
||||
}
|
||||
|
||||
this.$store.dispatch("notify", notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#app {
|
||||
/*font-family: Avenir, Helvetica, Arial, sans-serif;*/
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
/*text-align: center;*/
|
||||
color: #2c3e50;
|
||||
padding-bottom: 1em;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
width: 1rem;
|
||||
margin-right: 0.2rem;
|
||||
cursor: pointer;
|
||||
line-height: 1rem;
|
||||
height: 1rem;
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKICAgICB2aWV3Qm94PSIwIDAgNDI2LjY2NyA0MjYuNjY3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjYuNjY3IDQyNi42Njc7IiBmaWxsPSIjZmZmIj4KPGc+CiAgICA8Zz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHJlY3QgeD0iMTkyIiB5PSIxOTIiIHdpZHRoPSI0Mi42NjciIGhlaWdodD0iMTI4Ii8+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTMuMzMzLDBDOTUuNDY3LDAsMCw5NS40NjcsMCwyMTMuMzMzczk1LjQ2NywyMTMuMzMzLDIxMy4zMzMsMjEzLjMzM1M0MjYuNjY3LDMzMS4yLDQyNi42NjcsMjEzLjMzMwogICAgICAgICAgICAgICAgUzMzMS4yLDAsMjEzLjMzMywweiBNMjEzLjMzMywzODRjLTk0LjA4LDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMsNDIuNjY3LDIxMy4zMzMsNDIuNjY3CiAgICAgICAgICAgICAgICBTMzg0LDExOS4yNTMsMzg0LDIxMy4zMzNTMzA3LjQxMywzODQsMjEzLjMzMywzODR6Ii8+CiAgICAgICAgICAgIDxyZWN0IHg9IjE5MiIgeT0iMTA2LjY2NyIgd2lkdGg9IjQyLjY2NyIgaGVpZ2h0PSI0Mi42NjciLz4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+Cg==);
|
||||
filter: brightness(45%);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1500px) {
|
||||
.container {
|
||||
max-width: 1440px;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
117
sist2-admin/frontend/src/Sist2AdminApi.js
Normal file
117
sist2-admin/frontend/src/Sist2AdminApi.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import axios from "axios";
|
||||
|
||||
class Sist2AdminApi {
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = window.location.protocol + "//" + window.location.host;
|
||||
}
|
||||
|
||||
getJobs() {
|
||||
return axios.get(`${this.baseUrl}/api/job/`);
|
||||
}
|
||||
|
||||
getFrontends() {
|
||||
return axios.get(`${this.baseUrl}/api/frontend/`);
|
||||
}
|
||||
|
||||
getTasks() {
|
||||
return axios.get(`${this.baseUrl}/api/task/`);
|
||||
}
|
||||
|
||||
killTask(taskId) {
|
||||
return axios.post(`${this.baseUrl}/api/task/${taskId}/kill`)
|
||||
}
|
||||
|
||||
getTaskHistory() {
|
||||
return axios.get(`${this.baseUrl}/api/task/history`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
getJob(name) {
|
||||
return axios.get(`${this.baseUrl}/api/job/${name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
getFrontend(name) {
|
||||
return axios.get(`${this.baseUrl}/api/frontend/${name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
startFrontend(name) {
|
||||
return axios.post(`${this.baseUrl}/api/frontend/${name}/start`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
stopFrontend(name) {
|
||||
return axios.post(`${this.baseUrl}/api/frontend/${name}/stop`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param job
|
||||
*/
|
||||
updateJob(name, job) {
|
||||
return axios.put(`${this.baseUrl}/api/job/${name}`, job);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param frontend
|
||||
*/
|
||||
updateFrontend(name, frontend) {
|
||||
return axios.put(`${this.baseUrl}/api/frontend/${name}`, frontend);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
runJob(name) {
|
||||
return axios.get(`${this.baseUrl}/api/job/${name}/run`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
deleteJob(name) {
|
||||
return axios.delete(`${this.baseUrl}/api/job/${name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
deleteFrontend(name) {
|
||||
return axios.delete(`${this.baseUrl}/api/frontend/${name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
createJob(name) {
|
||||
return axios.post(`${this.baseUrl}/api/job/${name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
createFrontend(name) {
|
||||
return axios.post(`${this.baseUrl}/api/frontend/${name}`);
|
||||
}
|
||||
|
||||
pingEs(url, insecure) {
|
||||
return axios.get(`${this.baseUrl}/api/ping_es`, {params: {url, insecure}});
|
||||
}
|
||||
|
||||
getSist2AdminInfo() {
|
||||
return axios.get(`${this.baseUrl}/api/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Sist2AdminApi()
|
||||
31
sist2-admin/frontend/src/components/FrontendListItem.vue
Normal file
31
sist2-admin/frontend/src/components/FrontendListItem.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<b-list-group-item action :to="`/frontend/${frontend.name}`">
|
||||
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1" style="display: block">
|
||||
{{ frontend.name }}
|
||||
<b-badge variant="light">{{ formatBindAddress(frontend.web_options.bind) }}</b-badge>
|
||||
</h5>
|
||||
|
||||
<div>
|
||||
<b-badge v-if="frontend.running" variant="success">{{$t("online")}}</b-badge>
|
||||
<b-badge v-else variant="secondary">{{$t("offline")}}</b-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</b-list-group-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {formatBindAddress} from "@/util";
|
||||
|
||||
export default {
|
||||
name: "FrontendListItem",
|
||||
props: ["frontend"],
|
||||
data() {
|
||||
return {
|
||||
formatBindAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
64
sist2-admin/frontend/src/components/IndexOptions.vue
Normal file
64
sist2-admin/frontend/src/components/IndexOptions.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div>
|
||||
<label>{{ $t("indexOptions.threads") }}</label>
|
||||
<b-form-input v-model="options.threads" type="number" min="1" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("webOptions.esUrl") }}</label>
|
||||
<b-alert :variant="esTestOk ? 'success' : 'danger'" :show="showEsTestAlert" class="mt-1">
|
||||
{{ esTestMessage }}
|
||||
</b-alert>
|
||||
<b-input-group>
|
||||
<b-form-input v-model="options.es_url" @change="update()"></b-form-input>
|
||||
<b-input-group-append>
|
||||
<b-button variant="outline-primary" @click="testEs()">{{ $t("test") }}</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
|
||||
<label>{{ $t("indexOptions.esIndex") }}</label>
|
||||
<b-form-input v-model="options.es_index" @change="update()"></b-form-input>
|
||||
|
||||
<br>
|
||||
<b-form-checkbox v-model="options.es_insecure_ssl" :disabled="!options.es_url.startsWith('https')" @change="update()">
|
||||
{{ $t("webOptions.esInsecure") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<label>{{ $t("indexOptions.batchSize") }}</label>
|
||||
<b-form-input v-model="options.batch_size" type="number" min="1" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("indexOptions.script") }}</label>
|
||||
<b-form-textarea v-model="options.script" rows="6" @change="update()"></b-form-textarea>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import sist2AdminApi from "@/Sist2AdminApi";
|
||||
|
||||
export default {
|
||||
name: "IndexOptions",
|
||||
props: ["options"],
|
||||
data() {
|
||||
return {
|
||||
showEsTestAlert: false,
|
||||
esTestOk: false,
|
||||
esTestMessage: "",
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
this.$emit("change", this.options);
|
||||
},
|
||||
testEs() {
|
||||
sist2AdminApi.pingEs(this.options.es_url, this.options.es_insecure_ssl).then((resp) => {
|
||||
this.showEsTestAlert = true;
|
||||
this.esTestOk = resp.data.ok;
|
||||
this.esTestMessage = resp.data.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
42
sist2-admin/frontend/src/components/JobCheckboxGroup.vue
Normal file
42
sist2-admin/frontend/src/components/JobCheckboxGroup.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<h5>{{ $t("selectJobs") }}</h5>
|
||||
<b-progress v-if="loading" striped animated value="100"></b-progress>
|
||||
<b-form-group v-else>
|
||||
<b-form-checkbox-group
|
||||
v-if="jobs.length > 0"
|
||||
:checked="frontend.jobs"
|
||||
@input="frontend.jobs = $event; $emit('input')"
|
||||
>
|
||||
<div v-for="job in jobs" :key="job.name">
|
||||
<b-form-checkbox :disabled="job.status !== 'indexed'" :value="job.name">[{{ job.name }}]</b-form-checkbox>
|
||||
<br/>
|
||||
</div>
|
||||
</b-form-checkbox-group>
|
||||
<div v-else>
|
||||
<span class="text-muted">{{ $t('jobOptions.noJobAvailable') }}</span>
|
||||
<router-link to="/">{{$t("create")}}</router-link>
|
||||
</div>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Sist2AdminApi from "@/Sist2AdminApi";
|
||||
|
||||
export default {
|
||||
name: "JobCheckboxGroup",
|
||||
props: ["frontend"],
|
||||
mounted() {
|
||||
Sist2AdminApi.getJobs().then(resp => {
|
||||
this.jobs = resp.data;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
56
sist2-admin/frontend/src/components/JobListItem.vue
Normal file
56
sist2-admin/frontend/src/components/JobListItem.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<b-list-group-item class="flex-column align-items-start" action :to="`job/${job.name}`">
|
||||
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<div>
|
||||
<h5 class="mb-1">
|
||||
{{ job.name }}
|
||||
</h5>
|
||||
</div>
|
||||
<div>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<small v-if="job.last_index_date">
|
||||
{{ $t("scanned") }} {{ formatLastIndexDate(job.last_index_date) }}</small>
|
||||
<div v-else> </div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row v-if="job.schedule_enabled">
|
||||
<b-col>
|
||||
<small><code>{{job.cron_expression }}</code></small>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row v-else>
|
||||
<b-col>
|
||||
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</b-list-group-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from "moment";
|
||||
|
||||
export default {
|
||||
name: "JobListItem",
|
||||
props: ["job"],
|
||||
methods: {
|
||||
formatLastIndexDate(dateString) {
|
||||
if (dateString === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const date = Date.parse(dateString);
|
||||
return moment(date).fromNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
57
sist2-admin/frontend/src/components/JobOptions.vue
Normal file
57
sist2-admin/frontend/src/components/JobOptions.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-checkbox :checked="desktopNotificationsEnabled" @change="updateNotifications($event)">
|
||||
{{ $t("jobOptions.desktopNotifications") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox v-model="job.schedule_enabled" @change="update()">
|
||||
{{ $t("jobOptions.scheduleEnabled") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<label>{{ $t("jobOptions.cron") }}</label>
|
||||
<b-form-input class="text-monospace" :state="cronValid" v-model="job.cron_expression" :disabled="!job.schedule_enabled" @change="update()"></b-form-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "JobOptions",
|
||||
props: ["job"],
|
||||
data() {
|
||||
return {
|
||||
cronValid: undefined
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
desktopNotificationsEnabled() {
|
||||
return this.$store.state.jobDesktopNotificationMap[this.job.name];
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.cronValid = this.checkCron(this.job.cron_expression)
|
||||
},
|
||||
methods: {
|
||||
checkCron(expression) {
|
||||
return /((((\d+,)+\d+|(\d+([/-])\d+)|\d+|\*) ?){5,7})/.test(expression);
|
||||
},
|
||||
updateNotifications(value) {
|
||||
this.$store.dispatch("setJobDesktopNotification", {
|
||||
job: this.job.name,
|
||||
enabled: value
|
||||
});
|
||||
},
|
||||
update() {
|
||||
if (this.job.schedule_enabled) {
|
||||
this.cronValid = this.checkCron(this.job.cron_expression);
|
||||
} else {
|
||||
this.cronValid = undefined;
|
||||
}
|
||||
|
||||
if (this.cronValid !== false) {
|
||||
this.$emit("change", this.job);
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
69
sist2-admin/frontend/src/components/NavBar.vue
Normal file
69
sist2-admin/frontend/src/components/NavBar.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<b-navbar>
|
||||
<b-navbar-brand to="/">
|
||||
<Sist2Icon></Sist2Icon>
|
||||
</b-navbar-brand>
|
||||
|
||||
<b-button class="ml-auto" to="/task" variant="link">{{ $t("tasks") }}</b-button>
|
||||
</b-navbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Sist2Icon from "@/components/icons/Sist2Icon";
|
||||
|
||||
export default {
|
||||
name: "NavBar",
|
||||
components: {Sist2Icon},
|
||||
methods: {
|
||||
tagline() {
|
||||
return this.$store.state.sist2Info.tagline;
|
||||
},
|
||||
sist2Version() {
|
||||
return this.$store.state.sist2Info.version;
|
||||
},
|
||||
isDebug() {
|
||||
return this.$store.state.sist2Info.debug;
|
||||
},
|
||||
isLegacy() {
|
||||
return this.$store.state.sist2Info.esVersionLegacy;
|
||||
},
|
||||
hideLegacy() {
|
||||
return this.$store.state.optHideLegacy;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.navbar {
|
||||
box-shadow: 0 0.125rem 0.25rem rgb(0 0 0 / 8%) !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.theme-black .navbar {
|
||||
background: #546b7a30;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
color: #222 !important;
|
||||
font-size: 1.75rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.navbar-brand:hover {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
.version {
|
||||
color: #222 !important;
|
||||
margin-left: -18px;
|
||||
margin-top: -14px;
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
color: #222;
|
||||
}
|
||||
</style>
|
||||
109
sist2-admin/frontend/src/components/ScanOptions.vue
Normal file
109
sist2-admin/frontend/src/components/ScanOptions.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div>
|
||||
<label>{{ $t("scanOptions.path") }}</label>
|
||||
<b-form-input v-model="options.path" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("scanOptions.threads") }}</label>
|
||||
<b-form-input type="number" min="1" v-model="options.threads" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("scanOptions.thumbnailQuality") }}</label>
|
||||
<b-form-input type="number" min="1" max="31" v-model="options.thumbnail_quality" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("scanOptions.thumbnailCount") }}</label>
|
||||
<b-form-input type="number" min="0" max="1000" v-model="options.thumbnail_count" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("scanOptions.thumbnailSize") }}</label>
|
||||
<b-form-input type="number" min="100" v-model="options.thumbnail_size" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("scanOptions.contentSize") }}</label>
|
||||
<b-form-input type="number" min="0" v-model="options.content_size" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("scanOptions.rewriteUrl") }}</label>
|
||||
<b-form-input v-model="options.rewrite_url" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("scanOptions.depth") }}</label>
|
||||
<b-form-input type="number" min="0" v-model="options.depth" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("scanOptions.archive") }}</label>
|
||||
<b-form-select :options="['skip', 'list', 'shallow', 'recurse']" v-model="options.archive"
|
||||
@change="update()"></b-form-select>
|
||||
|
||||
<label>{{ $t("scanOptions.archivePassphrase") }}</label>
|
||||
<b-form-input v-model="options.archive_passphrase" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("scanOptions.ocrLang") }}</label>
|
||||
<b-alert variant="danger" show v-if="selectedOcrLangs.length === 0 && !disableOcrLang">{{ $t("scanOptions.ocrLangAlert") }}</b-alert>
|
||||
<b-checkbox-group :disabled="disableOcrLang" v-model="selectedOcrLangs" @input="onOcrLangChange">
|
||||
<b-checkbox v-for="lang in ocrLangs" :key="lang" :value="lang">{{ lang }}</b-checkbox>
|
||||
</b-checkbox-group>
|
||||
|
||||
<!-- <b-form-input readonly v-model="options.ocr_lang" @change="update()"></b-form-input>-->
|
||||
|
||||
<div style="height: 10px"></div>
|
||||
|
||||
<b-form-checkbox v-model="options.ocr_images" @change="update()">
|
||||
{{ $t("scanOptions.ocrImages") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox v-model="options.ocr_ebooks" @change="update()">
|
||||
{{ $t("scanOptions.ocrEbooks") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<label>{{ $t("scanOptions.exclude") }}</label>
|
||||
<b-form-input v-model="options.exclude" @change="update()"
|
||||
:placeholder="$t('scanOptions.excludePlaceholder')"></b-form-input>
|
||||
|
||||
<div style="height: 10px"></div>
|
||||
|
||||
<b-form-checkbox v-model="options.fast" @change="update()">
|
||||
{{ $t("scanOptions.fast") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox v-model="options.checksums" @change="update()">
|
||||
{{ $t("scanOptions.checksums") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox v-model="options.read_subtitles" @change="update()">
|
||||
{{ $t("scanOptions.readSubtitles") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox v-model="options.optimize_index" @change="update()">
|
||||
{{ $t("scanOptions.optimizeIndex") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<label>{{ $t("scanOptions.treemapThreshold") }}</label>
|
||||
<b-form-input type="number" min="0" v-model="options.treemap_threshold" @change="update()"></b-form-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "ScanOptions",
|
||||
props: ["options"],
|
||||
data() {
|
||||
return {
|
||||
disableOcrLang: false,
|
||||
selectedOcrLangs: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
ocrLangs() {
|
||||
return this.$store.state.sist2AdminInfo?.tesseract_langs || [];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOcrLangChange() {
|
||||
this.options.ocr_lang = this.selectedOcrLangs.join("+");
|
||||
},
|
||||
update() {
|
||||
this.disableOcrLang = this.options.ocr_images === false && this.options.ocr_ebooks === false;
|
||||
this.$emit("change", this.options);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.disableOcrLang = this.options.ocr_images === false && this.options.ocr_ebooks === false;
|
||||
this.selectedOcrLangs = this.options.ocr_lang ? this.options.ocr_lang.split("+") : [];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
57
sist2-admin/frontend/src/components/TaskListItem.vue
Normal file
57
sist2-admin/frontend/src/components/TaskListItem.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<b-list-group-item>
|
||||
<b-row style="height: 50px">
|
||||
<b-col><h5>{{ task.display_name }}</h5></b-col>
|
||||
<b-col class="shrink">
|
||||
<router-link class="btn btn-link" :to="`/log/${task.id}`">{{ $t("logs") }}</router-link>
|
||||
</b-col>
|
||||
<b-col class="shrink">
|
||||
<b-btn variant="link" @click="killTask(task.id)">{{ $t("kill") }}</b-btn>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-progress :max="task.progress.count">
|
||||
<b-progress-bar :value="task.progress.done" :label-html="label" :striped="!task.progress.waiting"/>
|
||||
</b-progress>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
</b-list-group-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import sist2AdminApi from "@/Sist2AdminApi";
|
||||
|
||||
export default {
|
||||
name: "TaskListItem",
|
||||
props: ["task"],
|
||||
computed: {
|
||||
label() {
|
||||
|
||||
const count = this.task.progress.count;
|
||||
const done = this.task.progress.done;
|
||||
|
||||
return `<span>${done}/${count}</span>`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
killTask(taskId) {
|
||||
sist2AdminApi.killTask(taskId).then(() => {
|
||||
this.$bvToast.toast(this.$t("killConfirmation"), {
|
||||
title: this.$t("killConfirmationTitle"),
|
||||
variant: "success",
|
||||
toaster: "b-toaster-bottom-right"
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.shrink {
|
||||
flex-grow: inherit;
|
||||
}
|
||||
</style>
|
||||
91
sist2-admin/frontend/src/components/WebOptions.vue
Normal file
91
sist2-admin/frontend/src/components/WebOptions.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div>
|
||||
<label>{{ $t("webOptions.esUrl") }}</label>
|
||||
<b-alert :variant="esTestOk ? 'success' : 'danger'" :show="showEsTestAlert" class="mt-1">
|
||||
{{ esTestMessage }}
|
||||
</b-alert>
|
||||
|
||||
<b-input-group>
|
||||
<b-form-input v-model="options.es_url" @change="update()"></b-form-input>
|
||||
<b-input-group-append>
|
||||
<b-button variant="outline-primary" @click="testEs()">{{ $t("test") }}</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
|
||||
<b-form-checkbox v-model="options.es_insecure_ssl" :disabled="!this.options.es_url.startsWith('https')" @change="update()">
|
||||
{{ $t("webOptions.esInsecure") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<label>{{ $t("webOptions.esIndex") }}</label>
|
||||
<b-form-input v-model="options.es_index" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("webOptions.lang") }}</label>
|
||||
<b-form-select v-model="options.lang" :options="['en', 'fr', 'zh-CN']" @change="update()"></b-form-select>
|
||||
|
||||
<label>{{ $t("webOptions.bind") }}</label>
|
||||
<b-form-input v-model="options.bind" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("webOptions.tagline") }}</label>
|
||||
<b-form-textarea v-model="options.tagline" @change="update()"></b-form-textarea>
|
||||
|
||||
<label>{{ $t("webOptions.auth") }}</label>
|
||||
<b-form-input v-model="options.auth" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("webOptions.tagAuth") }}</label>
|
||||
<b-form-input v-model="options.tag_auth" @change="update()"></b-form-input>
|
||||
|
||||
<br>
|
||||
<h5>Auth0 options</h5>
|
||||
<label>{{ $t("webOptions.auth0Audience") }}</label>
|
||||
<b-form-input v-model="options.auth0_audience" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("webOptions.auth0Domain") }}</label>
|
||||
<b-form-input v-model="options.auth0_domain" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("webOptions.auth0ClientId") }}</label>
|
||||
<b-form-input v-model="options.auth0_client_id" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("webOptions.auth0PublicKey") }}</label>
|
||||
<b-textarea rows="10" v-model="options.auth0_public_key" @change="update()"></b-textarea>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import sist2AdminApi from "@/Sist2AdminApi";
|
||||
|
||||
export default {
|
||||
name: "WebOptions",
|
||||
props: ["options", "frontendName"],
|
||||
data() {
|
||||
return {
|
||||
showEsTestAlert: false,
|
||||
esTestOk: false,
|
||||
esTestMessage: "",
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
if (!this.options.es_url.startsWith("https")) {
|
||||
this.options.es_insecure_ssl = false;
|
||||
}
|
||||
|
||||
this.$emit("change", this.options);
|
||||
},
|
||||
testEs() {
|
||||
sist2AdminApi.pingEs(this.options.es_url, this.options.es_insecure_ssl).then((resp) => {
|
||||
this.showEsTestAlert = true;
|
||||
this.esTestOk = resp.data.ok;
|
||||
this.esTestMessage = resp.data.message;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
40
sist2-admin/frontend/src/components/icons/Sist2Icon.vue
Normal file
40
sist2-admin/frontend/src/components/icons/Sist2Icon.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="27.868069mm"
|
||||
height="7.6446671mm"
|
||||
viewBox="0 0 27.868069 7.6446671"
|
||||
>
|
||||
<g transform="translate(-4.5018313,-4.1849793)">
|
||||
<g
|
||||
style="fill: currentColor;fill-opacity:1;stroke:none;stroke-width:0.26458332">
|
||||
<path
|
||||
d="m 6.3153296,11.829646 q -0.7717014,0 -1.8134983,-0.337619 v -0.916395 q 1.0128581,0.511252 1.803852,0.511252 0.5643067,0 0.901926,-0.236334 0.3376194,-0.236333 0.3376194,-0.63183 0,-0.3424428 -0.2845649,-0.5498376 Q 6.980922,9.4566645 6.3635609,9.3264399 L 5.9921796,9.2492698 Q 5.2301245,9.0949295 4.8732126,8.7428407 4.5211238,8.3859288 4.5211238,7.7733908 q 0,-0.7765245 0.5305447,-1.1961372 0.5305447,-0.4196126 1.5096409,-0.4196126 0.829579,0 1.6061036,0.3183268 V 7.3441319 Q 7.4101809,6.9004036 6.5854251,6.9004036 q -1.1671984,0 -1.1671984,0.7958171 0,0.2604492 0.1012858,0.4147895 0.1012858,0.1495171 0.3858507,0.2556261 0.2845649,0.1012858 0.8392253,0.2122179 l 0.3569119,0.067524 q 1.3408312,0.2652724 1.3408312,1.4614098 0,0.80064 -0.5691298,1.263661 -0.5691298,0.458197 -1.5578722,0.458197 z"
|
||||
style="stroke-width:0.26458332"
|
||||
/>
|
||||
<path
|
||||
d="m 11.943927,5.3087694 q -0.144694,0 -0.144694,-0.144694 V 4.3296733 q 0,-0.144694 0.144694,-0.144694 h 0.694531 q 0.144694,0 0.144694,0.144694 v 0.8344021 q 0,0.144694 -0.144694,0.144694 z M 13.5645,11.728361 q -0.795817,0 -1.234722,-0.511253 -0.434082,-0.516075 -0.434082,-1.4469398 V 6.9823969 H 10.714028 V 6.2878656 h 2.069124 v 3.4823026 q 0,0.5884228 0.221864,0.8971028 0.221865,0.308681 0.6463,0.308681 h 1.036974 v 0.752409 z"
|
||||
style="stroke-width:0.26458332"
|
||||
/>
|
||||
<path
|
||||
d="m 18.209178,11.829646 q -0.771701,0 -1.813498,-0.337619 v -0.916395 q 1.012858,0.511252 1.803852,0.511252 0.564306,0 0.901926,-0.236334 0.337619,-0.236333 0.337619,-0.63183 0,-0.3424428 -0.284565,-0.5498376 Q 18.87477,9.4566645 18.257409,9.3264399 l -0.371381,-0.07717 Q 17.123973,9.0949295 16.767061,8.7428407 16.414972,8.3859288 16.414972,7.7733908 q 0,-0.7765245 0.530545,-1.1961372 0.530545,-0.4196126 1.509641,-0.4196126 0.829579,0 1.606103,0.3183268 v 0.8681641 q -0.757232,-0.4437283 -1.581988,-0.4437283 -1.167198,0 -1.167198,0.7958171 0,0.2604492 0.101286,0.4147895 0.101286,0.1495171 0.385851,0.2556261 0.284565,0.1012858 0.839225,0.2122179 l 0.356912,0.067524 q 1.340831,0.2652724 1.340831,1.4614098 0,0.80064 -0.56913,1.263661 -0.56913,0.458197 -1.557872,0.458197 z"
|
||||
style="stroke-width:0.26458332"
|
||||
/>
|
||||
<path
|
||||
d="m 25.207545,11.709068 q -0.993565,0 -1.408355,-0.40032 -0.409966,-0.405143 -0.409966,-1.3794164 V 6.9775737 H 21.947107 V 6.2878656 h 1.442117 V 4.8746874 l 0.887457,-0.3858507 v 1.7990289 h 2.016069 v 0.6897081 h -2.016069 v 2.9517579 q 0,0.5932454 0.226687,0.8344024 0.226687,0.236333 0.790994,0.236333 h 0.998388 v 0.709001 z"
|
||||
style="stroke-width:0.26458332"
|
||||
/>
|
||||
<path
|
||||
d="m 27.995317,11.043476 q 0,-0.178456 0.120578,-0.299035 0.274919,-0.289388 0.651123,-0.684885 0.376205,-0.4003199 0.805464,-0.8681638 0.327973,-0.356912 0.491959,-0.5353679 0.16881,-0.1832791 0.255626,-0.2845649 0.09164,-0.1012858 0.178456,-0.2073948 0.255626,-0.3086805 0.405144,-0.5257215 0.15434,-0.2170411 0.250803,-0.4292589 0.168809,-0.3762045 0.168809,-0.7524089 0,-0.5980686 -0.352089,-0.935688 -0.356911,-0.3424425 -0.979096,-0.3424425 -0.863341,0 -1.938899,0.6414768 V 4.8361023 q 0.491959,-0.2363335 0.979096,-0.3569119 0.47749,-0.1205783 0.945334,-0.1205783 0.501606,0 0.940511,0.1350477 0.438905,0.1350478 0.766878,0.4244358 0.289388,0.2556261 0.463021,0.6270074 0.173633,0.3665582 0.173633,0.829579 0,0.4726671 -0.212218,0.9501574 -0.106109,0.2411567 -0.274919,0.4726671 -0.163986,0.2266873 -0.424435,0.540191 Q 31.270225,8.501684 31.077299,8.718725 30.884374,8.9357661 30.628748,9.2106847 30.445469,9.4084332 30.286305,9.5675966 30.131965,9.72676 29.958332,9.9003928 29.7847,10.069203 29.558012,10.300713 29.336148,10.5274 29.012998,10.869843 h 3.356901 v 0.819932 h -4.374582 z"
|
||||
style="stroke-width:0.26458332"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Sist2Icon"
|
||||
}
|
||||
</script>
|
||||
114
sist2-admin/frontend/src/i18n/messages.js
Normal file
114
sist2-admin/frontend/src/i18n/messages.js
Normal file
@@ -0,0 +1,114 @@
|
||||
export default {
|
||||
en: {
|
||||
start: "Start",
|
||||
stop: "Stop",
|
||||
go: "Go",
|
||||
online: "online",
|
||||
offline: "offline",
|
||||
delete: "Delete",
|
||||
runNow: "Index now",
|
||||
create: "Create",
|
||||
test: "Test",
|
||||
|
||||
jobTitle: "job configuration",
|
||||
tasks: "Tasks",
|
||||
runningTasks: "Running tasks",
|
||||
frontends: "Frontends",
|
||||
jobDisabled: "There is no valid index for this job",
|
||||
status: "Status",
|
||||
|
||||
taskHistory: "Task history",
|
||||
taskName: "Task name",
|
||||
taskStarted: "Started",
|
||||
taskDuration: "Duration",
|
||||
taskStatus: "Status",
|
||||
logs: "Logs",
|
||||
kill: "Kill",
|
||||
killConfirmation: "SIGTERM signal sent to sist2 process",
|
||||
killConfirmationTitle: "Confirmation",
|
||||
follow: "Follow",
|
||||
wholeFile: "Whole file",
|
||||
logLevel: "Log level",
|
||||
logMode: "Follow mode",
|
||||
logFile: "Reading log file",
|
||||
|
||||
jobs: "Jobs",
|
||||
newJobName: "New job name",
|
||||
newJobHelp: "Create a new job to get started!",
|
||||
newFrontendName: "New frontend name",
|
||||
scanned: "last scan",
|
||||
autoStart: "Start automatically",
|
||||
|
||||
runJobConfirmationTitle: "Task queued",
|
||||
runJobConfirmation: "Check the Tasks page to monitor the status.",
|
||||
|
||||
extraQueryArgs: "Extra query arguments when launching from sist2-admin",
|
||||
customUrl: "Custom URL when launching from sist2-admin",
|
||||
|
||||
selectJobs: "Select jobs",
|
||||
webOptions: {
|
||||
title: "Web options",
|
||||
esUrl: "Elasticsearch URL",
|
||||
esIndex: "Elasticsearch index name",
|
||||
esInsecure: "Do not verify SSL connections to Elasticsearch.",
|
||||
lang: "UI Language",
|
||||
bind: "Listen address",
|
||||
tagline: "Tagline in navbar",
|
||||
auth: "Basic auth in user:password format",
|
||||
tagAuth: "Basic auth in user:password format for tagging",
|
||||
auth0Audience: "Auth0 audience",
|
||||
auth0Domain: "Auth0 domain",
|
||||
auth0ClientId: "Auth0 client ID",
|
||||
auth0PublicKey: "Auth0 public key",
|
||||
},
|
||||
scanOptions: {
|
||||
title: "Scanning options",
|
||||
path: "Path",
|
||||
threads: "Number of threads",
|
||||
memThrottle: "Total memory threshold in MiB for scan throttling",
|
||||
thumbnailQuality: "Thumbnail quality, on a scale of 2 to 32, 2 being the best",
|
||||
thumbnailCount: "Number of thumbnails to generate. Set a value > 1 to create video previews, set to 0 to disable thumbnails.",
|
||||
thumbnailSize: "Thumbnail size, in pixels",
|
||||
contentSize: "Number of bytes to be extracted from text documents. Set to 0 to disable",
|
||||
rewriteUrl: "Serve files from this url instead of from disk",
|
||||
depth: "Scan up to this many subdirectories deep",
|
||||
archive: "Archive file mode",
|
||||
archivePassphrase: "Passphrase for encrypted archive files",
|
||||
ocrLang: "Tesseract language",
|
||||
ocrLangAlert: "You must select at least one language",
|
||||
ocrEbooks: "Enable OCR'ing of ebook files",
|
||||
ocrImages: "Enable OCR'ing of image files",
|
||||
exclude: "Files that match this regex will not be scanned",
|
||||
excludePlaceholder: "Exclude",
|
||||
fast: "Only index file names & mime type",
|
||||
checksums: "Calculate file checksums when scanning",
|
||||
readSubtitles: "Read subtitles from media files",
|
||||
memBuffer: "Maximum memory buffer size per thread in MiB for files inside archives",
|
||||
treemapThreshold: "Relative size threshold for treemap",
|
||||
optimizeIndex: "Defragment index file after scan to reduce its file size."
|
||||
},
|
||||
indexOptions: {
|
||||
title: "Indexing options",
|
||||
threads: "Number of threads",
|
||||
esUrl: "Elasticsearch URL",
|
||||
esIndex: "Elasticsearch index name",
|
||||
esInsecure: "Do not verify SSL connections to Elasticsearch.",
|
||||
batchSize: "Index batch size",
|
||||
script: "User script"
|
||||
},
|
||||
jobOptions: {
|
||||
title: "Job options",
|
||||
cron: "Job schedule",
|
||||
scheduleEnabled: "Enable scheduled re-scan",
|
||||
noJobAvailable: "No jobs available.",
|
||||
desktopNotifications: "Desktop notifications"
|
||||
},
|
||||
frontendOptions: {
|
||||
title: "Frontend options",
|
||||
noJobSelectedWarning: "You must select at least one job to start this frontend"
|
||||
},
|
||||
notifications: {
|
||||
indexCompleted: "Task completed for [$JOB$]"
|
||||
}
|
||||
}
|
||||
}
|
||||
31
sist2-admin/frontend/src/main.js
Normal file
31
sist2-admin/frontend/src/main.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
|
||||
|
||||
import "bootstrap/dist/css/bootstrap.min.css"
|
||||
import "bootstrap-vue/dist/bootstrap-vue.min.css"
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
Vue.use(IconsPlugin);
|
||||
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import VueI18n from "vue-i18n";
|
||||
import messages from "@/i18n/messages";
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
const i18n = new VueI18n({
|
||||
locale: "en",
|
||||
messages: messages
|
||||
});
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
i18n,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
45
sist2-admin/frontend/src/router/index.js
Normal file
45
sist2-admin/frontend/src/router/index.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Home from '../views/Home.vue'
|
||||
import Job from "@/views/Job";
|
||||
import Tasks from "@/views/Tasks";
|
||||
import Frontend from "@/views/Frontend";
|
||||
import Tail from "@/views/Tail";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
name: "Home",
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: "/job/:name",
|
||||
name: "Job",
|
||||
component: Job
|
||||
},
|
||||
{
|
||||
path: "/task/",
|
||||
name: "Tasks",
|
||||
component: Tasks
|
||||
},
|
||||
{
|
||||
path: "/frontend/:name",
|
||||
name: "Frontend",
|
||||
component: Frontend
|
||||
},
|
||||
{
|
||||
path: "/log/:taskId",
|
||||
name: "Tail",
|
||||
component: Tail
|
||||
},
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: "hash",
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
63
sist2-admin/frontend/src/store/index.js
Normal file
63
sist2-admin/frontend/src/store/index.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
function saveBrowserSettings(state) {
|
||||
const settings = {
|
||||
jobDesktopNotificationMap: state.jobDesktopNotificationMap
|
||||
};
|
||||
localStorage.setItem("sist2-admin-settings", JSON.stringify(settings));
|
||||
|
||||
console.log("SAVED");
|
||||
console.log(settings);
|
||||
}
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
sist2AdminInfo: null,
|
||||
jobDesktopNotificationMap: {}
|
||||
},
|
||||
mutations: {
|
||||
setSist2AdminInfo: (state, payload) => state.sist2AdminInfo = payload,
|
||||
setJobDesktopNotificationMap: (state, payload) => state.jobDesktopNotificationMap = payload,
|
||||
},
|
||||
actions: {
|
||||
notify: async ({state}, notification) => {
|
||||
|
||||
if (!state.jobDesktopNotificationMap[notification.job]) {
|
||||
console.log("pass");
|
||||
return;
|
||||
}
|
||||
|
||||
new Notification(notification.messageString.replace("$JOB$", notification.job));
|
||||
},
|
||||
setJobDesktopNotification: async ({state}, {job, enabled}) => {
|
||||
|
||||
if (enabled === true) {
|
||||
const permission = await Notification.requestPermission()
|
||||
|
||||
if (permission !== "granted") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
state.jobDesktopNotificationMap[job] = enabled;
|
||||
saveBrowserSettings(state);
|
||||
|
||||
return true;
|
||||
},
|
||||
loadBrowserSettings({commit}) {
|
||||
const settingString = localStorage.getItem("sist2-admin-settings");
|
||||
|
||||
if (!settingString) {
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = JSON.parse(settingString);
|
||||
|
||||
commit("setJobDesktopNotificationMap", settings["jobDesktopNotificationMap"]);
|
||||
}
|
||||
},
|
||||
modules: {}
|
||||
})
|
||||
8
sist2-admin/frontend/src/util.js
Normal file
8
sist2-admin/frontend/src/util.js
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
export function formatBindAddress(address) {
|
||||
if (address.startsWith("0.0.0.0")) {
|
||||
return address.slice("0.0.0.0".length)
|
||||
}
|
||||
|
||||
return address
|
||||
}
|
||||
129
sist2-admin/frontend/src/views/Frontend.vue
Normal file
129
sist2-admin/frontend/src/views/Frontend.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<b-card>
|
||||
<b-card-title>
|
||||
{{ name }}
|
||||
<small style="vertical-align: top">
|
||||
<b-badge v-if="!loading && frontend.running" variant="success">{{ $t("online") }}</b-badge>
|
||||
<b-badge v-else-if="!loading" variant="secondary">{{ $t("offline") }}</b-badge>
|
||||
</small>
|
||||
</b-card-title>
|
||||
|
||||
<div class="mb-3" v-if="!loading">
|
||||
<b-button class="mr-1" :disabled="frontend.running || !valid" variant="success" @click="start()">{{
|
||||
$t("start")
|
||||
}}
|
||||
</b-button>
|
||||
<b-button class="mr-1" :disabled="!frontend.running" variant="danger" @click="stop()">{{
|
||||
$t("stop")
|
||||
}}
|
||||
</b-button>
|
||||
<b-button class="mr-1" :disabled="!frontend.running" variant="primary" :href="frontendUrl" target="_blank">
|
||||
{{ $t("go") }}
|
||||
</b-button>
|
||||
<b-button variant="danger" @click="deleteFrontend()">{{ $t("delete") }}</b-button>
|
||||
</div>
|
||||
|
||||
|
||||
<b-progress v-if="loading" striped animated value="100"></b-progress>
|
||||
<b-card-body v-else>
|
||||
|
||||
<h4>{{ $t("frontendOptions.title") }}</h4>
|
||||
<b-card>
|
||||
<b-form-checkbox v-model="frontend.auto_start" @change="update()">
|
||||
{{ $t("autoStart") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<label>{{ $t("extraQueryArgs") }}</label>
|
||||
<b-form-input v-model="frontend.extra_query_args" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("customUrl") }}</label>
|
||||
<b-form-input v-model="frontend.custom_url" @change="update()" placeholder="http://"></b-form-input>
|
||||
|
||||
<br/>
|
||||
|
||||
<b-alert v-if="!valid" variant="warning" show>{{ $t("frontendOptions.noJobSelectedWarning") }}</b-alert>
|
||||
|
||||
<JobCheckboxGroup :frontend="frontend" @input="update()"></JobCheckboxGroup>
|
||||
</b-card>
|
||||
|
||||
<br/>
|
||||
|
||||
<h4>{{ $t("webOptions.title") }}</h4>
|
||||
<b-card>
|
||||
<WebOptions :options="frontend.web_options" :frontend-name="$route.params.name" @change="update()"></WebOptions>
|
||||
</b-card>
|
||||
</b-card-body>
|
||||
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Sist2AdminApi from "@/Sist2AdminApi";
|
||||
import JobCheckboxGroup from "@/components/JobCheckboxGroup";
|
||||
import WebOptions from "@/components/WebOptions";
|
||||
|
||||
export default {
|
||||
name: 'Frontend',
|
||||
components: {JobCheckboxGroup, WebOptions},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
frontend: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
valid() {
|
||||
return !this.loading && this.frontend.jobs.length > 0;
|
||||
},
|
||||
frontendUrl() {
|
||||
if (this.frontend.custom_url) {
|
||||
return this.frontend.custom_url + this.args;
|
||||
}
|
||||
|
||||
if (this.frontend.web_options.bind.startsWith("0.0.0.0")) {
|
||||
return window.location.protocol + "//" + window.location.hostname + ":" + this.port + this.args;
|
||||
}
|
||||
|
||||
return window.location.protocol + "//" + this.frontend.web_options.bind + this.args;
|
||||
},
|
||||
name() {
|
||||
return this.$route.params.name;
|
||||
},
|
||||
port() {
|
||||
return this.frontend.web_options.bind.split(":")[1]
|
||||
},
|
||||
args() {
|
||||
const args = this.frontend.extra_query_args;
|
||||
if (args !== "") {
|
||||
return "#" + (args.startsWith("?") ? (args) : ("?" + args));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
Sist2AdminApi.getFrontend(this.name).then(resp => {
|
||||
this.frontend = resp.data;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
start() {
|
||||
this.frontend.running = true;
|
||||
Sist2AdminApi.startFrontend(this.name)
|
||||
},
|
||||
stop() {
|
||||
this.frontend.running = false;
|
||||
Sist2AdminApi.stopFrontend(this.name)
|
||||
},
|
||||
deleteFrontend() {
|
||||
Sist2AdminApi.deleteFrontend(this.name).then(() => {
|
||||
this.$router.push("/frontends");
|
||||
});
|
||||
},
|
||||
update() {
|
||||
Sist2AdminApi.updateFrontend(this.name, this.frontend);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
122
sist2-admin/frontend/src/views/Home.vue
Normal file
122
sist2-admin/frontend/src/views/Home.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-card>
|
||||
<b-card-title>{{ $t("jobs") }}</b-card-title>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-input id="new-job" v-model="newJobName" :placeholder="$t('newJobName')"></b-input>
|
||||
<b-popover
|
||||
:show.sync="showHelp"
|
||||
target="new-job"
|
||||
placement="top"
|
||||
triggers="manual"
|
||||
variant="primary"
|
||||
:content="$t('newJobHelp')"
|
||||
></b-popover>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-button variant="primary" @click="createJob()" :disabled="!jobNameValid(newJobName)">{{ $t("create") }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<hr/>
|
||||
|
||||
<b-progress v-if="jobsLoading" striped animated value="100"></b-progress>
|
||||
<b-list-group v-else>
|
||||
<JobListItem v-for="job in jobs" :key="job.name" :job="job"></JobListItem>
|
||||
</b-list-group>
|
||||
</b-card>
|
||||
|
||||
<br/>
|
||||
|
||||
<b-card>
|
||||
|
||||
<b-card-title>{{ $t("frontends") }}</b-card-title>
|
||||
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-input v-model="newFrontendName" :placeholder="$t('newFrontendName')"></b-input>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-button variant="primary" @click="createFrontend()" :disabled="!frontendNameValid(newFrontendName)">
|
||||
{{ $t("create") }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<hr/>
|
||||
|
||||
<b-progress v-if="frontendsLoading" striped animated value="100"></b-progress>
|
||||
<b-list-group v-else>
|
||||
<FrontendListItem v-for="frontend in frontends"
|
||||
:key="frontend.name" :frontend="frontend"></FrontendListItem>
|
||||
</b-list-group>
|
||||
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import JobListItem from "@/components/JobListItem";
|
||||
import {formatBindAddress} from "@/util";
|
||||
import Sist2AdminApi from "@/Sist2AdminApi";
|
||||
import FrontendListItem from "@/components/FrontendListItem";
|
||||
|
||||
export default {
|
||||
name: "Jobs",
|
||||
components: {JobListItem, FrontendListItem},
|
||||
data() {
|
||||
return {
|
||||
jobsLoading: true,
|
||||
newJobName: "",
|
||||
jobs: [],
|
||||
|
||||
frontendsLoading: true,
|
||||
frontends: [],
|
||||
formatBindAddress,
|
||||
newFrontendName: "",
|
||||
|
||||
showHelp: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loading = true;
|
||||
this.reload();
|
||||
},
|
||||
methods: {
|
||||
jobNameValid(name) {
|
||||
if (this.jobs.some(job => job.name === name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /^[a-zA-Z0-9-_,.; ]+$/.test(name);
|
||||
},
|
||||
frontendNameValid(name) {
|
||||
if (this.frontends.some(job => job.name === name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /^[a-zA-Z0-9-_,.; ]+$/.test(name);
|
||||
},
|
||||
reload() {
|
||||
Sist2AdminApi.getJobs().then(resp => {
|
||||
this.jobs = resp.data;
|
||||
this.jobsLoading = false;
|
||||
|
||||
this.showHelp = this.jobs.length === 0;
|
||||
});
|
||||
Sist2AdminApi.getFrontends().then(resp => {
|
||||
this.frontends = resp.data;
|
||||
this.frontendsLoading = false;
|
||||
});
|
||||
},
|
||||
createJob() {
|
||||
Sist2AdminApi.createJob(this.newJobName).then(this.reload);
|
||||
},
|
||||
createFrontend() {
|
||||
Sist2AdminApi.createFrontend(this.newFrontendName).then(this.reload)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
92
sist2-admin/frontend/src/views/Job.vue
Normal file
92
sist2-admin/frontend/src/views/Job.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<b-card>
|
||||
<b-card-title>
|
||||
[{{ getName() }}]
|
||||
{{ $t("jobTitle") }}
|
||||
</b-card-title>
|
||||
|
||||
<div class="mb-3">
|
||||
<b-button class="mr-1" variant="primary" @click="runJob()">{{ $t("runNow") }}</b-button>
|
||||
<b-button variant="danger" @click="deleteJob()">{{ $t("delete") }}</b-button>
|
||||
</div>
|
||||
|
||||
<div v-if="job">
|
||||
{{ $t("status") }}: <code>{{ job.status }}</code>
|
||||
</div>
|
||||
|
||||
<b-progress v-if="loading" striped animated value="100"></b-progress>
|
||||
<b-card-body v-else>
|
||||
|
||||
<h4>{{ $t("jobOptions.title") }}</h4>
|
||||
<b-card>
|
||||
<JobOptions :job="job" @change="update"></JobOptions>
|
||||
</b-card>
|
||||
|
||||
<br/>
|
||||
|
||||
<h4>{{ $t("scanOptions.title") }}</h4>
|
||||
<b-card>
|
||||
<ScanOptions :options="job.scan_options" @change="update()"></ScanOptions>
|
||||
</b-card>
|
||||
|
||||
<br/>
|
||||
|
||||
<h4>{{ $t("indexOptions.title") }}</h4>
|
||||
<b-card>
|
||||
<IndexOptions :options="job.index_options" @change="update()"></IndexOptions>
|
||||
</b-card>
|
||||
|
||||
</b-card-body>
|
||||
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScanOptions from "@/components/ScanOptions";
|
||||
import Sist2AdminApi from "@/Sist2AdminApi";
|
||||
import IndexOptions from "@/components/IndexOptions";
|
||||
import JobOptions from "@/components/JobOptions";
|
||||
|
||||
export default {
|
||||
name: "Job",
|
||||
components: {
|
||||
IndexOptions,
|
||||
ScanOptions,
|
||||
JobOptions
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
job: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getName() {
|
||||
return this.$route.params.name;
|
||||
},
|
||||
update() {
|
||||
Sist2AdminApi.updateJob(this.getName(), this.job);
|
||||
},
|
||||
runJob() {
|
||||
Sist2AdminApi.runJob(this.getName()).then(() => {
|
||||
this.$bvToast.toast(this.$t("runJobConfirmation"), {
|
||||
title: this.$t("runJobConfirmationTitle"),
|
||||
variant: "success",
|
||||
toaster: "b-toaster-bottom-right"
|
||||
});
|
||||
});
|
||||
},
|
||||
deleteJob() {
|
||||
Sist2AdminApi.deleteJob(this.getName()).then(() => {
|
||||
this.$router.push("/");
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
Sist2AdminApi.getJob(this.getName()).then(resp => {
|
||||
this.loading = false;
|
||||
this.job = resp.data;
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
168
sist2-admin/frontend/src/views/Tail.vue
Normal file
168
sist2-admin/frontend/src/views/Tail.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<b-card>
|
||||
<b-card-body>
|
||||
|
||||
<h4 class="mb-3">{{ taskId }} {{ $t("logs") }}</h4>
|
||||
|
||||
<div v-if="$store.state.sist2AdminInfo">
|
||||
{{ $t("logFile") }}
|
||||
<code>{{ $store.state.sist2AdminInfo.logs_folder }}/sist2-{{ taskId }}.log</code>
|
||||
<br/>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<b-row>
|
||||
<b-col>
|
||||
<span>{{ $t("logLevel") }}</span>
|
||||
<b-select :options="levels.slice(0, -1)" v-model="logLevel" @input="connect()"></b-select>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<span>{{ $t("logMode") }}</span>
|
||||
<b-select :options="modeOptions" v-model="mode" @input="connect()"></b-select>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<div id="log-tail-output" class="mt-3 ml-1"></div>
|
||||
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "Tail",
|
||||
data() {
|
||||
return {
|
||||
logLevel: "DEBUG",
|
||||
levels: ["DEBUG", "INFO", "WARNING", "ERROR", "ADMIN", "FATAL"],
|
||||
socket: null,
|
||||
mode: "follow",
|
||||
modeOptions: [
|
||||
{
|
||||
"text": this.$t('follow'),
|
||||
"value": "follow"
|
||||
},
|
||||
{
|
||||
"text": this.$t('wholeFile'),
|
||||
"value": "wholeFile"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
taskId: function () {
|
||||
return this.$route.params.taskId;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
connect() {
|
||||
let lineCount = 0;
|
||||
const outputElem = document.getElementById("log-tail-output")
|
||||
outputElem.replaceChildren();
|
||||
if (this.socket !== null) {
|
||||
this.socket.close();
|
||||
}
|
||||
|
||||
const n = this.mode === "follow" ? 32 : 9999999999;
|
||||
this.socket = new WebSocket(`ws://${window.location.host}/log/${this.taskId}?n=${n}`);
|
||||
this.socket.onopen = () => {
|
||||
this.socket.send("Hello from client");
|
||||
}
|
||||
|
||||
this.socket.onmessage = e => {
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(e.data);
|
||||
} catch {
|
||||
console.error(e.data)
|
||||
return;
|
||||
}
|
||||
|
||||
if ("ping" in message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.level === undefined) {
|
||||
|
||||
if ("stderr" in message) {
|
||||
message.level = "ERROR";
|
||||
message.message = message["stderr"];
|
||||
} else {
|
||||
message.level = "ADMIN";
|
||||
message.message = message["sist2-admin"];
|
||||
}
|
||||
message.datetime = ""
|
||||
message.filepath = ""
|
||||
}
|
||||
|
||||
if (this.levels.indexOf(message.level) < this.levels.indexOf(this.logLevel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logLine = `${message.datetime} [${message.level} ${message.filepath}] ${message.message}`;
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.setAttribute("class", message.level);
|
||||
span.appendChild(document.createTextNode(logLine));
|
||||
|
||||
outputElem.appendChild(span);
|
||||
lineCount += 1;
|
||||
|
||||
if (this.mode === "follow" && lineCount >= n) {
|
||||
outputElem.firstChild.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.connect()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#log-tail-output span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
span.DEBUG {
|
||||
color: #9E9E9E;
|
||||
}
|
||||
|
||||
span.WARNING {
|
||||
color: #FFB300;
|
||||
}
|
||||
|
||||
span.INFO {
|
||||
color: #039BE5;
|
||||
}
|
||||
|
||||
span.ERROR {
|
||||
color: #F4511E;
|
||||
}
|
||||
|
||||
span.FATAL {
|
||||
color: #F4511E;
|
||||
}
|
||||
|
||||
span.ADMIN {
|
||||
color: #ee05ff;
|
||||
}
|
||||
|
||||
|
||||
#log-tail-output {
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
|
||||
padding: 6px;
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin: 3px;
|
||||
white-space: pre;
|
||||
color: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
150
sist2-admin/frontend/src/views/Tasks.vue
Normal file
150
sist2-admin/frontend/src/views/Tasks.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<b-card v-if="tasks.length > 0">
|
||||
<h2>{{ $t("runningTasks") }}</h2>
|
||||
<b-list-group>
|
||||
<TaskListItem v-for="task in tasks" :key="task.id" :task="task"></TaskListItem>
|
||||
</b-list-group>
|
||||
</b-card>
|
||||
|
||||
<b-card class="mt-4">
|
||||
|
||||
<b-card-title>{{ $t("taskHistory") }}</b-card-title>
|
||||
|
||||
<br/>
|
||||
|
||||
<b-table
|
||||
id="task-history"
|
||||
:items="historyItems"
|
||||
:fields="historyFields"
|
||||
:current-page="historyCurrentPage"
|
||||
:tbody-tr-class="rowClass"
|
||||
:per-page="10"
|
||||
>
|
||||
<template #cell(logs)="data">
|
||||
<router-link :to="`/log/${data.item.logs}`">{{ $t("logs") }}</router-link>
|
||||
</template>
|
||||
|
||||
</b-table>
|
||||
|
||||
<b-pagination limit="20" v-model="historyCurrentPage" :total-rows="historyItems.length"
|
||||
:per-page="10"></b-pagination>
|
||||
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TaskListItem from "@/components/TaskListItem";
|
||||
import Sist2AdminApi from "@/Sist2AdminApi";
|
||||
import moment from "moment";
|
||||
|
||||
const DAY = 3600 * 24;
|
||||
const HOUR = 3600;
|
||||
const MINUTE = 60;
|
||||
|
||||
function humanDuration(sec_num) {
|
||||
sec_num = sec_num / 1000;
|
||||
const days = Math.floor(sec_num / DAY);
|
||||
sec_num -= days * DAY;
|
||||
const hours = Math.floor(sec_num / HOUR);
|
||||
sec_num -= hours * HOUR;
|
||||
const minutes = Math.floor(sec_num / MINUTE);
|
||||
sec_num -= minutes * MINUTE;
|
||||
const seconds = Math.floor(sec_num);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days} days ${hours}h ${minutes}m ${seconds}s`;
|
||||
}
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${seconds}s`;
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${seconds}s`;
|
||||
}
|
||||
|
||||
if (seconds > 0) {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
return "<0s";
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Tasks',
|
||||
components: {TaskListItem},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
tasks: [],
|
||||
taskHistory: [],
|
||||
timerId: null,
|
||||
historyFields: [
|
||||
{key: "name", label: this.$t("taskName")},
|
||||
{key: "time", label: this.$t("taskStarted")},
|
||||
{key: "duration", label: this.$t("taskDuration")},
|
||||
{key: "status", label: this.$t("taskStatus")},
|
||||
{key: "logs", label: this.$t("logs")},
|
||||
],
|
||||
historyCurrentPage: 1,
|
||||
historyItems: []
|
||||
}
|
||||
},
|
||||
props: {
|
||||
msg: String
|
||||
},
|
||||
mounted() {
|
||||
this.loading = true;
|
||||
this.update().then(() => this.loading = false);
|
||||
|
||||
this.timerId = window.setInterval(this.update, 1000);
|
||||
this.updateHistory();
|
||||
},
|
||||
destroyed() {
|
||||
if (this.timerId) {
|
||||
window.clearInterval(this.timerId);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
rowClass(row) {
|
||||
if (row.status === "failed") {
|
||||
return "table-danger";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
updateHistory() {
|
||||
Sist2AdminApi.getTaskHistory().then(resp => {
|
||||
this.historyItems = resp.data.map(row => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
duration: this.taskDuration(row),
|
||||
time: moment(row.started).format("dd, MMM Do YYYY, HH:mm:ss"),
|
||||
logs: row.id,
|
||||
status: row.return_code === 0 ? "ok" : "failed"
|
||||
}));
|
||||
});
|
||||
},
|
||||
update() {
|
||||
return Sist2AdminApi.getTasks().then(resp => {
|
||||
this.tasks = resp.data;
|
||||
})
|
||||
},
|
||||
taskDuration(task) {
|
||||
const start = moment.utc(task.started);
|
||||
const end = moment.utc(task.ended);
|
||||
|
||||
return humanDuration(end.diff(start))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#task-history {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
5
sist2-admin/frontend/vue.config.js
Normal file
5
sist2-admin/frontend/vue.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
publicPath: "",
|
||||
filenameHashing: false,
|
||||
productionSourceMap: false,
|
||||
};
|
||||
5648
sist2-admin/frontend/yarn.lock
Normal file
5648
sist2-admin/frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
5
sist2-admin/requirements.txt
Normal file
5
sist2-admin/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
fastapi
|
||||
git+https://github.com/simon987/hexlib.git
|
||||
uvicorn
|
||||
websockets
|
||||
pycron
|
||||
390
sist2-admin/sist2_admin/app.py
Normal file
390
sist2-admin/sist2_admin/app.py
Normal file
@@ -0,0 +1,390 @@
|
||||
import asyncio
|
||||
import os
|
||||
import signal
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from hexlib.db import PersistentState
|
||||
from requests import ConnectionError
|
||||
from requests.exceptions import SSLError
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.responses import RedirectResponse
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from starlette.websockets import WebSocket
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
|
||||
import cron
|
||||
from config import LOG_FOLDER, logger, WEBSERVER_PORT, DATA_FOLDER, SIST2_BINARY
|
||||
from jobs import Sist2Job, Sist2ScanTask, TaskQueue, Sist2IndexTask, JobStatus
|
||||
from notifications import Subscribe, Notifications
|
||||
from sist2 import Sist2
|
||||
from state import migrate_v1_to_v2, RUNNING_FRONTENDS, TESSERACT_LANGS, DB_SCHEMA_VERSION
|
||||
from web import Sist2Frontend
|
||||
|
||||
sist2 = Sist2(SIST2_BINARY, DATA_FOLDER)
|
||||
db = PersistentState(dbfile=os.path.join(DATA_FOLDER, "state.db"))
|
||||
notifications = Notifications()
|
||||
task_queue = TaskQueue(sist2, db, notifications)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_credentials=True,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.mount("/ui/", StaticFiles(directory="./frontend/dist", html=True), name="static")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def home():
|
||||
return RedirectResponse("ui")
|
||||
|
||||
|
||||
@app.get("/api")
|
||||
async def api():
|
||||
return {
|
||||
"tesseract_langs": TESSERACT_LANGS,
|
||||
"logs_folder": LOG_FOLDER
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/job/{name:str}")
|
||||
async def get_job(name: str):
|
||||
job = db["jobs"][name]
|
||||
if not job:
|
||||
raise HTTPException(status_code=404)
|
||||
return job
|
||||
|
||||
|
||||
@app.get("/api/frontend/{name:str}")
|
||||
async def get_frontend(name: str):
|
||||
frontend = db["frontends"][name]
|
||||
frontend: Sist2Frontend
|
||||
if frontend:
|
||||
frontend.running = frontend.name in RUNNING_FRONTENDS
|
||||
return frontend
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
|
||||
@app.get("/api/job/")
|
||||
async def get_jobs():
|
||||
return list(db["jobs"])
|
||||
|
||||
|
||||
@app.put("/api/job/{name:str}")
|
||||
async def update_job(name: str, new_job: Sist2Job):
|
||||
# TODO: Check etag
|
||||
|
||||
new_job.last_modified = datetime.now()
|
||||
job = db["jobs"][name]
|
||||
if not job:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
args_that_trigger_full_scan = [
|
||||
"path",
|
||||
"thumbnail_count",
|
||||
"thumbnail_quality",
|
||||
"thumbnail_size",
|
||||
"content_size",
|
||||
"depth",
|
||||
"archive",
|
||||
"archive_passphrase",
|
||||
"ocr_lang",
|
||||
"ocr_images",
|
||||
"ocr_ebooks",
|
||||
"fast",
|
||||
"checksums",
|
||||
"read_subtitles",
|
||||
]
|
||||
for arg in args_that_trigger_full_scan:
|
||||
if getattr(new_job.scan_options, arg) != getattr(job.scan_options, arg):
|
||||
new_job.do_full_scan = True
|
||||
|
||||
db["jobs"][name] = new_job
|
||||
|
||||
|
||||
@app.put("/api/frontend/{name:str}")
|
||||
async def update_frontend(name: str, frontend: Sist2Frontend):
|
||||
db["frontends"][name] = frontend
|
||||
|
||||
# TODO: Check etag
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.get("/api/task/")
|
||||
async def get_tasks():
|
||||
return list(map(lambda t: t.json(), task_queue.tasks()))
|
||||
|
||||
|
||||
@app.get("/api/task/history")
|
||||
async def task_history():
|
||||
return list(db["task_done"].sql("ORDER BY started DESC"))
|
||||
|
||||
|
||||
@app.post("/api/task/{task_id:str}/kill")
|
||||
async def kill_job(task_id: str):
|
||||
return task_queue.kill_task(task_id)
|
||||
|
||||
|
||||
def _run_job(job: Sist2Job):
|
||||
job.last_modified = datetime.now()
|
||||
if job.status == JobStatus("created"):
|
||||
job.status = JobStatus("started")
|
||||
db["jobs"][job.name] = job
|
||||
|
||||
scan_task = Sist2ScanTask(job, f"Scan [{job.name}]")
|
||||
index_task = Sist2IndexTask(job, f"Index [{job.name}]", depends_on=scan_task)
|
||||
|
||||
task_queue.submit(scan_task)
|
||||
task_queue.submit(index_task)
|
||||
|
||||
|
||||
@app.get("/api/job/{name:str}/run")
|
||||
async def run_job(name: str):
|
||||
job = db["jobs"][name]
|
||||
if not job:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
_run_job(job)
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.delete("/api/job/{name:str}")
|
||||
async def delete_job(name: str):
|
||||
job = db["jobs"][name]
|
||||
if job:
|
||||
del db["jobs"][name]
|
||||
else:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
|
||||
@app.delete("/api/frontend/{name:str}")
|
||||
async def delete_frontend(name: str):
|
||||
if name in RUNNING_FRONTENDS:
|
||||
os.kill(RUNNING_FRONTENDS[name], signal.SIGTERM)
|
||||
del RUNNING_FRONTENDS[name]
|
||||
|
||||
frontend = db["frontends"][name]
|
||||
if frontend:
|
||||
del db["frontends"][name]
|
||||
else:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
|
||||
@app.post("/api/job/{name:str}")
|
||||
async def create_job(name: str):
|
||||
if db["jobs"][name]:
|
||||
raise ValueError("Job with the same name already exists")
|
||||
|
||||
job = Sist2Job.create_default(name)
|
||||
db["jobs"][name] = job
|
||||
|
||||
return job
|
||||
|
||||
|
||||
@app.post("/api/frontend/{name:str}")
|
||||
async def create_frontend(name: str):
|
||||
if db["frontends"][name]:
|
||||
raise ValueError("Frontend with the same name already exists")
|
||||
|
||||
frontend = Sist2Frontend.create_default(name)
|
||||
db["frontends"][name] = frontend
|
||||
|
||||
return frontend
|
||||
|
||||
|
||||
@app.get("/api/ping_es")
|
||||
async def ping_es(url: str, insecure: bool):
|
||||
return check_es_version(url, insecure)
|
||||
|
||||
|
||||
def check_es_version(es_url: str, insecure: bool):
|
||||
try:
|
||||
url = urlparse(es_url)
|
||||
if url.username:
|
||||
auth = (url.username, url.password)
|
||||
es_url = f"{url.scheme}://{url.hostname}:{url.port}"
|
||||
else:
|
||||
auth = None
|
||||
r = requests.get(es_url, verify=insecure, auth=auth)
|
||||
except SSLError:
|
||||
return {
|
||||
"ok": False,
|
||||
"message": "Invalid SSL certificate"
|
||||
}
|
||||
except ConnectionError as e:
|
||||
return {
|
||||
"ok": False,
|
||||
"message": "Connection refused"
|
||||
}
|
||||
except ValueError as e:
|
||||
return {
|
||||
"ok": False,
|
||||
"message": str(e)
|
||||
}
|
||||
|
||||
if r.status_code == 401:
|
||||
return {
|
||||
"ok": False,
|
||||
"message": "Authentication failure"
|
||||
}
|
||||
|
||||
try:
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "Elasticsearch version " + r.json()["version"]["number"]
|
||||
}
|
||||
except:
|
||||
return {
|
||||
"ok": False,
|
||||
"message": "Could not read version"
|
||||
}
|
||||
|
||||
|
||||
def start_frontend_(frontend: Sist2Frontend):
|
||||
frontend.web_options.indices = list(map(lambda j: db["jobs"][j].index_path, frontend.jobs))
|
||||
|
||||
pid = sist2.web(frontend.web_options, frontend.name)
|
||||
RUNNING_FRONTENDS[frontend.name] = pid
|
||||
|
||||
|
||||
@app.post("/api/frontend/{name:str}/start")
|
||||
async def start_frontend(name: str):
|
||||
frontend = db["frontends"][name]
|
||||
if not frontend:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
start_frontend_(frontend)
|
||||
|
||||
|
||||
@app.post("/api/frontend/{name:str}/stop")
|
||||
async def stop_frontend(name: str):
|
||||
if name in RUNNING_FRONTENDS:
|
||||
os.kill(RUNNING_FRONTENDS[name], signal.SIGTERM)
|
||||
del RUNNING_FRONTENDS[name]
|
||||
|
||||
|
||||
@app.get("/api/frontend/")
|
||||
async def get_frontends():
|
||||
res = []
|
||||
for frontend in db["frontends"]:
|
||||
frontend: Sist2Frontend
|
||||
frontend.running = frontend.name in RUNNING_FRONTENDS
|
||||
res.append(frontend)
|
||||
return res
|
||||
|
||||
|
||||
def tail(filepath: str, n: int):
|
||||
with open(filepath) as file:
|
||||
|
||||
reached_eof = False
|
||||
buffer = []
|
||||
|
||||
line = ""
|
||||
while True:
|
||||
tmp = file.readline()
|
||||
if tmp:
|
||||
line += tmp
|
||||
|
||||
if line.endswith("\n"):
|
||||
|
||||
if reached_eof:
|
||||
yield line
|
||||
else:
|
||||
if len(buffer) > n:
|
||||
buffer.pop(0)
|
||||
buffer.append(line)
|
||||
line = ""
|
||||
else:
|
||||
if not reached_eof:
|
||||
reached_eof = True
|
||||
yield from buffer
|
||||
yield None
|
||||
|
||||
|
||||
@app.websocket("/notifications")
|
||||
async def ws_tail_log(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
|
||||
try:
|
||||
await websocket.receive_text()
|
||||
|
||||
async with Subscribe(notifications) as ob:
|
||||
async for notification in ob.notifications():
|
||||
await websocket.send_json(notification)
|
||||
print(notification)
|
||||
|
||||
except ConnectionClosed:
|
||||
return
|
||||
|
||||
|
||||
@app.websocket("/log/{task_id}")
|
||||
async def ws_tail_log(websocket: WebSocket, task_id: str, n: int):
|
||||
log_file = os.path.join(LOG_FOLDER, f"sist2-{task_id}.log")
|
||||
|
||||
await websocket.accept()
|
||||
|
||||
try:
|
||||
await websocket.receive_text()
|
||||
except ConnectionClosed:
|
||||
return
|
||||
|
||||
while True:
|
||||
for line in tail(log_file, n):
|
||||
|
||||
try:
|
||||
if line:
|
||||
await websocket.send_text(line)
|
||||
else:
|
||||
await websocket.send_json({"ping": ""})
|
||||
await asyncio.sleep(0.1)
|
||||
except ConnectionClosed:
|
||||
return
|
||||
|
||||
|
||||
def main():
|
||||
uvicorn.run(app, port=WEBSERVER_PORT, host="0.0.0.0")
|
||||
|
||||
|
||||
def initialize_db():
|
||||
db["sist2_admin"]["info"] = {"version": DB_SCHEMA_VERSION}
|
||||
|
||||
frontend = Sist2Frontend.create_default("default")
|
||||
db["frontends"]["default"] = frontend
|
||||
|
||||
logger.info("Initialized database.")
|
||||
|
||||
|
||||
def start_frontends():
|
||||
for frontend in db["frontends"]:
|
||||
frontend: Sist2Frontend
|
||||
if frontend.auto_start and len(frontend.jobs) > 0:
|
||||
start_frontend_(frontend)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if not db["sist2_admin"]["info"]:
|
||||
initialize_db()
|
||||
if db["sist2_admin"]["info"]["version"] == "1":
|
||||
logger.info("Migrating to v2 database schema")
|
||||
migrate_v1_to_v2(db)
|
||||
if db["sist2_admin"]["info"]["version"] == "2":
|
||||
logger.error("Cannot migrate database from v2 to v3. Delete state.db to proceed.")
|
||||
exit(-1)
|
||||
|
||||
start_frontends()
|
||||
cron.initialize(db, _run_job)
|
||||
|
||||
logger.info("Started sist2-admin. Hello!")
|
||||
|
||||
main()
|
||||
30
sist2-admin/sist2_admin/config.py
Normal file
30
sist2-admin/sist2_admin/config.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
import logging
|
||||
import sys
|
||||
from logging import StreamHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
MAX_LOG_SIZE = 1 * 1024 * 1024
|
||||
|
||||
SIST2_BINARY = os.environ.get("SIST2_BINARY", "/root/sist2")
|
||||
DATA_FOLDER = os.environ.get("DATA_FOLDER", "/sist2-admin/")
|
||||
LOG_FOLDER = os.path.join(DATA_FOLDER, "logs")
|
||||
WEBSERVER_PORT = 8080
|
||||
|
||||
os.makedirs(LOG_FOLDER, exist_ok=True)
|
||||
os.makedirs(DATA_FOLDER, exist_ok=True)
|
||||
|
||||
logger = logging.Logger("sist2-admin")
|
||||
|
||||
_log_file = os.path.join(LOG_FOLDER, "sist2-admin.log")
|
||||
_log_fmt = "%(asctime)s [%(levelname)s] %(message)s"
|
||||
_log_formatter = logging.Formatter(_log_fmt, datefmt='%Y-%m-%d %H:%M:%S')
|
||||
|
||||
console_handler = StreamHandler(sys.stdout)
|
||||
console_handler.setFormatter(_log_formatter)
|
||||
|
||||
file_handler = RotatingFileHandler(_log_file, mode="a", maxBytes=MAX_LOG_SIZE, backupCount=1)
|
||||
file_handler.setFormatter(_log_formatter)
|
||||
|
||||
logger.addHandler(console_handler)
|
||||
logger.addHandler(file_handler)
|
||||
33
sist2-admin/sist2_admin/cron.py
Normal file
33
sist2-admin/sist2_admin/cron.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from threading import Thread
|
||||
|
||||
import pycron
|
||||
import time
|
||||
|
||||
from hexlib.db import PersistentState
|
||||
|
||||
from config import logger
|
||||
from jobs import Sist2Job
|
||||
|
||||
|
||||
def _check_schedule(db: PersistentState, run_job):
|
||||
for job in db["jobs"]:
|
||||
job: Sist2Job
|
||||
|
||||
if job.schedule_enabled:
|
||||
if pycron.is_now(job.cron_expression):
|
||||
logger.info(f"Submit scan task to queue for [{job.name}]")
|
||||
run_job(job)
|
||||
|
||||
|
||||
def _cron_thread(db, run_job):
|
||||
time.sleep(60 - (time.time() % 60))
|
||||
start = time.time()
|
||||
|
||||
while True:
|
||||
_check_schedule(db, run_job)
|
||||
time.sleep(60 - ((time.time() - start) % 60))
|
||||
|
||||
|
||||
def initialize(db, run_job):
|
||||
t = Thread(target=_cron_thread, args=(db, run_job), daemon=True, name="timer")
|
||||
t.start()
|
||||
317
sist2-admin/sist2_admin/jobs.py
Normal file
317
sist2-admin/sist2_admin/jobs.py
Normal file
@@ -0,0 +1,317 @@
|
||||
import json
|
||||
import logging
|
||||
import os.path
|
||||
import signal
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from logging import FileHandler
|
||||
from threading import Lock, Thread
|
||||
from time import sleep
|
||||
from uuid import uuid4, UUID
|
||||
|
||||
from hexlib.db import PersistentState
|
||||
from pydantic import BaseModel
|
||||
|
||||
from config import logger, LOG_FOLDER
|
||||
from notifications import Notifications
|
||||
from sist2 import ScanOptions, IndexOptions, Sist2
|
||||
from state import RUNNING_FRONTENDS
|
||||
from web import Sist2Frontend
|
||||
|
||||
|
||||
class JobStatus(Enum):
|
||||
CREATED = "created"
|
||||
STARTED = "started"
|
||||
INDEXED = "indexed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class Sist2Job(BaseModel):
|
||||
name: str
|
||||
scan_options: ScanOptions
|
||||
index_options: IndexOptions
|
||||
|
||||
cron_expression: str
|
||||
schedule_enabled: bool = False
|
||||
|
||||
previous_index: str = None
|
||||
index_path: str = None
|
||||
previous_index_path: str = None
|
||||
last_index_date: datetime = None
|
||||
status: JobStatus = JobStatus("created")
|
||||
last_modified: datetime
|
||||
etag: str = None
|
||||
do_full_scan: bool = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def create_default(name: str):
|
||||
return Sist2Job(
|
||||
name=name,
|
||||
scan_options=ScanOptions(path="/"),
|
||||
index_options=IndexOptions(),
|
||||
last_modified=datetime.now(),
|
||||
cron_expression="0 0 * * *"
|
||||
)
|
||||
|
||||
# @validator("etag", always=True)
|
||||
# def validate_etag(cls, value, values):
|
||||
# s = values["name"] + values["scan_options"].json() + values["index_options"].json() + values["cron_expression"]
|
||||
# return md5(s.encode()).hexdigest()
|
||||
|
||||
|
||||
class Sist2TaskProgress:
|
||||
|
||||
def __init__(self, done: int = 0, count: int = 0, index_size: int = 0, tn_size: int = 0, waiting: bool = False):
|
||||
self.done = done
|
||||
self.count = count
|
||||
self.index_size = index_size
|
||||
self.store_size = tn_size
|
||||
self.waiting = waiting
|
||||
|
||||
def percent(self):
|
||||
return (self.done / self.count) if self.count else 0
|
||||
|
||||
|
||||
class Sist2Task:
|
||||
|
||||
def __init__(self, job: Sist2Job, display_name: str, depends_on: uuid.UUID = None):
|
||||
self.job = job
|
||||
self.display_name = display_name
|
||||
|
||||
self.progress = Sist2TaskProgress()
|
||||
self.id = uuid4()
|
||||
self.pid = None
|
||||
self.started = None
|
||||
self.ended = None
|
||||
self.depends_on = depends_on
|
||||
|
||||
self._logger = logging.Logger(name=f"{self.id}")
|
||||
self._logger.addHandler(FileHandler(os.path.join(LOG_FOLDER, f"sist2-{self.id}.log")))
|
||||
|
||||
def json(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"job": self.job,
|
||||
"display_name": self.display_name,
|
||||
"progress": self.progress,
|
||||
"started": self.started,
|
||||
"ended": self.ended,
|
||||
"depends_on": self.depends_on,
|
||||
}
|
||||
|
||||
def log_callback(self, log_json):
|
||||
|
||||
if "progress" in log_json:
|
||||
self.progress = Sist2TaskProgress(**log_json["progress"])
|
||||
elif self._logger:
|
||||
self._logger.info(json.dumps(log_json))
|
||||
|
||||
def run(self, sist2: Sist2, db: PersistentState):
|
||||
self.started = datetime.now()
|
||||
|
||||
logger.info(f"Started task {self.display_name}")
|
||||
|
||||
|
||||
class Sist2ScanTask(Sist2Task):
|
||||
|
||||
def run(self, sist2: Sist2, db: PersistentState):
|
||||
super().run(sist2, db)
|
||||
|
||||
self.job.scan_options.name = self.job.name
|
||||
|
||||
if self.job.index_path is not None and not self.job.do_full_scan:
|
||||
self.job.scan_options.output = self.job.index_path
|
||||
else:
|
||||
self.job.scan_options.output = None
|
||||
|
||||
def set_pid(pid):
|
||||
self.pid = pid
|
||||
|
||||
return_code = sist2.scan(self.job.scan_options, logs_cb=self.log_callback, set_pid_cb=set_pid)
|
||||
self.ended = datetime.now()
|
||||
|
||||
if return_code != 0:
|
||||
self._logger.error(json.dumps({"sist2-admin": f"Process returned non-zero exit code ({return_code})"}))
|
||||
logger.info(f"Task {self.display_name} failed ({return_code})")
|
||||
else:
|
||||
self.job.index_path = self.job.scan_options.output
|
||||
self.job.last_index_date = datetime.now()
|
||||
self.job.do_full_scan = False
|
||||
db["jobs"][self.job.name] = self.job
|
||||
self._logger.info(json.dumps({"sist2-admin": f"Save last_index_date={self.job.last_index_date}"}))
|
||||
|
||||
logger.info(f"Completed {self.display_name} ({return_code=})")
|
||||
|
||||
# Remove old index
|
||||
if return_code == 0:
|
||||
if self.job.previous_index_path is not None and self.job.previous_index_path != self.job.index_path:
|
||||
self._logger.info(json.dumps({"sist2-admin": f"Remove {self.job.previous_index_path=}"}))
|
||||
try:
|
||||
os.remove(self.job.previous_index_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
self.job.previous_index_path = self.job.index_path
|
||||
db["jobs"][self.job.name] = self.job
|
||||
|
||||
return return_code
|
||||
|
||||
|
||||
class Sist2IndexTask(Sist2Task):
|
||||
|
||||
def __init__(self, job: Sist2Job, display_name: str, depends_on: Sist2Task):
|
||||
super().__init__(job, display_name, depends_on=depends_on.id)
|
||||
|
||||
def run(self, sist2: Sist2, db: PersistentState):
|
||||
super().run(sist2, db)
|
||||
|
||||
self.job.index_options.path = self.job.scan_options.output
|
||||
|
||||
return_code = sist2.index(self.job.index_options, logs_cb=self.log_callback)
|
||||
self.ended = datetime.now()
|
||||
|
||||
duration = self.ended - self.started
|
||||
|
||||
ok = return_code == 0
|
||||
|
||||
if ok:
|
||||
self.restart_running_frontends(db, sist2)
|
||||
|
||||
# Update status
|
||||
self.job.status = JobStatus("indexed") if ok else JobStatus("failed")
|
||||
self.job.previous_index_path = self.job.index_path
|
||||
db["jobs"][self.job.name] = self.job
|
||||
|
||||
self._logger.info(json.dumps({"sist2-admin": f"Sist2Scan task finished {return_code=}, {duration=}"}))
|
||||
|
||||
logger.info(f"Completed {self.display_name} ({return_code=})")
|
||||
|
||||
return return_code
|
||||
|
||||
def restart_running_frontends(self, db: PersistentState, sist2: Sist2):
|
||||
for frontend_name, pid in RUNNING_FRONTENDS.items():
|
||||
frontend = db["frontends"][frontend_name]
|
||||
frontend: Sist2Frontend
|
||||
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
try:
|
||||
os.wait()
|
||||
except ChildProcessError:
|
||||
pass
|
||||
|
||||
frontend.web_options.indices = map(lambda j: db["jobs"][j].index_path, frontend.jobs)
|
||||
|
||||
pid = sist2.web(frontend.web_options, frontend.name)
|
||||
RUNNING_FRONTENDS[frontend_name] = pid
|
||||
|
||||
self._logger.info(json.dumps({"sist2-admin": f"Restart frontend {pid=} {frontend_name=}"}))
|
||||
|
||||
|
||||
class TaskQueue:
|
||||
def __init__(self, sist2: Sist2, db: PersistentState, notifications: Notifications):
|
||||
self._lock = Lock()
|
||||
|
||||
self._sist2 = sist2
|
||||
self._db = db
|
||||
self._notifications = notifications
|
||||
|
||||
self._tasks = {}
|
||||
self._queue = []
|
||||
self._sem = 0
|
||||
|
||||
self._thread = Thread(target=self._check_new_task, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def _tasks_failed(self):
|
||||
done = set()
|
||||
|
||||
for row in self._db["task_done"].sql("WHERE return_code != 0"):
|
||||
done.add(uuid.UUID(row["id"]))
|
||||
|
||||
return done
|
||||
|
||||
def _tasks_done(self):
|
||||
|
||||
done = set()
|
||||
|
||||
for row in self._db["task_done"]:
|
||||
done.add(uuid.UUID(row["id"]))
|
||||
|
||||
return done
|
||||
|
||||
def _check_new_task(self):
|
||||
while True:
|
||||
with self._lock:
|
||||
for task in list(self._queue):
|
||||
task: Sist2Task
|
||||
|
||||
if self._sem >= 1:
|
||||
break
|
||||
|
||||
if not task.depends_on or task.depends_on in self._tasks_done():
|
||||
self._queue.remove(task)
|
||||
|
||||
if task.depends_on in self._tasks_failed():
|
||||
# The task which we depend on failed, continue
|
||||
continue
|
||||
|
||||
self._sem += 1
|
||||
|
||||
t = Thread(target=self._run_task, args=(task,))
|
||||
|
||||
self._tasks[task.id] = {
|
||||
"task": task,
|
||||
"thread": t,
|
||||
}
|
||||
|
||||
t.start()
|
||||
break
|
||||
sleep(1)
|
||||
|
||||
def tasks(self):
|
||||
return list(map(lambda t: t["task"], self._tasks.values()))
|
||||
|
||||
def kill_task(self, task_id):
|
||||
|
||||
task = self._tasks.get(UUID(task_id))
|
||||
|
||||
if task:
|
||||
pid = task["task"].pid
|
||||
logger.info(f"Killing task {task_id} (pid={pid})")
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _run_task(self, task: Sist2Task):
|
||||
task_result = task.run(self._sist2, self._db)
|
||||
|
||||
with self._lock:
|
||||
del self._tasks[task.id]
|
||||
self._sem -= 1
|
||||
|
||||
self._db["task_done"][task.id] = {
|
||||
"ended": task.ended,
|
||||
"started": task.started,
|
||||
"name": task.display_name,
|
||||
"return_code": task_result
|
||||
}
|
||||
if isinstance(task, Sist2IndexTask):
|
||||
self._notifications.notify({
|
||||
"message": "notifications.indexCompleted",
|
||||
"job": task.job.name
|
||||
})
|
||||
|
||||
def submit(self, task: Sist2Task):
|
||||
|
||||
logger.info(f"Submitted task to queue {task.display_name}")
|
||||
|
||||
with self._lock:
|
||||
self._queue.append(task)
|
||||
40
sist2-admin/sist2_admin/notifications.py
Normal file
40
sist2-admin/sist2_admin/notifications.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import asyncio
|
||||
from typing import List
|
||||
|
||||
|
||||
class Notifications:
|
||||
def __init__(self):
|
||||
self._subscribers: List[Subscribe] = []
|
||||
|
||||
def subscribe(self, ob):
|
||||
self._subscribers.append(ob)
|
||||
|
||||
def unsubscribe(self, ob):
|
||||
self._subscribers.remove(ob)
|
||||
|
||||
def notify(self, notification: dict):
|
||||
for ob in self._subscribers:
|
||||
ob.notify(notification)
|
||||
|
||||
|
||||
class Subscribe:
|
||||
def __init__(self, notifications: Notifications):
|
||||
self._queue = []
|
||||
self._notifications = notifications
|
||||
|
||||
async def __aenter__(self):
|
||||
self._notifications.subscribe(self)
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
self._notifications.unsubscribe(self)
|
||||
|
||||
def notify(self, notification: dict):
|
||||
self._queue.append(notification)
|
||||
|
||||
async def notifications(self):
|
||||
while True:
|
||||
try:
|
||||
yield self._queue.pop(0)
|
||||
except IndexError:
|
||||
await asyncio.sleep(0.1)
|
||||
323
sist2-admin/sist2_admin/sist2.py
Normal file
323
sist2-admin/sist2_admin/sist2.py
Normal file
@@ -0,0 +1,323 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os.path
|
||||
from datetime import datetime
|
||||
from io import TextIOWrapper
|
||||
from logging import FileHandler
|
||||
from subprocess import Popen, PIPE
|
||||
from tempfile import NamedTemporaryFile
|
||||
from threading import Thread
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from config import logger, LOG_FOLDER
|
||||
|
||||
|
||||
class Sist2Version:
|
||||
def __init__(self, version: str):
|
||||
self._version = version
|
||||
|
||||
self.major, self.minor, self.patch = [int(x) for x in version.split(".")]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.major}.{self.minor}.{self.patch}"
|
||||
|
||||
|
||||
class WebOptions(BaseModel):
|
||||
indices: List[str] = []
|
||||
es_url: str = "http://elasticsearch:9200"
|
||||
es_insecure_ssl: bool = False
|
||||
es_index: str = "sist2"
|
||||
bind: str = "0.0.0.0:4090"
|
||||
auth: str = None
|
||||
tag_auth: str = None
|
||||
tagline: str = "Lightning-fast file system indexer and search tool"
|
||||
dev: bool = False
|
||||
lang: str = "en"
|
||||
auth0_audience: str = None
|
||||
auth0_domain: str = None
|
||||
auth0_client_id: str = None
|
||||
auth0_public_key: str = None
|
||||
auth0_public_key_file: str = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def args(self):
|
||||
args = ["web", f"--es-url={self.es_url}", f"--bind={self.bind}",
|
||||
f"--tagline={self.tagline}", f"--lang={self.lang}"]
|
||||
|
||||
if self.auth0_audience:
|
||||
args.append(f"--auth0-audience={self.auth0_audience}")
|
||||
if self.auth0_domain:
|
||||
args.append(f"--auth0-domain={self.auth0_domain}")
|
||||
if self.auth0_client_id:
|
||||
args.append(f"--auth0-client-id={self.auth0_client_id}")
|
||||
if self.auth0_public_key_file:
|
||||
args.append(f"--auth0-public-key-file={self.auth0_public_key_file}")
|
||||
if self.es_insecure_ssl:
|
||||
args.append(f"--es-insecure-ssl")
|
||||
if self.auth:
|
||||
args.append(f"--auth={self.auth}")
|
||||
if self.tag_auth:
|
||||
args.append(f"--tag-auth={self.tag_auth}")
|
||||
if self.dev:
|
||||
args.append(f"--dev")
|
||||
|
||||
args.extend(self.indices)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
class IndexOptions(BaseModel):
|
||||
path: str = None
|
||||
threads: int = 1
|
||||
es_url: str = "http://elasticsearch:9200"
|
||||
es_insecure_ssl: bool = False
|
||||
es_index: str = "sist2"
|
||||
incremental_index: bool = True
|
||||
script: str = ""
|
||||
script_file: str = None
|
||||
batch_size: int = 70
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def args(self):
|
||||
|
||||
args = ["index", self.path, f"--threads={self.threads}", f"--es-url={self.es_url}",
|
||||
f"--es-index={self.es_index}", f"--batch-size={self.batch_size}"]
|
||||
|
||||
if self.script_file:
|
||||
args.append(f"--script-file={self.script_file}")
|
||||
if self.es_insecure_ssl:
|
||||
args.append(f"--es-insecure-ssl")
|
||||
if self.incremental_index:
|
||||
args.append(f"--incremental-index")
|
||||
|
||||
return args
|
||||
|
||||
|
||||
ARCHIVE_SKIP = "skip"
|
||||
ARCHIVE_LIST = "list"
|
||||
ARCHIVE_SHALLOW = "shallow"
|
||||
ARCHIVE_RECURSE = "recurse"
|
||||
|
||||
|
||||
class ScanOptions(BaseModel):
|
||||
path: str
|
||||
threads: int = 1
|
||||
thumbnail_quality: int = 2
|
||||
thumbnail_size: int = 552
|
||||
thumbnail_count: int = 1
|
||||
content_size: int = 32768
|
||||
depth: int = -1
|
||||
archive: str = ARCHIVE_RECURSE
|
||||
archive_passphrase: str = None
|
||||
ocr_lang: str = None
|
||||
ocr_images: bool = False
|
||||
ocr_ebooks: bool = False
|
||||
exclude: str = None
|
||||
fast: bool = False
|
||||
treemap_threshold: float = 0.0005
|
||||
mem_buffer: int = 2000
|
||||
read_subtitles: bool = False
|
||||
fast_epub: bool = False
|
||||
checksums: bool = False
|
||||
incremental: bool = True
|
||||
optimize_index: bool = False
|
||||
output: str = None
|
||||
name: str = None
|
||||
rewrite_url: str = None
|
||||
list_file: str = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def args(self):
|
||||
args = ["scan", self.path, f"--threads={self.threads}", f"--thumbnail-quality={self.thumbnail_quality}",
|
||||
f"--thumbnail-count={self.thumbnail_count}", f"--thumbnail-size={self.thumbnail_size}",
|
||||
f"--content-size={self.content_size}", f"--output={self.output}", f"--depth={self.depth}",
|
||||
f"--archive={self.archive}", f"--mem-buffer={self.mem_buffer}"]
|
||||
|
||||
if self.incremental:
|
||||
args.append(f"--incremental")
|
||||
if self.optimize_index:
|
||||
args.append(f"--optimize-index")
|
||||
if self.rewrite_url:
|
||||
args.append(f"--rewrite-url={self.rewrite_url}")
|
||||
if self.name:
|
||||
args.append(f"--name={self.name}")
|
||||
if self.archive_passphrase:
|
||||
args.append(f"--archive-passphrase={self.archive_passphrase}")
|
||||
if self.ocr_lang:
|
||||
args.append(f"--ocr-lang={self.ocr_lang}")
|
||||
if self.ocr_ebooks:
|
||||
args.append(f"--ocr-ebooks")
|
||||
if self.ocr_images:
|
||||
args.append(f"--ocr-images")
|
||||
if self.exclude:
|
||||
args.append(f"--exclude={self.exclude}")
|
||||
if self.fast:
|
||||
args.append(f"--fast")
|
||||
if self.treemap_threshold:
|
||||
args.append(f"--treemap-threshold={self.treemap_threshold}")
|
||||
if self.read_subtitles:
|
||||
args.append(f"--read-subtitles")
|
||||
if self.fast_epub:
|
||||
args.append(f"--fast-epub")
|
||||
if self.checksums:
|
||||
args.append(f"--checksums")
|
||||
if self.list_file:
|
||||
args.append(f"--list_file={self.list_file}")
|
||||
|
||||
return args
|
||||
|
||||
|
||||
class Sist2Index:
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
|
||||
with open(os.path.join(path, "descriptor.json")) as f:
|
||||
self._descriptor = json.load(f)
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"path": self.path,
|
||||
"version": self.version(),
|
||||
"timestamp": self.timestamp(),
|
||||
"name": self.name()
|
||||
}
|
||||
|
||||
def version(self) -> Sist2Version:
|
||||
return Sist2Version(self._descriptor["version"])
|
||||
|
||||
def timestamp(self) -> datetime:
|
||||
return datetime.fromtimestamp(self._descriptor["timestamp"])
|
||||
|
||||
def name(self) -> str:
|
||||
return self._descriptor["name"]
|
||||
|
||||
|
||||
class Sist2:
|
||||
|
||||
def __init__(self, bin_path: str, data_directory: str):
|
||||
self._bin_path = bin_path
|
||||
self._data_dir = data_directory
|
||||
|
||||
def index(self, options: IndexOptions, logs_cb):
|
||||
|
||||
if options.script:
|
||||
with NamedTemporaryFile("w", prefix="sist2-admin", suffix=".painless", delete=False) as f:
|
||||
f.write(options.script)
|
||||
options.script_file = f.name
|
||||
else:
|
||||
options.script_file = None
|
||||
|
||||
args = [
|
||||
self._bin_path,
|
||||
*options.args(),
|
||||
"--json-logs",
|
||||
"--very-verbose"
|
||||
]
|
||||
proc = Popen(args, stdout=PIPE, stderr=PIPE)
|
||||
|
||||
t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, proc))
|
||||
t_stderr.start()
|
||||
|
||||
self._consume_logs_stdout(logs_cb, proc)
|
||||
|
||||
t_stderr.join()
|
||||
|
||||
return proc.returncode
|
||||
|
||||
def scan(self, options: ScanOptions, logs_cb, set_pid_cb):
|
||||
|
||||
if options.output is None:
|
||||
options.output = os.path.join(
|
||||
self._data_dir,
|
||||
f"scan-{options.name.replace('/', '_')}-{datetime.now()}.sist2"
|
||||
)
|
||||
|
||||
args = [
|
||||
self._bin_path,
|
||||
*options.args(),
|
||||
"--json-logs",
|
||||
"--very-verbose"
|
||||
]
|
||||
|
||||
logs_cb({"sist2-admin": f"Starting sist2 command with args {args}"})
|
||||
|
||||
proc = Popen(args, stdout=PIPE, stderr=PIPE)
|
||||
|
||||
set_pid_cb(proc.pid)
|
||||
|
||||
t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, proc))
|
||||
t_stderr.start()
|
||||
|
||||
self._consume_logs_stdout(logs_cb, proc)
|
||||
|
||||
t_stderr.join()
|
||||
|
||||
return proc.returncode
|
||||
|
||||
@staticmethod
|
||||
def _consume_logs_stderr(logs_cb, proc):
|
||||
pipe_wrapper = TextIOWrapper(proc.stderr, encoding="utf8", errors="ignore")
|
||||
try:
|
||||
for line in pipe_wrapper:
|
||||
if line.strip() == "":
|
||||
continue
|
||||
logs_cb({"stderr": line})
|
||||
finally:
|
||||
proc.wait()
|
||||
pipe_wrapper.close()
|
||||
|
||||
@staticmethod
|
||||
def _consume_logs_stdout(logs_cb, proc):
|
||||
pipe_wrapper = TextIOWrapper(proc.stdout, encoding="utf8", errors="ignore")
|
||||
for line in pipe_wrapper:
|
||||
try:
|
||||
if line.strip() == "":
|
||||
continue
|
||||
log_object = json.loads(line)
|
||||
logs_cb(log_object)
|
||||
except Exception as e:
|
||||
try:
|
||||
logs_cb({"sist2-admin": f"Could not decode log line: {line}; {e}"})
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
def web(self, options: WebOptions, name: str):
|
||||
|
||||
if options.auth0_public_key:
|
||||
with NamedTemporaryFile("w", prefix="sist2-admin", suffix=".txt", delete=False) as f:
|
||||
f.write(options.auth0_public_key)
|
||||
options.auth0_public_key_file = f.name
|
||||
else:
|
||||
options.auth0_public_key_file = None
|
||||
|
||||
args = [
|
||||
self._bin_path,
|
||||
*options.args()
|
||||
]
|
||||
|
||||
web_logger = logging.Logger(name=f"sist2-frontend-{name}")
|
||||
web_logger.addHandler(FileHandler(os.path.join(LOG_FOLDER, f"frontend-{name}.log")))
|
||||
|
||||
def logs_cb(message):
|
||||
web_logger.info(json.dumps(message))
|
||||
|
||||
logger.info(f"Starting frontend {' '.join(args)}")
|
||||
|
||||
proc = Popen(args, stdout=PIPE, stderr=PIPE)
|
||||
|
||||
t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, proc))
|
||||
t_stderr.start()
|
||||
|
||||
t_stdout = Thread(target=self._consume_logs_stdout, args=(logs_cb, proc))
|
||||
t_stdout.start()
|
||||
|
||||
return proc.pid
|
||||
79
sist2-admin/sist2_admin/state.py
Normal file
79
sist2-admin/sist2_admin/state.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from typing import Dict
|
||||
import shutil
|
||||
|
||||
from hexlib.db import Table, PersistentState
|
||||
import pickle
|
||||
|
||||
from tesseract import get_tesseract_langs
|
||||
|
||||
RUNNING_FRONTENDS: Dict[str, int] = {}
|
||||
|
||||
TESSERACT_LANGS = get_tesseract_langs()
|
||||
|
||||
DB_SCHEMA_VERSION = "3"
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def _serialize(item):
|
||||
if isinstance(item, BaseModel):
|
||||
return pickle.dumps(item)
|
||||
if isinstance(item, bytes):
|
||||
raise Exception("FIXME: bytes in PickleTable")
|
||||
return item
|
||||
|
||||
|
||||
def _deserialize(item):
|
||||
if isinstance(item, bytes):
|
||||
return pickle.loads(item)
|
||||
return item
|
||||
|
||||
|
||||
class PickleTable(Table):
|
||||
|
||||
def __getitem__(self, item):
|
||||
row = super().__getitem__(item)
|
||||
if row:
|
||||
return dict((k, _deserialize(v)) for k, v in row.items())
|
||||
return row
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
value = dict((k, _serialize(v)) for k, v in value.items())
|
||||
super().__setitem__(key, value)
|
||||
|
||||
def __iter__(self):
|
||||
for row in super().__iter__():
|
||||
yield dict((k, _deserialize(v)) for k, v in row.items())
|
||||
|
||||
def sql(self, where_clause, *params):
|
||||
for row in super().sql(where_clause, *params):
|
||||
yield dict((k, _deserialize(v)) for k, v in row.items())
|
||||
|
||||
|
||||
def migrate_v1_to_v2(db: PersistentState):
|
||||
|
||||
shutil.copy(db.dbfile, db.dbfile + "-before-migrate-v2.bak")
|
||||
|
||||
# Frontends
|
||||
db._table_factory = PickleTable
|
||||
frontends = [row["frontend"] for row in db["frontends"]]
|
||||
del db["frontends"]
|
||||
|
||||
db._table_factory = Table
|
||||
for frontend in frontends:
|
||||
db["frontends"][frontend.name] = frontend
|
||||
list(db["frontends"])
|
||||
|
||||
# Jobs
|
||||
db._table_factory = PickleTable
|
||||
jobs = [row["job"] for row in db["jobs"]]
|
||||
del db["jobs"]
|
||||
|
||||
db._table_factory = Table
|
||||
for job in jobs:
|
||||
db["jobs"][job.name] = job
|
||||
list(db["jobs"])
|
||||
|
||||
db["sist2_admin"]["info"] = {
|
||||
"version": "2"
|
||||
}
|
||||
14
sist2-admin/sist2_admin/tesseract.py
Normal file
14
sist2-admin/sist2_admin/tesseract.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import subprocess
|
||||
|
||||
|
||||
def get_tesseract_langs():
|
||||
|
||||
res = subprocess.check_output([
|
||||
"tesseract",
|
||||
"--list-langs"
|
||||
]).decode()
|
||||
|
||||
languages = res.split("\n")[1:]
|
||||
|
||||
return list(filter(lambda lang: lang and lang != "osd", languages))
|
||||
|
||||
28
sist2-admin/sist2_admin/web.py
Normal file
28
sist2-admin/sist2_admin/web.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import os.path
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from sist2 import WebOptions
|
||||
|
||||
|
||||
class Sist2Frontend(BaseModel):
|
||||
name: str
|
||||
jobs: List[str]
|
||||
web_options: WebOptions
|
||||
running: bool = False
|
||||
|
||||
auto_start: bool = False
|
||||
extra_query_args: str = ""
|
||||
custom_url: str = None
|
||||
|
||||
def get_log_path(self, log_folder: str):
|
||||
return os.path.join(log_folder, f"frontend-{self.name}.log")
|
||||
|
||||
@staticmethod
|
||||
def create_default(name: str):
|
||||
return Sist2Frontend(
|
||||
name=name,
|
||||
web_options=WebOptions(),
|
||||
jobs=[]
|
||||
)
|
||||
9
sist2-vue/dist/css/chunk-vendors.css
vendored
9
sist2-vue/dist/css/chunk-vendors.css
vendored
File diff suppressed because one or more lines are too long
1
sist2-vue/dist/css/index.css
vendored
1
sist2-vue/dist/css/index.css
vendored
File diff suppressed because one or more lines are too long
3
sist2-vue/dist/index.html
vendored
3
sist2-vue/dist/index.html
vendored
@@ -1,3 +0,0 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>sist2</title><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="css/index.css" rel="preload" as="style"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="js/index.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link href="css/index.css" rel="stylesheet"></head><body><noscript><style>body {
|
||||
height: initial;
|
||||
}</style><div style="text-align: center; margin-top: 100px"><strong>We're sorry but sist2 doesn't work properly without JavaScript enabled. Please enable it to continue.</strong><br><strong>Nous sommes désolés mais sist2 ne fonctionne pas correctement si JavaScript est activé. Veuillez l'activer pour continuer.</strong></div></noscript><div id="app"></div><script src="js/chunk-vendors.js"></script><script src="js/index.js"></script></body></html>
|
||||
146
sist2-vue/dist/js/chunk-vendors.js
vendored
146
sist2-vue/dist/js/chunk-vendors.js
vendored
File diff suppressed because one or more lines are too long
1
sist2-vue/dist/js/index.js
vendored
1
sist2-vue/dist/js/index.js
vendored
File diff suppressed because one or more lines are too long
BIN
sist2-vue/fslightbox-vue.tgz
Normal file
BIN
sist2-vue/fslightbox-vue.tgz
Normal file
Binary file not shown.
22945
sist2-vue/package-lock.json
generated
22945
sist2-vue/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,15 +7,15 @@
|
||||
"build": "vue-cli-service build --mode production"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth0/auth0-spa-js": "^2.0.2",
|
||||
"@egjs/vue-infinitegrid": "3.3.0",
|
||||
"axios": "^0.25.0",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
"core-js": "^3.6.5",
|
||||
"crypto-es": "^1.2.7",
|
||||
"d3": "^5.16.0",
|
||||
"d3": "^5.6.1",
|
||||
"date-fns": "^2.21.3",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"fslightbox-vue": "file:../../../mnt/Hatchery/main/projects/sist2/fslightbox-vue-pro-1.3.1.tgz",
|
||||
"fslightbox-vue": "fslightbox-vue.tgz",
|
||||
"nouislider": "^15.2.0",
|
||||
"underscore": "^1.13.1",
|
||||
"vue": "^2.6.12",
|
||||
@@ -27,12 +27,12 @@
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/polyfill": "^7.11.5",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@vue/cli-plugin-babel": "~5.0.8",
|
||||
"@vue/cli-plugin-router": "~5.0.8",
|
||||
"@vue/cli-plugin-typescript": "^5.0.8",
|
||||
"@vue/cli-plugin-vuex": "~5.0.8",
|
||||
"@vue/cli-service": "^5.0.8",
|
||||
"@vue/test-utils": "^1.0.3",
|
||||
"bootstrap": "^4.5.2",
|
||||
"inspire-tree": "^4.3.1",
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
<template>
|
||||
<div id="app" :class="getClass()">
|
||||
<div id="app" :class="getClass()" v-if="!authLoading">
|
||||
<NavBar></NavBar>
|
||||
<router-view v-if="!configLoading"/>
|
||||
</div>
|
||||
<div class="loading-page" v-else>
|
||||
<div class="loading-spinners">
|
||||
<b-spinner type="grow" variant="primary"></b-spinner>
|
||||
<b-spinner type="grow" variant="primary"></b-spinner>
|
||||
<b-spinner type="grow" variant="primary"></b-spinner>
|
||||
</div>
|
||||
<div class="loading-text">
|
||||
Loading • Chargement • 装载 • Wird geladen
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavBar from "@/components/NavBar";
|
||||
import {mapGetters} from "vuex";
|
||||
import {mapActions, mapGetters, mapMutations} from "vuex";
|
||||
import Sist2Api from "@/Sist2Api";
|
||||
import {setupAuth0} from "@/main";
|
||||
|
||||
export default {
|
||||
components: {NavBar},
|
||||
data() {
|
||||
return {
|
||||
configLoading: false
|
||||
configLoading: false,
|
||||
authLoading: true,
|
||||
sist2InfoLoading: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -30,9 +44,43 @@ export default {
|
||||
this.configLoading = true;
|
||||
window.setTimeout(() => this.configLoading = false, 10);
|
||||
}
|
||||
|
||||
if (mutation.type === "setAuth0Token") {
|
||||
this.authLoading = false;
|
||||
}
|
||||
});
|
||||
|
||||
Sist2Api.getSist2Info().then(data => {
|
||||
|
||||
if (data.auth0Enabled) {
|
||||
this.authLoading = true;
|
||||
setupAuth0(data.auth0Domain, data.auth0ClientId, data.auth0Audience)
|
||||
|
||||
this.$auth.$watch("loading", loading => {
|
||||
if (loading === false) {
|
||||
|
||||
if (!this.$auth.isAuthenticated) {
|
||||
this.$auth.loginWithRedirect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove "code" param
|
||||
window.history.replaceState({}, "", "/" + window.location.hash);
|
||||
|
||||
this.$store.dispatch("loadAuth0Token");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.authLoading = false;
|
||||
}
|
||||
|
||||
this.setSist2Info(data);
|
||||
this.setIndices(data.indices)
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapActions(["setSist2Info",]),
|
||||
...mapMutations(["setIndices",]),
|
||||
getClass() {
|
||||
return {
|
||||
"theme-light": this.optTheme === "light",
|
||||
@@ -314,4 +362,22 @@ mark {
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.loading-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 15px
|
||||
}
|
||||
|
||||
.loading-spinners {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import axios from "axios";
|
||||
import {ext, strUnescape, lum} from "./util";
|
||||
import CryptoES from 'crypto-es';
|
||||
|
||||
export interface EsTag {
|
||||
id: string
|
||||
@@ -30,7 +29,6 @@ export interface EsHit {
|
||||
_index: string
|
||||
_id: string
|
||||
_score: number
|
||||
_path_md5: string
|
||||
_type: string
|
||||
_tags: Tag[]
|
||||
_seq: number
|
||||
@@ -63,6 +61,7 @@ export interface EsHit {
|
||||
isAudio: boolean
|
||||
hasThumbnail: boolean
|
||||
hasVidPreview: boolean
|
||||
imageAspectRatio: number
|
||||
/** Number of thumbnails available */
|
||||
tnNum: number
|
||||
}
|
||||
@@ -157,6 +156,9 @@ class Sist2Api {
|
||||
&& hit._source.videoc !== "raw" && hit._source.videoc !== "ppm") {
|
||||
hit._props.isPlayableImage = true;
|
||||
}
|
||||
if ("width" in hit._source && "height" in hit._source) {
|
||||
hit._props.imageAspectRatio = hit._source.width / hit._source.height;
|
||||
}
|
||||
break;
|
||||
case "video":
|
||||
if ("videoc" in hit._source) {
|
||||
@@ -189,30 +191,6 @@ class Sist2Api {
|
||||
setHitTags(hit: EsHit): void {
|
||||
const tags = [] as Tag[];
|
||||
|
||||
const mimeCategory = hit._source.mime == null ? null : hit._source.mime.split("/")[0];
|
||||
|
||||
switch (mimeCategory) {
|
||||
case "image":
|
||||
case "video":
|
||||
if ("videoc" in hit._source && hit._source.videoc) {
|
||||
tags.push({
|
||||
style: "video",
|
||||
text: hit._source.videoc.replace(" ", ""),
|
||||
userTag: false
|
||||
} as Tag);
|
||||
}
|
||||
break
|
||||
case "audio":
|
||||
if ("audioc" in hit._source && hit._source.audioc) {
|
||||
tags.push({
|
||||
style: "audio",
|
||||
text: hit._source.audioc,
|
||||
userTag: false
|
||||
} as Tag);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// User tags
|
||||
if ("tag" in hit._source) {
|
||||
hit._source.tag.forEach(tag => {
|
||||
@@ -249,11 +227,6 @@ class Sist2Api {
|
||||
res.hits.hits.forEach((hit: EsHit) => {
|
||||
hit["_source"]["name"] = strUnescape(hit["_source"]["name"]);
|
||||
hit["_source"]["path"] = strUnescape(hit["_source"]["path"]);
|
||||
hit["_path_md5"] = CryptoES.MD5(
|
||||
hit["_source"]["path"] +
|
||||
(hit["_source"]["path"] ? "/" : "") +
|
||||
hit["_source"]["name"] + ext(hit)
|
||||
).toString();
|
||||
|
||||
this.setHitProps(hit);
|
||||
this.setHitTags(hit);
|
||||
@@ -343,10 +316,6 @@ class Sist2Api {
|
||||
};
|
||||
}
|
||||
|
||||
getDocInfo(docId: string) {
|
||||
return axios.get(`${this.baseUrl}d/${docId}`);
|
||||
}
|
||||
|
||||
getTags() {
|
||||
return this.esQuery({
|
||||
aggs: {
|
||||
@@ -380,8 +349,7 @@ class Sist2Api {
|
||||
return axios.post(`${this.baseUrl}tag/` + hit["_source"]["index"], {
|
||||
delete: false,
|
||||
name: tag,
|
||||
doc_id: hit["_id"],
|
||||
path_md5: hit._path_md5
|
||||
doc_id: hit["_id"]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -389,8 +357,7 @@ class Sist2Api {
|
||||
return axios.post(`${this.baseUrl}tag/` + hit["_source"]["index"], {
|
||||
delete: true,
|
||||
name: tag,
|
||||
doc_id: hit["_id"],
|
||||
path_md5: hit._path_md5
|
||||
doc_id: hit["_id"]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ interface SortMode {
|
||||
|
||||
class Sist2Query {
|
||||
|
||||
searchQuery(): any {
|
||||
searchQuery(blankSearch: boolean = false): any {
|
||||
|
||||
const getters = store.getters;
|
||||
|
||||
@@ -93,22 +93,6 @@ class Sist2Query {
|
||||
{terms: {index: selectedIndexIds}}
|
||||
] as any[];
|
||||
|
||||
if (sizeMin && sizeMax) {
|
||||
filters.push({range: {size: {gte: sizeMin, lte: sizeMax}}})
|
||||
} else if (sizeMin) {
|
||||
filters.push({range: {size: {gte: sizeMin}}})
|
||||
} else if (sizeMax) {
|
||||
filters.push({range: {size: {lte: sizeMax}}})
|
||||
}
|
||||
|
||||
if (dateMin && dateMax) {
|
||||
filters.push({range: {mtime: {gte: dateMin, lte: dateMax}}})
|
||||
} else if (dateMin) {
|
||||
filters.push({range: {mtime: {gte: dateMin}}})
|
||||
} else if (dateMax) {
|
||||
filters.push({range: {mtime: {lte: dateMax}}})
|
||||
}
|
||||
|
||||
const fields = [
|
||||
"name^8",
|
||||
"content^3",
|
||||
@@ -128,20 +112,39 @@ class Sist2Query {
|
||||
fields.push("name.nGram^3");
|
||||
}
|
||||
|
||||
const path = pathText.replace(/\/$/, "").toLowerCase(); //remove trailing slashes
|
||||
if (path !== "") {
|
||||
filters.push({term: {path: path}})
|
||||
}
|
||||
if (!blankSearch) {
|
||||
if (sizeMin && sizeMax) {
|
||||
filters.push({range: {size: {gte: sizeMin, lte: sizeMax}}})
|
||||
} else if (sizeMin) {
|
||||
filters.push({range: {size: {gte: sizeMin}}})
|
||||
} else if (sizeMax) {
|
||||
filters.push({range: {size: {lte: sizeMax}}})
|
||||
}
|
||||
|
||||
if (selectedMimeTypes.length > 0) {
|
||||
filters.push({terms: {"mime": selectedMimeTypes}});
|
||||
}
|
||||
if (dateMin && dateMax) {
|
||||
filters.push({range: {mtime: {gte: dateMin, lte: dateMax}}})
|
||||
} else if (dateMin) {
|
||||
filters.push({range: {mtime: {gte: dateMin}}})
|
||||
} else if (dateMax) {
|
||||
filters.push({range: {mtime: {lte: dateMax}}})
|
||||
}
|
||||
|
||||
if (selectedTags.length > 0) {
|
||||
if (getters.optTagOrOperator) {
|
||||
filters.push({terms: {"tag": selectedTags}});
|
||||
} else {
|
||||
selectedTags.forEach((tag: string) => filters.push({term: {"tag": tag}}));
|
||||
const path = pathText.replace(/\/$/, "").toLowerCase(); //remove trailing slashes
|
||||
|
||||
if (path !== "") {
|
||||
filters.push({term: {path: path}})
|
||||
}
|
||||
|
||||
if (selectedMimeTypes.length > 0) {
|
||||
filters.push({terms: {"mime": selectedMimeTypes}});
|
||||
}
|
||||
|
||||
if (selectedTags.length > 0) {
|
||||
if (getters.optTagOrOperator) {
|
||||
filters.push({terms: {"tag": selectedTags}});
|
||||
} else {
|
||||
selectedTags.forEach((tag: string) => filters.push({term: {"tag": tag}}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +185,7 @@ class Sist2Query {
|
||||
size: size,
|
||||
} as any;
|
||||
|
||||
if (!empty) {
|
||||
if (!empty && !blankSearch) {
|
||||
q.query.bool.must = query;
|
||||
}
|
||||
|
||||
@@ -207,7 +210,7 @@ class Sist2Query {
|
||||
};
|
||||
|
||||
if (!legacyES) {
|
||||
q.highlight.max_analyzed_offset = 9_999_999;
|
||||
q.highlight.max_analyzed_offset = 999_999;
|
||||
}
|
||||
|
||||
if (getters.optSearchInPath) {
|
||||
@@ -237,7 +240,7 @@ class Sist2Query {
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty) {
|
||||
if (!empty && !blankSearch) {
|
||||
q.query.function_score.query.bool.must.push(query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
<DocFileTitle :doc="doc"></DocFileTitle>
|
||||
</div>
|
||||
|
||||
<!-- Featured line -->
|
||||
<div style="display: flex">
|
||||
<FeaturedFieldsLine :doc="doc"></FeaturedFieldsLine>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="card-text">
|
||||
<TagContainer :hit="doc"></TagContainer>
|
||||
@@ -43,10 +48,11 @@ import DocFileTitle from "@/components/DocFileTitle.vue";
|
||||
import DocInfoModal from "@/components/DocInfoModal.vue";
|
||||
import ContentDiv from "@/components/ContentDiv.vue";
|
||||
import FullThumbnail from "@/components/FullThumbnail";
|
||||
import FeaturedFieldsLine from "@/components/FeaturedFieldsLine";
|
||||
|
||||
|
||||
export default {
|
||||
components: {FullThumbnail, ContentDiv, DocInfoModal, DocFileTitle, TagContainer},
|
||||
components: {FeaturedFieldsLine, FullThumbnail, ContentDiv, DocInfoModal, DocFileTitle, TagContainer},
|
||||
props: ["doc", "width"],
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -50,6 +50,11 @@
|
||||
<span v-if="doc._source.author && doc._source.pages" class="mx-1">-</span>
|
||||
<span v-if="doc._source.author">{{ doc._source.author }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Featured line -->
|
||||
<div style="display: flex">
|
||||
<FeaturedFieldsLine :doc="doc"></FeaturedFieldsLine>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-list-group-item>
|
||||
@@ -61,10 +66,11 @@ import DocFileTitle from "@/components/DocFileTitle";
|
||||
import DocInfoModal from "@/components/DocInfoModal";
|
||||
import ContentDiv from "@/components/ContentDiv";
|
||||
import FileIcon from "@/components/icons/FileIcon";
|
||||
import FeaturedFieldsLine from "@/components/FeaturedFieldsLine";
|
||||
|
||||
export default {
|
||||
name: "DocListItem",
|
||||
components: {FileIcon, ContentDiv, DocInfoModal, DocFileTitle, TagContainer},
|
||||
components: {FileIcon, ContentDiv, DocInfoModal, DocFileTitle, TagContainer, FeaturedFieldsLine},
|
||||
props: ["doc"],
|
||||
data() {
|
||||
return {
|
||||
|
||||
42
sist2-vue/src/components/FeaturedFieldsLine.vue
Normal file
42
sist2-vue/src/components/FeaturedFieldsLine.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="featured-line" v-html="featuredLineHtml"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {humanDate, humanFileSize} from "@/util";
|
||||
|
||||
function scopedEval(context, expr) {
|
||||
const evaluator = Function.apply(null, [...Object.keys(context), "expr", "return eval(expr)"]);
|
||||
return evaluator.apply(null, [...Object.values(context), expr]);
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
name: "FeaturedFieldsLine",
|
||||
props: ["doc"],
|
||||
computed: {
|
||||
featuredLineHtml() {
|
||||
const scope = {doc: this.doc._source, humanDate: humanDate, humanFileSize: humanFileSize};
|
||||
|
||||
return this.$store.getters.optFeaturedFields
|
||||
.replaceAll(/\$\{([^}]*)}/g, (match, g1) => {
|
||||
return scopedEval(scope, g1);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.featured-line {
|
||||
font-size: 90%;
|
||||
font-family: 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif;
|
||||
color: #424242;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.theme-black .featured-line {
|
||||
color: #bebebe;
|
||||
}
|
||||
</style>
|
||||
@@ -6,13 +6,13 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="doc._props.isImage && !hover && doc._props.tnW / doc._props.tnH < 5"
|
||||
v-if="doc._props.isImage && doc._props.imageAspectRatio < 5"
|
||||
class="card-img-overlay"
|
||||
:class="{'small-badge': smallBadge}">
|
||||
<span class="badge badge-resolution">{{ `${doc._source.width}x${doc._source.height}` }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="(doc._props.isVideo || doc._props.isGif) && doc._source.duration > 0 && !hover"
|
||||
<div v-if="(doc._props.isVideo || doc._props.isGif) && doc._source.duration > 0"
|
||||
class="card-img-overlay"
|
||||
:class="{'small-badge': smallBadge}">
|
||||
<span class="badge badge-resolution">{{ humanTime(doc._source.duration) }}</span>
|
||||
@@ -63,6 +63,11 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
tnSrc() {
|
||||
return this.getThumbnailSrc(this.currentThumbnailNum);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getThumbnailSrc(thumbnailNum) {
|
||||
const doc = this.doc;
|
||||
const props = doc._props;
|
||||
if (props.isGif && this.hover) {
|
||||
@@ -70,10 +75,8 @@ export default {
|
||||
}
|
||||
return (this.currentThumbnailNum === 0)
|
||||
? `t/${doc._source.index}/${doc._id}`
|
||||
: `t/${doc._source.index}/${doc._id}${String(this.currentThumbnailNum).padStart(4, "0")}`;
|
||||
: `t/${doc._source.index}/${doc._id}/${String(thumbnailNum).padStart(4, "0")}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
humanTime: humanTime,
|
||||
onThumbnailClick() {
|
||||
this.$emit("onThumbnailClick");
|
||||
@@ -86,9 +89,14 @@ export default {
|
||||
},
|
||||
onTnEnter() {
|
||||
this.hover = true;
|
||||
const start = Date.now()
|
||||
if (this.doc._props.hasVidPreview) {
|
||||
this.currentThumbnailNum += 1;
|
||||
this.scheduleNextTnNum();
|
||||
let img = new Image();
|
||||
img.src = this.getThumbnailSrc(this.currentThumbnailNum + 1);
|
||||
img.onload = () => {
|
||||
this.currentThumbnailNum += 1;
|
||||
this.scheduleNextTnNum(Date.now() - start);
|
||||
}
|
||||
}
|
||||
},
|
||||
onTnLeave() {
|
||||
@@ -99,17 +107,23 @@ export default {
|
||||
this.timeoutId = null;
|
||||
}
|
||||
},
|
||||
scheduleNextTnNum() {
|
||||
const INTERVAL = this.$store.state.optVidPreviewInterval ?? 700;
|
||||
scheduleNextTnNum(offset = 0) {
|
||||
const INTERVAL = (this.$store.state.optVidPreviewInterval ?? 700) - offset;
|
||||
this.timeoutId = window.setTimeout(() => {
|
||||
const start = Date.now();
|
||||
if (!this.hover) {
|
||||
return;
|
||||
}
|
||||
this.scheduleNextTnNum();
|
||||
if (this.currentThumbnailNum === this.doc._props.tnNum - 1) {
|
||||
this.currentThumbnailNum = 0;
|
||||
this.scheduleNextTnNum();
|
||||
} else {
|
||||
this.currentThumbnailNum += 1;
|
||||
let img = new Image();
|
||||
img.src = this.getThumbnailSrc(this.currentThumbnailNum + 1);
|
||||
img.onload = () => {
|
||||
this.currentThumbnailNum += 1;
|
||||
this.scheduleNextTnNum(Date.now() - start);
|
||||
}
|
||||
}
|
||||
}, INTERVAL);
|
||||
},
|
||||
@@ -152,17 +166,18 @@ export default {
|
||||
}
|
||||
|
||||
.badge-resolution {
|
||||
color: #212529;
|
||||
background-color: #FFC107;
|
||||
color: #c6c6c6;
|
||||
background-color: #272727CC;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
|
||||
.card-img-overlay {
|
||||
pointer-events: none;
|
||||
padding: 0.75rem;
|
||||
bottom: unset;
|
||||
top: 0;
|
||||
padding: 2px 6px;
|
||||
bottom: 4px;
|
||||
top: unset;
|
||||
left: unset;
|
||||
right: unset;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.small-badge {
|
||||
|
||||
@@ -72,6 +72,12 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(src).forEach(key => {
|
||||
if (key.startsWith("mt_") || key.startsWith("int_")) {
|
||||
items.push({key: key, value: src[key]});
|
||||
}
|
||||
});
|
||||
|
||||
// Exif GPS
|
||||
if ("exif_gps_longitude_dec" in src) {
|
||||
items.push({
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<template>
|
||||
<Preloader v-if="loading"></Preloader>
|
||||
<div v-else-if="content" class="content-div">{{ content }}</div>
|
||||
<div v-else-if="content" class="content-div" v-html="content"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Sist2Api from "@/Sist2Api";
|
||||
import Preloader from "@/components/Preloader";
|
||||
import Sist2Query from "@/Sist2Query";
|
||||
import store from "@/store";
|
||||
|
||||
export default {
|
||||
name: "LazyContentDiv",
|
||||
@@ -18,10 +20,72 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
Sist2Api.getDocInfo(this.docId).then(src => {
|
||||
this.content = src.data.content;
|
||||
const query = Sist2Query.searchQuery();
|
||||
|
||||
if (this.$store.state.optHighlight) {
|
||||
|
||||
const fields = this.$store.state.fuzzy
|
||||
? {"content.nGram": {}}
|
||||
: {content: {}};
|
||||
|
||||
query.highlight = {
|
||||
pre_tags: ["<mark>"],
|
||||
post_tags: ["</mark>"],
|
||||
number_of_fragments: 0,
|
||||
fields,
|
||||
};
|
||||
|
||||
if (!store.state.sist2Info.esVersionLegacy) {
|
||||
query.highlight.max_analyzed_offset = 999_999;
|
||||
}
|
||||
}
|
||||
|
||||
if ("function_score" in query.query) {
|
||||
query.query = query.query.function_score.query;
|
||||
}
|
||||
|
||||
if (!("must" in query.query.bool)) {
|
||||
query.query.bool.must = [];
|
||||
} else if (!Array.isArray(query.query.bool.must)) {
|
||||
query.query.bool.must = [query.query.bool.must];
|
||||
}
|
||||
|
||||
query.query.bool.must.push({match: {_id: this.docId}});
|
||||
|
||||
delete query["sort"];
|
||||
delete query["aggs"];
|
||||
delete query["search_after"];
|
||||
delete query.query["function_score"];
|
||||
|
||||
query._source = {
|
||||
includes: ["content", "name", "path", "extension"]
|
||||
}
|
||||
|
||||
query.size = 1;
|
||||
|
||||
Sist2Api.esQuery(query).then(resp => {
|
||||
this.loading = false;
|
||||
})
|
||||
if (resp.hits.hits.length === 1) {
|
||||
this.content = this.getContent(resp.hits.hits[0]);
|
||||
} else {
|
||||
console.log("FIXME: could not get content")
|
||||
console.log(resp)
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
getContent(doc) {
|
||||
if (!doc.highlight) {
|
||||
return doc._source.content;
|
||||
}
|
||||
|
||||
if (doc.highlight["content.nGram"]) {
|
||||
return doc.highlight["content.nGram"][0];
|
||||
}
|
||||
if (doc.highlight.content) {
|
||||
return doc.highlight.content[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :class="{'disable-animations': $store.state.optSimpleLightbox}">
|
||||
<FsLightbox
|
||||
ref="lightbox"
|
||||
:key="lightboxKey"
|
||||
:toggler="showLightbox"
|
||||
:sources="lightboxSources"
|
||||
@@ -10,7 +11,7 @@
|
||||
:source-index="lightboxSlide"
|
||||
:custom-toolbar-buttons="customButtons"
|
||||
:slideshow-time="$store.getters.optLightboxSlideDuration * 1000"
|
||||
:zoom-increment="0.5"
|
||||
:zoom-increment="0.25"
|
||||
:load-only-current-source="$store.getters.optLightboxLoadOnlyCurrent"
|
||||
:on-close="onClose"
|
||||
:on-open="onShow"
|
||||
@@ -29,6 +30,7 @@ export default {
|
||||
components: {FsLightbox},
|
||||
data() {
|
||||
return {
|
||||
disableAnimations: true,
|
||||
customButtons: [
|
||||
{
|
||||
viewBox: "0 0 384.928 384.928",
|
||||
@@ -64,7 +66,84 @@ export default {
|
||||
return this.$store.getters["uiLightboxTypes"];
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const listener = document.onkeydown;
|
||||
|
||||
document.onkeydown = (e) => {
|
||||
|
||||
const ret = this.keyDownListener(e)
|
||||
|
||||
if (listener && ret) {
|
||||
return listener(e);
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
keyDownListener(e) {
|
||||
|
||||
const isLightboxOpen = this.$refs.lightbox === undefined || this.$refs.lightbox.$el.tagName === undefined;
|
||||
|
||||
if (isLightboxOpen) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const lightboxStore = this.$refs.lightbox.fsLightboxStore.slice(-1)[0];
|
||||
|
||||
switch (e.key) {
|
||||
case " ": {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
// Find video at current slide, toggle play/pause
|
||||
[...document.getElementsByClassName("fslightbox-absoluted")].forEach(elem => {
|
||||
if (elem.style.transform === "translate(0px)" || elem.style.transform === "translate(0px, 0px)") {
|
||||
const vid = elem.getElementsByTagName("video")[0];
|
||||
|
||||
if (vid) {
|
||||
if (vid.paused) {
|
||||
vid.play();
|
||||
} else {
|
||||
vid.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
case "ArrowUp":
|
||||
case "k": {
|
||||
if (!lightboxStore.data.isThumbing && lightboxStore.core.thumbsToggler) {
|
||||
lightboxStore.core.thumbsToggler.toggleThumbs();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case "ArrowDown":
|
||||
case "j": {
|
||||
if (lightboxStore.data.isThumbing && lightboxStore.core.thumbsToggler) {
|
||||
lightboxStore.core.thumbsToggler.toggleThumbs();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case "h": {
|
||||
if (lightboxStore.core.stageManager.getPreviousSlideIndex) {
|
||||
lightboxStore.core.slideIndexChanger.jumpTo(lightboxStore.core.stageManager.getPreviousSlideIndex());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case "l": {
|
||||
if (lightboxStore.core.stageManager.getNextSlideIndex) {
|
||||
lightboxStore.core.slideIndexChanger.jumpTo(lightboxStore.core.stageManager.getNextSlideIndex());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
onDownloadClick() {
|
||||
const url = this.lightboxSources[this.lightboxSlide];
|
||||
|
||||
@@ -81,9 +160,13 @@ export default {
|
||||
},
|
||||
onSlideChange() {
|
||||
// Pause all videos when changing slide
|
||||
document.getElementsByTagName("video").forEach((el) => {
|
||||
const videos = document.getElementsByTagName("video");
|
||||
if (videos.length === 0) {
|
||||
return
|
||||
}
|
||||
for (let el of videos) {
|
||||
el.pause();
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -125,4 +208,20 @@ export default {
|
||||
.fslightbox-toolbar-button:nth-child(7) {
|
||||
order: 7;
|
||||
}
|
||||
|
||||
.disable-animations .fslightbox-container {
|
||||
background: rgba(30,30,30,.9);
|
||||
}
|
||||
|
||||
.disable-animations .fslightbox-transform-transition {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.disable-animations .fslightbox-fade-in-strong {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.fslightbox-container video, .fslightbox-container img {
|
||||
cursor: unset !important;
|
||||
}
|
||||
</style>
|
||||
@@ -9,13 +9,15 @@
|
||||
|
||||
<span class="badge badge-pill version" v-if="$store && $store.state.sist2Info">
|
||||
v{{ sist2Version() }}<span v-if="isDebug()">-dbg</span><span v-if="isLegacy() && !hideLegacy()">-<a
|
||||
href="https://github.com/simon987/sist2/blob/master/docs/USAGE.md#elasticsearch" target="_blank">legacyES</a></span>
|
||||
href="https://github.com/simon987/sist2/blob/master/docs/USAGE.md#elasticsearch"
|
||||
target="_blank">legacyES</a></span>
|
||||
</span>
|
||||
|
||||
<span v-if="$store && $store.state.sist2Info" class="tagline" v-html="tagline()"></span>
|
||||
|
||||
<b-button class="ml-auto" to="stats" variant="link">{{ $t("stats") }}</b-button>
|
||||
<b-button to="config" variant="link">{{ $t("config") }}</b-button>
|
||||
<b-button v-if="$auth && $auth.isAuthenticated" variant="link" @click="onLogoutClick()">logout</b-button>
|
||||
</b-navbar>
|
||||
</template>
|
||||
|
||||
@@ -40,6 +42,9 @@ export default {
|
||||
},
|
||||
hideLegacy() {
|
||||
return this.$store.state.optHideLegacy;
|
||||
},
|
||||
onLogoutClick() {
|
||||
this.$auth.logout();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +65,6 @@ export default {
|
||||
color: #222 !important;
|
||||
font-size: 1.75rem;
|
||||
padding: 0;
|
||||
font-family: Hack;
|
||||
}
|
||||
|
||||
.navbar-brand:hover {
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {randomSeed} from "@/util";
|
||||
|
||||
export default {
|
||||
name: "SortSelect",
|
||||
computed: {
|
||||
@@ -52,7 +54,7 @@ export default {
|
||||
methods: {
|
||||
onSelect(sortMode) {
|
||||
if (sortMode === "random") {
|
||||
this.$store.commit("setSeed", Math.round(Math.random() * 100000));
|
||||
this.$store.commit("setSeed", randomSeed());
|
||||
}
|
||||
this.$store.commit("setSortMode", sortMode);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
|
||||
|
||||
<template v-for="tag in hit._tags">
|
||||
<!-- User tag-->
|
||||
<div v-if="tag.userTag" :key="tag.rawText" style="display: inline-block">
|
||||
<span
|
||||
:id="hit._id+tag.rawText"
|
||||
@@ -51,7 +52,7 @@
|
||||
>{{ tag.text.split(".").pop() }}</span>
|
||||
|
||||
<b-popover :target="hit._id+tag.rawText" triggers="focus blur" placement="top">
|
||||
<b-button variant="danger" @click="onTagDeleteClick(tag, $event)">{{$t("deleteTag")}}</b-button>
|
||||
<b-button variant="danger" @click="onTagDeleteClick(tag, $event)">{{ $t("deleteTag") }}</b-button>
|
||||
</b-popover>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +67,7 @@
|
||||
<small v-if="showAddButton" class="badge add-tag-button" @click="tagAdd()">{{$t("addTag")}}</small>
|
||||
|
||||
<!-- Size tag-->
|
||||
<small v-else class="text-muted badge-size">{{
|
||||
<small v-else class="text-muted badge-size" style="padding-left: 2px">{{
|
||||
humanFileSize(hit._source.size)
|
||||
}}</small>
|
||||
</div>
|
||||
@@ -211,7 +212,7 @@ export default Vue.extend({
|
||||
|
||||
return matches.sort().map(match => {
|
||||
return {
|
||||
title: match.split(".").slice(0,-1).join("."),
|
||||
title: match.split(".").slice(0, -1).join("."),
|
||||
id: match
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<div id="tagTree"></div>
|
||||
<div>
|
||||
<b-input-group v-if="showSearchBar" id="tag-picker-filter-bar">
|
||||
<b-form-input :value="filter"
|
||||
:placeholder="$t('tagFilter')"
|
||||
@input="onFilter($event)"></b-form-input>
|
||||
</b-input-group>
|
||||
|
||||
<div id="tagTree"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -112,10 +120,12 @@ function addTag(map, tag, id, count) {
|
||||
|
||||
export default {
|
||||
name: "TagPicker",
|
||||
props: ["showSearchBar"],
|
||||
data() {
|
||||
return {
|
||||
tagTree: null,
|
||||
loadedFromArgs: false,
|
||||
filter: ""
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -129,6 +139,10 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
onFilter(value) {
|
||||
this.filter = value;
|
||||
this.tagTree.search(value);
|
||||
},
|
||||
initializeTree() {
|
||||
const tagMap = [];
|
||||
this.tagTree = new InspireTree({
|
||||
@@ -163,7 +177,8 @@ export default {
|
||||
});
|
||||
},
|
||||
handleTreeClick(node, e) {
|
||||
if (e === "indeterminate" || e === "collapsed" || e === 'rendered' || e === "focused") {
|
||||
if (e === "indeterminate" || e === "collapsed" || e === 'rendered' || e === "focused"
|
||||
|| e === "matched" || e === "hidden") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -180,7 +195,15 @@ export default {
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.inspire-tree .focused>.wholerow {
|
||||
.inspire-tree .focused > .wholerow {
|
||||
border: none;
|
||||
}
|
||||
|
||||
#tag-picker-filter-bar {
|
||||
padding: 10px 4px 4px;
|
||||
}
|
||||
|
||||
.theme-black .inspire-tree .matched > .wholerow {
|
||||
background: rgba(251, 191, 41, 0.25);
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,7 @@ export default {
|
||||
advanced: "Advanced search",
|
||||
fuzzy: "Fuzzy"
|
||||
},
|
||||
addTag: "Add",
|
||||
addTag: "Tag",
|
||||
deleteTag: "Delete",
|
||||
download: "Download",
|
||||
and: "and",
|
||||
@@ -16,6 +16,8 @@ export default {
|
||||
pages: "pages",
|
||||
mimeTypes: "Media types",
|
||||
tags: "Tags",
|
||||
tagFilter: "Filter tags",
|
||||
forExample: "For example:",
|
||||
help: {
|
||||
simpleSearch: "Simple search",
|
||||
advancedSearch: "Advanced search",
|
||||
@@ -72,7 +74,11 @@ export default {
|
||||
hideLegacy: "Hide the 'legacyES' Elasticsearch notice",
|
||||
updateMimeMap: "Update the Media Types tree in real time",
|
||||
useDatePicker: "Use a Date Picker component rather than a slider",
|
||||
vidPreviewInterval: "Video preview frame duration in ms"
|
||||
vidPreviewInterval: "Video preview frame duration in ms",
|
||||
simpleLightbox: "Disable animations in image viewer",
|
||||
showTagPickerFilter: "Display the tag filter bar",
|
||||
featuredFields: "Featured fields Javascript template string. Will appear in the search results.",
|
||||
featuredFieldsList: "Available variables"
|
||||
},
|
||||
queryMode: {
|
||||
simple: "Simple",
|
||||
@@ -80,6 +86,7 @@ export default {
|
||||
},
|
||||
lang: {
|
||||
en: "English",
|
||||
de: "Deutsch",
|
||||
fr: "Français",
|
||||
"zh-CN": "简体中文",
|
||||
},
|
||||
@@ -165,6 +172,179 @@ export default {
|
||||
selectedIndices: "selected indices",
|
||||
},
|
||||
},
|
||||
de: {
|
||||
filePage: {
|
||||
notFound: "Nicht gefunden"
|
||||
},
|
||||
searchBar: {
|
||||
simple: "Suche",
|
||||
advanced: "Erweiterte Suche",
|
||||
fuzzy: "Fuzzy"
|
||||
},
|
||||
addTag: "Tag",
|
||||
deleteTag: "Löschen",
|
||||
download: "Herunterladen",
|
||||
and: "und",
|
||||
page: "Seite",
|
||||
pages: "Seiten",
|
||||
mimeTypes: "Medientypen",
|
||||
tags: "Tags",
|
||||
tagFilter: "Tags filtern",
|
||||
forExample: "Zum Beispiel:",
|
||||
help: {
|
||||
simpleSearch: "Einfache Suche",
|
||||
advancedSearch: "Erweiterte Suche",
|
||||
help: "Hilfe",
|
||||
term: "<BEGRIFF>",
|
||||
and: "UND Operator",
|
||||
or: "ODER Operator",
|
||||
not: "negiert einen einzelnen Begriff",
|
||||
quotes: "liefert Treffer, wenn die Abfolge in der genauen Reihenfolge gefunden wird",
|
||||
prefix: "liefert Treffer, wenn die Abfolge einen solchen Präfix hat",
|
||||
parens: "gruppiert Ausdrücke",
|
||||
tildeTerm: "liefert Treffer, im gegebenen 'Editierabstand'",
|
||||
tildePhrase: "liefert Treffer, mit dem Ausdruck. Erfolgt die gegebene Anzahl zwischenstehnde Nicht-Treffer-Wörter.",
|
||||
example1:
|
||||
"Zum Beispiel: <code>\"fried eggs\" +(eggplant | potato) -frittata</code> wird " +
|
||||
"<i>fried eggs</i> und <i>eggplant</i> oder <i>potato</i> finden, aber keine Ergebnisse, " +
|
||||
"die <i>frittata</i> enthalten.",
|
||||
defaultOperator:
|
||||
"Wenn weder <code>+</code> noch <code>|</code> angegeben sind, ist " +
|
||||
"<code>+</code> (and) der Standard.",
|
||||
fuzzy:
|
||||
"Wenn <b>Fuzzy</b> aktiviert ist, werden Teil-Treffer (3-grams) ebenfalls akzeptiert.",
|
||||
moreInfoSimple: "Für weitere Informationen s.<a target=\"_blank\" " +
|
||||
"rel=\"noreferrer\" href=\"//www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html\">Elasticsearch Dokumentation</a>",
|
||||
moreInfoAdvanced: "Für die Dokumentation der erweiterten Suche s. <a target=\"_blank\" rel=\"noreferrer\" href=\"//www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax\">Elasticsearch Dokumentation</a>"
|
||||
},
|
||||
config: "Konfiguration",
|
||||
configDescription: "Konfiguration wird in Echtzeit für diesen Browser gespeichert.",
|
||||
configReset: "Konfiguration zurücksetzen",
|
||||
searchOptions: "Such-Optionen",
|
||||
treemapOptions: "Kacheldiagramm-Optionen",
|
||||
displayOptions: "Anzeige-Optionen",
|
||||
opt: {
|
||||
lang: "Sprache",
|
||||
highlight: "Aktiviere Hervorhebung von Treffern",
|
||||
fuzzy: "Aktiviere Fuzzy-Suche standardmäßig",
|
||||
searchInPath: "Abgleich der Abfrage mit dem Dokumentpfad aktivieren",
|
||||
suggestPath: "Aktiviere Auto-Vervollständigung in Pfadfilter-Leiste",
|
||||
fragmentSize: "Kontextgröße in Zeichen hervorheben",
|
||||
queryMode: "Such-Modus",
|
||||
displayMode: "Ansicht",
|
||||
columns: "Anzahl Spalten",
|
||||
treemapType: "Kacheldiagramme Typ",
|
||||
treemapTiling: "Kacheldiagramm Tiling",
|
||||
treemapColorGroupingDepth: "Kacheldiagramme Gruppierungsfarbe Tiefe (flach)",
|
||||
treemapColor: "Kacheldiagramme Farbe (kaskadiert)",
|
||||
treemapSize: "Kacheldiagramm Größe",
|
||||
theme: "Theme",
|
||||
lightboxLoadOnlyCurrent: "keine Bilder in voller Größe für benachbachte Slides im Image-Viewer vorab laden.",
|
||||
slideDuration: "Slide Dauer",
|
||||
resultSize: "Anzahl Treffer pro Seite",
|
||||
tagOrOperator: "Verwende ODER Operator bei der Angabe mehrere Tags.",
|
||||
hideDuplicates: "Verstecke Duplikate basierend auf der Prüfsumme",
|
||||
hideLegacy: "Verstecke die 'legacyES' Elasticsearch Notiz",
|
||||
updateMimeMap: "Aktualisiere Medientyp-Baum in Echtzeit",
|
||||
useDatePicker: "Benutze Datumswähler statt Schieber",
|
||||
vidPreviewInterval: "Videovorschau Framedauer in ms",
|
||||
simpleLightbox: "Schalte Animationen im Image-Viewer ab",
|
||||
showTagPickerFilter: "Zeige die Tag-Filter-Leiste",
|
||||
featuredFields: "Ausgewählte Felder Javascript Vorlage String. Wird in den Suchergebnissen angezeigt.",
|
||||
featuredFieldsList: "Verfügbare Variablen"
|
||||
},
|
||||
queryMode: {
|
||||
simple: "Einfach",
|
||||
advanced: "Erweitert",
|
||||
},
|
||||
lang: {
|
||||
en: "English",
|
||||
de: "Deutsch",
|
||||
fr: "Français",
|
||||
"zh-CN": "简体中文",
|
||||
},
|
||||
displayMode: {
|
||||
grid: "Gitter",
|
||||
list: "Liste",
|
||||
},
|
||||
columns: {
|
||||
auto: "Auto"
|
||||
},
|
||||
treemapType: {
|
||||
cascaded: "kaskadiert",
|
||||
flat: "flach (kompakt)"
|
||||
},
|
||||
treemapSize: {
|
||||
small: "klein",
|
||||
medium: "mittel",
|
||||
large: "groß",
|
||||
xLarge: "sehr groß",
|
||||
xxLarge: "riesig",
|
||||
custom: "eigene",
|
||||
},
|
||||
treemapTiling: {
|
||||
binary: "binär",
|
||||
squarify: "quadratisch",
|
||||
slice: "Slice",
|
||||
dice: "Dice",
|
||||
sliceDice: "Slice & Dice",
|
||||
},
|
||||
theme: {
|
||||
light: "Hell",
|
||||
black: "Dunkel"
|
||||
},
|
||||
hit: "Treffer",
|
||||
hits: "Treffer",
|
||||
details: "Details",
|
||||
stats: "Statistiken",
|
||||
queryTime: "Abfragedauer",
|
||||
totalSize: "Gesamtgröße",
|
||||
pathBar: {
|
||||
placeholder: "Filter Pfad",
|
||||
modalTitle: "Wähle Pfad"
|
||||
},
|
||||
debug: "Debug Informationen",
|
||||
debugDescription: "Informationen für das Debugging. Wenn du Bugs gefunden oder Anregungen für " +
|
||||
"neue Features hast, poste sie bitte <a href='https://github.com/simon987/sist2/issues/new/choose'>hier</a>.",
|
||||
tagline: "Tagline",
|
||||
toast: {
|
||||
esConnErrTitle: "Elasticsearch Verbindungsfehler",
|
||||
esConnErr: "sist2 Web-Modul stellte einen Fehler beim Verbinden mit Elasticsearch fest. " +
|
||||
"Schau in die Server-Logs für weitere Informationen.",
|
||||
esQueryErrTitle: "Query Fehler",
|
||||
esQueryErr: "Konnte Query nicht verarbeiten/ausführen, bitte schaue in die Dokumentation zur erweiterten Suche. " +
|
||||
"Schau in die Server-Logs für weitere Informationen.",
|
||||
dupeTagTitle: "Tag Duplikat",
|
||||
dupeTag: "Dieser Tag existiert bereits für das Dokument.",
|
||||
copiedToClipboard: "In die Zwischenablage kopiert."
|
||||
},
|
||||
saveTagModalTitle: "Tag hinzufügen",
|
||||
saveTagPlaceholder: "Tag Name",
|
||||
confirm: "Bestätigen",
|
||||
indexPickerPlaceholder: "Index auswählen",
|
||||
sort: {
|
||||
relevance: "Relevanz",
|
||||
dateAsc: "Datum (älteste zuerst)",
|
||||
dateDesc: "Datum (neuste zuerst)",
|
||||
sizeAsc: "Größe (kleinste zuerst)",
|
||||
sizeDesc: "Größe (größte zuerst)",
|
||||
nameAsc: "Name (A-z)",
|
||||
nameDesc: "Name (Z-a)",
|
||||
random: "zufällig",
|
||||
},
|
||||
d3: {
|
||||
mimeCount: "Anzahlverteilung nach Medientyp",
|
||||
mimeSize: "Größenverteilung nach Medientyp",
|
||||
dateHistogram: "Verteilung der Änderungszeiten",
|
||||
sizeHistogram: "Verteilung der Dateigrößen",
|
||||
},
|
||||
indexPicker: {
|
||||
selectNone: "keinen auswählen",
|
||||
selectAll: "alle auswählen",
|
||||
selectedIndex: "ausgewählter Index",
|
||||
selectedIndices: "ausgewählte Indizes",
|
||||
},
|
||||
},
|
||||
fr: {
|
||||
filePage: {
|
||||
notFound: "Ficher introuvable"
|
||||
@@ -174,7 +354,7 @@ export default {
|
||||
advanced: "Recherche avancée",
|
||||
fuzzy: "Approximatif"
|
||||
},
|
||||
addTag: "Ajouter",
|
||||
addTag: "Taguer",
|
||||
deleteTag: "Supprimer",
|
||||
download: "Télécharger",
|
||||
and: "et",
|
||||
@@ -182,6 +362,8 @@ export default {
|
||||
pages: "pages",
|
||||
mimeTypes: "Types de médias",
|
||||
tags: "Tags",
|
||||
tagFilter: "Filtrer les tags",
|
||||
forExample: "Par exemple:",
|
||||
help: {
|
||||
simpleSearch: "Recherche simple",
|
||||
advancedSearch: "Recherche avancée",
|
||||
@@ -239,7 +421,11 @@ export default {
|
||||
hideLegacy: "Masquer la notice 'legacyES' Elasticsearch",
|
||||
updateMimeMap: "Mettre à jour l'arbre de Types de médias en temps réel",
|
||||
useDatePicker: "Afficher un composant « Date Picker » plutôt qu'un slider",
|
||||
vidPreviewInterval: "Durée des images d'aperçu video en millisecondes"
|
||||
vidPreviewInterval: "Durée des images d'aperçu video en millisecondes",
|
||||
simpleLightbox: "Désactiver les animations du visualiseur d'images",
|
||||
showTagPickerFilter: "Afficher le filtre dans l'onglet Tags",
|
||||
featuredFields: "Expression Javascript pour les variables mises en évidence. Sera affiché dans les résultats de recherche.",
|
||||
featuredFieldsList: "Variables disponibles"
|
||||
},
|
||||
queryMode: {
|
||||
simple: "Simple",
|
||||
@@ -247,6 +433,7 @@ export default {
|
||||
},
|
||||
lang: {
|
||||
en: "English",
|
||||
de: "Deutsch",
|
||||
fr: "Français",
|
||||
"zh-CN": "简体中文",
|
||||
},
|
||||
@@ -329,8 +516,8 @@ export default {
|
||||
indexPicker: {
|
||||
selectNone: "Sélectionner aucun",
|
||||
selectAll: "Sélectionner tout",
|
||||
selectedIndex: "indice sélectionné",
|
||||
selectedIndices: "indices sélectionnés",
|
||||
selectedIndex: "index sélectionné",
|
||||
selectedIndices: "index sélectionnés",
|
||||
},
|
||||
},
|
||||
"zh-CN": {
|
||||
@@ -342,7 +529,7 @@ export default {
|
||||
advanced: "高级搜索",
|
||||
fuzzy: "模糊搜索"
|
||||
},
|
||||
addTag: "添加",
|
||||
addTag: "签条",
|
||||
deleteTag: "删除",
|
||||
download: "下载",
|
||||
and: "与",
|
||||
@@ -350,6 +537,8 @@ export default {
|
||||
pages: "页",
|
||||
mimeTypes: "文件类型",
|
||||
tags: "标签",
|
||||
tagFilter: "筛选标签",
|
||||
forExample: "例如:",
|
||||
help: {
|
||||
simpleSearch: "简易搜索",
|
||||
advancedSearch: "高级搜索",
|
||||
@@ -406,7 +595,11 @@ export default {
|
||||
hideLegacy: "隐藏'legacyES' Elasticsearch 通知",
|
||||
updateMimeMap: "媒体类型树的实时更新",
|
||||
useDatePicker: "使用日期选择器组件而不是滑块",
|
||||
vidPreviewInterval: "视频预览帧的持续时间,以毫秒为单位"
|
||||
vidPreviewInterval: "视频预览帧的持续时间,以毫秒为单位",
|
||||
simpleLightbox: "在图片查看器中,禁用动画",
|
||||
showTagPickerFilter: "显示标签过滤栏",
|
||||
featuredFields: "特色领域的Javascript模板字符串。将出现在搜索结果中。",
|
||||
featuredFieldsList: "可利用的变量"
|
||||
},
|
||||
queryMode: {
|
||||
simple: "简单",
|
||||
@@ -414,6 +607,7 @@ export default {
|
||||
},
|
||||
lang: {
|
||||
en: "English",
|
||||
de: "Deutsch",
|
||||
fr: "Français",
|
||||
"zh-CN": "简体中文",
|
||||
},
|
||||
|
||||
@@ -3,16 +3,32 @@ import 'mutationobserver-shim'
|
||||
import Vue from 'vue'
|
||||
import './plugins/bootstrap-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import router, {setUseAuth0} from './router'
|
||||
import store from './store'
|
||||
import VueI18n from "vue-i18n";
|
||||
import messages from "@/i18n/messages";
|
||||
|
||||
import { Auth0Plugin } from './plugins/auth0';
|
||||
|
||||
import VueRouter from "vue-router";
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
export function setupAuth0(domain, clientId, audience) {
|
||||
|
||||
setUseAuth0(true);
|
||||
|
||||
Vue.use(Auth0Plugin, {
|
||||
domain,
|
||||
clientId,
|
||||
audience,
|
||||
onRedirectCallback: appState => {}
|
||||
});
|
||||
}
|
||||
|
||||
Vue.prototype.$auth = null;
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
Vue.use(VueI18n);
|
||||
Vue.use(VueRouter);
|
||||
|
||||
|
||||
138
sist2-vue/src/plugins/auth0.js
Normal file
138
sist2-vue/src/plugins/auth0.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import Vue from 'vue';
|
||||
import {createAuth0Client} from '@auth0/auth0-spa-js';
|
||||
|
||||
/** Define a default action to perform after authentication */
|
||||
const DEFAULT_REDIRECT_CALLBACK = () =>
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
|
||||
let instance;
|
||||
|
||||
/** Returns the current instance of the SDK */
|
||||
export const getInstance = () => instance;
|
||||
|
||||
/** Creates an instance of the Auth0 SDK. If one has already been created, it returns that instance */
|
||||
export const useAuth0 = ({
|
||||
domain, clientId, audience,
|
||||
onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
|
||||
redirectUri = window.location.origin,
|
||||
}) => {
|
||||
if (instance) return instance;
|
||||
|
||||
// The 'instance' is simply a Vue object
|
||||
instance = new Vue({
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
isAuthenticated: false,
|
||||
user: {},
|
||||
auth0Client: null,
|
||||
popupOpen: false,
|
||||
error: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/** Authenticates the user using a popup window */
|
||||
async loginWithPopup(options, config) {
|
||||
this.popupOpen = true;
|
||||
|
||||
try {
|
||||
await this.auth0Client.loginWithPopup(options, config);
|
||||
this.user = await this.auth0Client.getUser();
|
||||
this.isAuthenticated = await this.auth0Client.isAuthenticated();
|
||||
this.error = null;
|
||||
} catch (e) {
|
||||
this.error = e;
|
||||
// eslint-disable-next-line
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.popupOpen = false;
|
||||
}
|
||||
|
||||
this.user = await this.auth0Client.getUser();
|
||||
this.isAuthenticated = true;
|
||||
},
|
||||
/** Handles the callback when logging in using a redirect */
|
||||
async handleRedirectCallback() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await this.auth0Client.handleRedirectCallback();
|
||||
this.user = await this.auth0Client.getUser();
|
||||
this.isAuthenticated = true;
|
||||
this.error = null;
|
||||
} catch (e) {
|
||||
this.error = e;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
/** Authenticates the user using the redirect method */
|
||||
loginWithRedirect(o) {
|
||||
return this.auth0Client.loginWithRedirect(o);
|
||||
},
|
||||
/** Returns all the claims present in the ID token */
|
||||
getIdTokenClaims(o) {
|
||||
return this.auth0Client.getIdTokenClaims(o);
|
||||
},
|
||||
/** Returns the access token. If the token is invalid or missing, a new one is retrieved */
|
||||
getTokenSilently(o) {
|
||||
return this.auth0Client.getTokenSilently(o);
|
||||
},
|
||||
|
||||
/** Gets the access token using a popup window */
|
||||
|
||||
getTokenWithPopup(o) {
|
||||
return this.auth0Client.getTokenWithPopup(o);
|
||||
},
|
||||
/** Logs the user out and removes their session on the authorization server */
|
||||
logout() {
|
||||
return this.auth0Client.logout({ logoutParams: { returnTo: window.location.origin } });
|
||||
}
|
||||
},
|
||||
/** Use this lifecycle method to instantiate the SDK client */
|
||||
async created() {
|
||||
// Create a new instance of the SDK client using members of the given options object
|
||||
this.auth0Client = await createAuth0Client({
|
||||
domain: domain,
|
||||
clientId: clientId,
|
||||
|
||||
authorizationParams: {
|
||||
redirect_uri: redirectUri,
|
||||
audience: audience,
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
// If the user is returning to the app after authentication..
|
||||
if (
|
||||
window.location.search.includes('code=') &&
|
||||
window.location.search.includes('state=')
|
||||
) {
|
||||
// handle the redirect and retrieve tokens
|
||||
const {appState} = await this.auth0Client.handleRedirectCallback();
|
||||
|
||||
this.error = null;
|
||||
|
||||
// Notify subscribers that the redirect callback has happened, passing the appState
|
||||
// (useful for retrieving any pre-authentication state)
|
||||
onRedirectCallback(appState);
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = e;
|
||||
} finally {
|
||||
// Initialize our internal authentication state
|
||||
this.isAuthenticated = await this.auth0Client.isAuthenticated();
|
||||
this.user = await this.auth0Client.getUser();
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
// Create a simple Vue plugin to expose the wrapper object throughout the application
|
||||
export const Auth0Plugin = {
|
||||
install(Vue, options) {
|
||||
Vue.prototype.$auth = useAuth0(options);
|
||||
}
|
||||
};
|
||||
2
sist2-vue/src/plugins/bootstrap-vue.js
vendored
2
sist2-vue/src/plugins/bootstrap-vue.js
vendored
@@ -2,6 +2,6 @@ import Vue from "vue"
|
||||
|
||||
import BootstrapVue from "bootstrap-vue"
|
||||
import "bootstrap/dist/css/bootstrap.min.css"
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
import "bootstrap-vue/dist/bootstrap-vue.min.css"
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
28
sist2-vue/src/router/auth0.ts
Normal file
28
sist2-vue/src/router/auth0.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {getInstance} from "@/plugins/auth0";
|
||||
|
||||
export const authGuard = (to, from, next) => {
|
||||
|
||||
const authService = getInstance();
|
||||
|
||||
const fn = () => {
|
||||
// If the user is authenticated, continue with the route
|
||||
if (authService.isAuthenticated) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Otherwise, log in
|
||||
authService.loginWithRedirect({appState: {targetUrl: to.fullPath}});
|
||||
};
|
||||
|
||||
// If loading has already finished, check our auth state using `fn()`
|
||||
if (!authService.loading) {
|
||||
return fn();
|
||||
}
|
||||
|
||||
// Watch for the loading property to change before we check isAuthenticated
|
||||
authService.$watch("loading", loading => {
|
||||
if (loading === false) {
|
||||
return fn();
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -4,14 +4,29 @@ import StatsPage from "../views/StatsPage.vue"
|
||||
import Configuration from "../views/Configuration.vue"
|
||||
import SearchPage from "@/views/SearchPage.vue";
|
||||
import FilePage from "@/views/FilePage.vue";
|
||||
import {authGuard as auth0AuthGuard} from "@/router/auth0";
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
let USE_AUTH0 = false
|
||||
export function setUseAuth0(val) {
|
||||
USE_AUTH0 = val;
|
||||
}
|
||||
|
||||
const authGuard = (to, from, next) => {
|
||||
if (USE_AUTH0) {
|
||||
return auth0AuthGuard(to, from, next);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
const routes: Array<RouteConfig> = [
|
||||
{
|
||||
path: "/",
|
||||
name: "SearchPage",
|
||||
component: SearchPage
|
||||
component: SearchPage,
|
||||
beforeEnter: authGuard
|
||||
},
|
||||
{
|
||||
path: "/stats",
|
||||
@@ -34,7 +49,7 @@ const router = new VueRouter({
|
||||
mode: "hash",
|
||||
base: process.env.BASE_URL,
|
||||
routes,
|
||||
scrollBehavior (to, from, savedPosition) {
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// return desired position
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,7 +2,10 @@ import Vue from "vue"
|
||||
import Vuex from "vuex"
|
||||
import VueRouter, {Route} from "vue-router";
|
||||
import {EsHit, EsResult, EsTag, Index, Tag} from "@/Sist2Api";
|
||||
import {deserializeMimes, serializeMimes} from "@/util";
|
||||
import {deserializeMimes, randomSeed, serializeMimes} from "@/util";
|
||||
import {getInstance} from "@/plugins/auth0.js";
|
||||
|
||||
const CONF_VERSION = 2;
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
@@ -24,14 +27,15 @@ export default new Vuex.Store({
|
||||
sortMode: "score",
|
||||
|
||||
fuzzy: false,
|
||||
size: 60,
|
||||
|
||||
optLang: "en",
|
||||
optLangIsDefault: true,
|
||||
optHideDuplicates: true,
|
||||
optTheme: "light",
|
||||
optDisplay: "grid",
|
||||
optFeaturedFields: "",
|
||||
|
||||
optSize: 60,
|
||||
optHighlight: true,
|
||||
optTagOrOperator: false,
|
||||
optFuzzy: true,
|
||||
@@ -51,6 +55,8 @@ export default new Vuex.Store({
|
||||
optUpdateMimeMap: false,
|
||||
optUseDatePicker: false,
|
||||
optVidPreviewInterval: 700,
|
||||
optSimpleLightbox: true,
|
||||
optShowTagPickerFilter: true,
|
||||
|
||||
_onLoadSelectedIndices: [] as string[],
|
||||
_onLoadSelectedMimeTypes: [] as string[],
|
||||
@@ -78,7 +84,9 @@ export default new Vuex.Store({
|
||||
uiDetailsMimeAgg: null,
|
||||
uiShowDetails: false,
|
||||
|
||||
uiMimeMap: [] as any[]
|
||||
uiMimeMap: [] as any[],
|
||||
|
||||
auth0Token: null
|
||||
},
|
||||
mutations: {
|
||||
setUiShowDetails: (state, val) => state.uiShowDetails = val,
|
||||
@@ -149,8 +157,9 @@ export default new Vuex.Store({
|
||||
setOptSuggestPath: (state, val) => state.optSuggestPath = val,
|
||||
setOptFragmentSize: (state, val) => state.optFragmentSize = val,
|
||||
setOptQueryMode: (state, val) => state.optQueryMode = val,
|
||||
setOptResultSize: (state, val) => state.size = val,
|
||||
setOptResultSize: (state, val) => state.optSize = val,
|
||||
setOptTagOrOperator: (state, val) => state.optTagOrOperator = val,
|
||||
setOptFeaturedFields: (state, val) => state.optFeaturedFields = val,
|
||||
|
||||
setOptTreemapType: (state, val) => state.optTreemapType = val,
|
||||
setOptTreemapTiling: (state, val) => state.optTreemapTiling = val,
|
||||
@@ -161,6 +170,8 @@ export default new Vuex.Store({
|
||||
setOptUpdateMimeMap: (state, val) => state.optUpdateMimeMap = val,
|
||||
setOptUseDatePicker: (state, val) => state.optUseDatePicker = val,
|
||||
setOptVidPreviewInterval: (state, val) => state.optVidPreviewInterval = val,
|
||||
setOptSimpleLightbox: (state, val) => state.optSimpleLightbox = val,
|
||||
setOptShowTagPickerFilter: (state, val) => state.optShowTagPickerFilter = val,
|
||||
|
||||
setOptLightboxLoadOnlyCurrent: (state, val) => state.optLightboxLoadOnlyCurrent = val,
|
||||
setOptLightboxSlideDuration: (state, val) => state.optLightboxSlideDuration = val,
|
||||
@@ -182,6 +193,7 @@ export default new Vuex.Store({
|
||||
busTnTouchStart: (doc_id) => {
|
||||
// noop
|
||||
},
|
||||
setAuth0Token: (state, val) => state.auth0Token = val,
|
||||
},
|
||||
actions: {
|
||||
setSist2Info: (store, val) => {
|
||||
@@ -235,10 +247,18 @@ export default new Vuex.Store({
|
||||
|
||||
if (route.query.sort) {
|
||||
commit("setSortMode", route.query.sort);
|
||||
if (route.query.sort === "random" && route.query.seed === undefined) {
|
||||
route.query.seed = randomSeed().toString();
|
||||
}
|
||||
commit("setSeed", Number(route.query.seed));
|
||||
}
|
||||
},
|
||||
async updateArgs({state}, router: VueRouter) {
|
||||
|
||||
if (router.currentRoute.path !== "/") {
|
||||
return;
|
||||
}
|
||||
|
||||
await router.push({
|
||||
query: {
|
||||
q: state.searchText.trim() ? state.searchText.trim().replace(/\s+/g, " ") : undefined,
|
||||
@@ -267,6 +287,8 @@ export default new Vuex.Store({
|
||||
}
|
||||
});
|
||||
|
||||
conf["version"] = CONF_VERSION;
|
||||
|
||||
localStorage.setItem("sist2_configuration", JSON.stringify(conf));
|
||||
},
|
||||
loadConfiguration({state}) {
|
||||
@@ -274,6 +296,11 @@ export default new Vuex.Store({
|
||||
if (confString) {
|
||||
const conf = JSON.parse(confString);
|
||||
|
||||
if (!("version" in conf) || conf["version"] != CONF_VERSION) {
|
||||
localStorage.removeItem("sist2_configuration");
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
Object.keys(state).forEach((key) => {
|
||||
if (key.startsWith("opt")) {
|
||||
(state as any)[key] = conf[key];
|
||||
@@ -311,6 +338,14 @@ export default new Vuex.Store({
|
||||
commit("setUiLightboxCaptions", []);
|
||||
commit("setUiLightboxKey", 0);
|
||||
commit("setUiDetailsMimeAgg", null);
|
||||
},
|
||||
async loadAuth0Token({commit}) {
|
||||
const authService = getInstance();
|
||||
|
||||
const accessToken = await authService.getTokenSilently()
|
||||
commit("setAuth0Token", accessToken);
|
||||
|
||||
document.cookie = `sist2-auth0=${accessToken};`;
|
||||
}
|
||||
},
|
||||
modules: {},
|
||||
@@ -335,7 +370,7 @@ export default new Vuex.Store({
|
||||
searchText: state => state.searchText,
|
||||
pathText: state => state.pathText,
|
||||
fuzzy: state => state.fuzzy,
|
||||
size: state => state.size,
|
||||
size: state => state.optSize,
|
||||
sortMode: state => state.sortMode,
|
||||
lastQueryResult: state => state.lastQueryResults,
|
||||
lastDoc: function (state): EsHit | null {
|
||||
@@ -373,10 +408,13 @@ export default new Vuex.Store({
|
||||
optTreemapColor: state => state.optTreemapColor,
|
||||
optLightboxLoadOnlyCurrent: state => state.optLightboxLoadOnlyCurrent,
|
||||
optLightboxSlideDuration: state => state.optLightboxSlideDuration,
|
||||
optResultSize: state => state.size,
|
||||
optResultSize: state => state.optSize,
|
||||
optHideLegacy: state => state.optHideLegacy,
|
||||
optUpdateMimeMap: state => state.optUpdateMimeMap,
|
||||
optUseDatePicker: state => state.optUseDatePicker,
|
||||
optVidPreviewInterval: state => state.optVidPreviewInterval,
|
||||
optSimpleLightbox: state => state.optSimpleLightbox,
|
||||
optShowTagPickerFilter: state => state.optShowTagPickerFilter,
|
||||
optFeaturedFields: state => state.optFeaturedFields,
|
||||
}
|
||||
})
|
||||
@@ -57,6 +57,14 @@ export function humanTime(sec_num: number): string {
|
||||
const minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||
const seconds = sec_num - (hours * 3600) - (minutes * 60);
|
||||
|
||||
if (sec_num < 60) {
|
||||
return `${sec_num}s`
|
||||
}
|
||||
|
||||
if (sec_num < 3600) {
|
||||
return `${minutes < 10 ? "0" : ""}${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
|
||||
}
|
||||
|
||||
return `${hours < 10 ? "0" : ""}${hours}:${minutes < 10 ? "0" : ""}${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
|
||||
}
|
||||
|
||||
@@ -162,4 +170,8 @@ export function decompressMime(mime: string): string {
|
||||
.replace("F", "font/")
|
||||
.replace(",", "+")
|
||||
.replace("X", "x-")
|
||||
}
|
||||
|
||||
export function randomSeed(): number {
|
||||
return Math.round(Math.random() * 100000);
|
||||
}
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
<b-card>
|
||||
|
||||
<label><LanguageIcon/><span style="vertical-align: middle"> {{ $t("opt.lang") }}</span></label>
|
||||
<label>
|
||||
<LanguageIcon/>
|
||||
<span style="vertical-align: middle"> {{ $t("opt.lang") }}</span></label>
|
||||
<b-form-select :options="langOptions" :value="optLang" @input="setOptLang"></b-form-select>
|
||||
|
||||
<label>{{ $t("opt.theme") }}</label>
|
||||
@@ -45,6 +47,72 @@
|
||||
<b-form-checkbox :checked="optUseDatePicker" @input="setOptUseDatePicker">
|
||||
{{ $t("opt.useDatePicker") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox :checked="optSimpleLightbox" @input="setOptSimpleLightbox">{{
|
||||
$t("opt.simpleLightbox")
|
||||
}}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox :checked="optShowTagPickerFilter" @input="setOptShowTagPickerFilter">{{
|
||||
$t("opt.showTagPickerFilter")
|
||||
}}
|
||||
</b-form-checkbox>
|
||||
|
||||
<br/>
|
||||
<label>{{ $t("opt.featuredFields") }}</label>
|
||||
|
||||
<br>
|
||||
<b-button v-b-toggle.collapse-1 variant="secondary" class="dropdown-toggle">{{
|
||||
$t("opt.featuredFieldsList")
|
||||
}}
|
||||
</b-button>
|
||||
<b-collapse id="collapse-1" class="mt-2">
|
||||
<ul>
|
||||
<li><code>doc.checksum</code></li>
|
||||
<li><code>doc.path</code></li>
|
||||
<li><code>doc.mime</code></li>
|
||||
<li><code>doc.videoc</code></li>
|
||||
<li><code>doc.audioc</code></li>
|
||||
<li><code>doc.pages</code></li>
|
||||
<li><code>doc.mtime</code></li>
|
||||
<li><code>doc.font_name</code></li>
|
||||
<li><code>doc.album</code></li>
|
||||
<li><code>doc.artist</code></li>
|
||||
<li><code>doc.title</code></li>
|
||||
<li><code>doc.genre</code></li>
|
||||
<li><code>doc.album_artist</code></li>
|
||||
<li><code>doc.exif_make</code></li>
|
||||
<li><code>doc.exif_model</code></li>
|
||||
<li><code>doc.exif_software</code></li>
|
||||
<li><code>doc.exif_exposure_time</code></li>
|
||||
<li><code>doc.exif_fnumber</code></li>
|
||||
<li><code>doc.exif_iso_speed_ratings</code></li>
|
||||
<li><code>doc.exif_focal_length</code></li>
|
||||
<li><code>doc.exif_user_comment</code></li>
|
||||
<li><code>doc.exif_user_comment</code></li>
|
||||
<li><code>doc.exif_gps_longitude_ref</code></li>
|
||||
<li><code>doc.exif_gps_longitude_dms</code></li>
|
||||
<li><code>doc.exif_gps_longitude_dec</code></li>
|
||||
<li><code>doc.exif_gps_latitude_ref</code></li>
|
||||
<li><code>doc.exif_gps_latitude_dec</code></li>
|
||||
<li><code>humanDate()</code></li>
|
||||
<li><code>humanFileSize()</code></li>
|
||||
</ul>
|
||||
|
||||
<p>{{ $t("forExample") }}</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<code><b>${humanDate(doc.mtime)}</b> • ${doc.videoc || ''}</code>
|
||||
</li>
|
||||
<li>
|
||||
<code>${doc.pages ? (doc.pages + ' pages') : ''}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</b-collapse>
|
||||
<br/>
|
||||
<br/>
|
||||
<b-textarea rows="3" :value="optFeaturedFields" @input="setOptFeaturedFields"></b-textarea>
|
||||
</b-card>
|
||||
|
||||
<br/>
|
||||
@@ -143,12 +211,13 @@ export default {
|
||||
components: {LanguageIcon, GearIcon, DebugInfo, Preloader},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
loading: false,
|
||||
configLoading: false,
|
||||
langOptions: [
|
||||
{value: "en", text: this.$t("lang.en")},
|
||||
{value: "fr", text: this.$t("lang.fr")},
|
||||
{value: "zh-CN", text: this.$t("lang.zh-CN")},
|
||||
{value: "de", text: this.$t("lang.de")},
|
||||
],
|
||||
queryModeOptions: [
|
||||
{value: "simple", text: this.$t("queryMode.simple")},
|
||||
@@ -239,17 +308,15 @@ export default {
|
||||
"optUpdateMimeMap",
|
||||
"optUseDatePicker",
|
||||
"optVidPreviewInterval",
|
||||
"optSimpleLightbox",
|
||||
"optShowTagPickerFilter",
|
||||
"optFeaturedFields",
|
||||
]),
|
||||
clientWidth() {
|
||||
return window.innerWidth;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
sist2.getSist2Info().then(data => {
|
||||
this.setSist2Info(data);
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
this.$store.subscribe((mutation) => {
|
||||
if (mutation.type.startsWith("setOpt")) {
|
||||
this.$store.dispatch("updateConfiguration");
|
||||
@@ -285,6 +352,9 @@ export default {
|
||||
"setOptUpdateMimeMap",
|
||||
"setOptUseDatePicker",
|
||||
"setOptVidPreviewInterval",
|
||||
"setOptSimpleLightbox",
|
||||
"setOptShowTagPickerFilter",
|
||||
"setOptFeaturedFields",
|
||||
]),
|
||||
onResetClick() {
|
||||
localStorage.removeItem("sist2_configuration");
|
||||
|
||||
@@ -56,6 +56,22 @@ export default Vue.extend({
|
||||
onThumbnailClick() {
|
||||
window.open(`/f/${this.doc._id}`, "_blank");
|
||||
},
|
||||
findByCustomField(field, id) {
|
||||
return {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
[field]: id
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
size: 1
|
||||
}
|
||||
},
|
||||
findById(id) {
|
||||
return {
|
||||
query: {
|
||||
@@ -91,18 +107,13 @@ export default Vue.extend({
|
||||
|
||||
},
|
||||
mounted() {
|
||||
if (this.$store.state.sist2Info === null) {
|
||||
sist2.getSist2Info().then(data => {
|
||||
this.$store.dispatch("setSist2Info", data);
|
||||
this.$store.commit("setIndices", data.indices);
|
||||
});
|
||||
}
|
||||
|
||||
let query = null;
|
||||
if (this.$route.query.byId) {
|
||||
query = this.findById(this.$route.query.byId);
|
||||
} else if (this.$route.query.byName) {
|
||||
query = this.findByName(this.$route.query.byName);
|
||||
} else if (this.$route.query.by && this.$route.query.q) {
|
||||
query = this.findByCustomField(this.$route.query.by, this.$route.query.q)
|
||||
}
|
||||
|
||||
if (query) {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<MimePicker></MimePicker>
|
||||
</b-tab>
|
||||
<b-tab :title="$t('tags')">
|
||||
<TagPicker></TagPicker>
|
||||
<TagPicker :show-search-bar="$store.state.optShowTagPickerFilter"></TagPicker>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
</b-col>
|
||||
@@ -130,23 +130,18 @@ export default Vue.extend({
|
||||
});
|
||||
});
|
||||
|
||||
this.setIndices(this.$store.getters["sist2Info"].indices)
|
||||
|
||||
this.getDateRange().then((range: { min: number, max: number }) => {
|
||||
this.setDateBoundsMin(range.min);
|
||||
this.setDateBoundsMax(range.max);
|
||||
|
||||
sist2.getSist2Info().then(data => {
|
||||
this.setSist2Info(data);
|
||||
this.setIndices(data.indices);
|
||||
const doBlankSearch = !this.$store.state.optUpdateMimeMap;
|
||||
|
||||
Sist2Api.getMimeTypes(Sist2Query.searchQuery()).then(({mimeMap}) => {
|
||||
this.$store.commit("setUiMimeMap", mimeMap);
|
||||
this.uiLoading = false;
|
||||
this.search(true);
|
||||
});
|
||||
|
||||
}).catch(() => {
|
||||
this.showErrorToast();
|
||||
Sist2Api.getMimeTypes(Sist2Query.searchQuery(doBlankSearch)).then(({mimeMap}) => {
|
||||
this.$store.commit("setUiMimeMap", mimeMap);
|
||||
this.uiLoading = false;
|
||||
this.search(true);
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -206,7 +201,7 @@ export default Vue.extend({
|
||||
this.$store.commit("setUiReachedScrollEnd", false);
|
||||
},
|
||||
async handleSearch(resp: EsResult) {
|
||||
if (resp.hits.hits.length == 0) {
|
||||
if (resp.hits.hits.length == 0 || resp.hits.hits.length < this.$store.state.optSize) {
|
||||
this.$store.commit("setUiReachedScrollEnd", true);
|
||||
}
|
||||
|
||||
@@ -246,6 +241,8 @@ export default Vue.extend({
|
||||
this.$store.commit("setLastQueryResult", resp);
|
||||
|
||||
this.docs.push(...resp.hits.hits);
|
||||
|
||||
resp.hits.hits.forEach(hit => this.docIds.add(hit._id));
|
||||
},
|
||||
getDateRange(): Promise<{ min: number, max: number }> {
|
||||
return sist2.esQuery({
|
||||
|
||||
48
src/auth0/auth0_c_api.cpp
Normal file
48
src/auth0/auth0_c_api.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#include "auth0_c_api.h"
|
||||
#include "jwt/jwt.hpp"
|
||||
#include "iostream"
|
||||
#include "cjson/cJSON.h"
|
||||
|
||||
int auth0_verify_jwt(const char *secret_str, const char *token, const char *audience) {
|
||||
|
||||
using namespace jwt::params;
|
||||
|
||||
jwt::jwt_object object;
|
||||
try {
|
||||
object = jwt::decode(
|
||||
token,
|
||||
algorithms({"RS256"}),
|
||||
secret(secret_str),
|
||||
verify(true)
|
||||
);
|
||||
|
||||
} catch (const jwt::TokenExpiredError& e) {
|
||||
return AUTH0_ERR_EXPIRED;
|
||||
} catch (const jwt::SignatureFormatError& e) {
|
||||
return AUTH0_ERR_SIG_FORMAT;
|
||||
} catch (const jwt::DecodeError& e) {
|
||||
return AUTH0_ERR_DECODE;
|
||||
} catch (const jwt::VerificationError& e) {
|
||||
return AUTH0_ERR_VERIFICATION;
|
||||
}
|
||||
|
||||
std::stringstream buf;
|
||||
buf << object.payload();
|
||||
std::string json_payload_str = buf.str();
|
||||
cJSON *payload = cJSON_Parse(json_payload_str.c_str());
|
||||
|
||||
bool audience_ok = false;
|
||||
cJSON *aud;
|
||||
cJSON_ArrayForEach(aud, cJSON_GetObjectItem(payload, "aud")) {
|
||||
if (aud != nullptr && strcmp(aud->valuestring, audience) == 0) {
|
||||
audience_ok = true;
|
||||
}
|
||||
}
|
||||
cJSON_Delete(payload);
|
||||
|
||||
if (!audience_ok) {
|
||||
return AUTH0_ERR_AUDIENCE;
|
||||
}
|
||||
|
||||
return AUTH0_OK;
|
||||
}
|
||||
22
src/auth0/auth0_c_api.h
Normal file
22
src/auth0/auth0_c_api.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#ifndef SIST2_AUTH0_C_API_H
|
||||
#define SIST2_AUTH0_C_API_H
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
#define EXTERNC extern "C"
|
||||
#include "cstdlib"
|
||||
#else
|
||||
#define EXTERNC
|
||||
#include "stdlib.h"
|
||||
#endif
|
||||
|
||||
#define AUTH0_OK (0)
|
||||
#define AUTH0_ERR_EXPIRED (1)
|
||||
#define AUTH0_ERR_SIG_FORMAT (2)
|
||||
#define AUTH0_ERR_DECODE (3)
|
||||
#define AUTH0_ERR_VERIFICATION (4)
|
||||
#define AUTH0_ERR_AUDIENCE (5)
|
||||
|
||||
EXTERNC int auth0_verify_jwt(const char *secret, const char *token, const char* audience);
|
||||
|
||||
#endif
|
||||
214
src/cli.c
214
src/cli.c
@@ -2,16 +2,17 @@
|
||||
#include "ctx.h"
|
||||
#include <tesseract/capi.h>
|
||||
|
||||
#define DEFAULT_OUTPUT "index.sist2/"
|
||||
#define DEFAULT_OUTPUT "index.sist2"
|
||||
#define DEFAULT_NAME "index"
|
||||
#define DEFAULT_CONTENT_SIZE 32768
|
||||
#define DEFAULT_QUALITY 1
|
||||
#define DEFAULT_THUMBNAIL_SIZE 500
|
||||
#define DEFAULT_QUALITY 2
|
||||
#define DEFAULT_THUMBNAIL_SIZE 552
|
||||
#define DEFAULT_THUMBNAIL_COUNT 1
|
||||
#define DEFAULT_REWRITE_URL ""
|
||||
|
||||
#define DEFAULT_ES_URL "http://localhost:9200"
|
||||
#define DEFAULT_ES_INDEX "sist2"
|
||||
#define DEFAULT_BATCH_SIZE 100
|
||||
#define DEFAULT_BATCH_SIZE 70
|
||||
#define DEFAULT_TAGLINE "Lightning-fast file system indexer and search tool"
|
||||
#define DEFAULT_LANG "en"
|
||||
|
||||
@@ -20,8 +21,6 @@
|
||||
|
||||
#define DEFAULT_MAX_MEM_BUFFER 2000
|
||||
|
||||
#define DEFAULT_THROTTLE_MEMORY_THRESHOLD 0
|
||||
|
||||
const char *TESS_DATAPATHS[] = {
|
||||
"/usr/share/tessdata/",
|
||||
"/usr/share/tesseract-ocr/tessdata/",
|
||||
@@ -48,9 +47,6 @@ void scan_args_destroy(scan_args_t *args) {
|
||||
if (args->name != NULL) {
|
||||
free(args->name);
|
||||
}
|
||||
if (args->incremental != NULL) {
|
||||
free(args->incremental);
|
||||
}
|
||||
if (args->path != NULL) {
|
||||
free(args->path);
|
||||
}
|
||||
@@ -61,7 +57,6 @@ void scan_args_destroy(scan_args_t *args) {
|
||||
}
|
||||
|
||||
void index_args_destroy(index_args_t *args) {
|
||||
//todo
|
||||
if (args->es_mappings_path) {
|
||||
free(args->es_mappings);
|
||||
}
|
||||
@@ -76,11 +71,15 @@ void index_args_destroy(index_args_t *args) {
|
||||
}
|
||||
|
||||
void web_args_destroy(web_args_t *args) {
|
||||
//todo
|
||||
free(args);
|
||||
}
|
||||
|
||||
void exec_args_destroy(exec_args_t *args) {
|
||||
|
||||
if (args->index_path != NULL) {
|
||||
free(args->index_path);
|
||||
}
|
||||
|
||||
free(args);
|
||||
}
|
||||
|
||||
@@ -92,23 +91,17 @@ int scan_args_validate(scan_args_t *args, int argc, const char **argv) {
|
||||
|
||||
char *abs_path = abspath(argv[1]);
|
||||
if (abs_path == NULL) {
|
||||
LOG_FATALF("cli.c", "Invalid PATH argument. File not found: %s", argv[1])
|
||||
LOG_FATALF("cli.c", "Invalid PATH argument. File not found: %s", argv[1]);
|
||||
} else {
|
||||
abs_path = realloc(abs_path, strlen(abs_path) + 2);
|
||||
strcat(abs_path, "/");
|
||||
args->path = abs_path;
|
||||
}
|
||||
|
||||
if (args->incremental != OPTION_VALUE_UNSPECIFIED) {
|
||||
args->incremental = abspath(args->incremental);
|
||||
if (abs_path == NULL) {
|
||||
sist_log("main.c", LOG_SIST_WARNING, "Could not open original index! Disabled incremental scan feature.");
|
||||
args->incremental = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (args->tn_quality == OPTION_VALUE_UNSPECIFIED) {
|
||||
args->tn_quality = DEFAULT_QUALITY;
|
||||
} else if (args->tn_quality < 1.0f || args->tn_quality > 31.0f) {
|
||||
fprintf(stderr, "Invalid value for --thumbnail-quality argument: %f. Must be within [1.0, 31.0].\n",
|
||||
} else if (args->tn_quality < 2 || args->tn_quality > 31) {
|
||||
fprintf(stderr, "Invalid value for --thumbnail-quality argument: %d. Must be within [2, 31].\n",
|
||||
args->tn_quality);
|
||||
return 1;
|
||||
}
|
||||
@@ -124,6 +117,9 @@ int scan_args_validate(scan_args_t *args, int argc, const char **argv) {
|
||||
args->tn_count = DEFAULT_THUMBNAIL_COUNT;
|
||||
} else if (args->tn_count == OPTION_VALUE_DISABLE) {
|
||||
args->tn_count = 0;
|
||||
} else if (args->tn_count > 1000) {
|
||||
printf("Invalid value --thumbnail-count argument: %d. Must be <= 1000.\n", args->tn_size);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (args->content_size == OPTION_VALUE_UNSPECIFIED) {
|
||||
@@ -132,8 +128,8 @@ int scan_args_validate(scan_args_t *args, int argc, const char **argv) {
|
||||
|
||||
if (args->threads == 0) {
|
||||
args->threads = 1;
|
||||
} else if (args->threads < 0) {
|
||||
fprintf(stderr, "Invalid value for --threads: %d. Must be a positive number\n", args->threads);
|
||||
} else if (args->threads < 0 || args->threads > 256) {
|
||||
fprintf(stderr, "Invalid value for --threads: %d. Must be a positive number <= 256\n", args->threads);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -144,20 +140,24 @@ int scan_args_validate(scan_args_t *args, int argc, const char **argv) {
|
||||
args->output = expandpath(args->output);
|
||||
}
|
||||
|
||||
int ret = mkdir(args->output, S_IRUSR | S_IWUSR | S_IXUSR);
|
||||
if (ret != 0) {
|
||||
fprintf(stderr, "Invalid output: '%s' (%s).\n", args->output, strerror(errno));
|
||||
return 1;
|
||||
char *abs_output = abspath(args->output);
|
||||
if (args->incremental && abs_output == NULL) {
|
||||
LOG_WARNINGF("main.c", "Could not open original index for incremental scan: %s. Will not perform incremental scan.", args->output);
|
||||
args->incremental = FALSE;
|
||||
} else if (!args->incremental && abs_output != NULL) {
|
||||
LOG_FATALF("main.c", "Index already exists: %s. If you wish to perform incremental scan, you must specify --incremental", abs_output);
|
||||
}
|
||||
free(abs_output);
|
||||
|
||||
if (args->depth <= 0) {
|
||||
args->depth = G_MAXINT32;
|
||||
args->depth = 2147483647;
|
||||
} else {
|
||||
args->depth += 1;
|
||||
}
|
||||
|
||||
if (args->name == OPTION_VALUE_UNSPECIFIED) {
|
||||
args->name = g_path_get_basename(args->output);
|
||||
args->name = malloc(strlen(DEFAULT_NAME) + 1);
|
||||
strcpy(args->name, DEFAULT_NAME);
|
||||
} else {
|
||||
char *tmp = malloc(strlen(args->name) + 1);
|
||||
strcpy(tmp, args->name);
|
||||
@@ -216,7 +216,7 @@ int scan_args_validate(scan_args_t *args, int argc, const char **argv) {
|
||||
}
|
||||
if (trained_data_path != NULL && path != trained_data_path) {
|
||||
LOG_FATAL("cli.c", "When specifying more than one tesseract language, all the traineddata "
|
||||
"files must be in the same folder")
|
||||
"files must be in the same folder");
|
||||
}
|
||||
trained_data_path = path;
|
||||
|
||||
@@ -224,7 +224,7 @@ int scan_args_validate(scan_args_t *args, int argc, const char **argv) {
|
||||
}
|
||||
free(lang);
|
||||
|
||||
ret = TessBaseAPIInit3(api, trained_data_path, args->tesseract_lang);
|
||||
int ret = TessBaseAPIInit3(api, trained_data_path, args->tesseract_lang);
|
||||
if (ret != 0) {
|
||||
fprintf(stderr, "Could not initialize tesseract with lang '%s'\n", args->tesseract_lang);
|
||||
return 1;
|
||||
@@ -241,12 +241,12 @@ int scan_args_validate(scan_args_t *args, int argc, const char **argv) {
|
||||
|
||||
pcre *re = pcre_compile(args->exclude_regex, 0, &error, &error_offset, 0);
|
||||
if (error != NULL) {
|
||||
LOG_FATALF("cli.c", "pcre_compile returned error: %s (offset:%d)", error, error_offset)
|
||||
LOG_FATALF("cli.c", "pcre_compile returned error: %s (offset:%d)", error, error_offset);
|
||||
}
|
||||
|
||||
pcre_extra *re_extra = pcre_study(re, 0, &error);
|
||||
if (error != NULL) {
|
||||
LOG_FATALF("cli.c", "pcre_study returned error: %s", error)
|
||||
LOG_FATALF("cli.c", "pcre_study returned error: %s", error);
|
||||
}
|
||||
|
||||
ScanCtx.exclude = re;
|
||||
@@ -265,14 +265,10 @@ int scan_args_validate(scan_args_t *args, int argc, const char **argv) {
|
||||
args->max_memory_buffer_mib = DEFAULT_MAX_MEM_BUFFER;
|
||||
}
|
||||
|
||||
if (args->scan_mem_limit_mib == OPTION_VALUE_UNSPECIFIED || args->scan_mem_limit_mib == OPTION_VALUE_DISABLE) {
|
||||
args->scan_mem_limit_mib = DEFAULT_THROTTLE_MEMORY_THRESHOLD;
|
||||
}
|
||||
|
||||
if (args->list_path != OPTION_VALUE_UNSPECIFIED) {
|
||||
if (strcmp(args->list_path, "-") == 0) {
|
||||
args->list_file = stdin;
|
||||
LOG_DEBUG("cli.c", "Using stdin as list file")
|
||||
LOG_DEBUG("cli.c", "Using stdin as list file");
|
||||
} else {
|
||||
args->list_file = fopen(args->list_path, "r");
|
||||
|
||||
@@ -282,27 +278,27 @@ int scan_args_validate(scan_args_t *args, int argc, const char **argv) {
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUGF("cli.c", "arg tn_quality=%f", args->tn_quality)
|
||||
LOG_DEBUGF("cli.c", "arg tn_size=%d", args->tn_size)
|
||||
LOG_DEBUGF("cli.c", "arg tn_count=%d", args->tn_count)
|
||||
LOG_DEBUGF("cli.c", "arg content_size=%d", args->content_size)
|
||||
LOG_DEBUGF("cli.c", "arg threads=%d", args->threads)
|
||||
LOG_DEBUGF("cli.c", "arg incremental=%s", args->incremental)
|
||||
LOG_DEBUGF("cli.c", "arg output=%s", args->output)
|
||||
LOG_DEBUGF("cli.c", "arg rewrite_url=%s", args->rewrite_url)
|
||||
LOG_DEBUGF("cli.c", "arg name=%s", args->name)
|
||||
LOG_DEBUGF("cli.c", "arg depth=%d", args->depth)
|
||||
LOG_DEBUGF("cli.c", "arg path=%s", args->path)
|
||||
LOG_DEBUGF("cli.c", "arg archive=%s", args->archive)
|
||||
LOG_DEBUGF("cli.c", "arg archive_passphrase=%s", args->archive_passphrase)
|
||||
LOG_DEBUGF("cli.c", "arg tesseract_lang=%s", args->tesseract_lang)
|
||||
LOG_DEBUGF("cli.c", "arg tesseract_path=%s", args->tesseract_path)
|
||||
LOG_DEBUGF("cli.c", "arg exclude=%s", args->exclude_regex)
|
||||
LOG_DEBUGF("cli.c", "arg fast=%d", args->fast)
|
||||
LOG_DEBUGF("cli.c", "arg fast_epub=%d", args->fast_epub)
|
||||
LOG_DEBUGF("cli.c", "arg treemap_threshold=%f", args->treemap_threshold)
|
||||
LOG_DEBUGF("cli.c", "arg max_memory_buffer_mib=%d", args->max_memory_buffer_mib)
|
||||
LOG_DEBUGF("cli.c", "arg list_path=%s", args->list_path)
|
||||
LOG_DEBUGF("cli.c", "arg tn_quality=%f", args->tn_quality);
|
||||
LOG_DEBUGF("cli.c", "arg tn_size=%d", args->tn_size);
|
||||
LOG_DEBUGF("cli.c", "arg tn_count=%d", args->tn_count);
|
||||
LOG_DEBUGF("cli.c", "arg content_size=%d", args->content_size);
|
||||
LOG_DEBUGF("cli.c", "arg threads=%d", args->threads);
|
||||
LOG_DEBUGF("cli.c", "arg incremental=%d", args->incremental);
|
||||
LOG_DEBUGF("cli.c", "arg output=%s", args->output);
|
||||
LOG_DEBUGF("cli.c", "arg rewrite_url=%s", args->rewrite_url);
|
||||
LOG_DEBUGF("cli.c", "arg name=%s", args->name);
|
||||
LOG_DEBUGF("cli.c", "arg depth=%d", args->depth);
|
||||
LOG_DEBUGF("cli.c", "arg path=%s", args->path);
|
||||
LOG_DEBUGF("cli.c", "arg archive=%s", args->archive);
|
||||
LOG_DEBUGF("cli.c", "arg archive_passphrase=%s", args->archive_passphrase);
|
||||
LOG_DEBUGF("cli.c", "arg tesseract_lang=%s", args->tesseract_lang);
|
||||
LOG_DEBUGF("cli.c", "arg tesseract_path=%s", args->tesseract_path);
|
||||
LOG_DEBUGF("cli.c", "arg exclude=%s", args->exclude_regex);
|
||||
LOG_DEBUGF("cli.c", "arg fast=%d", args->fast);
|
||||
LOG_DEBUGF("cli.c", "arg fast_epub=%d", args->fast_epub);
|
||||
LOG_DEBUGF("cli.c", "arg treemap_threshold=%f", args->treemap_threshold);
|
||||
LOG_DEBUGF("cli.c", "arg max_memory_buffer_mib=%d", args->max_memory_buffer_mib);
|
||||
LOG_DEBUGF("cli.c", "arg list_path=%s", args->list_path);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -312,20 +308,20 @@ int load_external_file(const char *file_path, char **dst) {
|
||||
int res = stat(file_path, &info);
|
||||
|
||||
if (res == -1) {
|
||||
LOG_ERRORF("cli.c", "Error opening file '%s': %s\n", file_path, strerror(errno))
|
||||
LOG_ERRORF("cli.c", "Error opening file '%s': %s\n", file_path, strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int fd = open(file_path, O_RDONLY);
|
||||
if (fd == -1) {
|
||||
LOG_ERRORF("cli.c", "Error opening file '%s': %s\n", file_path, strerror(errno))
|
||||
LOG_ERRORF("cli.c", "Error opening file '%s': %s\n", file_path, strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
*dst = malloc(info.st_size + 1);
|
||||
res = read(fd, *dst, info.st_size);
|
||||
if (res < 0) {
|
||||
LOG_ERRORF("cli.c", "Error reading file '%s': %s\n", file_path, strerror(errno))
|
||||
LOG_ERRORF("cli.c", "Error reading file '%s': %s\n", file_path, strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -353,7 +349,7 @@ int index_args_validate(index_args_t *args, int argc, const char **argv) {
|
||||
|
||||
char *index_path = abspath(argv[1]);
|
||||
if (index_path == NULL) {
|
||||
LOG_FATALF("cli.c", "Invalid PATH argument. File not found: %s", argv[1])
|
||||
LOG_FATALF("cli.c", "Invalid PATH argument. File not found: %s", argv[1]);
|
||||
} else {
|
||||
args->index_path = index_path;
|
||||
}
|
||||
@@ -388,27 +384,28 @@ int index_args_validate(index_args_t *args, int argc, const char **argv) {
|
||||
args->batch_size = DEFAULT_BATCH_SIZE;
|
||||
}
|
||||
|
||||
LOG_DEBUGF("cli.c", "arg es_url=%s", args->es_url)
|
||||
LOG_DEBUGF("cli.c", "arg es_index=%s", args->es_index)
|
||||
LOG_DEBUGF("cli.c", "arg index_path=%s", args->index_path)
|
||||
LOG_DEBUGF("cli.c", "arg script_path=%s", args->script_path)
|
||||
LOG_DEBUGF("cli.c", "arg async_script=%d", args->async_script)
|
||||
LOG_DEBUGF("cli.c", "arg es_url=%s", args->es_url);
|
||||
LOG_DEBUGF("cli.c", "arg es_index=%s", args->es_index);
|
||||
LOG_DEBUGF("cli.c", "arg es_insecure_ssl=%d", args->es_insecure_ssl);
|
||||
LOG_DEBUGF("cli.c", "arg index_path=%s", args->index_path);
|
||||
LOG_DEBUGF("cli.c", "arg script_path=%s", args->script_path);
|
||||
LOG_DEBUGF("cli.c", "arg async_script=%d", args->async_script);
|
||||
|
||||
if (args->script) {
|
||||
char log_buf[5000];
|
||||
|
||||
strncpy(log_buf, args->script, sizeof(log_buf));
|
||||
*(log_buf + sizeof(log_buf) - 1) = '\0';
|
||||
LOG_DEBUGF("cli.c", "arg script=%s", log_buf)
|
||||
LOG_DEBUGF("cli.c", "arg script=%s", log_buf);
|
||||
}
|
||||
|
||||
LOG_DEBUGF("cli.c", "arg print=%d", args->print)
|
||||
LOG_DEBUGF("cli.c", "arg es_mappings_path=%s", args->es_mappings_path)
|
||||
LOG_DEBUGF("cli.c", "arg es_mappings=%s", args->es_mappings)
|
||||
LOG_DEBUGF("cli.c", "arg es_settings_path=%s", args->es_settings_path)
|
||||
LOG_DEBUGF("cli.c", "arg es_settings=%s", args->es_settings)
|
||||
LOG_DEBUGF("cli.c", "arg batch_size=%d", args->batch_size)
|
||||
LOG_DEBUGF("cli.c", "arg force_reset=%d", args->force_reset)
|
||||
LOG_DEBUGF("cli.c", "arg print=%d", args->print);
|
||||
LOG_DEBUGF("cli.c", "arg es_mappings_path=%s", args->es_mappings_path);
|
||||
LOG_DEBUGF("cli.c", "arg es_mappings=%s", args->es_mappings);
|
||||
LOG_DEBUGF("cli.c", "arg es_settings_path=%s", args->es_settings_path);
|
||||
LOG_DEBUGF("cli.c", "arg es_settings=%s", args->es_settings);
|
||||
LOG_DEBUGF("cli.c", "arg batch_size=%d", args->batch_size);
|
||||
LOG_DEBUGF("cli.c", "arg force_reset=%d", args->force_reset);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -492,28 +489,61 @@ int web_args_validate(web_args_t *args, int argc, const char **argv) {
|
||||
args->tag_auth_enabled = FALSE;
|
||||
}
|
||||
|
||||
if (args->auth0_public_key_path != NULL || args->auth0_audience != NULL || args->auth0_client_id ||
|
||||
args->auth0_domain) {
|
||||
|
||||
if (args->auth0_public_key_path == NULL) {
|
||||
fprintf(stderr, "Missing --auth0-public-key-file argument");
|
||||
return 1;
|
||||
}
|
||||
if (args->auth0_audience == NULL) {
|
||||
fprintf(stderr, "Missing --auth0-audience argument");
|
||||
return 1;
|
||||
}
|
||||
if (args->auth0_client_id == NULL) {
|
||||
fprintf(stderr, "Missing --auth0-client-id argument");
|
||||
return 1;
|
||||
}
|
||||
if (args->auth0_domain == NULL) {
|
||||
fprintf(stderr, "Missing --auth0-domain argument");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (args->auth0_public_key_path != NULL) {
|
||||
if (load_external_file(args->auth0_public_key_path, &args->auth0_public_key) != 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
args->auth0_enabled = TRUE;
|
||||
} else {
|
||||
args->auth0_enabled = FALSE;
|
||||
}
|
||||
|
||||
args->index_count = argc - 1;
|
||||
args->indices = argv + 1;
|
||||
|
||||
for (int i = 0; i < args->index_count; i++) {
|
||||
char *abs_path = abspath(args->indices[i]);
|
||||
if (abs_path == NULL) {
|
||||
LOG_FATALF("cli.c", "Index not found: %s", args->indices[i])
|
||||
LOG_FATALF("cli.c", "Index not found: %s", args->indices[i]);
|
||||
}
|
||||
free(abs_path);
|
||||
}
|
||||
|
||||
LOG_DEBUGF("cli.c", "arg es_url=%s", args->es_url)
|
||||
LOG_DEBUGF("cli.c", "arg es_index=%s", args->es_index)
|
||||
LOG_DEBUGF("cli.c", "arg tagline=%s", args->tagline)
|
||||
LOG_DEBUGF("cli.c", "arg dev=%d", args->dev)
|
||||
LOG_DEBUGF("cli.c", "arg listen=%s", args->listen_address)
|
||||
LOG_DEBUGF("cli.c", "arg credentials=%s", args->credentials)
|
||||
LOG_DEBUGF("cli.c", "arg tag_credentials=%s", args->tag_credentials)
|
||||
LOG_DEBUGF("cli.c", "arg auth_user=%s", args->auth_user)
|
||||
LOG_DEBUGF("cli.c", "arg auth_pass=%s", args->auth_pass)
|
||||
LOG_DEBUGF("cli.c", "arg index_count=%d", args->index_count)
|
||||
LOG_DEBUGF("cli.c", "arg es_url=%s", args->es_url);
|
||||
LOG_DEBUGF("cli.c", "arg es_index=%s", args->es_index);
|
||||
LOG_DEBUGF("cli.c", "arg es_insecure_ssl=%d", args->es_insecure_ssl);
|
||||
LOG_DEBUGF("cli.c", "arg tagline=%s", args->tagline);
|
||||
LOG_DEBUGF("cli.c", "arg dev=%d", args->dev);
|
||||
LOG_DEBUGF("cli.c", "arg listen=%s", args->listen_address);
|
||||
LOG_DEBUGF("cli.c", "arg credentials=%s", args->credentials);
|
||||
LOG_DEBUGF("cli.c", "arg tag_credentials=%s", args->tag_credentials);
|
||||
LOG_DEBUGF("cli.c", "arg auth_user=%s", args->auth_user);
|
||||
LOG_DEBUGF("cli.c", "arg auth_pass=%s", args->auth_pass);
|
||||
LOG_DEBUGF("cli.c", "arg index_count=%d", args->index_count);
|
||||
for (int i = 0; i < args->index_count; i++) {
|
||||
LOG_DEBUGF("cli.c", "arg indices[%d]=%s", i, args->indices[i])
|
||||
LOG_DEBUGF("cli.c", "arg indices[%d]=%s", i, args->indices[i]);
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -538,7 +568,7 @@ int exec_args_validate(exec_args_t *args, int argc, const char **argv) {
|
||||
|
||||
char *index_path = abspath(argv[1]);
|
||||
if (index_path == NULL) {
|
||||
LOG_FATALF("cli.c", "Invalid index PATH argument. File not found: %s", argv[1])
|
||||
LOG_FATALF("cli.c", "Invalid index PATH argument. File not found: %s", argv[1]);
|
||||
} else {
|
||||
args->index_path = index_path;
|
||||
}
|
||||
@@ -559,12 +589,12 @@ int exec_args_validate(exec_args_t *args, int argc, const char **argv) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
LOG_DEBUGF("cli.c", "arg script_path=%s", args->script_path)
|
||||
LOG_DEBUGF("cli.c", "arg script_path=%s", args->script_path);
|
||||
|
||||
char log_buf[5000];
|
||||
strncpy(log_buf, args->script, sizeof(log_buf));
|
||||
*(log_buf + sizeof(log_buf) - 1) = '\0';
|
||||
LOG_DEBUGF("cli.c", "arg script=%s", log_buf)
|
||||
LOG_DEBUGF("cli.c", "arg script=%s", log_buf);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user