provision.file.inc 13 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
  /**
   * 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.
   */
31
  function writable($path) {
32 33 34 35 36 37
    $this->_clear_state();

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

    return $this;
38 39
  }

40 41 42 43 44 45 46 47
  /**
   * Determine if $path exists.
   *
   * Sets @path token for ->succeed and ->fail.
   *
   * @param $path
   *   The path you want to perform this operation on.
   */
48
  function exists($path) {
49
    $this->_clear_state();
50

51 52 53 54
    $this->last_status = file_exists($path);
    $this->tokens = array('@path' => $path);

    return $this;
55 56
  }

57 58 59 60 61 62 63 64
  /**
   * Determine if $path is readable.
   *
   * Sets @path token for ->succeed and ->fail.
   *
   * @param $path
   *   The path you want to perform this operation on.
   */
65
  function readable($path) {
66
    $this->_clear_state();
67

68 69
    $this->last_status = is_readable($path);
    $this->tokens = array('@path' => $path);
70

71
    return $this;
72 73
  }

74 75 76 77 78 79 80 81
  /**
   * Create the $path directory.
   *
   * Sets @path token for ->succeed and ->fail.
   *
   * @param $path
   *   The path you want to perform this operation on.
   */
82
  function mkdir($path) {
83 84 85 86 87 88
    $this->_clear_state();

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

    return $this;
89 90
  }

91 92 93 94 95 96 97 98
  /**
   * Delete the directory $path.
   *
   * Sets @path token for ->succeed and ->fail.
   *
   * @param $path
   *   The path you want to perform this operation on.
   */
99
  function rmdir($path) {
100 101 102 103 104 105
    $this->_clear_state();

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

    return $this;
106 107
  }

108 109 110 111 112 113 114 115
  /**
   * Delete the file $path.
   *
   * Sets @path token for ->succeed and ->fail.
   *
   * @param $path
   *   The path you want to perform this operation on.
   */
116
  function unlink($path) {
117 118
    $this->_clear_state();

drumm's avatar
drumm committed
119 120 121 122 123 124
    if (file_exists($path) || is_link($path)) {
      $this->last_status = unlink($path);
    }
    else {
      $this->last_status = TRUE;
    }
125 126 127
    $this->tokens = array('@path' => $path);

    return $this;
128 129
  }

130 131 132 133 134
  /**
   * Change the file permissions of $path to the octal value in $perms.
   *
   * @param $perms
   *   An octal value denoting the desired file permissions.
135
   */
136 137
  function chmod($path, $perms, $recursive = FALSE) {
    $this->_clear_state();
138

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

141
    $func = ($recursive) ? array($this, '_chmod_recursive') : 'chmod';
142
    if (!@call_user_func($func, $path, $perms)) {
143
      $this->tokens['@reason'] = dt('chmod to @perm failed on @path', array('@perm' => sprintf('%o', $perms), '@path' => $path));
144 145
    }
    clearstatcache(); // this needs to be called, otherwise we get the old info 
146 147 148
    $this->last_status = substr(sprintf('%o', fileperms($path)), -4) == sprintf('%04o', $perms);

    return $this;
149 150
  }

151 152 153
  /**
   * Change the owner of $path to the user in $owner.
   *
drumm's avatar
drumm committed
154
   * Sets @path, @uid, and @reason tokens for ->succeed and ->fail.
155 156 157 158 159 160 161 162 163 164
   *
   * @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
165
    $this->tokens = array('@path' => $path, '@uid' => $owner);
166

drumm's avatar
drumm committed
167
    // We do not attempt to chown symlinks.
168
    if (is_link($path)) {
drumm's avatar
drumm committed
169
      return $this;
170
    } 
171

172 173 174
    $func = ($recursive) ? array($this, '_chown_recursive') : 'chown';
    if ($owner = provision_posix_username($owner)) {
      if (!call_user_func($func, $path, $owner)) {
175
        $this->tokens['@reason'] = dt("chown to @owner failed on @path", array('@owner' => $owner, '@path' => $path)) ; 
176 177 178
      }
    }
    else {
179
      $this->tokens['@reason'] = dt("the user does not exist");
180 181 182
    }

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

    return $this;
186 187
  }

188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
  /**
   * 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
204
    // We do not attempt to chown symlinks.
205
    if (is_link($path)) {
drumm's avatar
drumm committed
206
      return $this;
207
    } 
208

209 210
    $func = ($recursive) ? array($this, '_chgrp_recursive') : 'chgrp';
    if ($group = provision_posix_groupname($gid)) {
211
      if (provision_user_in_group(get_current_user(), $gid)) {
212 213
        if (!call_user_func($func, $path, $group)) {
          $this->tokens['@reason'] = dt("chgrp to @group failed on @path", array('@group' => $group, '@path' => $path));
214 215 216
        }
      }
      else {
217
        $this->tokens['@reason'] = dt("@user is not in @group group", array("@user" => get_current_user(), "@group" => $group));
218 219 220
      }
    }
    elseif (!@call_user_func($func, $path, $gid)) { # try to change the group anyways
221
      $this->tokens['@reason'] = dt("the group does not exist");
222 223 224
    }

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

227
    return $this;
228 229
  }

230 231 232 233 234 235 236 237 238 239
  /**
   * 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();
240

241
    $this->tokens = array('@path1' => $path1, '@path2' => $path2);
242

243 244
    $this->last_status = FALSE;

245
    //TODO : Add error reasons.
246
    $temp = $path1 . '.tmp';
247
    if (!file_exists($path1)) {
248
      $this->last_status = rename($path2, $path1);
249 250
    }
    elseif (!file_exists($path2)) {
251
      $this->last_status = rename($path1, $path2);
252 253 254 255
    }
    elseif (rename($path1, $temp)) { 
      if (rename($path2, $path1)) {
        if (rename($temp, $path2)) {
256
          $this->last_status = TRUE; // path1 is now path2
257 258 259
        }
        else {
          // same .. just in reverse
260
          $this->last_status = rename($path1, $path2) && rename($temp, $path1);
261 262 263 264
        }
      }
      else {
        // same .. just in reverse
265
        $this->last_status = rename($temp, $path1);
266 267
      }   
    }
268 269

    return $this;
270 271
  }

272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
  /**
   * 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);

287 288
    if (file_exists($path) && is_readable($path)) {
      if (is_writeable(dirname($target)) && !file_exists($target) && !is_dir($target)) {
289
        $this->mkdir($target);
290 291 292 293 294 295 296 297 298 299
        $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)));
        $result = provision_shell_exec($command, $path);
        chdir($oldcwd);

        if ($result && is_writeable(dirname($target)) && is_readable(dirname($target)) && is_dir($target)) {
300
          $this->last_status = TRUE;
301 302
        }
        else {
303 304
          $this->tokens['@reason'] = dt('The file could not be extracted');
          $this->last_status = FALSE;
305 306 307
        }
      }
      else {
308 309
        $this->tokens['@reason'] = dt('The target directory could not be written to');
        $this->last_status = FALSE;
310 311 312
      }
    }
    else {
313 314
      $this->tokens['@reason'] = dt('Backup file could not be opened');
      $this->last_status = FALSE;
315
    }
316 317

    return $this;
318 319
  }

320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
  /**
   * 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);

335
    if (file_exists($target) && !is_link($target)) {
336 337
      $this->tokens['@reason'] = dt("A file already exists at @path");
      $this->last_status = FALSE;
338
    }
339 340 341
    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;
342
    }
343 344
    elseif (is_link($target) && (readlink($target) == $path)) {
      $this->last_status = TRUE;
345
    }
346 347
    elseif (symlink($path, $target)) {
      $this->last_status = TRUE;
348 349
    }
    else {
350 351
      $this->tokens['@reason'] = dt('The symlink could not be created, an error has occured');
      $this->last_status = FALSE;
352
    }
353 354

    return $this;
355 356 357 358 359 360
  }

  /**
   * Small helper function for creation of configuration directories.
   */
  function create_dir($path, $name, $perms) {
361 362 363 364
    $exists = $this->exists($path)
      ->succeed($name . ' path @path exists.')
      ->fail($name . ' path @path does not exist.')
      ->status();
365 366

    if (!$exists) {
367 368 369 370
      $exists = $this->mkdir($path)
        ->succeed($name . ' path @path has been created.')
        ->fail($name . ' path @path could not be created.', 'DRUSH_PERM_ERROR')
        ->status();
371 372 373
    }

    if ($exists) {
374
      $this->chown($path, get_current_user())
drumm's avatar
drumm committed
375 376
        ->succeed($name . ' ownership of @path has been changed to @uid.')
        ->fail($name . ' ownership of @path could not be changed to @uid.', 'DRUSH_PERM_ERROR');
377

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

382 383 384
      $this->writable($path)
        ->succeed($name . ' path @path is writable.')
        ->fail($name . ' path @path is not writable.', 'DRUSH_PERM_ERROR');
385 386
    }

387
    return $exists;
388 389
  }

390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
  /**
   * 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()
   */
  function file_put_contents($path, $data, $flags) {
    $this->_clear_state();

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

    return $this;
  }

413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
  /**
   * 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
428 429 430 431 432 433
    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;
          }
434
        }
435
        closedir($dh);
436
      }
437
      $status = call_user_func($func, $path, $arg) && $status;
438 439 440 441 442 443 444 445 446
    }
    return $status;
  }

  /**
   * Chmod a directory recursively
   *
   */
  function _chmod_recursive($path, $filemode) {
447
    return $this->_call_recursive('chmod', $path, $filemode);
448 449 450 451 452 453
  }

  /**
   * Chown a directory recursively
   */
  function _chown_recursive($path, $owner) {
454
    return $this->_call_recursive('chown', $path, $owner);
455 456 457 458 459
  }

  /**
   * Chgrp a directory recursively
   */
460 461
  function _chgrp_recursive($path, $group) {
    return $this->_call_recursive('chgrp', $path, $group);
462
  }
463

464

465
}