Make Custom Field with File Exportable in Drupal Features

In this post, we cover a case where we have a custom field that contains a file and we would like to be able export/import it with a Features module in Drupal

Env: Drupal 7, UUID Features 7.x-1.0-alpha3+1(dev), UUID 7.x-1.0-alpha5+1(dev)

Intro

We have content type with a field field_elastic_slide that contains a file among other things(see our other post for “How To Implement Custom Field Types In Drupal”). We are exporting Nodes with this field using features module. Without doing anything, the export will contain FID(File ID) of the file which is not going to be relevant in another Drupal instance since there is completely different FIDs. So, we will alter the export to store all info of a file in the export instead only FID of the file. Afterwards, we will hook into the import process to use this file info to import the file in the new Drupal Instance

Alter Export To Include All File Info

We are going to use hook_uuid_node_features_export_render_alter(&$export, $node, $module){..} to alter the export as following:

function MODULE-NAME_uuid_node_features_export_render_alter(&$export, $node, $module){
        if($module == 'MODULE-NAME-DOING-EXPORT'){
        $field_instance = field_get_items('node', $node, 'field_elastic_slide');
        foreach($field_instance as $key => $instance){
            $file = file_load($instance['image']);
            $export->field_elastic_slide['und'][$key]['image'] = (array)$file;
            ...
        }
    }
}

In line 5, we load all the file by its FID. Afterwards, we alter the export for our custom field to store array of file info instead only FID. The Feature module will take from there and write the file info in the file ending .features.uuid_node.inc part of the export. So far, so good.

Alter Import To Import File

When importing custom fields containing file, we have to save the file in the table file_managed by ourselves. The feature module will do the work. It will only do the work of importing files for the default field types(file, text,etc). So, we will save the file in the table file_managed and that will generate the necessary FID to be set for our custom field.

To alter import, we use hook_uuid_node_features_rebuild_alter as following:

function MODULE-NAME_uuid_node_features_rebuild_alter(&$node, $module){
    if($module == 'MODULE-NAME'){
        $field_instance = field_get_items('node', $node, 'field_elastic_slide');
        uuid_features_load_module_includes();
        foreach($field_instance as $key => $instance){
            $uuid = $instance['image']['uuid'];
            unset($instance['image']['fid']);
            unset($instance['image']['timestamp']);
            $file_to_save = (object)$instance['image'];
            $final_file = file_save($file_to_save);
            $node->field_elastic_slide['und'][$key]['image'] = $final_file->fid;
            
            //transfer file into public dir available 
            $source = _get_source_file($file_to_save->uri);

            file_unmanaged_copy($source, $file_to_save->uri);
...
}

function _get_source_file($uri){
    $source = drupal_get_path('module','MODULE-NAME').'/imports/public/'.substr($uri, 9);
    if(!file_exists($source)){
        watchdog(WATCHDOG_NOTICE, 'MODULE-NAME Import: Image does not exists at '.$source);
    }
    return $source;
}

Here, the export is retrieved and available in the $node variable. So, in line 3, we retrieve our custom field containing file info we saved in the export in the previous step. Afterwards, we remove some info among it the FID that is irrelevant in the new Drupal instance and is not needed. Next, in highlighted line, we use File API to save the file that makes entry in table file_managed. Then, we set the new FID to our custom field leaving rest for Features module to continue with the import process. At last, the file is copied from predefined location to a Drupal public dir.

Clean Afterwards

When an instance with our custom field is deleted, we want to ensure to delete the file from the file_mage and file_usage tables. Otherwise, there is going to be an error – PDOException: SQLSTATE[23000]: Integrity constraint violation: 1062 next time re-enabling the module or uploading the file with the same name.

//clean file_manage database on field instance delete
function MODULE-NAME_node_delete($node){
    if($node->type == 'CERTAIN-TYPE'){
        //removes files from file_manage/file_usage tables
        $field_instance = field_get_items('node', $node, 'field_FIELED-NAME');
        foreach($field_instance as $key => $instance){
          if(isset($instance['image']) && $instance['image']){
            $file = file_load($instance['image']);
            file_delete($file);
            file_usage_delete($file, 'MODULE-NAME');
          }
  ...
}

The hook_node_delete($node) is called every time a node is being deleted. So, in line 3, we check that the particular node of our interest is being deleted. If it is, then we retrieve the field containing artifact. At last, in line 7, we load the artifact by its FID before removing from file_managed/file usage table and file system as well

This also removes the files from Drupal public directory(by default sites/default/files) that was copied when module enabled as described above

Summary

In short, for exporting and importing custom fields containing files, there is an extra work of physically saving the file that needs to be done by ourselves and is not done by Features module. To do the task at hand, we altered the import so that, instead only the FID, it stores all the info of the file in the export. Afterwards, we used this info to physically import the file(save in managed_file table) and set the new FID to our custom field containing the file in the New Drupal Instance that import takes place

Troubleshoot

The specified file [..] could not be copied, because the destination directory is not properly configured. This may be caused by a problem with file or directory permissions

This was coming up when trying to copy artifacts via file_unmanaged_copy but the destination directories were missing.

//ensure dir is present
                $directory = drupal_dirname($final_file->uri);
                file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
                
                file_unmanaged_copy($source, $final_file->uri);

By adding the file_prepare_directory() in front of file_unmanaged_copy(), creates a directories if missing. As result, the file_unmanaged_copy() executes without any errors

References:
http://www.phase2technology.com/blog/making-your-module-features-exportable/
https://origin.previousnext.com.au/blog/how-handle-embedded-images-when-migrating-content-plone-drupal

Leave a Reply

Your email address will not be published. Required fields are marked *