2016-AUG-21

This commit is contained in:
il
2026-08-21 13:07:52 +09:00
parent 5525f7cf85
commit fe2357ddb2
6 changed files with 252 additions and 12 deletions
@@ -14,6 +14,7 @@ PublishPort={{ services['papra']['ports']['http'] }}:1221/tcp
# Volumes
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
# 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.
ExecStart=/usr/bin/podman exec -u nonroot papra node --input-type=module -e '\
import { createClient } from "@libsql/client"; \
import { rename, rm } from "node:fs/promises"; \
import { rename, copyFile, rm } from "node:fs/promises"; \
\
/* Paths */ \
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"), \
].join("-"); \
\
const dst = backupDir + "/db_" + date + ".sqlite"; \
const dst = backupDir + "/db.sqlite"; \
const dated_dst = backupDir + "/db_" + date + ".sqlite"; \
const tmp = dst + ".tmp"; \
\
/* 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. */ \
await rename(tmp, dst); \
await copyFile(dst, dated_dst); \
completed = true; \
} finally { \
if (!completed) { \