AloFramework documentation
  • Namespace
  • Class
  • Tree
  • Deprecated
  • Todo

Namespaces

  • Alo
    • Cache
    • CLI
    • Controller
    • Db
    • Exception
    • FileSystem
    • Session
    • Statics
    • Validators
    • Windows
  • Controller
  • None
  • PHP

Classes

  • Cron
  • Curl
  • Email
  • Handler
  • Locale
  • Profiler
  • SFTP
  1 <?php
  2 
  3     namespace Alo;
  4 
  5     use Alo\Exception\ExtensionException as EE;
  6     use Alo\Exception\FileSystemException as FE;
  7     use Alo\Exception\SFTPException as SE;
  8     use Alo\FileSystem\File;
  9 
 10     if (!defined('GEN_START')) {
 11         http_response_code(404);
 12     } else {
 13 
 14         /**
 15          * SFTP handler
 16          *
 17          * @author Arturas Molcanovas <a.molcanovas@gmail.com>
 18          */
 19         class SFTP {
 20 
 21             /**
 22              * Defines the sort direction as ascending
 23              *
 24              * @var int
 25              */
 26             const SORT_ASC = SCANDIR_SORT_ASCENDING;
 27             /**
 28              * Defines the sort direction as descending
 29              *
 30              * @var int
 31              */
 32             const SORT_DESC = SCANDIR_SORT_DESCENDING;
 33             /**
 34              * Defines a parameter as "retry count"
 35              *
 36              * @var int
 37              */
 38             const P_RETRY_COUNT = 101;
 39             /**
 40              * Defines a parameter as "retry wait time"
 41              *
 42              * @var int
 43              */
 44             const P_RETRY_TIME = 102;
 45             /**
 46              * Static reference to the last instance of the class
 47              *
 48              * @var SFTP
 49              */
 50             static $this;
 51             /**
 52              * The endpoint URL
 53              *
 54              * @var string
 55              */
 56             protected $url;
 57             /**
 58              * The SFTP username
 59              *
 60              * @var string
 61              */
 62             protected $user;
 63             /**
 64              * Path to the public authentication key
 65              *
 66              * @var string
 67              */
 68             protected $pubkey;
 69             /**
 70              * Path to the private authentication key
 71              *
 72              * @var string
 73              */
 74             protected $privkey;
 75             /**
 76              * Private authentication key password
 77              *
 78              * @var string
 79              */
 80             protected $pw;
 81             /**
 82              * Directory in use
 83              *
 84              * @var string
 85              */
 86             protected $dir;
 87             /**
 88              * The SSH2 connection
 89              *
 90              * @var resource
 91              */
 92             protected $connection;
 93             /**
 94              * The SFTP subsystem
 95              *
 96              * @var resource
 97              */
 98             protected $sftp;
 99             /**
100              * The local directory set
101              *
102              * @var string
103              */
104             protected $localDir;
105             /**
106              * Maximum amount of retries for an operation
107              *
108              * @var int
109              */
110             protected $retryCountMax;
111 
112             /**
113              * Time in seconds between retries
114              *
115              * @var int
116              */
117             protected $retryTime;
118 
119             /**
120              * Instantiates the library
121              *
122              * @param array $params Optional parameters - see class P_* constants
123              *
124              * @see self::P_RETRY_COUNT
125              * @see self::P_RETRY_TIME
126              * @throws EE When the SSH2 extension is not loaded
127              */
128             function __construct($params = []) {
129                 if (!function_exists('ssh2_connect')) {
130                     throw new EE('SSH2 extension not loaded', EE::E_EXT_NOT_LOADED);
131                 } else {
132                     $this->dir = DIR_INDEX;
133 
134                     $this->retryCountMax = (int)\get($params[self::P_RETRY_COUNT]);
135                     $this->retryTime     = (int)\get($params[self::P_RETRY_TIME]) ? $params[self::P_RETRY_TIME] : 3;
136 
137                     \Log::debug('SSH2 class initialised');
138                 }
139 
140                 self::$this = &$this;
141             }
142 
143             /**
144              * Instantiates the library
145              *
146              * @param array $params Optional parameters - see class P_* constants
147              *
148              * @see self::P_RETRY_COUNT
149              * @see self::P_RETRY_TIME
150              * @throws EE When the SSH2 extension is not loaded
151              *
152              * @return SFTP
153              */
154             static function sftp($params = []) {
155                 return new SFTP($params);
156             }
157 
158             /**
159              * If no parameter is passed gets the maximum amount of retry attempts for failed operations, otherwise sets it.
160              *
161              * @author Art <a.molcanovas@gmail.com>
162              *
163              * @param int $int The amount
164              *
165              * @return boolean|int
166              */
167             function retryCount($int = -1) {
168                 if ($int === -1) {
169                     return $this->retryCountMax;
170                 } elseif (is_numeric($int) && $int >= 0) {
171                     $this->retryCountMax = (int)$int;
172                     \Log::debug('Retry count set to ' . $int);
173 
174                     return true;
175                 } else {
176                     return false;
177                 }
178             }
179 
180             /**
181              * If no parameter is passed gets the time to wait between operation retries, otherwise sets it
182              *
183              * @author Art <a.molcanovas@gmail.com>
184              *
185              * @param int $int The time in seconds
186              *
187              * @return boolean
188              */
189             function retryTime($int = -1) {
190                 if ($int === -1) {
191                     return $this->retryTime;
192                 } elseif (is_numeric($int) && $int > 0) {
193                     $this->retryTime = (int)$int;
194                     \Log::debug('Retry time set to ' . $int);
195 
196                     return true;
197                 } else {
198                     return false;
199                 }
200             }
201 
202             /**
203              * Sets the local directory
204              *
205              * @author Art <a.molcanovas@gmail.com>
206              *
207              * @param string $dir The directory path
208              *
209              * @return SFTP
210              */
211             function loc($dir) {
212                 $dir = rtrim($dir, ' ' . DIRECTORY_SEPARATOR);
213                 if (!$dir) {
214                     $dir = DIRECTORY_SEPARATOR;
215                 }
216                 $this->localDir = $dir;
217                 \Log::debug('Local dir set to ' . $dir);
218 
219                 return $this;
220             }
221 
222             /**
223              * Scans a directory for files and subdirectories
224              *
225              * @author Art <a.molcanovas@gmail.com>
226              *
227              * @param int $sortingOrder The sorting order
228              *
229              * @return array ["dirs" => [], "files" => []]
230              */
231             function scandir($sortingOrder = self::SORT_ASC) {
232                 $this->checkSubsystem();
233 
234                 $dir = scandir('ssh2.sftp://' . $this->sftp . DIRECTORY_SEPARATOR . $this->dir, $sortingOrder);
235                 $r   = ['dirs' => [], 'files' => []];
236 
237                 foreach ($dir as $v) {
238                     //Ignore hidden
239                     if ($v == '.' || $v == '..' || stripos($v, '.') === 0) {
240                         continue;
241                     } elseif (self::isFile($v)) {
242                         $r['files'][] = $v;
243                     } else {
244                         $r['dirs'][] = $v;
245                     }
246                 }
247 
248                 return $r;
249             }
250 
251             /**
252              * Checks if the SFTP subsystem was initialised
253              *
254              * @author Art <a.molcanovas@gmail.com>
255              * @throws SE When the connection ultimately fails
256              * @return SFTP
257              */
258             function checkSubsystem() {
259                 if (!$this->sftp) {
260                     \Log::debug('SFTP subsystem wasn\'t initialised when a dependant' .
261                                 ' method was called. Initialising.');
262                     $this->connect();
263                 }
264 
265                 return $this;
266             }
267 
268             /**
269              * Creates a SSH2 connection
270              *
271              * @author Art <a.molcanovas@gmail.com>
272              *
273              * @param int $attempt Current attempt number
274              *
275              * @return SFTP
276              * @throws SE When the connection ultimately fails
277              */
278             function connect($attempt = 0) {
279                 if (!($this->connection = ssh2_connect($this->url))) {
280                     $msg = 'Failed to initialise SSH2 connection';
281                     $attempt++;
282 
283                     if ($attempt - 1 < $this->retryCountMax) {
284                         \Log::error($msg . '. Retrying again' . ' in ' . $this->retryTime . ' seconds [' . $attempt .
285                                     '/' . $this->retryCountMax . ']');
286 
287                         time_sleep_until(time() + $this->retryTime);
288 
289                         return $this->connect($attempt);
290                     } else {
291                         throw new SE($msg . ' after ' . $attempt . ' attempts', SE::E_CONNECT);
292                     }
293                 } else {
294                     \Log::debug('Initialised SSH2 connection');
295 
296                     return $this->auth();
297                 }
298             }
299 
300             /**
301              * Authenticates the SSH2 connection
302              *
303              * @author Art <a.molcanovas@gmail.com>
304              *
305              * @param int $attempt The current attempt # at authentication
306              *
307              * @return SFTP
308              * @throws SE When authentication permanently fails
309              */
310             protected function auth($attempt = 0) {
311                 if (!ssh2_auth_pubkey_file($this->connection, $this->user, $this->pubkey, $this->privkey, $this->pw)) {
312                     $msg = 'Failed to authenticate SSH2 connection';
313                     ++$attempt;
314 
315                     if ($attempt - 1 < $this->retryCountMax) {
316                         \Log::error($msg . '. Retrying in ' . $this->retryTime . ' seconds [' . $attempt . '/' .
317                                     $this->retryCountMax . ']');
318                         time_sleep_until(time() + $this->retryTime);
319 
320                         return $this->auth($attempt);
321                     } else {
322                         throw new SE($msg . ' after ' . $attempt . ' retries', SE::E_AUTH);
323                     }
324                 } else {
325                     \Log::debug('SSH2 connection authenticated');
326 
327                     return $this->ssh2Sftp();
328                 }
329             }
330 
331             /**
332              * Initialises the SFTP subsystem
333              *
334              * @author Art <a.molcanovas@gmail.com>
335              *
336              * @param int $attempt Current retry number
337              *
338              * @return SFTP
339              * @throws SE When initialising the SFTP system permanently fails
340              */
341             protected function ssh2Sftp($attempt = 0) {
342                 if ($this->sftp = ssh2_sftp($this->connection)) {
343                     \Log::debug('Initialised SFTP subsystem');
344 
345                     return $this;
346                 } else {
347                     $msg = 'Failed to initialise SFTP subsystem';
348                     ++$attempt;
349 
350                     if ($attempt - 1 < $this->retryCountMax) {
351                         \Log::error($msg . '. Retrying again' . ' in ' . $this->retryTime . ' seconds [' . $attempt .
352                                     '/' . $this->retryCountMax . ']');
353                         time_sleep_until(time() + $this->retryTime);
354 
355                         return $this->ssh2Sftp($attempt);
356                     } else {
357                         throw new SE($msg . ' after ' . $attempt . ' attempts', SE::E_SUBSYSTEM);
358                     }
359                 }
360             }
361 
362             /**
363              * Checks whether a resource is a file or directory based on whether it has a file extension
364              *
365              * @author Art <a.molcanovas@gmail.com>
366              *
367              * @param string $resource The resource name to check
368              *
369              * @return boolean
370              */
371             protected static function isFile($resource) {
372                 $pos = stripos($resource, '.');
373 
374                 return $pos !== false && $pos !== 0;
375             }
376 
377             /**
378              * Downloads a file to $this->localDir
379              *
380              * @author Art <a.molcanovas@gmail.com>
381              * @see    self::$localDir
382              *
383              * @param string $file Remote file name
384              *
385              * @throws SE When the file cannot be fetched
386              * @throws FE When the name is invalid
387              * @return SFTP
388              */
389             function downloadFile($file) {
390                 \Log::debug('Downloading file ' . $file . ' to ' . $this->localDir);
391                 $fetch = $this->getFileContents($file);
392 
393                 $local = new File();
394                 $local->dir($this->localDir)->name($file);
395                 $local->content($fetch)->write();
396 
397                 return $this;
398             }
399 
400             /**
401              * Gets the file contents
402              *
403              * @author Art <a.molcanovas@gmail.com>
404              *
405              * @param string $file File name
406              *
407              * @return string String representation of the file
408              * @throws SE When the file cannot be fetched
409              */
410             function getFileContents($file) {
411                 $this->checkSubsystem();
412                 $remoteFile = $this->resolvePath($file);
413 
414                 $file = file_get_contents('ssh2.sftp://' . $this->sftp . '/' . $remoteFile);
415                 if ($file === false) {
416                     throw new SE('Failed to fetch file ' . $remoteFile, SE::E_FILE_NOT_FETCHED);
417                 } else {
418                     return $file;
419                 }
420             }
421 
422             /**
423              * Modifies the path based on whether it's relative or absolute
424              *
425              * @author Art <a.molcanovas@gmail.com>
426              *
427              * @param string $item Item name
428              *
429              * @return string The resolved path
430              */
431             protected function resolvePath($item) {
432                 return (stripos($item, DIRECTORY_SEPARATOR) === 0) ? substr($item, 1) :
433                     $this->dir . DIRECTORY_SEPARATOR . $item;
434             }
435 
436             /**
437              * Uploads a file to the SFTP folder from the local folder
438              *
439              * @author Art <a.molcanovas@gmail.com>
440              *
441              * @param string $file File name
442              *
443              * @return boolean
444              * @throws SE When the local file cannot be read
445              */
446             function upload($file) {
447                 $this->checkSubsystem();
448                 $path = $this->localDir . DIRECTORY_SEPARATOR . $file;
449 
450                 if (!$content = file_get_contents($path)) {
451                     throw new SE('Local file ' . $path . ' cannot be read', SE::E_LOCAL_FILE_NOT_READ);
452                 } else {
453                     \Log::debug('Uploading remote file ' . $file);
454 
455                     return $this->makeFile($file, $content);
456                 }
457             }
458 
459             /**
460              * Creates a file in the SFTP directory
461              *
462              * @author Art <a.molcanovas@gmail.com>
463              *
464              * @param string $file    File name
465              * @param mixed  $content File content
466              *
467              * @return boolean
468              * @throws SE When remote fopen fails
469              */
470             function makeFile($file, $content) {
471                 $this->checkSubsystem();
472                 $remoteFile = $this->resolvePath($file);
473 
474                 if (!$fp = fopen('ssh2.sftp://' . $this->sftp . DIRECTORY_SEPARATOR . $remoteFile,
475                                  File::M_WRITE_TRUNCATE_BEGIN)
476                 ) {
477                     throw new SE('Failed to remotely fopen ' . $remoteFile, SE::E_FILE_CREATE_FAIL);
478                 } else {
479                     flock($fp, LOCK_EX);
480                     fwrite($fp, $content);
481                     flock($fp, LOCK_UN);
482                     fclose($fp);
483                     \Log::debug('Wrote remote file ' . $remoteFile);
484 
485                     return true;
486                 }
487             }
488 
489             /**
490              * Deletes an item on the SFTP server
491              *
492              * @author Art <a.molcanovas@gmail.com>
493              *
494              * @param string $item File or directory name
495              *
496              * @return boolean
497              */
498             function delete($item) {
499                 $this->checkSubsystem();
500                 $path = $this->resolvePath($item);
501 
502                 if (self::isFile($item)) {
503                     $success = ssh2_sftp_unlink($this->sftp, $path);
504                 } else {
505                     $success = ssh2_sftp_rmdir($this->sftp, $path);
506                 }
507 
508                 if ($success) {
509                     \Log::debug('Deleted ' . $item);
510 
511                     return true;
512                 } else {
513                     \Log::error('Failed to delete ' . $item);
514 
515                     return false;
516                 }
517             }
518 
519             /**
520              * Returns a string representation of SFTP credentials
521              *
522              * @author Art <a.molcanovas@gmail.com>
523              * @return string
524              */
525             function __toString() {
526                 return 'User: ' . $this->user . '; PrivKey:' . $this->privkey . '; PubKey: ' . $this->pubkey .
527                        '; Password hash: ' . get($this->pw) ? md5($this->pw) : 'NO HASH CONTENT SET';
528             }
529 
530             /**
531              * If no parameter is passed gets the SFTP server URL, otherwise sets it.
532              *
533              * @author Art <a.molcanovas@gmail.com>
534              *
535              * @param string $url
536              *
537              * @return SFTP
538              * @throws SE When the URL is not a string
539              */
540             function url($url = '') {
541                 if ($url === '') {
542                     return $this->url;
543                 } elseif (is_string($url)) {
544                     $this->url = $url;
545                     \Log::debug('SFTP URL set to ' . $url);
546                 } else {
547                     throw new SE('Invalid URL', SE::E_URL_INVALID);
548                 }
549 
550                 return $this;
551             }
552 
553             /**
554              * If no parameter is passed gets the SFTP username, otherwise sets it.
555              *
556              * @author Art <a.molcanovas@gmail.com>
557              *
558              * @param string $user The username
559              *
560              * @throws SE When $user isn't scalar
561              * @return SFTP
562              */
563             function user($user = '') {
564                 if ($user === '') {
565                     return $this->user;
566                 } elseif (is_scalar($user)) {
567                     $this->user = $user;
568                     \Log::debug('SFTP user set to ' . $user);
569                 } else {
570                     throw new SE('Invalid username', SE::E_USER_INVALID);
571                 }
572 
573                 return $this;
574             }
575 
576             /**
577              * Sets the SFTP public key path
578              *
579              * @author Art <a.molcanovas@gmail.com>
580              *
581              * @param string $pubkey The path
582              *
583              * @throws SE When $pubkey isn't a string
584              * @return SFTP
585              */
586             function pubkey($pubkey = '') {
587                 if ($pubkey === '') {
588                     return $this->pubkey;
589                 } elseif (is_string($pubkey)) {
590                     $this->pubkey = $pubkey;
591                     \Log::debug('SFTP pubkey set');
592                 } else {
593                     throw new SE('$pubkey must be a valid path', SE::E_PATH_INVALID);
594                 }
595 
596                 return $this;
597             }
598 
599             /**
600              * If no parameter is passed gets the SFTP private key path, otherwise sets it
601              *
602              * @author Art <a.molcanovas@gmail.com>
603              *
604              * @param string $privkey The path
605              *
606              * @throws SE When the $privkey isn't a string
607              * @return SFTP
608              */
609             function privkey($privkey = '') {
610                 if ($privkey === '') {
611                     return $this->privkey;
612                 } elseif (is_string($privkey)) {
613                     $this->privkey = $privkey;
614                     \Log::debug('SFTP privkey set');
615                 } else {
616                     throw new SE('$privkey must be a valid path', SE::E_PATH_INVALID);
617                 }
618 
619                 return $this;
620             }
621 
622             /**
623              * If no parameter is passed gets the SFTP private key password, otherwise sets it
624              *
625              * @author Art <a.molcanovas@gmail.com>
626              *
627              * @param string $pw The password
628              *
629              * @throws SE When the password isn't scalar
630              * @return SFTP
631              */
632             function pw($pw = '') {
633                 if ($pw === '') {
634                     return $this->pw;
635                 } elseif (is_scalar($pw)) {
636                     $this->pw = $pw;
637                     \Log::debug('SFTP password set');
638                 } else {
639                     throw new SE('Invalid password provided', SE::E_PW_INVALID);
640                 }
641 
642                 return $this;
643             }
644 
645             /**
646              * If no argument is passed, gets the working directory, otherwise sets it.
647              *
648              * @author Art <a.molcanovas@gmail.com>
649              *
650              * @param mixed $dir
651              *
652              * @throws SE When the name is invalid
653              * @return SFTP
654              */
655             function dir($dir = -1) {
656                 if ($dir === -1) {
657                     return $this->dir;
658                 } elseif (is_scalar($dir)) {
659                     $dir = trim($dir, DIRECTORY_SEPARATOR . ' ');
660                     if (!$dir || $dir == DIRECTORY_SEPARATOR) {
661                         $dir = '.' . DIRECTORY_SEPARATOR;
662                     }
663                     $this->dir = $dir;
664                     \Log::debug('Directory set to ' . $dir);
665                 } else {
666                     throw new SE('Directory name not scalar', SE::E_NAME_INVALID);
667                 }
668 
669                 return $this;
670             }
671 
672         }
673     }
674 
AloFramework documentation API documentation generated byApiGen 2.8.0