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.incView 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
Name | 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
Name | Description |
---|---|
MigrateFileInterface | Interface for taking some value representing a file and returning a Drupal file entity (creating the entity if necessary). |