In this article I will show you how to create a simple file uploader for Drupal 7. The example will show how to upload a .csv file to the server and parse it using batch processing. (Batch processing will allow you to go through any size csv without having to worry about the page timing out.) Specifically, we developed a similar module to provide a customer with a drop-dead simple user interface to upload their pricing spreadsheets into their site. They needed to keep the master price list in a local spreadsheet, but they wanted to have a foolproof way to upload, purge and update their pricing files on multiple web sites. During each update, the upload did various conversions and checks before adding the data to the database. (Thanks for pointing out that we didn't have the use case in the original post, @ultimateboy.) First, you'll need to create a menu entry and assure that access is only granted for trusted user roles.
/**
 * Implements hook_permission()
 */
function module_name_permission() {
  return array(
    'administer uploader' => array(
      'title' => t('Administer Uploader'),
      'description' => t('Allow the following roles to upload csv files to the server.'),
    ),
  );
}
 
/**
 * Implements hook_menu()
 */
function module_name_menu() {
  $items['file-uploader'] = array(
    'title' => 'Upload a File',
    'type' => MENU_CALLBACK,
    'description' => 'Import a csv',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('module_name_import_form'),
    'access arguments' => array('administer uploader'),
  );
  return $items;
}
Now we have a path and only selected roles can use it. Next, we will create the form so users can actually upload csv files.
/**
 * Builds a form that will allow users to upload csv files
 * 
 * @see
 *   hook_menu()
 */
function module_name_import_form($form, $form_state) {
  $form['notes'] = array(
    '#type' => 'markup',
    '#markup' => '<div class="import-notes">A few notes when uploading. <ul><li>Make sure the file is in a .csv format.</li><li>Columns should be in *this* order</li><li>Be sure to click the "Upload" button when you select a csv.</li></ul></div>',
    '#upload_location' => 'public://tmp/',
  );
  $form['import'] = array(
    '#title' => t('Import'),
    '#type' => 'managed_file',
    '#description' => t('The uploaded csv will be imported and temporarily saved.'),
    '#upload_location' => 'public://tmp/',
    '#upload_validators' => array(
      'file_validate_extensions' => array('csv'),
    ),
  );
  $form['submit'] = array (
    '#type' => 'submit',
    '#value' => t('Import'),
  );
  return $form;
}
Excellent! Now we have a nice form that will import a file into a temporay location. Note that the location doesn't have to be temporary, but in most cases you just want to grab a csv, extract the needed information out of it, and then have the file removed from the server. Lets see what it looks like! I am not going to use a validation hander because the validators in '#upload_validators' will take care of that. You can add more validators there or create a custom validation handler, but for this example we do not need it. The following is the submit handler.
/**
 * Submit handler for module_name_import_form()
 */
function module_name_import_form_submit($form, $form_state) {
  // Check to make sure that the file was uploaded to the server properly
  $uri = db_query("SELECT uri FROM {file_managed} WHERE fid = :fid", array(
    ':fid' => $form_state['input']['import']['fid'],
  ))->fetchField();
  if(!empty($uri)) {
    if(file_exists(drupal_realpath($uri))) { 
      // Open the csv
      $handle = fopen(drupal_realpath($uri), "r");
      // Go through each row in the csv and run a function on it. In this case we are parsing by '|' (pipe) characters.
      // If you want commas are any other character, replace the pipe with it.
      while (($data = fgetcsv($handle, 0, '|', '"')) !== FALSE) {
        $operations[] = array(
          'module_name_import_batch_processing',  // The function to run on each row
          array($data),  // The row in the csv
        );
      }
 
      // Once everything is gathered and ready to be processed... well... process it!
      $batch = array(
        'title' => t('Importing CSV...'),
        'operations' => $operations,  // Runs all of the queued processes from the while loop above.
        'finished' => 'module_name_import_finished', // Function to run when the import is successful
        'error_message' => t('The installation has encountered an error.'),
        'progress_message' => t('Imported @current of @total products.'),
      );
      batch_set($batch);
      fclose($handle);    
    }
  }
  else {
    drupal_set_message(t('There was an error uploading your file. Please contact a System administator.'), 'error');
  }
}
We now have a batch processing system that parses a csv. Now it is time to do something with the rows that were gathered. This function will look very different depending on what you are expecting in the csv. If you are creating nodes you want to do a check to make sure the node doesn't already exist. Duplicates are bad, especially if you are importing tens of thousands of nodes at a time. Lets create a very simple example that will create a node with a title, body, and serial code.
/**
 * This function runs the batch processing and creates nodes with then given information
 * @see
 * module_name_import_form_submit()
 */
function module_name_import_batch_processing($data) {
  // Lets make the variables more readable.
  $title = $data[0];
  $body = $data[1];
  $serial_num = $data[2];
  // Find out if the node already exists by looking up its serial number. Each serial number should be unique. You can use whatever you want.
  $nid = db_query("SELECT DISTINCT n.nid FROM {node} n " . 
    "INNER JOIN {field_data_field_serial_number} s ON s.revision_id = n.vid AND s.entity_id = n.nid " .
    "WHERE field_serial_number_value = :serial", array(
      ':serial' => $serial_num,
    ))->fetchField();
  if(!empty($nid)) {
    // The node exists! Load it.
    $node = node_load($nid);
 
    // Change the values. No need to update the serial number though.
    $node->title = $title;
    $node->body['und'][0]['value'] = $body;
    $node->body['und'][0]['safe_value'] = check_plain($body);
    node_save($node);
  }
  else {
    // The node does not exist! Create it.
    global $user;
    $node = new StdClass();
    $node->type = 'page'; // Choose your type
    $node->status = 1; // Sets to published automatically, 0 will be unpublished
    $node->title = $title;
    $node->uid = $user->uid;		
    $node->body['und'][0]['value'] = $body;
    $node->body['und'][0]['safe_value'] = check_plain($body);
    $node->language = 'und';
 
    $node->field_serial_number['und'][0]['value'] = $serial_num;
    $node->field_serial_number['und'][0]['safe_value'] = check_plain($serial_num);
    node_save($node);
  }
}
Before we say we are done, we have to declare the completed function. Remember the line 'finished' => 'module_name_import_finished'?
/**
 * This function runs when the batch processing is complete
 *
 * @see
 * module_name_import_form_submit()
 */
function module_name_import_finished() {
  drupal_set_message(t('Import Completed Successfully'));
}
Now when we check new content we can see that the imported content is now there. I created the "module_name" module and tested it just to make sure everything worked. I decided to attach the module so that you will have an easier time modifying it! Enjoy.
Attachments