From 223ee42a53a14eb289c7ee262d8f60208bc9cab8 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 10 Mar 2020 15:58:33 +0100 Subject: [PATCH] faster implementation of SFTP write stream using mostly the same techniques as the read stream Signed-off-by: Robin Appelman --- apps/files_external/lib/Lib/Storage/SFTP.php | 3 + .../lib/Lib/Storage/SFTPReadStream.php | 3 + .../lib/Lib/Storage/SFTPWriteStream.php | 178 ++++++++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 apps/files_external/lib/Lib/Storage/SFTPWriteStream.php diff --git a/apps/files_external/lib/Lib/Storage/SFTP.php b/apps/files_external/lib/Lib/Storage/SFTP.php index 1caebf8a9d..5d83bf253e 100644 --- a/apps/files_external/lib/Lib/Storage/SFTP.php +++ b/apps/files_external/lib/Lib/Storage/SFTP.php @@ -378,6 +378,9 @@ class SFTP extends \OC\Files\Storage\Common { return RetryWrapper::wrap($handle); case 'w': case 'wb': + SFTPWriteStream::register(); + $context = stream_context_create(['sftp' => ['session' => $this->getConnection()]]); + return fopen('sftpwrite://' . trim($absPath, '/'), 'w', false, $context); case 'a': case 'ab': case 'r+': diff --git a/apps/files_external/lib/Lib/Storage/SFTPReadStream.php b/apps/files_external/lib/Lib/Storage/SFTPReadStream.php index 850a546900..7a59cbf489 100644 --- a/apps/files_external/lib/Lib/Storage/SFTPReadStream.php +++ b/apps/files_external/lib/Lib/Storage/SFTPReadStream.php @@ -129,6 +129,9 @@ class SFTPReadStream implements File { $data = substr($this->buffer, 0, $count); $this->buffer = substr($this->buffer, $count); + if ($this->buffer === false) { + $this->buffer = ''; + } $this->readPosition += strlen($data); return $data; diff --git a/apps/files_external/lib/Lib/Storage/SFTPWriteStream.php b/apps/files_external/lib/Lib/Storage/SFTPWriteStream.php new file mode 100644 index 0000000000..42b38ead1c --- /dev/null +++ b/apps/files_external/lib/Lib/Storage/SFTPWriteStream.php @@ -0,0 +1,178 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Files_External\Lib\Storage; + +use Icewind\Streams\File; +use phpseclib\Net\SSH2; + +class SFTPWriteStream implements File { + /** @var resource */ + public $context; + + /** @var \phpseclib\Net\SFTP */ + private $sftp; + + /** @var resource */ + private $handle; + + /** @var int */ + private $internalPosition = 0; + + /** @var int */ + private $writePosition = 0; + + /** @var bool */ + private $eof = false; + + private $buffer = ''; + + static function register($protocol = 'sftpwrite') { + if (in_array($protocol, stream_get_wrappers(), true)) { + return false; + } + return stream_wrapper_register($protocol, get_called_class()); + } + + /** + * Load the source from the stream context and return the context options + * + * @param string $name + * @return array + * @throws \BadMethodCallException + */ + protected function loadContext($name) { + $context = stream_context_get_options($this->context); + if (isset($context[$name])) { + $context = $context[$name]; + } else { + throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set'); + } + if (isset($context['session']) and $context['session'] instanceof \phpseclib\Net\SFTP) { + $this->sftp = $context['session']; + } else { + throw new \BadMethodCallException('Invalid context, session not set'); + } + return $context; + } + + public function stream_open($path, $mode, $options, &$opened_path) { + [, $path] = explode('://', $path); + $this->loadContext('sftp'); + + if (!($this->sftp->bitmap & SSH2::MASK_LOGIN)) { + return false; + } + + $remote_file = $this->sftp->_realpath($path); + if ($remote_file === false) { + return false; + } + + $packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE | NET_SFTP_OPEN_TRUNCATE, 0); + if (!$this->sftp->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { + return false; + } + + $response = $this->sftp->_get_sftp_packet(); + switch ($this->sftp->packet_type) { + case NET_SFTP_HANDLE: + $this->handle = substr($response, 4); + break; + case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED + $this->sftp->_logError($response); + return false; + default: + user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); + return false; + } + + return true; + } + + public function stream_seek($offset, $whence = SEEK_SET) { + return false; + } + + public function stream_tell() { + return $this->writePosition; + } + + public function stream_read($count) { + return false; + } + + public function stream_write($data) { + $written = strlen($data); + $this->writePosition += $written; + + $this->buffer .= $data; + + if (strlen($this->buffer) > 64 * 1024) { + if (!$this->stream_flush()) { + return false; + } + } + + return $written; + } + + public function stream_set_option($option, $arg1, $arg2) { + return false; + } + + public function stream_truncate($size) { + return false; + } + + public function stream_stat() { + return false; + } + + public function stream_lock($operation) { + return false; + } + + public function stream_flush() { + $size = strlen($this->buffer); + $packet = pack('Na*N3a*', strlen($this->handle), $this->handle, $this->internalPosition / 4294967296, $this->internalPosition, $size, $this->buffer); + if (!$this->sftp->_send_sftp_packet(NET_SFTP_WRITE, $packet)) { + return false; + } + $this->internalPosition += $size; + $this->buffer = ''; + + return $this->sftp->_read_put_responses(1); + } + + public function stream_eof() { + return $this->eof; + } + + public function stream_close() { + $this->stream_flush(); + if (!$this->sftp->_close_handle($this->handle)) { + return false; + } + } + +} +