You are here

Moving Files Around A Node

Corey Pennycuff's picture
plupload.png
plupload image
Image from drupal.org.

When building websites, we use a lot of smoke-and-mirrors to get things accomplished, and we often do it by re-purposing preexisting functionality and code. Once the page is themed, who will be the wiser, right? Unfortunately, a seemingly straightforward idea can become unwieldy when you are unfamiliar with the underlying APIs involved. Such was the case for me when I needed to be able to programmatically move a file from one file field to another in Drupal 7.

The Use Case

I was building a site that had a seemingly simple requirement: users needed to be able to upload multiple files at a time and then browse them appropriately, whether they were photos, videos, archives, etc. For this implementation, it was determined that Bulk Media Upload was a perfect solution for the upload, due to its interface and how it could create individual nodes for each file uploaded. Users could then upload as many files as they liked in any combination of file types, and BMU would handle all of the heavy lifting (e.g. cross-browser compatibility). Unfortunately, BMU only uploads to a single file field, so that is something that I would have to adapt in code.

The Node's Architecture

While BMU allows users to upload mixed file types, the website itself had to treat these file types differently for storage, processing, and display purposes. The easiest way to accomplish this was to use the same node type for all uploads, but then to do post-processing on the file on a cron run that would examine the file and move it to a different field within that same node. The result would be that everything would be uploaded to field_upload, but that once the cron was run, pictures would be moved to field_photo, video would be moved to field_video, etc. Differentiating the nodes, then, to get a list of only pictures would be as simple as creating a view to filter out nodes for which field_photo is not NULL. From the end user's perspective, they can still easily upload files en masse, after which the files would appear in the appropriate format in the appropriate list.

The Problem

Moving a file from one field to another is as simple as making the change programatically on the node object and calling node_save()... If you're lucky, that is. You see, Drupal has an entire framework built to manage file usage. When saving the node, if the destination field is processed first, then a new usage is recorded for that file. Next, when the source field is processed, a usage is removed for that file. The end result is that the file was moved, there was no net change in the file usage count, and everything worked as intended.

The problem is, however, that you cannot control the order that the fields are processed. If you are unlucky and the source field is processed first, then the file usage is reduced, and if the usage count is 0, the file is instantly deleted. Then, when the source field is saved, Drupal tries to save the change, but since the file's entry was already deleted from the database, you have a (potentially) broken reference and have lost the file contents.

The fact is that all of this is unpredictable, based on the unknown order of processing, on whether or not the file is referenced elsewhere in the system (for example in another revision), and whether or not there is a full moon (not verified).

The Solution

We can easily circumvent all of the unknowns by wrapping the node_save() with our own file usage entries. The code might look something like this:

function MYMODULE_cron() {
  // Assemble a list of nodes to load.
  // This may be from a predetermined list, a SQL query, etc.
  $nids = array(1, 2, 3);
  $nodes = node_load_multiple($nids);
  foreach ($nodes as $node) {
    $lang = $node->language;
    // Keep a record of which files are being moved.
    $files = array();
    
    // Loop through the source field and decide where to move the file.
    if (!empty($node->field_upload[$lang])) {
      foreach ($node->field_upload[$lang] as $delta => $file_array) {
        // Convert the array to an object
        $file = (object)$file_array;
        // Conversely, you may need to load the file explicitely, depending
        // on your use case:
        // $file = file_load($file_array['fid']);
        
        // Determine where to move the file.
        $mimetype = file_get_mimetype($file->uri);
        
        $field = '';
        if (strpos($mimetype, 'image') !== FALSE) {
          $field = 'field_photo';
        }
        elseif (strpos($mimetype, 'video') !== FALSE) {
          $field = 'field_video';
        }
        else {
          // Don't move the file.
        }
        
        if (!empty($field)) {
          // Keep track of which files were moved.
          $files[] = $file;
          
          // This just keeps PHP Notices from flooding our logs.
          if (!isset($node->{$field}[$lang])) {
            $node->{$field}[$lang] = array();
          }
          
          // Now actually move the file in the $node object.
          $node->{field}[$lang][] = $node->field_upload[$lang][$delta];
          unset($node->field_upload[$lang][$delta]);
        }
      }
    }
    
    // If we actually moved files, we need to re-save the node
    if (!empty($files)) {
      // Add a file usage record.
      foreach ($files as $file) {
        file_usage_add($file, 'MYMODULE', 'file_move', $node->nid);
      }
      
      // Save the node.
      node_save($node);
      
      // Remove the file usage record.
      $fids = array();
      foreach ($files as $file) {
        file_usage_delete($file, 'MYMODULE', 'file_move', $node->nid);
        $fids[] = $file->fid;
      }
 
      // Clear the appropriate file caches since they were moved around.
      entity_get_controller('file')->resetCache($fids);
    }
  }
}

Closing Remarks

Obviously, as with every code snippet, this example will need to be tailored to your use case, but the logic should be straightforward enough to make it easily modifiable. Despite the difficulty I encountered with moving a file in this manner, I do not see this as a mark against Drupal's design. Quite to the contrary, it allows me to see the depth of forethought that the developers put into portions of code that don't often see the light of day. All it was a good learning experience.

Tags: 

1 Comment

Corey Pennycuff's picture

EDIT: Added file cache clear

I discovered that, in some cases, the file cache needs to be cleared in order for some module interactions to function appropriately, so I added a cache clear of just the file information. My use case involved Image Cache, Storage API, and different Storage Container classes, but I can see this type of problem in other situations as well.

NOTE: I put my cache clear after the file_usage_delete(), which is appropriate for my use case, however you may need to place it before the node_save() depending on what your code is trying to do. As always, YMMV.