Add reMarkable backup system with web UI

- Docker container with rmapi cloud backup, PDF conversion pipeline
- Web UI: file browser, multi-select, delete/move, thumbnail preview
- Sync: backup from reMarkable cloud, rmdoc→PDF conversion via rmc+Inkscape
- Excluded-files mechanism to prevent deleted items from returning after sync

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Borgal
2026-03-24 23:43:36 +01:00
parent 747a35e172
commit b63131c0c5
13 changed files with 1881 additions and 0 deletions

157
remarkable/web/index2.php Executable file
View File

@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
require __DIR__ . '/auth.php';
$backupRoot = getenv('RM_BACKUP_ROOT') ?: '/www/remarkable/notizen/snapshots/latest';
$backupRootReal = realpath($backupRoot);
function h(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function isInsideRoot(string $root, string $path): bool
{
$rootPrefix = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
return str_starts_with($path, $rootPrefix) || $path === $root;
}
$query = isset($_GET['q']) ? trim((string)$_GET['q']) : '';
$selected = isset($_GET['file']) ? (string)$_GET['file'] : '';
$error = '';
if ($backupRootReal === false || !is_dir($backupRootReal)) {
$error = 'Backup-Pfad nicht gefunden: ' . $backupRoot;
}
$files = [];
$selectedPath = null;
if ($error === '') {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
$backupRootReal,
FilesystemIterator::SKIP_DOTS
),
RecursiveIteratorIterator::SELF_FIRST
);
$qLower = mb_strtolower($query);
foreach ($iterator as $entry) {
$absolutePath = $entry->getPathname();
$relativePath = ltrim(substr($absolutePath, strlen($backupRootReal)), DIRECTORY_SEPARATOR);
if ($relativePath === '') {
continue;
}
$relativePathLower = mb_strtolower($relativePath);
if ($query !== '' && !str_contains($relativePathLower, $qLower)) {
continue;
}
$files[] = [
'relative' => str_replace(DIRECTORY_SEPARATOR, '/', $relativePath),
'is_dir' => $entry->isDir(),
'size' => $entry->isFile() ? $entry->getSize() : 0,
'mtime' => $entry->getMTime(),
'is_pdf' => $entry->isFile() && str_ends_with(strtolower($relativePath), '.pdf'),
];
}
usort($files, static function (array $a, array $b): int {
if ($a['is_dir'] !== $b['is_dir']) {
return $a['is_dir'] ? -1 : 1;
}
return strnatcasecmp($a['relative'], $b['relative']);
});
if ($selected !== '') {
$candidate = realpath($backupRootReal . DIRECTORY_SEPARATOR . $selected);
if ($candidate !== false && is_file($candidate) && isInsideRoot($backupRootReal, $candidate)) {
$selectedPath = $candidate;
}
}
if (isset($_GET['download']) && $selectedPath !== null) {
header('Content-Type: application/octet-stream');
header('Content-Disposition: inline; filename="' . basename($selectedPath) . '"');
header('Content-Length: ' . (string) filesize($selectedPath));
readfile($selectedPath);
exit;
}
}
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>reMarkable Archiv</title>
<style>
body { margin: 0; font-family: Arial, sans-serif; background: #f6f7f9; color: #222; }
.layout { display: grid; grid-template-columns: 430px 1fr; min-height: 100vh; }
.sidebar { border-right: 1px solid #ddd; background: #fff; padding: 14px; overflow: auto; }
.content { padding: 14px; overflow: auto; }
.muted { color: #666; font-size: 12px; }
.item { display: block; padding: 8px 10px; margin: 4px 0; border: 1px solid #eee; border-radius: 8px; text-decoration: none; color: #222; background: #fafafa; }
.item:hover { background: #f0f4ff; border-color: #c9d7ff; }
.dir { font-weight: 600; }
.meta { color: #777; font-size: 12px; margin-top: 4px; }
form { margin-bottom: 12px; }
input[type="text"] { width: 75%; padding: 8px; border: 1px solid #ccc; border-radius: 6px; }
button { padding: 8px 10px; border-radius: 6px; border: 1px solid #bbb; background: #fff; cursor: pointer; }
iframe { width: 100%; height: calc(100vh - 110px); border: 1px solid #ccc; border-radius: 8px; background: #fff; }
.notice { padding: 10px; border: 1px solid #f0c36d; background: #fff8e1; border-radius: 8px; }
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<h2 style="margin: 0 0 8px 0;">reMarkable Archiv</h2>
<div class="muted">Quelle: <?= h($backupRootReal ?: $backupRoot) ?></div>
<form method="get">
<input type="text" name="q" value="<?= h($query) ?>" placeholder="Suche nach Dateinamen oder Ordner...">
<button type="submit">Suchen</button>
</form>
<?php if ($error !== ''): ?>
<div class="notice"><?= h($error) ?></div>
<?php else: ?>
<?php if (count($files) === 0): ?>
<div class="notice">Keine Treffer.</div>
<?php else: ?>
<?php foreach ($files as $entry): ?>
<?php if ($entry['is_dir']): ?>
<div class="item dir">📁 <?= h($entry['relative']) ?></div>
<?php else: ?>
<a class="item" href="?q=<?= rawurlencode($query) ?>&file=<?= rawurlencode($entry['relative']) ?>">
📄 <?= h($entry['relative']) ?>
<div class="meta">
<?= number_format((int) $entry['size'] / 1024, 1, ',', '.') ?> KB ·
<?= h(date('Y-m-d H:i:s', (int) $entry['mtime'])) ?>
<?= $entry['is_pdf'] ? ' · PDF' : '' ?>
</div>
</a>
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>
<?php endif; ?>
</aside>
<main class="content">
<?php if ($selectedPath === null): ?>
<div class="notice">Links eine Datei waehlen. PDFs werden direkt angezeigt.</div>
<?php else: ?>
<?php $rel = ltrim(substr($selectedPath, strlen($backupRootReal)), DIRECTORY_SEPARATOR); ?>
<h3 style="margin-top: 0;"><?= h(str_replace(DIRECTORY_SEPARATOR, '/', $rel)) ?></h3>
<div class="muted" style="margin-bottom: 10px;">
<a href="?q=<?= rawurlencode($query) ?>&file=<?= rawurlencode(str_replace(DIRECTORY_SEPARATOR, '/', $rel)) ?>&download=1">Datei oeffnen/herunterladen</a>
</div>
<?php if (str_ends_with(strtolower($selectedPath), '.pdf')): ?>
<iframe src="?q=<?= rawurlencode($query) ?>&file=<?= rawurlencode(str_replace(DIRECTORY_SEPARATOR, '/', $rel)) ?>&download=1"></iframe>
<?php else: ?>
<div class="notice">Keine PDF-Datei. Bitte ueber den Link oben herunterladen/oeffnen.</div>
<?php endif; ?>
<?php endif; ?>
</main>
</div>
</body>
</html>