Backup state stays valid JSON with a name that is not UTF-8; Bin reads on from an id, not a position

This commit is contained in:
2026-11-06 20:02:09 +01:00
parent 1736005648
commit 1b621b7149
8 changed files with 38 additions and 18 deletions
+2 -2
View File
@@ -179,12 +179,12 @@ final class AdminApiController extends Controller {
}
}
public function trash(int $targetId, int $offset = 0): JSONResponse {
public function trash(int $targetId, string $after = ''): JSONResponse {
try {
$delay = $this->keys->deleteDelayDays() * 86400;
$repo = $this->targets->repository($this->targets->get((string)$targetId));
// In parts, so that the page can show how far it is (a read per removed restore point).
$page = $repo->trashPage($offset, microtime(true) + self::TRASH_PAGE_SECONDS);
$page = $repo->trashPage($after, microtime(true) + self::TRASH_PAGE_SECONDS);
$out = [];
foreach ($page['items'] as $id => $info) {
$out[] = ['id' => $id, 'label' => $info['label'], 'time' => $info['time'], 'removedAt' => $info['forgottenAt'],
+2 -2
View File
@@ -62,9 +62,9 @@ final class BackupRun {
if (!mb_check_encoding($logical, 'UTF-8') || !Repository::isSafePath($logical)) {
// A name a snapshot cannot hold (a backslash, a control character, not UTF-8). One
// such path must not make a backup fail, or the whole snapshot unreadable later.
// The cursor stays where it is: the state is JSON, which cannot hold such a name. A
// next step meets it again, and it is reported once.
self::recordUnreadable($state, $stats, mb_scrub($logical, 'UTF-8'));
$state['cur'] = null;
$state['after'] = $logical;
continue;
}
if (is_dir($path)) {
+8 -3
View File
@@ -818,15 +818,20 @@ final class Repository {
/**
* Part of trash(), for a page that shows how far it is: each entry is a read of its own on the
* location. Reads on from $offset until $deadline (microtime(true)), at least one entry.
* location. Reads the entries with an id after $after, in order, until $deadline
* (microtime(true)), at least one. The id, not a position: a cleanup that changes the trash
* between two parts then makes no entry come twice or be missed.
*
* @return array{total: int, items: array<string, array<string, mixed>>}
*/
public function trashPage(int $offset, float $deadline): array {
public function trashPage(string $after, float $deadline): array {
$ids = array_map('strval', array_keys($this->catalog->trash));
sort($ids, SORT_STRING);
$items = [];
foreach (array_slice($ids, max(0, $offset)) as $id) {
foreach ($ids as $id) {
if (strcmp($id, $after) <= 0) {
continue;
}
try {
$items[$id] = $this->trashInfo($id);
} catch (\Throwable) {