StorageBackend: read SFTP locations with several requests in flight

files_external's SFTP read stream asks for the next 256 KiB only after
the previous answer has arrived, so reading costs one round trip per
request whatever the line speed: 4.9 MB/s at 47 ms to a Hetzner Storage
Box. SftpReadStream keeps up to eight READ requests in flight over the
same phpseclib connection (15 MB/s on that link), starting with one so
a small read does not fetch megabytes ahead.

A server that answers less than asked is handled by asking again from
where the answer ended; when more data follows, that size is used for
later reads over the same connection, never below the 32 KiB every
server must support. Without the needed connection methods, or when the
open fails, the storage's own stream is used as before.
This commit is contained in:
2026-10-09 04:27:21 +02:00
parent bcfec17cfe
commit d8e772137f
3 changed files with 518 additions and 0 deletions
+13
View File
@@ -17,6 +17,8 @@ use OCP\Files\Storage\IWriteStreamStorage;
final class StorageBackend implements IBackend {
/** Object stores write whole objects atomically; other storages get a temp name + rename. */
private bool $atomicPut;
/** SFTP: read through SftpReadStream, which keeps several requests in flight. */
private bool $sftpReads;
public function __construct(
private IStorage $storage,
@@ -25,6 +27,7 @@ final class StorageBackend implements IBackend {
$this->base = trim($base, '/');
self::validate($this->base, true);
$this->atomicPut = $storage->instanceOfStorage(\OCA\Files_External\Lib\Storage\AmazonS3::class);
$this->sftpReads = $storage->instanceOfStorage(\OCA\Files_External\Lib\Storage\SFTP::class);
if ($this->base !== '') {
$this->mkdirs($this->base);
}
@@ -79,6 +82,16 @@ final class StorageBackend implements IBackend {
}
public function get(string $path) {
if ($this->sftpReads) {
$fh = SftpReadStream::open($this->storage, $this->abs($path));
if (is_resource($fh)) {
return $fh;
}
if ($fh === null) {
$this->sftpReads = false;
}
// false: not opened, which need not mean missing; the storage's own stream decides
}
$fh = $this->storage->fopen($this->abs($path), 'r');
if ($fh === false) {
throw new BackendException('Not found: ' . $path);