2016-AUG-21
This commit is contained in:
@@ -341,19 +341,19 @@ version:
|
|||||||
authelia: "4.39.20"
|
authelia: "4.39.20"
|
||||||
# App
|
# App
|
||||||
vaultwarden: "1.37.1"
|
vaultwarden: "1.37.1"
|
||||||
gitea: "1.27.1"
|
gitea: "1.27.2"
|
||||||
redis: "8.10.0"
|
redis: "8.10.0"
|
||||||
immich: "v3.1.0"
|
immich: "v3.1.0"
|
||||||
nextcloud: "34.0.2"
|
nextcloud: "34.0.3"
|
||||||
collabora: "26.04.2.4.1"
|
collabora: "26.04.3.1.1"
|
||||||
sure: "0.7.2"
|
sure: "0.7.3"
|
||||||
outline: "1.9.2"
|
outline: "1.9.2"
|
||||||
memos: "0.30.0"
|
memos: "0.30.0"
|
||||||
mealie: "v3.22.0"
|
mealie: "v3.22.0"
|
||||||
homebox: "0.26.2"
|
homebox: "0.26.2"
|
||||||
homepage: "v1.13.2"
|
homepage: "v2.0.0"
|
||||||
snappymail: "v2.38.2"
|
snappymail: "v2.38.2"
|
||||||
mastodon: "v4.6.4"
|
mastodon: "v4.6.6"
|
||||||
booklogr: "v1.11.1"
|
booklogr: "v1.11.1"
|
||||||
papra: "26.6.1-rootless"
|
papra: "26.6.1-rootless"
|
||||||
romm: "5.1.0"
|
romm: "5.1.0"
|
||||||
|
|||||||
@@ -10,8 +10,21 @@
|
|||||||
- "data/containers/papra"
|
- "data/containers/papra"
|
||||||
- "data/containers/papra/data"
|
- "data/containers/papra/data"
|
||||||
- "data/containers/papra/backup"
|
- "data/containers/papra/backup"
|
||||||
|
- "containers/papra"
|
||||||
|
- "containers/papra/script"
|
||||||
|
- "containers/papra/db"
|
||||||
become: true
|
become: true
|
||||||
|
|
||||||
|
- name: Check backup file
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ node['home_path'] }}/data/containers/papra/backup/db.sqlite"
|
||||||
|
register: "is_db_backup"
|
||||||
|
|
||||||
|
- name: Check database file
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ node['home_path'] }}/containers/papra/db/db.sqlite"
|
||||||
|
register: "is_db"
|
||||||
|
|
||||||
- name: Pull image
|
- name: Pull image
|
||||||
containers.podman.podman_image:
|
containers.podman.podman_image:
|
||||||
name: "ghcr.io/papra-hq/papra"
|
name: "ghcr.io/papra-hq/papra"
|
||||||
@@ -45,6 +58,19 @@
|
|||||||
notify: "notification_restart_papra"
|
notify: "notification_restart_papra"
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
|
- name: Restore db file
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "{{ node['home_path'] }}/data/containers/papra/backup/db.sqlite"
|
||||||
|
remote_src: true
|
||||||
|
dest: "{{ node['home_path'] }}/containers/papra/db/db.sqlite"
|
||||||
|
owner: "{{ services['papra']['subuid'] }}"
|
||||||
|
group: "{{ node['group'] }}"
|
||||||
|
mode: "0660"
|
||||||
|
become: true
|
||||||
|
when:
|
||||||
|
- is_db_backup.stat.exists
|
||||||
|
- not is_db.stat.exists
|
||||||
|
|
||||||
- name: Deploy papra.container file
|
- name: Deploy papra.container file
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/papra/papra.container.j2"
|
src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/papra/papra.container.j2"
|
||||||
@@ -66,10 +92,10 @@
|
|||||||
- "papra-backup.service"
|
- "papra-backup.service"
|
||||||
- "papra-backup.timer"
|
- "papra-backup.timer"
|
||||||
|
|
||||||
- name: Deploy papra exporter
|
- name: Deploy papra script
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/papra/script/papra-exporter.sh"
|
src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/papra/script/"
|
||||||
dest: "{{ node['home_path'] }}/data/containers/papra/"
|
dest: "{{ node['home_path'] }}/containers/papra/script/"
|
||||||
owner: "{{ ansible_user }}"
|
owner: "{{ ansible_user }}"
|
||||||
group: "{{ node['group'] }}"
|
group: "{{ node['group'] }}"
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ PublishPort={{ services['papra']['ports']['http'] }}:1221/tcp
|
|||||||
|
|
||||||
# Volumes
|
# Volumes
|
||||||
Volume=%h/data/containers/papra/data:/app/app-data:rw
|
Volume=%h/data/containers/papra/data:/app/app-data:rw
|
||||||
|
Volume=%h/containers/papra/db:/app/app-data/db:rw
|
||||||
Volume=%h/data/containers/papra/backup:/app/backup:rw
|
Volume=%h/data/containers/papra/backup:/app/backup:rw
|
||||||
|
|
||||||
# General
|
# General
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
URL=""
|
||||||
|
ORG_ID=""
|
||||||
|
TOKEN=""
|
||||||
|
apply=false
|
||||||
|
|
||||||
|
page_size=100
|
||||||
|
page_index=0
|
||||||
|
documents_read=0
|
||||||
|
matched=0
|
||||||
|
unmatched=0
|
||||||
|
updated=0
|
||||||
|
failed=0
|
||||||
|
|
||||||
|
# usage() function
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 -u \"URL\" -o \"ORG_ID\" -t \"TOKEN\" [-a]"
|
||||||
|
echo "-u <URL>: Specify the URL of Papra, including https://"
|
||||||
|
echo "-o <ORG_ID>: Specify the organization ID of Papra"
|
||||||
|
echo "-t <TOKEN>: Specify the token to access the Papra API"
|
||||||
|
echo "-a: Apply changes. Without this option, only perform a dry run"
|
||||||
|
echo "Token requires documents:read and documents:update permissions"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Log function
|
||||||
|
log() {
|
||||||
|
local timestamp
|
||||||
|
local level="$1"
|
||||||
|
local msg="$2"
|
||||||
|
|
||||||
|
timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||||
|
echo "time=\"$timestamp\" level=\"$level\" msg=\"$msg\" source=\"papra-date-change.sh\"" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# getopts to get arguments
|
||||||
|
while getopts ":u:o:t:ah" opt; do
|
||||||
|
case "$opt" in
|
||||||
|
u)
|
||||||
|
URL="$OPTARG"
|
||||||
|
;;
|
||||||
|
o)
|
||||||
|
ORG_ID="$OPTARG"
|
||||||
|
;;
|
||||||
|
t)
|
||||||
|
TOKEN="$OPTARG"
|
||||||
|
;;
|
||||||
|
a)
|
||||||
|
apply=true
|
||||||
|
;;
|
||||||
|
h)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
\?) # unknown option
|
||||||
|
log "error" "Invalid option: -$OPTARG"
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
:) # parameter required
|
||||||
|
log "error" "Option -$OPTARG requires an argument"
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Validate required arguments
|
||||||
|
if [[ -z "$URL" || -z "$ORG_ID" || -z "$TOKEN" ]]; then
|
||||||
|
log "error" "URL, ORG_ID and TOKEN are required"
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Require an explicit HTTPS URL
|
||||||
|
if [[ "$URL" != https://* ]]; then
|
||||||
|
log "error" "URL must start with https://"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove trailing slash
|
||||||
|
URL="${URL%/}"
|
||||||
|
|
||||||
|
# Collect all documents before modifying them.
|
||||||
|
# This prevents pagination results from changing during updates.
|
||||||
|
documents_file="$(mktemp)"
|
||||||
|
trap 'rm -f -- "$documents_file"' EXIT
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
response="$(
|
||||||
|
curl \
|
||||||
|
--fail \
|
||||||
|
--silent \
|
||||||
|
--show-error \
|
||||||
|
--get \
|
||||||
|
--header "Authorization: Bearer $TOKEN" \
|
||||||
|
--data-urlencode "pageIndex=$page_index" \
|
||||||
|
--data-urlencode "pageSize=$page_size" \
|
||||||
|
--data-urlencode "sortField=createdAt" \
|
||||||
|
--data-urlencode "sortOrder=asc" \
|
||||||
|
"$URL/api/organizations/$ORG_ID/documents"
|
||||||
|
)"
|
||||||
|
|
||||||
|
count="$(jq -r '.documents | length' <<<"$response")"
|
||||||
|
total="$(jq -r '.documentsCount' <<<"$response")"
|
||||||
|
|
||||||
|
jq -c '.documents[]' <<<"$response" >>"$documents_file"
|
||||||
|
|
||||||
|
documents_read=$((documents_read + count))
|
||||||
|
|
||||||
|
if (( count == 0 || documents_read >= total )); then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
page_index=$((page_index + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
log "info" "Documents read: $documents_read"
|
||||||
|
|
||||||
|
while IFS= read -r document; do
|
||||||
|
id="$(jq -r '.id' <<<"$document")"
|
||||||
|
name="$(jq -r '.name // empty' <<<"$document")"
|
||||||
|
|
||||||
|
if [[ -z "$id" || -z "$name" ]]; then
|
||||||
|
unmatched=$((unmatched + 1))
|
||||||
|
log "warning" "Skipped document with missing ID or name"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract the first _YYYY.MM.DD_ pattern.
|
||||||
|
if [[ "$name" =~ (^|_)([0-9]{4})\.([0-9]{2})\.([0-9]{2})(_|$) ]]; then
|
||||||
|
year="${BASH_REMATCH[2]}"
|
||||||
|
month="${BASH_REMATCH[3]}"
|
||||||
|
day="${BASH_REMATCH[4]}"
|
||||||
|
document_date="$year-$month-$day"
|
||||||
|
else
|
||||||
|
unmatched=$((unmatched + 1))
|
||||||
|
printf '[SKIP] %s: date pattern not found\n' "$name"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reject impossible dates such as 2026.02.30.
|
||||||
|
parsed_date="$(
|
||||||
|
date \
|
||||||
|
--utc \
|
||||||
|
--date="$document_date" \
|
||||||
|
+"%Y-%m-%d" \
|
||||||
|
2>/dev/null ||
|
||||||
|
true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ "$parsed_date" != "$document_date" ]]; then
|
||||||
|
unmatched=$((unmatched + 1))
|
||||||
|
printf '[SKIP] %s: invalid date %s\n' \
|
||||||
|
"$name" \
|
||||||
|
"$document_date"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
matched=$((matched + 1))
|
||||||
|
|
||||||
|
if [[ "$apply" != true ]]; then
|
||||||
|
printf '[DRY-RUN] %s -> %s\n' \
|
||||||
|
"$name" \
|
||||||
|
"$document_date"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
payload="$(
|
||||||
|
jq \
|
||||||
|
--null-input \
|
||||||
|
--compact-output \
|
||||||
|
--arg documentDate "$document_date" \
|
||||||
|
'{documentDate: $documentDate}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
if ! curl \
|
||||||
|
--fail \
|
||||||
|
--silent \
|
||||||
|
--show-error \
|
||||||
|
--request PATCH \
|
||||||
|
--header "Authorization: Bearer $TOKEN" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--data "$payload" \
|
||||||
|
--output /dev/null \
|
||||||
|
"$URL/api/organizations/$ORG_ID/documents/$id"
|
||||||
|
then
|
||||||
|
failed=$((failed + 1))
|
||||||
|
log "error" "Failed to update document: $id ($name)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
updated=$((updated + 1))
|
||||||
|
|
||||||
|
printf '[%d/%d] %s -> %s\n' \
|
||||||
|
"$updated" \
|
||||||
|
"$matched" \
|
||||||
|
"$name" \
|
||||||
|
"$document_date"
|
||||||
|
done <"$documents_file"
|
||||||
|
|
||||||
|
if [[ "$apply" == true ]]; then
|
||||||
|
log "info" "Update completed: read=$documents_read matched=$matched unmatched=$unmatched updated=$updated failed=$failed"
|
||||||
|
else
|
||||||
|
log "info" "Dry run completed: read=$documents_read matched=$matched unmatched=$unmatched"
|
||||||
|
log "info" "No documents were modified. Run again with -a to apply"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( failed > 0 )); then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -8,7 +8,7 @@ Type=oneshot
|
|||||||
# Create a consistent SQLite snapshot and atomically publish it.
|
# Create a consistent SQLite snapshot and atomically publish it.
|
||||||
ExecStart=/usr/bin/podman exec -u nonroot papra node --input-type=module -e '\
|
ExecStart=/usr/bin/podman exec -u nonroot papra node --input-type=module -e '\
|
||||||
import { createClient } from "@libsql/client"; \
|
import { createClient } from "@libsql/client"; \
|
||||||
import { rename, rm } from "node:fs/promises"; \
|
import { rename, copyFile, rm } from "node:fs/promises"; \
|
||||||
\
|
\
|
||||||
/* Paths */ \
|
/* Paths */ \
|
||||||
const databaseUrl = "file:/app/app-data/db/db.sqlite"; \
|
const databaseUrl = "file:/app/app-data/db/db.sqlite"; \
|
||||||
@@ -22,7 +22,8 @@ ExecStart=/usr/bin/podman exec -u nonroot papra node --input-type=module -e '\
|
|||||||
String(now.getDate()).padStart(2, "0"), \
|
String(now.getDate()).padStart(2, "0"), \
|
||||||
].join("-"); \
|
].join("-"); \
|
||||||
\
|
\
|
||||||
const dst = backupDir + "/db_" + date + ".sqlite"; \
|
const dst = backupDir + "/db.sqlite"; \
|
||||||
|
const dated_dst = backupDir + "/db_" + date + ".sqlite"; \
|
||||||
const tmp = dst + ".tmp"; \
|
const tmp = dst + ".tmp"; \
|
||||||
\
|
\
|
||||||
/* Never expose a partially-created backup as a completed one. */ \
|
/* Never expose a partially-created backup as a completed one. */ \
|
||||||
@@ -43,6 +44,7 @@ ExecStart=/usr/bin/podman exec -u nonroot papra node --input-type=module -e '\
|
|||||||
\
|
\
|
||||||
/* Publish only after VACUUM INTO has completed successfully. */ \
|
/* Publish only after VACUUM INTO has completed successfully. */ \
|
||||||
await rename(tmp, dst); \
|
await rename(tmp, dst); \
|
||||||
|
await copyFile(dst, dated_dst); \
|
||||||
completed = true; \
|
completed = true; \
|
||||||
} finally { \
|
} finally { \
|
||||||
if (!completed) { \
|
if (!completed) { \
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ whitelist:
|
|||||||
- "evt.Meta.target_fqdn == '{{ services['nextcloud']['domain']['public'] }}.{{ domain['public'] }}' && evt.Meta.http_status == '404' && evt.Meta.http_verb == 'GET' && evt.Meta.http_path startsWith '/index.php/core/preview?'"
|
- "evt.Meta.target_fqdn == '{{ services['nextcloud']['domain']['public'] }}.{{ domain['public'] }}' && evt.Meta.http_status == '404' && evt.Meta.http_verb == 'GET' && evt.Meta.http_path startsWith '/index.php/core/preview?'"
|
||||||
# nextcloud chunks.mjs request false positive
|
# nextcloud chunks.mjs request false positive
|
||||||
- "evt.Meta.target_fqdn == '{{ services['nextcloud']['domain']['public'] }}.{{ domain['public'] }}' && evt.Meta.http_status in ['200', '304'] && evt.Meta.http_verb == 'GET' && evt.Meta.http_path contains 'chunk.mjs'"
|
- "evt.Meta.target_fqdn == '{{ services['nextcloud']['domain']['public'] }}.{{ domain['public'] }}' && evt.Meta.http_status in ['200', '304'] && evt.Meta.http_verb == 'GET' && evt.Meta.http_path contains 'chunk.mjs'"
|
||||||
|
# nextcloud app notification false positive (http-proving)
|
||||||
|
- "evt.Meta.target_fqdn == '{{ services['nextcloud']['domain']['public'] }}.{{ domain['public'] }}' && evt.Meta.http_status == '404' && evt.Meta.http_verb == 'GET' && evt.Meta.http_path startsWith '/ocs/v2.php/apps/notifications/api/v2/notifications/'"
|
||||||
# outline POST 401 errors false positive
|
# outline POST 401 errors false positive
|
||||||
- "evt.Meta.target_fqdn == '{{ services['outline']['domain']['public'] }}.{{ domain['public'] }}' && evt.Meta.http_status == '401' && evt.Meta.http_verb == 'POST' && evt.Meta.http_path startsWith '/api/'"
|
- "evt.Meta.target_fqdn == '{{ services['outline']['domain']['public'] }}.{{ domain['public'] }}' && evt.Meta.http_status == '401' && evt.Meta.http_verb == 'POST' && evt.Meta.http_path startsWith '/api/'"
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user