mirror of
https://github.com/Terranom674/Piwigo_Bratonien_Tools.git
synced 2026-09-19 22:14:33 +00:00
Compare commits
16 Commits
ad90cd678b
...
774c82ab68
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
774c82ab68 | ||
|
|
7024d3cd3d | ||
|
|
042a090db4 | ||
|
|
2edacf5e0b | ||
|
|
e53041f1c1 | ||
|
|
3512f60136 | ||
|
|
73e4b08dbd | ||
|
|
e06539c125 | ||
|
|
e0cd91a0b7 | ||
|
|
c28a8c6f80 | ||
|
|
032d9a151c | ||
|
|
86fad7aa1f | ||
|
|
62b17fcd09 | ||
|
|
1e77988831 | ||
|
|
eb3bc48ceb | ||
|
|
b8d31e37ca |
8
.github/workflows/lint.yml
vendored
8
.github/workflows/lint.yml
vendored
@@ -30,3 +30,11 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 -m compileall -q runtime
|
||||
|
||||
- name: Shell Syntax pruefen
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
while IFS= read -r -d '' file; do
|
||||
bash -n "$file"
|
||||
done < <(find . -type f -name '*.sh' -print0)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
/*
|
||||
Plugin Name: Bratonien Tools
|
||||
Version: 0.9.4.9
|
||||
Version: 0.9.5.1
|
||||
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
|
||||
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
|
||||
Author: Bratonien
|
||||
|
||||
142
runtime/lib/build_user_manifest.py
Normal file
142
runtime/lib/build_user_manifest.py
Normal file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a manifest from one connection's explicitly selected user filesystem roots.
|
||||
|
||||
This mode deliberately does not read the legacy Showcase source view. The storage
|
||||
configuration must already resolve to the authenticated Nextcloud user's local
|
||||
home/files tree. Only the selected include prefixes are exposed to Piwigo.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
|
||||
def safe_relative(value: str) -> str:
|
||||
value = str(value).strip("/")
|
||||
parts = PurePosixPath(value).parts
|
||||
if ".." in parts:
|
||||
raise ValueError(f"unsafe relative path: {value!r}")
|
||||
return value
|
||||
|
||||
|
||||
def contained(root: Path, relative: str) -> Path:
|
||||
relative = safe_relative(relative)
|
||||
candidate = root.joinpath(*PurePosixPath(relative).parts) if relative else root
|
||||
root_real = root.resolve()
|
||||
candidate_real = candidate.resolve()
|
||||
try:
|
||||
candidate_real.relative_to(root_real)
|
||||
except ValueError as error:
|
||||
raise ValueError(f"path escapes configured user root: {relative!r}") from error
|
||||
return candidate
|
||||
|
||||
|
||||
def read_config(path: Path) -> list[tuple[str, str, Path, str]]:
|
||||
rows: list[tuple[str, str, Path, str]] = []
|
||||
with path.open(encoding="utf-8") as handle:
|
||||
for number, raw in enumerate(handle, 1):
|
||||
line = raw.rstrip("\n")
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
fields = line.split("\t")
|
||||
if len(fields) not in {3, 4}:
|
||||
raise ValueError(
|
||||
f"{path}:{number}: expected storage_id, source_prefix, local_mount and optional include_prefix"
|
||||
)
|
||||
storage_id, source_prefix, local_mount = fields[:3]
|
||||
include_prefix = fields[3] if len(fields) == 4 else ""
|
||||
source_prefix = safe_relative(source_prefix)
|
||||
include_prefix = safe_relative(include_prefix)
|
||||
mount = Path(local_mount)
|
||||
if not storage_id.strip():
|
||||
raise ValueError(f"{path}:{number}: storage_id is empty")
|
||||
if not mount.is_absolute():
|
||||
raise ValueError(f"{path}:{number}: local_mount must be absolute")
|
||||
rows.append((storage_id.strip(), source_prefix, mount, include_prefix))
|
||||
if not rows:
|
||||
raise ValueError("no user storage mappings configured")
|
||||
return rows
|
||||
|
||||
|
||||
def stable_id(source: Path) -> str:
|
||||
digest = hashlib.sha256(str(source.resolve()).encode("utf-8")).hexdigest()[:24]
|
||||
return f"user-{digest}"
|
||||
|
||||
|
||||
def manifest_entry(source: Path) -> str:
|
||||
if source.is_symlink():
|
||||
raise ValueError(f"selected source must not be a symlink: {source}")
|
||||
if source.is_dir():
|
||||
item_type = "folder"
|
||||
elif source.is_file():
|
||||
item_type = "file"
|
||||
else:
|
||||
raise FileNotFoundError(f"selected source is unavailable: {source}")
|
||||
name = source.name
|
||||
if not name:
|
||||
raise ValueError(f"selected source has no display name: {source}")
|
||||
for value in (name, str(source)):
|
||||
if any(char in value for char in ("\t", "\n", "\r")):
|
||||
raise ValueError(f"selected source contains unsupported control characters: {source}")
|
||||
return f"{stable_id(source)}\t{item_type}\t{name}\t{source}"
|
||||
|
||||
|
||||
def build(storage_config: Path, output: Path) -> dict[str, object]:
|
||||
mappings = read_config(storage_config)
|
||||
entries: dict[str, str] = {}
|
||||
|
||||
for _storage_id, source_prefix, mount, include_prefix in mappings:
|
||||
if not mount.is_dir():
|
||||
raise FileNotFoundError(f"user storage mount unavailable: {mount}")
|
||||
user_root = contained(mount, source_prefix)
|
||||
if not user_root.is_dir():
|
||||
raise FileNotFoundError(f"user files root unavailable: {user_root}")
|
||||
|
||||
if include_prefix:
|
||||
selected = contained(user_root, include_prefix)
|
||||
line = manifest_entry(selected)
|
||||
entries[str(selected.resolve())] = line
|
||||
continue
|
||||
|
||||
# Empty selection means the user's root. Expose its direct children as
|
||||
# Piwigo roots instead of creating an artificial "files" album.
|
||||
for child in sorted(user_root.iterdir(), key=lambda item: (item.name.casefold(), item.name)):
|
||||
if child.is_symlink():
|
||||
continue
|
||||
if not (child.is_dir() or child.is_file()):
|
||||
continue
|
||||
line = manifest_entry(child)
|
||||
entries[str(child.resolve())] = line
|
||||
|
||||
if not entries:
|
||||
raise RuntimeError("the selected Nextcloud directories contain no readable files or folders")
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=output.parent, delete=False) as handle:
|
||||
for line in entries.values():
|
||||
handle.write(line + "\n")
|
||||
temporary = Path(handle.name)
|
||||
temporary.replace(output)
|
||||
return {"roots": len(entries), "manifest": str(output)}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--storage-config", required=True, type=Path)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
print(json.dumps(build(args.storage_config, args.output), ensure_ascii=False))
|
||||
except Exception as error:
|
||||
print(f"user-manifest: {error}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
195
runtime/reconcile-user-scope.php
Normal file
195
runtime/reconcile-user-scope.php
Normal file
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
if (PHP_SAPI !== 'cli')
|
||||
{
|
||||
fwrite(STDERR, "CLI only\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function user_scope_fail($message)
|
||||
{
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
function user_scope_path_has_segment($path, $segment)
|
||||
{
|
||||
$parts = preg_split('#[/\\\\]+#', trim((string)$path, '/\\'));
|
||||
foreach ($parts as $part)
|
||||
{
|
||||
if ((string)$part === (string)$segment) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function user_scope_candidate($mount, $prefix, $accessUser)
|
||||
{
|
||||
$mount = rtrim((string)$mount, '/');
|
||||
$prefix = trim((string)$prefix, '/');
|
||||
if ($mount === '' || $mount[0] !== '/') return null;
|
||||
$root = $mount.($prefix !== '' ? '/'.$prefix : '');
|
||||
if (!is_dir($root) || !is_readable($root)) return null;
|
||||
$real = realpath($root);
|
||||
if ($real === false || !user_scope_path_has_segment($real, $accessUser)) return null;
|
||||
return array('local_mount'=>$mount, 'source_prefix'=>$prefix, 'root'=>$real);
|
||||
}
|
||||
|
||||
function user_scope_storage(array $storage, $accessUser)
|
||||
{
|
||||
$mount = rtrim(trim((string)($storage['local_mount'] ?? '')), '/');
|
||||
$prefix = trim((string)($storage['source_prefix'] ?? ''), '/');
|
||||
$include = trim((string)($storage['include_prefix'] ?? ''), '/');
|
||||
$candidates = array();
|
||||
|
||||
$add = function($candidate) use (&$candidates) {
|
||||
if (!$candidate) return;
|
||||
$candidates[$candidate['root']] = $candidate;
|
||||
};
|
||||
|
||||
$add(user_scope_candidate($mount, $prefix, $accessUser));
|
||||
if ($mount !== '')
|
||||
{
|
||||
$add(user_scope_candidate(dirname($mount).'/'.$accessUser, $prefix, $accessUser));
|
||||
$add(user_scope_candidate($mount.'/'.$accessUser, $prefix, $accessUser));
|
||||
}
|
||||
|
||||
if ($prefix !== '')
|
||||
{
|
||||
$parts = explode('/', $prefix);
|
||||
if ($parts && (string)$parts[0] !== (string)$accessUser)
|
||||
{
|
||||
array_shift($parts);
|
||||
$rest = implode('/', $parts);
|
||||
$add(user_scope_candidate($mount.'/'.$accessUser, $rest, $accessUser));
|
||||
}
|
||||
}
|
||||
|
||||
if (count($candidates) !== 1)
|
||||
{
|
||||
user_scope_fail('Der lokale Dateistamm für Nextcloud-Benutzer '.$accessUser.' konnte nicht eindeutig bestimmt werden. Die Verbindung wird aus Sicherheitsgründen nicht gestartet.');
|
||||
}
|
||||
|
||||
$resolved = reset($candidates);
|
||||
return array(
|
||||
'storage_id'=>'user:'.$accessUser,
|
||||
'source_prefix'=>$resolved['source_prefix'],
|
||||
'local_mount'=>$resolved['local_mount'],
|
||||
'include_prefix'=>$include,
|
||||
);
|
||||
}
|
||||
|
||||
function user_scope_write_status($piwigoRoot, $id, $message)
|
||||
{
|
||||
$dir = rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-connector-status';
|
||||
if (!is_dir($dir)) @mkdir($dir, 0755, true);
|
||||
$payload = array(
|
||||
'state'=>'error',
|
||||
'message'=>'Benutzerbezogene Datenquelle konnte nicht vorbereitet werden',
|
||||
'timestamp'=>time(),
|
||||
'auth_mode'=>'failed',
|
||||
'api'=>array('state'=>'not_run','message'=>''),
|
||||
'fallback'=>array('state'=>'not_run','message'=>''),
|
||||
'error_detail'=>(string)$message,
|
||||
);
|
||||
@file_put_contents($dir.'/connection-'.$id.'.json', json_encode($payload, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)."\n", LOCK_EX);
|
||||
}
|
||||
|
||||
function user_scope_shell_value($value)
|
||||
{
|
||||
return escapeshellarg((string)$value);
|
||||
}
|
||||
|
||||
$pluginRoot = dirname(__DIR__);
|
||||
$piwigoRoot = dirname($pluginRoot, 2);
|
||||
$dbConfig = $piwigoRoot.'/local/config/database.inc.php';
|
||||
$configDir = '/etc/bratonien-tools/nc-connector';
|
||||
|
||||
try
|
||||
{
|
||||
if (!is_readable($dbConfig)) user_scope_fail('Piwigo-Datenbankkonfiguration ist nicht lesbar.');
|
||||
$conf = array();
|
||||
$prefixeTable = 'piwigo_';
|
||||
require $dbConfig;
|
||||
foreach (array('db_host','db_user','db_password','db_base') as $key)
|
||||
{
|
||||
if (!isset($conf[$key])) user_scope_fail('Piwigo-Datenbankkonfiguration ist unvollständig: '.$key);
|
||||
}
|
||||
|
||||
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
|
||||
if ($db->connect_errno) user_scope_fail('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error);
|
||||
$db->set_charset('utf8mb4');
|
||||
$table = $prefixeTable.'bratonien_tools_nc_connections';
|
||||
$rows = $db->query("SELECT id,adapter,config_json FROM `{$table}` ORDER BY id");
|
||||
if (!$rows) user_scope_fail('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error);
|
||||
|
||||
while ($row = $rows->fetch_assoc())
|
||||
{
|
||||
$id = (int)$row['id'];
|
||||
if ((string)$row['adapter'] !== 'local') continue;
|
||||
$config = json_decode((string)$row['config_json'], true);
|
||||
if (!is_array($config)) continue;
|
||||
if ((string)($config['origin'] ?? '') !== 'native') continue;
|
||||
|
||||
$accessUser = trim((string)($config['nextcloud_access_user'] ?? $config['access_user'] ?? ''));
|
||||
if ($accessUser === '') continue;
|
||||
|
||||
try
|
||||
{
|
||||
$storages = isset($config['storages']) && is_array($config['storages']) ? $config['storages'] : array();
|
||||
if (!$storages) user_scope_fail('Keine Speicherzuordnung für Benutzer '.$accessUser.' vorhanden.');
|
||||
|
||||
$resolved = array();
|
||||
foreach ($storages as $storage)
|
||||
{
|
||||
$item = user_scope_storage((array)$storage, $accessUser);
|
||||
$key = $item['local_mount'].'|'.$item['source_prefix'].'|'.$item['include_prefix'];
|
||||
$resolved[$key] = $item;
|
||||
}
|
||||
$config['storages'] = array_values($resolved);
|
||||
$config['source_mode'] = 'user-filesystem';
|
||||
$config['access_user'] = $accessUser;
|
||||
|
||||
$json = json_encode($config, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($json)) user_scope_fail('Benutzerbezogene Connector-Konfiguration konnte nicht serialisiert werden.');
|
||||
$escaped = $db->real_escape_string($json);
|
||||
if (!$db->query("UPDATE `{$table}` SET config_json='{$escaped}' WHERE id={$id} LIMIT 1"))
|
||||
{
|
||||
user_scope_fail('Benutzerbezogene Connector-Konfiguration konnte nicht gespeichert werden: '.$db->error);
|
||||
}
|
||||
|
||||
$base = $configDir.'/connection-'.$id;
|
||||
$storagePath = $base.'.storages.tsv';
|
||||
$configPath = $base.'.conf';
|
||||
if (!is_file($configPath)) continue;
|
||||
|
||||
$lines = array('# storage_id<TAB>source_prefix<TAB>local_mount<TAB>include_prefix');
|
||||
foreach ($config['storages'] as $storage)
|
||||
{
|
||||
$lines[] = (string)$storage['storage_id']."\t".(string)$storage['source_prefix']."\t".(string)$storage['local_mount']."\t".(string)$storage['include_prefix'];
|
||||
}
|
||||
file_put_contents($storagePath, implode("\n", $lines)."\n", LOCK_EX);
|
||||
chmod($storagePath, 0600);
|
||||
|
||||
$existing = file($configPath, FILE_IGNORE_NEW_LINES);
|
||||
if (!is_array($existing)) user_scope_fail('Runtime-Konfiguration konnte nicht gelesen werden.');
|
||||
$filtered = array_values(array_filter($existing, function($line) {
|
||||
return strpos($line, 'SOURCE_MODE=') !== 0 && strpos($line, 'ACCESS_USER=') !== 0;
|
||||
}));
|
||||
$filtered[] = 'SOURCE_MODE=user-filesystem';
|
||||
$filtered[] = 'ACCESS_USER='.user_scope_shell_value($accessUser);
|
||||
file_put_contents($configPath, implode("\n", $filtered)."\n", LOCK_EX);
|
||||
chmod($configPath, 0600);
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
@unlink($configDir.'/connection-'.$id.'.conf');
|
||||
user_scope_write_status($piwigoRoot, $id, $e->getMessage());
|
||||
fwrite(STDERR, 'NC Connector #'.$id.': '.$e->getMessage()."\n");
|
||||
}
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
fwrite(STDERR, 'NC Connector User Scope: '.$e->getMessage()."\n");
|
||||
exit(1);
|
||||
}
|
||||
259
runtime/reconcile.php
Normal file
259
runtime/reconcile.php
Normal file
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
if (PHP_SAPI !== 'cli')
|
||||
{
|
||||
fwrite(STDERR, "CLI only\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function fail_reconcile($message)
|
||||
{
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
function decrypt_reconcile($blob, $hexKey)
|
||||
{
|
||||
if (!preg_match('/^[a-f0-9]{64}$/', (string)$hexKey)) fail_reconcile('Connector-Schluessel ist ungueltig.');
|
||||
$outer = base64_decode(trim((string)$blob), true);
|
||||
$payload = is_string($outer) ? json_decode($outer, true) : null;
|
||||
if (!is_array($payload) || (int)($payload['v'] ?? 0) !== 1) fail_reconcile('Connector-Zugangsdaten haben ein unbekanntes Format.');
|
||||
$iv = base64_decode((string)($payload['iv'] ?? ''), true);
|
||||
$tag = base64_decode((string)($payload['tag'] ?? ''), true);
|
||||
$cipher = base64_decode((string)($payload['data'] ?? ''), true);
|
||||
$plain = openssl_decrypt($cipher, 'aes-256-gcm', hex2bin($hexKey), OPENSSL_RAW_DATA, $iv, $tag);
|
||||
if ($plain === false || $plain === '') fail_reconcile('Connector-Zugangsdaten konnten nicht entschluesselt werden.');
|
||||
$decoded = json_decode($plain, true);
|
||||
if (!is_array($decoded))
|
||||
{
|
||||
return array('db_password'=>$plain,'piwigo_user'=>'','piwigo_password'=>'','api_key_id'=>'','api_key_secret'=>'');
|
||||
}
|
||||
return array(
|
||||
'db_password'=>(string)($decoded['db_password'] ?? ''),
|
||||
'piwigo_user'=>(string)($decoded['piwigo_user'] ?? ''),
|
||||
'piwigo_password'=>(string)($decoded['piwigo_password'] ?? ''),
|
||||
'api_key_id'=>(string)($decoded['api_key_id'] ?? ''),
|
||||
'api_key_secret'=>(string)($decoded['api_key_secret'] ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
function encrypt_reconcile(array $credentials, $hexKey)
|
||||
{
|
||||
$plain = json_encode(array(
|
||||
'v'=>2,
|
||||
'db_password'=>(string)$credentials['db_password'],
|
||||
'piwigo_user'=>(string)$credentials['piwigo_user'],
|
||||
'piwigo_password'=>(string)$credentials['piwigo_password'],
|
||||
'api_key_id'=>(string)$credentials['api_key_id'],
|
||||
'api_key_secret'=>(string)$credentials['api_key_secret'],
|
||||
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($plain)) fail_reconcile('Connector-Zugangsdaten konnten nicht serialisiert werden.');
|
||||
$iv = random_bytes(12);
|
||||
$tag = '';
|
||||
$cipher = openssl_encrypt($plain, 'aes-256-gcm', hex2bin($hexKey), OPENSSL_RAW_DATA, $iv, $tag);
|
||||
if ($cipher === false) fail_reconcile('Connector-Zugangsdaten konnten nicht verschluesselt werden.');
|
||||
return base64_encode(json_encode(array(
|
||||
'v'=>1,
|
||||
'iv'=>base64_encode($iv),
|
||||
'tag'=>base64_encode($tag),
|
||||
'data'=>base64_encode($cipher),
|
||||
)));
|
||||
}
|
||||
|
||||
function write_runtime_status($piwigoRoot, $id, $stateDir, $message)
|
||||
{
|
||||
$payload = json_encode(array(
|
||||
'state'=>'error',
|
||||
'message'=>'Runtime-Vorbereitung fehlgeschlagen',
|
||||
'timestamp'=>time(),
|
||||
'auth_mode'=>'failed',
|
||||
'api'=>array('state'=>'not_run','message'=>''),
|
||||
'fallback'=>array('state'=>'not_run','message'=>''),
|
||||
'error_detail'=>(string)$message,
|
||||
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($payload)) return;
|
||||
$targets = array(rtrim($piwigoRoot, '/').'/_data/bratonien-tools/nc-connector-status/connection-'.$id.'.json');
|
||||
if ($stateDir !== '') $targets[] = rtrim($stateDir, '/').'/connector-status.json';
|
||||
foreach ($targets as $target)
|
||||
{
|
||||
$dir = dirname($target);
|
||||
if (!is_dir($dir)) @mkdir($dir, 0755, true);
|
||||
@file_put_contents($target, $payload."\n", LOCK_EX);
|
||||
@chmod($target, 0644);
|
||||
}
|
||||
}
|
||||
|
||||
function sql_reconcile(mysqli $db, $value)
|
||||
{
|
||||
return $db->real_escape_string((string)$value);
|
||||
}
|
||||
|
||||
$pluginRoot = dirname(__DIR__);
|
||||
$piwigoRoot = dirname($pluginRoot, 2);
|
||||
$dbConfig = $piwigoRoot.'/local/config/database.inc.php';
|
||||
$configDir = '/etc/bratonien-tools/nc-connector';
|
||||
$stateRoot = '/var/lib/bratonien-tools/nc-connector';
|
||||
|
||||
try
|
||||
{
|
||||
if (!is_readable($dbConfig)) fail_reconcile('Piwigo-Datenbankkonfiguration ist nicht lesbar: '.$dbConfig);
|
||||
$conf = array();
|
||||
$prefixeTable = 'piwigo_';
|
||||
require $dbConfig;
|
||||
foreach (array('db_host','db_user','db_password','db_base') as $key)
|
||||
{
|
||||
if (!isset($conf[$key])) fail_reconcile('Piwigo-Datenbankkonfiguration ist unvollstaendig: '.$key);
|
||||
}
|
||||
|
||||
$db = new mysqli($conf['db_host'], $conf['db_user'], $conf['db_password'], $conf['db_base']);
|
||||
if ($db->connect_errno) fail_reconcile('Piwigo-Datenbank ist nicht erreichbar: '.$db->connect_error);
|
||||
$db->set_charset('utf8mb4');
|
||||
|
||||
$keyResult = $db->query("SELECT value FROM `{$prefixeTable}config` WHERE param='bratonien_nc_connector_secret' LIMIT 1");
|
||||
if (!$keyResult || !$keyResult->num_rows) fail_reconcile('Connector-Schluessel wurde nicht gefunden.');
|
||||
$hexKey = trim((string)$keyResult->fetch_assoc()['value']);
|
||||
|
||||
$table = $prefixeTable.'bratonien_tools_nc_connections';
|
||||
$rows = $db->query("SELECT id,name,adapter,enabled,takeover_state,config_json,secret_blob FROM `{$table}` ORDER BY id");
|
||||
if (!$rows) fail_reconcile('Connector-Verbindungen konnten nicht gelesen werden: '.$db->error);
|
||||
|
||||
if (!is_dir($configDir) && !mkdir($configDir, 0700, true)) fail_reconcile('Runtime-Konfigurationsverzeichnis konnte nicht angelegt werden.');
|
||||
chmod($configDir, 0700);
|
||||
|
||||
while ($row = $rows->fetch_assoc())
|
||||
{
|
||||
$id = (int)$row['id'];
|
||||
$config = json_decode((string)$row['config_json'], true);
|
||||
if (!is_array($config)) $config = array();
|
||||
$stateDir = rtrim((string)($config['state_dir'] ?? ''), '/');
|
||||
if ($stateDir === '') $stateDir = $stateRoot.'/connection-'.$id;
|
||||
|
||||
try
|
||||
{
|
||||
if ((string)$row['adapter'] !== 'local') continue;
|
||||
|
||||
$isActive = (int)$row['enabled'] === 1 && (string)$row['takeover_state'] === 'active';
|
||||
$wizardConnection = (string)($config['origin'] ?? '') === 'native'
|
||||
&& trim((string)($config['nextcloud_url'] ?? '')) !== ''
|
||||
&& trim((string)($config['showcase_user'] ?? '')) !== '';
|
||||
$verification = isset($config['verification']) && is_array($config['verification']) ? $config['verification'] : null;
|
||||
$verificationFailed = is_array($verification) && empty($verification['ok']);
|
||||
|
||||
if (!$isActive && (!$wizardConnection || $verificationFailed)) continue;
|
||||
|
||||
foreach (array('host','port','database','user','source_view','activity_view','gallery_root') as $key)
|
||||
{
|
||||
if (trim((string)($config[$key] ?? '')) === '') fail_reconcile('Konfiguration unvollstaendig: '.$key.' fehlt.');
|
||||
}
|
||||
$storages = isset($config['storages']) && is_array($config['storages']) ? $config['storages'] : array();
|
||||
if (!$storages) fail_reconcile('Keine Storage-Zuordnungen gespeichert.');
|
||||
|
||||
$credentials = decrypt_reconcile((string)$row['secret_blob'], $hexKey);
|
||||
if ($credentials['db_password'] === '') fail_reconcile('Datenbankpasswort fehlt.');
|
||||
|
||||
if (!$isActive && !array_key_exists('api_enabled', $config))
|
||||
{
|
||||
// Alte Wizard-Verbindungen ohne API, aber mit gespeichertem Fallback,
|
||||
// duerfen niemals den globalen API-Key einer anderen Verbindung erben.
|
||||
$hasFallback = $credentials['piwigo_user'] !== '' && $credentials['piwigo_password'] !== '';
|
||||
if (!$hasFallback) fail_reconcile('Die Verbindung besitzt weder einen eigenen API-Zugang noch einen eigenen Fallback.');
|
||||
$credentials['api_key_id'] = '';
|
||||
$credentials['api_key_secret'] = '';
|
||||
$config['piwigo_auth'] = 'connection-scoped';
|
||||
$config['api_enabled'] = false;
|
||||
$row['secret_blob'] = encrypt_reconcile($credentials, $hexKey);
|
||||
}
|
||||
|
||||
$connectionScoped = (string)($config['piwigo_auth'] ?? '') === 'connection-scoped' || array_key_exists('api_enabled', $config);
|
||||
if ($connectionScoped)
|
||||
{
|
||||
$apiAvailable = !empty($config['api_enabled']) && $credentials['api_key_id'] !== '' && $credentials['api_key_secret'] !== '';
|
||||
$fallbackAvailable = $credentials['piwigo_user'] !== '' && $credentials['piwigo_password'] !== '';
|
||||
if (!$apiAvailable && !$fallbackAvailable) fail_reconcile('Kein verbindungseigener Piwigo-Zugang gespeichert.');
|
||||
}
|
||||
|
||||
if (!is_dir($stateDir) && !mkdir($stateDir, 0750, true)) fail_reconcile('State-Verzeichnis konnte nicht angelegt werden.');
|
||||
chmod($stateDir, 0750);
|
||||
|
||||
$base = $configDir.'/connection-'.$id;
|
||||
$dbPasswordPath = $base.'.db-password';
|
||||
$piwigoPasswordPath = $base.'.piwigo-password';
|
||||
$storagePath = $base.'.storages.tsv';
|
||||
$configPath = $base.'.conf';
|
||||
$statusFile = $stateDir.'/connector-status.json';
|
||||
|
||||
file_put_contents($dbPasswordPath, $credentials['db_password']."\n", LOCK_EX);
|
||||
chmod($dbPasswordPath, 0600);
|
||||
|
||||
$fallbackAvailable = $credentials['piwigo_user'] !== '' && $credentials['piwigo_password'] !== '';
|
||||
if ($fallbackAvailable)
|
||||
{
|
||||
file_put_contents($piwigoPasswordPath, $credentials['piwigo_password']."\n", LOCK_EX);
|
||||
chmod($piwigoPasswordPath, 0600);
|
||||
}
|
||||
else
|
||||
{
|
||||
@unlink($piwigoPasswordPath);
|
||||
}
|
||||
|
||||
$storageLines = array('# storage_id<TAB>source_prefix<TAB>local_mount<TAB>include_prefix');
|
||||
foreach ($storages as $storage)
|
||||
{
|
||||
$storageLines[] = (string)($storage['storage_id'] ?? '')."\t"
|
||||
.trim((string)($storage['source_prefix'] ?? ''), '/')."\t"
|
||||
.(string)($storage['local_mount'] ?? '')."\t"
|
||||
.trim((string)($storage['include_prefix'] ?? ''), '/');
|
||||
}
|
||||
file_put_contents($storagePath, implode("\n", $storageLines)."\n", LOCK_EX);
|
||||
chmod($storagePath, 0600);
|
||||
|
||||
$lines = array(
|
||||
'PIWIGO_ROOT='.$piwigoRoot,
|
||||
'GALLERY_ROOT='.(string)$config['gallery_root'],
|
||||
'STATE_DIR='.$stateDir,
|
||||
'STATUS_FILE='.$statusFile,
|
||||
'NC_DB_HOST='.(string)$config['host'],
|
||||
'NC_DB_PORT='.(string)$config['port'],
|
||||
'NC_DB_NAME='.(string)$config['database'],
|
||||
'NC_DB_USER='.(string)$config['user'],
|
||||
'NC_DB_VIEW='.(string)$config['source_view'],
|
||||
'NC_ACTIVITY_VIEW='.(string)$config['activity_view'],
|
||||
'NC_DB_PASSWORD_FILE='.$dbPasswordPath,
|
||||
'STORAGE_CONFIG='.$storagePath,
|
||||
'QUIET_SECONDS='.(int)($config['quiet_seconds'] ?? 120),
|
||||
'MAX_WAIT_SECONDS='.(int)($config['max_wait_seconds'] ?? 900),
|
||||
'FULL_SYNC_SECONDS='.(int)($config['full_sync_seconds'] ?? 86400),
|
||||
'PIWIGO_SYNC_ENABLED=1',
|
||||
);
|
||||
if ($fallbackAvailable)
|
||||
{
|
||||
$lines[] = 'PIWIGO_SYNC_USER='.$credentials['piwigo_user'];
|
||||
$lines[] = 'PIWIGO_SYNC_PASSWORD_FILE='.$piwigoPasswordPath;
|
||||
}
|
||||
file_put_contents($configPath, implode("\n", $lines)."\n", LOCK_EX);
|
||||
chmod($configPath, 0600);
|
||||
|
||||
$config['state_dir'] = $stateDir;
|
||||
$config['status_file'] = $statusFile;
|
||||
$config['runtime'] = array('mode'=>'shared-runner','config'=>$configPath,'reconciled_at'=>date('Y-m-d H:i:s'));
|
||||
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($json)) fail_reconcile('Connector-Konfiguration konnte nicht serialisiert werden.');
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$sql = "UPDATE `{$table}` SET enabled=1,takeover_state='active',config_json='".sql_reconcile($db,$json)."',secret_blob='".sql_reconcile($db,$row['secret_blob'])."',updated='".sql_reconcile($db,$now)."' WHERE id=".$id;
|
||||
if (!$db->query($sql)) fail_reconcile('Runtime-Status konnte nicht gespeichert werden: '.$db->error);
|
||||
|
||||
if (!$isActive) echo "NC Connector: Verbindung #{$id} ({$row['name']}) automatisch in die gemeinsame Runtime uebernommen.\n";
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
write_runtime_status($piwigoRoot, $id, $stateDir, $e->getMessage());
|
||||
fwrite(STDERR, "NC Connector #{$id}: ".$e->getMessage()."\n");
|
||||
}
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
catch (Throwable $e)
|
||||
{
|
||||
fwrite(STDERR, "NC Connector Reconcile: ".$e->getMessage()."\n");
|
||||
exit(1);
|
||||
}
|
||||
@@ -4,6 +4,17 @@ set -Eeuo pipefail
|
||||
CONFIG_DIR="/etc/bratonien-tools/nc-connector"
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
shopt -s nullglob
|
||||
|
||||
if ! php "$SCRIPT_DIR/reconcile.php"; then
|
||||
echo "NC Connector: gespeicherte Verbindungen konnten nicht mit der Runtime abgeglichen werden." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! php "$SCRIPT_DIR/reconcile-user-scope.php"; then
|
||||
echo "NC Connector: benutzerbezogene Verbindungen konnten nicht sicher vorbereitet werden." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
configs=("$CONFIG_DIR"/connection-*.conf)
|
||||
|
||||
if [[ ${#configs[@]} -eq 0 ]]; then
|
||||
|
||||
158
runtime/sync.sh
158
runtime/sync.sh
@@ -11,6 +11,17 @@ source "$CONFIG_FILE"
|
||||
|
||||
NC_ACTIVITY_VIEW="${NC_ACTIVITY_VIEW:-piwigo_showcase_activity}"
|
||||
NC_DB_VIEW="${NC_DB_VIEW:-piwigo_showcase_sources}"
|
||||
SOURCE_MODE="${SOURCE_MODE:-showcase-view}"
|
||||
ACCESS_USER="${ACCESS_USER:-}"
|
||||
|
||||
case "$SOURCE_MODE" in
|
||||
showcase-view|user-filesystem) ;;
|
||||
*) echo "Unbekannter SOURCE_MODE: $SOURCE_MODE" >&2; exit 1 ;;
|
||||
esac
|
||||
if [[ "$SOURCE_MODE" == "user-filesystem" && -z "$ACCESS_USER" ]]; then
|
||||
echo "ACCESS_USER fehlt fuer die benutzerbezogene Verbindung." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$PIWIGO_SYNC_OVERRIDE_VALUE" ]]; then
|
||||
case "$PIWIGO_SYNC_OVERRIDE_VALUE" in
|
||||
@@ -50,6 +61,12 @@ API_MESSAGE=""
|
||||
FALLBACK_STATE="not_run"
|
||||
FALLBACK_MESSAGE=""
|
||||
ERROR_DETAIL=""
|
||||
ERROR_STAGE="Vorbereitung"
|
||||
ERROR_MESSAGE="Synchronisierung fehlgeschlagen"
|
||||
|
||||
compact_output() {
|
||||
tail -n 8 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//'
|
||||
}
|
||||
|
||||
write_status() {
|
||||
local state="$1" message="$2"
|
||||
@@ -86,11 +103,48 @@ PY
|
||||
}
|
||||
|
||||
failure() {
|
||||
[[ -n "$ERROR_DETAIL" ]] || ERROR_DETAIL="Synchronisierung wurde durch einen technischen Fehler abgebrochen."
|
||||
write_status error "Synchronisierung fehlgeschlagen"
|
||||
local exit_code="${1:-1}"
|
||||
local failed_command="${2:-unbekannt}"
|
||||
local failed_line="${3:-?}"
|
||||
trap - ERR
|
||||
if [[ -z "$ERROR_DETAIL" ]]; then
|
||||
ERROR_DETAIL="Schritt: $ERROR_STAGE. Exit-Code: $exit_code. Zeile: $failed_line. Fehlgeschlagener Befehl: $failed_command"
|
||||
fi
|
||||
write_status error "$ERROR_MESSAGE"
|
||||
exit "$exit_code"
|
||||
}
|
||||
trap failure ERR
|
||||
trap 'failure $? "$BASH_COMMAND" "$LINENO"' ERR
|
||||
|
||||
run_stage() {
|
||||
local stage="$1"
|
||||
local message="$2"
|
||||
shift 2
|
||||
local output=""
|
||||
local exit_code=0
|
||||
|
||||
ERROR_STAGE="$stage"
|
||||
ERROR_MESSAGE="$message"
|
||||
if output="$("$@" 2>&1)"; then
|
||||
exit_code=0
|
||||
else
|
||||
exit_code=$?
|
||||
fi
|
||||
if [[ -n "$output" ]]; then
|
||||
printf '%s\n' "$output"
|
||||
fi
|
||||
if [[ "$exit_code" -ne 0 ]]; then
|
||||
ERROR_DETAIL="Schritt: $stage. Exit-Code: $exit_code."
|
||||
if [[ -n "$output" ]]; then
|
||||
ERROR_DETAIL+=" Ausgabe: $(printf '%s\n' "$output" | compact_output)"
|
||||
fi
|
||||
trap - ERR
|
||||
write_status error "$message"
|
||||
exit "$exit_code"
|
||||
fi
|
||||
}
|
||||
|
||||
ERROR_STAGE="Lokalen Zustand prüfen"
|
||||
ERROR_MESSAGE="Lokaler Connector-Zustand konnte nicht geprüft werden"
|
||||
NEEDS_LOCAL_REPAIR=0
|
||||
if [[ ! -s "$MAP_FILE" ]]; then
|
||||
NEEDS_LOCAL_REPAIR=1
|
||||
@@ -122,18 +176,29 @@ if [[ "$NEEDS_LOCAL_REPAIR" == "0" && "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; the
|
||||
[[ "$PIWIGO_ALBUMS_INTACT" == "0" ]] || NEEDS_LOCAL_REPAIR=1
|
||||
fi
|
||||
|
||||
if [[ "$NEEDS_LOCAL_REPAIR" == "1" ]]; then
|
||||
if [[ "$SOURCE_MODE" == "user-filesystem" ]]; then
|
||||
# Benutzerbezogene Verbindungen werden pro Timerlauf aus ihrem eigenen
|
||||
# lokalen Home-Dateibaum aufgebaut. Die alte globale Showcase-Aktivitaets-
|
||||
# View darf hier weder Daten anderer Benutzer steuern noch Aenderungen
|
||||
# dieses Benutzers verschlucken.
|
||||
GATE_RESULT=0
|
||||
elif [[ "$NEEDS_LOCAL_REPAIR" == "1" ]]; then
|
||||
GATE_RESULT=0
|
||||
else
|
||||
if python3 "$SCRIPT_DIR/lib/activity_gate.py" check \
|
||||
ERROR_STAGE="Nextcloud-Aktivität prüfen"
|
||||
ERROR_MESSAGE="Nextcloud-Aktivität konnte nicht geprüft werden"
|
||||
if GATE_OUTPUT="$(python3 "$SCRIPT_DIR/lib/activity_gate.py" check \
|
||||
--state "$ACTIVITY_STATE" --host "$NC_DB_HOST" --port "$NC_DB_PORT" \
|
||||
--database "$NC_DB_NAME" --user "$NC_DB_USER" --password-file "$NC_DB_PASSWORD_FILE" \
|
||||
--view "$NC_ACTIVITY_VIEW" --source-view "$NC_DB_VIEW" \
|
||||
--quiet "$QUIET_SECONDS" --max-wait "$MAX_WAIT_SECONDS" --full-after "$FULL_SYNC_SECONDS"; then
|
||||
--quiet "$QUIET_SECONDS" --max-wait "$MAX_WAIT_SECONDS" --full-after "$FULL_SYNC_SECONDS" 2>&1)"; then
|
||||
GATE_RESULT=0
|
||||
else
|
||||
GATE_RESULT=$?
|
||||
fi
|
||||
if [[ -n "$GATE_OUTPUT" ]]; then
|
||||
printf '%s\n' "$GATE_OUTPUT"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$GATE_RESULT" == "3" ]]; then
|
||||
@@ -141,24 +206,49 @@ if [[ "$GATE_RESULT" == "3" ]]; then
|
||||
write_status ok "Keine Änderungen gefunden"
|
||||
exit 0
|
||||
fi
|
||||
[[ "$GATE_RESULT" == "0" ]] || { ERROR_DETAIL="Activity-Gate fehlgeschlagen (Exit-Code $GATE_RESULT)."; exit "$GATE_RESULT"; }
|
||||
if [[ "$GATE_RESULT" != "0" ]]; then
|
||||
ERROR_DETAIL="Schritt: Nextcloud-Aktivität prüfen. Exit-Code: $GATE_RESULT."
|
||||
if [[ -n "${GATE_OUTPUT:-}" ]]; then
|
||||
ERROR_DETAIL+=" Ausgabe: $(printf '%s\n' "$GATE_OUTPUT" | compact_output)"
|
||||
fi
|
||||
trap - ERR
|
||||
write_status error "Nextcloud-Aktivität konnte nicht geprüft werden"
|
||||
exit "$GATE_RESULT"
|
||||
fi
|
||||
|
||||
python3 "$SCRIPT_DIR/lib/build_manifest.py" \
|
||||
--host "$NC_DB_HOST" --port "$NC_DB_PORT" --database "$NC_DB_NAME" --user "$NC_DB_USER" \
|
||||
--password-file "$NC_DB_PASSWORD_FILE" --view "$NC_DB_VIEW" \
|
||||
--storage-config "$STORAGE_CONFIG" --output "$MANIFEST"
|
||||
if [[ "$SOURCE_MODE" == "user-filesystem" ]]; then
|
||||
run_stage \
|
||||
"Benutzer-Dateiliste lesen" \
|
||||
"Dateiliste des Nextcloud-Benutzers konnte nicht erstellt werden" \
|
||||
python3 "$SCRIPT_DIR/lib/build_user_manifest.py" \
|
||||
--storage-config "$STORAGE_CONFIG" --output "$MANIFEST"
|
||||
else
|
||||
run_stage \
|
||||
"Nextcloud-Dateiliste lesen" \
|
||||
"Dateiliste aus Nextcloud konnte nicht erstellt werden" \
|
||||
python3 "$SCRIPT_DIR/lib/build_manifest.py" \
|
||||
--host "$NC_DB_HOST" --port "$NC_DB_PORT" --database "$NC_DB_NAME" --user "$NC_DB_USER" \
|
||||
--password-file "$NC_DB_PASSWORD_FILE" --view "$NC_DB_VIEW" \
|
||||
--storage-config "$STORAGE_CONFIG" --output "$MANIFEST"
|
||||
fi
|
||||
|
||||
python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
|
||||
run_stage \
|
||||
"Lokalen Galeriebaum aktualisieren" \
|
||||
"Lokaler Galeriebaum konnte nicht aktualisiert werden" \
|
||||
python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
|
||||
--manifest "$MANIFEST" --destination "$GALLERY_ROOT" --state "$MAP_FILE"
|
||||
|
||||
if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
|
||||
set +e
|
||||
PIWIGO_OUTPUT="$(php "$SCRIPT_DIR/lib/piwigo-sync.php" \
|
||||
ERROR_STAGE="Piwigo synchronisieren"
|
||||
ERROR_MESSAGE="Piwigo-Synchronisierung fehlgeschlagen"
|
||||
if PIWIGO_OUTPUT="$(php "$SCRIPT_DIR/lib/piwigo-sync.php" \
|
||||
--piwigo-root="$PIWIGO_ROOT" \
|
||||
--connection-id="$CONNECTION_ID" \
|
||||
--base-url="http://127.0.0.1" 2>&1)"
|
||||
PIWIGO_EXIT=$?
|
||||
set -e
|
||||
--base-url="http://127.0.0.1" 2>&1)"; then
|
||||
PIWIGO_EXIT=0
|
||||
else
|
||||
PIWIGO_EXIT=$?
|
||||
fi
|
||||
printf '%s\n' "$PIWIGO_OUTPUT"
|
||||
|
||||
if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then
|
||||
@@ -189,23 +279,45 @@ if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
|
||||
fi
|
||||
|
||||
if [[ "$PIWIGO_EXIT" -ne 0 ]]; then
|
||||
ERROR_DETAIL="$(tail -n 3 <<<"$PIWIGO_OUTPUT" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/[[:space:]]$//')"
|
||||
ERROR_DETAIL="Schritt: Piwigo synchronisieren. Exit-Code: $PIWIGO_EXIT. Ausgabe: $(printf '%s\n' "$PIWIGO_OUTPUT" | compact_output)"
|
||||
trap - ERR
|
||||
write_status error "Synchronisierung fehlgeschlagen"
|
||||
write_status error "Piwigo-Synchronisierung fehlgeschlagen"
|
||||
exit "$PIWIGO_EXIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
python3 "$SCRIPT_DIR/lib/activity_gate.py" commit \
|
||||
--state "$ACTIVITY_STATE" --host "$NC_DB_HOST" --port "$NC_DB_PORT" \
|
||||
--database "$NC_DB_NAME" --user "$NC_DB_USER" --password-file "$NC_DB_PASSWORD_FILE" \
|
||||
--view "$NC_ACTIVITY_VIEW" --source-view "$NC_DB_VIEW"
|
||||
if [[ "$SOURCE_MODE" != "user-filesystem" ]]; then
|
||||
ERROR_STAGE="Aktivitätsstand speichern"
|
||||
ERROR_MESSAGE="Aktivitätsstand konnte nicht gespeichert werden"
|
||||
if COMMIT_OUTPUT="$(python3 "$SCRIPT_DIR/lib/activity_gate.py" commit \
|
||||
--state "$ACTIVITY_STATE" --host "$NC_DB_HOST" --port "$NC_DB_PORT" \
|
||||
--database "$NC_DB_NAME" --user "$NC_DB_USER" --password-file "$NC_DB_PASSWORD_FILE" \
|
||||
--view "$NC_ACTIVITY_VIEW" --source-view "$NC_DB_VIEW" 2>&1)"; then
|
||||
COMMIT_EXIT=0
|
||||
else
|
||||
COMMIT_EXIT=$?
|
||||
fi
|
||||
if [[ -n "$COMMIT_OUTPUT" ]]; then
|
||||
printf '%s\n' "$COMMIT_OUTPUT"
|
||||
fi
|
||||
if [[ "$COMMIT_EXIT" -ne 0 ]]; then
|
||||
ERROR_DETAIL="Schritt: Aktivitätsstand speichern. Exit-Code: $COMMIT_EXIT."
|
||||
if [[ -n "$COMMIT_OUTPUT" ]]; then
|
||||
ERROR_DETAIL+=" Ausgabe: $(printf '%s\n' "$COMMIT_OUTPUT" | compact_output)"
|
||||
fi
|
||||
trap - ERR
|
||||
write_status error "Aktivitätsstand konnte nicht gespeichert werden"
|
||||
exit "$COMMIT_EXIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
trap - ERR
|
||||
if [[ "$AUTH_MODE" == "fallback" ]]; then
|
||||
write_status warning "Synchronisierung erfolgreich über Fallback; API war nicht nutzbar"
|
||||
elif [[ "$AUTH_MODE" == "api" ]]; then
|
||||
write_status ok "Synchronisierung erfolgreich über API"
|
||||
elif [[ "$SOURCE_MODE" == "user-filesystem" ]]; then
|
||||
write_status ok "Benutzerbezogene Synchronisierung erfolgreich"
|
||||
else
|
||||
write_status ok "Synchronisierung erfolgreich"
|
||||
fi
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
|
||||
var lastRunNode=valueNodeForLabel('Letzter Lauf');
|
||||
var nextRunNode=valueNodeForLabel('Nächster Lauf');
|
||||
var lastResultNode=valueNodeForLabel('Letztes Ergebnis');
|
||||
|
||||
function poll(){
|
||||
fetch(endpoint+'?_='+Date.now(),{credentials:'same-origin',cache:'no-store'})
|
||||
@@ -102,6 +103,7 @@
|
||||
.then(function(data){
|
||||
if(lastRunNode&&data.last_run_label)lastRunNode.textContent=data.last_run_label;
|
||||
if(nextRunNode&&data.next_run_label)nextRunNode.textContent=data.next_run_label;
|
||||
if(lastResultNode&&typeof data.message==='string'&&data.message!=='')lastResultNode.textContent=data.message;
|
||||
})
|
||||
.catch(function(){});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user