Compare commits

...

13 Commits
3.1.0 ... 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
30 changed files with 1216 additions and 701 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

@@ -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

@@ -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,6 +65,18 @@ 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",
@@ -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
@@ -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

@@ -93,9 +93,12 @@ 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) {
@@ -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

@@ -51,11 +51,11 @@
#include <ctype.h> #include <ctype.h>
#include "git_hash.h" #include "git_hash.h"
#define VERSION "3.1.0" #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 = 1; static const int VersionMinor = 1;
static const int VersionPatch = 0; 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;
} }