mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
admin: backup logs via parameters.php, nextcloud secondary backup
- surface backup/cleanup cron logs + backup freshness status - email xamxam@erg.be when SQLite backups go stale (backup watchdog) - sync SQLite snapshots to Nextcloud WebDAV + remote-freshness watchdog - precise retention pruning, manual sync in check recipe, and Nextcloud-sync docs
This commit is contained in:
@@ -32,7 +32,23 @@ sqlite3 "$DB_PATH" ".backup $TMP_SNAPSHOT"
|
||||
gzip -c "$TMP_SNAPSHOT" > "$BACKUP_FILE"
|
||||
rm -f "$TMP_SNAPSHOT"
|
||||
|
||||
# Prune old backups
|
||||
find "$BACKUP_DIR" -name "*.db.gz" -mtime "+${RETENTION_DAYS}" -delete 2>/dev/null || true
|
||||
# Prune old backups.
|
||||
# Use filename timestamps (deterministic) rather than -mtime, which has
|
||||
# day-rounding ambiguity (-mtime +N means N+1 days). Files are named
|
||||
# db-YYYY-MM-DDTHH-MM-SS.db.gz, so we delete anything whose date is more
|
||||
# than RETENTION_DAYS in the past.
|
||||
RETENTION_SECONDS=$((RETENTION_DAYS * 86400))
|
||||
NOW_EPOCH=$(date +%s)
|
||||
for f in "$BACKUP_DIR"/db-*.db.gz; do
|
||||
[ -e "$f" ] || continue
|
||||
base=$(basename "$f" .db.gz) # db-YYYY-MM-DDTHH-MM-SS
|
||||
stamp=${base#db-} # YYYY-MM-DDTHH-MM-SS
|
||||
# Reformat YYYY-MM-DDTHH-MM-SS → YYYY-MM-DD HH:MM:SS for date(1).
|
||||
named=$(echo "$stamp" | sed 's/T/ /; s/-/:/3g') || continue
|
||||
ts=$(date -d "$named" +%s 2>/dev/null) || continue
|
||||
if [ $((NOW_EPOCH - ts)) -gt $RETENTION_SECONDS ]; then
|
||||
rm -f "$f"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup written: $BACKUP_FILE ($(du -h "$BACKUP_FILE" | cut -f1))"
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* backup-watchdog.php — alert by email when SQLite backups go stale.
|
||||
*
|
||||
* Checks the newest db-*.db.gz in /var/backups/xamxam and, if it is older
|
||||
* than the staleness threshold, sends an alert via the app's SMTP relay to
|
||||
* xamxam@erg.be (or the configured notify_email). A state file prevents
|
||||
* repeated alerts for the same incident: an email is only sent on the
|
||||
* transition fresh → stale, not on every run while backups remain down.
|
||||
*
|
||||
* Usage:
|
||||
* php scripts/backup-watchdog.php # 2h staleness threshold
|
||||
* STALE_AFTER_SECONDS=86400 php scripts/backup-watchdog.php
|
||||
*
|
||||
* Expected to run from cron (after each backup cron), e.g.:
|
||||
* 15 * * * * www-data php /usr/local/bin/backup-watchdog.php >> /var/log/xamxam-backup-$(date +\%Y-\%m-\%d).log 2>&1
|
||||
*
|
||||
* Exit codes: 0 ok (or alert sent), 1 error.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Resolve APP_ROOT robustly — same convention as the other CLI scripts here.
|
||||
$prodRoot = '/var/www/xamxam';
|
||||
if (is_dir($prodRoot . '/src') && is_file($prodRoot . '/src/Database.php')) {
|
||||
define('APP_ROOT', $prodRoot);
|
||||
} else {
|
||||
define('APP_ROOT', dirname(__DIR__) . '/app');
|
||||
}
|
||||
|
||||
const BACKUP_DIR = '/var/backups/xamxam';
|
||||
const STATE_FILE = BACKUP_DIR . '/.last_backup_alert';
|
||||
const ALERT_EMAIL = 'xamxam@erg.be';
|
||||
const WEBDAV_BASE = 'https://cloud.erg.school/remote.php/dav/files/xamxam%40erg.be/XAMXAM-BCK';
|
||||
const REMOTE_PREFIX = 'xamxam-db-';
|
||||
const REMOTE_STALE_SECONDS = 172800; // 48h — daily sync, allow one missed day
|
||||
|
||||
// Autoload dependencies (PHPMailer, etc.) from composer.
|
||||
$autoloads = [
|
||||
APP_ROOT . '/../vendor/autoload.php', // repo root / vendor
|
||||
APP_ROOT . '/vendor/autoload.php', // app / vendor
|
||||
];
|
||||
foreach ($autoloads as $a) {
|
||||
if (is_file($a)) {
|
||||
require_once $a;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Read-only probe: do NOT run schema migrations against the live DB just to
|
||||
// check backup freshness. If DatabaseMigrations isn't loaded, no-op it so the
|
||||
// Database constructor only opens a PDO connection.
|
||||
if (!class_exists('DatabaseMigrations', false)) {
|
||||
class DatabaseMigrations
|
||||
{
|
||||
public function run(): void {}
|
||||
}
|
||||
}
|
||||
|
||||
require_once APP_ROOT . '/src/Crypto.php';
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/SmtpRelay.php';
|
||||
|
||||
// Staleness threshold: default 2 hours (matches the admin system-page status).
|
||||
$staleAfterSeconds = (int) (getenv('STALE_AFTER_SECONDS') ?: 7200);
|
||||
if ($staleAfterSeconds < 60) {
|
||||
$staleAfterSeconds = 7200;
|
||||
}
|
||||
|
||||
$newestMtime = 0;
|
||||
$newestName = null;
|
||||
$files = glob(BACKUP_DIR . '/db-*.db.gz') ?: [];
|
||||
foreach ($files as $f) {
|
||||
$m = @filemtime($f);
|
||||
if ($m !== false && $m > $newestMtime) {
|
||||
$newestMtime = $m;
|
||||
$newestName = $f;
|
||||
}
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$age = $newestMtime > 0 ? ($now - $newestMtime) : null;
|
||||
$localDown = ($age === null) || ($age > $staleAfterSeconds);
|
||||
|
||||
// Remote (Nextcloud) staleness — only relevant if we have credentials.
|
||||
// PROPFIND the backup folder and find the newest snapshot's last-modified time.
|
||||
$remoteDown = false;
|
||||
$remoteStatus = '';
|
||||
|
||||
try {
|
||||
$db = new Database();
|
||||
$smtp = SmtpRelay::getSettings($db);
|
||||
} catch (Throwable $e) {
|
||||
// Cannot read credentials — skip the remote check rather than fail the run.
|
||||
$db = null;
|
||||
$smtp = null;
|
||||
}
|
||||
|
||||
if ($db !== null && !empty($smtp['username']) && !empty($smtp['password'])) {
|
||||
$remote = remoteFreshness($smtp['username'], $smtp['password']);
|
||||
if ($remote['oldest_age'] === null) {
|
||||
$remoteDown = true;
|
||||
$remoteStatus = 'aucune copie distante trouvée dans XAMXAM-BCK';
|
||||
} elseif ($remote['oldest_age'] > REMOTE_STALE_SECONDS) {
|
||||
$remoteDown = true;
|
||||
$hours = (int) round($remote['oldest_age'] / 3600.0);
|
||||
$remoteStatus = "la copie Nextcloud la plus récente date d'il y a {$hours} h";
|
||||
}
|
||||
} else {
|
||||
// No creds — can't check; don't alarm about remote.
|
||||
$remoteDown = false;
|
||||
}
|
||||
|
||||
$isDown = $localDown || $remoteDown;
|
||||
|
||||
if (!$isDown) {
|
||||
// Backups are fresh. Clear any prior alert marker so a future outage
|
||||
// triggers a fresh email.
|
||||
if (is_file(STATE_FILE)) {
|
||||
@unlink(STATE_FILE);
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Stale/absent — avoid flooding: only email once per incident.
|
||||
if (is_file(STATE_FILE)) {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Compose the alert.
|
||||
$issues = [];
|
||||
if ($localDown) {
|
||||
if ($age === null) {
|
||||
$issues[] = 'aucune sauvegarde locale trouvée dans ' . BACKUP_DIR;
|
||||
} else {
|
||||
$hours = (int) round($age / 3600.0);
|
||||
$issues[] = "sauvegarde locale la plus récente il y a {$hours} h"
|
||||
. ($newestName ? ' (' . basename($newestName) . ')' : '');
|
||||
}
|
||||
}
|
||||
if ($remoteDown) {
|
||||
$issues[] = $remoteStatus;
|
||||
}
|
||||
$status = implode(' ; ', $issues);
|
||||
|
||||
$subject = '⚠ XAMXAM — sauvegarde SQLite inactive';
|
||||
$remoteNote = $remoteDown
|
||||
? "\n <p>Copie distante (Nextcloud) : <strong>en retard</strong> — {$remoteStatus}.</p>"
|
||||
: '';
|
||||
$body = <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8"></head>
|
||||
<body style="font-family:system-ui,Arial,sans-serif;line-height:1.6;color:#333">
|
||||
<div style="max-width:600px;margin:0 auto;padding:20px">
|
||||
<h2 style="color:#c53030">Sauvegarde SQLite inactive</h2>
|
||||
<p>Le système de sauvegarde XAMXAM semble en panne : {$status}.</p>{$remoteNote}
|
||||
<p>Veuillez vérifier le cron de sauvegarde et les journaux :</p>
|
||||
<pre style="background:#f7fafc;padding:12px;border-left:4px solid #c53030;overflow-x:auto">tail -50 /var/log/xamxam-backup-$(date +%Y-%m-%d).log
|
||||
ls -lth /var/backups/xamxam/</pre>
|
||||
<p style="margin-top:20px;color:#666;font-size:.9em">
|
||||
Cet e-mail est envoyé automatiquement par le moniteur de sauvegarde XAMXAM.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
|
||||
$plain = "Sauvegarde SQLite inactive — {$status}.\n\n"
|
||||
. "Vérifiez : tail -50 /var/log/xamxam-backup-$(date +%Y-%m-%d).log\n"
|
||||
. "et : ls -lth /var/backups/xamxam/\n";
|
||||
|
||||
try {
|
||||
if ($db === null) {
|
||||
// Cannot read SMTP credentials to send the alert — log and exit.
|
||||
error_log('[backup-watchdog] Backup stale but SMTP creds unreadable; alert not sent. ' . $status);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Prefer the configured admin notification address; fall back to the
|
||||
// hard-coded XAMXAM account so this alert is never silently dropped.
|
||||
$to = SmtpRelay::getNotifyEmail($db);
|
||||
if ($to === '') {
|
||||
$to = ALERT_EMAIL;
|
||||
}
|
||||
|
||||
SmtpRelay::send($db, $to, $subject, $body, $plain);
|
||||
|
||||
// Persist the alert marker only after a successful send, so a transient
|
||||
// SMTP failure does not suppress the next run's alert.
|
||||
@file_put_contents(STATE_FILE, $now . "\n");
|
||||
error_log('[backup-watchdog] Alert sent to ' . $to . ' — ' . $status);
|
||||
exit(0);
|
||||
} catch (Throwable $e) {
|
||||
error_log('[backup-watchdog] Alert delivery failed: ' . $e->getMessage());
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* PROPFIND the Nextcloud backup folder and return the newest snapshot's age.
|
||||
*
|
||||
* @return array{oldest_age:?int, count:int} oldest_age in seconds (null if none)
|
||||
*/
|
||||
function remoteFreshness(string $user, string $pass): array
|
||||
{
|
||||
$ch = curl_init(rtrim(WEBDAV_BASE, '/') . '/');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_CUSTOMREQUEST => 'PROPFIND',
|
||||
CURLOPT_USERPWD => $user . ':' . $pass,
|
||||
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
|
||||
CURLOPT_HTTPHEADER => ['Depth: 1'],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
]);
|
||||
|
||||
$body = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($body === false) {
|
||||
return ['oldest_age' => null, 'count' => 0]; // unreachable → treat as stale
|
||||
}
|
||||
|
||||
// Find the newest last-modified timestamp among our snapshot files.
|
||||
$newest = 0;
|
||||
$count = 0;
|
||||
// Match <d:href>…</d:href> and adjacent lastmodified, or fall back to
|
||||
// parsing <d:getlastmodified> entries in order.
|
||||
preg_match_all(
|
||||
'/<d:href>([^<]*' . preg_quote(REMOTE_PREFIX, '/') . '[^<]*\.db\.gz)<\/d:href>.*?<d:getlastmodified>([^<]+)<\/d:getlastmodified>/is',
|
||||
$body,
|
||||
$m,
|
||||
PREG_SET_ORDER
|
||||
);
|
||||
foreach ($m as $entry) {
|
||||
$href = rawurldecode(basename(rtrim($entry[1], '/')));
|
||||
if (!str_starts_with($href, REMOTE_PREFIX) || !str_ends_with($href, '.db.gz')) {
|
||||
continue;
|
||||
}
|
||||
$count++;
|
||||
$ts = strtotime($entry[2]);
|
||||
if ($ts !== false && $ts > $newest) {
|
||||
$newest = $ts;
|
||||
}
|
||||
}
|
||||
|
||||
if ($newest === 0) {
|
||||
return ['oldest_age' => null, 'count' => 0]; // none found
|
||||
}
|
||||
|
||||
return ['oldest_age' => time() - $newest, 'count' => $count];
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* nextcloud-sync.php — push the latest SQLite snapshot to Nextcloud WebDAV.
|
||||
*
|
||||
* Uploads the newest db-*.db.gz from /var/backups/xamxam to the XAMXAM backup
|
||||
* folder on cloud.erg.school, reusing the SMTP credentials (xamxam@erg.be)
|
||||
* already stored/decryptable in smtp_settings. Filenames are timestamped and
|
||||
* the remote folder is pruned to the most recent N snapshots.
|
||||
*
|
||||
* Transport is PHP's curl extension (no rclone / shell curl needed): WebDAV
|
||||
* upload is an HTTP PUT with Basic auth, then a PROPFIND to verify size.
|
||||
*
|
||||
* Usage:
|
||||
* php scripts/nextcloud-sync.php # keep 7 remote snapshots
|
||||
* REMOTE_KEEP=14 php scripts/nextcloud-sync.php
|
||||
*
|
||||
* Expected to run from cron shortly after the daily backup snapshot.
|
||||
* Exit codes: 0 on success, 1 on failure (logged, never fatal to the backup).
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Resolve APP_ROOT robustly — same convention as the other CLI scripts here.
|
||||
$prodRoot = '/var/www/xamxam';
|
||||
if (is_dir($prodRoot . '/src') && is_file($prodRoot . '/src/Database.php')) {
|
||||
define('APP_ROOT', $prodRoot);
|
||||
} else {
|
||||
define('APP_ROOT', dirname(__DIR__) . '/app');
|
||||
}
|
||||
|
||||
const BACKUP_DIR = '/var/backups/xamxam';
|
||||
const WEBDAV_BASE = 'https://cloud.erg.school/remote.php/dav/files/xamxam%40erg.be/XAMXAM-BCK';
|
||||
const REMOTE_PREFIX = 'xamxam-db-';
|
||||
|
||||
// Autoload dependencies (PHPMailer, etc.) from composer.
|
||||
$autoloads = [
|
||||
APP_ROOT . '/../vendor/autoload.php', // repo root / vendor
|
||||
APP_ROOT . '/vendor/autoload.php', // app / vendor
|
||||
];
|
||||
foreach ($autoloads as $a) {
|
||||
if (is_file($a)) {
|
||||
require_once $a;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Read-only probe: do NOT run schema migrations against the live DB just to
|
||||
// sync a snapshot. No-op DatabaseMigrations so the Database constructor only
|
||||
// opens a PDO connection for reading SMTP credentials.
|
||||
if (!class_exists('DatabaseMigrations', false)) {
|
||||
class DatabaseMigrations
|
||||
{
|
||||
public function run(): void {}
|
||||
}
|
||||
}
|
||||
|
||||
require_once APP_ROOT . '/src/Crypto.php';
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
require_once APP_ROOT . '/src/SmtpRelay.php';
|
||||
|
||||
if (!extension_loaded('curl')) {
|
||||
error_log('[nextcloud-sync] curl extension not available');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Remote retention: keep the most recent N snapshots.
|
||||
$remoteKeep = (int) (getenv('REMOTE_KEEP') ?: 7);
|
||||
if ($remoteKeep < 1) {
|
||||
$remoteKeep = 7;
|
||||
}
|
||||
|
||||
// Locate the newest local snapshot.
|
||||
$newest = null;
|
||||
$newestMtime = 0;
|
||||
foreach (glob(BACKUP_DIR . '/db-*.db.gz') ?: [] as $f) {
|
||||
$m = @filemtime($f);
|
||||
if ($m !== false && $m > $newestMtime) {
|
||||
$newestMtime = $m;
|
||||
$newest = $f;
|
||||
}
|
||||
}
|
||||
|
||||
if ($newest === null) {
|
||||
error_log('[nextcloud-sync] No local snapshot found in ' . BACKUP_DIR);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$localSize = filesize($newest);
|
||||
$suffix = substr(basename($newest), 0, -strlen('.db.gz')); // strip .db.gz
|
||||
$remoteName = REMOTE_PREFIX . $suffix . '.db.gz';
|
||||
$remoteUrl = rtrim(WEBDAV_BASE, '/') . '/' . rawurlencode($remoteName);
|
||||
|
||||
try {
|
||||
$db = new Database();
|
||||
$s = SmtpRelay::getSettings($db);
|
||||
$user = $s['username'];
|
||||
$pass = $s['password'];
|
||||
|
||||
if ($user === '' || $pass === '') {
|
||||
error_log('[nextcloud-sync] SMTP credentials missing (username/password)');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$ch = curl_init($remoteUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_CUSTOMREQUEST => 'PUT',
|
||||
CURLOPT_USERPWD => $user . ':' . $pass,
|
||||
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
|
||||
CURLOPT_UPLOAD => true, // stream upload, memory-safe
|
||||
CURLOPT_INFILE => fopen($newest, 'rb'),
|
||||
CURLOPT_INFILESIZE => $localSize,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 0, // no timeout on large uploads
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
]);
|
||||
|
||||
$resp = curl_exec($ch);
|
||||
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($resp === false || ($code !== 201 && $code !== 204)) {
|
||||
error_log("[nextcloud-sync] PUT failed (HTTP {$code}): {$err}");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Integrity check — PROPFIND the uploaded file and confirm size matches.
|
||||
$remoteSize = webdavSize($remoteUrl, $user, $pass);
|
||||
if ($remoteSize !== $localSize) {
|
||||
error_log(
|
||||
"[nextcloud-sync] size mismatch after upload: local {$localSize} vs remote "
|
||||
. ($remoteSize === null ? 'unknown' : $remoteSize)
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "[nextcloud-sync] Uploaded {$remoteName} ({$localSize} bytes) to XAMXAM-BCK\n";
|
||||
error_log("[nextcloud-sync] Uploaded {$remoteName} ({$localSize} bytes)");
|
||||
|
||||
// Prune remote to the most recent N files (by name, which is timestamped).
|
||||
pruneRemote($user, $pass, $remoteKeep);
|
||||
exit(0);
|
||||
} catch (Throwable $e) {
|
||||
error_log('[nextcloud-sync] Error: ' . $e->getMessage());
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* PROPFIND the remote file and return its size in bytes, or null on failure.
|
||||
*/
|
||||
function webdavSize(string $url, string $user, string $pass): ?int
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_CUSTOMREQUEST => 'PROPFIND',
|
||||
CURLOPT_USERPWD => $user . ':' . $pass,
|
||||
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
|
||||
CURLOPT_HTTPHEADER => ['Depth: 0'],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
]);
|
||||
|
||||
$body = curl_exec($ch);
|
||||
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($body === false || $code !== 207) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Nextcloud returns getcontentlength in the PROPFIND response.
|
||||
if (preg_match('/<d:getcontentlength[^>]*>(\d+)<\/d:getcontentlength>/i', $body, $m)) {
|
||||
return (int) $m[1];
|
||||
}
|
||||
// Fallback: any numeric "getcontentlength" (namespace may differ).
|
||||
if (preg_match('/getcontentlength[^>]*>(\d+)</i', $body, $m)) {
|
||||
return (int) $m[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List remote snapshots and delete the oldest beyond $keep (by name order).
|
||||
*/
|
||||
function pruneRemote(string $user, string $pass, int $keep): bool
|
||||
{
|
||||
$ch = curl_init(rtrim(WEBDAV_BASE, '/') . '/');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_CUSTOMREQUEST => 'PROPFIND',
|
||||
CURLOPT_USERPWD => $user . ':' . $pass,
|
||||
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
|
||||
CURLOPT_HTTPHEADER => ['Depth: 1'],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
]);
|
||||
|
||||
$body = curl_exec($ch);
|
||||
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($body === false || $code !== 207) {
|
||||
error_log("[nextcloud-sync] prune: PROPFIND failed (HTTP {$code})");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract hrefs of our snapshot files (names starting with REMOTE_PREFIX).
|
||||
$names = [];
|
||||
if (preg_match_all('/<d:href>([^<]+)<\/d:href>/i', $body, $m)) {
|
||||
foreach ($m[1] as $href) {
|
||||
$decoded = rawurldecode(basename(rtrim($href, '/')));
|
||||
if (str_starts_with($decoded, REMOTE_PREFIX) && str_ends_with($decoded, '.db.gz')) {
|
||||
$names[] = $decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$names = array_values(array_unique($names));
|
||||
sort($names); // ascending; oldest last become deletable
|
||||
|
||||
$excess = count($names) - $keep;
|
||||
if ($excess <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$toDelete = array_slice($names, 0, $excess); // oldest first
|
||||
foreach ($toDelete as $name) {
|
||||
$url = rtrim(WEBDAV_BASE, '/') . '/' . rawurlencode($name);
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_CUSTOMREQUEST => 'DELETE',
|
||||
CURLOPT_USERPWD => $user . ':' . $pass,
|
||||
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
]);
|
||||
curl_exec($ch);
|
||||
$delCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($delCode === 204 || $delCode === 404) {
|
||||
echo "[nextcloud-sync] Pruned remote {$name}\n";
|
||||
} else {
|
||||
error_log("[nextcloud-sync] prune: DELETE {$name} failed (HTTP {$delCode})");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user