diff --git a/actions/archive.action.inc b/actions/archive.action.inc
new file mode 100644
index 0000000..5c3bb9f
--- /dev/null
+++ b/actions/archive.action.inc
@@ -0,0 +1,181 @@
+<?php
+
+/**
+ * @file
+ * Provides an action for creating a zip archive of selected files.
+ * An entry in the {file_managed} table is created for the newly created archive,
+ * and it is marked as permanent or temporary based on the operation settings.
+ */
+
+function views_bulk_operations_archive_action_info() {
+  $actions = array();
+  if (function_exists('zip_open')) {
+    $actions['views_bulk_operations_archive_action'] = array(
+      'type' => 'file',
+      'label' => t('Create an archive of selected files'),
+      'configurable' => TRUE,
+      'aggregate' => FALSE,
+    );
+  }
+  return $actions;
+}
+
+/**
+ * Since Drupal's Archiver doesn't abstract properly the archivers it implements
+ * (Archive_Tar and ZipArchive), it can't be used here.
+ */
+function views_bulk_operations_archive_action($file, $context) {
+  global $user;
+  static $archive_contents = array();
+
+  // Adding a non-existent file to the archive crashes ZipArchive on close().
+  if (file_exists($file->uri)) {
+    $destination = $context['scheme'] . '://' . $context['filename'];
+    $zip = new ZipArchive();
+    // If the archive already exists, open it. If not, create it.
+    if (file_exists($destination)) {
+      $opened = $zip->open(drupal_realpath($destination));
+    }
+    else {
+      $opened = $zip->open(drupal_realpath($destination), ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE);
+    }
+
+    if ($opened) {
+      // Create a list of all files in the archive. Used for duplicate checking.
+      if (empty($archive_contents)) {
+        for ($i=0; $i < $zip->numFiles; $i++) {
+          $archive_contents[] = $zip->getNameIndex($i);
+        }
+      }
+      // Make sure that the target filename is unique.
+      $filename = _views_bulk_operations_archive_action_create_filename(basename($file->uri), $archive_contents);
+      // Note that the actual addition happens on close(), hence the need
+      // to open / close the archive each time the action runs.
+      $zip->addFile(drupal_realpath($file->uri), $filename);
+      $zip->close();
+      $archive_contents[] = $filename;
+    }
+  }
+
+  // The operation is complete, create a file entity and provide a download
+  // link to the user.
+  if ($context['progress']['current'] == $context['progress']['total']) {
+    $archive_file = new stdClass();
+    $archive_file->uri = $destination;
+    $archive_file->filename = basename($destination);
+    $archive_file->filemime = file_get_mimetype($destination);
+    $archive_file->uid      = $user->uid;
+    $archive_file->status   = $context['settings']['temporary'] ? FALSE : FILE_STATUS_PERMANENT;
+    file_save($archive_file);
+
+    $url = file_create_url($archive_file->uri);
+    $url = l($url, $url, array('absolute' => TRUE));
+    _views_bulk_operations_log(t('An archive has been created and can be downloaded from: !url', array('!url' => $url)));
+  }
+}
+
+/**
+ * Configuration form shown to the user before the action gets executed.
+ */
+function views_bulk_operations_archive_action_form($context) {
+  // Pass the scheme as a value, so that the submit callback can access it.
+  $form['scheme'] = array(
+    '#type' => 'value',
+    '#value' => $context['settings']['scheme'],
+  );
+
+  $form['filename'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Filename'),
+    '#default_value' => 'vbo_archive_' . date('Ymd'),
+    '#field_suffix' => '.zip',
+    '#description' => t('The name of the archive file.'),
+  );
+  return $form;
+}
+
+function views_bulk_operations_archive_action_submit($form, $form_state) {
+  // If the chosen filename already exists, file_destination() will append
+  // an integer to it in order to make it unique.
+  $destination = $form_state['values']['scheme'] . '://' . $form_state['values']['filename'] . '.zip';
+  $destination = file_destination($destination, FILE_EXISTS_RENAME);
+  $parts = explode('://', $destination);
+
+  return array(
+    'scheme' => $parts[0],
+    'filename' => $parts[1],
+  );
+}
+
+/**
+ * Settings form (embed into the VBO field settings in the Views UI).
+ */
+function views_bulk_operations_archive_action_views_bulk_operations_form($options) {
+  $scheme_options = array();
+  foreach (file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL_NORMAL) as $scheme => $stream_wrapper) {
+    $scheme_options[$scheme] = $stream_wrapper['name'];
+  }
+  if (count($scheme_options) > 1) {
+    $form['scheme'] = array(
+      '#type' => 'radios',
+      '#title' => t('Storage'),
+      '#options' => $scheme_options,
+      '#default_value' => !empty($options['scheme']) ? $options['scheme'] : variable_get('file_default_scheme', 'public'),
+      '#description' => t('Select where the archive should be stored. Private file storage has significantly more overhead than public files, but allows restricted access.'),
+    );
+  }
+  else {
+    $scheme_option_keys = array_keys($scheme_options);
+    $form['scheme'] = array(
+      '#type' => 'value',
+      '#value' => reset($scheme_option_keys),
+    );
+  }
+
+  $form['temporary'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Temporary'),
+    '#default_value' => isset($options['temporary']) ? $options['temporary'] : TRUE,
+    '#description' => t('Temporary files older than 6 hours are removed when cron runs.'),
+  );
+  return $form;
+}
+
+/**
+ * Create a sanitized and unique version of the provided filename.
+ *
+ * @param $filename
+ *   String filename
+ *
+ * @return
+ *   The new filename.
+ */
+function _views_bulk_operations_archive_action_create_filename($filename, $archive_list) {
+  // Strip control characters (ASCII value < 32). Though these are allowed in
+  // some filesystems, not many applications handle them well.
+  $filename = preg_replace('/[\x00-\x1F]/u', '_', $filename);
+  if (substr(PHP_OS, 0, 3) == 'WIN') {
+    // These characters are not allowed in Windows filenames
+    $filename = str_replace(array(':', '*', '?', '"', '<', '>', '|'), '_', $filename);
+  }
+
+  if (in_array($filename, $archive_list)) {
+    // Destination file already exists, generate an alternative.
+    $pos = strrpos($filename, '.');
+    if ($pos !== FALSE) {
+      $name = substr($filename, 0, $pos);
+      $ext = substr($filename, $pos);
+    }
+    else {
+      $name = $filename;
+      $ext = '';
+    }
+
+    $counter = 0;
+    do {
+      $filename = $name . '_' . $counter++ . $ext;
+    } while (in_array($filename, $archive_list));
+  }
+
+  return $filename;
+}
diff --git a/plugins/operation_types/action.class.php b/plugins/operation_types/action.class.php
index d6eb9f7..2d40ec8 100644
--- a/plugins/operation_types/action.class.php
+++ b/plugins/operation_types/action.class.php
@@ -129,6 +129,7 @@ class ViewsBulkOperationsAction extends ViewsBulkOperationsBaseOperation {
    */
   public function execute($entity, array $context) {
     $context['entity_type'] = $this->entityType;
+    $context['settings'] = $this->getAdminOption('settings', array());
     $context += $this->formOptions;
     $context += $this->operationInfo['parameters'];
 
diff --git a/plugins/operation_types/base.class.php b/plugins/operation_types/base.class.php
index 374ac9d..053bb9f 100644
--- a/plugins/operation_types/base.class.php
+++ b/plugins/operation_types/base.class.php
@@ -48,8 +48,8 @@ abstract class ViewsBulkOperationsBaseOperation {
   /**
    * Returns the value of an admin option.
    */
-  public function getAdminOption($key) {
-    return isset($this->adminOptions[$key]) ? $this->adminOptions[$key] : NULL;
+  public function getAdminOption($key, $default = NULL) {
+    return isset($this->adminOptions[$key]) ? $this->adminOptions[$key] : $default;
   }
 
   /**
diff --git a/views_bulk_operations.module b/views_bulk_operations.module
index be9c24a..ccc9844 100644
--- a/views_bulk_operations.module
+++ b/views_bulk_operations.module
@@ -43,6 +43,7 @@ function views_bulk_operations_load_action_includes() {
   // caching the result has its cost).
   $path = drupal_get_path('module', 'views_bulk_operations') . '/actions/';
   $files = array(
+    'archive.action.inc',
     'argument_selector.action.inc',
     'delete.action.inc',
     'script.action.inc',
@@ -349,7 +350,11 @@ function views_bulk_operations_form($form, &$form_state, $vbo) {
       foreach ($vbo->view->result as $row_index => $result) {
         $dummy_selection[$row_index] = $result->{$vbo->field_alias};
       }
-      $context = array('selection' => $dummy_selection);
+
+      $context = array(
+        'settings' => $operation->getAdminOption('settings', array()),
+        'selection' => $dummy_selection,
+      );
       $form += $operation->form($form, $form_state, $context);
     }
     $form_state['operation'] = $operation;
@@ -419,6 +424,7 @@ function views_bulk_operations_config_form($form, &$form_state, $view, $output)
   drupal_set_title(t('Set parameters for %operation', array('%operation' => $operation->label())), PASS_THROUGH);
 
   $context = array(
+    'settings' => $operation->getAdminOption('settings', array()),
     'selection' => $form_state['selection'],
   );
   $form += $operation->form($form, $form_state, $context);
@@ -715,17 +721,29 @@ function _views_bulk_operations_execute($vbo, $selection, $operation, $force_dir
 
   if (!$force_direct && $operation->getAdminOption('use_queue')) {
     $entity_type = $vbo->get_entity_type();
+    $current = 1;
     foreach ($rows as $row_index => $row) {
+      // Some operations rely on knowing their position in the execution set
+      // (because of specific things that need to be done at the beginning
+      // or the end of the set).
+      $context = array(
+        'progress' => array(
+          'current' => $current,
+          'total' => count($rows),
+        ),
+      );
+
       $job = array(
         'description' => t('Perform %operation on @type !entity_id.', array(
           '%operation' => $operation->label(),
           '@type' => $entity_type,
           '!entity_id' => $row['entity_id'],
         )),
-        'arguments' => array($row_index, $row, $operation, $options, $user->uid),
+        'arguments' => array($row_index, $row, $operation, $context, $options, $user->uid),
       );
       $queue = DrupalQueue::get('views_bulk_operations');
       $queue->createItem($job);
+      $current++;
     }
     if ($options['display_result']) {
       drupal_set_message(t('Enqueued the selected operation (%operation).', array(
@@ -759,7 +777,7 @@ function _views_bulk_operations_execute($vbo, $selection, $operation, $force_dir
  * Process function for the Drupal Queue execution type.
  */
 function _views_bulk_operations_queue_process($data) {
-  list($row_index, $row, $operation, $options, $uid) = $data['arguments'];
+  list($row_index, $row, $operation, $operation_context, $options, $uid) = $data['arguments'];
 
   $entity_type = $operation->entityType;
   $entities = _views_bulk_operations_entity_load($entity_type, array($row['entity_id']), $options['revision']);
@@ -779,7 +797,6 @@ function _views_bulk_operations_queue_process($data) {
     return;
   }
 
-  $operation_context = array();
   // Pass the selected row to the operation if needed.
   if ($operation->needsRows()) {
     $operation_context['rows'] = array($row_index => $row['views_row']);
@@ -837,7 +854,15 @@ function _views_bulk_operations_batch_process($rows, $operation, $options, &$con
       continue;
     }
 
-    $operation_context = array();
+    // Some operations rely on knowing their position in the execution set
+    // (because of specific things that need to be done at the beginning
+    // or the end of the set).
+    $operation_context = array(
+      'progress' => array(
+        'current' => $context['sandbox']['progress'] + 1,
+        'total' => $context['sandbox']['max'],
+      ),
+    );
     // Pass the selected row to the operation if needed.
     if ($operation->needsRows()) {
       $operation_context['rows'] = array($row_index => $row['views_row']);
@@ -918,7 +943,15 @@ function _views_bulk_operations_direct_process($operation, $rows, $options, &$co
       }
 
       $entity = $entities[$entity_id];
-      $operation_context = array();
+      // Some operations rely on knowing their position in the execution set
+      // (because of specific things that need to be done at the beginning
+      // or the end of the set).
+      $operation_context = array(
+        'progress' => array(
+          'current' => $context['results']['rows'] + 1,
+          'total' => count($rows),
+        ),
+      );
       // Pass the selected rows to the operation if needed.
       if ($operation->needsRows()) {
         $operation_context['rows'] = array($row_index => $row['views_row']);
@@ -981,7 +1014,7 @@ function _views_bulk_operations_execute_finished($success, $results, $operations
   }
 
   if (!empty($options['display_result'])) {
-    drupal_set_message($message);
+    _views_bulk_operations_log($message);
   }
 }
 
@@ -1111,9 +1144,10 @@ function _views_bulk_operations_report_error($msg, $arg) {
 }
 
 /**
- * Helper function to log an information.
+ * Display a message to the user through the relevant function.
  */
 function _views_bulk_operations_log($msg) {
+  // Is VBO being run through drush?
   if (function_exists('drush_log')) {
     drush_log(strip_tags($msg), 'ok');
   }
