├── README.md └── SftpClient.class.php /README.md: -------------------------------------------------------------------------------- 1 | # php-ssh2-sftp-client 2 | PHP Sftp Client Class using SSH2 functions and shell commands, with server-side error handlings 3 | 4 | 5 | *By GR admin@admin.ge* 6 | 7 | *[Portfolio](http://www.admin.ge/portfolio)*
8 | *[GR8cms.com](http://www.GR8cms.com)* 9 | 10 | *Copyright (c) 2015 GR*
11 | *licensed under the MIT licenses*
12 | *http://www.opensource.org/licenses/mit-license.html* 13 | 14 | 15 | ## USAGE 16 | 17 | ##### Connect to an SSH server & authenticate: 18 | ```php 19 | connect($host, $port, $timeout); 26 | 27 | // login 28 | $sftp->login($user, $pass); 29 | ``` 30 | 31 | ##### Get current directory: 32 | ```php 33 | getCurrentDirectory(); 36 | ``` 37 | 38 | ##### Create directory 39 | ```php 40 | createDirectory($path, $ignore_if_exists); 43 | ``` 44 | 45 | ##### Delete directory 46 | ```php 47 | deleteDirectory($path); 50 | ``` 51 | 52 | ##### Delete file 53 | ```php 54 | deleteFile($path); 57 | ``` 58 | 59 | ##### Get directory content list 60 | ```php 61 | getDirectoryList($path, $recursive); 65 | 66 | // rawlist 67 | $sftp->getDirectoryRawList($path, $recursive); 68 | 69 | // formated rawlist 70 | $sftp->getDirectoryRawListFormatted($path, $recursive); 71 | ``` 72 | 73 | ##### Get file stat 74 | ```php 75 | stat($path); 78 | ``` 79 | 80 | ##### Download a file 81 | ```php 82 | downloadFile($remote_file, $local_file); 85 | ``` 86 | 87 | ##### Upload a file 88 | ```php 89 | uploadFile($local_file, $remote_file[, int $create_mode = 0644 ] ); 92 | ``` 93 | 94 | ##### Rename file/directory 95 | ```php 96 | renameFile($oldname, $newname); 100 | 101 | // Rename Folder 102 | $sftp->renameDirectory($oldname, $newname); 103 | 104 | // both of them are alias of 105 | $sftp->renameFileOrFolder($oldname, $newname); 106 | ``` 107 | 108 | ##### Create Symlink 109 | ```php 110 | createSymlink($target, $link); 113 | ``` 114 | 115 | ##### Execute custom command 116 | ```php 117 | ssh2_exec($cmd); 120 | ``` 121 | 122 | ##### Close connection 123 | ```php 124 | close(); 127 | ``` 128 | 129 | #### Handle Errors 130 | ```php 131 | connect($host, $port, $timeout); 136 | $sftp->login($user, $pass); 137 | } 138 | catch(ErrorException $e) { 139 | // handle the error 140 | } 141 | ``` 142 | -------------------------------------------------------------------------------- /SftpClient.class.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright (c) GR 2015 12 | * @license http://www.opensource.org/licenses/mit-license.html (The MIT License) 13 | * @link http://www.admin.ge/ 14 | * @link http://www.GR8cms.com/ 15 | * @version v1.0 16 | */ 17 | 18 | namespace GR; 19 | 20 | use \ErrorException; 21 | 22 | class SftpClient { 23 | /** 24 | * Current connection 25 | * @var resource 26 | */ 27 | private $connection; 28 | 29 | /** 30 | * Host 31 | * @var string 32 | */ 33 | private $host; 34 | 35 | /** 36 | * Port 37 | * @var number 38 | */ 39 | private $port = 22; 40 | 41 | /** 42 | * Remote connection userame 43 | * @var 44 | */ 45 | private $user; 46 | 47 | /** 48 | * Remote connection password 49 | * @var string 50 | */ 51 | private $pass; 52 | 53 | /** 54 | * Remote connection timeout 55 | * @var number 56 | */ 57 | private $timeout = 90; 58 | 59 | /** 60 | * SFTP subsystem 61 | * @var resource 62 | */ 63 | private $sftp; 64 | 65 | /** 66 | * SSH2 stream error message 67 | * @var string 68 | */ 69 | private $sshErrMsg = ''; 70 | 71 | /** 72 | * Remote OS 73 | * @var string 74 | */ 75 | private $OS_TYPE = 'NIX'; // WIN, NIX 76 | 77 | /** 78 | * File system entry types 79 | * @var array 80 | */ 81 | private $types = array('d' => 'dir', '-' => 'file', 'l' => 'link'); 82 | 83 | /** 84 | * Escape chars (DO NOT CHANGE THE ORDER!!!) 85 | * @var array 86 | */ 87 | private $escapeChars = array('\\', '"', '`'); 88 | 89 | /** 90 | * LANG Constants 91 | */ 92 | const ERR_LIBSSH2 = 'libssh2 does not exists'; 93 | const ERR_OS_TYPE = 'Remote OS Type is not defined'; 94 | const ERR_LOCAL_FILE_IS_NOT_WRITABLE = 'The local file does not exists or is not writable'; 95 | const ERR_DOWNLOAD_FAILED = 'Unable to download file'; 96 | const ERR_UPLOAD_FAILED = 'Unable to upload file'; 97 | const ERR_GET_STAT_FAILED = 'Unable to get stat'; 98 | 99 | /** 100 | * Constructor 101 | * 102 | * @throws ErrorException 103 | */ 104 | public function __construct() { 105 | if (!extension_loaded('ssh2')) { 106 | throw new ErrorException(self::ERR_LIBSSH2); 107 | } 108 | } 109 | 110 | /** 111 | * Connect 112 | * 113 | * @param string $host 114 | * @param number $port 115 | * @param number $timeout 116 | * @throws \ErrorException 117 | * @return \GR\SftpClient 118 | */ 119 | public function connect($host, $port = 22, $timeout = 90) { 120 | $this->host = $host; 121 | $this->port = $port; 122 | $this->timeout = $timeout; 123 | 124 | if (!$this->connection = ssh2_connect($this->host, $this->port)) { 125 | $error = error_get_last(); 126 | throw new \ErrorException($error['message'], 0, 1, __FILE__, __LINE__); 127 | } 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Remote login 134 | * 135 | * @param string $user 136 | * @param string $pass 137 | * @throws \ErrorException 138 | * @return \GR\SftpClient 139 | */ 140 | public function login($user, $pass) { 141 | $this->user = $user; 142 | $this->pass = $pass; 143 | 144 | if (!@ssh2_auth_password($this->connection, $this->user, $this->pass)) { 145 | $error = error_get_last(); 146 | throw new \ErrorException($error['message'], 0, 1, __FILE__, __LINE__); 147 | } 148 | else { 149 | $this->sftp = @ssh2_sftp($this->connection); 150 | 151 | if (!$this->sftp){ 152 | $error = error_get_last(); 153 | throw new \ErrorException($error['message'], 0, 1, __FILE__, __LINE__); 154 | } 155 | } 156 | 157 | // define remote OS Type 158 | $this->defineRemoteOSType(); 159 | 160 | return $this; 161 | } 162 | 163 | /** 164 | * Defines remote OS Type 165 | * 166 | * @throws \ErrorException 167 | */ 168 | private function defineRemoteOSType() { 169 | $uname = $this->ssh2_exec('uname'); 170 | 171 | if ($uname === false) { 172 | throw new \ErrorException($this->sshErrMsg, 0, 1, __FILE__, __LINE__); 173 | } 174 | 175 | if (strtolower($uname) != 'windows') { 176 | $this->OS_TYPE = 'NIX'; 177 | } 178 | } 179 | 180 | /** 181 | * Defines remote OS Type 182 | */ 183 | private function defineRemoteOSType2() { 184 | $rawlist = $this->getDirectoryRawList('.', false); 185 | 186 | if ($rawlist) { 187 | $firstFileData = preg_split("/[\s]+/", $rawlist[1], 9); 188 | 189 | // for windows the first one is a date 190 | // for linux - it's permissions string 191 | if (strlen($firstFileData[0]) == 8 && !preg_match("/[a-z]/i", $firstFileData[0])) 192 | $this->OS_TYPE = 'WIN'; 193 | } 194 | } 195 | 196 | /** 197 | * Returns current directory 198 | * 199 | * @throws \ErrorException 200 | * @return string 201 | */ 202 | public function getCurrentDirectory() { 203 | $pwd = $this->ssh2_exec('pwd'); 204 | 205 | if ($pwd === false) { 206 | throw new \ErrorException($this->sshErrMsg, 0, 1, __FILE__, __LINE__); 207 | } 208 | 209 | return $pwd; 210 | } 211 | 212 | /** 213 | * Creates remote directory 214 | * 215 | * @param string $path - full path 216 | * @param boolean $ignore_if_exists 217 | * @throws \ErrorException 218 | * @return \GR\SftpClient 219 | */ 220 | public function createDirectory($path, $ignore_if_exists = true) { 221 | // on failure it does not returns any error message 222 | // $result = ssh2_sftp_mkdir($this->sftp, $path); 223 | 224 | $result = $this->ssh2_exec('mkdir '. $this->escapePath($path)); 225 | 226 | if ($result === false && !($ignore_if_exists && preg_match('/:[^:]*exists.*/i', $this->sshErrMsg)) ) { 227 | throw new \ErrorException($this->sshErrMsg, 0, 1, __FILE__, __LINE__); 228 | } 229 | 230 | return $this; 231 | } 232 | 233 | /** 234 | * Deletes file 235 | * 236 | * @param string $path - full path 237 | * @return S_FTPClient 238 | */ 239 | public function deleteFile($path) { 240 | // Removes a directory 241 | // $result = ssh2_sftp_rmdir($this->sftp, $source); 242 | 243 | // Deletes a file 244 | // $result = ssh2_sftp_unlink($this->sftp, $source); 245 | 246 | $result = $this->ssh2_exec('rm -fr '. $this->escapePath($path)); 247 | 248 | if ($result === false) { 249 | throw new \ErrorException($this->sshErrMsg, 0, 1, __FILE__, __LINE__); 250 | } 251 | 252 | return $this; 253 | } 254 | 255 | /** 256 | * Deletes directory 257 | * 258 | * @param string $path - full path 259 | * @throws \ErrorException 260 | * @return \GR\SftpClient 261 | */ 262 | public function deleteDirectory($path) { 263 | return $this->deleteFile($path); 264 | } 265 | 266 | /** 267 | * Gets directory files/folders list 268 | * 269 | * @param string $path - full path 270 | * @throws \ErrorException 271 | * @return array 272 | */ 273 | public function getDirectoryList($path, $recursive = false) { 274 | $result = $this->ssh2_exec('ls -a' . ($recursive ? 'R' : '') .' '. $this->escapePath($path)); 275 | 276 | if ($result === false) { 277 | throw new \ErrorException($this->sshErrMsg, 0, 1, __FILE__, __LINE__); 278 | } 279 | 280 | return explode("\n", $result); 281 | } 282 | 283 | /** 284 | * Gets directory rawlist 285 | * 286 | * @param unknown $path 287 | * @param string $recursive 288 | * @throws \ErrorException 289 | * @return multitype: 290 | */ 291 | public function getDirectoryRawList($path, $recursive = false) { 292 | $result = $this->ssh2_exec('ls -la' . ($recursive ? 'R' : '') .' '. $this->escapePath($path)); 293 | 294 | if ($result === false) { 295 | throw new \ErrorException($this->sshErrMsg, 0, 1, __FILE__, __LINE__); 296 | } 297 | 298 | return explode("\n", $result); 299 | } 300 | 301 | /** 302 | * Returns data list of files 303 | * 304 | * @param string $directory 305 | * @param string $recursive 306 | * @param array $ignoreNames 307 | * @return multitype:Ambigous 308 | */ 309 | public function getDirectoryRawListFormatted($path, $recursive = true, array $ignoreFilenames = array('', '.', '..')) { 310 | $output = array(); 311 | 312 | $this->getDirectoryRawListFormattedRecursive($output, $path, $recursive, $ignoreFilenames); 313 | 314 | return $output; 315 | } 316 | 317 | /** 318 | * Returns data list of files 319 | * 320 | * @param array $output 321 | * @param string $path 322 | * @param boolean $recursive 323 | * @param array $ignoreNames 324 | */ 325 | private function getDirectoryRawListFormattedRecursive(array &$output, $path, $recursive, array $ignoreFilenames) { 326 | $rawfiles = $this->getDirectoryRawList($path); 327 | 328 | foreach ((array)$rawfiles as $rawfile) { 329 | if (empty($rawfile)) continue; 330 | 331 | $content = $this->getRawfileFormatted($rawfile, $path, $ignoreFilenames); 332 | 333 | if (!$content) continue; 334 | 335 | $output[] = $content; 336 | 337 | if ($recursive && $content['typeDescriptor'] == 'd') { 338 | $this->getDirectoryRawListFormattedRecursive($output, $content['path'], $recursive, $ignoreFilenames); 339 | } 340 | } 341 | } 342 | 343 | /** 344 | * Returns formatted data 345 | * 346 | * @param string $rawfile 347 | * @param string $path 348 | * @param array $ignoreFilenames 349 | * @return array 350 | */ 351 | private function getRawfileFormatted($rawfile, $path, array $ignoreFilenames) { 352 | switch ($this->OS_TYPE) { 353 | case 'WIN': return $this->getRawfileFormatted_WIN($rawfile, $path, $ignoreFilenames); 354 | default: return $this->getRawfileFormatted_NIX($rawfile, $path, $ignoreFilenames); 355 | } 356 | } 357 | 358 | /** 359 | * Returns file specific data 360 | * 361 | * @param string $rawfile 362 | * @param string $directory 363 | * @param array $ignoreNames 364 | * @param array $ignorePaths 365 | * @return array 366 | */ 367 | private function getRawfileFormatted_NIX($rawfile, $path, array $ignoreFilenames) { 368 | $out = array(); 369 | 370 | $info = preg_split("/[\s]+/", $rawfile, 9); 371 | 372 | $month = $info[5]; 373 | $day = $info[6]; 374 | $year = preg_match('/^\d+$/', $info[7]) ? $info[7] : date('Y'); 375 | $time = preg_match('/^\d+$/', $info[7]) ? '' : $info[7]; 376 | 377 | $typeDescriptor = $info[0]{0}; 378 | 379 | $name = ''; 380 | $symlinkPath = ''; 381 | list($name, $symlinkPath) = explode('->', $info[8]); 382 | 383 | $name = trim($name); 384 | $path = $typeDescriptor == 'l' ? trim($symlinkPath) : $path . '/' .$name; 385 | 386 | $out = array( 387 | 'name' => $name, 388 | 'path' => $path, 389 | 'type' => $this->types[ $typeDescriptor ], 390 | 'typeDescriptor'=> $typeDescriptor, 391 | 'size' => $info[4], 392 | 'chmod' => substr($info[0], 1), 393 | 'date' => strtotime($day .' '. $month .' '. $year .' '. $time), 394 | ); 395 | 396 | if (in_array($out['name'], $ignoreFilenames)) 397 | return false; 398 | 399 | return $out; 400 | } 401 | 402 | // @todo 403 | /** 404 | * Returns file specific data (for WINDOWS) 405 | * 406 | * @param string $rawfile 407 | * @param string $directory 408 | * @param array $ignoreNames 409 | * @param array $ignorePaths 410 | * @return array 411 | */ 412 | private function getRawfileFormatted_WIN($rawfile, $directory, array $ignoreNames) { 413 | $out = array(); 414 | 415 | return $out; 416 | } 417 | 418 | /** 419 | * Gets file stat 420 | * 421 | * @param string $path (full path) 422 | * @throws \ErrorException 423 | * @return array 424 | */ 425 | public function stat($path) { 426 | $result = @ssh2_sftp_stat($this->sftp, $path); 427 | 428 | if ($result === false) { 429 | throw new \ErrorException(self::ERR_GET_STAT_FAILED, 0, 1, __FILE__, __LINE__); 430 | } 431 | 432 | return $result; 433 | } 434 | 435 | /** 436 | * Downloads a file 437 | * 438 | * @param string $remote_file 439 | * @param string $local_file 440 | * @param bool|number $create_mode 441 | * @throws \ErrorException 442 | * @return \GR\SftpClient 443 | */ 444 | public function downloadFile($remote_file, $local_file, $create_mode = false) { 445 | // if file not exists - try to create it 446 | if (!file_exists($local_file)) { 447 | $handle = @fopen($local_file, 'w'); 448 | @fclose($handle); 449 | } 450 | 451 | if (!is_writable($local_file)) { 452 | throw new \ErrorException(self::ERR_LOCAL_FILE_IS_NOT_WRITABLE, 0, 1, __FILE__, __LINE__); 453 | } 454 | 455 | $result = @ssh2_scp_recv($this->connection, $remote_file, $local_file); 456 | 457 | if ($result === false) { 458 | throw new \ErrorException(self::ERR_DOWNLOAD_FAILED, 0, 1, __FILE__, __LINE__); 459 | } 460 | 461 | if ($create_mode !== false) { 462 | @chmod($local_file, $create_mode); 463 | } 464 | 465 | return $this; 466 | } 467 | 468 | /** 469 | * Uploads a file 470 | * 471 | * @param string $local_file 472 | * @param string $remote_file 473 | * @param number $create_mode 474 | * @throws \ErrorException 475 | * @return \GR\SftpClient 476 | */ 477 | public function uploadFile($local_file, $remote_file, $create_mode = 0644) { 478 | $result = @ssh2_scp_send($this->connection, $local_file, $remote_file, $create_mode); 479 | 480 | if ($result === false) { 481 | throw new \ErrorException(self::ERR_UPLOAD_FAILED, 0, 1, __FILE__, __LINE__); 482 | } 483 | 484 | return $this; 485 | } 486 | 487 | /** 488 | * Renames a file or directory 489 | * 490 | * @param string $oldname (full path) 491 | * @param string $newname (full path) 492 | * @throws \ErrorException 493 | * @return \GR\SftpClient 494 | */ 495 | public function renameFileOrFolder($oldname, $newname) { 496 | // on failure it does not returns any error message 497 | // $result = ssh2_sftp_rename($this->sftp, $oldname, $newname); 498 | 499 | $result = $this->ssh2_exec('mv -T '. $this->escapePath($oldname) .' '. $this->escapePath($newname) .''); 500 | 501 | if ($result === false) { 502 | throw new \ErrorException($this->sshErrMsg, 0, 1, __FILE__, __LINE__); 503 | } 504 | 505 | return $this; 506 | } 507 | 508 | /** 509 | * Renames a directory (Alias) 510 | * 511 | * @param string $oldname (full path) 512 | * @param string $newname (full path) 513 | * @throws \ErrorException 514 | * @return \GR\SftpClient 515 | */ 516 | public function renameDirectory($oldname, $newname) { 517 | return $this->renameFileOrFolder($oldname, $newname); 518 | } 519 | 520 | /** 521 | * Renames a directory (Alias) 522 | * 523 | * @param string $oldname (full path) 524 | * @param string $newname (full path) 525 | * @throws \ErrorException 526 | * @return \GR\SftpClient 527 | */ 528 | public function renameFile($oldname, $newname) { 529 | return $this->renameFileOrFolder($oldname, $newname); 530 | } 531 | 532 | /** 533 | * Creates a symlink 534 | * 535 | * @param string $target (full path) 536 | * @param string $link (full path) 537 | * @throws \ErrorException 538 | * @return \GR\SftpClient 539 | */ 540 | public function createSymlink($target, $link) { 541 | // $result = ssh2_sftp_symlink($this->sftp, $target, $name); 542 | 543 | $result = $this->ssh2_exec('ln -s '. $this->escapePath($target) .' '. $this->escapePath($link) .''); 544 | 545 | if ($result === false) { 546 | throw new \ErrorException($this->sshErrMsg, 0, 1, __FILE__, __LINE__); 547 | } 548 | 549 | return $this; 550 | } 551 | 552 | /** 553 | * Run shell script 554 | * 555 | * @param string $cmd 556 | * @param string $trimOutput 557 | * @return Ambigous: (FALSE on error) 558 | */ 559 | public function ssh2_exec($cmd, $trimOutput = true) { 560 | $stream = ssh2_exec($this->connection, $cmd); 561 | stream_set_blocking($stream, true); 562 | 563 | $content = stream_get_contents($stream); 564 | $content = $trimOutput ? trim($content) : $content; 565 | 566 | $stderr_stream = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR); 567 | $content_error = stream_get_contents($stderr_stream); 568 | $this->sshErrMsg = $content_error; 569 | 570 | return strlen($this->sshErrMsg) ? false : $content; 571 | } 572 | 573 | /** 574 | * Escapes path string 575 | * 576 | * @param string $path 577 | * @return string 578 | */ 579 | private function escapePath($path) { 580 | $out = $path; 581 | 582 | foreach ($this->escapeChars as $char) { 583 | $out = str_replace($char, '\\'.$char, $out); 584 | } 585 | 586 | return '"' . $out . '"'; 587 | } 588 | 589 | /** 590 | * close existed connection 591 | * 592 | * @return boolean 593 | */ 594 | public function close() { 595 | unset($this->connection); 596 | } 597 | 598 | /** destructor */ 599 | public function __destruct() { 600 | $this->close(); 601 | } 602 | } 603 | 604 | 605 | --------------------------------------------------------------------------------