Add file destination
authorMike Ryan
Wed, 22 Dec 2010 22:30:43 +0000 (22:30 +0000)
committerMike Ryan
Wed, 22 Dec 2010 22:30:43 +0000 (22:30 +0000)
CHANGELOG.txt
includes/migration.inc
migrate.info
plugins/destinations/file.inc [new file with mode: 0644]
plugins/destinations/path.inc

index 3e87c4d..c92bd94 100644 (file)
@@ -13,6 +13,8 @@ Features and enhancements
   MigrationBase::getInstance now takes a machine name rather than a class name. 
   Migration class names are no longer required to end in 'Migration'.
 - #992898 - Pass options to source and destination constructors as arrays.
+File destinations (i.e., migrating directly to the file_managed table, with
+  option copying of the files themselves) are now supported.
 Allow migration of comment enable/disable.
 Check max_execution_time as well as memory_limit, for graceful exit when
   max_execution_time is in play.
index 6f4f991..d422933 100644 (file)
@@ -228,14 +228,10 @@ abstract class Migration extends MigrationBase {
 
     // Call pre-process methods
     if ($this->status == Migration::STATUS_IMPORTING) {
-      if (method_exists($this->destination, 'preImport')) {
-        $this->destination->preImport();
-      }
+      $this->preImport();
     }
     else {
-      if (method_exists($this->destination, 'preRollback')) {
-        $this->destination->preRollback();
-      }
+      $this->preRollback();
     }
   }
 
@@ -246,20 +242,45 @@ abstract class Migration extends MigrationBase {
   public function endProcess() {
     // Call post-process methods
     if ($this->status == Migration::STATUS_IMPORTING) {
-      if (method_exists($this->destination, 'postImport')) {
-        $this->destination->postImport();
-      }
+      $this->postImport();
     }
     else {
-      if (method_exists($this->destination, 'postRollback')) {
-        $this->destination->postRollback();
-      }
+      $this->postRollback();
     }
 
     parent::endProcess();
   }
 
   /**
+   * Default implementations of pre/post import/rollback methods. These call
+   * the destination methods (if they exist) - when overriding, always
+   * call parent::preImport() etc.
+   */
+  protected function preImport() {
+    if (method_exists($this->destination, 'preRollback')) {
+      $this->destination->preImport();
+    }
+  }
+
+  protected function preRollback() {
+    if (method_exists($this->destination, 'preRollback')) {
+      $this->destination->preRollback();
+    }
+  }
+
+  protected function postImport() {
+    if (method_exists($this->destination, 'postImport')) {
+      $this->destination->postImport();
+    }
+  }
+
+  protected function postRollback() {
+    if (method_exists($this->destination, 'postRollback')) {
+      $this->destination->postRollback();
+    }
+  }
+
+  /**
    * Perform a rollback operation - remove migrated items from the destination.
    */
   protected function rollback() {
index f8a373e..7165ee8 100755 (executable)
@@ -19,6 +19,7 @@ files[] = plugins/destinations/term.inc
 files[] = plugins/destinations/user.inc
 files[] = plugins/destinations/node.inc
 files[] = plugins/destinations/comment.inc
+files[] = plugins/destinations/file.inc
 files[] = plugins/destinations/path.inc
 files[] = plugins/destinations/fields.inc
 files[] = plugins/destinations/table_copy.inc
@@ -27,7 +28,6 @@ files[] = plugins/sources/sql.inc
 files[] = plugins/sources/sqlmap.inc
 files[] = plugins/sources/mssql.inc
 files[] = plugins/sources/xml.inc
-;NYI files[] = plugins/sources/view.inc
 files[] = tests/plugins/destinations/comment.test
 files[] = tests/plugins/destinations/node.test
 files[] = tests/plugins/destinations/term.test
diff --git a/plugins/destinations/file.inc b/plugins/destinations/file.inc
new file mode 100644 (file)
index 0000000..1211b9d
--- /dev/null
@@ -0,0 +1,189 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Support for files as destinations.
+ */
+
+/**
+ * Destination class implementing migration into the files table.
+ */
+class MigrateDestinationFile extends MigrateDestinationEntity {
+  /**
+   * Whether to copy a file from the provided URI into the Drupal installation.
+   * If FALSE, the URI is presumed to be local to the Drupal install. If TRUE,
+   * the file will be copied according to the copyScheme value.
+   *
+   * @var boolean
+   */
+  protected $copyFile = FALSE;
+
+  /**
+   * When copying files, the destination scheme/directory. Defaults to 'public://'.
+   *
+   * @var string
+   */
+  protected $copyDestination = 'public://';
+
+  static public function getKeySchema() {
+    return array(
+      'fid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'description' => 'file_managed ID',
+      ),
+    );
+  }
+
+  /**
+   * Return an options array for file destinations.
+   *
+   * @param boolean $copy_file
+   *  TRUE to have the file copied from the provided URI to Drupal. Defaults to FALSE.
+   * @param string $copy_destination
+   *  If $copy_file is TRUE, the scheme/directory to use as the destination for the copied file.
+   *  Defaults to 'public://'.
+   * @param string $language
+   *  Default language for files created via this destination class.
+   * @param string $text_format
+   *  Default text format for files created via this destination class.
+   */
+  static public function options($copy_file, $copy_destination, $language, $text_format) {
+    return compact('copy_file', 'copy_destination', 'language', 'text_format');
+  }
+
+  /**
+   * Basic initialization
+   *
+   * @param array $options
+   *  Options applied to files.
+   */
+  public function __construct(array $options = array()) {
+    if (isset($options['copy_file'])) {
+      $this->copyFile = $options['copy_file'];
+    }
+    if (isset($options['copy_destination'])) {
+      $this->copyDestination = $options['copy_destination'];
+    }
+    parent::__construct('file', 'file', $options);
+  }
+
+  /**
+   * Returns a list of fields available to be mapped for the entity type (bundle)
+   *
+   * @return array
+   *  Keys: machine names of the fields (to be passed to addFieldMapping)
+   *  Values: Human-friendly descriptions of the fields.
+   */
+  public function fields() {
+    $fields = array();
+    // First the core properties
+    $fields['fid'] = t('File: Existing file ID');
+    $fields['uid'] = t('File: Uid of user associated with file');
+    $fields['filename'] = t('File: Name of the file with no path components');
+    $fields['uri'] = t('URI of the file');
+    $fields['filemime'] = t('File: The file\'s MIME type');
+    $fields['status'] = t('File: A bitmapped field indicating the status of the file');
+    $fields['timestamp'] = t('File: 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);
+    $fields += migrate_handler_invoke_all('File', 'fields');
+
+    return $fields;
+  }
+
+  /**
+   * Delete a file entry.
+   *
+   * @param array $fid
+   *  Fid to delete, arrayed.
+   */
+  public function rollback(array $fid) {
+    migrate_instrument_start('file_delete');
+    $file = file_load(reset($fid));
+    if ($file) {
+      // TODO: Error checking
+      file_delete($file);
+    }
+    migrate_instrument_stop('file_delete');
+  }
+
+  /**
+   * 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'));
+      }
+      $old_file = file_load($file->fid);
+    }
+
+    if ($this->copyFile) {
+      $path = trim(parse_url($file->uri, PHP_URL_PATH), '/');
+      $destination = $this->copyDestination . $path;
+      $dirname = drupal_dirname($destination);
+      if (file_prepare_directory($dirname, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
+        // We'd like to use file_unmanaged_copy, but it calls file_exists, which
+        // won't work for remote URLs
+        migrate_instrument_start('MigrateDestinationFile copy');
+        $result = @copy($file->uri, $destination);
+        migrate_instrument_stop('MigrateDestinationFile copy');
+        if ($result) {
+          $file->uri = $destination;
+        }
+        else {
+          throw new MigrateException(t('Could not copy file !uri', array('!uri' => $file->uri)),
+            MigrationBase::MESSAGE_ERROR);
+        }
+      }
+      else {
+        throw new MigrateException(t('Could not create directory !dirname',
+            array('!dirname' => $dirname)),
+          MigrationBase::MESSAGE_ERROR);
+      }
+    }
+
+    // Default filename to the basename of the (final) URI
+    if (!isset($file->filename) || !$file->filename) {
+      $file->filename = basename($file->uri);
+    }
+
+    // Detect the mime type if not provided
+    if (!isset($file->filemime) || !$file->filemime) {
+      $file->filemime = file_get_mimetype($file->filename);
+    }
+    // Invoke migration prepare handlers
+    $this->prepare($file, $row);
+    migrate_instrument_start('file_save');
+    $file = file_save($file);
+    migrate_instrument_stop('file_save');
+    $this->complete($file, $row);
+    $return = isset($file->fid) ? array($file->fid) : FALSE;
+    return $return;
+  }
+}
index e96f8b2..66b087a 100644 (file)
@@ -12,6 +12,7 @@ class MigratePathEntityHandler extends MigrateDestinationHandler {
   }
 
   public function fields() {
+    // TODO: Only surface when path module is enabled, and for appropriate entities
     return array('path' => t('Node: Path alias'));
   }