├── 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 |
--------------------------------------------------------------------------------