Overview

Namespaces

  • GAubry
    • ErrorHandler
    • Helpers
    • Logger
    • Shell
  • Himedia
    • Padocc
      • DB
      • Minifier
      • Numbering
      • Properties
      • Task
        • Base
        • Extended
  • None
  • Psr
    • Log

Classes

  • PathStatus
  • ShellAdapter
  • Overview
  • Namespace
  • Class
  • Tree
  • Deprecated
  • Todo
  1: <?php
  2: 
  3: namespace GAubry\Shell;
  4: 
  5: use Psr\Log\LoggerInterface;
  6: use GAubry\Helpers\Helpers;
  7: 
  8: /**
  9:  *
 10:  */
 11: class ShellAdapter
 12: {
 13: 
 14:     /**
 15:      * Cache of status of file system paths.
 16:      *
 17:      * @var array
 18:      * @see getPathStatus()
 19:      * @see Shell_PathStatus
 20:      */
 21:     private $_aFileStatus;
 22: 
 23:     /**
 24:      * PSR-3 logger
 25:      *
 26:      * @var \Psr\Log\LoggerInterface
 27:      * @see exec()
 28:      */
 29:     private $_oLogger;
 30: 
 31:     /**
 32:      * Default configuration.
 33:      *
 34:      * @var array
 35:      */
 36:     private static $aDefaultConfig = array(
 37:         // (string) Path of Bash:
 38:         'bash_path' => '/bin/bash',
 39: 
 40:         // (string) List of '-o option' options used for all SSH and SCP commands:
 41:         'ssh_options' => '-o ServerAliveInterval=10 -o StrictHostKeyChecking=no -o ConnectTimeout=10 -o BatchMode=yes',
 42: 
 43:         // (int) Maximal number of command shells launched simultaneously (parallel processes):
 44:         'parallelization_max_nb_processes' => 10,
 45: 
 46:         // (int) Maximal number of parallel RSYNC (overriding 'parallelization_max_nb_processes'):
 47:         'rsync_max_nb_processes' => 5,
 48: 
 49:         // (array) List of exclusion patterns for RSYNC command (converted into list of '--exclude <pattern>'):
 50:         'default_rsync_exclude' => array(
 51:             '.bzr/', '.cvsignore', '.git/', '.gitignore', '.svn/', 'cvslog.*', 'CVS', 'CVS.adm'
 52:         )
 53:     );
 54: 
 55:     /**
 56:      * Current configuration.
 57:      *
 58:      * @var array
 59:      * @see $aDefaultConfig
 60:      */
 61:     private $_aConfig;
 62: 
 63:     /**
 64:      * Bash pattern command to call 'parallelize.sh' script.
 65:      *
 66:      * @var string
 67:      */
 68:     private $sParallelizeCmdPattern;
 69: 
 70:     /**
 71:      * Constructor.
 72:      *
 73:      * @param \Psr\Log\LoggerInterface $oLogger Used to log exectued shell commands
 74:      * @param array $aConfig see $aDefaultConfig
 75:      */
 76:     public function __construct (LoggerInterface $oLogger, array $aConfig = array())
 77:     {
 78:         $this->_oLogger = $oLogger;
 79:         $this->_aConfig = Helpers::arrayMergeRecursiveDistinct(self::$aDefaultConfig, $aConfig);
 80:         $this->_aFileStatus = array();
 81:         $this->sParallelizeCmdPattern = $this->_aConfig['bash_path']
 82:                                       . ' ' . realpath(__DIR__ . '/../../inc/parallelize.sh') . ' "%s" "%s"';
 83:     }
 84: 
 85:     /**
 86:      * Launch parallel processes running pattern filled with each of specified values.
 87:      * If number of values is greater than $iMax, then several batches are launched.
 88:      *
 89:      * Example: $this->parallelize(array('user1@server', 'user2@server'), "ssh [] /bin/bash <<EOF\nls -l\nEOF\n", 2);
 90:      * Example: $this->parallelize(array('a', 'b'), 'cat /path/to/resources/[].txt', 2);
 91:      *
 92:      * @param array $aValues liste de valeurs qui viendront remplacer le(s) '[]' du pattern
 93:      * @param string $sPattern pattern possédant une ou plusieurs occurences de paires de crochets vides '[]'
 94:      * qui seront remplacées dans les processus lancés en parallèle par l'une des valeurs spécifiées.
 95:      * @param int $iMax nombre maximal de processus lancés en parallèles
 96:      * @return array liste de tableau associatif : array(
 97:      *     array(
 98:      *         'value' => (string)"l'une des valeurs de $aValues",
 99:      *         'error_code' => (int)code de retour Shell,
100:      *         'elapsed_time' => (int) temps approximatif en secondes,
101:      *         'cmd' => (string) commande shell exécutée,
102:      *         'output' => (string) sortie standard,
103:      *         'error' => (string) sortie d'erreur standard,
104:      *     ), ...
105:      * )
106:      * @throws \RuntimeException si le moindre code de retour Shell non nul apparaît.
107:      * @throws \RuntimeException si une valeur hors de $aValues apparaît dans les entrées 'value'.
108:      * @throws \RuntimeException s'il manque des valeurs de $aValues dans le résultat final.
109:      */
110:     public function parallelize (array $aValues, $sPattern, $iMax)
111:     {
112:         // Segmentation de la demande de parallélisation en lots séquentiels de taille maîtrisée :
113:         $aAllValues = $aValues;
114:         $aAllResults = array();
115:         while (count($aValues) > $iMax) {
116:             $aSubset = array_slice($aValues, 0, $iMax);
117:             $aAllResults = array_merge($aAllResults, $this->parallelize($aSubset, $sPattern, $iMax));
118:             $aValues = array_slice($aValues, $iMax);
119:         }
120: 
121:         // Exécution de la demande de parallélisation :
122:         $sCmd = sprintf(
123:             $this->sParallelizeCmdPattern,
124:             addcslashes(implode(' ', $aValues), '"'),
125:             addcslashes($sPattern, '"')
126:         );
127:         $aExecResult = $this->exec($sCmd);
128: 
129:         // Découpage du flux de retour d'exécution :
130:         $sResult = implode("\n", $aExecResult) . "\n";
131:         $sRegExp = '#^---\[(.*?)\]-->(\d+)\|(\d+)s\n\[CMD\]\n(.*?)\n\[OUT\]\n(.*?)\[ERR\]\n(.*?)///#ms';
132:         preg_match_all($sRegExp, $sResult, $aMatches, PREG_SET_ORDER);
133: 
134:         // Formatage des résultats :
135:         $aResult = array();
136:         foreach ($aMatches as $aSet) {
137:             $aResult[] = array(
138:                 'value' => $aSet[1],
139:                 'error_code' => (int)$aSet[2],
140:                 'elapsed_time' => (int)$aSet[3],
141:                 'cmd' => $aSet[4],
142:                 'output' => (strlen($aSet[5]) > 0 ? substr($aSet[5], 0, -1) : ''),
143:                 'error' => (strlen($aSet[6]) > 0 ? substr($aSet[6], 0, -1) : '')
144:             );
145:         }
146: 
147:         // Pas de code d'erreur shell ni de valeur non attendue ?
148:         foreach ($aResult as $aSubResult) {
149:             if ($aSubResult['error_code'] !== 0) {
150:                 $sMsg = $aSubResult['error'] . "\nParallel result:\n" . print_r($aResult, true);
151:                 throw new \RuntimeException($sMsg, $aSubResult['error_code']);
152:             } else if ( ! in_array($aSubResult['value'], $aValues)) {
153:                 $sMsg = "Not asked value: '" . $aSubResult['value'] . "'!\n"
154:                       . "Aksed values: '" . implode("', '", $aValues) . "'\n"
155:                       . "Parallel result:\n" . print_r($aResult, true);
156:                 throw new \RuntimeException($sMsg, 1);
157:             }
158:         }
159: 
160:         // Tous le monde est-il là ?
161:         $aAllResults = array_merge($aAllResults, $aResult);
162:         if (count($aAllResults) != count($aAllValues)) {
163:             $sMsg = "Missing values!\n"
164:                   . "Aksed values: '" . implode("', '", $aValues) . "'\n"
165:                   . "Parallel result:\n" . print_r($aAllResults, true);
166:             throw new \RuntimeException($sMsg, 1);
167:         }
168: 
169:         return $aAllResults;
170:     }
171: 
172:     /**
173:      * Exécute la commande shell spécifiée et retourne la sortie découpée par ligne dans un tableau.
174:      * En cas d'erreur shell (code d'erreur <> 0), lance une exception incluant le message d'erreur.
175:      *
176:      * @param string $sCmd
177:      * @return array tableau indexé du flux de sortie shell découpé par ligne
178:      * @throws \RuntimeException en cas d'erreur shell
179:      */
180:     public function exec ($sCmd)
181:     {
182:         $this->_oLogger->debug('[DEBUG] shell# ' . trim($sCmd, " \t"));
183:         $sFullCmd = '( ' . $sCmd . ' ) 2>&1';
184:         exec($sFullCmd, $aResult, $iReturnCode);
185:         if ($iReturnCode !== 0) {
186:             throw new \RuntimeException(
187:                 "Exit code not null: $iReturnCode. Result: '" . implode("\n", $aResult) . "'",
188:                 $iReturnCode
189:             );
190:         }
191:         return $aResult;
192:     }
193: 
194:     /**
195:      * Exécute la commande shell spécifiée en l'encapsulant au besoin dans une connexion SSH
196:      * pour atteindre les hôtes distants.
197:      *
198:      * @param string $sPatternCmd commande au format printf
199:      * @param string $sParam paramètre du pattern $sPatternCmd, permettant en plus de décider si l'on
200:      * doit encapsuler la commande dans un SSH (si serveur distant) ou non.
201:      * @return array tableau indexé du flux de sortie shell découpé par ligne
202:      * @throws \RuntimeException en cas d'erreur shell
203:      * @see isRemotePath()
204:      */
205:     public function execSSH ($sPatternCmd, $sParam)
206:     {
207:         return $this->exec($this->buildSSHCmd($sPatternCmd, $sParam));
208:     }
209: 
210:     /**
211:      * Retourne la commande Shell spécifiée envoyée à sprintf avec $sParam,
212:      * et encapsule au besoin le tout dans une connexion SSH
213:      * pour atteindre les hôtes distants (si $sParam est un hôte distant).
214:      *
215:      * @param string $sPatternCmd commande au format printf
216:      * @param string $sParam paramètre du pattern $sPatternCmd, permettant en plus de décider si l'on
217:      * doit encapsuler la commande dans un SSH (si serveur distant) ou non.
218:      * @return string la commande Shell spécifiée envoyée à sprintf avec $sParam,
219:      * et encapsule au besoin le tout dans une connexion SSH
220:      * pour atteindre les hôtes distants (si $sParam est un hôte distant).
221:      * @see isRemotePath()
222:      */
223:     public function buildSSHCmd ($sPatternCmd, $sParam)
224:     {
225:         list($bIsRemote, $sServer, $sRealPath) = $this->isRemotePath($sParam);
226:         $sCmd = sprintf($sPatternCmd, $this->escapePath($sRealPath));
227:         //$sCmd = vsprintf($sPatternCmd, array_map(array(self, 'escapePath'), $mParams));
228:         if ($bIsRemote) {
229:             $sCmd = 'ssh ' . $this->_aConfig['ssh_options'] . " -T $sServer "
230:                   . $this->_aConfig['bash_path'] . " <<EOF\n$sCmd\nEOF\n";
231:         }
232:         return $sCmd;
233:     }
234: 
235:     /**
236:      * Retourne l'une des constantes de Shell_PathStatus, indiquant pour le chemin spécifié s'il est
237:      * inexistant, un fichier, un répertoire, un lien symbolique sur fichier ou encore un lien symbolique sur
238:      * répertoire.
239:      *
240:      * Les éventuels slash terminaux sont supprimés.
241:      * Si le statut est différent de inexistant, l'appel est mis en cache.
242:      * Un appel à remove() s'efforce de maintenir cohérent ce cache.
243:      *
244:      * Le chemin spécifié peut concerner un hôte distant (user@server:/path), auquel cas un appel SSH sera effectué.
245:      *
246:      * @param string $sPath chemin à tester, de la forme [user@server:]/path
247:      * @return int l'une des constantes de Shell_PathStatus
248:      * @throws \RuntimeException en cas d'erreur shell
249:      * @see Shell_PathStatus
250:      * @see _aFileStatus
251:      */
252:     public function getPathStatus ($sPath)
253:     {
254:         if (substr($sPath, -1) === '/') {
255:             $sPath = substr($sPath, 0, -1);
256:         }
257:         if (isset($this->_aFileStatus[$sPath])) {
258:             $iStatus = $this->_aFileStatus[$sPath];
259:         } else {
260:             $sFormat = '[ -h %1$s ] && echo -n 1; [ -d %1$s ] && echo 2 || ([ -f %1$s ] && echo 1 || echo 0)';
261:             //$aResult = $this->execSSH($sFormat, $sPath);
262:             $aResult = $this->exec($this->buildSSHCmd($sFormat, $sPath));
263:             $iStatus = (int)$aResult[0];
264:             if ($iStatus !== 0) {
265:                 $this->_aFileStatus[$sPath] = $iStatus;
266:             }
267:         }
268:         return $iStatus;
269:     }
270: 
271:     /**
272:      * Pour chaque serveur retourne l'une des constantes de Shell_PathStatus, indiquant pour le chemin spécifié
273:      * s'il est inexistant, un fichier, un répertoire, un lien symbolique sur fichier
274:      * ou encore un lien symbolique sur répertoire.
275:      *
276:      * Comme getPathStatus(), mais sur une liste de serveurs.
277:      *
278:      * Les éventuels slash terminaux sont supprimés.
279:      * Si le statut est différent de inexistant, l'appel est mis en cache.
280:      * Un appel à remove() s'efforce de maintenir cohérent ce cache.
281:      *
282:      * @param string $sPath chemin à tester, sans mention de serveur
283:      * @param array $aServers liste de serveurs sur lesquels faire la demande de statut
284:      * @return array tableau associatif listant par serveur (clé) le status (valeur, constante de Shell_PathStatus)
285:      * @throws \RuntimeException en cas d'erreur shell
286:      * @see getPathStatus()
287:      */
288:     public function getParallelSSHPathStatus ($sPath, array $aServers)
289:     {
290:         if (substr($sPath, -1) === '/') {
291:             $sPath = substr($sPath, 0, -1);
292:         }
293: 
294:         // Déterminer les serveurs pour lesquels nous n'avons pas la réponse en cache :
295:         $aResult = array();
296:         foreach ($aServers as $sServer) {
297:             $sKey = $sServer . ':' . $sPath;
298:             if (isset($this->_aFileStatus[$sKey])) {
299:                 $aResult[$sServer] = $this->_aFileStatus[$sKey];
300:             }
301:         }
302:         $aServersToCheck = array_diff($aServers, array_keys($aResult));
303: 
304:         // Paralléliser l'appel sur chacun des serveurs restants :
305:         if (count($aServersToCheck) > 0) {
306:             $sFormat = '[ -h %1$s ] && echo -n 1; [ -d %1$s ] && echo 2 || ([ -f %1$s ] && echo 1 || echo 0)';
307:             $sPattern = $this->buildSSHCmd($sFormat, '[]:' . $sPath);
308:             $aParallelResult = $this->parallelize($aServersToCheck, $sPattern, $this->_aConfig['parallelization_max_nb_processes']);
309: 
310:             // Traiter les résultats et MAJ le cache :
311:             foreach ($aParallelResult as $aServerResult) {
312:                 $sServer = $aServerResult['value'];
313:                 $iStatus = (int)$aServerResult['output'];
314:                 if ($iStatus !== 0) {
315:                     $this->_aFileStatus[$sServer . ':' . $sPath] = $iStatus;
316:                 }
317:                 $aResult[$sServer] = $iStatus;
318:             }
319:         }
320: 
321:         return $aResult;
322:     }
323: 
324:     /**
325:      * Retourne un triplet dont la 1re valeur (bool) indique si le chemin spécifié commence par
326:      * '[user@]servername_or_ip:', la 2e (string) est le serveur (ou chaîne vide si $sPath est local),
327:      * et la 3e (string) est le chemin dépourvu de l'éventuel serveur.
328:      *
329:      * @param string $sPath chemin au format [[user@]servername_or_ip:]/path
330:      * @return array triplet dont la 1re valeur (bool) indique si le chemin spécifié commence par
331:      * '[user@]servername_or_ip:', la 2e (string) est le serveur (ou chaîne vide si $sPath est local),
332:      * et la 3e (string) est le chemin dépourvu de l'éventuel serveur.
333:      */
334:     public function isRemotePath ($sPath)
335:     {
336:         $result = preg_match('/^((?:[^@]+@)?[^:]+):(.+)$/i', $sPath, $aMatches);
337:         $bIsRemotePath = ($result === 1);
338:         if ($bIsRemotePath) {
339:             $sServer = $aMatches[1];
340:             $sRealPath = $aMatches[2];
341:         } else {
342:             $sServer = '';
343:             $sRealPath = $sPath;
344:         }
345: 
346:         return array($bIsRemotePath, $sServer, $sRealPath);
347:     }
348: 
349:     /**
350:      * Copie un chemin vers un autre.
351:      * Les jokers '*' et '?' sont autorisés.
352:      * Par exemple copiera le contenu de $sSrcPath si celui-ci se termine par '/*'.
353:      * Si le chemin de destination n'existe pas, il sera créé.
354:      *
355:      * @param string $sSrcPath chemin source, au format [[user@]hostname_or_ip:]/path
356:      * @param string $sDestPath chemin de destination, au format [[user@]hostname_or_ip:]/path
357:      * @param bool $bIsDestFile précise si le chemin de destination est un simple fichier ou non,
358:      * information nécessaire si l'on doit créer une partie de ce chemin si inexistant
359:      * @return array tableau indexé du flux de sortie shell découpé par ligne
360:      * @throws \RuntimeException en cas d'erreur shell
361:      */
362:     public function copy ($sSrcPath, $sDestPath, $bIsDestFile=false)
363:     {
364:         if ($bIsDestFile) {
365:             $this->mkdir(pathinfo($sDestPath, PATHINFO_DIRNAME));
366:         } else {
367:             $this->mkdir($sDestPath);
368:         }
369:         list(, $sSrcServer, ) = $this->isRemotePath($sSrcPath);
370:         list(, $sDestServer, $sDestRealPath) = $this->isRemotePath($sDestPath);
371: 
372:         if ($sSrcServer != $sDestServer) {
373:             $sCmd = 'scp ' . $this->_aConfig['ssh_options'] . ' -rpq '
374:                   . $this->escapePath($sSrcPath) . ' ' . $this->escapePath($sDestPath);
375:             return $this->exec($sCmd);
376:         } else {
377:             $sCmd = 'cp -a %s ' . $this->escapePath($sDestRealPath);
378:             return $this->execSSH($sCmd, $sSrcPath);
379:         }
380:     }
381: 
382:     /**
383:      * Crée un lien symbolique de chemin $sLinkPath vers la cible $sTargetPath.
384:      *
385:      * @param string $sLinkPath nom du lien, au format [[user@]hostname_or_ip:]/path
386:      * @param string $sTargetPath cible sur laquelle faire pointer le lien, au format [[user@]hostname_or_ip:]/path
387:      * @return array tableau indexé du flux de sortie shell découpé par ligne
388:      * @throws \DomainException si les chemins référencent des serveurs différents
389:      * @throws \RuntimeException en cas d'erreur shell
390:      */
391:     public function createLink ($sLinkPath, $sTargetPath)
392:     {
393:         list(, $sLinkServer, ) = $this->isRemotePath($sLinkPath);
394:         list(, $sTargetServer, $sTargetRealPath) = $this->isRemotePath($sTargetPath);
395:         if ($sLinkServer != $sTargetServer) {
396:             throw new \DomainException("Hosts must be equals. Link='$sLinkPath'. Target='$sTargetPath'.");
397:         }
398:         $aResult = $this->execSSH('mkdir -p "$(dirname %1$s)" && ln -snf "' . $sTargetRealPath . '" %1$s', $sLinkPath);
399:         // TODO optimisation possible :
400:         // $this->_aFileStatus[$sPath] = Shell_PathStatus::STATUS_SYMLINKED_DIR ou STATUS_SYMLINKED_FILE;
401:         return $aResult;
402:     }
403: 
404:     /**
405:      * Entoure le chemin de guillemets doubles en tenant compte des jokers '*' et '?' qui ne les supportent pas.
406:      * Par exemple : '/a/b/img*jpg', donnera : '"/a/b/img"*"jpg"'.
407:      * Pour rappel, '*' vaut pour 0 à n caractères, '?' vaut pour exactement 1 caractère (et non 0 à 1).
408:      *
409:      * @param string $sPath
410:      * @return string
411:      */
412:     public function escapePath ($sPath)
413:     {
414:         $sEscapedPath = preg_replace('#(\*|\?)#', '"\1"', '"' . $sPath . '"');
415:         $sEscapedPath = str_replace('""', '', $sEscapedPath);
416:         return $sEscapedPath;
417:     }
418: 
419:     /**
420:      * Supprime le chemin spécifié, répertoire ou fichier, distant ou local.
421:      * S'efforce de maintenir cohérent le cache de statut de chemins rempli par getPathStatus().
422:      *
423:      * @param string $sPath chemin à supprimer, au format [[user@]hostname_or_ip:]/path
424:      * @return array tableau indexé du flux de sortie shell découpé par ligne
425:      * @throws \DomainException si chemin invalide (garde-fou)
426:      * @throws \RuntimeException en cas d'erreur shell
427:      * @see getPathStatus()
428:      */
429:     public function remove ($sPath)
430:     {
431:         $sPath = trim($sPath);
432: 
433:         // Garde-fou :
434:         if (empty($sPath) || strlen($sPath) < 4) {
435:             throw new \DomainException("Illegal path: '$sPath'");
436:         }
437: 
438:         // Supprimer du cache de getPathStatus() :
439:         foreach (array_keys($this->_aFileStatus) as $sCachedPath) {
440:             if (substr($sCachedPath, 0, strlen($sPath)+1) === $sPath . '/') {
441:                 unset($this->_aFileStatus[$sCachedPath]);
442:             }
443:         }
444:         unset($this->_aFileStatus[$sPath]);
445: 
446:         return $this->execSSH('rm -rf %s', $sPath);
447:     }
448: 
449:     /**
450:      * Effectue un tar gzip du répertoire $sSrcPath dans $sBackupPath.
451:      *
452:      * @param string $sSrcPath au format [[user@]hostname_or_ip:]/path
453:      * @param string $sBackupPath au format [[user@]hostname_or_ip:]/path
454:      * @return array tableau indexé du flux de sortie shell découpé par ligne
455:      * @throws \RuntimeException en cas d'erreur shell
456:      */
457:     public function backup ($sSrcPath, $sBackupPath)
458:     {
459:         list($bIsSrcRemote, $sSrcServer, $sSrcRealPath) = $this->isRemotePath($sSrcPath);
460:         list(, $sBackupServer, $sBackupRealPath) = $this->isRemotePath($sBackupPath);
461: 
462:         if ($sSrcServer != $sBackupServer) {
463:             $sTmpDir = ($bIsSrcRemote ? $sSrcServer. ':' : '') . realpath(sys_get_temp_dir()) . '/'
464:                      . uniqid('deployment_', true);
465:             $sTmpPath = $sTmpDir . '/' . pathinfo($sBackupPath, PATHINFO_BASENAME);
466:             return array_merge(
467:                 $this->backup($sSrcPath, $sTmpPath),
468:                 $this->copy($sTmpPath, $sBackupPath, true),
469:                 $this->remove($sTmpDir)
470:             );
471:         } else {
472:             $this->mkdir(pathinfo($sBackupPath, PATHINFO_DIRNAME));
473:             $sSrcFile = pathinfo($sSrcRealPath, PATHINFO_BASENAME);
474:             $sFormat = 'cd %1$s; tar cfpz %2$s ./%3$s';
475:             if ($bIsSrcRemote) {
476:                 $sSrcDir = pathinfo($sSrcRealPath, PATHINFO_DIRNAME);
477:                 $sFormat = 'ssh %4$s <<EOF' . "\n" . $sFormat . "\nEOF\n";
478:                 $sCmd = sprintf(
479:                     $sFormat,
480:                     $this->escapePath($sSrcDir),
481:                     $this->escapePath($sBackupRealPath),
482:                     $this->escapePath($sSrcFile),
483:                     $sSrcServer
484:                 );
485:             } else {
486:                 $sSrcDir = pathinfo($sSrcPath, PATHINFO_DIRNAME);
487:                 $sCmd = sprintf(
488:                     $sFormat,
489:                     $this->escapePath($sSrcDir),
490:                     $this->escapePath($sBackupPath),
491:                     $this->escapePath($sSrcFile)
492:                 );
493:             }
494:             return $this->exec($sCmd);
495:         }
496:     }
497: 
498:     /**
499:      * Crée le chemin spécifié s'il n'existe pas déjà, avec les droits éventuellement transmis dans tous les cas.
500:      *
501:      * @param string $sPath chemin à créer, au format [[user@]hostname_or_ip:]/path
502:      * @param string $sMode droits utilisateur du chemin appliqués même si ce dernier existe déjà.
503:      * Par exemple '644'.
504:      * @param array $aValues liste de valeurs (string) optionnelles pour générer autant de demandes de
505:      * synchronisation en parallèle. Dans ce cas ces valeurs viendront remplacer l'une après l'autre
506:      * les occurences de crochets vide '[]' présents dans $sSrcPath ou $sDestPath.
507:      * @throws \RuntimeException en cas d'erreur shell
508:      */
509:     public function mkdir ($sPath, $sMode='', array $aValues=array())
510:     {
511:         // On passe par 'chmod' car 'mkdir -m xxx' exécuté ssi répertoire inexistant :
512:         if ($sMode !== '') {
513:             $sMode = " && chmod $sMode %1\$s";
514:         }
515:         $sPattern = "mkdir -p %1\$s$sMode";
516:         $sCmd = $this->buildSSHCmd($sPattern, $sPath);
517:         //var_dump($sPath, $sPattern, $sCmd);
518: 
519:         if (strpos($sPath, '[]') !== false && count($aValues) > 0) {
520:             $aParallelResult = $this->parallelize($aValues, $sCmd, $this->_aConfig['parallelization_max_nb_processes']);
521: 
522:             // Traiter les résultats et MAJ le cache :
523:             foreach ($aParallelResult as $aServerResult) {
524:                 $sValue = $aServerResult['value'];
525:                 $sFinalPath = str_replace('[]', $sValue, $sPath);
526:                 $this->_aFileStatus[$sFinalPath] = PathStatus::STATUS_DIR;
527:             }
528:         } else {
529:             $this->exec($sCmd);
530:             $this->_aFileStatus[$sPath] = PathStatus::STATUS_DIR;
531:         }
532:     }
533: 
534:     /**
535:      * Synchronise une source avec une ou plusieurs destinations.
536:      *
537:      * @param string $sSrcPath au format [[user@]hostname_or_ip:]/path
538:      * @param string $sDestPath chaque destination au format [[user@]hostname_or_ip:]/path
539:      * @param array $aValues liste de valeurs (string) optionnelles pour générer autant de demandes de
540:      * synchronisation en parallèle. Dans ce cas ces valeurs viendront remplacer l'une après l'autre
541:      * les occurences de crochets vide '[]' présents dans $sSrcPath ou $sDestPath.
542:      * @param array $aIncludedPaths chemins à transmettre aux paramètres --include de la commande shell rsync.
543:      * Il précéderons les paramètres --exclude.
544:      * @param array $aExcludedPaths chemins à transmettre aux paramètres --exclude de la commande shell rsync
545:      * @param string $sRsyncPattern
546:      * @return array tableau indexé du flux de sortie shell des commandes rsync exécutées,
547:      * découpé par ligne et analysé par _resumeSyncResult()
548:      */
549:     public function sync ($sSrcPath, $sDestPath, array $aValues=array(),
550:             array $aIncludedPaths=array(), array $aExcludedPaths=array(),
551:             $sRsyncPattern='')
552:     {
553:         if (empty($sRsyncPattern)) {
554:             $sRsyncPattern = 'rsync -axz --delete %1$s%2$s--stats -e "ssh '
555:                            . $this->_aConfig['ssh_options'] . '" %3$s %4$s';
556:         }
557: 
558:         // Cas non gérés :
559:         list($bIsSrcRemote, $sSrcServer, $sSrcRealPath) = $this->isRemotePath($sSrcPath);
560:         list($bIsDestRemote, $sDestServer, $sDestRealPath) = $this->isRemotePath($sDestPath);
561:         $this->mkdir($sDestPath, '', $aValues);
562: 
563:         // Inclusions / exclusions :
564:         $sIncludedPaths = (count($aIncludedPaths) === 0
565:                               ? ''
566:                               : '--include="' . implode('" --include="', array_unique($aIncludedPaths)) . '" ');
567:         $aExcludedPaths = array_unique(array_merge($this->_aConfig['default_rsync_exclude'], $aExcludedPaths));
568:         $sExcludedPaths = (count($aExcludedPaths) === 0
569:                               ? ''
570:                               : '--exclude="' . implode('" --exclude="', $aExcludedPaths) . '" ');
571: 
572:         // Construction de la commande :
573:         $sRsyncCmd = sprintf(
574:             $sRsyncPattern,
575:             $sIncludedPaths, $sExcludedPaths, '%s', '%s'
576:         );
577:         if (substr($sSrcPath, -2) === '/*') {
578:             $sRsyncCmd = 'if ls -1 "' . substr($sSrcRealPath, 0, -2) . '" | grep -q .; then ' . $sRsyncCmd . '; fi';
579:         }
580:         if ($bIsSrcRemote && $bIsDestRemote) {
581:             $sFinalDestPath = ($sSrcServer == $sDestServer ? $sDestRealPath : $sDestPath);
582:             $sRsyncCmd = sprintf($sRsyncCmd, '%s', $this->escapePath($sFinalDestPath));
583:             $sRsyncCmd = $this->buildSSHCmd($sRsyncCmd, $sSrcPath);
584:         } else {
585:             $sRsyncCmd = sprintf($sRsyncCmd, $this->escapePath($sSrcPath), $this->escapePath($sDestPath));
586:         }
587: 
588:         if (count($aValues) === 0 || (count($aValues) === 1 && $aValues[0] == '')) {
589:             $aValues=array('-');
590:         }
591:         $aParallelResult = $this->parallelize($aValues, $sRsyncCmd, $this->_aConfig['rsync_max_nb_processes']);
592:         $aAllResults = array();
593:         foreach ($aParallelResult as $aServerResult) {
594:             if ($aServerResult['value'] == '-') {
595:                 $sHeader = '';
596:             } else {
597:                 $sHeader = "Server: " . $aServerResult['value']
598:                          . ' (~' . $aServerResult['elapsed_time'] . 's)' . "\n";
599:             }
600:             $aRawOutput = explode("\n", $aServerResult['output']);
601:             $sOutput = $this->_resumeSyncResult($aRawOutput);
602:             $aOutput = array($sHeader . $sOutput);
603:             $aAllResults = array_merge($aAllResults, $aOutput);
604:         }
605: 
606:         return $aAllResults;
607:     }
608: 
609:     /**
610:      * Analyse la sortie shell de commandes rsync et en propose un résumé.
611:      *
612:      * Exemple :
613:      *  - entrée :
614:      *      Number of files: 1774
615:      *      Number of files transferred: 2
616:      *      Total file size: 64093953 bytes
617:      *      Total transferred file size: 178 bytes
618:      *      Literal data: 178 bytes
619:      *      Matched data: 0 bytes
620:      *      File list size: 39177
621:      *      File list generation time: 0.013 seconds
622:      *      File list transfer time: 0.000 seconds
623:      *      Total bytes sent: 39542
624:      *      Total bytes received: 64
625:      *      sent 39542 bytes  received 64 bytes  26404.00 bytes/sec
626:      *      total size is 64093953  speedup is 1618.29
627:      *  - sortie :
628:      *      Number of transferred files ( / total): 2 / 1774
629:      *      Total transferred file size ( / total): <1 / 61 Mio
630:      *
631:      * @param array $aRawResult tableau indexé du flux de sortie shell de la commande rsync, découpé par ligne
632:      * @return array du tableau indexé du flux de sortie shell de commandes rsync résumé
633:      * et découpé par ligne
634:      */
635:     private function _resumeSyncResult (array $aRawResult)
636:     {
637:         if (count($aRawResult) === 0 || (count($aRawResult) === 1 && $aRawResult[0] == '')) {
638:             $sResult = 'Empty source directory.';
639:         } else {
640:             $aKeys = array(
641:                 'number of files',
642:                 'number of files transferred',
643:                 'total file size',
644:                 'total transferred file size',
645:             );
646:             $aEmptyStats = array_fill_keys($aKeys, '?');
647: 
648:             $aStats = $aEmptyStats;
649:             foreach ($aRawResult as $sLine) {
650:                 if (preg_match('/^([^:]+):\s(\d+)\b/i', $sLine, $aMatches) === 1) {
651:                     $sKey = strtolower($aMatches[1]);
652:                     if (isset($aStats[$sKey])) {
653:                         $aStats[$sKey] = (int)$aMatches[2];
654:                     }
655:                 }
656:             }
657: 
658:             list($sTransferred, $sTransfUnit) =
659:                 Helpers::intToMultiple($aStats['total transferred file size'], true);
660:             list($sTotal, $sTotalUnit) = Helpers::intToMultiple($aStats['total file size'], true);
661: 
662:             $sResult = 'Number of transferred files ( / total): ' . $aStats['number of files transferred']
663:                      . ' / ' . $aStats['number of files'] . "\n"
664:                      . 'Total transferred file size ( / total): '
665:                      . round($sTransferred) . ' ' . $sTransfUnit . 'o / ' . round($sTotal) . ' ' . $sTotalUnit . 'o';
666:         }
667:         return $sResult;
668:     }
669: }
670: 
Platform for Automatized Deployments with pOwerful Concise Configuration API documentation generated by ApiGen 2.8.0