Compare commits

...

19 Commits
3.0.7 ... 3.1.2

Author SHA1 Message Date
ca845d80e8 Version bump 2023-06-07 20:40:45 -04:00
e2025df2c0 sist2-admin: don't set status to failed when using debug binary 2023-06-07 20:40:11 -04:00
7eb064162e close db connection before loop #346 2023-06-07 20:25:13 -04:00
7bc4b73e43 Use relative paths in sist2-admin #369 2023-06-07 19:59:50 -04:00
ca2e308d89 Bug fixes 2023-06-04 10:11:46 -04:00
c03c148273 SQLite backend support for sist2-admin #366 2023-06-03 18:33:44 -04:00
5522bcfa9b Add log cleanup features 2023-05-27 18:54:54 -04:00
f0fd708082 Fix timestamps in sist2-admin #359 2023-05-25 20:51:06 -04:00
6bf2b4c74d Merge remote-tracking branch 'origin/master' 2023-05-25 19:56:56 -04:00
d907576406 Update ES mapping for mtime #364 2023-05-25 19:56:46 -04:00
7659b481fa Merge pull request #365 from simon987/dependabot/npm_and_yarn/sist2-admin/frontend/socket.io-parser-4.2.3
Bump socket.io-parser from 4.2.2 to 4.2.3 in /sist2-admin/frontend
2023-05-24 08:52:16 -04:00
dependabot[bot]
e81e5ee457 Bump socket.io-parser from 4.2.2 to 4.2.3 in /sist2-admin/frontend
Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 4.2.2 to 4.2.3.
- [Release notes](https://github.com/socketio/socket.io-parser/releases)
- [Changelog](https://github.com/socketio/socket.io-parser/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io-parser/compare/4.2.2...4.2.3)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-24 01:24:25 +00:00
d4820d2fad updates 2023-05-22 10:52:53 -04:00
b3b3005692 Update thumbnail-quality parameter in sist2-admin 2023-05-20 13:16:07 -04:00
610882112d Use WEBP to encode thumbnails 2023-05-20 13:12:12 -04:00
e2e0cf260f Skip encrypted files when no passphrase is supplied 2023-05-18 20:09:17 -04:00
3ffa30cc6f Only fetch ES aggregations on the first request (#357) 2023-05-18 19:56:40 -04:00
7920318406 Add polish localization (pt.2) 2023-05-18 19:52:15 -04:00
41ef940623 Add polish localization 2023-05-18 19:42:39 -04:00
47 changed files with 1512 additions and 775 deletions

View File

@@ -37,4 +37,5 @@ state.db
build/ build/
__pycache__/ __pycache__/
sist2-vue/dist sist2-vue/dist
sist2-admin/frontend/dist sist2-admin/frontend/dist
*.fts

View File

@@ -45,6 +45,7 @@ RUN mkdir -p /usr/share/tessdata && \
curl -o /usr/share/tesseract-ocr/4.00/tessdata/spa.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/spa.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/spa.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/spa.traineddata &&\
curl -o /usr/share/tesseract-ocr/4.00/tessdata/deu.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/deu.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/deu.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/deu.traineddata &&\
curl -o /usr/share/tesseract-ocr/4.00/tessdata/equ.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/equ.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/equ.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/equ.traineddata &&\
curl -o /usr/share/tesseract-ocr/4.00/tessdata/pol.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/pol.traineddata &&\
curl -o /usr/share/tesseract-ocr/4.00/tessdata/chi_sim.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/chi_sim.traineddata curl -o /usr/share/tesseract-ocr/4.00/tessdata/chi_sim.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/chi_sim.traineddata
# sist2 # sist2

View File

@@ -20,16 +20,17 @@ RUN apt update && apt install -y curl libasan5 libmagic1 tesseract-ocr python3-p
RUN mkdir -p /usr/share/tessdata && \ RUN mkdir -p /usr/share/tessdata && \
cd /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/tesseract-ocr/4.00/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/tesseract-ocr/4.00/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/tesseract-ocr/4.00/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/tesseract-ocr/4.00/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/tesseract-ocr/4.00/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/tesseract-ocr/4.00/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/tesseract-ocr/4.00/tessdata/spa.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/spa.traineddata &&\
curl -o /usr/share/tessdata/deu.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/deu.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/deu.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/deu.traineddata &&\
curl -o /usr/share/tessdata/equ.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/equ.traineddata &&\ curl -o /usr/share/tesseract-ocr/4.00/tessdata/equ.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/equ.traineddata &&\
curl -o /usr/share/tessdata/chi_sim.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/chi_sim.traineddata curl -o /usr/share/tesseract-ocr/4.00/tessdata/pol.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/pol.traineddata &&\
curl -o /usr/share/tesseract-ocr/4.00/tessdata/chi_sim.traineddata https://raw.githubusercontent.com/tesseract-ocr/tessdata/master/chi_sim.traineddata
# sist2 # sist2
COPY --from=build /build/build/sist2 /root/sist2 COPY --from=build /build/build/sist2 /root/sist2

View File

@@ -28,7 +28,7 @@ sist2 (Simple incremental search tool)
\* See [format support](#format-support) \* See [format support](#format-support)
\*\* See [Archive files](#archive-files) \*\* See [Archive files](#archive-files)
\*\*\* See [OCR](#ocr) \*\*\* See [OCR](#ocr)
\*\*\*\* See [Named-Entity Recognition](#NER) \*\*\*\* See [Named-Entity Recognition](#NER)
## Getting Started ## Getting Started
@@ -46,7 +46,7 @@ services:
- "discovery.type=single-node" - "discovery.type=single-node"
- "ES_JAVA_OPTS=-Xms2g -Xmx2g" - "ES_JAVA_OPTS=-Xms2g -Xmx2g"
sist2-admin: sist2-admin:
image: simon987/sist2:3.0.7-x64-linux image: simon987/sist2:3.1.0-x64-linux
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./sist2-admin-data/:/sist2-admin/ - ./sist2-admin-data/:/sist2-admin/
@@ -126,7 +126,7 @@ Download the language data files with your package manager (`apt install tessera
directly [from Github](https://github.com/tesseract-ocr/tesseract/wiki/Data-Files). directly [from Github](https://github.com/tesseract-ocr/tesseract/wiki/Data-Files).
The `simon987/sist2` image comes with common languages The `simon987/sist2` image comes with common languages
(hin, jpn, eng, fra, rus, spa, chi_sim, deu) pre-installed. (hin, jpn, eng, fra, rus, spa, chi_sim, deu, pol) pre-installed.
You can use the `+` separator to specify multiple languages. The language You can use the `+` separator to specify multiple languages. The language
name must be identical to the `*.traineddata` file installed on your system name must be identical to the `*.traineddata` file installed on your system
@@ -206,7 +206,7 @@ docker run --rm --entrypoint cat my-sist2-image /root/sist2 > sist2-x64-linux
3. Install vcpkg dependencies 3. Install vcpkg dependencies
```bash ```bash
vcpkg install curl[core,openssl] sqlite3[core,fts5] cpp-jwt pcre cjson brotli libarchive[core,bzip2,libxml2,lz4,lzma,lzo] pthread tesseract libxml2 libmupdf gtest mongoose libmagic libraw gumbo ffmpeg[core,avcodec,avformat,swscale,swresample] vcpkg install curl[core,openssl] sqlite3[core,fts5] cpp-jwt pcre cjson brotli libarchive[core,bzip2,libxml2,lz4,lzma,lzo] pthread tesseract libxml2 libmupdf gtest mongoose libmagic libraw gumbo ffmpeg[core,avcodec,avformat,swscale,swresample,webp]
``` ```
4. Build 4. Build

View File

@@ -17,7 +17,7 @@ Lightning-fast file system indexer and search tool.
Scan options Scan options
-t, --threads=<int> Number of threads. DEFAULT: 1 -t, --threads=<int> Number of threads. DEFAULT: 1
-q, --thumbnail-quality=<int> Thumbnail quality, on a scale of 2 to 31, 2 being the best. DEFAULT: 2 -q, --thumbnail-quality=<int> Thumbnail quality, on a scale of 0 to 100, 100 being the best. DEFAULT: 50
--thumbnail-size=<int> Thumbnail size, in pixels. DEFAULT: 552 --thumbnail-size=<int> Thumbnail size, in pixels. DEFAULT: 552
--thumbnail-count=<int> Number of thumbnails to generate. Set a value > 1 to create video previews, set to 0 to disable thumbnails. DEFAULT: 1 --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 --content-size=<int> Number of bytes to be extracted from text documents. Set to 0 to disable. DEFAULT: 32768
@@ -88,8 +88,8 @@ Made by simon987 <me@simon987.net>. Released under GPL-3.0
See chart below for rough estimate of thumbnail size vs. thumbnail size & quality arguments: See chart below for rough estimate of thumbnail size vs. thumbnail size & quality arguments:
For example, `--thumbnail-size=500`, `--thumbnail-quality=2` for a directory with 8 million images will create a thumbnail database For example, `--thumbnail-size=500`, `--thumbnail-quality=50` for a directory with 8 million images will create a thumbnail database
that is about `8000000 * 36kB = 288GB`. that is about `8000000 * 11.8kB = 94.4GB`.
![thumbnail_size](thumbnail_size.png) ![thumbnail_size](thumbnail_size.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 169 KiB

View File

@@ -68,7 +68,7 @@
}, },
"mtime": { "mtime": {
"type": "date", "type": "date",
"format": "epoch_millis" "format": "epoch_second"
}, },
"size": { "size": {
"type": "long" "type": "long"

0
scripts/sqlite_extension_compile.sh Normal file → Executable file
View File

View File

@@ -0,0 +1,7 @@
docker build . -t tmp
docker run --rm -it\
-v $(pwd):/host \
tmp \
scan --ocr-lang eng --ocr-ebooks -t6 --incremental --very-verbose \
-o /host/docker.sist2 /host/third-party/libscan/libscan-test-files/test_files/

View File

@@ -9528,9 +9528,9 @@
} }
}, },
"node_modules/socket.io-parser": { "node_modules/socket.io-parser": {
"version": "4.2.2", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz",
"integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", "integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==",
"dependencies": { "dependencies": {
"@socket.io/component-emitter": "~3.1.0", "@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1" "debug": "~4.3.1"
@@ -17996,9 +17996,9 @@
} }
}, },
"socket.io-parser": { "socket.io-parser": {
"version": "4.2.2", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.3.tgz",
"integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", "integrity": "sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==",
"requires": { "requires": {
"@socket.io/component-emitter": "~3.1.0", "@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1" "debug": "~4.3.1"

View File

@@ -33,9 +33,26 @@ class Sist2AdminApi {
return axios.get(`${this.baseUrl}/api/job/${name}`); return axios.get(`${this.baseUrl}/api/job/${name}`);
} }
/** getSearchBackend(name) {
* @param {string} name return axios.get(`${this.baseUrl}/api/search_backend/${name}`);
*/ }
updateSearchBackend(name, data) {
return axios.put(`${this.baseUrl}/api/search_backend/${name}`, data);
}
getSearchBackends() {
return axios.get(`${this.baseUrl}/api/search_backend/`);
}
deleteBackend(name) {
return axios.delete(`${this.baseUrl}/api/search_backend/${name}`)
}
createBackend(name) {
return axios.post(`${this.baseUrl}/api/search_backend/${name}`);
}
getFrontend(name) { getFrontend(name) {
return axios.get(`${this.baseUrl}/api/frontend/${name}`); return axios.get(`${this.baseUrl}/api/frontend/${name}`);
} }
@@ -112,6 +129,16 @@ class Sist2AdminApi {
getSist2AdminInfo() { getSist2AdminInfo() {
return axios.get(`${this.baseUrl}/api/`); return axios.get(`${this.baseUrl}/api/`);
} }
getLogsToDelete(jobName, n) {
return axios.get(`${this.baseUrl}/api/job/${jobName}/logs_to_delete`, {
params: {n: n}
});
}
deleteTaskLogs(taskId) {
return axios.post(`${this.baseUrl}/api/task/${taskId}/delete_logs`);
}
} }
export default new Sist2AdminApi() export default new Sist2AdminApi()

View File

@@ -1,64 +0,0 @@
<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>

View File

@@ -44,8 +44,7 @@ export default {
return ""; return "";
} }
const date = Date.parse(dateString); return moment.utc(dateString).local().fromNow();
return moment(date).fromNow();
} }
} }
} }

View File

@@ -1,57 +1,94 @@
<template> <template>
<div> <div>
<b-form-checkbox :checked="desktopNotificationsEnabled" @change="updateNotifications($event)"> <b-form-checkbox :checked="desktopNotificationsEnabled" @change="updateNotifications($event)">
{{ $t("jobOptions.desktopNotifications") }} {{ $t("jobOptions.desktopNotifications") }}
</b-form-checkbox> </b-form-checkbox>
<b-form-checkbox v-model="job.schedule_enabled" @change="update()"> <b-form-checkbox v-model="job.schedule_enabled" @change="update()">
{{ $t("jobOptions.scheduleEnabled") }} {{ $t("jobOptions.scheduleEnabled") }}
</b-form-checkbox> </b-form-checkbox>
<label>{{ $t("jobOptions.cron") }}</label> <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> <b-form-input class="text-monospace" :state="cronValid" v-model="job.cron_expression"
</div> :disabled="!job.schedule_enabled" @change="update()"></b-form-input>
<label>{{ $t("jobOptions.keepNLogs") }}</label>
<b-input-group>
<b-form-input type="number" v-model="job.keep_last_n_logs" @change="update()"></b-form-input>
<b-input-group-append>
<b-button variant="danger" @click="onDeleteNowClick()">{{ $t("jobOptions.deleteNow") }}</b-button>
</b-input-group-append>
</b-input-group>
</div>
</template> </template>
<script> <script>
import Sist2AdminApi from "@/Sist2AdminApi";
export default { export default {
name: "JobOptions", name: "JobOptions",
props: ["job"], props: ["job"],
data() { data() {
return { return {
cronValid: undefined cronValid: undefined,
} logsToDelete: null
}, }
computed: { },
desktopNotificationsEnabled() { computed: {
return this.$store.state.jobDesktopNotificationMap[this.job.name]; desktopNotificationsEnabled() {
} return this.$store.state.jobDesktopNotificationMap[this.job.name];
}, }
},
mounted() { mounted() {
this.cronValid = this.checkCron(this.job.cron_expression) this.cronValid = this.checkCron(this.job.cron_expression)
}, },
methods: { methods: {
checkCron(expression) { checkCron(expression) {
return /((((\d+,)+\d+|(\d+([/-])\d+)|\d+|\*) ?){5,7})/.test(expression); return /((((\d+,)+\d+|(\d+([/-])\d+)|\d+|\*) ?){5,7})/.test(expression);
}, },
updateNotifications(value) { updateNotifications(value) {
this.$store.dispatch("setJobDesktopNotification", { this.$store.dispatch("setJobDesktopNotification", {
job: this.job.name, job: this.job.name,
enabled: value enabled: value
}); });
}, },
update() { update() {
if (this.job.schedule_enabled) { if (this.job.schedule_enabled) {
this.cronValid = this.checkCron(this.job.cron_expression); this.cronValid = this.checkCron(this.job.cron_expression);
} else { } else {
this.cronValid = undefined; this.cronValid = undefined;
} }
if (this.cronValid !== false) { if (this.cronValid !== false) {
this.$emit("change", this.job); this.$emit("change", this.job);
} }
},
onDeleteNowClick() {
Sist2AdminApi.getLogsToDelete(this.job.name, this.job.keep_last_n_logs).then(resp => {
const toDelete = resp.data;
const message = `Delete ${toDelete.length} log files?`;
this.$bvModal.msgBoxConfirm(message, {
title: this.$t("confirmation"),
size: "sm",
buttonSize: "sm",
okVariant: "danger",
okTitle: this.$t("delete"),
cancelTitle: this.$t("cancel"),
footerClass: "p-2",
hideHeaderClose: false,
centered: true
}).then(value => {
if (value) {
toDelete.forEach(row => {
Sist2AdminApi.deleteTaskLogs(row["id"]);
});
}
});
})
}
}, },
},
} }
</script> </script>

View File

@@ -7,7 +7,7 @@
<b-form-input type="number" min="1" v-model="options.threads" @change="update()"></b-form-input> <b-form-input type="number" min="1" v-model="options.threads" @change="update()"></b-form-input>
<label>{{ $t("scanOptions.thumbnailQuality") }}</label> <label>{{ $t("scanOptions.thumbnailQuality") }}</label>
<b-form-input type="number" min="1" max="31" v-model="options.thumbnail_quality" @change="update()"></b-form-input> <b-form-input type="number" min="0" max="100" v-model="options.thumbnail_quality" @change="update()"></b-form-input>
<label>{{ $t("scanOptions.thumbnailCount") }}</label> <label>{{ $t("scanOptions.thumbnailCount") }}</label>
<b-form-input type="number" min="0" max="1000" v-model="options.thumbnail_count" @change="update()"></b-form-input> <b-form-input type="number" min="0" max="1000" v-model="options.thumbnail_count" @change="update()"></b-form-input>

View File

@@ -0,0 +1,24 @@
<template>
<b-list-group-item action :to="`/searchBackend/${backend.name}`">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">
{{ backend.name }}
</h5>
<div>
<b-badge v-if="backend.backend_type === 'sqlite'" variant="info">SQLite</b-badge>
<b-badge v-else variant="info">Elasticsearch</b-badge>
</div>
</div>
</b-list-group-item>
</template>
<script>
export default {
name: "SearchBackendListItem",
props: ["backend"],
}
</script>

View File

@@ -0,0 +1,37 @@
<template>
<b-progress v-if="loading" striped animated value="100"></b-progress>
<div v-else>
<label>{{$t("backendOptions.searchBackend")}}</label>
<b-select :options="options" :value="value" @change="$emit('change', $event)"></b-select>
</div>
</template>
<script>
import Sist2AdminApi from "@/Sist2AdminApi";
export default {
name: "SearchBackendSelect",
props: ["value"],
data() {
return {
loading: true,
backends: null,
}
},
computed: {
options() {
return this.backends.map(backend => backend.name)
}
},
mounted() {
Sist2AdminApi.getSearchBackends().then(resp => {
this.loading = false;
this.backends = resp.data
})
}
}
</script>
<style scoped>
</style>

View File

@@ -1,56 +1,35 @@
<template> <template>
<div> <div>
<label>{{ $t("webOptions.esUrl") }}</label>
<b-alert :variant="esTestOk ? 'success' : 'danger'" :show="showEsTestAlert" class="mt-1">
{{ esTestMessage }}
</b-alert>
<b-input-group> <label>{{ $t("webOptions.lang") }}</label>
<b-form-input v-model="options.es_url" @change="update()"></b-form-input> <b-form-select v-model="options.lang" :options="['en', 'fr', 'zh-CN']" @change="update()"></b-form-select>
<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()"> <label>{{ $t("webOptions.bind") }}</label>
{{ $t("webOptions.esInsecure") }} <b-form-input v-model="options.bind" @change="update()"></b-form-input>
</b-form-checkbox>
<label>{{ $t("webOptions.esIndex") }}</label> <label>{{ $t("webOptions.tagline") }}</label>
<b-form-input v-model="options.es_index" @change="update()"></b-form-input> <b-form-textarea v-model="options.tagline" @change="update()"></b-form-textarea>
<label>{{ $t("webOptions.lang") }}</label> <label>{{ $t("webOptions.auth") }}</label>
<b-form-select v-model="options.lang" :options="['en', 'fr', 'zh-CN']" @change="update()"></b-form-select> <b-form-input v-model="options.auth" @change="update()"></b-form-input>
<label>{{ $t("webOptions.bind") }}</label> <label>{{ $t("webOptions.tagAuth") }}</label>
<b-form-input v-model="options.bind" @change="update()"></b-form-input> <b-form-input v-model="options.tag_auth" @change="update()"></b-form-input>
<label>{{ $t("webOptions.tagline") }}</label> <br>
<b-form-textarea v-model="options.tagline" @change="update()"></b-form-textarea> <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.auth") }}</label> <label>{{ $t("webOptions.auth0Domain") }}</label>
<b-form-input v-model="options.auth" @change="update()"></b-form-input> <b-form-input v-model="options.auth0_domain" @change="update()"></b-form-input>
<label>{{ $t("webOptions.tagAuth") }}</label> <label>{{ $t("webOptions.auth0ClientId") }}</label>
<b-form-input v-model="options.tag_auth" @change="update()"></b-form-input> <b-form-input v-model="options.auth0_client_id" @change="update()"></b-form-input>
<br> <label>{{ $t("webOptions.auth0PublicKey") }}</label>
<h5>Auth0 options</h5> <b-textarea rows="10" v-model="options.auth0_public_key" @change="update()"></b-textarea>
<label>{{ $t("webOptions.auth0Audience") }}</label> </div>
<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> </template>
<script> <script>
@@ -58,31 +37,24 @@
import sist2AdminApi from "@/Sist2AdminApi"; import sist2AdminApi from "@/Sist2AdminApi";
export default { export default {
name: "WebOptions", name: "WebOptions",
props: ["options", "frontendName"], props: ["options", "frontendName"],
data() { data() {
return { return {
showEsTestAlert: false, showEsTestAlert: false,
esTestOk: false, esTestOk: false,
esTestMessage: "", esTestMessage: "",
} }
},
methods: {
update() {
if (!this.options.es_url.startsWith("https")) {
this.options.es_insecure_ssl = false;
}
this.$emit("change", this.options);
}, },
testEs() { methods: {
sist2AdminApi.pingEs(this.options.es_url, this.options.es_insecure_ssl).then((resp) => { update() {
this.showEsTestAlert = true; if (!this.options.es_url.startsWith("https")) {
this.esTestOk = resp.data.ok; this.options.es_insecure_ssl = false;
this.esTestMessage = resp.data.message; }
});
this.$emit("change", this.options);
},
} }
}
} }
</script> </script>

View File

@@ -5,10 +5,13 @@ export default {
go: "Go", go: "Go",
online: "online", online: "online",
offline: "offline", offline: "offline",
view: "View",
delete: "Delete", delete: "Delete",
runNow: "Index now", runNow: "Index now",
create: "Create", create: "Create",
cancel: "Cancel",
test: "Test", test: "Test",
confirmation: "Confirmation",
jobTitle: "job configuration", jobTitle: "job configuration",
tasks: "Tasks", tasks: "Tasks",
@@ -45,12 +48,13 @@ export default {
extraQueryArgs: "Extra query arguments when launching from sist2-admin", extraQueryArgs: "Extra query arguments when launching from sist2-admin",
customUrl: "Custom URL when launching from sist2-admin", customUrl: "Custom URL when launching from sist2-admin",
searchBackends: "Search backends",
searchBackendTitle: "search backend configuration",
newBackendName: "New search backend name",
selectJobs: "Select jobs", selectJobs: "Select jobs",
webOptions: { webOptions: {
title: "Web options", title: "Web options",
esUrl: "Elasticsearch URL",
esIndex: "Elasticsearch index name",
esInsecure: "Do not verify SSL connections to Elasticsearch.",
lang: "UI Language", lang: "UI Language",
bind: "Listen address", bind: "Listen address",
tagline: "Tagline in navbar", tagline: "Tagline in navbar",
@@ -61,12 +65,24 @@ export default {
auth0ClientId: "Auth0 client ID", auth0ClientId: "Auth0 client ID",
auth0PublicKey: "Auth0 public key", auth0PublicKey: "Auth0 public key",
}, },
backendOptions: {
title: "Search backend options",
searchBackend: "Search backend",
type: "Search backend type",
esUrl: "Elasticsearch URL",
esIndex: "Elasticsearch index name",
esInsecure: "Do not verify SSL connections to Elasticsearch.",
threads: "Number of threads",
batchSize: "Index batch size",
script: "User script",
searchIndex: "Search index file location"
},
scanOptions: { scanOptions: {
title: "Scanning options", title: "Scanning options",
path: "Path", path: "Path",
threads: "Number of threads", threads: "Number of threads",
memThrottle: "Total memory threshold in MiB for scan throttling", memThrottle: "Total memory threshold in MiB for scan throttling",
thumbnailQuality: "Thumbnail quality, on a scale of 2 to 32, 2 being the best", thumbnailQuality: "Thumbnail quality, on a scale of 0 to 100, 100 being the best",
thumbnailCount: "Number of thumbnails to generate. Set a value > 1 to create video previews, set to 0 to disable thumbnails.", 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", thumbnailSize: "Thumbnail size, in pixels",
contentSize: "Number of bytes to be extracted from text documents. Set to 0 to disable", contentSize: "Number of bytes to be extracted from text documents. Set to 0 to disable",
@@ -87,20 +103,14 @@ export default {
treemapThreshold: "Relative size threshold for treemap", treemapThreshold: "Relative size threshold for treemap",
optimizeIndex: "Defragment index file after scan to reduce its file size." optimizeIndex: "Defragment index file after scan to reduce its file size."
}, },
indexOptions: {
title: "Indexing options",
threads: "Number of threads",
esUrl: "Elasticsearch URL",
esIndex: "Elasticsearch index name",
esInsecure: "Do not verify SSL connections to Elasticsearch.",
batchSize: "Index batch size",
script: "User script"
},
jobOptions: { jobOptions: {
title: "Job options", title: "Job options",
cron: "Job schedule", cron: "Job schedule",
keepNLogs: "Keep last N log files. Set to -1 to keep all logs.",
deleteNow: "Delete now",
scheduleEnabled: "Enable scheduled re-scan", scheduleEnabled: "Enable scheduled re-scan",
noJobAvailable: "No jobs available.", noJobAvailable: "No jobs available.",
noBackendError: "You must select a search backend to run this job",
desktopNotifications: "Desktop notifications" desktopNotifications: "Desktop notifications"
}, },
frontendOptions: { frontendOptions: {

View File

@@ -5,6 +5,7 @@ import Job from "@/views/Job";
import Tasks from "@/views/Tasks"; import Tasks from "@/views/Tasks";
import Frontend from "@/views/Frontend"; import Frontend from "@/views/Frontend";
import Tail from "@/views/Tail"; import Tail from "@/views/Tail";
import SearchBackend from "@/views/SearchBackend.vue";
Vue.use(VueRouter); Vue.use(VueRouter);
@@ -29,6 +30,11 @@ const routes = [
name: "Frontend", name: "Frontend",
component: Frontend component: Frontend
}, },
{
path: "/searchBackend/:name",
name: "SearchBackend",
component: SearchBackend
},
{ {
path: "/log/:taskId", path: "/log/:taskId",
name: "Tail", name: "Tail",

View File

@@ -1,60 +1,70 @@
<template> <template>
<b-card> <b-card>
<b-card-title> <b-card-title>
{{ name }} {{ name }}
<small style="vertical-align: top"> <small style="vertical-align: top">
<b-badge v-if="!loading && frontend.running" variant="success">{{ $t("online") }}</b-badge> <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> <b-badge v-else-if="!loading" variant="secondary">{{ $t("offline") }}</b-badge>
</small> </small>
</b-card-title> </b-card-title>
<div class="mb-3" v-if="!loading"> <div class="mb-3" v-if="!loading">
<b-button class="mr-1" :disabled="frontend.running || !valid" variant="success" @click="start()">{{ <b-button class="mr-1" :disabled="frontend.running || !valid" variant="success" @click="start()">{{
$t("start") $t("start")
}} }}
</b-button> </b-button>
<b-button class="mr-1" :disabled="!frontend.running" variant="danger" @click="stop()">{{ <b-button class="mr-1" :disabled="!frontend.running" variant="danger" @click="stop()">{{
$t("stop") $t("stop")
}} }}
</b-button> </b-button>
<b-button class="mr-1" :disabled="!frontend.running" variant="primary" :href="frontendUrl" target="_blank"> <b-button class="mr-1" :disabled="!frontend.running" variant="primary" :href="frontendUrl" target="_blank">
{{ $t("go") }} {{ $t("go") }}
</b-button> </b-button>
<b-button variant="danger" @click="deleteFrontend()">{{ $t("delete") }}</b-button> <b-button variant="danger" @click="deleteFrontend()">{{ $t("delete") }}</b-button>
</div> </div>
<b-progress v-if="loading" striped animated value="100"></b-progress> <b-progress v-if="loading" striped animated value="100"></b-progress>
<b-card-body v-else> <b-card-body v-else>
<h4>{{ $t("frontendOptions.title") }}</h4> <h4>{{ $t("frontendOptions.title") }}</h4>
<b-card> <b-card>
<b-form-checkbox v-model="frontend.auto_start" @change="update()"> <b-form-checkbox v-model="frontend.auto_start" @change="update()">
{{ $t("autoStart") }} {{ $t("autoStart") }}
</b-form-checkbox> </b-form-checkbox>
<label>{{ $t("extraQueryArgs") }}</label> <label>{{ $t("extraQueryArgs") }}</label>
<b-form-input v-model="frontend.extra_query_args" @change="update()"></b-form-input> <b-form-input v-model="frontend.extra_query_args" @change="update()"></b-form-input>
<label>{{ $t("customUrl") }}</label> <label>{{ $t("customUrl") }}</label>
<b-form-input v-model="frontend.custom_url" @change="update()" placeholder="http://"></b-form-input> <b-form-input v-model="frontend.custom_url" @change="update()" placeholder="http://"></b-form-input>
<br/> <br/>
<b-alert v-if="!valid" variant="warning" show>{{ $t("frontendOptions.noJobSelectedWarning") }}</b-alert> <b-alert v-if="!valid" variant="warning" show>{{ $t("frontendOptions.noJobSelectedWarning") }}</b-alert>
<JobCheckboxGroup :frontend="frontend" @input="update()"></JobCheckboxGroup> <JobCheckboxGroup :frontend="frontend" @input="update()"></JobCheckboxGroup>
</b-card> </b-card>
<br/> <br/>
<h4>{{ $t("webOptions.title") }}</h4> <h4>{{ $t("webOptions.title") }}</h4>
<b-card> <b-card>
<WebOptions :options="frontend.web_options" :frontend-name="$route.params.name" @change="update()"></WebOptions> <WebOptions :options="frontend.web_options" :frontend-name="$route.params.name"
</b-card> @change="update()"></WebOptions>
</b-card-body> </b-card>
</b-card> <br>
<h4>{{ $t("backendOptions.title") }}</h4>
<b-card>
<SearchBackendSelect :value="frontend.web_options.search_backend"
@change="onBackendSelect($event)"></SearchBackendSelect>
</b-card>
</b-card-body>
</b-card>
</template> </template>
<script> <script>
@@ -62,68 +72,73 @@
import Sist2AdminApi from "@/Sist2AdminApi"; import Sist2AdminApi from "@/Sist2AdminApi";
import JobCheckboxGroup from "@/components/JobCheckboxGroup"; import JobCheckboxGroup from "@/components/JobCheckboxGroup";
import WebOptions from "@/components/WebOptions"; import WebOptions from "@/components/WebOptions";
import SearchBackendSelect from "@/components/SearchBackendSelect.vue";
export default { export default {
name: 'Frontend', name: 'Frontend',
components: {JobCheckboxGroup, WebOptions}, components: {SearchBackendSelect, JobCheckboxGroup, WebOptions},
data() { data() {
return { return {
loading: true, loading: true,
frontend: null, frontend: null,
} }
},
computed: {
valid() {
return !this.loading && this.frontend.jobs.length > 0;
}, },
frontendUrl() { computed: {
if (this.frontend.custom_url) { valid() {
return this.frontend.custom_url + this.args; 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")) { 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 + "//" + window.location.hostname + ":" + this.port + this.args;
} }
return window.location.protocol + "//" + this.frontend.web_options.bind + 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 "";
}
}, },
name() { mounted() {
return this.$route.params.name; Sist2AdminApi.getFrontend(this.name).then(resp => {
this.frontend = resp.data;
this.loading = false;
});
}, },
port() { methods: {
return this.frontend.web_options.bind.split(":")[1] start() {
}, this.frontend.running = true;
args() { Sist2AdminApi.startFrontend(this.name)
const args = this.frontend.extra_query_args; },
if (args !== "") { stop() {
return "#" + (args.startsWith("?") ? (args) : ("?" + args)); this.frontend.running = false;
} Sist2AdminApi.stopFrontend(this.name)
return ""; },
deleteFrontend() {
Sist2AdminApi.deleteFrontend(this.name).then(() => {
this.$router.push("/");
});
},
update() {
Sist2AdminApi.updateFrontend(this.name, this.frontend);
},
onBackendSelect(backend) {
this.frontend.web_options.search_backend = backend;
this.update();
}
} }
},
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> </script>

View File

@@ -1,60 +1,89 @@
<template> <template>
<div> <div>
<b-card> <b-card>
<b-card-title>{{ $t("jobs") }}</b-card-title> <b-card-title>{{ $t("jobs") }}</b-card-title>
<b-row> <b-row>
<b-col> <b-col>
<b-input id="new-job" v-model="newJobName" :placeholder="$t('newJobName')"></b-input> <b-input id="new-job" v-model="newJobName" :placeholder="$t('newJobName')"></b-input>
<b-popover <b-popover
:show.sync="showHelp" :show.sync="showHelp"
target="new-job" target="new-job"
placement="top" placement="top"
triggers="manual" triggers="manual"
variant="primary" variant="primary"
:content="$t('newJobHelp')" :content="$t('newJobHelp')"
></b-popover> ></b-popover>
</b-col> </b-col>
<b-col> <b-col>
<b-button variant="primary" @click="createJob()" :disabled="!jobNameValid(newJobName)">{{ $t("create") }} <b-button variant="primary" @click="createJob()" :disabled="!jobNameValid(newJobName)">
</b-button> {{ $t("create") }}
</b-col> </b-button>
</b-row> </b-col>
</b-row>
<hr/> <hr/>
<b-progress v-if="jobsLoading" striped animated value="100"></b-progress> <b-progress v-if="jobsLoading" striped animated value="100"></b-progress>
<b-list-group v-else> <b-list-group v-else>
<JobListItem v-for="job in jobs" :key="job.name" :job="job"></JobListItem> <JobListItem v-for="job in jobs" :key="job.name" :job="job"></JobListItem>
</b-list-group> </b-list-group>
</b-card> </b-card>
<br/> <br/>
<b-card> <b-card>
<b-card-title>{{ $t("frontends") }}</b-card-title> <b-card-title>{{ $t("frontends") }}</b-card-title>
<b-row> <b-row>
<b-col> <b-col>
<b-input v-model="newFrontendName" :placeholder="$t('newFrontendName')"></b-input> <b-input v-model="newFrontendName" :placeholder="$t('newFrontendName')"></b-input>
</b-col> </b-col>
<b-col> <b-col>
<b-button variant="primary" @click="createFrontend()" :disabled="!frontendNameValid(newFrontendName)"> <b-button variant="primary" @click="createFrontend()"
{{ $t("create") }} :disabled="!frontendNameValid(newFrontendName)">
</b-button> {{ $t("create") }}
</b-col> </b-button>
</b-row> </b-col>
</b-row>
<hr/> <hr/>
<b-progress v-if="frontendsLoading" striped animated value="100"></b-progress> <b-progress v-if="frontendsLoading" striped animated value="100"></b-progress>
<b-list-group v-else> <b-list-group v-else>
<FrontendListItem v-for="frontend in frontends" <FrontendListItem v-for="frontend in frontends"
:key="frontend.name" :frontend="frontend"></FrontendListItem> :key="frontend.name" :frontend="frontend"></FrontendListItem>
</b-list-group> </b-list-group>
</b-card> </b-card>
</div>
<br/>
<b-card>
<b-card-title>{{ $t("searchBackends") }}</b-card-title>
<b-row>
<b-col>
<b-input v-model="newBackendName" :placeholder="$t('newBackendName')"></b-input>
</b-col>
<b-col>
<b-button variant="primary" @click="createBackend()"
:disabled="!backendNameValid(newBackendName)">
{{ $t("create") }}
</b-button>
</b-col>
</b-row>
<hr/>
<b-progress v-if="backendsLoading" striped animated value="100"></b-progress>
<b-list-group v-else>
<SearchBackendListItem v-for="backend in backends"
:key="backend.name" :backend="backend"></SearchBackendListItem>
</b-list-group>
</b-card>
</div>
</template> </template>
<script> <script>
@@ -62,61 +91,80 @@ import JobListItem from "@/components/JobListItem";
import {formatBindAddress} from "@/util"; import {formatBindAddress} from "@/util";
import Sist2AdminApi from "@/Sist2AdminApi"; import Sist2AdminApi from "@/Sist2AdminApi";
import FrontendListItem from "@/components/FrontendListItem"; import FrontendListItem from "@/components/FrontendListItem";
import SearchBackendListItem from "@/components/SearchBackendListItem.vue";
export default { export default {
name: "Jobs", name: "Jobs",
components: {JobListItem, FrontendListItem}, components: {SearchBackendListItem, JobListItem, FrontendListItem},
data() { data() {
return { return {
jobsLoading: true, jobsLoading: true,
newJobName: "", newJobName: "",
jobs: [], jobs: [],
frontendsLoading: true, frontendsLoading: true,
frontends: [], frontends: [],
formatBindAddress, formatBindAddress,
newFrontendName: "", newFrontendName: "",
showHelp: false backends: [],
backendsLoading: true,
newBackendName: "",
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(frontend => frontend.name === name)) {
return false;
}
return /^[a-zA-Z0-9-_,.; ]+$/.test(name);
},
backendNameValid(name) {
if (this.backends.some(backend => backend.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;
});
Sist2AdminApi.getSearchBackends().then(resp => {
this.backends = resp.data;
this.backendsLoading = false;
})
},
createJob() {
Sist2AdminApi.createJob(this.newJobName).then(this.reload);
},
createFrontend() {
Sist2AdminApi.createFrontend(this.newFrontendName).then(this.reload)
},
createBackend() {
Sist2AdminApi.createBackend(this.newBackendName).then(this.reload);
}
} }
},
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> </script>

View File

@@ -1,92 +1,112 @@
<template> <template>
<b-card> <b-card>
<b-card-title> <b-card-title>
[{{ getName() }}] [{{ getName() }}]
{{ $t("jobTitle") }} {{ $t("jobTitle") }}
</b-card-title> </b-card-title>
<div class="mb-3"> <div class="mb-3">
<b-button class="mr-1" variant="primary" @click="runJob()">{{ $t("runNow") }}</b-button> <b-button class="mr-1" variant="primary" @click="runJob()" :disabled="!valid">{{ $t("runNow") }}</b-button>
<b-button variant="danger" @click="deleteJob()">{{ $t("delete") }}</b-button> <b-button variant="danger" @click="deleteJob()">{{ $t("delete") }}</b-button>
</div> </div>
<div v-if="job"> <div v-if="job">
{{ $t("status") }}: <code>{{ job.status }}</code> {{ $t("status") }}: <code>{{ job.status }}</code>
</div> </div>
<b-progress v-if="loading" striped animated value="100"></b-progress> <b-progress v-if="loading" striped animated value="100"></b-progress>
<b-card-body v-else> <b-card-body v-else>
<h4>{{ $t("jobOptions.title") }}</h4> <h4>{{ $t("jobOptions.title") }}</h4>
<b-card> <b-card>
<JobOptions :job="job" @change="update"></JobOptions> <JobOptions :job="job" @change="update"></JobOptions>
</b-card> </b-card>
<br/> <br/>
<h4>{{ $t("scanOptions.title") }}</h4> <h4>{{ $t("scanOptions.title") }}</h4>
<b-card> <b-card>
<ScanOptions :options="job.scan_options" @change="update()"></ScanOptions> <ScanOptions :options="job.scan_options" @change="update()"></ScanOptions>
</b-card> </b-card>
<br/> <br/>
<h4>{{ $t("indexOptions.title") }}</h4> <h4>{{ $t("backendOptions.title") }}</h4>
<b-card> <b-card>
<IndexOptions :options="job.index_options" @change="update()"></IndexOptions> <b-alert v-if="!valid" variant="warning" show>{{ $t("jobOptions.noBackendError") }}</b-alert>
</b-card> <SearchBackendSelect :value="job.index_options.search_backend"
@change="onBackendSelect($event)"></SearchBackendSelect>
</b-card>
</b-card-body> </b-card-body>
</b-card> </b-card>
</template> </template>
<script> <script>
import ScanOptions from "@/components/ScanOptions"; import ScanOptions from "@/components/ScanOptions";
import Sist2AdminApi from "@/Sist2AdminApi"; import Sist2AdminApi from "@/Sist2AdminApi";
import IndexOptions from "@/components/IndexOptions";
import JobOptions from "@/components/JobOptions"; import JobOptions from "@/components/JobOptions";
import SearchBackendSelect from "@/components/SearchBackendSelect.vue";
export default { export default {
name: "Job", name: "Job",
components: { components: {
IndexOptions, SearchBackendSelect,
ScanOptions, ScanOptions,
JobOptions JobOptions
}, },
data() { data() {
return { return {
loading: true, loading: true,
job: null 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("/");
})
.catch(err => {
this.$bvToast.toast("Cannot delete job " +
"because it is referenced by a frontend", {
title: "Error",
variant: "danger",
toaster: "b-toaster-bottom-right"
});
})
},
onBackendSelect(backend) {
this.job.index_options.search_backend = backend;
this.update();
}
},
mounted() {
Sist2AdminApi.getJob(this.getName()).then(resp => {
this.loading = false;
this.job = resp.data;
})
},
computed: {
valid() {
return this.job?.index_options.search_backend != 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> </script>

View File

@@ -0,0 +1,126 @@
<template>
<b-card>
<b-card-title>
<span class="text-monospace">{{ getName() }}</span>
{{ $t("searchBackendTitle") }}
</b-card-title>
<div class="mb-3">
<b-button variant="danger" @click="deleteBackend()">{{ $t("delete") }}</b-button>
</div>
<b-progress v-if="loading" striped animated value="100"></b-progress>
<b-card-body v-else>
<label>{{ $t("backendOptions.type") }}</label>
<b-select :options="backendTypeOptions" v-model="backend.backend_type" @change="update()"></b-select>
<hr/>
<template v-if="backend.backend_type === 'elasticsearch'">
<b-alert :variant="esTestOk ? 'success' : 'danger'" :show="showEsTestAlert" class="mt-1">
{{ esTestMessage }}
</b-alert>
<label>{{ $t("backendOptions.esUrl") }}</label>
<b-input-group>
<b-form-input v-model="backend.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="backend.es_insecure_ssl" :disabled="!this.backend.es_url.startsWith('https')"
@change="update()">
{{ $t("backendOptions.esInsecure") }}
</b-form-checkbox>
<label>{{ $t("backendOptions.esIndex") }}</label>
<b-form-input v-model="backend.es_index" @change="update()"></b-form-input>
<label>{{ $t("backendOptions.threads") }}</label>
<b-form-input v-model="backend.threads" type="number" min="1" @change="update()"></b-form-input>
<label>{{ $t("backendOptions.batchSize") }}</label>
<b-form-input v-model="backend.batch_size" type="number" min="1" @change="update()"></b-form-input>
<label>{{ $t("backendOptions.script") }}</label>
<b-form-textarea v-model="backend.script" rows="6" @change="update()"></b-form-textarea>
</template>
<template v-else>
<label>{{ $t("backendOptions.searchIndex") }}</label>
<b-form-input v-model="backend.search_index" disabled></b-form-input>
</template>
</b-card-body>
</b-card>
</template>
<script>
import sist2AdminApi from "@/Sist2AdminApi";
import Sist2AdminApi from "@/Sist2AdminApi";
export default {
name: "SearchBackend",
data() {
return {
showEsTestAlert: false,
esTestOk: false,
esTestMessage: "",
loading: true,
backend: null,
backendTypeOptions: [
{
text: "Elasticsearch",
value: "elasticsearch"
},
{
text: "SQLite",
value: "sqlite"
}
]
}
},
mounted() {
Sist2AdminApi.getSearchBackend(this.getName()).then(resp => {
this.backend = resp.data;
this.loading = false;
});
},
methods: {
getName() {
return this.$route.params.name;
},
testEs() {
sist2AdminApi.pingEs(this.backend.es_url, this.backend.es_insecure_ssl)
.then((resp) => {
this.showEsTestAlert = true;
this.esTestOk = resp.data.ok;
this.esTestMessage = resp.data.message;
});
},
update() {
Sist2AdminApi.updateSearchBackend(this.getName(), this.backend);
},
deleteBackend() {
Sist2AdminApi.deleteBackend(this.getName())
.then(() => {
this.$router.push("/");
})
.catch(err => {
this.$bvToast.toast("Cannot delete search backend " +
"because it is referenced by a job or frontend", {
title: "Error",
variant: "danger",
toaster: "b-toaster-bottom-right"
});
})
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,38 +1,49 @@
<template> <template>
<div> <div>
<b-card v-if="tasks.length > 0"> <b-card v-if="tasks.length > 0">
<h2>{{ $t("runningTasks") }}</h2> <h2>{{ $t("runningTasks") }}</h2>
<b-list-group> <b-list-group>
<TaskListItem v-for="task in tasks" :key="task.id" :task="task"></TaskListItem> <TaskListItem v-for="task in tasks" :key="task.id" :task="task"></TaskListItem>
</b-list-group> </b-list-group>
</b-card> </b-card>
<b-card class="mt-4"> <b-card class="mt-4">
<b-card-title>{{ $t("taskHistory") }}</b-card-title> <b-card-title>{{ $t("taskHistory") }}</b-card-title>
<br/> <br/>
<b-table <b-table
id="task-history" id="task-history"
:items="historyItems" :items="historyItems"
:fields="historyFields" :fields="historyFields"
:current-page="historyCurrentPage" :current-page="historyCurrentPage"
:tbody-tr-class="rowClass" :tbody-tr-class="rowClass"
:per-page="10" :per-page="10"
> >
<template #cell(logs)="data"> <template #cell(logs)="data">
<router-link :to="`/log/${data.item.logs}`">{{ $t("logs") }}</router-link> <template v-if="data.item._row.has_logs">
</template> <b-button variant="link" size="sm" :to="`/log/${data.item.id}`">
{{ $t("view") }}
</b-button>
/
<b-button variant="link" size="sm" @click="deleteLogs(data.item.id)">
{{ $t("delete") }}
</b-button>
</template>
</template>
</b-table> <template #cell(delete)="data">
</template>
<b-pagination limit="20" v-model="historyCurrentPage" :total-rows="historyItems.length" </b-table>
:per-page="10"></b-pagination>
</b-card> <b-pagination limit="20" v-model="historyCurrentPage" :total-rows="historyItems.length"
</div> :per-page="10"></b-pagination>
</b-card>
</div>
</template> </template>
<script> <script>
@@ -45,106 +56,116 @@ const HOUR = 3600;
const MINUTE = 60; const MINUTE = 60;
function humanDuration(sec_num) { function humanDuration(sec_num) {
sec_num = sec_num / 1000; sec_num = sec_num / 1000;
const days = Math.floor(sec_num / DAY); const days = Math.floor(sec_num / DAY);
sec_num -= days * DAY; sec_num -= days * DAY;
const hours = Math.floor(sec_num / HOUR); const hours = Math.floor(sec_num / HOUR);
sec_num -= hours * HOUR; sec_num -= hours * HOUR;
const minutes = Math.floor(sec_num / MINUTE); const minutes = Math.floor(sec_num / MINUTE);
sec_num -= minutes * MINUTE; sec_num -= minutes * MINUTE;
const seconds = Math.floor(sec_num); const seconds = Math.floor(sec_num);
if (days > 0) { if (days > 0) {
return `${days} days ${hours}h ${minutes}m ${seconds}s`; return `${days} days ${hours}h ${minutes}m ${seconds}s`;
} }
if (hours > 0) { if (hours > 0) {
return `${hours}h ${minutes}m ${seconds}s`; return `${hours}h ${minutes}m ${seconds}s`;
} }
if (minutes > 0) { if (minutes > 0) {
return `${minutes}m ${seconds}s`; return `${minutes}m ${seconds}s`;
} }
if (seconds > 0) { if (seconds > 0) {
return `${seconds}s`; return `${seconds}s`;
} }
return "<0s"; return "<0s";
} }
export default { export default {
name: 'Tasks', name: 'Tasks',
components: {TaskListItem}, components: {TaskListItem},
data() { data() {
return { return {
loading: true, loading: true,
tasks: [], tasks: [],
taskHistory: [], taskHistory: [],
timerId: null, timerId: null,
historyFields: [ historyFields: [
{key: "name", label: this.$t("taskName")}, {key: "name", label: this.$t("taskName")},
{key: "time", label: this.$t("taskStarted")}, {key: "time", label: this.$t("taskStarted")},
{key: "duration", label: this.$t("taskDuration")}, {key: "duration", label: this.$t("taskDuration")},
{key: "status", label: this.$t("taskStatus")}, {key: "status", label: this.$t("taskStatus")},
{key: "logs", label: this.$t("logs")}, {key: "logs", label: this.$t("logs")},
], ],
historyCurrentPage: 1, historyCurrentPage: 1,
historyItems: [] historyItems: []
} }
}, },
props: { props: {
msg: String msg: String
}, },
mounted() { mounted() {
this.loading = true; this.loading = true;
this.update().then(() => this.loading = false); this.update().then(() => this.loading = false);
this.timerId = window.setInterval(this.update, 1000); this.timerId = window.setInterval(this.update, 1000);
this.updateHistory(); this.updateHistory();
},
destroyed() {
if (this.timerId) {
window.clearInterval(this.timerId);
}
},
methods: {
rowClass(row) {
if (row.status === "failed") {
return "table-danger";
}
return null;
}, },
updateHistory() { destroyed() {
Sist2AdminApi.getTaskHistory().then(resp => { if (this.timerId) {
this.historyItems = resp.data.map(row => ({ window.clearInterval(this.timerId);
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() { methods: {
return Sist2AdminApi.getTasks().then(resp => { rowClass(row) {
this.tasks = resp.data; if (row.status === "failed") {
}) return "table-danger";
}, }
taskDuration(task) { return null;
const start = moment.utc(task.started); },
const end = moment.utc(task.ended); updateHistory() {
Sist2AdminApi.getTaskHistory().then(resp => {
this.historyItems = resp.data.map(row => ({
id: row.id,
name: row.name,
duration: this.taskDuration(row),
time: moment.utc(row.started).local().format("dd, MMM Do YYYY, HH:mm:ss"),
logs: null,
status: [0,1].includes(row.return_code) ? "ok" : "failed",
_row: row
}));
});
},
update() {
return Sist2AdminApi.getTasks().then(resp => {
this.tasks = resp.data;
})
},
taskDuration(task) {
const start = moment.utc(task.started);
const end = moment.utc(task.ended);
return humanDuration(end.diff(start)) return humanDuration(end.diff(start))
},
deleteLogs(taskId) {
Sist2AdminApi.deleteTaskLogs(taskId).then(() => {
this.updateHistory();
})
}
} }
}
} }
</script> </script>
<style scoped> <style scoped>
#task-history { #task-history {
font-family: monospace; font-family: monospace;
font-size: 12px; font-size: 12px;
}
.btn-link {
padding: 0;
} }
</style> </style>

View File

@@ -4913,9 +4913,9 @@ socket.io-client@^4.5.1:
socket.io-parser "~4.2.0" socket.io-parser "~4.2.0"
socket.io-parser@~4.2.0: socket.io-parser@~4.2.0:
version "4.2.2" version "4.2.3"
resolved "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz" resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.3.tgz#926bcc6658e2ae0883dc9dee69acbdc76e4e3667"
integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== integrity sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==
dependencies: dependencies:
"@socket.io/component-emitter" "~3.1.0" "@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1" debug "~4.3.1"

View File

@@ -20,8 +20,9 @@ import cron
from config import LOG_FOLDER, logger, WEBSERVER_PORT, DATA_FOLDER, SIST2_BINARY from config import LOG_FOLDER, logger, WEBSERVER_PORT, DATA_FOLDER, SIST2_BINARY
from jobs import Sist2Job, Sist2ScanTask, TaskQueue, Sist2IndexTask, JobStatus from jobs import Sist2Job, Sist2ScanTask, TaskQueue, Sist2IndexTask, JobStatus
from notifications import Subscribe, Notifications from notifications import Subscribe, Notifications
from sist2 import Sist2 from sist2 import Sist2, Sist2SearchBackend
from state import migrate_v1_to_v2, RUNNING_FRONTENDS, TESSERACT_LANGS, DB_SCHEMA_VERSION from state import migrate_v1_to_v2, RUNNING_FRONTENDS, TESSERACT_LANGS, DB_SCHEMA_VERSION, migrate_v3_to_v4, \
get_log_files_to_remove, delete_log_file, create_default_search_backends
from web import Sist2Frontend from web import Sist2Frontend
sist2 = Sist2(SIST2_BINARY, DATA_FOLDER) sist2 = Sist2(SIST2_BINARY, DATA_FOLDER)
@@ -80,9 +81,7 @@ async def get_jobs():
@app.put("/api/job/{name:str}") @app.put("/api/job/{name:str}")
async def update_job(name: str, new_job: Sist2Job): async def update_job(name: str, new_job: Sist2Job):
# TODO: Check etag new_job.last_modified = datetime.utcnow()
new_job.last_modified = datetime.now()
job = db["jobs"][name] job = db["jobs"][name]
if not job: if not job:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
@@ -134,8 +133,18 @@ async def kill_job(task_id: str):
return task_queue.kill_task(task_id) return task_queue.kill_task(task_id)
@app.post("/api/task/{task_id:str}/delete_logs")
async def delete_task_logs(task_id: str):
if not db["task_done"][task_id]:
raise HTTPException(status_code=404)
delete_log_file(db, task_id)
return "ok"
def _run_job(job: Sist2Job): def _run_job(job: Sist2Job):
job.last_modified = datetime.now() job.last_modified = datetime.utcnow()
if job.status == JobStatus("created"): if job.status == JobStatus("created"):
job.status = JobStatus("started") job.status = JobStatus("started")
db["jobs"][job.name] = job db["jobs"][job.name] = job
@@ -158,14 +167,29 @@ async def run_job(name: str):
return "ok" return "ok"
@app.get("/api/job/{name:str}/logs_to_delete")
async def task_history(n: int, name: str):
return get_log_files_to_remove(db, name, n)
@app.delete("/api/job/{name:str}") @app.delete("/api/job/{name:str}")
async def delete_job(name: str): async def delete_job(name: str):
job = db["jobs"][name] job: Sist2Job = db["jobs"][name]
if job: if not job:
del db["jobs"][name]
else:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
if any(name in frontend.jobs for frontend in db["frontends"]):
raise HTTPException(status_code=400, detail="in use (frontend)")
try:
os.remove(job.previous_index)
except:
pass
del db["jobs"][name]
return "ok"
@app.delete("/api/frontend/{name:str}") @app.delete("/api/frontend/{name:str}")
async def delete_frontend(name: str): async def delete_frontend(name: str):
@@ -251,9 +275,21 @@ def check_es_version(es_url: str, insecure: bool):
def start_frontend_(frontend: Sist2Frontend): def start_frontend_(frontend: Sist2Frontend):
frontend.web_options.indices = list(map(lambda j: db["jobs"][j].index_path, frontend.jobs)) frontend.web_options.indices = [
os.path.join(DATA_FOLDER, db["jobs"][j].index_path)
for j in frontend.jobs
]
pid = sist2.web(frontend.web_options, frontend.name) backend_name = frontend.web_options.search_backend
search_backend = db["search_backends"][backend_name]
if search_backend is None:
logger.error(
f"Error while running task: search backend not found: {backend_name}")
return -1
logger.debug(f"Fetched search backend options for {backend_name}")
pid = sist2.web(frontend.web_options, search_backend, frontend.name)
RUNNING_FRONTENDS[frontend.name] = pid RUNNING_FRONTENDS[frontend.name] = pid
@@ -283,6 +319,62 @@ async def get_frontends():
return res return res
@app.get("/api/search_backend/")
async def get_search_backends():
return list(db["search_backends"])
@app.put("/api/search_backend/{name:str}")
async def update_search_backend(name: str, backend: Sist2SearchBackend):
if not db["search_backends"][name]:
raise HTTPException(status_code=404)
db["search_backends"][name] = backend
return "ok"
@app.get("/api/search_backend/{name:str}")
def get_search_backend(name: str):
backend = db["search_backends"][name]
if not backend:
raise HTTPException(status_code=404)
return backend
@app.delete("/api/search_backend/{name:str}")
def delete_search_backend(name: str):
backend: Sist2SearchBackend = db["search_backends"][name]
if not backend:
raise HTTPException(status_code=404)
if any(frontend.web_options.search_backend == name for frontend in db["frontends"]):
raise HTTPException(status_code=400, detail="in use (frontend)")
if any(job.index_options.search_backend == name for job in db["jobs"]):
raise HTTPException(status_code=400, detail="in use (job)")
del db["search_backends"][name]
try:
os.remove(os.path.join(DATA_FOLDER, backend.search_index))
except:
pass
return "ok"
@app.post("/api/search_backend/{name:str}")
def create_search_backend(name: str):
if db["search_backends"][name] is not None:
return HTTPException(status_code=400, detail="already exists")
backend = Sist2SearchBackend.create_default(name)
db["search_backends"][name] = backend
return backend
def tail(filepath: str, n: int): def tail(filepath: str, n: int):
with open(filepath) as file: with open(filepath) as file:
@@ -321,7 +413,6 @@ async def ws_tail_log(websocket: WebSocket):
async with Subscribe(notifications) as ob: async with Subscribe(notifications) as ob:
async for notification in ob.notifications(): async for notification in ob.notifications():
await websocket.send_json(notification) await websocket.send_json(notification)
print(notification)
except ConnectionClosed: except ConnectionClosed:
return return
@@ -352,7 +443,7 @@ async def ws_tail_log(websocket: WebSocket, task_id: str, n: int):
def main(): def main():
uvicorn.run(app, port=WEBSERVER_PORT, host="0.0.0.0") uvicorn.run(app, port=WEBSERVER_PORT, host="0.0.0.0", timeout_graceful_shutdown=0)
def initialize_db(): def initialize_db():
@@ -361,6 +452,8 @@ def initialize_db():
frontend = Sist2Frontend.create_default("default") frontend = Sist2Frontend.create_default("default")
db["frontends"]["default"] = frontend db["frontends"]["default"] = frontend
create_default_search_backends(db)
logger.info("Initialized database.") logger.info("Initialized database.")
@@ -381,6 +474,12 @@ if __name__ == '__main__':
if db["sist2_admin"]["info"]["version"] == "2": if db["sist2_admin"]["info"]["version"] == "2":
logger.error("Cannot migrate database from v2 to v3. Delete state.db to proceed.") logger.error("Cannot migrate database from v2 to v3. Delete state.db to proceed.")
exit(-1) exit(-1)
if db["sist2_admin"]["info"]["version"] == "3":
logger.info("Migrating to v4 database schema")
migrate_v3_to_v4(db)
if db["sist2_admin"]["info"]["version"] != DB_SCHEMA_VERSION:
raise Exception(f"Incompatible database version for {db.dbfile}")
start_frontends() start_frontends()
cron.initialize(db, _run_job) cron.initialize(db, _run_job)

View File

@@ -10,7 +10,9 @@ from jobs import Sist2Job
def _check_schedule(db: PersistentState, run_job): def _check_schedule(db: PersistentState, run_job):
for job in db["jobs"]: jobs = list(db["jobs"])
for job in jobs:
job: Sist2Job job: Sist2Job
if job.schedule_enabled: if job.schedule_enabled:

View File

@@ -13,10 +13,10 @@ from uuid import uuid4, UUID
from hexlib.db import PersistentState from hexlib.db import PersistentState
from pydantic import BaseModel from pydantic import BaseModel
from config import logger, LOG_FOLDER from config import logger, LOG_FOLDER, DATA_FOLDER
from notifications import Notifications from notifications import Notifications
from sist2 import ScanOptions, IndexOptions, Sist2 from sist2 import ScanOptions, IndexOptions, Sist2
from state import RUNNING_FRONTENDS from state import RUNNING_FRONTENDS, get_log_files_to_remove, delete_log_file
from web import Sist2Frontend from web import Sist2Frontend
@@ -35,6 +35,8 @@ class Sist2Job(BaseModel):
cron_expression: str cron_expression: str
schedule_enabled: bool = False schedule_enabled: bool = False
keep_last_n_logs: int = -1
previous_index: str = None previous_index: str = None
index_path: str = None index_path: str = None
previous_index_path: str = None previous_index_path: str = None
@@ -53,15 +55,10 @@ class Sist2Job(BaseModel):
name=name, name=name,
scan_options=ScanOptions(path="/"), scan_options=ScanOptions(path="/"),
index_options=IndexOptions(), index_options=IndexOptions(),
last_modified=datetime.now(), last_modified=datetime.utcnow(),
cron_expression="0 0 * * *" 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: class Sist2TaskProgress:
@@ -111,7 +108,7 @@ class Sist2Task:
self._logger.info(json.dumps(log_json)) self._logger.info(json.dumps(log_json))
def run(self, sist2: Sist2, db: PersistentState): def run(self, sist2: Sist2, db: PersistentState):
self.started = datetime.now() self.started = datetime.utcnow()
logger.info(f"Started task {self.display_name}") logger.info(f"Started task {self.display_name}")
@@ -132,14 +129,16 @@ class Sist2ScanTask(Sist2Task):
self.pid = pid self.pid = pid
return_code = sist2.scan(self.job.scan_options, logs_cb=self.log_callback, set_pid_cb=set_pid) return_code = sist2.scan(self.job.scan_options, logs_cb=self.log_callback, set_pid_cb=set_pid)
self.ended = datetime.now() self.ended = datetime.utcnow()
if return_code != 0: is_ok = return_code in (0, 1)
if not is_ok:
self._logger.error(json.dumps({"sist2-admin": f"Process returned non-zero exit code ({return_code})"})) 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})") logger.info(f"Task {self.display_name} failed ({return_code})")
else: else:
self.job.index_path = self.job.scan_options.output self.job.index_path = self.job.scan_options.output
self.job.last_index_date = datetime.now() self.job.last_index_date = datetime.utcnow()
self.job.do_full_scan = False self.job.do_full_scan = False
db["jobs"][self.job.name] = self.job db["jobs"][self.job.name] = self.job
self._logger.info(json.dumps({"sist2-admin": f"Save last_index_date={self.job.last_index_date}"})) self._logger.info(json.dumps({"sist2-admin": f"Save last_index_date={self.job.last_index_date}"}))
@@ -147,7 +146,7 @@ class Sist2ScanTask(Sist2Task):
logger.info(f"Completed {self.display_name} ({return_code=})") logger.info(f"Completed {self.display_name} ({return_code=})")
# Remove old index # Remove old index
if return_code == 0: if is_ok:
if self.job.previous_index_path is not None and self.job.previous_index_path != self.job.index_path: if self.job.previous_index_path is not None and self.job.previous_index_path != self.job.index_path:
self._logger.info(json.dumps({"sist2-admin": f"Remove {self.job.previous_index_path=}"})) self._logger.info(json.dumps({"sist2-admin": f"Remove {self.job.previous_index_path=}"}))
try: try:
@@ -171,8 +170,15 @@ class Sist2IndexTask(Sist2Task):
self.job.index_options.path = self.job.scan_options.output self.job.index_options.path = self.job.scan_options.output
return_code = sist2.index(self.job.index_options, logs_cb=self.log_callback) search_backend = db["search_backends"][self.job.index_options.search_backend]
self.ended = datetime.now() if search_backend is None:
logger.error(f"Error while running task: search backend not found: {self.job.index_options.search_backend}")
return -1
logger.debug(f"Fetched search backend options for {self.job.index_options.search_backend}")
return_code = sist2.index(self.job.index_options, search_backend, logs_cb=self.log_callback)
self.ended = datetime.utcnow()
duration = self.ended - self.started duration = self.ended - self.started
@@ -206,9 +212,20 @@ class Sist2IndexTask(Sist2Task):
except ChildProcessError: except ChildProcessError:
pass pass
frontend.web_options.indices = map(lambda j: db["jobs"][j].index_path, frontend.jobs) backend_name = frontend.web_options.search_backend
search_backend = db["search_backends"][backend_name]
if search_backend is None:
logger.error(f"Error while running task: search backend not found: {backend_name}")
return -1
pid = sist2.web(frontend.web_options, frontend.name) logger.debug(f"Fetched search backend options for {backend_name}")
frontend.web_options.indices = [
os.path.join(DATA_FOLDER, db["jobs"][j].index_path)
for j in frontend.jobs
]
pid = sist2.web(frontend.web_options, search_backend, frontend.name)
RUNNING_FRONTENDS[frontend_name] = pid RUNNING_FRONTENDS[frontend_name] = pid
self._logger.info(json.dumps({"sist2-admin": f"Restart frontend {pid=} {frontend_name=}"})) self._logger.info(json.dumps({"sist2-admin": f"Restart frontend {pid=} {frontend_name=}"}))
@@ -232,7 +249,7 @@ class TaskQueue:
def _tasks_failed(self): def _tasks_failed(self):
done = set() done = set()
for row in self._db["task_done"].sql("WHERE return_code != 0"): for row in self._db["task_done"].sql("WHERE return_code NOT IN (0,1)"):
done.add(uuid.UUID(row["id"])) done.add(uuid.UUID(row["id"]))
return done return done
@@ -301,8 +318,14 @@ class TaskQueue:
"ended": task.ended, "ended": task.ended,
"started": task.started, "started": task.started,
"name": task.display_name, "name": task.display_name,
"return_code": task_result "return_code": task_result,
"has_logs": 1
} }
logs_to_delete = get_log_files_to_remove(self._db, task.job.name, task.job.keep_last_n_logs)
for row in logs_to_delete:
delete_log_file(self._db, row["id"])
if isinstance(task, Sist2IndexTask): if isinstance(task, Sist2IndexTask):
self._notifications.notify({ self._notifications.notify({
"message": "notifications.indexCompleted", "message": "notifications.indexCompleted",

View File

@@ -3,6 +3,7 @@ import json
import logging import logging
import os.path import os.path
from datetime import datetime from datetime import datetime
from enum import Enum
from io import TextIOWrapper from io import TextIOWrapper
from logging import FileHandler from logging import FileHandler
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
@@ -12,7 +13,7 @@ from typing import List
from pydantic import BaseModel from pydantic import BaseModel
from config import logger, LOG_FOLDER from config import logger, LOG_FOLDER, DATA_FOLDER
class Sist2Version: class Sist2Version:
@@ -25,77 +26,60 @@ class Sist2Version:
return f"{self.major}.{self.minor}.{self.patch}" return f"{self.major}.{self.minor}.{self.patch}"
class WebOptions(BaseModel): class SearchBackendType(Enum):
indices: List[str] = [] SQLITE = "sqlite"
ELASTICSEARCH = "elasticsearch"
class Sist2SearchBackend(BaseModel):
backend_type: SearchBackendType = SearchBackendType("elasticsearch")
name: str
search_index: str = ""
es_url: str = "http://elasticsearch:9200" es_url: str = "http://elasticsearch:9200"
es_insecure_ssl: bool = False es_insecure_ssl: bool = False
es_index: str = "sist2" 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"--es-index={self.es_index}", 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 threads: int = 1
es_url: str = "http://elasticsearch:9200"
es_insecure_ssl: bool = False
es_index: str = "sist2"
incremental_index: bool = True
script: str = "" script: str = ""
script_file: str = None script_file: str = None
batch_size: int = 70 batch_size: int = 70
@staticmethod
def create_default(name: str, backend_type: SearchBackendType = SearchBackendType("elasticsearch")):
return Sist2SearchBackend(
name=name,
search_index=f"search-index-{name.replace('/', '_')}.sist2",
backend_type=backend_type
)
class IndexOptions(BaseModel):
path: str = None
incremental_index: bool = True
search_backend: str = None
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
def args(self): def args(self, search_backend):
absolute_path = os.path.join(DATA_FOLDER, self.path)
args = ["index", self.path, f"--threads={self.threads}", f"--es-url={self.es_url}", if search_backend.backend_type == SearchBackendType("sqlite"):
f"--es-index={self.es_index}", f"--batch-size={self.batch_size}"] search_index_absolute = os.path.join(DATA_FOLDER, search_backend.search_index)
args = ["sqlite-index", absolute_path, "--search-index", search_index_absolute]
else:
args = ["index", absolute_path, f"--threads={search_backend.threads}",
f"--es-url={search_backend.es_url}",
f"--es-index={search_backend.es_index}",
f"--batch-size={search_backend.batch_size}"]
if self.script_file: if search_backend.script_file:
args.append(f"--script-file={self.script_file}") args.append(f"--script-file={search_backend.script_file}")
if self.es_insecure_ssl: if search_backend.es_insecure_ssl:
args.append(f"--es-insecure-ssl") args.append(f"--es-insecure-ssl")
if self.incremental_index: if self.incremental_index:
args.append(f"--incremental-index") args.append(f"--incremental-index")
return args return args
@@ -109,7 +93,7 @@ ARCHIVE_RECURSE = "recurse"
class ScanOptions(BaseModel): class ScanOptions(BaseModel):
path: str path: str
threads: int = 1 threads: int = 1
thumbnail_quality: int = 2 thumbnail_quality: int = 50
thumbnail_size: int = 552 thumbnail_size: int = 552
thumbnail_count: int = 1 thumbnail_count: int = 1
content_size: int = 32768 content_size: int = 32768
@@ -137,9 +121,12 @@ class ScanOptions(BaseModel):
super().__init__(**kwargs) super().__init__(**kwargs)
def args(self): def args(self):
output_path = os.path.join(DATA_FOLDER, self.output)
args = ["scan", self.path, f"--threads={self.threads}", f"--thumbnail-quality={self.thumbnail_quality}", args = ["scan", self.path, f"--threads={self.threads}", f"--thumbnail-quality={self.thumbnail_quality}",
f"--thumbnail-count={self.thumbnail_count}", f"--thumbnail-size={self.thumbnail_size}", f"--thumbnail-count={self.thumbnail_count}", f"--thumbnail-size={self.thumbnail_size}",
f"--content-size={self.content_size}", f"--output={self.output}", f"--depth={self.depth}", f"--content-size={self.content_size}", f"--output={output_path}", f"--depth={self.depth}",
f"--archive={self.archive}", f"--mem-buffer={self.mem_buffer}"] f"--archive={self.archive}", f"--mem-buffer={self.mem_buffer}"]
if self.incremental: if self.incremental:
@@ -200,28 +187,80 @@ class Sist2Index:
def name(self) -> str: def name(self) -> str:
return self._descriptor["name"] return self._descriptor["name"]
class WebOptions(BaseModel):
indices: List[str] = []
search_backend: str = "elasticsearch"
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, search_backend: Sist2SearchBackend):
args = ["web", f"--bind={self.bind}", f"--tagline={self.tagline}",
f"--lang={self.lang}"]
if search_backend.backend_type == SearchBackendType("sqlite"):
args.append(f"--search-index={search_backend.search_index}")
else:
args.append(f"--es-url={search_backend.es_url}")
args.append(f"--es-index={search_backend.es_index}")
if search_backend.es_insecure_ssl:
args.append(f"--es-insecure-ssl")
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.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 Sist2: class Sist2:
def __init__(self, bin_path: str, data_directory: str): def __init__(self, bin_path: str, data_directory: str):
self._bin_path = bin_path self.bin_path = bin_path
self._data_dir = data_directory self._data_dir = data_directory
def index(self, options: IndexOptions, logs_cb): def index(self, options: IndexOptions, search_backend: Sist2SearchBackend, logs_cb):
if options.script: if search_backend.script and search_backend.backend_type == SearchBackendType("elasticsearch"):
with NamedTemporaryFile("w", prefix="sist2-admin", suffix=".painless", delete=False) as f: with NamedTemporaryFile("w", prefix="sist2-admin", suffix=".painless", delete=False) as f:
f.write(options.script) f.write(search_backend.script)
options.script_file = f.name search_backend.script_file = f.name
else: else:
options.script_file = None search_backend.script_file = None
args = [ args = [
self._bin_path, self.bin_path,
*options.args(), *options.args(search_backend),
"--json-logs", "--json-logs",
"--very-verbose" "--very-verbose"
] ]
logs_cb({"sist2-admin": f"Starting sist2 command with args {args}"})
proc = Popen(args, stdout=PIPE, stderr=PIPE) proc = Popen(args, stdout=PIPE, stderr=PIPE)
t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, proc)) t_stderr = Thread(target=self._consume_logs_stderr, args=(logs_cb, proc))
@@ -236,13 +275,10 @@ class Sist2:
def scan(self, options: ScanOptions, logs_cb, set_pid_cb): def scan(self, options: ScanOptions, logs_cb, set_pid_cb):
if options.output is None: if options.output is None:
options.output = os.path.join( options.output = f"scan-{options.name.replace('/', '_')}-{datetime.utcnow()}.sist2"
self._data_dir,
f"scan-{options.name.replace('/', '_')}-{datetime.now()}.sist2"
)
args = [ args = [
self._bin_path, self.bin_path,
*options.args(), *options.args(),
"--json-logs", "--json-logs",
"--very-verbose" "--very-verbose"
@@ -290,7 +326,7 @@ class Sist2:
except NameError: except NameError:
pass pass
def web(self, options: WebOptions, name: str): def web(self, options: WebOptions, search_backend: Sist2SearchBackend, name: str):
if options.auth0_public_key: if options.auth0_public_key:
with NamedTemporaryFile("w", prefix="sist2-admin", suffix=".txt", delete=False) as f: with NamedTemporaryFile("w", prefix="sist2-admin", suffix=".txt", delete=False) as f:
@@ -300,8 +336,8 @@ class Sist2:
options.auth0_public_key_file = None options.auth0_public_key_file = None
args = [ args = [
self._bin_path, self.bin_path,
*options.args() *options.args(search_backend)
] ]
web_logger = logging.Logger(name=f"sist2-frontend-{name}") web_logger = logging.Logger(name=f"sist2-frontend-{name}")
@@ -321,3 +357,5 @@ class Sist2:
t_stdout.start() t_stdout.start()
return proc.pid return proc.pid

View File

@@ -1,16 +1,20 @@
from typing import Dict from typing import Dict
import os
import shutil import shutil
from hexlib.db import Table, PersistentState from hexlib.db import Table, PersistentState
import pickle import pickle
from tesseract import get_tesseract_langs from tesseract import get_tesseract_langs
import sqlite3
from config import LOG_FOLDER, logger
from sist2 import SearchBackendType, Sist2SearchBackend
RUNNING_FRONTENDS: Dict[str, int] = {} RUNNING_FRONTENDS: Dict[str, int] = {}
TESSERACT_LANGS = get_tesseract_langs() TESSERACT_LANGS = get_tesseract_langs()
DB_SCHEMA_VERSION = "3" DB_SCHEMA_VERSION = "4"
from pydantic import BaseModel from pydantic import BaseModel
@@ -50,8 +54,35 @@ class PickleTable(Table):
yield dict((k, _deserialize(v)) for k, v in row.items()) yield dict((k, _deserialize(v)) for k, v in row.items())
def migrate_v1_to_v2(db: PersistentState): def get_log_files_to_remove(db: PersistentState, job_name: str, n: int):
if n < 0:
return []
counter = 0
to_remove = []
for row in db["task_done"].sql("WHERE has_logs=1 ORDER BY started DESC"):
if row["name"].endswith(f"[{job_name}]"):
counter += 1
if counter > n:
to_remove.append(row)
return to_remove
def delete_log_file(db: PersistentState, task_id: str):
db["task_done"][task_id] = {
"has_logs": 0
}
try:
os.remove(os.path.join(LOG_FOLDER, f"sist2-{task_id}.log"))
except:
pass
def migrate_v1_to_v2(db: PersistentState):
shutil.copy(db.dbfile, db.dbfile + "-before-migrate-v2.bak") shutil.copy(db.dbfile, db.dbfile + "-before-migrate-v2.bak")
# Frontends # Frontends
@@ -77,3 +108,29 @@ def migrate_v1_to_v2(db: PersistentState):
db["sist2_admin"]["info"] = { db["sist2_admin"]["info"] = {
"version": "2" "version": "2"
} }
def create_default_search_backends(db: PersistentState):
es_backend = Sist2SearchBackend.create_default(name="elasticsearch",
backend_type=SearchBackendType("elasticsearch"))
db["search_backends"]["elasticsearch"] = es_backend
sqlite_backend = Sist2SearchBackend.create_default(name="sqlite", backend_type=SearchBackendType("sqlite"))
db["search_backends"]["sqlite"] = sqlite_backend
def migrate_v3_to_v4(db: PersistentState):
shutil.copy(db.dbfile, db.dbfile + "-before-migrate-v4.bak")
create_default_search_backends(db)
try:
conn = sqlite3.connect(db.dbfile)
conn.execute("ALTER TABLE task_done ADD COLUMN has_logs INTEGER DEFAULT 1")
conn.commit()
conn.close()
except Exception as e:
logger.exception(e)
db["sist2_admin"]["info"] = {
"version": "4"
}

View File

@@ -19,12 +19,6 @@
We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue. Please enable it to continue.
</strong> </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> </div>
</noscript> </noscript>
<div id="app"></div> <div id="app"></div>

View File

@@ -10,7 +10,7 @@
<b-spinner type="grow" variant="primary"></b-spinner> <b-spinner type="grow" variant="primary"></b-spinner>
</div> </div>
<div class="loading-text"> <div class="loading-text">
Loading Chargement 装载 Wird geladen Loading Chargement 装载 Wird geladen Ładowanie
</div> </div>
</div> </div>
</template> </template>

View File

@@ -170,14 +170,16 @@ class Sist2ElasticsearchQuery {
} }
}, },
sort: SORT_MODES[getters.sortMode].mode, sort: SORT_MODES[getters.sortMode].mode,
aggs:
{
total_size: {"sum": {"field": "size"}},
total_count: {"value_count": {"field": "size"}}
},
size: size, size: size,
} as any; } as any;
if (!after) {
q.aggs = {
total_size: {"sum": {"field": "size"}},
total_count: {"value_count": {"field": "size"}}
};
}
if (!empty && !blankSearch) { if (!empty && !blankSearch) {
q.query.bool.must = query; q.query.bool.must = query;
} }

View File

@@ -1,7 +1,7 @@
export default { export default {
en: { en: {
filePage: { filePage: {
notFound: "Not found" notFound: "Not found"
}, },
searchBar: { searchBar: {
simple: "Search", simple: "Search",
@@ -92,6 +92,7 @@ export default {
en: "English", en: "English",
de: "Deutsch", de: "Deutsch",
fr: "Français", fr: "Français",
pl: "Polski",
"zh-CN": "简体中文", "zh-CN": "简体中文",
}, },
displayMode: { displayMode: {
@@ -184,7 +185,7 @@ export default {
}, },
de: { de: {
filePage: { filePage: {
notFound: "Nicht gefunden" notFound: "Nicht gefunden"
}, },
searchBar: { searchBar: {
simple: "Suche", simple: "Suche",
@@ -271,6 +272,7 @@ export default {
en: "English", en: "English",
de: "Deutsch", de: "Deutsch",
fr: "Français", fr: "Français",
pl: "Polski",
"zh-CN": "简体中文", "zh-CN": "简体中文",
}, },
displayMode: { displayMode: {
@@ -445,6 +447,7 @@ export default {
en: "English", en: "English",
de: "Deutsch", de: "Deutsch",
fr: "Français", fr: "Français",
pl: "Polski",
"zh-CN": "简体中文", "zh-CN": "简体中文",
}, },
displayMode: { displayMode: {
@@ -619,6 +622,7 @@ export default {
en: "English", en: "English",
de: "Deutsch", de: "Deutsch",
fr: "Français", fr: "Français",
pl: "Polski",
"zh-CN": "简体中文", "zh-CN": "简体中文",
}, },
displayMode: { displayMode: {
@@ -703,4 +707,188 @@ export default {
selectedIndices: "选中索引", selectedIndices: "选中索引",
}, },
}, },
pl: {
filePage: {
notFound: "Nie znaleziono"
},
searchBar: {
simple: "Szukaj",
advanced: "Zaawansowane szukanie",
fuzzy: "Również podobne"
},
addTag: "Tag",
deleteTag: "Usuń",
download: "Pobierz",
and: "i",
page: "strona",
pages: "stron",
mimeTypes: "Typy danych",
tags: "Tagi",
tagFilter: "Filtruj tagi",
forExample: "Na przykład:",
help: {
simpleSearch: "Proste szukanie",
advancedSearch: "Zaawansowane szukanie",
help: "Pomoc",
term: "<WYRAZ>",
and: "operator I",
or: "operator LUB",
not: "zabrania danego wyrazu",
quotes: "znajdzie objętą sekwencję wyrazów w podanej kolejności",
prefix: "znajdzie dowolny wyraz rozpoczynający się na takie litery, jeśli zastosowane na końcu wyrazu",
parens: "używane do grupowania wyrażeń",
tildeTerm: "znajdzie wyraz w podanej odległości",
tildePhrase: "znajdzie frazę przeplecioną podaną liczbą niepasujących wyrazów",
example1:
"Na przykład: <code>\"pieczone jajko\" +(kiełbasa | ziemniak) -frytki</code> znajdzie frazę " +
"<i>pieczone jajko</i> gdzie występuje też: <i>kiełbasa</i> albo <i>ziemniak</i>, ale zignoruje rezultat " +
"zawierający <i>frytki</i>.",
defaultOperator:
"Kiedy nie podano ani <code>+</code>, ani <code>|</code>, to domyślnym operatorem jest " +
"<code>+</code> (i).",
fuzzy:
"Kiedy opcja <b>Również podobne</b> jest zaznaczona, częściowo zgodne wyrazy są również znajdywane.",
moreInfoSimple: "Po więcej informacji sięgnij do <a target=\"_blank\" " +
"rel=\"noreferrer\" href=\"//www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html\">dokumentacji Elasticsearch</a>",
moreInfoAdvanced: "Aby uzyskać więcej informacji o zaawansowanym szukaniu, przeczytaj <a target=\"_blank\" rel=\"noreferrer\" href=\"//www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax\">dokumentację Elasticsearch</a>"
},
config: "Ustawienia",
configDescription: "Ustawienia są zapisywane na żywo w tej przeglądarce.",
configReset: "Zresetuj ustawienia",
searchOptions: "Opcje szukania",
treemapOptions: "Opcje mapy",
mlOptions: "Opcje uczenia maszynowego",
displayOptions: "Opcje wyświetlania",
opt: {
lang: "Język",
highlight: "Zaznaczaj znalezione fragmenty",
fuzzy: "Ustaw szukanie również podobnych jako domyślne",
searchInPath: "Włącz szukanie również w ścieżce dokumentu",
suggestPath: "Włącz auto-uzupełnianie w filtrze ścieżek",
fragmentSize: "Podświetl wielkość kontekstu w znakach",
queryMode: "Tryb szukania",
displayMode: "Wyświetlanie",
columns: "Liczba kolumn",
treemapType: "Typ mapy",
treemapTiling: "Układanie mapy",
treemapColorGroupingDepth: "Jak głęboko grupować kolory mapy (na płasko)",
treemapColor: "Kolor mapy (kaskadowo)",
treemapSize: "Wielkość mapy",
theme: "Styl graficzny",
lightboxLoadOnlyCurrent: "Nie pobieraj od razu obrazów w pełnej wielkości dla sąsiednich obrazów podglądu.",
slideDuration: "Czas trwania jednego slajdu w pokazie slajdów",
resultSize: "Liczba wyników na stronę",
tagOrOperator: "Użyj operatora LUB przy wyborze kilku tagów",
hideDuplicates: "Ukryj zduplikowane wyniki (według sumy kontrolnej)",
hideLegacy: "Ukryj powiadomienie Elasticsearch 'legacyES'",
updateMimeMap: "Uaktualniaj drzewo typów mediów na żywo",
useDatePicker: "Używaj kalendarza do wyboru dat, zamiast suwaka",
vidPreviewInterval: "Czas trwania jednej klatki w podglądzie wideo (w ms)",
simpleLightbox: "Wyłącz animacje w podglądzie obrazów",
showTagPickerFilter: "Pokazuj pole filtrowania tagów",
featuredFields: "Wybrane pola szablonu Javascript. Będą pojawiać się przy wynikach wyszukiwania.",
featuredFieldsList: "Dostępne zmienne",
autoAnalyze: "Automatycznie analizuj tekst",
defaultModel: "Domyślny model",
mlRepositories: "Repozytoria modeli (każde w osobnej linii)"
},
queryMode: {
simple: "Proste",
advanced: "Zaawansowane",
},
lang: {
en: "English",
de: "Deutsch",
fr: "Français",
pl: "Polski",
"zh-CN": "简体中文",
},
displayMode: {
grid: "Siatka",
list: "Lista",
},
columns: {
auto: "Automatyczna"
},
treemapType: {
cascaded: "Kaskadowa",
flat: "Płaska (kompaktowa)"
},
treemapSize: {
small: "Mała",
medium: "Średnia",
large: "Duża",
xLarge: "Bardzo duża",
xxLarge: "Ogromna",
custom: "Inna",
},
treemapTiling: {
binary: "Binarnie",
squarify: "Kwadratowo",
slice: "Wycinek",
dice: "Kostka",
sliceDice: "Wycinek i kostka",
},
theme: {
light: "Jasny",
black: "Czarny"
},
hit: "traf",
hits: "trafień",
details: "Szczegóły",
stats: "Statystyki",
queryTime: "Czas szukania",
totalSize: "Całkowita wielkość",
pathBar: {
placeholder: "Filtruj ścieżki",
modalTitle: "Wybierz ścieżkę"
},
debug: "Informacje dla programistów",
debugDescription: "Informacje przydatne do znajdowania błędów w oprogramowaniu. Jeśli napotkasz błąd lub masz" +
" propozycje zmian, zgłoś to proszę <a href='https://github.com/simon987/sist2/issues/new/choose'>tutaj</a>.",
tagline: "Slogan",
toast: {
esConnErrTitle: "Problem z połączeniem z Elasticsearch",
esConnErr: "Moduł strony internetowej sist2 napotkał problem przy połączeniu z Elasticsearch." +
" Zobacz logi serwera, aby uzyskać więcej informacji.",
esQueryErrTitle: "Problem z kwerendą",
esQueryErr: "Kwerenda szukania jest niezrozumiała albo nie udało się jej przesłać. Sprawdź dokumentację zaawansowanego szukania. " +
"Zobacz logi serwera, aby uzyskać więcej informacji.",
dupeTagTitle: "Zduplikowany tag",
dupeTag: "Ten dokument już ma taki tag.",
copiedToClipboard: "Skopiowano do schowka"
},
saveTagModalTitle: "Dodaj tag",
saveTagPlaceholder: "Nazwa",
confirm: "Zatwierdź",
indexPickerPlaceholder: "Wybierz indeks",
sort: {
relevance: "Zgodność z szukanym",
dateAsc: "Data (najpierw starsze)",
dateDesc: "Data (najpierw nowsze)",
sizeAsc: "Wielkość (najpierw mniejsze)",
sizeDesc: "Wielkość (najpierw większe)",
nameAsc: "Nazwa (A-z)",
nameDesc: "Nazwa (Z-a)",
random: "Losowo",
},
d3: {
mimeCount: "Dystrybucja liczby plików według typów mediów",
mimeSize: "Dystrybucja wielkości plików według typów mediów",
dateHistogram: "Dystrybucja dat modyfikacji plików",
sizeHistogram: "Dystrybucja wielkości plików",
},
indexPicker: {
selectNone: "Zaznacz nic",
selectAll: "Zaznacz wszystko",
selectedIndex: "wybrany indeks",
selectedIndices: "wybrane indeksy",
},
ml: {
analyzeText: "Analizuj",
auto: "Automatycznie",
repoFetchError: "Nie udało się uzyskać listy modeli. Zobacz konsolę przeglądarki, aby uzyskać więcej informacji.",
repoFetchErrorTitle: "Nie udało się pobrać repozytoriów modeli",
}
}
} }

View File

@@ -237,6 +237,7 @@ export default {
{value: "fr", text: this.$t("lang.fr")}, {value: "fr", text: this.$t("lang.fr")},
{value: "zh-CN", text: this.$t("lang.zh-CN")}, {value: "zh-CN", text: this.$t("lang.zh-CN")},
{value: "de", text: this.$t("lang.de")}, {value: "de", text: this.$t("lang.de")},
{value: "pl", text: this.$t("lang.pl")},
], ],
queryModeOptions: [ queryModeOptions: [
{value: "simple", text: this.$t("queryMode.simple")}, {value: "simple", text: this.$t("queryMode.simple")},

View File

@@ -5,7 +5,7 @@
#define DEFAULT_OUTPUT "index.sist2" #define DEFAULT_OUTPUT "index.sist2"
#define DEFAULT_NAME "index" #define DEFAULT_NAME "index"
#define DEFAULT_CONTENT_SIZE 32768 #define DEFAULT_CONTENT_SIZE 32768
#define DEFAULT_QUALITY 2 #define DEFAULT_QUALITY 50
#define DEFAULT_THUMBNAIL_SIZE 552 #define DEFAULT_THUMBNAIL_SIZE 552
#define DEFAULT_THUMBNAIL_COUNT 1 #define DEFAULT_THUMBNAIL_COUNT 1
#define DEFAULT_REWRITE_URL "" #define DEFAULT_REWRITE_URL ""
@@ -93,15 +93,18 @@ int scan_args_validate(scan_args_t *args, int argc, const char **argv) {
if (abs_path == NULL) { if (abs_path == NULL) {
LOG_FATALF("cli.c", "Invalid PATH argument. File not found: %s", argv[1]); LOG_FATALF("cli.c", "Invalid PATH argument. File not found: %s", argv[1]);
} else { } else {
abs_path = realloc(abs_path, strlen(abs_path) + 2); char *new_abs_path = realloc(abs_path, strlen(abs_path) + 2);
strcat(abs_path, "/"); if (new_abs_path == NULL) {
args->path = abs_path; LOG_FATALF("cli.c", "FIXME: realloc() failed for argv[1]=%s, abs_path=%s", argv[1], abs_path);
}
strcat(new_abs_path, "/");
args->path = new_abs_path;
} }
if (args->tn_quality == OPTION_VALUE_UNSPECIFIED) { if (args->tn_quality == OPTION_VALUE_UNSPECIFIED) {
args->tn_quality = DEFAULT_QUALITY; args->tn_quality = DEFAULT_QUALITY;
} else if (args->tn_quality < 2 || args->tn_quality > 31) { } else if (args->tn_quality < 0 || args->tn_quality > 100) {
fprintf(stderr, "Invalid value for --thumbnail-quality argument: %d. Must be within [2, 31].\n", fprintf(stderr, "Invalid value for --thumbnail-quality argument: %d. Must be within [0, 100].\n",
args->tn_quality); args->tn_quality);
return 1; return 1;
} }
@@ -109,7 +112,7 @@ int scan_args_validate(scan_args_t *args, int argc, const char **argv) {
if (args->tn_size == OPTION_VALUE_UNSPECIFIED) { if (args->tn_size == OPTION_VALUE_UNSPECIFIED) {
args->tn_size = DEFAULT_THUMBNAIL_SIZE; args->tn_size = DEFAULT_THUMBNAIL_SIZE;
} else if (args->tn_size < 32) { } else if (args->tn_size < 32) {
printf("Invalid value --thumbnail-size argument: %d. Must be greater than 32 pixels.\n", args->tn_size); printf("Invalid value --thumbnail-size argument: %d. Must be >= 32 pixels.\n", args->tn_size);
return 1; return 1;
} }
@@ -142,10 +145,14 @@ int scan_args_validate(scan_args_t *args, int argc, const char **argv) {
char *abs_output = abspath(args->output); char *abs_output = abspath(args->output);
if (args->incremental && abs_output == NULL) { if (args->incremental && abs_output == NULL) {
LOG_WARNINGF("main.c", "Could not open original index for incremental scan: %s. Will not perform incremental scan.", args->output); LOG_WARNINGF("main.c",
"Could not open original index for incremental scan: %s. Will not perform incremental scan.",
args->output);
args->incremental = FALSE; args->incremental = FALSE;
} else if (!args->incremental && abs_output != NULL) { } else if (!args->incremental && abs_output != NULL) {
LOG_FATALF("main.c", "Index already exists: %s. If you wish to perform incremental scan, you must specify --incremental", abs_output); LOG_FATALF("main.c",
"Index already exists: %s. If you wish to perform incremental scan, you must specify --incremental",
abs_output);
} }
free(abs_output); free(abs_output);

View File

@@ -466,8 +466,8 @@ cJSON *database_fts_search(database_t *db, const char *query, const char *path,
const char *path_where = path_where_clause(path); const char *path_where = path_where_clause(path);
const char *size_where = size_where_clause(size_min, size_max); const char *size_where = size_where_clause(size_min, size_max);
const char *date_where = date_where_clause(date_min, date_max); const char *date_where = date_where_clause(date_min, date_max);
const char *index_id_where = index_ids_where_clause(index_ids); char *index_id_where = index_ids_where_clause(index_ids);
const char *mime_where = mime_types_where_clause(mime_types); char *mime_where = mime_types_where_clause(mime_types);
const char *query_where = match_where(query); const char *query_where = match_where(query);
const char *after_where = get_after_where(after, sort); const char *after_where = get_after_where(after, sort);
const char *tags_where = tags_where_clause(tags); const char *tags_where = tags_where_clause(tags);

View File

@@ -3,6 +3,7 @@
#include <signal.h> #include <signal.h>
#define LOG_MAX_LENGTH 8192 #define LOG_MAX_LENGTH 8192
#define LOG_SIST_DEBUG 0 #define LOG_SIST_DEBUG 0
@@ -33,11 +34,12 @@
#define LOG_FATALF(filepath, fmt, ...)\ #define LOG_FATALF(filepath, fmt, ...)\
sist_logf(filepath, LOG_SIST_FATAL, fmt, __VA_ARGS__);\ sist_logf(filepath, LOG_SIST_FATAL, fmt, __VA_ARGS__);\
raise(SIGUSR1) raise(SIGUSR1); \
exit(-1)
#define LOG_FATAL(filepath, str) \ #define LOG_FATAL(filepath, str) \
sist_log(filepath, LOG_SIST_FATAL, str);\ sist_log(filepath, LOG_SIST_FATAL, str);\
exit(SIGUSR1) raise(SIGUSR1); \
exit(-1)
#define LOG_FATALF_NO_EXIT(filepath, fmt, ...) \ #define LOG_FATALF_NO_EXIT(filepath, fmt, ...) \
sist_logf(filepath, LOG_SIST_FATAL, fmt, __VA_ARGS__) sist_logf(filepath, LOG_SIST_FATAL, fmt, __VA_ARGS__)
#define LOG_FATAL_NO_EXIT(filepath, str) \ #define LOG_FATAL_NO_EXIT(filepath, str) \
@@ -46,6 +48,7 @@
#include "sist.h" #include "sist.h"
void sist_logf(const char *filepath, int level, char *format, ...); void sist_logf(const char *filepath, int level, char *format, ...);
void vsist_logf(const char *filepath, int level, char *format, va_list ap); void vsist_logf(const char *filepath, int level, char *format, va_list ap);
void sist_log(const char *filepath, int level, char *str); void sist_log(const char *filepath, int level, char *str);

View File

@@ -490,7 +490,7 @@ int main(int argc, const char *argv[]) {
OPT_GROUP("Scan options"), OPT_GROUP("Scan options"),
OPT_INTEGER('t', "threads", &common_threads, "Number of threads. DEFAULT: 1"), OPT_INTEGER('t', "threads", &common_threads, "Number of threads. DEFAULT: 1"),
OPT_INTEGER('q', "thumbnail-quality", &scan_args->tn_quality, OPT_INTEGER('q', "thumbnail-quality", &scan_args->tn_quality,
"Thumbnail quality, on a scale of 2 to 31, 2 being the best. DEFAULT: 2", "Thumbnail quality, on a scale of 0 to 100, 100 being the best. DEFAULT: 50",
set_to_negative_if_value_is_zero, (intptr_t) &scan_args->tn_quality), set_to_negative_if_value_is_zero, (intptr_t) &scan_args->tn_quality),
OPT_INTEGER(0, "thumbnail-size", &scan_args->tn_size, OPT_INTEGER(0, "thumbnail-size", &scan_args->tn_size,
"Thumbnail size, in pixels. DEFAULT: 552", "Thumbnail size, in pixels. DEFAULT: 552",

View File

@@ -51,11 +51,11 @@
#include <ctype.h> #include <ctype.h>
#include "git_hash.h" #include "git_hash.h"
#define VERSION "3.0.7" #define VERSION "3.1.2"
static const char *const Version = VERSION; static const char *const Version = VERSION;
static const int VersionMajor = 3; static const int VersionMajor = 3;
static const int VersionMinor = 0; static const int VersionMinor = 1;
static const int VersionPatch = 7; static const int VersionPatch = 2;
#ifndef SIST_PLATFORM #ifndef SIST_PLATFORM
#define SIST_PLATFORM unknown #define SIST_PLATFORM unknown

View File

@@ -170,7 +170,7 @@ fts_search_req_t *get_search_req(struct mg_http_message *hm) {
cJSON_Delete(json); cJSON_Delete(json);
return NULL; return NULL;
} }
if (req_path.val && (strstr(req_path.val->valuestring, "*") || strlen(req_path.val) >= PATH_MAX)) { if (req_path.val && (strstr(req_path.val->valuestring, "*") || strlen(req_path.val->valuestring) >= PATH_MAX)) {
cJSON_Delete(json); cJSON_Delete(json);
return NULL; return NULL;
} }

View File

@@ -4,7 +4,6 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <fcntl.h> #include <fcntl.h>
#include <openssl/evp.h>
#include <pcre.h> #include <pcre.h>
#define MAX_DECOMPRESSED_SIZE_RATIO 40.0 #define MAX_DECOMPRESSED_SIZE_RATIO 40.0
@@ -211,11 +210,20 @@ scan_code_t parse_archive(scan_arc_ctx_t *ctx, vfile_t *f, document_t *doc, pcre
double decompressed_size_ratio = (double) sub_job->vfile.st_size / (double) f->st_size; double decompressed_size_ratio = (double) sub_job->vfile.st_size / (double) f->st_size;
if (decompressed_size_ratio > MAX_DECOMPRESSED_SIZE_RATIO) { if (decompressed_size_ratio > MAX_DECOMPRESSED_SIZE_RATIO) {
CTX_LOG_DEBUGF("arc.c", "Skipped %s, possible zip bomb (decompressed_size_ratio=%f)", sub_job->filepath, CTX_LOG_ERRORF("arc.c", "Skipped %s, possible zip bomb (decompressed_size_ratio=%f)",
sub_job->filepath,
decompressed_size_ratio); decompressed_size_ratio);
break; break;
} }
if ((archive_entry_is_encrypted(entry) || archive_entry_is_data_encrypted(entry) ||
archive_entry_is_metadata_encrypted(entry)) && ctx->passphrase[0] == 0) {
// Is encrypted but no password is specified, skip
CTX_LOG_ERRORF("arc.c", "Skipped %s, archive is encrypted but no passphrase is supplied",
doc->filepath);
break;
}
// Handle excludes // Handle excludes
if (exclude != NULL && EXCLUDED(sub_job->filepath)) { if (exclude != NULL && EXCLUDED(sub_job->filepath)) {
CTX_LOG_DEBUGF("arc.c", "Excluded: %s", sub_job->filepath); CTX_LOG_DEBUGF("arc.c", "Excluded: %s", sub_job->filepath);

View File

@@ -153,22 +153,23 @@ int render_cover(scan_ebook_ctx_t *ctx, fz_context *fzctx, document_t *doc, fz_d
sws_freeContext(sws_ctx); sws_freeContext(sws_ctx);
// YUV420p -> JPEG // YUV420p -> JPEG/WEBP
AVCodecContext *jpeg_encoder = alloc_jpeg_encoder(pixmap->w, pixmap->h, ctx->tn_qscale); AVCodecContext *thumbnail_encoder = alloc_webp_encoder(pixmap->w, pixmap->h, ctx->tn_qscale);
avcodec_send_frame(jpeg_encoder, scaled_frame); avcodec_send_frame(thumbnail_encoder, scaled_frame);
avcodec_send_frame(thumbnail_encoder, NULL); // Send EOF
AVPacket jpeg_packet; AVPacket thumbnail_packet;
av_init_packet(&jpeg_packet); av_init_packet(&thumbnail_packet);
avcodec_receive_packet(jpeg_encoder, &jpeg_packet); avcodec_receive_packet(thumbnail_encoder, &thumbnail_packet);
APPEND_LONG_META(doc, MetaThumbnail, 1); APPEND_LONG_META(doc, MetaThumbnail, 1);
ctx->store(doc->doc_id, 0, (char *) jpeg_packet.data, jpeg_packet.size); ctx->store(doc->doc_id, 0, (char *) thumbnail_packet.data, thumbnail_packet.size);
free(samples); free(samples);
av_packet_unref(&jpeg_packet); av_packet_unref(&thumbnail_packet);
av_free(*scaled_frame->data); av_free(*scaled_frame->data);
av_frame_free(&scaled_frame); av_frame_free(&scaled_frame);
avcodec_free_context(&jpeg_encoder); avcodec_free_context(&thumbnail_encoder);
fz_drop_pixmap(fzctx, pixmap); fz_drop_pixmap(fzctx, pixmap);
fz_drop_page(fzctx, cover); fz_drop_page(fzctx, cover);

View File

@@ -68,7 +68,7 @@ void *scale_frame(const AVCodecContext *decoder, const AVFrame *frame, int size)
struct SwsContext *sws_ctx = sws_getContext( struct SwsContext *sws_ctx = sws_getContext(
decoder->width, decoder->height, decoder->pix_fmt, decoder->width, decoder->height, decoder->pix_fmt,
dstW, dstH, AV_PIX_FMT_YUVJ420P, dstW, dstH, AV_PIX_FMT_YUV420P,
SIST_SWS_ALGO, 0, 0, 0 SIST_SWS_ALGO, 0, 0, 0
); );
@@ -436,7 +436,8 @@ int decode_frame_and_save_thumbnail(scan_media_ctx_t *ctx, AVFormatContext *pFor
} }
if (seek_ok == FALSE && thumbnail_index != 0) { if (seek_ok == FALSE && thumbnail_index != 0) {
CTX_LOG_WARNING(doc->filepath, "(media.c) Could not seek media file. Can't generate additional thumbnails."); CTX_LOG_WARNING(doc->filepath,
"(media.c) Could not seek media file. Can't generate additional thumbnails.");
return SAVE_THUMBNAIL_FAILED; return SAVE_THUMBNAIL_FAILED;
} }
} }
@@ -470,18 +471,19 @@ int decode_frame_and_save_thumbnail(scan_media_ctx_t *ctx, AVFormatContext *pFor
ctx->store(doc->doc_id, 0, frame_and_packet->packet->data, frame_and_packet->packet->size); ctx->store(doc->doc_id, 0, frame_and_packet->packet->data, frame_and_packet->packet->size);
} else { } else {
// Encode frame to jpeg // Encode frame
AVCodecContext *jpeg_encoder = alloc_jpeg_encoder(scaled_frame->width, scaled_frame->height, AVCodecContext *thumbnail_encoder = alloc_webp_encoder(scaled_frame->width, scaled_frame->height,
ctx->tn_qscale); ctx->tn_qscale);
avcodec_send_frame(jpeg_encoder, scaled_frame); avcodec_send_frame(thumbnail_encoder, scaled_frame);
avcodec_send_frame(thumbnail_encoder, NULL); // send EOF
AVPacket jpeg_packet; AVPacket thumbnail_packet;
av_init_packet(&jpeg_packet); av_init_packet(&thumbnail_packet);
avcodec_receive_packet(jpeg_encoder, &jpeg_packet); avcodec_receive_packet(thumbnail_encoder, &thumbnail_packet);
// Save thumbnail // Save thumbnail
if (thumbnail_index == 0) { if (thumbnail_index == 0) {
ctx->store(doc->doc_id, 0, jpeg_packet.data, jpeg_packet.size); ctx->store(doc->doc_id, 0, thumbnail_packet.data, thumbnail_packet.size);
return_value = SAVE_THUMBNAIL_OK; return_value = SAVE_THUMBNAIL_OK;
} else if (thumbnail_index > 1) { } else if (thumbnail_index > 1) {
@@ -489,15 +491,15 @@ int decode_frame_and_save_thumbnail(scan_media_ctx_t *ctx, AVFormatContext *pFor
// I figure out a better fix. // I figure out a better fix.
thumbnail_index -= 1; thumbnail_index -= 1;
ctx->store(doc->doc_id, thumbnail_index, jpeg_packet.data, jpeg_packet.size); ctx->store(doc->doc_id, thumbnail_index, thumbnail_packet.data, thumbnail_packet.size);
return_value = SAVE_THUMBNAIL_OK; return_value = SAVE_THUMBNAIL_OK;
} else { } else {
return_value = SAVE_THUMBNAIL_SKIPPED; return_value = SAVE_THUMBNAIL_SKIPPED;
} }
avcodec_free_context(&jpeg_encoder); avcodec_free_context(&thumbnail_encoder);
av_packet_unref(&jpeg_packet); av_packet_unref(&thumbnail_packet);
av_free(*scaled_frame->data); av_free(*scaled_frame->data);
av_frame_free(&scaled_frame); av_frame_free(&scaled_frame);
} }
@@ -854,9 +856,10 @@ int store_image_thumbnail(scan_media_ctx_t *ctx, void *buf, size_t buf_len, docu
ctx->store(doc->doc_id, 0, frame_and_packet->packet->data, frame_and_packet->packet->size); ctx->store(doc->doc_id, 0, frame_and_packet->packet->data, frame_and_packet->packet->size);
} else { } else {
// Encode frame to jpeg // Encode frame to jpeg
AVCodecContext *jpeg_encoder = alloc_jpeg_encoder(scaled_frame->width, scaled_frame->height, AVCodecContext *jpeg_encoder = alloc_webp_encoder(scaled_frame->width, scaled_frame->height,
ctx->tn_qscale); ctx->tn_qscale);
avcodec_send_frame(jpeg_encoder, scaled_frame); avcodec_send_frame(jpeg_encoder, scaled_frame);
avcodec_send_frame(jpeg_encoder, NULL); // Send EOF
AVPacket jpeg_packet; AVPacket jpeg_packet;
av_init_packet(&jpeg_packet); av_init_packet(&jpeg_packet);

View File

@@ -48,6 +48,28 @@ static AVCodecContext *alloc_jpeg_encoder(int w, int h, int qscale) {
return jpeg; return jpeg;
} }
static AVCodecContext *alloc_webp_encoder(int w, int h, int qscale) {
const AVCodec *webp_codec = avcodec_find_encoder(AV_CODEC_ID_WEBP);
AVCodecContext *webp = avcodec_alloc_context3(webp_codec);
webp->width = w;
webp->height = h;
webp->time_base.den = 1000000;
webp->time_base.num = 1;
webp->compression_level = 6;
webp->global_quality = FF_QP2LAMBDA * qscale;
webp->pix_fmt = AV_PIX_FMT_YUV420P;
webp->color_range = AVCOL_RANGE_JPEG;
int ret = avcodec_open2(webp, webp_codec, NULL);
if (ret != 0) {
return NULL;
}
return webp;
}
void parse_media(scan_media_ctx_t *ctx, vfile_t *f, document_t *doc, const char *mime_str); void parse_media(scan_media_ctx_t *ctx, vfile_t *f, document_t *doc, const char *mime_str);

View File

@@ -52,7 +52,7 @@ int store_thumbnail_rgb24(scan_raw_ctx_t *ctx, libraw_processed_image_t *img, do
struct SwsContext *sws_ctx = sws_getContext( struct SwsContext *sws_ctx = sws_getContext(
img->width, img->height, AV_PIX_FMT_RGB24, img->width, img->height, AV_PIX_FMT_RGB24,
dstW, dstH, AV_PIX_FMT_YUVJ420P, dstW, dstH, AV_PIX_FMT_YUV420P,
SIST_SWS_ALGO, 0, 0, 0 SIST_SWS_ALGO, 0, 0, 0
); );
@@ -76,20 +76,21 @@ int store_thumbnail_rgb24(scan_raw_ctx_t *ctx, libraw_processed_image_t *img, do
sws_freeContext(sws_ctx); sws_freeContext(sws_ctx);
AVCodecContext *jpeg_encoder = alloc_jpeg_encoder(scaled_frame->width, scaled_frame->height, 1.0f); AVCodecContext *thumbnail_encoder = alloc_webp_encoder(scaled_frame->width, scaled_frame->height, ctx->tn_qscale);
avcodec_send_frame(jpeg_encoder, scaled_frame); avcodec_send_frame(thumbnail_encoder, scaled_frame);
avcodec_send_frame(thumbnail_encoder, NULL); // Send EOF
AVPacket jpeg_packet; AVPacket thumbnail_packet;
av_init_packet(&jpeg_packet); av_init_packet(&thumbnail_packet);
avcodec_receive_packet(jpeg_encoder, &jpeg_packet); avcodec_receive_packet(thumbnail_encoder, &thumbnail_packet);
APPEND_LONG_META(doc, MetaThumbnail, 1); APPEND_LONG_META(doc, MetaThumbnail, 1);
ctx->store((char *) doc->doc_id, sizeof(doc->doc_id), (char *) jpeg_packet.data, jpeg_packet.size); ctx->store((char *) doc->doc_id, sizeof(doc->doc_id), (char *) thumbnail_packet.data, thumbnail_packet.size);
av_packet_unref(&jpeg_packet); av_packet_unref(&thumbnail_packet);
av_free(*scaled_frame->data); av_free(*scaled_frame->data);
av_frame_free(&scaled_frame); av_frame_free(&scaled_frame);
avcodec_free_context(&jpeg_encoder); avcodec_free_context(&thumbnail_encoder);
return TRUE; return TRUE;
} }