- 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>
158 lines
6.2 KiB
PHP
Executable File
158 lines
6.2 KiB
PHP
Executable File
<?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>
|