You are here

file.inc in Migrate 7.2

Support for file entity as destination. Note that File Fields have their own destination in fields.inc

File

plugins/destinations/file.inc
View source
<?php

/**
 * @file
 * Support for file entity as destination. Note that File Fields have their
 * own destination in fields.inc
 */

/**
 * Interface for taking some value representing a file and returning
 * a Drupal file entity (creating the entity if necessary).
 */
interface MigrateFileInterface {

  /**
   * Return a list of subfields and options specific to this implementation,
   * keyed by name.
   */
  public static function fields();

  /**
   * Create or link to a Drupal file entity.
   *
   * @param $value
   *  A class-specific value (URI, pre-existing file ID, file blob, ...)
   *  representing file content.
   *
   * @param $owner
   *  uid of an account to be recorded as the file owner.
   *
   * @return object
   *  File entity being created or referenced.
   */
  public function processFile($value, $owner);

}
abstract class MigrateFileBase implements MigrateFileInterface {

  /**
   * Extension of the core FILE_EXISTS_* constants, offering an alternative to
   * reuse the existing file if present as-is (core only offers the options of
   * replacing it or renaming to avoid collision).
   */
  const FILE_EXISTS_REUSE = -1;

  /**
   * An optional file object to use as a default starting point for building the
   * file entity.
   *
   * @var stdClass
   */
  protected $defaultFile;

  /**
   * How to handle destination filename collisions.
   *
   * @var int
   */
  protected $fileReplace = FILE_EXISTS_RENAME;

  /**
   * Set to TRUE to prevent file deletion on rollback.
   *
   * @var bool
   */
  protected $preserveFiles = FALSE;
  public function __construct($arguments = array(), $default_file = NULL) {
    if (isset($arguments['preserve_files'])) {
      $this->preserveFiles = $arguments['preserve_files'];
    }
    if (isset($arguments['file_replace'])) {
      $this->fileReplace = $arguments['file_replace'];
    }
    if ($default_file) {
      $this->defaultFile = $default_file;
    }
    else {
      $this->defaultFile = new stdClass();
    }
  }

  /**
   * Default implementation of MigrateFileInterface::fields().
   *
   * @return array
   */
  public static function fields() {
    return array(
      'preserve_files' => t('Option: <a href="@doc">Boolean indicating whether files should be preserved or deleted on rollback</a>', array(
        '@doc' => 'http://drupal.org/node/1540106#preserve_files',
      )),
    );
  }

  /**
   * Setup a file entity object suitable for saving.
   *
   * @param $destination
   *  Path to the Drupal copy of the file.
   * @param $owner
   *  Uid of the file owner.
   *
   * @return stdClass
   *  A file object ready to be saved.
   */
  protected function createFileEntity($destination, $owner) {
    $file = clone $this->defaultFile;
    $file->uri = $destination;
    $file->uid = $owner;
    if (!isset($file->filename)) {
      $file->filename = drupal_basename($destination);
    }
    if (!isset($file->filemime)) {
      $file->filemime = file_get_mimetype(urldecode($destination));
    }
    if (!isset($file->status)) {
      $file->status = FILE_STATUS_PERMANENT;
    }
    if (empty($file->type) || $file->type == 'file') {

      // Try to determine the file type.
      if (module_exists('file_entity')) {
        $type = file_get_type($file);
      }
      elseif ($slash_pos = strpos($file->filemime, '/')) {
        $type = substr($file->filemime, 0, $slash_pos);
      }
      $file->type = isset($type) ? $type : 'file';
    }

    // If we are replacing or reusing an existing filesystem entry,
    // also re-use its database record.
    if ($this->fileReplace == FILE_EXISTS_REPLACE || $this->fileReplace == self::FILE_EXISTS_REUSE) {
      $existing_files = file_load_multiple(array(), array(
        'uri' => $destination,
      ));
      if (count($existing_files)) {
        $existing = reset($existing_files);
        $file->fid = $existing->fid;
        $file->filename = $existing->filename;
      }
    }
    return $file;
  }

  /**
   * If asked to preserve files from deletion on rollback, add a file_usage
   * entry.
   *
   * @param $fid
   */
  protected function markForPreservation($fid) {
    if (!empty($this->preserveFiles)) {

      // We do this directly instead of calling file_usage_add, to force the
      // count to 1 - otherwise, updates will increment the counter and the file
      // will never be deletable
      db_merge('file_usage')
        ->key(array(
        'fid' => $fid,
        'module' => 'migrate',
        'type' => 'file',
        'id' => $fid,
      ))
        ->fields(array(
        'count' => 1,
      ))
        ->execute();
    }
  }

}

/**
 * The simplest possible file class - where the value is a remote URI which
 * simply needs to be saved as the URI on the destination side, with no attempt
 * to copy or otherwise use it.
 */
class MigrateFileUriAsIs extends MigrateFileBase {
  public function processFile($value, $owner) {
    $file = file_save($this
      ->createFileEntity($value, $owner));
    return $file;
  }

}

/**
 * Handle the degenerate case where we already have a file ID.
 */
class MigrateFileFid extends MigrateFileBase {

  /**
   * Implementation of MigrateFileInterface::processFile().
   *
   * @param $value
   *  An existing file entity ID (fid).
   * @param $owner
   *  User ID (uid) to be the owner of the file. Ignored in this case.
   *
   * @return int
   *  The file entity corresponding to the fid that was passed in.
   */
  public function processFile($value, $owner) {
    $this
      ->markForPreservation($value);
    return file_load($value);
  }

}

/**
 * Base class for creating core file entities.
 */
abstract class MigrateFile extends MigrateFileBase {

  /**
   * The destination directory within Drupal.
   *
   * @var string
   */
  protected $destinationDir = 'public://';

  /**
   * The filename relative to destinationDir to which to save the current file.
   *
   * @var string
   */
  protected $destinationFile = '';
  public function __construct($arguments = array(), $default_file = NULL) {
    parent::__construct($arguments, $default_file);
    if (isset($arguments['destination_dir'])) {
      $this->destinationDir = $arguments['destination_dir'];
    }
    if (isset($arguments['destination_file'])) {
      $this->destinationFile = $arguments['destination_file'];
    }
  }

  /**
   * Implementation of MigrateFileInterface::fields().
   *
   * @return array
   */
  public static function fields() {
    return parent::fields() + array(
      'destination_dir' => t('Subfield: <a href="@doc">Path within Drupal files directory to store file</a>', array(
        '@doc' => 'http://drupal.org/node/1540106#destination_dir',
      )),
      'destination_file' => t('Subfield: <a href="@doc">Path within destination_dir to store the file.</a>', array(
        '@doc' => 'http://drupal.org/node/1540106#destination_file',
      )),
      'file_replace' => t('Option: <a href="@doc">Value of $replace in that file function. Defaults to FILE_EXISTS_RENAME.</a>', array(
        '@doc' => 'http://drupal.org/node/1540106#file_replace',
      )),
    );
  }

  /**
   * By whatever appropriate means, put the file in the right place.
   *
   * @param $destination
   *  Destination path within Drupal.
   *
   * @return bool
   *  TRUE if the file is successfully saved, FALSE otherwise.
   */
  protected abstract function copyFile($destination);

  /**
   * Default implementation of MigrateFileInterface::processFiles().
   *
   * @param $value
   *  The URI or local filespec of a file to be imported.
   * @param $owner
   *  User ID (uid) to be the owner of the file.
   *
   * @return object
   *  The file entity being created or referenced.
   */
  public function processFile($value, $owner) {
    $migration = Migration::currentMigration();

    // Determine the final path we want in Drupal - start with our preferred path.
    $destination = file_stream_wrapper_uri_normalize($this->destinationDir . '/' . ltrim($this->destinationFile, "/\\"));

    // Our own file_replace behavior - if the file exists, use it without
    // replacing it
    if ($this->fileReplace == self::FILE_EXISTS_REUSE) {

      // See if we this file already (we'll reuse and resave a file entity if it exists).
      if (file_exists($destination)) {
        $file = $this
          ->createFileEntity($destination, $owner);
        $file = file_save($file);
        $this
          ->markForPreservation($file->fid);
        return $file;
      }

      // No existing one to reuse, reset to REPLACE
      $this->fileReplace = FILE_EXISTS_REPLACE;
    }

    // Prepare the destination directory.
    $destdir = drupal_dirname($destination);
    if (!file_prepare_directory($destdir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
      $migration
        ->saveMessage(t('Could not create destination directory for !dest', array(
        '!dest' => $destination,
      )));
      return FALSE;
    }

    // Determine whether we can perform this operation based on overwrite rules.
    $destination = file_destination($destination, $this->fileReplace);
    if ($destination === FALSE) {
      $migration
        ->saveMessage(t('The file could not be copied because file %dest already exists in the destination directory.', array(
        '%dest' => $destination,
      )));
      return FALSE;
    }

    // Make sure the .htaccess files are present.
    file_ensure_htaccess();

    // Put the file where it needs to be.
    if (!$this
      ->copyFile($destination)) {
      return FALSE;
    }

    // Set the permissions on the new file.
    drupal_chmod($destination);

    // Create and save the file entity.
    $file = file_save($this
      ->createFileEntity($destination, $owner));

    // Prevent deletion of the file on rollback if requested.
    if (is_object($file)) {
      $this
        ->markForPreservation($file->fid);
      return $file;
    }
    else {
      return FALSE;
    }
  }

}

/**
 * Handle cases where we're handed a URI, or local filespec, representing a file
 * to be imported to Drupal.
 */
class MigrateFileUri extends MigrateFile {

  /**
   * The source directory for the file, relative to which the value (source
   * file) will be taken.
   *
   * @var string
   */
  protected $sourceDir = '';

  /**
   * The full path to the source file.
   *
   * @var string
   */
  protected $sourcePath = '';

  /**
   * Whether to apply rawurlencode to the components of an incoming file path.
   */
  protected $urlEncode = TRUE;
  public function __construct($arguments = array(), $default_file = NULL) {
    parent::__construct($arguments, $default_file);
    if (isset($arguments['source_dir'])) {
      $this->sourceDir = rtrim($arguments['source_dir'], "/\\");
    }
    if (isset($arguments['urlencode'])) {
      $this->urlEncode = $arguments['urlencode'];
    }
  }

  /**
   * Implementation of MigrateFileInterface::fields().
   *
   * @return array
   */
  public static function fields() {
    return parent::fields() + array(
      'source_dir' => t('Subfield: <a href="@doc">Path to source file.</a>', array(
        '@doc' => 'http://drupal.org/node/1540106#source_dir',
      )),
      'urlencode' => t('Option: <a href="@doc">Encode all segments of the incoming path (defaults to TRUE).</a>', array(
        '@doc' => 'http://drupal.org/node/1540106#urlencode',
      )),
    );
  }

  /**
   * Implementation of MigrateFile::copyFile().
   *
   * @param $destination
   *  Destination within Drupal.
   *
   * @return bool
   *  TRUE if the copy succeeded, FALSE otherwise.
   */
  protected function copyFile($destination) {
    if ($this->urlEncode) {

      // Perform the copy operation, with a cleaned-up path.
      $this->sourcePath = self::urlencode($this->sourcePath);
    }
    try {
      $copied = copy($this->sourcePath, $destination);
      if ($copied == FALSE) {
        $migration = Migration::currentMigration();
        $migration
          ->saveMessage(t('The specified file %file could not be copied to %destination', array(
          '%file' => $this->sourcePath,
          '%destination' => $destination,
        )));
      }
      return $copied;
    } catch (Exception $e) {
      $migration = Migration::currentMigration();
      $migration
        ->saveMessage(t('The specified file %file could not be copied to %destination: "%exception_msg"', array(
        '%file' => $this->sourcePath,
        '%destination' => $destination,
        '%exception_msg' => $e
          ->getMessage(),
      )));
      return FALSE;
    }
  }

  /**
   * Urlencode all the components of a remote filename.
   *
   * @param $filename
   *
   * @return string
   */
  public static function urlencode($filename) {

    // Only apply to a full URL
    if (strpos($filename, '://')) {
      $components = explode('/', $filename);
      foreach ($components as $key => $component) {
        $components[$key] = rawurlencode($component);
      }
      $filename = implode('/', $components);

      // Actually, we don't want certain characters encoded
      $filename = str_replace('%3A', ':', $filename);
      $filename = str_replace('%3F', '?', $filename);
      $filename = str_replace('%26', '&', $filename);
      $filename = str_replace('%40', '@', $filename);
    }
    return $filename;
  }

  /**
   * Implementation of MigrateFileInterface::processFiles().
   *
   * @param $value
   *  The URI or local filespec of a file to be imported.
   * @param $owner
   *  User ID (uid) to be the owner of the file.
   *
   * @return object
   *  The file entity being created or referenced.
   */
  public function processFile($value, $owner) {

    // Identify the full path to the source file
    if (!empty($this->sourceDir)) {
      $this->sourcePath = rtrim($this->sourceDir, "/\\") . '/' . ltrim($value, "/\\");
    }
    else {
      $this->sourcePath = $value;
    }
    if (empty($this->destinationFile)) {
      $path = explode('?', $this->sourcePath);
      $this->destinationFile = basename($path[0]);
    }

    // MigrateFile has most of the smarts - the key is that it will call back
    // to our copyFile() implementation.
    $file = parent::processFile($value, $owner);
    return $file;
  }

}

/**
 * Handle cases where we're handed a blob (i.e., the actual contents of a file,
 * such as image data) to be stored as a real file in Drupal.
 */
class MigrateFileBlob extends MigrateFile {

  /**
   * The file contents we will be writing to a real file.
   *
   * @var
   */
  protected $fileContents;

  /**
   * Implementation of MigrateFile::copyFile().
   *
   * @param $destination
   *  Drupal destination path.
   *
   * @return bool
   *  TRUE if the file contents were successfully written, FALSE otherwise.
   */
  protected function copyFile($destination) {
    if (file_put_contents($destination, $this->fileContents)) {
      return TRUE;
    }
    else {
      $migration = Migration::currentMigration();
      $migration
        ->saveMessage(t('Failed to write blob data to %destination', array(
        '%destination' => $destination,
      )));
      return FALSE;
    }
  }

  /**
   * Implementation of MigrateFileInterface::processFile().
   *
   * @param $value
   *  The file contents to be saved as a file.
   * @param $owner
   *  User ID (uid) to be the owner of the file.
   *
   * @return object
   *  File entity being created or referenced.
   */
  public function processFile($value, $owner) {
    $this->fileContents = $value;
    $file = parent::processFile($value, $owner);
    return $file;
  }

}

/**
 * Destination class implementing migration into the files table.
 */
class MigrateDestinationFile extends MigrateDestinationEntity {

  /**
   * File class (MigrateFileUri etc.) doing the dirty wrk.
   *
   * @var string
   */
  protected $fileClass;
  public function setFileClass($file_class) {
    $this->fileClass = $file_class;
  }

  /**
   * Boolean indicating whether we should avoid deleting the actual file on
   * rollback.
   *
   * @var bool
   */
  protected $preserveFiles = FALSE;

  /**
   * Implementation of MigrateDestination::getKeySchema().
   *
   * @return array
   */
  public static function getKeySchema() {
    return array(
      'fid' => array(
        'type' => 'int',
        'unsigned' => TRUE,
        'description' => 'file_managed ID',
      ),
    );
  }

  /**
   * Basic initialization
   *
   * @param array $options
   *  Options applied to files.
   */
  public function __construct($bundle = 'file', $file_class = 'MigrateFileUri', $options = array()) {
    parent::__construct('file', $bundle, $options);
    $this->fileClass = $file_class;
  }

  /**
   * Returns a list of fields available to be mapped for the entity type
   * (bundle)
   *
   * @param Migration $migration
   *  Optionally, the migration containing this destination.
   *
   * @return array
   *  Keys: machine names of the fields (to be passed to addFieldMapping)
   *  Values: Human-friendly descriptions of the fields.
   */
  public function fields($migration = NULL) {
    $fields = array();

    // First the core properties
    $fields['fid'] = t('Existing file ID');
    $fields['uid'] = t('Uid of user associated with file');
    $fields['value'] = t('Representation of the source file (usually a URI)');
    $fields['timestamp'] = t('UNIX timestamp for the date the file was added');

    // Then add in anything provided by handlers
    $fields += migrate_handler_invoke_all('Entity', 'fields', $this->entityType, $this->bundle, $migration);
    $fields += migrate_handler_invoke_all('File', 'fields', $this->entityType, $this->bundle, $migration);

    // Plus anything provided by the file class
    $fields += call_user_func(array(
      $this->fileClass,
      'fields',
    ));
    return $fields;
  }

  /**
   * Delete a file entry.
   *
   * @param array $fid
   *  Fid to delete, arrayed.
   */
  public function rollback(array $fid) {
    migrate_instrument_start('file_load');
    $file = file_load(reset($fid));
    migrate_instrument_stop('file_load');
    if ($file) {
      migrate_instrument_start('file_delete');

      // If we're preserving files, roll our own version of file_delete() to make
      // sure we don't delete them. If we're not, make sure we do the job completely.
      $migration = Migration::currentMigration();
      $mappings = $migration
        ->getFieldMappings();
      if (isset($mappings['preserve_files'])) {

        // Assumes it's set using defaultValue
        $preserve_files = $mappings['preserve_files']
          ->getDefaultValue();
      }
      else {
        $preserve_files = FALSE;
      }
      $this
        ->prepareRollback($fid);
      if ($preserve_files) {
        $this
          ->fileDelete($file);
      }
      else {
        file_delete($file, TRUE);
      }
      $this
        ->completeRollback($fid);
      migrate_instrument_stop('file_delete');
    }
  }

  /**
   * Delete database references to a file without deleting the file itself.
   *
   * @param $file
   */
  protected function fileDelete($file) {

    // Let other modules clean up any references to the deleted file.
    module_invoke_all('file_delete', $file);
    module_invoke_all('entity_delete', $file, 'file');
    db_delete('file_managed')
      ->condition('fid', $file->fid)
      ->execute();
    db_delete('file_usage')
      ->condition('fid', $file->fid)
      ->execute();
  }

  /**
   * Import a single file record.
   *
   * @param $file
   *  File object to build. Prefilled with any fields mapped in the Migration.
   * @param $row
   *  Raw source data object - passed through to prepare/complete handlers.
   *
   * @return array
   *  Array of key fields (fid only in this case) of the file that was saved if
   *  successful. FALSE on failure.
   */
  public function import(stdClass $file, stdClass $row) {

    // Updating previously-migrated content?
    $migration = Migration::currentMigration();
    if (isset($row->migrate_map_destid1)) {
      if (isset($file->fid)) {
        if ($file->fid != $row->migrate_map_destid1) {
          throw new MigrateException(t("Incoming fid !fid and map destination fid !destid1 don't match", array(
            '!fid' => $file->fid,
            '!destid1' => $row->migrate_map_destid1,
          )));
        }
      }
      else {
        $file->fid = $row->migrate_map_destid1;
      }
    }
    if ($migration
      ->getSystemOfRecord() == Migration::DESTINATION) {
      if (!isset($file->fid)) {
        throw new MigrateException(t('System-of-record is DESTINATION, but no destination fid provided'));
      }

      // @todo: Support DESTINATION case
      $old_file = file_load($file->fid);
    }

    // 'type' is the bundle property on file entities. It must be set here for
    // the sake of the prepare handlers, although it may be overridden later
    // based on the detected mime type.
    if (empty($file->type)) {

      // If a bundle was specified in the constructor we use it for filetype.
      if ($this->bundle != 'file') {
        $file->type = $this->bundle;
      }
      else {
        $file->type = 'file';
      }
    }

    // Invoke migration prepare handlers
    $this
      ->prepare($file, $row);
    if (isset($file->fid)) {
      $updating = TRUE;
    }
    else {
      $updating = FALSE;
    }
    if (!isset($file->uid)) {
      $file->uid = 1;
    }

    // file_save() unconditionally sets timestamp - if we have an explicit
    // value we want, we need to set it manually after file_save.
    if (isset($file->timestamp)) {
      $timestamp = MigrationBase::timestamp($file->timestamp);
    }

    // Don't pass preserve_files through to the file class, which will add
    // file_usage - we will handle it ourselves in rollback().
    $file->preserve_files = FALSE;
    $file_class = $this->fileClass;
    $source = new $file_class((array) $file, $file);
    $file = $source
      ->processFile($file->value, $file->uid);
    if (is_object($file) && isset($file->fid)) {
      $this
        ->complete($file, $row);
      if (isset($timestamp)) {
        db_update('file_managed')
          ->fields(array(
          'timestamp' => $timestamp,
        ))
          ->condition('fid', $file->fid)
          ->execute();
        $file->timestamp = $timestamp;
      }
      $return = array(
        $file->fid,
      );
      if ($updating) {
        $this->numUpdated++;
      }
      else {
        $this->numCreated++;
      }
    }
    else {
      $return = FALSE;
    }
    return $return;
  }

}

Classes

Namesort descending Description
MigrateDestinationFile Destination class implementing migration into the files table.
MigrateFile Base class for creating core file entities.
MigrateFileBase
MigrateFileBlob Handle cases where we're handed a blob (i.e., the actual contents of a file, such as image data) to be stored as a real file in Drupal.
MigrateFileFid Handle the degenerate case where we already have a file ID.
MigrateFileUri Handle cases where we're handed a URI, or local filespec, representing a file to be imported to Drupal.
MigrateFileUriAsIs The simplest possible file class - where the value is a remote URI which simply needs to be saved as the URI on the destination side, with no attempt to copy or otherwise use it.

Interfaces

Namesort descending Description
MigrateFileInterface Interface for taking some value representing a file and returning a Drupal file entity (creating the entity if necessary).