provision.file.inc 13.4 KB
Newer Older
1 2 3 4 5 6 7 8 9
<?php
/**
 * @file Filesystem access module.
 *
 * Handle writing and syncing configuation files across multiple servers.
 * Provides an interface to common path handling operations, through the path
 * helper method, which will take care of verification and any error logging
 * required.
 */
10
include_once('provision.inc');
11

12 13
function provision_file() {
  static $instance = null;
14

15 16 17 18 19
  if (is_null($instance)) {
    $instance = new provisionFileSystem();
  }

  return $instance;
20 21
}

22
class provisionFileSystem extends provisionChainedState {
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
   /**
   * Copy file from $source to $destination.
   *
   * @param $source
   *   The path that you want copy.
   * @param $destination
   *   The destination path.
   */
  function copy($source, $destination) {
    $this->_clear_state();

    $this->tokens = array('@source' => $source, '@destination' => $destination);

    $this->last_status = FALSE;

    $this->last_status = copy($source, $destination);

    return $this;
  }


44 45 46 47 48 49 50 51
  /**
   * Determine if $path can be written to.
   *
   * Sets @path token for ->succeed and ->fail.
   *
   * @param $path
   *   The path you want to perform this operation on.
   */
52
  function writable($path) {
53 54 55 56 57 58
    $this->_clear_state();

    $this->last_status = is_writable($path);
    $this->tokens = array('@path' => $path);

    return $this;
59 60
  }

61 62 63 64 65 66 67 68
  /**
   * Determine if $path exists.
   *
   * Sets @path token for ->succeed and ->fail.
   *
   * @param $path
   *   The path you want to perform this operation on.
   */
69
  function exists($path) {
70
    $this->_clear_state();
71

72 73 74 75
    $this->last_status = file_exists($path);
    $this->tokens = array('@path' => $path);

    return $this;
76 77
  }

78 79 80 81 82 83 84 85
  /**
   * Determine if $path is readable.
   *
   * Sets @path token for ->succeed and ->fail.
   *
   * @param $path
   *   The path you want to perform this operation on.
   */
86
  function readable($path) {
87
    $this->_clear_state();
88

89 90
    $this->last_status = is_readable($path);
    $this->tokens = array('@path' => $path);
91

92
    return $this;
93 94
  }

95 96 97 98 99 100 101 102
  /**
   * Create the $path directory.
   *
   * Sets @path token for ->succeed and ->fail.
   *
   * @param $path
   *   The path you want to perform this operation on.
   */
103
  function mkdir($path) {
104 105 106 107 108 109
    $this->_clear_state();

    $this->last_status = mkdir($path, 0770, TRUE);
    $this->tokens = array('@path' => $path);

    return $this;
110 111
  }

112 113 114 115 116 117 118 119
  /**
   * Delete the directory $path.
   *
   * Sets @path token for ->succeed and ->fail.
   *
   * @param $path
   *   The path you want to perform this operation on.
   */
120
  function rmdir($path) {
121 122 123 124 125 126
    $this->_clear_state();

    $this->last_status = rmdir($path);
    $this->tokens = array('@path' => $path);

    return $this;
127 128
  }

129 130 131 132 133 134 135 136
  /**
   * Delete the file $path.
   *
   * Sets @path token for ->succeed and ->fail.
   *
   * @param $path
   *   The path you want to perform this operation on.
   */
137
  function unlink($path) {
138 139
    $this->_clear_state();

drumm's avatar
drumm committed
140 141 142 143 144 145
    if (file_exists($path) || is_link($path)) {
      $this->last_status = unlink($path);
    }
    else {
      $this->last_status = TRUE;
    }
146 147 148
    $this->tokens = array('@path' => $path);

    return $this;
149 150
  }

151 152 153 154 155
  /**
   * Change the file permissions of $path to the octal value in $perms.
   *
   * @param $perms
   *   An octal value denoting the desired file permissions.
156
   */
157 158
  function chmod($path, $perms, $recursive = FALSE) {
    $this->_clear_state();
159

160
    $this->tokens = array('@path' => $path, '@perm' => sprintf('%o', $perms));
161

162
    $func = ($recursive) ? array($this, '_chmod_recursive') : 'chmod';
163
    if (!@call_user_func($func, $path, $perms)) {
164
      $this->tokens['@reason'] = dt('chmod to @perm failed on @path', array('@perm' => sprintf('%o', $perms), '@path' => $path));
165 166
    }
    clearstatcache(); // this needs to be called, otherwise we get the old info 
167 168 169
    $this->last_status = substr(sprintf('%o', fileperms($path)), -4) == sprintf('%04o', $perms);

    return $this;
170 171
  }

172 173 174
  /**
   * Change the owner of $path to the user in $owner.
   *
drumm's avatar
drumm committed
175
   * Sets @path, @uid, and @reason tokens for ->succeed and ->fail.
176 177 178 179 180 181 182 183 184 185
   *
   * @param $path
   *   The path you want to perform this operation on.
   * @param $owner
   *   The name or user id you wish to change the file ownership to.
   * @param $recursive
   *   TRUE to descend into subdirectories.
   */
  function chown($path, $owner, $recursive = FALSE) {
    $this->_clear_state();
drumm's avatar
drumm committed
186
    $this->tokens = array('@path' => $path, '@uid' => $owner);
187

drumm's avatar
drumm committed
188
    // We do not attempt to chown symlinks.
189
    if (is_link($path)) {
drumm's avatar
drumm committed
190
      return $this;
191
    } 
192

193 194 195
    $func = ($recursive) ? array($this, '_chown_recursive') : 'chown';
    if ($owner = provision_posix_username($owner)) {
      if (!call_user_func($func, $path, $owner)) {
196
        $this->tokens['@reason'] = dt("chown to @owner failed on @path", array('@owner' => $owner, '@path' => $path)) ; 
197 198 199
      }
    }
    else {
200
      $this->tokens['@reason'] = dt("the user does not exist");
201 202 203
    }

    clearstatcache(); // this needs to be called, otherwise we get the old info 
204 205 206
    $this->last_status = $owner == provision_posix_username(fileowner($path));

    return $this;
207 208
  }

209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
  /**
   * Change the group of $path to the group in $gid.
   *
   * Sets @path, @gid, and @reason tokens for ->succeed and ->fail.
   *
   * @param $path
   *   The path you want to perform this operation on.
   * @param $gid
   *   The name of group id you wish to change the file group ownership to.
   * @param $recursive
   *   TRUE to descend into subdirectories.
   */
  function chgrp($path, $gid, $recursive = FALSE) {
    $this->_clear_state();
    $this->tokens = array('@path' => $path, '@gid' => $gid);

drumm's avatar
drumm committed
225
    // We do not attempt to chown symlinks.
226
    if (is_link($path)) {
drumm's avatar
drumm committed
227
      return $this;
228
    } 
229

230 231
    $func = ($recursive) ? array($this, '_chgrp_recursive') : 'chgrp';
    if ($group = provision_posix_groupname($gid)) {
232
      if (provision_user_in_group(provision_current_user(), $gid)) {
233 234
        if (!call_user_func($func, $path, $group)) {
          $this->tokens['@reason'] = dt("chgrp to @group failed on @path", array('@group' => $group, '@path' => $path));
235 236 237
        }
      }
      else {
238
        $this->tokens['@reason'] = dt("@user is not in @group group", array("@user" => provision_current_user(), "@group" => $group));
239 240 241
      }
    }
    elseif (!@call_user_func($func, $path, $gid)) { # try to change the group anyways
242
      $this->tokens['@reason'] = dt("the group does not exist");
243 244 245
    }

    clearstatcache(); // this needs to be called, otherwise we get the old info 
246
    $this->last_status = $group == provision_posix_groupname(filegroup($path));
247

248
    return $this;
249 250
  }

251 252 253 254 255 256 257 258 259 260
  /**
   * Move $path1 to $path2, and vice versa.
   *
   * @param $path1
   *   The path that you want to replace the $path2 with.
   * @param $path2
   *   The path that you want to replace the $path1 with.
   */
  function switch_paths($path1, $path2) {
    $this->_clear_state();
261

262
    $this->tokens = array('@path1' => $path1, '@path2' => $path2);
263

264 265
    $this->last_status = FALSE;

266
    //TODO : Add error reasons.
267
    $temp = $path1 . '.tmp';
268
    if (!file_exists($path1)) {
269
      $this->last_status = rename($path2, $path1);
270 271
    }
    elseif (!file_exists($path2)) {
272
      $this->last_status = rename($path1, $path2);
273 274 275 276
    }
    elseif (rename($path1, $temp)) { 
      if (rename($path2, $path1)) {
        if (rename($temp, $path2)) {
277
          $this->last_status = TRUE; // path1 is now path2
278 279 280
        }
        else {
          // same .. just in reverse
281
          $this->last_status = rename($path1, $path2) && rename($temp, $path1);
282 283 284 285
        }
      }
      else {
        // same .. just in reverse
286
        $this->last_status = rename($temp, $path1);
287 288
      }   
    }
289 290

    return $this;
291 292
  }

293 294


295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
  /**
   * Extract gzip-compressed tar archive.
   *
   * Sets @path, @target, and @reason tokens for ->succeed and ->fail.
   *
   * @param $path
   *   The path you want to extract.
   * @param $target
   *   The destination path to extract to.
   */
  function extract($path, $target) {
    $this->_clear_state();

    $this->tokens = array('@path' => $path, '@target' => $target);

310 311
    if (file_exists($path) && is_readable($path)) {
      if (is_writeable(dirname($target)) && !file_exists($target) && !is_dir($target)) {
312
        $this->mkdir($target);
313 314 315 316 317 318
        $oldcwd = getcwd();
        // we need to do this because some retarded implementations of tar (e.g. SunOS) don't support -C
        chdir($target);
        // same here: some do not support -z
        $command = 'gunzip -c %s | tar pxf -';
        drush_log(dt('Running: %command in %target', array('%command' => sprintf($command, $path), '%target' => $target)));
319
        $result = drush_shell_exec($command, $path);
320 321 322
        chdir($oldcwd);

        if ($result && is_writeable(dirname($target)) && is_readable(dirname($target)) && is_dir($target)) {
323
          $this->last_status = TRUE;
324 325
        }
        else {
326 327
          $this->tokens['@reason'] = dt('The file could not be extracted');
          $this->last_status = FALSE;
328 329 330
        }
      }
      else {
331 332
        $this->tokens['@reason'] = dt('The target directory could not be written to');
        $this->last_status = FALSE;
333 334 335
      }
    }
    else {
336 337
      $this->tokens['@reason'] = dt('Backup file could not be opened');
      $this->last_status = FALSE;
338
    }
339 340

    return $this;
341 342
  }

343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
  /**
   * Create a symlink from $path to $target.
   *
   * Sets @path, @target, and @reason tokens for ->succeed and ->fail.
   *
   * @param $path
   *   The path you want to perform this operation on.
   * @param $target
   *   The path you want the link to point to.
   */
  function symlink($path, $target) {
    $this->_clear_state();

    $this->tokens = array('@path' => $path, '@target' => $target);

358
    if (file_exists($target) && !is_link($target)) {
359 360
      $this->tokens['@reason'] = dt("A file already exists at @path");
      $this->last_status = FALSE;
361
    }
362 363 364
    elseif (is_link($target) && (readlink($target) != $path)) {
      $this->tokens['@reason'] = dt("A symlink already exists at target, but it is pointing to @link", array("@link" => readlink($target)));
      $this->last_status = FALSE;
365
    }
366 367
    elseif (is_link($target) && (readlink($target) == $path)) {
      $this->last_status = TRUE;
368
    }
369 370
    elseif (symlink($path, $target)) {
      $this->last_status = TRUE;
371 372
    }
    else {
373 374
      $this->tokens['@reason'] = dt('The symlink could not be created, an error has occured');
      $this->last_status = FALSE;
375
    }
376 377

    return $this;
378 379 380 381 382 383
  }

  /**
   * Small helper function for creation of configuration directories.
   */
  function create_dir($path, $name, $perms) {
384 385 386
    $exists = $this->exists($path)
      ->succeed($name . ' path @path exists.')
      ->status();
387 388

    if (!$exists) {
389 390 391 392
      $exists = $this->mkdir($path)
        ->succeed($name . ' path @path has been created.')
        ->fail($name . ' path @path could not be created.', 'DRUSH_PERM_ERROR')
        ->status();
393 394 395
    }

    if ($exists) {
396
      $this->chown($path, provision_current_user())
drumm's avatar
drumm committed
397 398
        ->succeed($name . ' ownership of @path has been changed to @uid.')
        ->fail($name . ' ownership of @path could not be changed to @uid.', 'DRUSH_PERM_ERROR');
399

400
      $this->chmod($path, $perms)
drumm's avatar
drumm committed
401 402
        ->succeed($name . ' permissions of @path have been changed to @perm.')
        ->fail($name . ' permissions of @path could not be changed to @perm.', 'DRUSH_PERM_ERROR');
403

404 405 406
      $this->writable($path)
        ->succeed($name . ' path @path is writable.')
        ->fail($name . ' path @path is not writable.', 'DRUSH_PERM_ERROR');
407 408
    }

409
    return $exists;
410 411
  }

412 413 414 415 416 417 418 419 420 421 422 423 424 425
  /**
   * Write $data to $path.
   *
   * Sets @path token for ->succeed and ->fail.
   *
   * @param $path
   *   The path you want to perform this operation on.
   * @param $data
   *   The data to write.
   * @param $flags
   *   The file_put_contents() flags to use.
   *
   * @see file_put_contents()
   */
426
  function file_put_contents($path, $data, $flags = 0) {
427 428 429 430 431 432 433 434
    $this->_clear_state();

    $this->tokens = array('@path' => $path);
    $this->last_status = file_put_contents($path, $data, $flags) !== FALSE;

    return $this;
  }

435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
  /**
   * Walk the given tree recursively (depth first), calling a function on each file
   *
   * $func is not checked for existence and called directly with $path and $arg
   * for every file encountered.
   *
   * @param string $func a valid callback, usually chmod, chown or chgrp
   * @param string $path a path in the filesystem
   * @param string $arg the second argument to $func
   * @return boolean returns TRUE if every $func call returns true
   */
  function _call_recursive($func, $path, $arg) {
    $status = 1;
    // do not follow symlinks as it could lead to a DOS attack
    // consider someone creating a symlink from files/foo to ..: it would create an infinite loop
450 451 452 453 454 455
    if (!is_link($path)) {
      if ($dh = @opendir($path)) {
        while (($file = readdir($dh)) !== false) {
          if ($file != '.' && $file != '..') {
            $status = $this->_call_recursive($func, $path . "/" . $file, $arg) && $status;
          }
456
        }
457
        closedir($dh);
458
      }
459
      $status = call_user_func($func, $path, $arg) && $status;
460 461 462 463 464 465 466 467 468
    }
    return $status;
  }

  /**
   * Chmod a directory recursively
   *
   */
  function _chmod_recursive($path, $filemode) {
469
    return $this->_call_recursive('chmod', $path, $filemode);
470 471 472 473 474 475
  }

  /**
   * Chown a directory recursively
   */
  function _chown_recursive($path, $owner) {
476
    return $this->_call_recursive('chown', $path, $owner);
477 478 479 480 481
  }

  /**
   * Chgrp a directory recursively
   */
482 483
  function _chgrp_recursive($path, $group) {
    return $this->_call_recursive('chgrp', $path, $group);
484
  }
485

486

487
}