When creating a new "File Directory", BAM let me create one using the URL of a remote file storage (for instance s3://). However, this does not work as source. Trying to create a backup result in the following error messages:

  • Directory does not exist.
  • No files available.
  • Could not complete the backup.

This is caused by the usage of drupal_realpath() in backup_migrate_destination_filesource. Real path only make sense for local filesystems.

BAM should either prevent creation of a "File Directory" source usign a remote file storage (on validation, if drupal_realpath() returns FALSE. Or add support for remote file storage in backup_migrate_destination_filesource. IMHO, support for remote file storage is probably best be left to additional module.

Comments

pbuyle created an issue. See original summary.

pbuyle’s picture

A quick test shows that Archive_Tar::addModify() used in backup_migrate_destination_filesource::_backup_to_file_php() supports remote file storage (tested with the Amazon S3 module stream wrapper).

pbuyle’s picture

Here is a class that implements a working remote file storage source. I'm posting this since I'm note sure I will have time to continue working on this.


/**
 * Backup and Migrate source for remote file storage.
 */
class backup_migrate_source_remotefilesource extends backup_migrate_source {

  function type_name() {
    return t("Remote Files Directory");
  }

  /**
   * Declare the current remote file storage as a backup sources.
   */
  function sources() {
    $out = array();
    $wrappers = file_get_stream_wrappers();
    foreach ($wrappers as $scheme => $wrapper) {
      if (!($wrapper['type'] & STREAM_WRAPPERS_LOCAL)) {
        $out["{$scheme}_files"] = backup_migrate_create_destination('remotefiles', array(
          'machine_name' => "{$scheme}_files",
          'location' => "$scheme://",
          'name' => $wrapper['name'],
          'show_in_list' => TRUE
        ));
      }
    }
    return $out;
  }

  /**
   * Get the form for the settings for the files destination.
   */
  function edit_form() {
    $form = parent::edit_form();
    $form['location'] = array(
      "#type" => "textfield",
      "#title" => t("Directory path"),
      "#default_value" => $this->get_location(),
      "#required" => TRUE,
      "#description" => t('Enter the path to the directory to save the backups to. Use a relative path to pick a path relative to your Drupal root directory. The web server must be able to write to this path.'),
    );
    return $form;
  }

  /**
   * Return a list of backup filetypes.
   */
  function file_types() {
    return array(
      "tar" => array(
        "extension" => "tar",
        "filemime" => "application/x-tar",
        "backup" => TRUE,
        "restore" => TRUE,
      ),
    );
  }

  /**
   * Get the form for the settings for this destination.
   *
   * Return the default directories whose data can be ignored. These
   * directories contain info which can be easily reproducted. Also exclude the
   * backup and migrate folder to prevent exponential bloat.
   */
  function backup_settings_default() {
    return array(
      'exclude_filepaths' => "backup_migrate\nstyles\ncss\njs\nctools\nless",
    );
  }

  /**
   * Get the form for the backup settings for this destination.
   */
  function backup_settings_form($settings) {
    $form['exclude_filepaths'] = array(
      "#type" => "textarea",
      "#multiple" => TRUE,
      "#title" => t("Exclude the following files or directories"),
      "#default_value" => $settings['exclude_filepaths'],
      "#description" => t("A list of files or directories to be excluded from backups. Add one path per line relative to the directory being backed up."),
    );
    return $form;
  }

  function log($message, $variables = array(), $severity = WATCHDOG_NOTICE) {
    watchdog('Backup and Migrate', $message, $variables, $severity);
  }

  /**
   * Backup from this source.
   *
   * @param backup_file $file
   * @param array $settings
   *
   * @return backup_file
   */
  function backup_to_file($file, $settings) {
    $this->log("Entering @method", array('@method' => __METHOD__), WATCHDOG_DEBUG);

    $file->push_type('tar');
    $archive = new Archive_Tar($file->filepath(), false);

    /** @var array $excludes The excluded paths */
    $excludes = $this->get_excluded_paths($settings);

    // Iterate over the directories (recursively).
    /** @var Iterator $iterator */
    $iterator =  new RecursiveDirectoryIterator($this->get_location(), FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS);

    // Reject excluded files and directories.
    $iterator = new RecursiveCallbackFilterIterator($iterator, function (SplFileInfo $current, $key, $iterator) use ($excludes) {
      if (in_array($current->getPathname(), $excludes)) {
        return FALSE;
      }
      return TRUE;
    });

    // Flatten the recursive iterator.
    $iterator = new RecursiveIteratorIterator($iterator);

    // Only keep files
    // FIXME: What about symlinks?
    $iterator = new CallbackFilterIterator($iterator, function(SplFileInfo $current, $key, $iterator) {
      return $current->isFile();
    });

    // Iterate over the files
    /** @var SplFileInfo $fileInfo */
    foreach ($iterator as $fileInfo) {
      // Add reable file to the archive.
      if ($fileInfo->isReadable()) {
        $this->log("Adding @file to @archive", array('@file' => $fileInfo->getPathname(), '@archive' => $file->filepath()), WATCHDOG_DEBUG);
        $archive->addModify($fileInfo->getPathname(), '', $this->get_location());
      }
      // Store unreadable files for reporting
      else {
        $errors[] = $fileInfo->getPathname();
      }
    }

    $this->log('Leaving ' . __METHOD__, WATCHDOG_DEBUG);
    return $file;
  }


  /**
   * Restore to this source.
   */
  function restore_from_file($file, &$settings) {
    $from = $file->pop_type();
    $temp = backup_migrate_temp_directory();

    $tar = new Archive_Tar($from->filepath());
    $tar->extractModify($temp, $file->name);

    // Older B&M Files format included a base 'files' directory.
    if (file_exists($temp .'/files')) {
      $temp = $temp . '/files';
    }
    if (file_exists($temp .'/'. $file->name .'/files')) {
      $temp = $temp . '/files';
    }

    // Move the files from the temp directory.
    _backup_migrate_move_files($temp, $this->get_realpath());

    return $file;
  }

  /**
   * Break the excluded paths string into a usable list of paths.
   */
  function get_excluded_paths($settings) {
    $base_dir = $this->get_location() . '/';
    $base_scheme = file_uri_scheme($base_dir);
    $paths = empty($settings->filters['exclude_filepaths']) ? '' : $settings->filters['exclude_filepaths'];
    $excludes = explode("\n", $paths);


    foreach ($excludes as $key => $exclude) {
      $scheme = file_uri_scheme($exclude);
      // If the path as no scheme, preprend the location scheme.
      if (!$scheme) {
        $excludes[$key] = "{$base_scheme}://$exclude";
      }
      // If the path ison a different scheme, ignore it.
      elseif ($scheme != $base_scheme) {
        unset($excludes[$key]);
      }
      // If the path is not part of the location, ignore it.
      elseif (substr($exclude, 0, strlen($base_dir)) != $base_dir) {
        unset($excludes[$key]);
      }
    }

    return $excludes;
  }

}

Notes

  • This should works with local file storage too.
  • I used iterators, this allow direct addition of the file to the archive instead of collecting all the files first, then adding them all. On a site with several files, stroing all of them in memory as a gigantic array is a waste of resources.
pbuyle’s picture

I finally adopted an alternative approach, creating backup of only managed files, it is available as a separated module: Backup and Migrate - Managed Files Source. Please note that the BAM issue is still relevant. Use should be prevented from creating a "File Directory" for a remote wrapper location, or support for remote file wrapper should be added.

couturier’s picture

Status: Active » Closed (won't fix)

Sounds like you found a manual work-around. Please be aware that most of the new development work is taking place now in the 8.x branch.