Phase 0: fixes from review (torn files, pending blobs, exact sequences, UTF-8, storage checks)

- detect files changing during reading or between resumable steps and
  re-read them (size/mtime/ctime/inode), spike F overwrites mid-run
- drop pending blob ids when a pack or index upload fails
- dump and restore PostgreSQL sequence state exactly, bound parameters,
  validated catalog names; spike C checks all sequences exactly
- keep invalid UTF-8 byte-exact (tagged base64)
- refuse large tables without a primary key
- StorageBackend: check write/flush/close results, validate base path
- user_migration import/export: root paths, intermediate folders,
  partial writes, read errors
- spikes: never touch an existing account, own fixtures, 512M note
This commit is contained in:
2026-10-01 18:58:51 +02:00
parent e00d6579f7
commit aa541d58b2
13 changed files with 274 additions and 70 deletions
+24 -5
View File
@@ -23,6 +23,7 @@ final class StorageBackend implements IBackend {
private string $base = '',
) {
$this->base = trim($base, '/');
self::validate($this->base, true);
$this->atomicPut = $storage->instanceOfStorage(\OCA\Files_External\Lib\Storage\AmazonS3::class);
if ($this->base !== '') {
$this->mkdirs($this->base);
@@ -37,13 +38,18 @@ final class StorageBackend implements IBackend {
if ($this->storage->instanceOfStorage(IWriteStreamStorage::class)) {
/** @var IWriteStreamStorage $s */
$s = $this->storage;
$s->writeStream($writeTo, $stream);
$written = $s->writeStream($writeTo, $stream);
if ($written <= 0 && $this->storage->filesize($writeTo) !== 0) {
throw new BackendException('Write reported no data for ' . $path);
}
} else {
$out = $this->storage->fopen($writeTo, 'w');
if ($out === false || stream_copy_to_stream($stream, $out) === false) {
if ($out === false || stream_copy_to_stream($stream, $out) === false || !fflush($out)) {
throw new BackendException('Write failed for ' . $path);
}
fclose($out);
if (!fclose($out)) {
throw new BackendException('Close failed for ' . $path);
}
}
if (is_resource($stream)) {
fclose($stream);
@@ -139,10 +145,23 @@ final class StorageBackend implements IBackend {
$this->storage->mkdir($dir);
}
private function abs(string $path): string {
if (str_contains($path, '..')) {
/** Reject empty segments, '.', '..', NUL, backslashes and absolute paths. */
private static function validate(string $path, bool $allowEmpty = false): void {
if ($path === '' && $allowEmpty) {
return;
}
if ($path === '' || str_starts_with($path, '/') || str_contains($path, "\\") || str_contains($path, "\0")) {
throw new BackendException('Invalid path: ' . $path);
}
foreach (explode('/', $path) as $segment) {
if ($segment === '' || $segment === '.' || $segment === '..') {
throw new BackendException('Invalid path: ' . $path);
}
}
}
private function abs(string $path): string {
self::validate(ltrim($path, '/'));
return ltrim(($this->base === '' ? '' : $this->base . '/') . ltrim($path, '/'), '/');
}
}