I think MigrateDestinationFile should provide better control as to where a destination file is copied to.

For example, if my Migration was going to copy a file from URI http://example.com/folder1/folder2/foo.jpg

The current implementation would place the copied file here: $this->copyDestination/folder1/folder2/foo.jpg ($this->copyDestination is set to public://) by default.

The following code in MigrateDestinationFile, inside import() function implements this behavior

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))   {

I would like to have an option for a fixed destination. For example:


if ($this->copyFile) {
   if (!$this->fixedDestination) {
      $path = trim(parse_url($file->uri, PHP_URL_PATH), '/');
      $destination = $this->copyDestination . $path;
      $dirname = drupal_dirname($destination);
   } else {
      $destination = $this->copyDestination;
      $dirname = drupal_dirname($destination);
   }
   if (file_prepare_directory($dirname, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS))   {

This would allow a migration to put all migrated files into a common directory. For example you could set copyDestination = "public://migrated/"

and then http://example.com/folder1/folder2/foo.jpg would be migrated to public://migrated/foo.jpg

CommentFileSizeAuthor
#8 migrate-flatten-paths-1120136.patch922 bytestwod

Comments

moshe weitzman’s picture

Status: Active » Postponed (maintainer needs more info)

The intent of the file handler is to migrate into a file Field. Each file field has a configured destination already. I'm not clear why you would want to migrate into some other destination. If you really want that, just copy it yourself in a prepare hook.

myname.com’s picture

I'm working on migrating an old forum into drupal forum. In the old forum software, each user could upload or link to an external image for their avatar. My initial thought was to copy in all of the users' avatar files into public://pictures/ using MigrateDestinationFile and then link the picture to their drupal user account via user table picture field (a foreign key to files_managed table).

Do you have a different model for doing this sort of thing?

My current solution is to create a sub-class of MigrateDestinationFile and and override the import() method.

moshe weitzman’s picture

Category: feature » support
Status: Postponed (maintainer needs more info) » Fixed

The most native Drupal way is to enable user pictures and then import right into that. An alternative is to put an Image field on the user entity or on a new profile entity (see Profile2 module).

You can go with a custom picture implementation like you describe but then you have to build own UI and code for add/edit/delete

myname.com’s picture

Status: Fixed » Active

Thanks for your suggestions. I'm pretty sure that what I'm doing is what you described as "enable user pictures and then import right into that"

The result of my migration is that for each avatar in the old forum system, I am copying the file to public://pictures/ and adding a row to the files_managed table for this image. Then I am updating the user table 'picture' column to have the file id from files_managed.

My only request is the copy operation in MigrateDestinationFile allow an option to not mirror the path from the source URI into public://pictures/

For example if user 1 in the old forum system had his avatar stored at http://example.com/foo/bar/pic.jpg
and user 2 had his avatar stored at http://example2.com/dir1/dir2/pic2.jpg

Then using the MigrateDestinationFile class distributed with Migrate module, the end result is that the following directory structure is created in my public://pictures/ directory

/pictures/foo/bar
/pictures/dir1/dir2

Apply this over 1000's of users and the public/pictures directory just became a major mess. I would rather put all of the imported avatars into one directory.

Here is a sample of my migrate code to give you a feel for what I'm doing

class ForumUserPicsMigration extends CommonForumMigration {
  public function __construct() {
    parent::__construct();
    $this->description = t("Forum Users' Pictures");
    $this->dependencies = array('ForumUser'); //ForumUser migration must be completed first or we can't link the imported files to a user record
    $this->map = new MigrateSQLMap($this->machineName,
        array('id' => array(
                'type' => 'int',
                'not null' => TRUE,
                )
             ),
        MigrateDestinationFileFix::getKeySchema()
    );
    
    $query = db_select('ibf_members', 'u')
             ->fields('u', array('id', 'avatar', 'name'));
    
    $this->source = new MigrateSourceSQL($query);

    //MigrateDestinationFileFix is a subclass of MigrateDestinationFile that doesn't create all of the extra directories
    $this->destination = new MigrateDestinationFileFix(array("copy_file" => TRUE, "copy_destination" => "public://pictures/"));

    // Mapped fields
    $this->addFieldMapping('uid', 'id'); //we'll look up the real user_id from the migrate_map in prepare()
    $this->addFieldMapping('uri', 'avatar');
    
    // Default values
    $this->addFieldMapping('status')
         	->defaultValue(1); //permanent         	 	              
  }
  public function prepare($file, $row) {
  	 $message = "Importing pic for $row->name ";    
         $old_uid = $row->id;
         $sql = "SELECT destid1 FROM migrate_map_forumuser WHERE sourceid1 = :source_id";
         $new_uid = db_query($sql, array(':source_id' => $old_uid))->fetchField();     
      	 $file->uid = $new_uid;
    	
    watchdog(WATCHDOG_INFO, $message);
  }
  //Called after the file is imported
  public function complete($file, $row) {		
  	$sql = "UPDATE users set picture = :fid where uid = :uid";
  	if (db_query($sql, array(':fid' => $file->fid, ':uid' => $file->uid)))
		watchdog(WATCHDOG_INFO, "Edited user $account->uid to set picture = $file->fid");		
  }
}
mikeryan’s picture

Title: Provide better control of file destination for MigrateDestinationFile class » Option to flatten file hierarchies on import
Category: support » feature

OK, so what you would like is an option to flatten the file hierarchy on import, correct?

myname.com’s picture

Yes, thanks!

Fidelix’s picture

My current solution is to create a sub-class of MigrateDestinationFile and and override the import() method.

myname.com, can you share what you've done, please?

twod’s picture

Status: Active » Needs review
StatusFileSize
new922 bytes

I'm having a similar problem with MigrateFileFieldHandler when my Migrate class downloads files from FTP to /tmp/mypics/filename.jpg (in prepareRow) and then map them to an image field. Since the files are no longer remote I use the 'file_move' operation to let the field handler move things into public://myphotos (set as the image field's destination).

Since I know no filenames will collide, I would have expected all files to end up as www.example.com/sites/default/files/myphotos/filename.jpg. (public:// is pointing to sites/default/files)
But, all filenames are modified so the path ends up like this: www.example.com/sites/default/files/myphotos/-tmp-mypics-filename.jpg.
This is a bit of a problem since I want the filenames to stay intact.

My field mapping looks like this.

    // prepareRow() fills $row->photos with a pipe-delimited list of downloaded files: /tmp/mypics/filename1.jpg|/tmp/mypics/filename2.jpg
    $arguments = MigrateFileFieldHandler::arguments(NULL, 'file_move', FILE_EXISTS_REPLACE);
    $this->addFieldMapping('field_photos', 'photos')
      ->arguments($arguments)
      ->separator('|');

I traced it back to fields.inc (around line 320 in 7.x-2.1, which is what I'm using):

    $destination_dir = file_field_widget_uri($field_info, $instance);
    file_prepare_directory($destination_dir, FILE_CREATE_DIRECTORY);
    if ($remote) {
      $destination_file = $destination_dir . "/" . basename($full_path);
    }
    else {
      $destination_file = file_stream_wrapper_uri_normalize($destination_dir . "/" . str_replace('/', '-', $full_path));
    }

    $real_destination_file = drupal_realpath($destination_file);

Why is basename() never used for local files?

Could we add an argument to tell the file handler to apply basename() even if the file is local?
I put together a simple patch for 7.x-2.x which does what I need, but it hasn't had much testing with alternate setups yet.

darksnow’s picture

I'm in almost exactly the same boat as myname.com

I'm importing an old forum, with user avatars. I've chosen to import from a http:// uri and have supplied the path from web root to the location of the files.

In the SQL source, I have a file name. In order for migrate to find the file, I'm prepending the full URL to the file name giving me something like http://example.com/images/avatars/upload/filename.ext

I've set the migrate destination like this:

    $this->destination = new MigrateDestinationFile(array(
      'copy_file' => TRUE,
      'copy_destination' => file_build_uri(variable_get('user_picture_path', 'pictures')) . '/'
    ));

Which gets the set path for user pictures (public://pictures/)

The result is, I get files in:
public://pictures/images/avatars/upload/filename.ext
Where as, I would like them to be:
public://pictures/filename.ext

This would give me consistency with any new avatars uploaded after the migration.

It seems to me a option to only use the basename of the migrated file would be exactly what I need.

mototribe’s picture

as a workaround I just downloaded the files on my local drive and then imported from there. The advantage is that the import is now faster, I can remove the extra folder structure, can rename the filenames and keep a backup copy, just in case ;-)

twod’s picture

I've got ~5000 images to import from FTP in one go, then there's maybe up to 40 new images each day, separated across hundreds of directories and subdirectories...

mototribe’s picture

I just write PHP scripts to curl the files. I'm downloading 100,000+ images and videos. I'm glad TB harddrives are cheap and comcast hasn't throttled my Internet connection (yet) ;-)

I just noticed that I migrated some node images (from remote url) and they got saved in the "sites/default/files" directory even though the remote url has 4 or 5 folders.

I used this code:

    $arguments = MigrateFileFieldHandler::arguments(NULL,'file_copy', FILE_EXISTS_RENAME, NULL);
    $this->addFieldMapping('field_logo', 'pathtophoto')
         ->arguments($arguments);

twod’s picture

I don't know how many files each imported item has beforehand, I just know the naming scheme - based on item id - and in which folder they should exist (sorted into subdirs by item group and then by image number). Using the same folder hierarchy on the site's server makes no sense, which is why this patch is important to me.

@mototribe, I'll probably approach those numbers when my client switches data provider, but I've got a 30s execution time limit to battle too, so that's not likely an option for me.

StuartDH’s picture

I'm finding that MigrateDestinationFile maintains folder structures, which is what I need for my project

However, when I then use the MigrateFileFieldHandler the folders are flattened...which is the opposite of what I need

See this issue http://drupal.org/node/1436838

mikeryan’s picture

Title: Option to flatten file hierarchies on import » Option to flatten or preserve file hierarchies on import
Version: 7.x-2.0 » 7.x-2.x-dev
Status: Needs review » Postponed
Issue tags: +Migrate 2.4

See also #1436838: copy folder structure bug. Both fall under #1240928: META: Refactoring of file destination/field handlers, which we plan on addressing for Migrate 2.4.

mikeryan’s picture

Status: Postponed » Active
mikeryan’s picture

Status: Active » Closed (duplicate)

This is dealt with in #1240928: META: Refactoring of file destination/field handlers - by default, the hierarchy will be flattened, but by setting destination_file to the hierarchy you want you can preserve it.