14bf055c1cef30bd6d5f69864991caaa34bd007f
[yaffs-website] / vendor / drush / drush / includes / filesystem.inc
1 <?php
2
3 /**
4  * @file
5  * Filesystem utilities.
6  */
7 use Webmozart\PathUtil\Path;
8
9 /**
10  * @defgroup filesystemfunctions Filesystem convenience functions.
11  * @{
12  */
13
14 /**
15  * Behavior for drush_copy_dir() and drush_move_dir() when destinations exist.
16  */
17 define('FILE_EXISTS_ABORT', 0);
18 define('FILE_EXISTS_OVERWRITE', 1);
19 define('FILE_EXISTS_MERGE', 2);
20
21  /**
22   * Determines whether the provided path is absolute or not
23   * on the specified O.S. -- starts with "/" on *nix, or starts
24   * with "[A-Z]:\" or "[A-Z]:/" on Windows.
25   */
26 function drush_is_absolute_path($path, $os = NULL) {
27   // Relative paths will never start with a '/', even on Windows,
28   // so it is safe to just call all paths that start with a '/'
29   // absolute.  This simplifies things for Windows with CYGWIN / MINGW / CWRSYNC,
30   // where absolute paths sometimes start c:\path and sometimes
31   // start /cygdrive/c/path.
32   if ($path[0] == '/') {
33     return TRUE;
34   }
35   if (drush_is_windows($os)) {
36     return preg_match('@^[a-zA-Z]:[\\\/]@', $path);
37   }
38   return FALSE;
39 }
40
41 /**
42  * If we are going to pass a path to exec or proc_open,
43  * then we need to fix it up under CYGWIN or MINGW.  In
44  * both of these environments, PHP works with absolute paths
45  * such as "C:\path".  CYGWIN expects these to be converted
46  * to "/cygdrive/c/path" and MINGW expects these to be converted
47  * to "/c/path"; otherwise, the exec will not work.
48  *
49  * This call does nothing if the parameter is not an absolute
50  * path, or we are not running under CYGWIN / MINGW.
51  *
52  * UPDATE:  It seems I was mistaken; this is only necessary if we
53  * are using cwRsync.  We do not need to correct every path to
54  * exec or proc_open (thank god).
55  */
56 function drush_correct_absolute_path_for_exec($path, $os = NULL) {
57   if (drush_is_windows() && drush_is_absolute_path($path, "WINNT")) {
58     if (drush_is_mingw($os)) {
59       $path = preg_replace('/(\w):/', '/${1}', str_replace('\\', '/', $path));
60     }
61     elseif (drush_is_cygwin($os)) {
62       $path = preg_replace('/(\w):/', '/cygdrive/${1}', str_replace('\\', '/', $path));
63     }
64   }
65   return $path;
66 }
67
68 /**
69  * Remove the trailing DIRECTORY_SEPARATOR from a path.
70  * Will actually remove either / or \ on Windows.
71  */
72 function drush_trim_path($path, $os = NULL) {
73   if (drush_is_windows($os)) {
74     return rtrim($path, '/\\');
75   }
76   else {
77     return rtrim($path, '/');
78   }
79 }
80
81 /**
82  * Makes sure the path has only path separators native for the current operating system
83  */
84 function drush_normalize_path($path) {
85   if (drush_is_windows()) {
86     $path = str_replace('/', '\\',  strtolower($path));
87   }
88   else {
89     $path = str_replace('\\', '/', $path);
90   }
91   return drush_trim_path($path);
92 }
93
94 /**
95  * Calculates a single md5 hash for all files a directory (incuding subdirectories)
96  */
97 function drush_dir_md5($dir) {
98   $flist = drush_scan_directory($dir, '/./', array('.', '..'), 0, TRUE, 'filename', 0, TRUE);
99   $hashes = array();
100   foreach ($flist as $f) {
101     $sum = array();
102     exec('cksum ' . escapeshellarg($f->filename), $sum);
103     $hashes[] = trim(str_replace(array($dir), array(''), $sum[0]));
104   }
105   sort($hashes);
106   return md5(implode("\n", $hashes));
107 }
108
109 /**
110  * Deletes the specified file or directory and everything inside it.
111  *
112  * Usually respects read-only files and folders. To do a forced delete use
113  * drush_delete_tmp_dir() or set the parameter $forced.
114  *
115  * @param string $dir
116  *   The file or directory to delete.
117  * @param bool $force
118  *   Whether or not to try everything possible to delete the directory, even if
119  *   it's read-only. Defaults to FALSE.
120  * @param bool $follow_symlinks
121  *   Whether or not to delete symlinked files. Defaults to FALSE--simply
122  *   unlinking symbolic links.
123  *
124  * @return bool
125  *   FALSE on failure, TRUE if everything was deleted.
126  */
127 function drush_delete_dir($dir, $force = FALSE, $follow_symlinks = FALSE) {
128   // Do not delete symlinked files, only unlink symbolic links
129   if (is_link($dir) && !$follow_symlinks) {
130     return unlink($dir);
131   }
132   // Allow to delete symlinks even if the target doesn't exist.
133   if (!is_link($dir) && !file_exists($dir)) {
134     return TRUE;
135   }
136   if (!is_dir($dir)) {
137     if ($force) {
138       // Force deletion of items with readonly flag.
139       @chmod($dir, 0777);
140     }
141     return unlink($dir);
142   }
143   if (drush_delete_dir_contents($dir, $force) === FALSE) {
144     return FALSE;
145   }
146   if ($force) {
147     // Force deletion of items with readonly flag.
148     @chmod($dir, 0777);
149   }
150   return rmdir($dir);
151 }
152
153 /**
154  * Deletes the contents of a directory.
155  *
156  * @param string $dir
157  *   The directory to delete.
158  * @param bool $force
159  *   Whether or not to try everything possible to delete the contents, even if
160  *   they're read-only. Defaults to FALSE.
161  *
162  * @return bool
163  *   FALSE on failure, TRUE if everything was deleted.
164  */
165 function drush_delete_dir_contents($dir, $force = FALSE) {
166   $scandir = @scandir($dir);
167   if (!is_array($scandir)) {
168     return FALSE;
169   }
170
171   foreach ($scandir as $item) {
172     if ($item == '.' || $item == '..') {
173       continue;
174     }
175     if ($force) {
176       @chmod($dir, 0777);
177     }
178     if (!drush_delete_dir($dir . '/' . $item, $force)) {
179       return FALSE;
180     }
181   }
182   return TRUE;
183 }
184
185 /**
186  * Deletes the provided file or folder and everything inside it.
187  * This function explicitely tries to delete read-only files / folders.
188  *
189  * @param $dir
190  *   The directory to delete
191  * @return
192  *   FALSE on failure, TRUE if everything was deleted
193  */
194 function drush_delete_tmp_dir($dir) {
195   return drush_delete_dir($dir, TRUE);
196 }
197
198 /**
199  * Copy $src to $dest.
200  *
201  * @param $src
202  *   The directory to copy.
203  * @param $dest
204  *   The destination to copy the source to, including the new name of
205  *   the directory.  To copy directory "a" from "/b" to "/c", then
206  *   $src = "/b/a" and $dest = "/c/a".  To copy "a" to "/c" and rename
207  *   it to "d", then $dest = "/c/d".
208  * @param $overwrite
209  *   Action to take if destination already exists.
210  *     - FILE_EXISTS_OVERWRITE - completely removes existing directory.
211  *     - FILE_EXISTS_ABORT - aborts the operation.
212  *     - FILE_EXISTS_MERGE - Leaves existing files and directories in place.
213  * @return
214  *   TRUE on success, FALSE on failure.
215  */
216 function drush_copy_dir($src, $dest, $overwrite = FILE_EXISTS_ABORT) {
217   // Preflight based on $overwrite if $dest exists.
218   if (file_exists($dest)) {
219     if ($overwrite === FILE_EXISTS_OVERWRITE) {
220       drush_op('drush_delete_dir', $dest, TRUE);
221     }
222     elseif ($overwrite === FILE_EXISTS_ABORT) {
223       return drush_set_error('DRUSH_DESTINATION_EXISTS', dt('Destination directory !dest already exists.', array('!dest' => $dest)));
224     }
225     elseif ($overwrite === FILE_EXISTS_MERGE) {
226       // $overwrite flag may indicate we should merge instead.
227       drush_log(dt('Merging existing !dest directory', array('!dest' => $dest)));
228     }
229   }
230   // $src readable?
231   if (!is_readable($src)) {
232     return drush_set_error('DRUSH_SOURCE_NOT_EXISTS', dt('Source directory !src is not readable or does not exist.', array('!src' => $src)));
233   }
234   // $dest writable?
235   if (!is_writable(dirname($dest))) {
236     return drush_set_error('DRUSH_DESTINATION_NOT_WRITABLE', dt('Destination directory !dest is not writable.', array('!dest' => dirname($dest))));
237   }
238   // Try to do a recursive copy.
239   if (@drush_op('_drush_recursive_copy', $src, $dest)) {
240     return TRUE;
241   }
242
243   return drush_set_error('DRUSH_COPY_DIR_FAILURE', dt('Unable to copy !src to !dest.', array('!src' => $src, '!dest' => $dest)));
244 }
245
246 /**
247  * Internal function called by drush_copy_dir; do not use directly.
248  */
249 function _drush_recursive_copy($src, $dest) {
250   // all subdirectories and contents:
251   if(is_dir($src)) {
252     if (!drush_mkdir($dest, TRUE)) {
253       return FALSE;
254     }
255     $dir_handle = opendir($src);
256     while($file = readdir($dir_handle)) {
257       if ($file != "." && $file != "..") {
258         if (_drush_recursive_copy("$src/$file", "$dest/$file") !== TRUE) {
259           return FALSE;
260         }
261       }
262     }
263     closedir($dir_handle);
264   }
265   elseif (is_link($src)) {
266     symlink(readlink($src), $dest);
267   }
268   elseif (!copy($src, $dest)) {
269     return FALSE;
270   }
271
272   // Preserve file modification time.
273   // https://github.com/drush-ops/drush/pull/1146
274   touch($dest, filemtime($src));
275
276   // Preserve execute permission.
277   if (!is_link($src) && !drush_is_windows()) {
278     // Get execute bits of $src.
279     $execperms = fileperms($src) & 0111;
280     // Apply execute permissions if any.
281     if ($execperms > 0) {
282       $perms = fileperms($dest) | $execperms;
283       chmod($dest, $perms);
284     }
285   }
286
287   return TRUE;
288 }
289
290 /**
291  * Move $src to $dest.
292  *
293  * If the php 'rename' function doesn't work, then we'll do copy & delete.
294  *
295  * @param $src
296  *   The directory to move.
297  * @param $dest
298  *   The destination to move the source to, including the new name of
299  *   the directory.  To move directory "a" from "/b" to "/c", then
300  *   $src = "/b/a" and $dest = "/c/a".  To move "a" to "/c" and rename
301  *   it to "d", then $dest = "/c/d" (just like php rename function).
302  * @param $overwrite
303  *   If TRUE, the destination will be deleted if it exists.
304  * @return
305  *   TRUE on success, FALSE on failure.
306  */
307 function drush_move_dir($src, $dest, $overwrite = FALSE) {
308   // Preflight based on $overwrite if $dest exists.
309   if (file_exists($dest)) {
310     if ($overwrite) {
311       drush_op('drush_delete_dir', $dest, TRUE);
312     }
313     else {
314       return drush_set_error('DRUSH_DESTINATION_EXISTS', dt('Destination directory !dest already exists.', array('!dest' => $dest)));
315     }
316   }
317   // $src readable?
318   if (!drush_op('is_readable', $src)) {
319     return drush_set_error('DRUSH_SOURCE_NOT_EXISTS', dt('Source directory !src is not readable or does not exist.', array('!src' => $src)));
320   }
321   // $dest writable?
322   if (!drush_op('is_writable', dirname($dest))) {
323     return drush_set_error('DRUSH_DESTINATION_NOT_WRITABLE', dt('Destination directory !dest is not writable.', array('!dest' => dirname($dest))));
324   }
325   // Try rename. It will fail if $src and $dest are not in the same partition.
326   if (@drush_op('rename', $src, $dest)) {
327     return TRUE;
328   }
329   // Eventually it will create an empty file in $dest. See
330   // http://www.php.net/manual/es/function.rename.php#90025
331   elseif (is_file($dest)) {
332     drush_op('unlink', $dest);
333   }
334
335   // If 'rename' fails, then we will use copy followed
336   // by a delete of the source.
337   if (drush_copy_dir($src, $dest)) {
338     drush_op('drush_delete_dir', $src, TRUE);
339     return TRUE;
340   }
341
342   return drush_set_error('DRUSH_MOVE_DIR_FAILURE', dt('Unable to move !src to !dest.', array('!src' => $src, '!dest' => $dest)));
343 }
344
345 /**
346  * Cross-platform compatible helper function to recursively create a directory tree.
347  *
348  * @param path
349  *   Path to directory to create.
350  * @param required
351  *   If TRUE, then drush_mkdir will call drush_set_error on failure.
352  *
353  * Callers should *always* do their own error handling after calling drush_mkdir.
354  * If $required is FALSE, then a different location should be selected, and a final
355  * error message should be displayed if no usable locations can be found.
356  * @see drush_directory_cache().
357  * If $required is TRUE, then the execution of the current command should be
358  * halted if the required directory cannot be created.
359  */
360 function drush_mkdir($path, $required = TRUE) {
361   if (!is_dir($path)) {
362     if (drush_mkdir(dirname($path))) {
363       if (@mkdir($path)) {
364         return TRUE;
365       }
366       elseif (is_dir($path) && is_writable($path)) {
367         // The directory was created by a concurrent process.
368         return TRUE;
369       }
370       else {
371         if (!$required) {
372           return FALSE;
373         }
374         if (is_writable(dirname($path))) {
375           return drush_set_error('DRUSH_CREATE_DIR_FAILURE', dt('Unable to create !dir.', array('!dir' => preg_replace('/\w+\/\.\.\//', '', $path))));
376         }
377         else {
378           return drush_set_error('DRUSH_PARENT_NOT_WRITABLE', dt('Unable to create !newdir in !dir. Please check directory permissions.', array('!newdir' => basename($path), '!dir' => realpath(dirname($path)))));
379         }
380       }
381     }
382     return FALSE;
383   }
384   else {
385     if (!is_writable($path)) {
386       if (!$required) {
387         return FALSE;
388       }
389       return drush_set_error('DRUSH_DESTINATION_NOT_WRITABLE', dt('Directory !dir exists, but is not writable. Please check directory permissions.', array('!dir' => realpath($path))));
390     }
391     return TRUE;
392   }
393 }
394
395 /*
396  * Determine if program exists on user's PATH.
397  *
398  * @return bool|null
399  */
400 function drush_program_exists($program) {
401   if (drush_has_bash()) {
402     $bucket = drush_bit_bucket();
403     return drush_op_system("command -v $program >$bucket 2>&1") === 0 ? TRUE : FALSE;
404   }
405 }
406
407 /**
408  * Save a string to a temporary file. Does not depend on Drupal's API.
409  * The temporary file will be automatically deleted when drush exits.
410  *
411  * @param string $data
412  * @param string $suffix
413  *   Append string to filename. use of this parameter if is discouraged. @see
414  *   drush_tempnam().
415  * @return string
416  *   A path to the file.
417  */
418 function drush_save_data_to_temp_file($data, $suffix = NULL) {
419   static $fp;
420
421   $file = drush_tempnam('drush_', NULL, $suffix);
422   $fp = fopen($file, "w");
423   fwrite($fp, $data);
424   $meta_data = stream_get_meta_data($fp);
425   $file = $meta_data['uri'];
426   fclose($fp);
427
428   return $file;
429 }
430
431 /**
432  * Returns the path to a temporary directory.
433  *
434  * This is a custom version of Drupal's file_directory_path().
435  * We can't directly rely on sys_get_temp_dir() as this
436  * path is not valid in some setups for Mac, and we want to honor
437  * an environment variable (used by tests).
438  */
439 function drush_find_tmp() {
440   static $temporary_directory;
441
442   if (!isset($temporary_directory)) {
443     $directories = array();
444
445     // Get user specific and operating system temp folders from system environment variables.
446     // See http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/ntcmds_shelloverview.mspx?mfr=true
447     $tempdir = getenv('TEMP');
448     if (!empty($tempdir)) {
449       $directories[] = $tempdir;
450     }
451     $tmpdir = getenv('TMP');
452     if (!empty($tmpdir)) {
453       $directories[] = $tmpdir;
454     }
455     // Operating system specific dirs.
456     if (drush_is_windows()) {
457       $windir = getenv('WINDIR');
458       if (isset($windir)) {
459         // WINDIR itself is not writable, but it always contains a /Temp dir,
460         // which is the system-wide temporary directory on older versions. Newer
461         // versions only allow system processes to use it.
462         $directories[] = Path::join($windir, 'Temp');
463       }
464     }
465     else {
466       $directories[] = Path::canonicalize('/tmp');
467     }
468     $directories[] = Path::canonicalize(sys_get_temp_dir());
469
470     foreach ($directories as $directory) {
471       if (is_dir($directory) && is_writable($directory)) {
472         $temporary_directory = $directory;
473         break;
474       }
475     }
476
477     if (empty($temporary_directory)) {
478       // If no directory has been found, create one in cwd.
479       $temporary_directory = Path::join(drush_cwd(), 'tmp');
480       drush_mkdir($temporary_directory, TRUE);
481       if (!is_dir($temporary_directory)) {
482         return drush_set_error('DRUSH_UNABLE_TO_CREATE_TMP_DIR', dt("Unable to create a temporary directory."));
483       }
484       drush_register_file_for_deletion($temporary_directory);
485     }
486   }
487
488   return $temporary_directory;
489 }
490
491 /**
492  * Creates a temporary file, and registers it so that
493  * it will be deleted when drush exits.  Whenever possible,
494  * drush_save_data_to_temp_file() should be used instead
495  * of this function.
496  *
497  * @param string $suffix
498  *   Append this suffix to the filename. Use of this parameter is discouraged as
499  *   it can break the guarantee of tempname(). See http://www.php.net/manual/en/function.tempnam.php#42052.
500  *   Originally added to support Oracle driver.
501  */
502 function drush_tempnam($pattern, $tmp_dir = NULL, $suffix = '') {
503   if ($tmp_dir == NULL) {
504     $tmp_dir = drush_find_tmp();
505   }
506   $tmp_file = tempnam($tmp_dir, $pattern);
507   drush_register_file_for_deletion($tmp_file);
508   $tmp_file_with_suffix = $tmp_file . $suffix;
509   drush_register_file_for_deletion($tmp_file_with_suffix);
510   return $tmp_file_with_suffix;
511 }
512
513 /**
514  * Creates a temporary directory and return its path.
515  */
516 function drush_tempdir() {
517   $tmp_dir = drush_trim_path(drush_find_tmp());
518   $tmp_dir .= '/' . 'drush_tmp_' . uniqid(time() . '_');
519
520   drush_mkdir($tmp_dir);
521   drush_register_file_for_deletion($tmp_dir);
522
523   return $tmp_dir;
524 }
525
526 /**
527  * Any file passed in to this function will be deleted
528  * when drush exits.
529  */
530 function drush_register_file_for_deletion($file = NULL) {
531   static $registered_files = array();
532
533   if (isset($file)) {
534     if (empty($registered_files)) {
535       register_shutdown_function('_drush_delete_registered_files');
536     }
537     $registered_files[] = $file;
538   }
539
540   return $registered_files;
541 }
542
543 /**
544  * Delete all of the registered temporary files.
545  */
546 function _drush_delete_registered_files() {
547   $files_to_delete = drush_register_file_for_deletion();
548
549   foreach ($files_to_delete as $file) {
550     // We'll make sure that the file still exists, just
551     // in case someone came along and deleted it, even
552     // though they did not need to.
553     if (file_exists($file)) {
554       if (is_dir($file)) {
555         drush_delete_dir($file, TRUE);
556       }
557       else {
558         @chmod($file, 0777); // Make file writeable
559         unlink($file);
560       }
561     }
562   }
563 }
564
565 /**
566  * Decide where our backup directory should go
567  *
568  * @param string $subdir
569  *   The name of the desired subdirectory(s) under drush-backups.
570  *   Usually a database name.
571  */
572 function drush_preflight_backup_dir($subdir = NULL) {
573   $backup_dir = drush_get_context('DRUSH_BACKUP_DIR', drush_get_option('backup-location'));
574
575   if (empty($backup_dir)) {
576     // Try to use db name as subdir if none was provided.
577     if (empty($subdir)) {
578       $subdir = 'unknown';
579       if ($sql = drush_sql_get_class()) {
580         $db_spec = $sql->db_spec();
581         $subdir = $db_spec['database'];
582       }
583     }
584
585     // Save the date to be used in the backup directory's path name.
586     $date = gmdate('YmdHis', $_SERVER['REQUEST_TIME']);
587
588     $backup_dir = drush_get_option('backup-dir', Path::join(drush_server_home(), 'drush-backups'));
589     $backup_dir = Path::join($backup_dir, $subdir, $date);
590     drush_set_context('DRUSH_BACKUP_DIR', $backup_dir);
591   }
592   else {
593     Path::canonicalize($backup_dir);
594   }
595   return $backup_dir;
596 }
597
598 /**
599  * Prepare a backup directory
600  */
601 function drush_prepare_backup_dir($subdir = NULL) {
602   $backup_dir = drush_preflight_backup_dir($subdir);
603   $backup_parent = Path::getDirectory($backup_dir);
604   $drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT');
605
606   if ((!empty($drupal_root)) && (strpos($backup_parent, $drupal_root) === 0)) {
607     return drush_set_error('DRUSH_PM_BACKUP_FAILED', dt('It\'s not allowed to store backups inside the Drupal root directory.'));
608   }
609   if (!file_exists($backup_parent)) {
610     if (!drush_mkdir($backup_parent, TRUE)) {
611       return drush_set_error('DRUSH_PM_BACKUP_FAILED', dt('Unable to create backup directory !dir.', array('!dir' => $backup_parent)));
612     }
613   }
614   if (!is_writable($backup_parent)) {
615     return drush_set_error('DRUSH_PM_BACKUP_FAILED', dt('Backup directory !dir is not writable.', array('!dir' => $backup_parent)));
616   }
617
618   if (!drush_mkdir($backup_dir, TRUE)) {
619     return FALSE;
620   }
621   return $backup_dir;
622 }
623
624 /**
625  * Test to see if a file exists and is not empty
626  */
627 function drush_file_not_empty($file_to_test) {
628   if (file_exists($file_to_test)) {
629     clearstatcache();
630     $stat = stat($file_to_test);
631     if ($stat['size'] > 0) {
632       return TRUE;
633     }
634   }
635   return FALSE;
636 }
637
638 /**
639  * Finds all files that match a given mask in a given directory.
640  * Directories and files beginning with a period are excluded; this
641  * prevents hidden files and directories (such as SVN working directories
642  * and GIT repositories) from being scanned.
643  *
644  * @param $dir
645  *   The base directory for the scan, without trailing slash.
646  * @param $mask
647  *   The regular expression of the files to find.
648  * @param $nomask
649  *   An array of files/directories to ignore.
650  * @param $callback
651  *   The callback function to call for each match.
652  * @param $recurse_max_depth
653  *   When TRUE, the directory scan will recurse the entire tree
654  *   starting at the provided directory.  When FALSE, only files
655  *   in the provided directory are returned.  Integer values
656  *   limit the depth of the traversal, with zero being treated
657  *   identically to FALSE, and 1 limiting the traversal to the
658  *   provided directory and its immediate children only, and so on.
659  * @param $key
660  *   The key to be used for the returned array of files. Possible
661  *   values are "filename", for the path starting with $dir,
662  *   "basename", for the basename of the file, and "name" for the name
663  *   of the file without an extension.
664  * @param $min_depth
665  *   Minimum depth of directories to return files from.
666  * @param $include_dot_files
667  *   If TRUE, files that begin with a '.' will be returned if they
668  *   match the provided mask.  If FALSE, files that begin with a '.'
669  *   will not be returned, even if they match the provided mask.
670  * @param $depth
671  *   Current depth of recursion. This parameter is only used internally and should not be passed.
672  *
673  * @return
674  *   An associative array (keyed on the provided key) of objects with
675  *   "path", "basename", and "name" members corresponding to the
676  *   matching files.
677  */
678 function drush_scan_directory($dir, $mask, $nomask = array('.', '..', 'CVS'), $callback = 0, $recurse_max_depth = TRUE, $key = 'filename', $min_depth = 0, $include_dot_files = FALSE, $depth = 0) {
679   $key = (in_array($key, array('filename', 'basename', 'name')) ? $key : 'filename');
680   $files = array();
681
682   // Exclude Bower and Node directories.
683   $nomask = array_merge($nomask, array('node_modules', 'bower_components'));
684
685   if (is_string($dir) && is_dir($dir) && $handle = opendir($dir)) {
686     while (FALSE !== ($file = readdir($handle))) {
687       if (!in_array($file, $nomask) && (($include_dot_files && (!preg_match("/\.\+/",$file))) || ($file[0] != '.'))) {
688         if (is_dir("$dir/$file") && (($recurse_max_depth === TRUE) || ($depth < $recurse_max_depth))) {
689           // Give priority to files in this folder by merging them in after any subdirectory files.
690           $files = array_merge(drush_scan_directory("$dir/$file", $mask, $nomask, $callback, $recurse_max_depth, $key, $min_depth, $include_dot_files, $depth + 1), $files);
691         }
692         elseif ($depth >= $min_depth && preg_match($mask, $file)) {
693           // Always use this match over anything already set in $files with the same $$key.
694           $filename = "$dir/$file";
695           $basename = basename($file);
696           $name = substr($basename, 0, strrpos($basename, '.'));
697           $files[$$key] = new stdClass();
698           $files[$$key]->filename = $filename;
699           $files[$$key]->basename = $basename;
700           $files[$$key]->name = $name;
701           if ($callback) {
702             drush_op($callback, $filename);
703           }
704         }
705       }
706     }
707
708     closedir($handle);
709   }
710
711   return $files;
712 }
713
714 /**
715  * Simple helper function to append data to a given file.
716  *
717  * @param string $file
718  *   The full path to the file to append the data to.
719  * @param string $data
720  *   The data to append.
721  *
722  * @return boolean
723  *   TRUE on success, FALSE in case of failure to open or write to the file.
724  */
725 function drush_file_append_data($file, $data) {
726   if (!$fd = fopen($file, 'a+')) {
727     drush_set_error(dt("ERROR: fopen(@file, 'ab') failed", array('@file' => $file)));
728     return FALSE;
729   }
730   if (!fwrite($fd, $data)) {
731     drush_set_error(dt("ERROR: fwrite(@file) failed", array('@file' => $file)) . '<pre>' . $data);
732     return FALSE;
733   }
734   return TRUE;
735 }
736
737 /**
738  * Return 'TRUE' if one directory is located anywhere inside
739  * the other.
740  */
741 function drush_is_nested_directory($base_dir, $test_is_nested) {
742   $common = Path::getLongestCommonBasePath([$test_is_nested, $base_dir]);
743   return $common == Path::canonicalize($base_dir);
744 }
745
746 /**
747  * @} End of "defgroup filesystemfunctions".
748  */