base = trim($base, '/'); $this->atomicPut = $storage->instanceOfStorage(\OCA\Files_External\Lib\Storage\AmazonS3::class); if ($this->base !== '') { $this->mkdirs($this->base); } } public function put(string $path, $stream): void { $target = $this->abs($path); $this->mkdirs(dirname($target)); $writeTo = $this->atomicPut ? $target : $target . '.part-' . bin2hex(random_bytes(4)); try { if ($this->storage->instanceOfStorage(IWriteStreamStorage::class)) { /** @var IWriteStreamStorage $s */ $s = $this->storage; $s->writeStream($writeTo, $stream); } else { $out = $this->storage->fopen($writeTo, 'w'); if ($out === false || stream_copy_to_stream($stream, $out) === false) { throw new BackendException('Write failed for ' . $path); } fclose($out); } if (is_resource($stream)) { fclose($stream); } if ($writeTo !== $target && !$this->storage->rename($writeTo, $target)) { throw new BackendException('Cannot finalise ' . $path); } } catch (\Throwable $e) { if (is_resource($stream)) { fclose($stream); } if ($writeTo !== $target) { $this->storage->unlink($writeTo); } throw $e instanceof BackendException ? $e : new BackendException('Write failed for ' . $path . ': ' . $e->getMessage(), 0, $e); } } public function get(string $path) { $fh = $this->storage->fopen($this->abs($path), 'r'); if ($fh === false) { throw new BackendException('Not found: ' . $path); } return $fh; } public function getRange(string $path, int $offset, int $length): string { $fh = $this->get($path); try { if (fseek($fh, $offset) !== 0) { throw new BackendException('Seek failed in ' . $path); } $data = ''; while (strlen($data) < $length && !feof($fh)) { $chunk = fread($fh, $length - strlen($data)); if ($chunk === false) { break; } $data .= $chunk; } if (strlen($data) !== $length) { throw new BackendException('Short read in ' . $path); } return $data; } finally { fclose($fh); } } public function exists(string $path): bool { return $this->storage->file_exists($this->abs($path)); } public function list(string $prefix): array { $result = []; $this->listInto($this->abs($prefix), $result); sort($result); $cut = $this->base === '' ? 0 : strlen($this->base) + 1; return array_map(fn ($p) => substr($p, $cut), $result); } public function delete(string $path): void { $this->storage->unlink($this->abs($path)); } private function listInto(string $dir, array &$result): void { if (!$this->storage->is_dir($dir)) { return; } $dh = $this->storage->opendir($dir); if ($dh === false) { return; } while (($name = readdir($dh)) !== false) { if ($name === '.' || $name === '..' || str_contains($name, '.part-')) { continue; } $child = $dir . '/' . $name; if ($this->storage->is_dir($child)) { $this->listInto($child, $result); } else { $result[] = $child; } } closedir($dh); } private function mkdirs(string $dir): void { if ($dir === '' || $dir === '.' || $this->storage->is_dir($dir)) { return; } $this->mkdirs(dirname($dir) === '.' ? '' : dirname($dir)); $this->storage->mkdir($dir); } private function abs(string $path): string { if (str_contains($path, '..')) { throw new BackendException('Invalid path: ' . $path); } return ltrim(($this->base === '' ? '' : $this->base . '/') . ltrim($path, '/'), '/'); } }