mirror of
https://github.com/simon987/sist2.git
synced 2025-12-12 15:08:53 +00:00
Compare commits
399 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | ||
| 2882741926 | |||
| edba9b7917 | |||
| e89964d592 | |||
| 329afcbe4f | |||
| 2a2664a5cd | |||
| 0d18637e88 | |||
| 8ad9fc9e32 | |||
| f075b542fe | |||
| 3d4331b27d | |||
| a0db49e7d8 | |||
| 065146ff8a | |||
| d58fcbc788 | |||
| b483447b1c | |||
|
|
0d68d5fc7f | ||
|
|
1813bf505c | ||
|
|
9a6e7c7c47 | ||
|
|
68252b4e80 | ||
|
|
d1f13f2c84 | ||
|
|
6075c21a3a | ||
|
|
f3674ffa02 | ||
|
|
de187eff1c | ||
|
|
8e96174e1f | ||
| 8fa34da02f | |||
| 37919932de | |||
| 8ab8124370 | |||
| bfd080943d | |||
| c6820b6cc6 | |||
| 3c09c45694 | |||
|
|
bb5c17ec78 | ||
|
|
501064da10 | ||
|
|
8f7edf3190 | ||
|
|
e65905a165 | ||
|
|
2cb57f3634 | ||
|
|
679e12f786 | ||
|
|
291d307689 | ||
|
|
7d40b9e959 | ||
| cf56bdfb74 | |||
| b799a2e976 | |||
| 727b57b78a | |||
| 61cb845a0e | |||
| dad14fb66d | |||
| c98a09d264 | |||
| b978132ee0 | |||
| 4dedd281f1 | |||
| 65c499e477 | |||
| 625f3d0d6e | |||
| 64b8aab8bf | |||
| ad95684771 | |||
| b37e5a4ad4 | |||
| 15ae2190cf | |||
| 255bc2d689 | |||
| fe1aa6dd4c | |||
| cd2a44e016 | |||
| ed2a3f342a | |||
| 1107fe9a53 | |||
| a96e65d039 | |||
| 87936eecd4 | |||
|
|
d817a0e9dd | ||
|
|
94a5e0ac59 | ||
| d40f5052f9 | |||
| ee9a8fa514 | |||
| 81008d8936 | |||
| 52466d5d8a | |||
| 5f73fc024b | |||
| f2fd7ccf41 | |||
| d87fee8e00 | |||
|
|
672d1344d7 | ||
| 27e32db1ed | |||
| bb91139ffb | |||
| 70cfa8c37c | |||
| 7493dedc8c | |||
| c786a31bb2 | |||
| 48d024e751 | |||
| 08b2ca9d43 | |||
| ed8b4f4fad | |||
| 66de93a8bd | |||
| e3f78fb693 | |||
| 030643cee0 | |||
| b17b9439df | |||
| 414f65346c | |||
| be8eedc9c7 | |||
| 5b62fe77f2 | |||
| 61ab68ce15 | |||
| 82ecb8bb85 | |||
| a41b5dcc1f | |||
| 06f21d5f0f | |||
| e82a388d1e | |||
| bf02e571b3 | |||
| 750a392a61 | |||
| 3d7b977a82 | |||
| cd71551a22 | |||
| 58741058cf | |||
| 0a7e59b646 | |||
| 43a566fe2f | |||
| b2631a86c8 | |||
| d0a1deca30 | |||
| b03ce90a05 | |||
| a5eacb4950 | |||
| 0887046b41 | |||
| 17fda1e540 | |||
| 34b363bfd8 | |||
| c9aa4bed72 | |||
| 7267d4bd2c | |||
| 43470e9ce6 | |||
| 0331d46fff | |||
| bbf1aca936 | |||
| 27560a82bb | |||
| f16ead1902 | |||
| e2e07e80c7 | |||
| 9499c6b189 | |||
| c5cd00b76c | |||
| ec5f07cab8 | |||
| f098f7916a | |||
| 85d67a9393 | |||
| c5ac89813f | |||
| ec5642a3df | |||
| c1de74e7eb | |||
| f31f138f2e | |||
| 6a48b219e6 | |||
| 97e56b664c | |||
| 0acdaab31a | |||
| d0290b886b | |||
| 4eea376869 | |||
| f4e1d90a6b | |||
| 391d8ed9d9 | |||
| 89fd68be8e | |||
| 9c0f3e0e31 | |||
| 648e4817c4 | |||
| bb23651087 | |||
| 78f3c897e2 | |||
| a2209e91ca | |||
| ba31531d3a | |||
| d5a47b8dab | |||
| d8c0b80524 | |||
| 142a4869e6 | |||
| ddb7f8d5d7 | |||
| dfb8c67490 | |||
| 3da2c8cae3 | |||
| 2f0e999b06 | |||
| bf28dc8993 | |||
| c6fee7f6e2 | |||
| 201c2a1a47 | |||
| 7c46ad632a | |||
| 5b8c13fd13 | |||
| efa4a06e56 | |||
| 81670ee107 | |||
| f9dac80905 | |||
| f8d9b718c0 | |||
| 6f5fdc2935 | |||
| a01f6dff1f | |||
| 22dd58e140 | |||
| f3e07fb7f7 | |||
| 7990e5cd2e | |||
| e3ca660983 | |||
| b87fb25458 | |||
| c7a77869ad | |||
| 523c123e2e | |||
| fc7f30d670 | |||
| 152fe11669 | |||
| 33f97f6bfb | |||
| 71f9dfcfe0 | |||
| 5f657d61b3 | |||
| 908def1016 | |||
| db3d312835 | |||
| 32c9cb28a3 | |||
| f839127129 | |||
| 8111a6c143 | |||
| 707a570828 | |||
|
|
5073b00225 | ||
|
|
4923d1b51f | ||
|
|
097e332015 | ||
|
|
d4babe216b | ||
|
|
44511a2202 | ||
| 50771bd1dc | |||
| bc884e137c | |||
| ce1e241dea | |||
| 5fe9c9efa3 | |||
| 75e4e93ddd | |||
| 013c54daa0 | |||
| 54308ef5e2 | |||
| 638c2a5c1a | |||
| 9587caddd9 | |||
| f5bbe0dc97 | |||
| f87eac1f90 | |||
| ddafbab6a6 | |||
| b91d574756 | |||
| 576140e542 | |||
| 050c1283a3 | |||
| c6e1ba03bc | |||
| 10e32f707f | |||
| 86e83bafaf | |||
| 51a40c8819 | |||
|
|
36281a5108 | ||
|
|
76a0bda48b | ||
| 0cf29a660c | |||
| 6cd0741848 | |||
| bc120f349d | |||
| 8cac8c98d7 | |||
| 30921ac52e | |||
| 95bbe39afc | |||
| 72ce217f9c | |||
| 641a8ec90c | |||
| 7a505c2287 | |||
| 12f162d760 | |||
| 4b4ab12fac | |||
| ae283f77ad | |||
| d3bd53a5ea | |||
| f7887f24d1 | |||
| 5c8de19188 | |||
| d861d278a4 | |||
| b6ddeee0e0 | |||
| 0cd2523b05 | |||
| 5e798f9367 | |||
| 5da6c1488b | |||
| 9568e25f84 | |||
| 6a8027789a | |||
| b1d16d8abf | |||
| b2a157e24d | |||
| 9aead9389a | |||
| a32c68cba8 | |||
| d116cf9d91 | |||
|
|
a020a8b32c | ||
| 5d5d9c3092 | |||
| 3379d5ce71 | |||
| a0ff4a1f01 | |||
| 4589f3bde7 | |||
| 1c898640cf | |||
| a0739d5177 | |||
| 8f9d29dbc6 | |||
| 3ff4b70223 | |||
| 02ad035b09 | |||
| c11feb213d | |||
| 72902947cd | |||
| a18bb81222 | |||
| 1520288f19 | |||
| e507de194b | |||
| 0e517d5e2b | |||
| 8223ef3860 | |||
| 995a196690 | |||
| 465d017e18 | |||
| ca994d3914 | |||
| db2285973f | |||
| 61de9e9f14 | |||
| 3015ef0ff4 | |||
| b55d432841 | |||
| ed90a140ce | |||
| 052df82373 | |||
| 5676136777 | |||
| c061613302 | |||
| d0325fd9b9 | |||
| e05a6f3863 | |||
| f1690a9cca | |||
| 100a264413 | |||
| 29390bb454 | |||
| 4d43036ded | |||
| 0b5cdbd130 | |||
| 53d7695f66 | |||
| 8d53456404 | |||
| cbc08a7cc9 | |||
| e629b4d7d3 | |||
| 22f7073b39 | |||
| 1781a74960 | |||
| db96c95ac7 | |||
| 7b9fa4cc0a | |||
| 5cc1fa86a9 | |||
| 649689ce30 | |||
| c8536f65a8 | |||
| 75b5e249c1 | |||
|
|
f49e03ac79 | ||
| a6d2afc8dc | |||
| 8f8f66ba05 | |||
| 1d9fcf7105 | |||
| 8127745f2b | |||
| 230988d6d1 | |||
| 13f4dbed2d | |||
| ed15e89f45 | |||
| c636d3d921 | |||
| 7e92d4b7d1 | |||
| 8ffe780ab2 | |||
| d3c8928fe8 | |||
| d9f628fca4 | |||
| 68289268c1 | |||
| 649c50c465 | |||
| 7b49a0dc49 | |||
| eb559b53aa | |||
| 6d01f9c0df | |||
| e724fec668 | |||
| fe5e93b300 | |||
| ecad85fd7d | |||
| 74cc898259 | |||
| dc2e4443c4 | |||
| 1a64431b52 | |||
|
|
9bad515e06 | ||
| 648559cedb | |||
| 3e6cd9cd5c | |||
| f249992798 | |||
|
|
e9645ecdaa | ||
| 046edea0e2 | |||
| a011b7e97b | |||
| 8c1c1697e0 | |||
| 018b49fa4c | |||
| 27b4e6403e | |||
| 13fdbd9e69 | |||
| 5e7fdaf8dd | |||
| 19d5c8ac9f | |||
| 99497049a8 | |||
|
|
1a3181d78b | ||
| 449aa77c8f | |||
| 3058c55510 | |||
| dedf9287b2 | |||
| ab199b0c0c | |||
| c4fbae123e | |||
| dd2397ef5c | |||
| ee0f71f4d3 | |||
| 0bbb96b149 | |||
| 78f6e16701 | |||
| 4625bca9a9 | |||
| f2ae653886 | |||
| 5686bc864d | |||
| cf513b4ad8 | |||
| 013423424e |
36
.dockerignore
Normal file
36
.dockerignore
Normal file
@@ -0,0 +1,36 @@
|
||||
.idea/
|
||||
*.sist2
|
||||
docs/
|
||||
test_i/
|
||||
test_i_inc/
|
||||
Testing/
|
||||
.drone.yml
|
||||
**/cmake_install.cmake
|
||||
**/CMakeCache.txt
|
||||
**/CMakeFiles/
|
||||
.cmake
|
||||
LICENSE
|
||||
Makefile
|
||||
**/*.md
|
||||
**/*.cbp
|
||||
VERSION
|
||||
**/node_modules/
|
||||
.git/
|
||||
sist2-*-linux-debug
|
||||
sist2-*-linux
|
||||
sist2_debug
|
||||
sist2
|
||||
**/libscan-test-files
|
||||
**/scan_ub_test
|
||||
**/scan_a_test
|
||||
**/scan_test
|
||||
**/ext_ffmpeg
|
||||
**/ext_libmobi
|
||||
**/ext_libwpd
|
||||
**/core
|
||||
*.a
|
||||
tmp_scan/
|
||||
Dockerfile
|
||||
Dockerfile.arm64
|
||||
docker-compose.yml
|
||||
state.db
|
||||
88
.drone.yml
Normal file
88
.drone.yml
Normal file
@@ -0,0 +1,88 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: amd64
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: simon987/sist2-build
|
||||
commands:
|
||||
- ./scripts/build.sh
|
||||
- name: scp files
|
||||
image: appleboy/drone-scp
|
||||
settings:
|
||||
host:
|
||||
from_secret: SSH_HOST
|
||||
port:
|
||||
from_secret: SSH_PORT
|
||||
user:
|
||||
from_secret: SSH_USER
|
||||
key:
|
||||
from_secret: SSH_KEY
|
||||
target: /files/sist2/${DRONE_REPO_OWNER}_${DRONE_REPO_NAME}/${DRONE_BRANCH}_${DRONE_BUILD_NUMBER}_${DRONE_COMMIT}/
|
||||
source:
|
||||
- ./VERSION
|
||||
- ./sist2-x64-linux
|
||||
- ./sist2-x64-linux-debug
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username:
|
||||
from_secret: DOCKER_USER
|
||||
password:
|
||||
from_secret: DOCKER_PASSWORD
|
||||
repo: simon987/sist2
|
||||
context: ./
|
||||
dockerfile: ./Dockerfile
|
||||
auto_tag: true
|
||||
auto_tag_suffix: x64-linux
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: arm64
|
||||
|
||||
platform:
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: simon987/sist2-build-arm64
|
||||
commands:
|
||||
- ./scripts/build_arm64.sh
|
||||
- name: scp files
|
||||
image: appleboy/drone-scp
|
||||
settings:
|
||||
host:
|
||||
from_secret: SSH_HOST
|
||||
port:
|
||||
from_secret: SSH_PORT
|
||||
user:
|
||||
from_secret: SSH_USER
|
||||
key:
|
||||
from_secret: SSH_KEY
|
||||
target: /files/sist2/${DRONE_REPO_OWNER}_${DRONE_REPO_NAME}/arm_${DRONE_BRANCH}_${DRONE_BUILD_NUMBER}_${DRONE_COMMIT}/
|
||||
source:
|
||||
- ./sist2-arm64-linux
|
||||
- ./sist2-arm64-linux-debug
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username:
|
||||
from_secret: DOCKER_USER
|
||||
password:
|
||||
from_secret: DOCKER_PASSWORD
|
||||
repo: simon987/sist2
|
||||
context: ./
|
||||
dockerfile: ./Dockerfile.arm64
|
||||
auto_tag: true
|
||||
auto_tag_suffix: arm64-linux
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -1,3 +1,3 @@
|
||||
CMakeModules/* linguist-vendored
|
||||
web/js/*.min.js linguist-vendored
|
||||
web/css/*.min.css linguist-vendored
|
||||
**/*_generated.c linguist-vendored
|
||||
**/*_generated.h linguist-vendored
|
||||
|
||||
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: "🐞 Bug Report"
|
||||
about: Submit a bug report
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Device Information (please complete the following information):**
|
||||
- OS: `[e.g., Ubuntu 20.04, WSL2]`
|
||||
- Deployment: `[Linux, Linux ARM64 or Docker]`
|
||||
- Browser *(if relevant)*: `[e.g., chrome, safari]`
|
||||
- SIST2 Version: `[e.g., v2.9.0]`
|
||||
- Elasticsearch Version *(if relevant)* : ``
|
||||
|
||||
**Command with arguments**
|
||||
<!-- `ex: "scan ~/Documents -o ./i2 --threads 3 -q 1.0` -->
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**Steps To Reproduce**
|
||||
Please be specific!
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. etc.
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Actual Behavior**
|
||||
<!-- A clear and concise description of what actually happens. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. If applicable, please include why you think the bug is occurring and/or troubleshooting you have already performed. -->
|
||||
<!-- If the issue is related to the `scan` module, please attach the files necessary to reproduce the error or email them to me[at]simon987.net. -->
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: SIST2 Documentation
|
||||
url: https://github.com/simon987/sist2/blob/master/docs/USAGE.md
|
||||
about: Check out the SIST2 documentation for answers to common questions
|
||||
18
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
18
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: "🚀 Feature Request"
|
||||
about: Suggest an idea for SIST2
|
||||
title: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
**Which SIST2 component is your Feature Request related to?**
|
||||
<!-- e.g., Scan, Index, or Web? -->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. e.g., "I'm always frustrated when [...]" -->
|
||||
|
||||
**What would you like to see happen?**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
4
.github/ISSUE_TEMPLATE/issue-template.md
vendored
4
.github/ISSUE_TEMPLATE/issue-template.md
vendored
@@ -9,7 +9,9 @@ assignees: ''
|
||||
|
||||
sist2 version:
|
||||
|
||||
Platform (please indicate if you're using Docker):
|
||||
Platform (Linux or Docker, x86-64 or arm64):
|
||||
|
||||
Elasticsearch version:
|
||||
|
||||
Command with arguments: `ex: "scan ~/Documents -o ./i2 --threads 3 -q 1.0`
|
||||
|
||||
|
||||
22
.gitignore
vendored
22
.gitignore
vendored
@@ -1,6 +1,5 @@
|
||||
.idea
|
||||
thumbs
|
||||
test
|
||||
*.cbp
|
||||
CMakeCache.txt
|
||||
CMakeFiles
|
||||
@@ -10,10 +9,27 @@ Makefile
|
||||
*.out
|
||||
LOG
|
||||
sist2*
|
||||
index.sist2/
|
||||
!sist2-vue/
|
||||
!sist2-admin
|
||||
!sist2_admin
|
||||
!sist2.py
|
||||
*.sist2/
|
||||
bundle*.css
|
||||
bundle.js
|
||||
*.a
|
||||
vgcore.*
|
||||
build/
|
||||
third-party/
|
||||
third-party/argparse
|
||||
*.idx/
|
||||
VERSION
|
||||
git_hash.h
|
||||
Testing/
|
||||
test_i
|
||||
test_i_inc
|
||||
node_modules/
|
||||
.cmake/
|
||||
i_inc/
|
||||
state.db
|
||||
*.pyc
|
||||
!sist2-admin/frontend/dist
|
||||
*.js.map
|
||||
|
||||
14
.gitmodules
vendored
14
.gitmodules
vendored
@@ -1,6 +1,12 @@
|
||||
[submodule "third-party/libscan"]
|
||||
path = third-party/libscan
|
||||
url = https://github.com/simon987/libscan
|
||||
[submodule "third-party/argparse"]
|
||||
path = third-party/argparse
|
||||
url = https://github.com/cofyc/argparse
|
||||
url = https://github.com/simon987/argparse
|
||||
[submodule "third-party/libscan/third-party/utf8.h"]
|
||||
path = third-party/libscan/third-party/utf8.h
|
||||
url = https://github.com/sheredom/utf8.h
|
||||
[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
|
||||
|
||||
69
.teamcity/settings.kts
vendored
69
.teamcity/settings.kts
vendored
@@ -1,69 +0,0 @@
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.*
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.ExecBuildStep
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.exec
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs
|
||||
import jetbrains.buildServer.configs.kotlin.v2019_2.vcs.GitVcsRoot
|
||||
|
||||
/*
|
||||
The settings script is an entry point for defining a TeamCity
|
||||
project hierarchy. The script should contain a single call to the
|
||||
project() function with a Project instance or an init function as
|
||||
an argument.
|
||||
|
||||
VcsRoots, BuildTypes, Templates, and subprojects can be
|
||||
registered inside the project using the vcsRoot(), buildType(),
|
||||
template(), and subProject() methods respectively.
|
||||
|
||||
To debug settings scripts in command-line, run the
|
||||
|
||||
mvnDebug org.jetbrains.teamcity:teamcity-configs-maven-plugin:generate
|
||||
|
||||
command and attach your debugger to the port 8000.
|
||||
|
||||
To debug in IntelliJ Idea, open the 'Maven Projects' tool window (View
|
||||
-> Tool Windows -> Maven Projects), find the generate task node
|
||||
(Plugins -> teamcity-configs -> teamcity-configs:generate), the
|
||||
'Debug' option is available in the context menu for the task.
|
||||
*/
|
||||
|
||||
version = "2019.2"
|
||||
|
||||
project {
|
||||
|
||||
vcsRoot(HttpsGithubComSimon987sist2refsHeadsMaster)
|
||||
|
||||
buildType(Build)
|
||||
}
|
||||
|
||||
object Build : BuildType({
|
||||
name = "Build"
|
||||
|
||||
artifactRules = """
|
||||
sist2
|
||||
sist2_scan
|
||||
""".trimIndent()
|
||||
|
||||
vcs {
|
||||
root(HttpsGithubComSimon987sist2refsHeadsMaster)
|
||||
}
|
||||
|
||||
steps {
|
||||
exec {
|
||||
name = "Build"
|
||||
path = "./ci/build.sh"
|
||||
dockerImage = "simon987/general_ci"
|
||||
dockerImagePlatform = ExecBuildStep.ImagePlatform.Linux
|
||||
dockerPull = true
|
||||
}
|
||||
}
|
||||
|
||||
triggers {
|
||||
vcs {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
object HttpsGithubComSimon987sist2refsHeadsMaster : GitVcsRoot({
|
||||
name = "https://github.com/simon987/sist2#refs/heads/master"
|
||||
url = "https://github.com/simon987/sist2"
|
||||
})
|
||||
@@ -1,16 +1,27 @@
|
||||
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_FAST "Enable more optimisation flags" off)
|
||||
option(SIST_FAKE_STORE "Disable IO operations of LMDB stores for debugging purposes" 0)
|
||||
|
||||
add_compile_definitions(
|
||||
"SIST_PLATFORM=${SIST_PLATFORM}"
|
||||
)
|
||||
|
||||
if (SIST_DEBUG)
|
||||
add_compile_definitions(
|
||||
"SIST_DEBUG=${SIST_DEBUG}"
|
||||
)
|
||||
endif()
|
||||
|
||||
add_subdirectory(third-party/libscan)
|
||||
set(ARGPARSE_SHARED off)
|
||||
add_subdirectory(third-party/argparse)
|
||||
|
||||
add_executable(
|
||||
sist2
|
||||
add_executable(sist2
|
||||
src/main.c
|
||||
src/sist.h
|
||||
src/io/walk.h src/io/walk.c
|
||||
@@ -25,23 +36,32 @@ add_executable(
|
||||
src/util.c src/util.h
|
||||
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/auth0/auth0_c_api.h src/auth0/auth0_c_api.cpp
|
||||
|
||||
# argparse
|
||||
third-party/argparse/argparse.h third-party/argparse/argparse.c
|
||||
|
||||
src/cli.c src/cli.h
|
||||
)
|
||||
)
|
||||
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-glib CONFIG REQUIRED)
|
||||
find_package(unofficial-mongoose CONFIG REQUIRED)
|
||||
find_library(UUID_LIB NAMES uuid)
|
||||
|
||||
#find_package(OpenSSL REQUIRED)
|
||||
find_package(CURL CONFIG REQUIRED)
|
||||
find_library(MAGIC_LIB
|
||||
NAMES libmagic.so.1 magic
|
||||
PATHS /usr/lib/x86_64-linux-gnu/ /usr/lib/aarch64-linux-gnu/
|
||||
)
|
||||
|
||||
|
||||
target_include_directories(
|
||||
@@ -50,13 +70,13 @@ 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(
|
||||
sist2
|
||||
PRIVATE
|
||||
-fPIC
|
||||
-Werror
|
||||
)
|
||||
|
||||
if (SIST_DEBUG)
|
||||
@@ -67,22 +87,36 @@ if (SIST_DEBUG)
|
||||
-fstack-protector
|
||||
-fno-omit-frame-pointer
|
||||
-fsanitize=address
|
||||
-fno-inline
|
||||
# -O2
|
||||
)
|
||||
target_link_options(
|
||||
sist2
|
||||
PRIVATE
|
||||
-fsanitize=address
|
||||
# -static
|
||||
-static-libasan
|
||||
)
|
||||
set_target_properties(
|
||||
sist2
|
||||
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
|
||||
@@ -102,15 +136,17 @@ target_link_libraries(
|
||||
lmdb
|
||||
cjson
|
||||
argparse
|
||||
unofficial::glib::glib
|
||||
${GLIB_LDFLAGS}
|
||||
unofficial::mongoose::mongoose
|
||||
# OpenSSL::SSL OpenSSL::Crypto
|
||||
CURL::libcurl
|
||||
|
||||
${UUID_LIB}
|
||||
pthread
|
||||
magic
|
||||
|
||||
c
|
||||
|
||||
scan
|
||||
|
||||
${MAGIC_LIB}
|
||||
)
|
||||
|
||||
add_custom_target(
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
rm ./sist2 sist2_debug
|
||||
cp ../sist2.gz .
|
||||
gzip -d sist2.gz
|
||||
strip sist2
|
||||
|
||||
version=$(./sist2 --version)
|
||||
|
||||
echo "Version ${version}"
|
||||
docker build . -t simon987/sist2:${version} -t simon987/sist2:latest
|
||||
|
||||
docker push simon987/sist2:${version}
|
||||
docker push simon987/sist2:latest
|
||||
|
||||
docker run --rm simon987/sist2 -v
|
||||
46
Dockerfile
Normal file
46
Dockerfile
Normal file
@@ -0,0 +1,46 @@
|
||||
FROM simon987/sist2-build as build
|
||||
MAINTAINER simon987 <me@simon987.net>
|
||||
|
||||
WORKDIR /build/
|
||||
|
||||
COPY scripts scripts
|
||||
COPY schema schema
|
||||
COPY CMakeLists.txt .
|
||||
COPY third-party third-party
|
||||
COPY src src
|
||||
COPY sist2-vue sist2-vue
|
||||
|
||||
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@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 libpq-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /usr/share/tessdata && \
|
||||
cd /usr/share/tessdata/ && \
|
||||
curl -o /usr/share/tessdata/hin.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/hin.traineddata &&\
|
||||
curl -o /usr/share/tessdata/jpn.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/jpn.traineddata &&\
|
||||
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/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/chi_sim.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/chi_sim.traineddata
|
||||
|
||||
# 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 sist2-admin/ sist2-admin/
|
||||
@@ -1,9 +1,15 @@
|
||||
FROM ubuntu:19.10
|
||||
FROM simon987/sist2-build-arm64 as build
|
||||
MAINTAINER simon987 <me@simon987.net>
|
||||
|
||||
RUN apt update
|
||||
RUN apt install -y libglib2.0-0 libcurl4 libmagic1 libharfbuzz-bin libopenjp2-7 libarchive13 liblzma5 libzstd1 liblz4-1 \
|
||||
curl libtiff5 libpng16-16 libpcre3
|
||||
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
|
||||
|
||||
FROM --platform="linux/arm64/v8" ubuntu:20.04
|
||||
|
||||
RUN apt update && apt install -y curl libasan5 && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /usr/share/tessdata && \
|
||||
cd /usr/share/tessdata/ && \
|
||||
@@ -12,11 +18,11 @@ 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 && ls -lh
|
||||
|
||||
ADD sist2 /root/sist2
|
||||
curl -o /usr/share/tessdata/spa.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/spa.traineddata
|
||||
|
||||
ENV LANG C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
|
||||
ENTRYPOINT ["/root/sist2"]
|
||||
|
||||
COPY --from=build /build/sist2 /root/sist2
|
||||
132
README.md
132
README.md
@@ -1,6 +1,8 @@
|
||||

|
||||
[](https://www.codefactor.io/repository/github/simon987/sist2)
|
||||
[/statusIcon)](https://files.simon987.net/artifacts/Sist2/Build/)
|
||||
[](https://files.simon987.net/.gate/sist2/simon987_sist2/)
|
||||
|
||||
**Demo**: [sist2.simon987.net](https://sist2.simon987.net/)
|
||||
|
||||
# sist2
|
||||
|
||||
@@ -8,124 +10,152 @@ sist2 (Simple incremental search tool)
|
||||
|
||||
*Warning: sist2 is in early development*
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
* Fast, low memory usage, multi-threaded
|
||||
* Mobile-friendly Web interface
|
||||
* Portable (all its features are packaged in a single executable)
|
||||
* Extracts text from common file types \*
|
||||
* Extracts text and metadata from common file types \*
|
||||
* Generates thumbnails \*
|
||||
* Incremental scanning
|
||||
* Automatic tagging from file attributes via [user scripts](scripting/README.md)
|
||||
* Manual tagging from the UI and automatic tagging based on file attributes via [user scripts](docs/scripting.md)
|
||||
* Recursive scan inside archive files \*\*
|
||||
* OCR support with tesseract \*\*\*
|
||||
|
||||
* Stats page & disk utilisation visualization
|
||||
|
||||
\* See [format support](#format-support)
|
||||
\*\* See [Archive files](#archive-files)
|
||||
\*\*\* See [OCR](#ocr)
|
||||
|
||||

|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Have an Elasticsearch (>= 6.X.X) instance running
|
||||
1. Have an Elasticsearch (>= 6.8.X, ideally >=7.14.0) instance running
|
||||
1. Download [from official website](https://www.elastic.co/downloads/elasticsearch)
|
||||
1. *(or)* Run using docker:
|
||||
```bash
|
||||
docker run -d --name es1 --net sist2_net -p 9200:9200 \
|
||||
-e "discovery.type=single-node" elasticsearch:7.5.2
|
||||
docker run -d -p 9200:9200 -e "discovery.type=single-node" elasticsearch:7.14.0
|
||||
```
|
||||
1. *(or)* Run using docker-compose:
|
||||
```yaml
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:7.5.2
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:7.14.0
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- "ES_JAVA_OPTS=-Xms1G -Xmx2G"
|
||||
```
|
||||
1. Download sist2 executable
|
||||
1. Download the [latest sist2 release](https://github.com/simon987/sist2/releases) *
|
||||
1. *(or)* Download a [development snapshot](https://files.simon987.net/artifacts/Sist2/Build/) *(Not recommended!)*
|
||||
1. *(or)* `docker pull simon987/sist2:latest`
|
||||
|
||||
1. See [Usage guide](DOCS/USAGE.md)
|
||||
1. Download the [latest sist2 release](https://github.com/simon987/sist2/releases).
|
||||
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.12.1-x64-linux`
|
||||
|
||||
1. See [Usage guide](docs/USAGE.md)
|
||||
|
||||
\* *Windows users*: **sist2** runs under [WSL](https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux)
|
||||
|
||||
|
||||
## Example usage
|
||||
|
||||
See [Usage guide](DOCS/USAGE.md) for more details
|
||||
See [Usage guide](docs/USAGE.md) for more details
|
||||
|
||||
1. Scan a directory: `sist2 scan ~/Documents -o ./docs_idx`
|
||||
1. Push index to Elasticsearch: `sist2 index ./docs_idx`
|
||||
1. Start web interface: `sist2 web ./docs_idx`
|
||||
|
||||
|
||||
## Format support
|
||||
|
||||
File type | Library | Content | Thumbnail | Metadata
|
||||
:---|:---|:---|:---|:---
|
||||
pdf,xps,cbz,cbr,fb2,epub | MuPDF | text+ocr | yes, `png` | title |
|
||||
`audio/*` | ffmpeg | - | yes, `jpeg` | ID3 tags |
|
||||
`video/*` | ffmpeg | - | yes, `jpeg` | title, comment, artist |
|
||||
`image/*` | ffmpeg | - | yes, `jpeg` | [Common EXIF tags](https://github.com/simon987/sist2/blob/efdde2734eca9b14a54f84568863b7ffd59bdba3/src/parsing/media.c#L190) |
|
||||
ttf,ttc,cff,woff,fnt,otf | Freetype2 | - | yes, `bmp` | Name & style |
|
||||
`text/plain` | *(none)* | yes | no | - |
|
||||
html, xml | *(none)* | yes | no | - |
|
||||
tar, zip, rar, 7z, ar ... | Libarchive | yes\* | - | no |
|
||||
docx, xlsx, pptx | *(none)* | yes | no | creator, modified_by, title |
|
||||
mobi, azw, azw3 | libmobi | yes | no | author, title |
|
||||
| File type | Library | Content | Thumbnail | Metadata |
|
||||
|:--------------------------------------------------------------------------|:-----------------------------------------------------------------------------|:---------|:------------|:---------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| pdf,xps,fb2,epub | MuPDF | text+ocr | yes | author, title |
|
||||
| cbz,cbr | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | - | yes | - |
|
||||
| `audio/*` | ffmpeg | - | yes | ID3 tags |
|
||||
| `video/*` | ffmpeg | - | yes | title, comment, artist |
|
||||
| `image/*` | ffmpeg | ocr | yes | [Common EXIF tags](https://github.com/simon987/sist2/blob/efdde2734eca9b14a54f84568863b7ffd59bdba3/src/parsing/media.c#L190), GPS tags |
|
||||
| raw, rw2, dng, cr2, crw, dcr, k25, kdc, mrw, pef, xf3, arw, sr2, srf, erf | LibRaw | no | yes | Common EXIF tags, GPS tags |
|
||||
| ttf,ttc,cff,woff,fnt,otf | Freetype2 | - | yes, `bmp` | Name & style |
|
||||
| `text/plain` | [libscan](https://github.com/simon987/sist2/tree/master/third-party/libscan) | yes | no | - |
|
||||
| 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 |
|
||||
| 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 | - | - |
|
||||
|
||||
\* *See [Archive files](#archive-files)*
|
||||
|
||||
### Archive files
|
||||
**sist2** will scan files stored into archive files (zip, tar, 7z...) as if
|
||||
they were directly in the file system. Recursive (archives inside archives)
|
||||
|
||||
**sist2** will scan files stored into archive files (zip, tar, 7z...) as if they were directly in the file system.
|
||||
Recursive (archives inside archives)
|
||||
scan is also supported.
|
||||
|
||||
**Limitations**:
|
||||
* Parsing media files with formats that require
|
||||
*seek* (e.g. `.gif`, `.mp4` w/ fragmented metadata etc.) is not supported.
|
||||
|
||||
* Support for parsing media files with formats that require *seek* (e.g. `.gif`, `.mp4` w/ fragmented metadata etc.)
|
||||
is limitted (see `--mem-buffer` option)
|
||||
* Archive files are scanned sequentially, by a single thread. On systems where
|
||||
**sist2** is not I/O bound, scans might be faster when larger archives are split
|
||||
into smaller parts.
|
||||
|
||||
To check if a media file can be parsed without *seek*, execute `cat file.mp4 | ffprobe -`
|
||||
|
||||
**sist2** is not I/O bound, scans might be faster when larger archives are split into smaller parts.
|
||||
|
||||
### OCR
|
||||
|
||||
You can enable OCR support for pdf,xps,cbz,cbr,fb2,epub file types with the
|
||||
`--ocr <lang>` option. Download the language data files with your
|
||||
package manager (`apt install tesseract-ocr-eng`) or directly [from Github](https://github.com/tesseract-ocr/tesseract/wiki/Data-Files).
|
||||
You can enable OCR support for ebook (pdf,xps,fb2,epub) or image file types with the
|
||||
`--ocr-lang <lang>` option in combination with `--ocr-images` and/or `--ocr-ebooks`.
|
||||
Download the language data files with your package manager (`apt install tesseract-ocr-eng`) or
|
||||
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.
|
||||
|
||||
Examples
|
||||
```bash
|
||||
sist2 scan --ocr jpn ~/Books/Manga/
|
||||
sist2 scan --ocr eng ~/Books/Textbooks/
|
||||
```
|
||||
You can use the `+` separator to specify multiple languages. The language
|
||||
name must be identical to the `*.traineddata` file installed on your system
|
||||
(use `chi_sim` rather than `chi-sim`).
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
sist2 scan --ocr-ebooks --ocr-lang jpn ~/Books/Manga/
|
||||
sist2 scan --ocr-images --ocr-lang eng ~/Images/Screenshots/
|
||||
sist2 scan --ocr-ebooks --ocr-images --ocr-lang eng+chi_sim ~/Chinese-Bilingual/
|
||||
```
|
||||
|
||||
## Build from source
|
||||
|
||||
You can compile **sist2** by yourself if you don't want to use the pre-compiled
|
||||
binaries (GCC 7+ required).
|
||||
You can compile **sist2** by yourself if you don't want to use the pre-compiled binaries
|
||||
|
||||
### With docker (recommended)
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/simon987/sist2/
|
||||
cd sist2
|
||||
docker build . -f ./Dockerfile -t my-sist2-image
|
||||
docker run --rm --entrypoint cat my-sist2-image /root/sist2 > sist2-x64-linux
|
||||
```
|
||||
|
||||
### On a linux computer
|
||||
|
||||
1. Install compile-time dependencies
|
||||
|
||||
```bash
|
||||
vcpkg install lmdb cjson glib libarchive[core,bzip2,libxml2,lz4,lzma,lzo] pthread tesseract libxml2 ffmpeg zstd gtest mongoose libuuid libmagic
|
||||
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
|
||||
```
|
||||
|
||||
2. Build
|
||||
1. Apply vcpkg patches, as per [sist2-build](https://github.com/simon987/sist2-build) Dockerfile
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
1. Build
|
||||
```bash
|
||||
git clone --recursive https://github.com/simon987/sist2/
|
||||
cmake -D <VCPKG_ROOT>/scripts/buildsystems/vcpkg.cmake .
|
||||
cmake -DSIST_DEBUG=off -DCMAKE_TOOLCHAIN_FILE=<VCPKG_ROOT>/scripts/buildsystems/vcpkg.cmake .
|
||||
make
|
||||
```
|
||||
|
||||
16
ci/build.sh
16
ci/build.sh
@@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
rm *.gz
|
||||
|
||||
rm -rf CMakeFiles CMakeCache.txt
|
||||
cmake -DSIST_DEBUG=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake .
|
||||
make
|
||||
strip sist2
|
||||
gzip -9 sist2
|
||||
|
||||
rm -rf CMakeFiles CMakeCache.txt
|
||||
cmake -DSIST_DEBUG=on -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake .
|
||||
make
|
||||
cp /usr/lib/x86_64-linux-gnu/libasan.so.2.0.0 libasan.so.2
|
||||
tar -czf sist2_debug.tar.gz sist2_debug libasan.so.2
|
||||
7
contrib/systemd/Makefile
Normal file
7
contrib/systemd/Makefile
Normal file
@@ -0,0 +1,7 @@
|
||||
install:
|
||||
install sist2-update-all.sh /usr/bin/sist2-update-all.sh
|
||||
install sist2-update-files.sh /usr/bin/sist2-update-files.sh
|
||||
install sist2-update-nextcloud.sh /usr/bin/sist2-update-nextcloud.sh
|
||||
install sist2-update.service /etc/systemd/system/sist2-update.service
|
||||
install sist2-update.timer /etc/systemd/system/sist2-update.timer
|
||||
systemctl daemon-reload
|
||||
31
contrib/systemd/README.md
Normal file
31
contrib/systemd/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Systemd integration example
|
||||
|
||||
This example contains my (yatli) personal configuration for sist2 auto-updating.
|
||||
The following indices are involved in this configuration:
|
||||
|
||||
| Index | Path | Description |
|
||||
|-----------|------------------|--------------------------------------------|
|
||||
| files | /zpool/files | Main file repository |
|
||||
| nextcloud | /zpool/nextcloud | Externally synchronized to a cloud account |
|
||||
|
||||
The systemd integration achieves automatic sist2 scanning & indexing everyday at 3:00AM.
|
||||
|
||||
### Tailoring the configuration for yourself
|
||||
|
||||
`sist2-update-all.sh` calls update scripts for each sist2 index. Add or remove
|
||||
update scripts accordingly to suit your need. Each update script (e.g.
|
||||
`sist2-update-files.sh`) has important parameters laid down at the beginning so
|
||||
make sure to edit them to point to your files and index locations.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# install the services and scripts
|
||||
sudo make install
|
||||
# enable & start the timer
|
||||
sudo systemctl enable sist2-update.timer
|
||||
sudo systemctl start sist2-update.timer
|
||||
# verify that the timer has been enabled
|
||||
systemctl list-timers --all
|
||||
```
|
||||
|
||||
9
contrib/systemd/sist2-update-all.sh
Executable file
9
contrib/systemd/sist2-update-all.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
echo "Update index: Files"
|
||||
source ${__dir}/sist2-update-files.sh
|
||||
echo "Update index: Nextcloud"
|
||||
source ${__dir}/sist2-update-nextcloud.sh
|
||||
echo "Done. Restarting sist2."
|
||||
docker restart sist2-sist2-1
|
||||
34
contrib/systemd/sist2-update-files.sh
Executable file
34
contrib/systemd/sist2-update-files.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
DATE=$(date +%Y_%m_%d)
|
||||
CONTENT=/zpool/files
|
||||
ORIG=/mnt/ssd/sist-index/files.idx
|
||||
NEW=/mnt/ssd/sist-index/files_$DATE.idx
|
||||
EXCLUDE='ZArchives|TorrentStore|TorrentDownload|624f0c59-1fef-44f6-95e9-7483296f2833|ubuntu-full-2021-12-07'
|
||||
NAME=Files
|
||||
#REWRITE_URL="http://localhost:33333/activate?collection=$NAME&path="
|
||||
REWRITE_URL=""
|
||||
|
||||
sist2 scan \
|
||||
--threads 14 \
|
||||
--mem-throttle 32768 \
|
||||
--quality 1.0 \
|
||||
--name $NAME \
|
||||
--ocr-lang=eng+chi_sim \
|
||||
--ocr-ebooks \
|
||||
--ocr-images \
|
||||
--exclude=$EXCLUDE \
|
||||
--rewrite-url=$REWRITE_URL \
|
||||
--incremental=$ORIG \
|
||||
--output=$NEW \
|
||||
$CONTENT
|
||||
echo ">>> Scan complete"
|
||||
rm -rf $ORIG
|
||||
mv $NEW $ORIG
|
||||
|
||||
unset http_proxy
|
||||
unset https_proxy
|
||||
unset HTTP_PROXY
|
||||
unset HTTPS_PROXY
|
||||
sist2 index $ORIG --incremental-index
|
||||
echo ">>> Index complete"
|
||||
33
contrib/systemd/sist2-update-nextcloud.sh
Executable file
33
contrib/systemd/sist2-update-nextcloud.sh
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
DATE=$(date +%Y_%m_%d)
|
||||
CONTENT=/zpool/nextcloud/v-yadli
|
||||
ORIG=/mnt/ssd/sist-index/nextcloud.idx
|
||||
NEW=/mnt/ssd/sist-index/nextcloud_$DATE.idx
|
||||
EXCLUDE='Yatao|.*263418493\\/Image\\/.*'
|
||||
NAME=NextCloud
|
||||
# REWRITE_URL="http://localhost:33333/activate?collection=$NAME&path="
|
||||
REWRITE_URL=""
|
||||
|
||||
sist2 scan \
|
||||
--threads 14 \
|
||||
--mem-throttle 32768 \
|
||||
--quality 1.0 \
|
||||
--name $NAME \
|
||||
--ocr-lang=eng+chi_sim \
|
||||
--ocr-ebooks \
|
||||
--ocr-images \
|
||||
--exclude=$EXCLUDE \
|
||||
--rewrite-url=$REWRITE_URL \
|
||||
--incremental=$ORIG \
|
||||
--output=$NEW \
|
||||
$CONTENT
|
||||
echo ">>> Scan complete"
|
||||
rm -rf $ORIG
|
||||
mv $NEW $ORIG
|
||||
|
||||
unset http_proxy
|
||||
unset https_proxy
|
||||
unset HTTP_PROXY
|
||||
unset HTTPS_PROXY
|
||||
sist2 index $ORIG --incremental-index
|
||||
6
contrib/systemd/sist2-update.service
Normal file
6
contrib/systemd/sist2-update.service
Normal file
@@ -0,0 +1,6 @@
|
||||
[Unit]
|
||||
Description=sist2-update
|
||||
|
||||
[Service]
|
||||
User=yatli
|
||||
ExecStart=/bin/bash /usr/bin/sist2-update-all.sh
|
||||
10
contrib/systemd/sist2-update.timer
Normal file
10
contrib/systemd/sist2-update.timer
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=sist2-update
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 3:00:00
|
||||
Persistent=true
|
||||
Unit=sist2-update.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
24
docker-compose.yml
Normal file
24
docker-compose.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
elasticsearch:
|
||||
image: elasticsearch:7.14.0
|
||||
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:
|
||||
# NOTE: Don't export this port publicly!
|
||||
- 8080:8080
|
||||
- 4090:4090
|
||||
working_dir: /root/sist2-admin/
|
||||
entrypoint: python3
|
||||
command:
|
||||
- /root/sist2-admin/sist2_admin/app.py
|
||||
325
docs/USAGE.md
325
docs/USAGE.md
@@ -13,48 +13,82 @@
|
||||
* [options](#web-options)
|
||||
* [examples](#web-examples)
|
||||
* [rewrite_url](#rewrite_url)
|
||||
* [link to specific indices](#link-to-specific-indices)
|
||||
* [elasticsearch](#elasticsearch)
|
||||
* [exec-script](#exec-script)
|
||||
* [tagging](#tagging)
|
||||
* [sidecar files](#sidecar-files)
|
||||
|
||||
```
|
||||
Usage: sist2 scan [OPTION]... PATH
|
||||
or: sist2 index [OPTION]... INDEX
|
||||
or: sist2 web [OPTION]... INDEX...
|
||||
or: sist2 exec-script [OPTION]... INDEX
|
||||
Lightning-fast file system indexer and search tool.
|
||||
|
||||
-h, --help show this help message and exit
|
||||
-v, --version Show version and exit
|
||||
--verbose Turn on logging
|
||||
--very-verbose Turn on debug messages
|
||||
-h, --help show this help message and exit
|
||||
-v, --version Show version and exit
|
||||
--verbose Turn on logging
|
||||
--very-verbose Turn on debug messages
|
||||
|
||||
Scan options
|
||||
-t, --threads=<int> Number of threads. DEFAULT=1
|
||||
-q, --quality=<flt> Thumbnail quality, on a scale of 1.0 to 31.0, 1.0 being the best. DEFAULT=5
|
||||
--size=<int> Thumbnail size, in pixels. Use negative value to disable. DEFAULT=500
|
||||
--content-size=<int> Number of bytes to be extracted from text documents. Use negative value to disable. DEFAULT=32768
|
||||
--incremental=<str> Reuse an existing index and only scan modified files.
|
||||
-o, --output=<str> Output directory. DEFAULT=index.sist2/
|
||||
--rewrite-url=<str> Serve files from this url instead of from disk.
|
||||
--name=<str> Index display name. DEFAULT: (name of the directory)
|
||||
--depth=<int> Scan up to DEPTH subdirectories deep. Use 0 to only scan files in PATH. DEFAULT: -1
|
||||
--archive=<str> Archive file mode (skip|list|shallow|recurse). skip: Don't parse, list: only get file names as text, shallow: Don't parse archives inside archives. DEFAULT: recurse
|
||||
--ocr=<str> Tesseract language (use tesseract --list-langs to see which are installed on your machine)
|
||||
-e, --exclude=<str> Files that match this regex will not be scanned
|
||||
--fast Only index file names & mime type
|
||||
-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
|
||||
--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
|
||||
--incremental=<str> Reuse an existing index and only scan modified files.
|
||||
-o, --output=<str> Output directory. DEFAULT=index.sist2/
|
||||
--rewrite-url=<str> Serve files from this url instead of from disk.
|
||||
--name=<str> Index display name. DEFAULT: (name of the directory)
|
||||
--depth=<int> Scan up to DEPTH subdirectories deep. Use 0 to only scan files in PATH. DEFAULT: -1
|
||||
--archive=<str> Archive file mode (skip|list|shallow|recurse). skip: Don't parse, list: only get file names as text, shallow: Don't parse archives inside archives. DEFAULT: recurse
|
||||
--archive-passphrase=<str> Passphrase for encrypted archive files
|
||||
--ocr-lang=<str> Tesseract language (use 'tesseract --list-langs' to see which are installed on your machine)
|
||||
--ocr-images Enable OCR'ing of image files.
|
||||
--ocr-ebooks Enable OCR'ing of ebook files.
|
||||
-e, --exclude=<str> Files that match this regex will not be scanned
|
||||
--fast Only index file names & mime type
|
||||
--treemap-threshold=<str> Relative size threshold for treemap (see USAGE.md). DEFAULT: 0.0005
|
||||
--mem-buffer=<int> Maximum memory buffer size per thread in MiB for files inside archives (see USAGE.md). DEFAULT: 2000
|
||||
--read-subtitles Read subtitles from media files.
|
||||
--fast-epub Faster but less accurate EPUB parsing (no thumbnails, metadata)
|
||||
--checksums Calculate file checksums when scanning.
|
||||
--list-file=<str> Specify a list of newline-delimited paths to be scanned instead of normal directory traversal. Use '-' to read from stdin.
|
||||
|
||||
Index options
|
||||
--es-url=<str> Elasticsearch url with port. DEFAULT=http://localhost:9200
|
||||
-p, --print Just print JSON documents to stdout.
|
||||
--script-file=<str> Path to user script.
|
||||
--batch-size=<int> Index batch size. DEFAULT: 100
|
||||
-f, --force-reset Reset Elasticsearch mappings and settings. (You must use this option the first time you use the index command)
|
||||
|
||||
-t, --threads=<int> Number of threads. DEFAULT=1
|
||||
--es-url=<str> Elasticsearch url with port. DEFAULT=http://localhost:9200
|
||||
--es-index=<str> Elasticsearch index name. DEFAULT=sist2
|
||||
-p, --print Just print JSON documents to stdout.
|
||||
--incremental-index Conduct incremental indexing, assumes that the old index is already digested by Elasticsearch.
|
||||
--script-file=<str> Path to user script.
|
||||
--mappings-file=<str> Path to Elasticsearch mappings.
|
||||
--settings-file=<str> Path to Elasticsearch settings.
|
||||
--async-script Execute user script asynchronously.
|
||||
--batch-size=<int> Index batch size. DEFAULT: 100
|
||||
-f, --force-reset Reset Elasticsearch mappings and settings. (You must use this option the first time you use the index command)
|
||||
|
||||
Web options
|
||||
--es-url=<str> Elasticsearch url. DEFAULT=http://localhost:9200
|
||||
--bind=<str> Listen on this address. DEFAULT=localhost:4090
|
||||
--auth=<str> Basic auth in user:password format
|
||||
Made by simon987 <me@simon987.net>. Released under GPL-3.0
|
||||
--es-url=<str> Elasticsearch url. DEFAULT=http://localhost:9200
|
||||
--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)
|
||||
--lang=<str> Default UI language. Can be changed by the user
|
||||
|
||||
Exec-script options
|
||||
--es-url=<str> Elasticsearch url. DEFAULT=http://localhost:9200
|
||||
--es-index=<str> Elasticsearch index name. DEFAULT=sist2
|
||||
--script-file=<str> Path to user script.
|
||||
--async-script Execute user script asynchronously.
|
||||
Made by simon987 <me@simon987.net>. Released under GPL-3.0
|
||||
```
|
||||
|
||||
## Scan
|
||||
@@ -62,19 +96,27 @@ Made by simon987 <me@simon987.net>. Released under GPL-3.0
|
||||
### Scan options
|
||||
|
||||
* `-t, --threads`
|
||||
Number of threads for file parsing. **Do not set a number higher than `$(nproc)`!**.
|
||||
* `-q, --quality`
|
||||
Thumbnail quality, on a scale of 1.0 to 31.0, 1.0 being the best. *Does not affect PDF thumbnails quality*
|
||||
* `--size`
|
||||
Number of threads for file parsing. **Do not set a number higher than `$(nproc)` or `$(Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors` in Windows!**
|
||||
* `--mem-throttle`
|
||||
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-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 ~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 and PDFs).
|
||||
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.
|
||||
Set to 0 to completely disable content parsing.
|
||||
* `--incremental`
|
||||
Specify an existing index. Information about files in this index that were not modified (based on *mtime* attribute)
|
||||
will be copied to the new index and will not be parsed again.
|
||||
* `-o, --output` Output directory.
|
||||
* `--rewrite-url` Set the `rewrite_url` option for the web module (See [rewrite_url](#rewrite_url))
|
||||
* `--rewrite-url` Set the `rewrite_url` option for the web module (See [rewrite_url](#rewrite_url))
|
||||
* `--name` Set the `name` option for the web module
|
||||
* `--depth` Maximum scan dept. Set to 0 only scan files directly in the root directory, set to -1 for infinite depth
|
||||
* `--archive` Archive file mode.
|
||||
@@ -82,7 +124,7 @@ Made by simon987 <me@simon987.net>. Released under GPL-3.0
|
||||
* list: Only get file names as text
|
||||
* shallow: Don't parse archives inside archives.
|
||||
* recurse: Scan archives recursively (default)
|
||||
* `--ocr` See [OCR](../README.md#OCR)
|
||||
* `--ocr-lang`, `--ocr-ebooks`, `--ocr-images` See [OCR](../README.md#OCR)
|
||||
* `-e, --exclude` Regex pattern to exclude files. A file is excluded if the pattern matches any
|
||||
part of the full absolute path.
|
||||
|
||||
@@ -94,6 +136,23 @@ Made by simon987 <me@simon987.net>. Released under GPL-3.0
|
||||
* `-e "(^/usr/)|(^/var/)|(^/media/DRIVE-A/tmp/)|(^/media/DRIVE-B/Trash/)"` Exclude the
|
||||
`/usr`, `/var`, `/media/DRIVE-A/tmp`, `/media/DRIVE-B/Trash` directories
|
||||
* `--fast` Only index file names and mime type
|
||||
* `--treemap-threshold` Directories smaller than (`treemap-threshold` * `<total size of the index>`)
|
||||
will not be considered for the disk utilisation visualization; their size will be added to
|
||||
the parent directory. If the parent directory is still smaller than the threshold, it will also be "merged upwards"
|
||||
and so on.
|
||||
|
||||
In effect, smaller `treemap-threshold` values will yield a more detailed
|
||||
(but also a more cluttered and harder to read) visualization.
|
||||
|
||||
* `--mem-buffer` Maximum memory buffer size in MiB (per thread) for files inside archives. Media files
|
||||
larger than this number will be read sequentially and no *seek* operations will be supported.
|
||||
|
||||
To check if a media file can be parsed without *seek*, execute `cat file.mp4 | ffprobe -`
|
||||
* `--read-subtitles` When enabled, will attempt to read the subtitles stream from media files.
|
||||
* `--fast-epub` Much faster but less accurate EPUB parsing. When enabled, sist2 will use a simple HTML parser to read epub files instead of the MuPDF library. No thumbnails are generated and author/title metadata are not parsed.
|
||||
* `--checksums` Calculate file checksums (SHA1) when scanning files. This option does not cause any additional read
|
||||
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).
|
||||
|
||||
### Scan examples
|
||||
|
||||
@@ -114,22 +173,27 @@ sist2 scan --incremental ./orig_idx/ -o ./updated_idx/ ~/Documents
|
||||
|
||||
### Index format
|
||||
|
||||
A typical `binary` type index structure looks like this:
|
||||
A typical `ndjson` type index structure looks like this:
|
||||
```
|
||||
documents.idx/
|
||||
├── descriptor.json
|
||||
├── _index_139965416830720
|
||||
├── _index_139965425223424
|
||||
├── _index_139965433616128
|
||||
├── _index_139965442008832
|
||||
└── thumbs
|
||||
├── _index_main.ndjson.zst
|
||||
├── treemap.csv
|
||||
├── agg_mime.csv
|
||||
├── agg_date.csv
|
||||
├── add_size.csv
|
||||
├── thumbs/
|
||||
| ├── data.mdb
|
||||
| └── lock.mdb
|
||||
├── tags/
|
||||
| ├── data.mdb
|
||||
| └── lock.mdb
|
||||
└── meta/
|
||||
├── data.mdb
|
||||
└── lock.mdb
|
||||
```
|
||||
|
||||
The `_index_*` files contain the raw binary index data and are not meant to be
|
||||
read by other applications. The format is generally compatible across different
|
||||
sist2 versions.
|
||||
The `_index_*.ndjson.zst` files contain the document data in JSON format, in a compressed newline-delemited file.
|
||||
|
||||
The `thumbs/` folder is a [LMDB](https://en.wikipedia.org/wiki/Lightning_Memory-Mapped_Database)
|
||||
database containing the thumbnails.
|
||||
@@ -137,71 +201,14 @@ database containing the thumbnails.
|
||||
The `descriptor.json` file contains general information about the index. The
|
||||
following fields are safe to modify manually: `root`, `name`, [rewrite_url](#rewrite_url) and `timestamp`.
|
||||
|
||||
|
||||
*Advanced usage*
|
||||
|
||||
Instead of using the `scan` module, you can also import an index generated
|
||||
by a third party application. The 'external' index must have the following format:
|
||||
|
||||
```
|
||||
my_index/
|
||||
├── descriptor.json
|
||||
├── _index_0
|
||||
└── thumbs
|
||||
├── data.mdb
|
||||
└── lock.mdb
|
||||
```
|
||||
|
||||
*descriptor.json*:
|
||||
```json
|
||||
{
|
||||
"uuid": "<valid UUID4>",
|
||||
"version": "_external_v1",
|
||||
"root": "(optional)",
|
||||
"name": "<name>",
|
||||
"rewrite_url": "(optional)",
|
||||
"type": "json",
|
||||
"timestamp": 1578971024
|
||||
}
|
||||
```
|
||||
|
||||
*_index_0*: NDJSON format (One json object per line)
|
||||
|
||||
```json
|
||||
{
|
||||
"_id": "unique uuid for the file",
|
||||
"index": "index uuid4 (same one as descriptor.json!)",
|
||||
"mime": "application/x-cbz",
|
||||
"size": 14341204,
|
||||
"mtime": 1578882996,
|
||||
"extension": "cbz",
|
||||
"name": "my_book",
|
||||
"path": "path/to/books",
|
||||
"content": "text contents of the book",
|
||||
"title": "Title of the book",
|
||||
"tag": ["genre.fiction", "author.someguy", "etc..."],
|
||||
"_keyword": [
|
||||
{"k": "ISBN", "v": "ABCD34789231"}
|
||||
],
|
||||
"_text": [
|
||||
{"k": "other", "v": "This will be indexed as text"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can find the full list of supported fields [here](../src/io/serialize.c#L90)
|
||||
|
||||
The `_keyword.*` items will be indexed and searchable as **keyword** fields (only full matches allowed).
|
||||
The `_text.*` items will be indexed and searchable as **text** fields (fuzzy searching allowed)
|
||||
|
||||
The `.csv` are pre-computed aggregations necessary for the stats page.
|
||||
|
||||
*thumbs/*:
|
||||
|
||||
LMDB key-value store. Keys are **binary** 128-bit UUID4s (`_id` field)
|
||||
LMDB key-value store. Keys are **binary** 16-byte md5 hash* (`_id` field)
|
||||
and values are raw image bytes.
|
||||
|
||||
Importing an external `binary` type index is technically possible but
|
||||
it is currently unsupported and has no guaranties of back/forward compatibility.
|
||||
*\* Hash is calculated from the full path of the file, including the extension, relative to the index root*
|
||||
|
||||
|
||||
## Index
|
||||
@@ -209,17 +216,29 @@ it is currently unsupported and has no guaranties of back/forward compatibility.
|
||||
* `--es-url`
|
||||
Elasticsearch url and port. If you are using docker, make sure that both containers are on the
|
||||
same network.
|
||||
* `--es-index`
|
||||
Elasticsearch index name. DEFAULT=sist2
|
||||
* `-p, --print`
|
||||
Print index in JSON format to stdout.
|
||||
* `--incremental-index`
|
||||
Conduct incremental indexing. Assumes that the old index is already ingested in Elasticsearch.
|
||||
Only the new changes since the last scan will be sent.
|
||||
* `--script-file`
|
||||
Path to user script. See [Scripting](scripting/README.md).
|
||||
Path to user script. See [Scripting](scripting.md).
|
||||
* `--mappings-file`
|
||||
Path to custom Elasticsearch mappings. If none is specified, [the bundled mappings](https://github.com/simon987/sist2/tree/master/schema) will be used.
|
||||
* `--settings-file`
|
||||
Path to custom Elasticsearch settings. *(See above)*
|
||||
* `--async-script`
|
||||
Use `wait_for_completion=false` elasticsearch option while executing user script.
|
||||
(See [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/tasks.html))
|
||||
* `--batch-size=<int>`
|
||||
Index batch size. Indexing is generally faster with larger batches, but payloads that
|
||||
are too large will fail and additional overhead for retrying with smaller sizes may slow
|
||||
down the process.
|
||||
* `-f, --force-reset`
|
||||
Reset Elasticsearch mappings and settings.
|
||||
**(You must use this option the first time you use the index command)**.
|
||||
* `-t, --threads` Number of threads to use. Ideally, choose a number equal to the number of logical cores of the machine hosting Elasticsearch.
|
||||
|
||||
### Index examples
|
||||
|
||||
@@ -243,8 +262,17 @@ sist2 index --print ./my_index/ | jq | less
|
||||
|
||||
### Web options
|
||||
* `--es-url=<str>` Elasticsearch url.
|
||||
* `--es-index`
|
||||
Elasticsearch index name. DEFAULT=sist2
|
||||
* `--bind=<str>` Listen on this address.
|
||||
* `--auth=<str>` Basic auth in user:password format
|
||||
* `--tag-auth=<str>` Basic auth in user:password format. Works the same way as the
|
||||
`--auth` argument, but authentication is only applied the `/tag/` endpoint.
|
||||
* `--tagline=<str>` When specified, will replace the default tagline in the navbar.
|
||||
* `--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
|
||||
|
||||
@@ -264,11 +292,86 @@ sist2 web index1 index2 index3 index4
|
||||
When the `rewrite_url` field is not empty, the web module ignores the `root`
|
||||
field and will return a HTTP redirect to `<rewrite_url><path>/<name><extension>`
|
||||
instead of serving the file from disk.
|
||||
Both the `root` and `rewrite_url` fields are safe to manually modify from the
|
||||
Both the `root` and `rewrite_url` fields are safe to manually modify from the
|
||||
`descriptor.json` file.
|
||||
|
||||
### Link to specific indices
|
||||
# Elasticsearch
|
||||
|
||||
To link to specific indices, you can add a list of comma-separated index name to
|
||||
the URL: `?i=<name>,<name>`. By default, indices with `"(nsfw)"` in their name are
|
||||
not displayed.
|
||||
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:
|
||||
|
||||
- Bug fix for large documents (See #198)
|
||||
|
||||
When using a legacy version of ES, a notice will be displayed next to the sist2 version in the web UI.
|
||||
If you don't care about the features above, you can ignore it or disable it in the configuration page.
|
||||
|
||||
## exec-script
|
||||
|
||||
The `exec-script` command is used to execute a user script for an index that has already been imported to Elasticsearch with the `index` command. Note that the documents will not be reset to their default state before each execution as the `index` command does: if you make undesired changes to the documents by accident, you will need to run `index` again to revert to the original state.
|
||||
|
||||
|
||||
# Tagging
|
||||
|
||||
### Manual tagging
|
||||
|
||||
You can modify tags of individual documents directly from the
|
||||
`web` interface. Note that you can setup authentication for this feature
|
||||
with the `--tag-auth` option (See [web options](#web-options))
|
||||
|
||||

|
||||
|
||||
Tags that are manually added are saved both in the
|
||||
index folder (in `/tags/`) and in Elasticsearch*. When re-`index`ing,
|
||||
they are read from the index and automatically applied.
|
||||
|
||||
You can safely copy the `/tags/` database to another index.
|
||||
|
||||
See [Automatic tagging](#automatic-tagging) for information about tag
|
||||
hierarchies and tag colors.
|
||||
|
||||
\* *It can take a few seconds to take effect in new search queries.*
|
||||
|
||||
|
||||
### Automatic tagging
|
||||
|
||||
See [scripting](scripting.md) documentation.
|
||||
|
||||
# Sidecar files
|
||||
|
||||
When scanning, sist2 will read metadata from `.s2meta` JSON files and overwrite the
|
||||
original document's indexed metadata (does not modify the actual file). Sidecar metadata files will also work inside archives.
|
||||
Sidecar files themselves are not saved in the index.
|
||||
|
||||
This feature is useful to leverage third-party applications such as speech-to-text or
|
||||
OCR to add additional metadata to a file.
|
||||
|
||||
**Example**
|
||||
|
||||
```
|
||||
~/Documents/
|
||||
├── Video.mp4
|
||||
└── Video.mp4.s2meta
|
||||
```
|
||||
|
||||
The sidecar file must have exactly the same file path and the `.s2meta` suffix.
|
||||
|
||||
`Video.mp4.s2meta`:
|
||||
```json
|
||||
{
|
||||
"content": "This sidecar file will overwrite some metadata fields of Video.mp4",
|
||||
"author": "Some author",
|
||||
"duration": 12345,
|
||||
"bitrate": 67890,
|
||||
"some_arbitrary_field": [1,2,3]
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
sist2 scan ~/Documents -o ./docs.idx
|
||||
sist2 index ./docs.idx
|
||||
```
|
||||
|
||||
*NOTE*: It is technically possible to overwrite the `tag` value using sidecar files, however,
|
||||
it is not currently possible to restore both manual tags and sidecar tags without user scripts
|
||||
while reindexing.
|
||||
|
||||
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/manual_tag.png
Normal file
BIN
docs/manual_tag.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -39,7 +39,7 @@ it adds the `genre.<genre>` tag.
|
||||
ArrayList tags = ctx._source.tag = new ArrayList();
|
||||
|
||||
if (ctx._source?.genre != null) {
|
||||
tags.add("genre." + ctx._source.genre.toLowerCase())
|
||||
tags.add("genre." + ctx._source.genre.toLowerCase());
|
||||
}
|
||||
```
|
||||
|
||||
@@ -67,7 +67,7 @@ ArrayList tags = ctx._source.tag = new ArrayList();
|
||||
|
||||
Matcher m = /[\(\.+](20[0-9]{2})[\)\.+]/.matcher(ctx._source.name);
|
||||
if (m.find()) {
|
||||
tags.add("year." + m.group(1))
|
||||
tags.add("year." + m.group(1));
|
||||
}
|
||||
```
|
||||
|
||||
@@ -111,16 +111,6 @@ if (ctx._source.path != "") {
|
||||
}
|
||||
```
|
||||
|
||||
Set the name of the last folder (`/path/to/<studio>/file.mp4`) to `studio.<studio>` tag
|
||||
```Java
|
||||
ArrayList tags = ctx._source.tag = new ArrayList();
|
||||
|
||||
if (ctx._source.path != "") {
|
||||
String[] names = ctx._source.path.splitOnToken('/');
|
||||
tags.add("studio." + names[names.length-1]);
|
||||
}
|
||||
```
|
||||
|
||||
Parse `EXIF:F Number` tag
|
||||
```Java
|
||||
if (ctx._source?.exif_fnumber != null) {
|
||||
|
||||
BIN
docs/sist2.png
BIN
docs/sist2.png
Binary file not shown.
|
Before Width: | Height: | Size: 889 KiB After Width: | Height: | Size: 1011 KiB |
BIN
docs/stats.png
Normal file
BIN
docs/stats.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
@@ -4,12 +4,17 @@
|
||||
"type": "keyword",
|
||||
"doc_values": true
|
||||
},
|
||||
"checksum": {
|
||||
"type": "keyword",
|
||||
"index": false
|
||||
},
|
||||
"_depth": {
|
||||
"type": "integer"
|
||||
},
|
||||
"path": {
|
||||
"type": "text",
|
||||
"analyzer": "path_analyzer",
|
||||
"copy_to": "suggest-path",
|
||||
"fielddata": true,
|
||||
"fields": {
|
||||
"nGram": {
|
||||
@@ -22,13 +27,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"suggest-path": {
|
||||
"type": "completion",
|
||||
"analyzer": "case_insensitive_kw_analyzer"
|
||||
},
|
||||
"mime": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"thumbnail": {
|
||||
"parent": {
|
||||
"type": "keyword",
|
||||
"index": false
|
||||
},
|
||||
"thumbnail": {
|
||||
"type": "integer",
|
||||
"index": false
|
||||
},
|
||||
"videoc": {
|
||||
"type": "keyword",
|
||||
"index": false
|
||||
@@ -38,7 +51,7 @@
|
||||
"index": false
|
||||
},
|
||||
"duration": {
|
||||
"type": "float",
|
||||
"type": "integer",
|
||||
"index": false
|
||||
},
|
||||
"width": {
|
||||
@@ -49,8 +62,13 @@
|
||||
"type": "integer",
|
||||
"index": false
|
||||
},
|
||||
"pages": {
|
||||
"type": "integer",
|
||||
"index": false
|
||||
},
|
||||
"mtime": {
|
||||
"type": "integer"
|
||||
"type": "date",
|
||||
"format": "epoch_millis"
|
||||
},
|
||||
"size": {
|
||||
"type": "long"
|
||||
@@ -61,6 +79,7 @@
|
||||
"name": {
|
||||
"analyzer": "content_analyzer",
|
||||
"type": "text",
|
||||
"fielddata": true,
|
||||
"fields": {
|
||||
"nGram": {
|
||||
"type": "text",
|
||||
@@ -92,10 +111,10 @@
|
||||
"analyzer": "my_nGram",
|
||||
"type": "text"
|
||||
},
|
||||
"_keyword.*": {
|
||||
"_keyword.*": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"_text.*": {
|
||||
"_text.*": {
|
||||
"analyzer": "content_analyzer",
|
||||
"type": "text",
|
||||
"fields": {
|
||||
@@ -121,7 +140,14 @@
|
||||
}
|
||||
},
|
||||
"tag": {
|
||||
"type": "keyword"
|
||||
"type": "text",
|
||||
"fielddata": true,
|
||||
"analyzer": "tag_analyzer",
|
||||
"copy_to": "suggest-tag"
|
||||
},
|
||||
"suggest-tag": {
|
||||
"type": "completion",
|
||||
"analyzer": "case_insensitive_kw_analyzer"
|
||||
},
|
||||
"exif_make": {
|
||||
"type": "text"
|
||||
@@ -147,6 +173,30 @@
|
||||
"exif_user_comment": {
|
||||
"type": "text"
|
||||
},
|
||||
"exif_gps_longitude_ref": {
|
||||
"type": "keyword",
|
||||
"index": false
|
||||
},
|
||||
"exif_gps_longitude_dms": {
|
||||
"type": "keyword",
|
||||
"index": false
|
||||
},
|
||||
"exif_gps_longitude_dec": {
|
||||
"type": "keyword",
|
||||
"index": false
|
||||
},
|
||||
"exif_gps_latitude_ref": {
|
||||
"type": "keyword",
|
||||
"index": false
|
||||
},
|
||||
"exif_gps_latitude_dms": {
|
||||
"type": "keyword",
|
||||
"index": false
|
||||
},
|
||||
"exif_gps_latitude_dec": {
|
||||
"type": "keyword",
|
||||
"index": false
|
||||
},
|
||||
"author": {
|
||||
"type": "text"
|
||||
},
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
{
|
||||
"index": {
|
||||
"refresh_interval": "30s",
|
||||
"codec": "best_compression"
|
||||
"codec": "best_compression",
|
||||
"number_of_replicas": 0,
|
||||
"highlight.max_analyzed_offset": 1000000
|
||||
},
|
||||
"analysis": {
|
||||
"tokenizer": {
|
||||
"path_tokenizer": {
|
||||
"type": "path_hierarchy"
|
||||
"type": "path_hierarchy",
|
||||
"delimiter": "/"
|
||||
},
|
||||
"tag_tokenizer": {
|
||||
"type": "path_hierarchy",
|
||||
"delimiter": "."
|
||||
},
|
||||
"my_nGram_tokenizer": {
|
||||
"type": "nGram",
|
||||
"type": "ngram",
|
||||
"min_gram": 3,
|
||||
"max_gram": 3
|
||||
}
|
||||
@@ -21,6 +28,12 @@
|
||||
"lowercase"
|
||||
]
|
||||
},
|
||||
"tag_analyzer": {
|
||||
"tokenizer": "tag_tokenizer",
|
||||
"filter": [
|
||||
"lowercase"
|
||||
]
|
||||
},
|
||||
"case_insensitive_kw_analyzer": {
|
||||
"tokenizer": "keyword",
|
||||
"filter": [
|
||||
|
||||
58
schema/settings_legacy.json
Normal file
58
schema/settings_legacy.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"index": {
|
||||
"refresh_interval": "30s",
|
||||
"codec": "best_compression",
|
||||
"number_of_replicas": 0
|
||||
},
|
||||
"analysis": {
|
||||
"tokenizer": {
|
||||
"path_tokenizer": {
|
||||
"type": "path_hierarchy",
|
||||
"delimiter": "/"
|
||||
},
|
||||
"tag_tokenizer": {
|
||||
"type": "path_hierarchy",
|
||||
"delimiter": "."
|
||||
},
|
||||
"my_nGram_tokenizer": {
|
||||
"type": "nGram",
|
||||
"min_gram": 3,
|
||||
"max_gram": 3
|
||||
}
|
||||
},
|
||||
"analyzer": {
|
||||
"path_analyzer": {
|
||||
"tokenizer": "path_tokenizer",
|
||||
"filter": [
|
||||
"lowercase"
|
||||
]
|
||||
},
|
||||
"tag_analyzer": {
|
||||
"tokenizer": "tag_tokenizer",
|
||||
"filter": [
|
||||
"lowercase"
|
||||
]
|
||||
},
|
||||
"case_insensitive_kw_analyzer": {
|
||||
"tokenizer": "keyword",
|
||||
"filter": [
|
||||
"lowercase"
|
||||
]
|
||||
},
|
||||
"my_nGram": {
|
||||
"tokenizer": "my_nGram_tokenizer",
|
||||
"filter": [
|
||||
"lowercase",
|
||||
"asciifolding"
|
||||
]
|
||||
},
|
||||
"content_analyzer": {
|
||||
"tokenizer": "standard",
|
||||
"filter": [
|
||||
"lowercase",
|
||||
"asciifolding"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,9 @@
|
||||
|
||||
rm -rf index.sist2/
|
||||
|
||||
rm src/static/js/bundle.js 2> /dev/null
|
||||
cat `ls src/static/js/*.min.js` > src/static/js/bundle.js
|
||||
cat src/static/js/{util,dom,search}.js >> src/static/js/bundle.js
|
||||
|
||||
rm src/static/css/bundle*.css 2> /dev/null
|
||||
cat src/static/css/*.min.css > src/static/css/bundle.css
|
||||
cat src/static/css/light.css >> src/static/css/bundle.css
|
||||
cat src/static/css/*.min.css > src/static/css/bundle_dark.css
|
||||
cat src/static/css/dark.css >> src/static/css/bundle_dark.css
|
||||
|
||||
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
|
||||
17
scripts/build.sh
Executable file
17
scripts/build.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
17
scripts/build_arm64.sh
Executable file
17
scripts/build_arm64.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
@@ -3,6 +3,7 @@ import json
|
||||
files = [
|
||||
"schema/mappings.json",
|
||||
"schema/settings.json",
|
||||
"schema/settings_legacy.json",
|
||||
"schema/pipeline.json",
|
||||
]
|
||||
|
||||
|
||||
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)))
|
||||
@@ -13,7 +13,7 @@ application/epub+zip, epub
|
||||
application/freeloader, frl
|
||||
application/futuresplash, spl
|
||||
application/groupwise, vew
|
||||
application/gzip, gz
|
||||
application/gzip, gz|tgz
|
||||
application/hta, hta
|
||||
application/i-deas, unv
|
||||
application/iges, iges|igs
|
||||
@@ -22,6 +22,7 @@ application/java-archive, jar
|
||||
application/java, class
|
||||
application/javascript,
|
||||
application/json, json
|
||||
application/ndjson, jsonl|ndjson
|
||||
application/marc, mrc
|
||||
application/mbedlet, mbd
|
||||
application/mime, aps
|
||||
@@ -78,9 +79,7 @@ application/vocaltec-media-desc, vmd
|
||||
application/vocaltec-media-file, vmf
|
||||
application/warc, warc
|
||||
application/winhelp, hlp
|
||||
application/wordperfect6.0, w60
|
||||
application/wordperfect6.1, w61
|
||||
application/wordperfect, wp|wp5|wp6|wpd
|
||||
application/wordperfect, wp|wp5|wp6|wpd|w60|w61
|
||||
application/x-123, wk1
|
||||
application/x-7z-compressed, 7z
|
||||
application/x-aim, aim
|
||||
@@ -111,7 +110,7 @@ application/x-dbf, dbf
|
||||
application/x-dbt,
|
||||
application/x-debian-package, deb
|
||||
application/x-deepv, deepv
|
||||
application/x-director, dcr|dir|dxr
|
||||
application/x-director, dir|dxr
|
||||
application/x-dmp, dmp
|
||||
application/x-dosdriver,
|
||||
application/x-dosexec, dll
|
||||
@@ -157,7 +156,6 @@ application/x-livescreen, ivy
|
||||
application/x-lotus, wq1
|
||||
application/x-lz4+json, jsonlz4
|
||||
application/x-lz4, lz4
|
||||
application/x-lz4, lz4
|
||||
application/x-lzh-compressed,
|
||||
application/x-lzh, lzh
|
||||
application/x-lzip, lz
|
||||
@@ -347,7 +345,8 @@ text/javascript, js
|
||||
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
|
||||
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
|
||||
application/vnd.coffeescript, coffee
|
||||
text/richtext, rt|rtf|rtx
|
||||
text/rtf,
|
||||
text/scriplet, wsc
|
||||
@@ -429,3 +428,23 @@ video/x-qtc, qtc
|
||||
video/x-sgi-movie, movie|mv
|
||||
x-epoc/x-sisx-app,
|
||||
application/x-zstd-dictionary,
|
||||
application/vnd.ms-outlook, msg
|
||||
image/x-olympus-orf, orf
|
||||
image/x-nikon-nef, nef
|
||||
image/x-fuji-raf, raf
|
||||
image/x-panasonic-raw, rw2|raw
|
||||
image/x-adobe-dng, dng
|
||||
image/x-canon-cr2, cr2
|
||||
image/x-canon-crw, crw
|
||||
image/x-dcraw,
|
||||
image/x-kodak-dcr, dcr
|
||||
image/x-kodak-k25, k25
|
||||
image/x-kodak-kdc, kdc
|
||||
image/x-minolta-mrw, mrw
|
||||
image/x-pentax-pef, pef
|
||||
image/x-sigma-x3f, xf3
|
||||
image/x-sony-arw, arw
|
||||
image/x-sony-sr2, sr2
|
||||
image/x-sony-srf, srf
|
||||
image/x-epson-erf, erf
|
||||
sist2/sidecar, s2meta
|
||||
|
@@ -3,6 +3,7 @@ noparse = set()
|
||||
ext_in_hash = set()
|
||||
|
||||
major_mime = {
|
||||
"sist2": 0,
|
||||
"model": 1,
|
||||
"example": 2,
|
||||
"message": 3,
|
||||
@@ -18,7 +19,6 @@ major_mime = {
|
||||
|
||||
pdf = (
|
||||
"application/pdf",
|
||||
"application/x-cbz",
|
||||
"application/epub+zip",
|
||||
"application/vnd.ms-xpsdocument",
|
||||
)
|
||||
@@ -73,6 +73,29 @@ markup = (
|
||||
"text/x-sgml"
|
||||
)
|
||||
|
||||
raw = (
|
||||
"image/x-olympus-orf",
|
||||
"image/x-nikon-nef",
|
||||
"image/x-fuji-raf",
|
||||
"image/x-panasonic-raw",
|
||||
"image/x-adobe-dng",
|
||||
"image/x-canon-cr2",
|
||||
"image/x-canon-crw",
|
||||
"image/x-dcraw",
|
||||
"image/x-kodak-dcr",
|
||||
"image/x-kodak-k25",
|
||||
"image/x-kodak-kdc",
|
||||
"image/x-minolta-mrw",
|
||||
"image/x-pentax-pef",
|
||||
"image/x-sigma-x3f",
|
||||
"image/x-sony-arw",
|
||||
"image/x-sony-sr2",
|
||||
"image/x-sony-srf",
|
||||
"image/x-minolta-mrw",
|
||||
"image/x-pentax-pef",
|
||||
"image/x-epson-erf",
|
||||
)
|
||||
|
||||
cnt = 1
|
||||
|
||||
|
||||
@@ -97,8 +120,14 @@ def mime_id(mime):
|
||||
mime_id += " | 0x02000000"
|
||||
elif mime in markup:
|
||||
mime_id += " | 0x01000000"
|
||||
elif mime in raw:
|
||||
mime_id += " | 0x00800000"
|
||||
elif mime == "application/x-empty":
|
||||
cnt -= 1
|
||||
return "1"
|
||||
elif mime == "sist2/sidecar":
|
||||
cnt -= 1
|
||||
return "2"
|
||||
return mime_id
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
files = [
|
||||
"src/static/css/bundle.css",
|
||||
"src/static/css/bundle_dark.css",
|
||||
"src/static/js/bundle.js",
|
||||
"src/static/img/sprite-skin-flat.png",
|
||||
"src/static/img/sprite-skin-flat-dark.png",
|
||||
"src/static/search.html",
|
||||
"sist2-vue/src/assets/favicon.ico",
|
||||
"sist2-vue/dist/css/chunk-vendors.css",
|
||||
"sist2-vue/dist/css/index.css",
|
||||
"sist2-vue/dist/js/chunk-vendors.js",
|
||||
"sist2-vue/dist/js/index.js",
|
||||
"sist2-vue/dist/index.html",
|
||||
]
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ def clean(filepath):
|
||||
|
||||
|
||||
for file in files:
|
||||
with open(file, "rb") as f:
|
||||
data = f.read()
|
||||
try:
|
||||
with open(file, "rb") as f:
|
||||
data = f.read()
|
||||
except:
|
||||
data = bytes([])
|
||||
|
||||
print("char %s[%d] = {%s};" % (clean(file), len(data), ",".join(str(int(b)) for b in data)))
|
||||
|
||||
3
scripts/start_dev_es.sh
Executable file
3
scripts/start_dev_es.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
docker run --rm -it --name "sist2-dev-es"\
|
||||
-p 9200:9200 -e "discovery.type=single-node" \
|
||||
-e "ES_JAVA_OPTS=-Xms8g -Xmx8g" elasticsearch:7.14.0
|
||||
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'
|
||||
]
|
||||
}
|
||||
1
sist2-admin/frontend/dist/css/app.css
vendored
Normal file
1
sist2-admin/frontend/dist/css/app.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.navbar[data-v-27bc1d68]{box-shadow:0 .125rem .25rem rgba(0,0,0,.08)!important;border-radius:0}.theme-black .navbar[data-v-27bc1d68]{background:rgba(84,107,122,.18823529411764706);border-bottom:none}.navbar-brand[data-v-27bc1d68]{color:#222!important;font-size:1.75rem;padding:0}.navbar-brand[data-v-27bc1d68]:hover{color:#000!important}.version[data-v-27bc1d68]{color:#222!important;margin-left:-18px;margin-top:-14px;font-size:11px;font-family:monospace}.btn-link[data-v-27bc1d68]{color:#222}body,html{height:100%}#app{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:#2c3e50;padding-bottom:1em;min-height:100%}.info-icon{width:1rem;margin-right:.2rem;cursor:pointer;line-height:1rem;height:1rem;background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MjYuNjY3IDQyNi42NjciIGZpbGw9IiNmZmYiPjxwYXRoIGQ9Ik0xOTIgMTkyaDQyLjY2N3YxMjhIMTkyeiIvPjxwYXRoIGQ9Ik0yMTMuMzMzIDBDOTUuNDY3IDAgMCA5NS40NjcgMCAyMTMuMzMzczk1LjQ2NyAyMTMuMzMzIDIxMy4zMzMgMjEzLjMzM1M0MjYuNjY3IDMzMS4yIDQyNi42NjcgMjEzLjMzMyAzMzEuMiAwIDIxMy4zMzMgMHptMCAzODRjLTk0LjA4IDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMgNDIuNjY3IDIxMy4zMzMgNDIuNjY3IDM4NCAxMTkuMjUzIDM4NCAyMTMuMzMzIDMwNy40MTMgMzg0IDIxMy4zMzMgMzg0eiIvPjxwYXRoIGQ9Ik0xOTIgMTA2LjY2N2g0Mi42Njd2NDIuNjY3SDE5MnoiLz48L3N2Zz4=);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:.5rem;margin-bottom:0}.shrink[data-v-9b017c42]{flex-grow:inherit}#task-history[data-v-46960281]{font-family:monospace;font-size:12px}#log-tail-output span{display:block}span.DEBUG{color:#9e9e9e}span.WARNING{color:#ffb300}span.INFO{color:#039be5}span.ERROR,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}
|
||||
8
sist2-admin/frontend/dist/css/chunk-vendors.css
vendored
Normal file
8
sist2-admin/frontend/dist/css/chunk-vendors.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
sist2-admin/frontend/dist/favicon.ico
vendored
Normal file
BIN
sist2-admin/frontend/dist/favicon.ico
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
sist2-admin/frontend/dist/index.html
vendored
Normal file
1
sist2-admin/frontend/dist/index.html
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<!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"><link rel="icon" href="favicon.ico"><title>sist2-admin</title><link href="css/app.css" rel="preload" as="style"><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="js/app.js" rel="preload" as="script"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link href="css/app.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but sist2-admin-vue doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.js"></script><script src="js/app.js"></script></body></html>
|
||||
1
sist2-admin/frontend/dist/js/app.js
vendored
Normal file
1
sist2-admin/frontend/dist/js/app.js
vendored
Normal file
File diff suppressed because one or more lines are too long
344
sist2-admin/frontend/dist/js/chunk-vendors.js
vendored
Normal file
344
sist2-admin/frontend/dist/js/chunk-vendors.js
vendored
Normal file
File diff suppressed because one or more lines are too long
19452
sist2-admin/frontend/package-lock.json
generated
Normal file
19452
sist2-admin/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
sist2-admin/frontend/package.json
Normal file
52
sist2-admin/frontend/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"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-eslint": "~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",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.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 %>favicon.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>
|
||||
51
sist2-admin/frontend/src/components/JobOptions.vue
Normal file
51
sist2-admin/frontend/src/components/JobOptions.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<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];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateNotifications(value) {
|
||||
this.$store.dispatch("setJobDesktopNotification", {
|
||||
job: this.job.name,
|
||||
enabled: value
|
||||
})
|
||||
},
|
||||
update() {
|
||||
if (this.job.schedule_enabled) {
|
||||
this.cronValid = /((((\d+,)+\d+|(\d+([/-])\d+)|\d+|\*) ?){5,7})/.test(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>
|
||||
111
sist2-admin/frontend/src/components/ScanOptions.vue
Normal file
111
sist2-admin/frontend/src/components/ScanOptions.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<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.memThrottle") }}</label>
|
||||
<b-form-input type="number" min="0" v-model="options.mem_throttle" @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>
|
||||
|
||||
<label>{{ $t("scanOptions.memBuffer") }}</label>
|
||||
<b-form-input type="number" min="0" v-model="options.mem_buffer" @change="update()"></b-form-input>
|
||||
|
||||
<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>
|
||||
109
sist2-admin/frontend/src/i18n/messages.js
Normal file
109
sist2-admin/frontend/src/i18n/messages.js
Normal file
@@ -0,0 +1,109 @@
|
||||
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",
|
||||
},
|
||||
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 1.0 to 31.0, 1.0 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"
|
||||
},
|
||||
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>
|
||||
124
sist2-admin/frontend/src/views/Tasks.vue
Normal file
124
sist2-admin/frontend/src/views/Tasks.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<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";
|
||||
|
||||
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(task.started);
|
||||
const end = moment(task.ended);
|
||||
|
||||
let duration = moment.utc(end.diff(start)).format("HH[h] mm[m] ss[s]");
|
||||
|
||||
duration = duration.replace("00h ", "");
|
||||
duration = duration.replace(/^00m /, "");
|
||||
duration = duration.replace(/00s/, "<1s");
|
||||
duration = duration.replace(/^0/, "");
|
||||
|
||||
return duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
</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,
|
||||
};
|
||||
5559
sist2-admin/frontend/yarn.lock
Normal file
5559
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
|
||||
392
sist2-admin/sist2_admin/app.py
Normal file
392
sist2-admin/sist2_admin/app.py
Normal file
@@ -0,0 +1,392 @@
|
||||
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 PickleTable, RUNNING_FRONTENDS, TESSERACT_LANGS, DB_SCHEMA_VERSION
|
||||
from web import Sist2Frontend
|
||||
|
||||
VERSION = "1.0"
|
||||
|
||||
sist2 = Sist2(SIST2_BINARY, DATA_FOLDER)
|
||||
db = PersistentState(table_factory=PickleTable, 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 {
|
||||
"version": VERSION,
|
||||
"tesseract_langs": TESSERACT_LANGS,
|
||||
"logs_folder": LOG_FOLDER
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/job/{name:str}")
|
||||
async def get_job(name: str):
|
||||
row = db["jobs"][name]
|
||||
if row:
|
||||
return row["job"]
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
|
||||
@app.get("/api/frontend/{name:str}")
|
||||
async def get_frontend(name: str):
|
||||
row = db["frontends"][name]
|
||||
if row:
|
||||
frontend = row["frontend"]
|
||||
frontend: Sist2Frontend
|
||||
frontend.running = frontend.name in RUNNING_FRONTENDS
|
||||
return frontend
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
|
||||
@app.get("/api/job/")
|
||||
async def get_jobs():
|
||||
return [row["job"] for row in db["jobs"]]
|
||||
|
||||
|
||||
@app.put("/api/job/{name:str}")
|
||||
async def update_job(name: str, job: Sist2Job):
|
||||
# TODO: Check etag
|
||||
|
||||
job.last_modified = datetime.now()
|
||||
row = db["jobs"][name]
|
||||
if not row:
|
||||
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(row["job"].scan_options, arg) != getattr(job.scan_options, arg):
|
||||
job.do_full_scan = True
|
||||
|
||||
db["jobs"][name] = {"job": job}
|
||||
|
||||
|
||||
@app.put("/api/frontend/{name:str}")
|
||||
async def update_frontend(name: str, frontend: Sist2Frontend):
|
||||
db["frontends"][name] = {"frontend": 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": 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):
|
||||
row = db["jobs"][name]
|
||||
if not row:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
_run_job(row["job"])
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.delete("/api/job/{name:str}")
|
||||
async def delete_job(name: str):
|
||||
row = db["jobs"][name]
|
||||
if row:
|
||||
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]
|
||||
|
||||
row = db["frontends"][name]
|
||||
if row:
|
||||
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": job}
|
||||
|
||||
return job
|
||||
|
||||
|
||||
@app.post("/api/frontend/{name:str}")
|
||||
async def create_frontend(name: str):
|
||||
if db["frontend"][name]:
|
||||
raise ValueError("Frontend with the same name already exists")
|
||||
|
||||
frontend = Sist2Frontend.create_default(name)
|
||||
db["frontends"][name] = {"frontend": 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]["job"].last_index, 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):
|
||||
row = db["frontends"][name]
|
||||
if not row:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
start_frontend_(row["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 row in db["frontends"]:
|
||||
frontend = row["frontend"]
|
||||
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": frontend}
|
||||
|
||||
logger.info("Initialized database.")
|
||||
|
||||
|
||||
def start_frontends():
|
||||
for row in db["frontends"]:
|
||||
frontend: Sist2Frontend = row["frontend"]
|
||||
if frontend.auto_start and len(frontend.jobs) > 0:
|
||||
start_frontend_(frontend)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if not db["sist2_admin"]["info"]:
|
||||
initialize_db()
|
||||
elif db["sist2_admin"]["info"]["version"] != DB_SCHEMA_VERSION:
|
||||
print("Database has incompatible schema version! Delete state.db to continue.")
|
||||
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 (row["job"] for row 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()
|
||||
315
sist2-admin/sist2_admin/jobs.py
Normal file
315
sist2-admin/sist2_admin/jobs.py
Normal file
@@ -0,0 +1,315 @@
|
||||
import json
|
||||
import logging
|
||||
import os.path
|
||||
import shutil
|
||||
import signal
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from hashlib import md5
|
||||
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, validator
|
||||
|
||||
from config import logger, LOG_FOLDER
|
||||
from notifications import Notifications
|
||||
from sist2 import ScanOptions, IndexOptions, Sist2, Sist2Index
|
||||
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
|
||||
last_index: 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.last_index and os.path.exists(self.job.last_index) and not self.job.do_full_scan:
|
||||
self.job.scan_options.incremental = self.job.last_index
|
||||
else:
|
||||
self.job.scan_options.incremental = 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:
|
||||
index = Sist2Index(self.job.scan_options.output)
|
||||
|
||||
# Save latest index
|
||||
self.job.previous_index = self.job.last_index
|
||||
|
||||
self.job.last_index = index.path
|
||||
self.job.last_index_date = datetime.now()
|
||||
self.job.do_full_scan = False
|
||||
db["jobs"][self.job.name] = {"job": self.job}
|
||||
self._logger.info(json.dumps({"sist2-admin": f"Save last_index={self.job.last_index}"}))
|
||||
|
||||
logger.info(f"Completed {self.display_name} ({return_code=})")
|
||||
|
||||
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:
|
||||
# Remove old index
|
||||
if self.job.previous_index is not None:
|
||||
self._logger.info(json.dumps({"sist2-admin": f"Remove {self.job.previous_index=}"}))
|
||||
try:
|
||||
shutil.rmtree(self.job.previous_index)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
self.restart_running_frontends(db, sist2)
|
||||
|
||||
# Update status
|
||||
self.job.status = JobStatus("indexed") if ok else JobStatus("failed")
|
||||
db["jobs"][self.job.name] = {"job": 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"]
|
||||
frontend: Sist2Frontend
|
||||
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
try:
|
||||
os.wait()
|
||||
except ChildProcessError:
|
||||
pass
|
||||
|
||||
frontend.web_options.indices = map(lambda j: db["jobs"][j]["job"].last_index, 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)
|
||||
328
sist2-admin/sist2_admin/sist2.py
Normal file
328
sist2-admin/sist2_admin/sist2.py
Normal file
@@ -0,0 +1,328 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os.path
|
||||
import traceback
|
||||
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 = False
|
||||
script: str = ""
|
||||
script_file: str = None
|
||||
batch_size: int = 100
|
||||
|
||||
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
|
||||
mem_throttle: int = 0
|
||||
thumbnail_quality: float = 1.0
|
||||
thumbnail_size: int = 500
|
||||
thumbnail_count: int = 1
|
||||
content_size: int = 32768
|
||||
depth: int = -1
|
||||
archive: str = ARCHIVE_RECURSE
|
||||
archive_passphrase: str = None
|
||||
ocr_lang: bool = 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: str = None
|
||||
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"--mem-throttle={self.mem_throttle}",
|
||||
f"--thumbnail-quality={self.thumbnail_quality}", f"--thumbnail-count={self.thumbnail_count}",
|
||||
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={self.incremental}")
|
||||
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):
|
||||
|
||||
output_dir = os.path.join(
|
||||
self._data_dir,
|
||||
f"scan-{datetime.now()}.sist2"
|
||||
)
|
||||
options.output = output_dir
|
||||
|
||||
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")
|
||||
try:
|
||||
for line in pipe_wrapper:
|
||||
if line.strip() == "":
|
||||
continue
|
||||
log_object = json.loads(line)
|
||||
logs_cb(log_object)
|
||||
except Exception as e:
|
||||
proc.kill()
|
||||
try:
|
||||
print(line)
|
||||
except NameError:
|
||||
pass
|
||||
print(traceback.format_exc())
|
||||
finally:
|
||||
pass
|
||||
# proc.wait()
|
||||
# pipe_wrapper.close()
|
||||
|
||||
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
|
||||
50
sist2-admin/sist2_admin/state.py
Normal file
50
sist2-admin/sist2_admin/state.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from typing import Dict
|
||||
|
||||
from hexlib.db import Table
|
||||
import pickle
|
||||
|
||||
from tesseract import get_tesseract_langs
|
||||
|
||||
RUNNING_FRONTENDS: Dict[str, int] = {}
|
||||
|
||||
TESSERACT_LANGS = get_tesseract_langs()
|
||||
|
||||
DB_SCHEMA_VERSION = "1"
|
||||
|
||||
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())
|
||||
|
||||
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=[]
|
||||
)
|
||||
23
sist2-vue/.gitignore
vendored
Normal file
23
sist2-vue/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.iml
|
||||
5
sist2-vue/babel.config.js
Normal file
5
sist2-vue/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
"presets": [
|
||||
"@vue/cli-plugin-babel/preset"
|
||||
]
|
||||
}
|
||||
8
sist2-vue/dist/css/chunk-vendors.css
vendored
Normal file
8
sist2-vue/dist/css/chunk-vendors.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
sist2-vue/dist/css/index.css
vendored
Normal file
1
sist2-vue/dist/css/index.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
sist2-vue/dist/index.html
vendored
Normal file
3
sist2-vue/dist/index.html
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<!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><script defer="defer" src="js/chunk-vendors.js"></script><script defer="defer" src="js/index.js"></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></body></html>
|
||||
99
sist2-vue/dist/js/chunk-vendors.js
vendored
Normal file
99
sist2-vue/dist/js/chunk-vendors.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
sist2-vue/dist/js/index.js
vendored
Normal file
1
sist2-vue/dist/js/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
20415
sist2-vue/package-lock.json
generated
Normal file
20415
sist2-vue/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
54
sist2-vue/package.json
Normal file
54
sist2-vue/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "sist2",
|
||||
"version": "2.11.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"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",
|
||||
"d3": "^5.6.1",
|
||||
"date-fns": "^2.21.3",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"fslightbox-vue": "file:../../../../mnt/Hatchery/projects/sist2/fslightbox-vue-pro-1.3.1.tgz",
|
||||
"nouislider": "^15.2.0",
|
||||
"underscore": "^1.13.1",
|
||||
"vue": "^2.6.12",
|
||||
"vue-color": "^2.8.1",
|
||||
"vue-i18n": "^8.24.4",
|
||||
"vue-masonry-wall": "^0.3.2",
|
||||
"vue-router": "^3.2.0",
|
||||
"vue-simple-suggest": "^1.11.1",
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"inspire-tree-dom": "^4.0.6",
|
||||
"mutationobserver-shim": "^0.3.7",
|
||||
"popper.js": "^1.16.1",
|
||||
"portal-vue": "^2.1.7",
|
||||
"sass": "^1.26.11",
|
||||
"sass-loader": "^10.0.2",
|
||||
"typescript": "~4.1.5",
|
||||
"vue-cli-plugin-bootstrap-vue": "~0.7.0",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
32
sist2-vue/public/index.html
Normal file
32
sist2-vue/public/index.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!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.0, maximum-scale=1.0, user-scalable=no'/>
|
||||
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<style>
|
||||
body {
|
||||
height: initial;
|
||||
}
|
||||
</style>
|
||||
<div style="text-align: center; margin-top: 100px">
|
||||
<strong>
|
||||
We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
|
||||
Please enable it to continue.
|
||||
</strong>
|
||||
<br/>
|
||||
<strong>
|
||||
Nous sommes désolés mais <%= htmlWebpackPlugin.options.title %> ne fonctionne pas correctement
|
||||
si JavaScript est activé.
|
||||
Veuillez l'activer pour continuer.
|
||||
</strong>
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
383
sist2-vue/src/App.vue
Normal file
383
sist2-vue/src/App.vue
Normal file
@@ -0,0 +1,383 @@
|
||||
<template>
|
||||
<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 • 装载
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavBar from "@/components/NavBar";
|
||||
import {mapActions, mapGetters, mapMutations} from "vuex";
|
||||
import Sist2Api from "@/Sist2Api";
|
||||
import {setupAuth0} from "@/main";
|
||||
|
||||
export default {
|
||||
components: {NavBar},
|
||||
data() {
|
||||
return {
|
||||
configLoading: false,
|
||||
authLoading: true,
|
||||
sist2InfoLoading: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["optTheme"]),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch("loadConfiguration").then(() => {
|
||||
this.$root.$i18n.locale = this.$store.state.optLang;
|
||||
});
|
||||
|
||||
this.$store.subscribe((mutation) => {
|
||||
if (mutation.type === "setOptLang") {
|
||||
this.$root.$i18n.locale = mutation.payload;
|
||||
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",
|
||||
"theme-black": this.optTheme === "black",
|
||||
}
|
||||
}
|
||||
}
|
||||
,
|
||||
}
|
||||
</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%;
|
||||
}
|
||||
|
||||
/*Black theme*/
|
||||
.theme-black {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.theme-black .card, .theme-black .modal-content {
|
||||
background: #212121;
|
||||
color: #e0e0e0;
|
||||
border-radius: 1px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
.theme-black .table {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.theme-black .table td, .theme-black .table th {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.theme-black .table thead th {
|
||||
border-bottom: 1px solid #646464;
|
||||
}
|
||||
|
||||
.theme-black .custom-select {
|
||||
overflow: auto;
|
||||
background-color: #37474F;
|
||||
border: 1px solid #616161;
|
||||
color: #bdbdbd;
|
||||
}
|
||||
|
||||
.theme-black .custom-select:focus {
|
||||
border-color: #757575;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 .2rem rgba(0, 123, 255, .25);
|
||||
}
|
||||
|
||||
.theme-black .inspire-tree .selected > .wholerow, .theme-black .inspire-tree .selected > .title-wrap:hover + .wholerow {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.theme-black .inspire-tree .icon-expand::before, .theme-black .inspire-tree .icon-collapse::before {
|
||||
background-color: black !important;
|
||||
}
|
||||
|
||||
.theme-black .inspire-tree .title {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.theme-black .inspire-tree {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
font-family: Helvetica, Nueue, Verdana, sans-serif;
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.inspire-tree [type=checkbox] {
|
||||
left: 22px !important;
|
||||
top: 7px !important;
|
||||
}
|
||||
|
||||
.theme-black .form-control {
|
||||
background-color: #37474F;
|
||||
border: 1px solid #616161;
|
||||
color: #dbdbdb !important;
|
||||
}
|
||||
|
||||
.theme-black .form-control:focus {
|
||||
background-color: #546E7A;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.theme-black .input-group-text, .theme-black .default-input {
|
||||
background: #37474F !important;
|
||||
border: 1px solid #616161 !important;
|
||||
color: #dbdbdb !important;
|
||||
}
|
||||
|
||||
.theme-black ::placeholder {
|
||||
color: #BDBDBD !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.theme-black .nav-tabs .nav-link {
|
||||
color: #e0e0e0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.theme-black .nav-tabs .nav-item.show .nav-link, .theme-black .nav-tabs .nav-link.active {
|
||||
background-color: #212121;
|
||||
border-color: #616161 #616161 #212121;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.theme-black .nav-tabs .nav-link:focus, .theme-black .nav-tabs .nav-link:focus {
|
||||
border-color: #616161 #616161 #212121;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.theme-black .nav-tabs .nav-link:focus, .theme-black .nav-tabs .nav-link:hover {
|
||||
border-color: #e0e0e0 #e0e0e0 #212121;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.theme-black .nav-tabs {
|
||||
border-bottom: #616161;
|
||||
}
|
||||
|
||||
.theme-black a:hover, .theme-black .btn:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.theme-black .b-dropdown a:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.theme-black .btn {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.theme-black .modal-header .close {
|
||||
color: #e0e0e0;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.theme-black .modal-header {
|
||||
border-bottom: 1px solid #646464;
|
||||
}
|
||||
|
||||
/* -------------------------- */
|
||||
|
||||
#nav {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
#nav a {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
#nav a.router-link-exact-active {
|
||||
color: #42b983;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
@media (max-width: 650px) {
|
||||
.mobile {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.not-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.grid-single-column .fit {
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0
|
||||
}
|
||||
|
||||
.lightbox-caption {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.noUi-connects {
|
||||
border-radius: 1px !important;
|
||||
}
|
||||
|
||||
mark {
|
||||
background: #fff217;
|
||||
border-radius: 0;
|
||||
padding: 1px 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.theme-black mark {
|
||||
background: rgba(251, 191, 41, 0.25);
|
||||
border-radius: 0;
|
||||
padding: 1px 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.theme-black .content-div mark {
|
||||
background: rgba(251, 191, 41, 0.40);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.content-div {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
padding: 1em;
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin: 3px;
|
||||
white-space: normal;
|
||||
color: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.theme-black .content-div {
|
||||
background-color: #37474F;
|
||||
border: 1px solid #616161;
|
||||
color: #E0E0E0FF;
|
||||
}
|
||||
|
||||
.graph {
|
||||
display: inline-block;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user