diff --git a/plugins/views_data_export_plugin_display_export.inc b/plugins/views_data_export_plugin_display_export.inc
index ce2c6d7..9ce5721 100644
--- a/plugins/views_data_export_plugin_display_export.inc
+++ b/plugins/views_data_export_plugin_display_export.inc
@@ -238,8 +238,17 @@ class views_data_export_plugin_display_export extends views_plugin_display_feed
     }
 
     // Try and get a batch context if possible.
-    $eid = !empty($_GET['eid']) ? $_GET['eid'] :
-            (!empty($this->batched_execution_state->eid) ? $this->batched_execution_state->eid : FALSE);
+
+    if (!empty($_GET['eid']) && !empty($_GET['token']) && drupal_valid_token($_GET['token'], 'views_data_export/' . $_GET['eid'])) {
+      $eid = $_GET['eid'];
+    }
+    elseif (!empty($this->batched_execution_state->eid)) {
+      $eid = $this->batched_execution_state->eid;
+    }
+    else {
+      $eid = FALSE;
+    }
+
     if ($eid) {
       $this->batched_execution_state = views_data_export_get($eid);
     }
@@ -293,6 +302,9 @@ class views_data_export_plugin_display_export extends views_plugin_display_feed
     $this->batched_execution_state = views_data_export_new($this->view->name, $this->view->current_display, $this->outputfile_create());
     views_data_export_view_store($this->batched_execution_state->eid, $this->view);
 
+    // Record a usage of our file, so we can identify our exports later.
+    file_usage_add(file_load($this->batched_execution_state->fid), 'views_data_export', 'eid', $this->batched_execution_state->eid);
+
     // We need to build the index right now, before we lose $_GET etc.
     $this->initialize_index();
     //$this->batched_execution_state->fid = $this->outputfile_create();
@@ -306,9 +318,13 @@ class views_data_export_plugin_display_export extends views_plugin_display_feed
     list($usec, $sec) = explode(' ', microtime());
     $this->batched_execution_state->sandbox['started'] = (float) $usec + (float) $sec;
 
+    // Pop something into the session to ensure it stays aorund.
+    $_SESSION['views_data_export'][$this->batched_execution_state->eid] = TRUE;
+
     // Build up our querystring for the final page callback.
     $querystring = array(
       'eid' => $this->batched_execution_state->eid,
+      'token' => drupal_get_token('views_data_export/' . $this->batched_execution_state->eid),
       'return-url' => NULL,
     );
 
@@ -424,6 +440,13 @@ class views_data_export_plugin_display_export extends views_plugin_display_feed
   function execute_final() {
     // Should we download the file.
     if (!empty($_GET['download'])) {
+      // Clean up our session, if we need to.
+      if (isset($_SESSION)) {
+        unset($_SESSION['views_data_export'][$this->batched_execution_state->eid]);
+        if (empty($_SESSION['views_data_export'])) {
+          unset($_SESSION['views_data_export']);
+        }
+      }
       // This next method will exit.
       $this->transfer_file();
     }
@@ -532,6 +555,7 @@ class views_data_export_plugin_display_export extends views_plugin_display_feed
     $query = array(
       'download' => 1,
       'eid' => $this->batched_execution_state->eid,
+      'token' => drupal_get_token('views_data_export/' . $this->batched_execution_state->eid),
     );
 
     return theme('views_data_export_complete_page', array(
@@ -577,28 +601,12 @@ class views_data_export_plugin_display_export extends views_plugin_display_feed
     if (!$this->view->init_style()) {
       $this->view->build_info['fail'] = TRUE;
     }
-
-    $uri = $this->outputfile_path();
-    $scheme = file_uri_scheme($uri);
-    if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) {
-      $headers = file_download_headers($uri);
-      if (count($headers)) {
-        // Set our headers and ensure no other one conflicts with them.
-        $this->add_http_headers();
-        $reserved_headers = array_flip(array('content-type', 'cache-control', 'content-disposition'));
-        foreach ($headers as $name => $value) {
-          if (isset($reserved_headers[drupal_strtolower($name)])) {
-            unset($headers[$name]);
-          }
-        }
-        file_transfer($uri, $headers);
-      }
-      drupal_access_denied();
-    }
-    else {
-      drupal_not_found();
-    }
-    drupal_exit();
+    // Set the headers.
+    $this->add_http_headers();
+    $headers = array(
+      'Content-Length' => $this->outputfile_entity()->filesize,
+    );
+    file_transfer($this->outputfile_path(), $headers);
   }
 
   /**
@@ -691,9 +699,9 @@ class views_data_export_plugin_display_export extends views_plugin_display_feed
   }
 
   /**
-   * Get the output file path.
+   * Get the output file entity.
    */
-  function outputfile_path() {
+  public function outputfile_entity() {
     if (empty($this->_output_file)) {
       if (!empty($this->batched_execution_state->fid)) {
         // Return the filename associated with this file.
@@ -703,7 +711,16 @@ class views_data_export_plugin_display_export extends views_plugin_display_feed
         return NULL;
       }
     }
-    return $this->_output_file->uri;
+    return $this->_output_file;
+  }
+
+  /**
+   * Get the output file path.
+   */
+  public function outputfile_path() {
+    if ($file = $this->outputfile_entity()) {
+      return $file->uri;
+    }
   }
 
   /**
@@ -725,6 +742,11 @@ class views_data_export_plugin_display_export extends views_plugin_display_feed
     // Save the file into the DB.
     $file = $this->file_save_file($path);
 
+    // Make sure the file is marked as temporary.
+    // There is no FILE_STATUS_TEMPORARY constant.
+    $file->status = 0;
+    file_save($file);
+
     return $file->fid;
   }
 
@@ -742,10 +764,8 @@ class views_data_export_plugin_display_export extends views_plugin_display_feed
    * Updates the file size in the file entity.
    */
   protected function outputfile_update_size() {
-    $output_file = $this->outputfile_path();
-    $file = current(file_load_multiple(array(), array('uri' => $output_file)));
-    if ($file) {
-      $file->filesize = filesize($output_file);
+    if ($file = $this->outputfile_entity()) {
+      $file->filesize = filesize($file->uri);
       file_save($file);
     }
   }
diff --git a/tests/access.test b/tests/access.test
index 1b58fa9..f826d75 100644
--- a/tests/access.test
+++ b/tests/access.test
@@ -38,19 +38,45 @@ class ViewsDataExportAccessTest extends ViewsDataExportBaseTest {
     variable_set('menu_rebuild_needed', TRUE);
 
     $this->drupalLogin($this->admin_user1);
+    // Catpure the session_id as the redirects in the request ditch it.
+    $session_id = $this->session_id;
     $this->assertBatchedExportEqual($path, $expected, 'Batched access export matched expected output.');
 
-    // Assert that we can re-download directly.
+    // Remove all the test data, so future exports will be different.
+    db_truncate('views_test')->execute();
+    $this->resetAll();
+
+    // Assert that we can re-download directly when supplying the token.
+    // We rely on this being the first export in this test class.
+    // Restore the session_id from above so we can use drupalGetToken.
+    $this->session_id = $session_id;
+    $token = $this->drupalGetToken('views_data_export/1');
+    $this->drupalGet($path, array('query' => array('eid' => 1, 'download' => 1, 'token' => $token)));
+    $output = $this->drupalGetContent();
+    $this->assertEqual($this->normaliseString($output), $expected, 'Re-download of export file by original user is possible with session token.');
+
+    // Assert that we cannot re-download directly without supplying the token.
     // We rely on this being the first export in this test class.
     $this->drupalGet($path, array('query' => array('eid' => 1, 'download' => 1)));
     $output = $this->drupalGetContent();
-    $this->assertEqual($this->normaliseString($output), $this->normaliseString($expected), 'Re-download of export file is possible.');
+    $this->assertEqual($this->normaliseString($output), '', 'Re-download of export file by original user is not possible.');
 
     // Assert that someone else can't download our file.
     // We rely on this being the first export in this test class.
     $this->drupalLogin($this->admin_user2);
-    $this->drupalGet($path, array('query' => array('eid' => 1, 'download' => 1)));
-    $this->assertResponse(403, 'Re-download of export file by another user is not possible.');
+    $this->drupalGet($path, array('query' => array('eid' => 1, 'download' => 1, 'token' => $token)));
+    $output = $this->drupalGetContent();
+    $this->assertEqual($this->normaliseString($output), '', 'Re-download of export file by different user is not possible.');
+  }
+
+  /**
+   * Overrides DrupalWebTestCase::drupalGetToken() to support the hash salt.
+   *
+   * @todo Remove when http://drupal.org/node/1555862 is fixed in core.
+   */
+  protected function drupalGetToken($value = '') {
+    $private_key = drupal_get_private_key();
+    return drupal_hmac_base64($value, $this->session_id . $private_key . drupal_get_hash_salt());
   }
 
   /**
diff --git a/tests/garbagecollection.test b/tests/garbagecollection.test
new file mode 100644
index 0000000..c60d75b
--- /dev/null
+++ b/tests/garbagecollection.test
@@ -0,0 +1,188 @@
+<?php
+
+/**
+ * Test class for garbage collection of VDE export data.
+ */
+class ViewsDataExportGarbageCollectionTest extends ViewsDataExportBaseTest {
+
+  protected $profile = 'testing';
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Garbage collection',
+      'description' => 'Checks garbage collection of batched exports',
+      'group' => 'Views Data Export',
+    );
+  }
+
+  /**
+   * Test that VDE export can only be downloaded by the user that created them.
+   */
+  public function testExportedGarbageCollection() {
+    // Run a batched export.
+    $path = 'vde_test/' . $this->randomName();
+    list($view, $expected) = $this->getExportView($path);
+    $display = &$view->display['vde_test']->handler;
+    // Set this view to be batched.
+    $display->override_option('use_batch', 'batch');
+    // Save this view so we can hit the path.
+    $view->save();
+    // Ensure that the menu router system is rebuilt on the next page load.
+    variable_set('menu_rebuild_needed', TRUE);
+    $exports = $this->getNumberOfStoredExports();
+    $files = $this->getNumberOfFiles();
+    $this->assertBatchedExportEqual($path, $expected, 'Batched access export matched expected output.');
+    // We should have created a new export and file.
+    $this->assertEqual($this->getNumberOfStoredExports(), $exports + 1, 'A single new batched export was created');
+    $this->assertEqual($this->getNumberOfFiles(), $files + 1, 'A single new temporary file was created');
+
+    $middle_timestamp = time();
+    sleep(1);
+    $this->assertBatchedExportEqual($path, $expected, 'Batched access export matched expected output.');
+    // We should have created a new export and file.
+    $this->assertEqual($this->getNumberOfStoredExports(), $exports + 2, 'A single new batched export was created');
+    $this->assertEqual($this->getNumberOfFiles(), $files + 2, 'A single new temporary file was created');
+
+
+    // Garbage collect the first export only.
+    views_data_export_garbage_collect(REQUEST_TIME - $middle_timestamp);
+    $this->assertEqual($this->getNumberOfStoredExports(), $exports + 1, 'Garbage collection removed 1 old export');
+    $this->assertEqual($this->getNumberOfFiles(), $files + 1, 'Garbage collection removed 1 old temporary file');
+  }
+
+  protected function getNumberOfStoredExports() {
+    return (int) db_select('views_data_export')->countQuery()->execute()->fetchField();
+  }
+
+  protected function getNumberOfFiles() {
+    return (int) db_select('file_managed')->countQuery()->execute()->fetchField();
+  }
+
+
+  /**
+   * Build and return a basic view of the views_test table.
+   *
+   * @return view
+   */
+  protected function getBasicExportView() {
+    views_include('view');
+
+    // Create the basic view.
+    $view = new view();
+    $view->vid = 'new';
+    $view->base_table = 'views_test';
+
+    // Set up the fields we need.
+    $display = $view->new_display('default', 'Master', 'default');
+
+    $display->override_option('fields', array(
+      'id' => array(
+        'id' => 'id',
+        'table' => 'views_test',
+        'field' => 'id',
+        'relationship' => 'none',
+      ),
+      'name' => array(
+        'id' => 'name',
+        'table' => 'views_test',
+        'field' => 'name',
+        'relationship' => 'none',
+      ),
+      'age' => array(
+        'id' => 'age',
+        'table' => 'views_test',
+        'field' => 'age',
+        'relationship' => 'none',
+      ),
+    ));
+
+    // Set up the sort order.
+    $display->override_option('sorts', array(
+      'id' => array(
+        'order' => 'ASC',
+        'id' => 'id',
+        'table' => 'views_test',
+        'field' => 'id',
+        'relationship' => 'none',
+      ),
+    ));
+
+    // Set up the pager.
+    $display->override_option('pager', array(
+      'type' => 'none',
+      'options' => array('offset' => 0),
+    ));
+
+    return $view;
+  }
+
+  protected function getStylePluginName() {
+    return 'views_data_export_txt';
+  }
+
+  protected function getExportView($path = 'vde_test') {
+    // Create the basic view.
+    $view = $this->getBasicExportView();
+
+    $display = $view->new_display('views_data_export', 'Data export', 'vde_test');
+    $display->override_option('style_plugin', $this->getStylePluginName());
+    $display->override_option('path', $path);
+
+    $expected = '[ID]
+
+1
+[Name]
+
+John
+[Age]
+
+25
+----------------------------------------
+
+[ID]
+
+2
+[Name]
+
+George
+[Age]
+
+27
+----------------------------------------
+
+[ID]
+
+3
+[Name]
+
+Ringo
+[Age]
+
+28
+----------------------------------------
+
+[ID]
+
+4
+[Name]
+
+Paul
+[Age]
+
+26
+----------------------------------------
+
+[ID]
+
+5
+[Name]
+
+Meredith
+[Age]
+
+30
+----------------------------------------';
+
+    return array(&$view, $expected);
+  }
+}
\ No newline at end of file
diff --git a/views_data_export.info b/views_data_export.info
index d896842..1ace4bd 100644
--- a/views_data_export.info
+++ b/views_data_export.info
@@ -15,6 +15,7 @@ files[] = plugins/views_data_export_plugin_style_export_xml.inc
 ; Tests
 files[] = "tests/base.test"
 files[] = "tests/access.test"
+files[] = "tests/garbagecollection.test"
 files[] = "tests/csv_export.test"
 files[] = "tests/doc_export.test"
 files[] = "tests/txt_export.test"
diff --git a/views_data_export.module b/views_data_export.module
index 1861b14..a616545 100644
--- a/views_data_export.module
+++ b/views_data_export.module
@@ -31,22 +31,6 @@ function views_data_export_views_api() {
 }
 
 /**
- * Implements hook_file_download().
- */
-function views_data_export_file_download($uri) {
-  if (views_data_export_is_export_file($uri)) {
-    $result = -1;
-    // Allow only owners to access export files.
-    $file = current(entity_load('file', FALSE, array('uri' => $uri)));
-    if ($file && $file->uid == $GLOBALS['user']->uid) {
-      // This is only necessary for file_download_headers() to return a result
-      // evaluating to TRUE, in case no other module added any header.
-      $result = array('X-Drupal-ViewsDataExport' => 1);
-    }
-    return $result;
-  }
-}
-
 /**
  * Checks whether the passed URI identifies an export file.
  *
@@ -57,7 +41,12 @@ function views_data_export_file_download($uri) {
  *   TRUE if the URI identifies an export file, FALSE otherwise.
  */
 function views_data_export_is_export_file($uri) {
-  return file_uri_scheme($uri) == 'temporary' && strpos(file_uri_target($uri), 'views_data_export') === 0;
+  foreach (entity_load('file', FALSE, array('uri' => $uri)) as $file) {
+    // See if this is an export file.
+    $usages = file_usage_list($file);
+    return !empty($usages['views_data_export']['eid']);
+  }
+  return FALSE;
 }
 
 /**
@@ -144,9 +133,10 @@ function views_data_export_garbage_collect($expires = NULL, $chunk = NULL) {
     }
 
     // We do two things to exports we want to garbage collect
-    // 1. Delete the index table for it, if it is still around
-    // 2. Delete the row from the exports table
-    // 3. Delete the view from the object_cache
+    // 1. Delete the index table for it, if it is still around.
+    // 2. Delete the files used during the export.
+    // 3. Delete the row from the exports table.
+    // 4. Delete the view from the object_cache.
     if (count($eids_to_clear)) {
       foreach ($eids_to_clear as $eid) {
         // 1. Delete index table, if it is still around for some reason
@@ -154,14 +144,19 @@ function views_data_export_garbage_collect($expires = NULL, $chunk = NULL) {
         if (db_table_exists($table)) {
           db_drop_table($table);
         }
+
+        // 2. Delete the files used during the export.
+        foreach (views_data_export_export_list_files($eid) as $file) {
+          file_delete($file, TRUE);
+        }
       }
 
-      // 2. Delete the entries in the exports table.
+      // 3. Delete the entries in the exports table.
       db_delete('views_data_export')
         ->condition('eid', $eids_to_clear, 'IN')
         ->execute();
 
-      // 3. Clear the cached views
+      // 4. Clear the cached views
       views_data_export_view_clear($eids_to_clear);
     }
 
@@ -169,6 +164,25 @@ function views_data_export_garbage_collect($expires = NULL, $chunk = NULL) {
   }
 }
 
+/**
+ * Determines where a file is used.
+ *
+ * @param $eid
+ *   The ID of a Views Data Export.
+ *
+ * @return array
+ *   An array of loaded files objects used by the specified export.
+ */
+function views_data_export_export_list_files($eid) {
+  $result = db_select('file_usage', 'f')
+    ->fields('f', array('fid'))
+    ->condition('id', $eid)
+    ->condition('type', 'eid')
+    ->condition('module', 'views_data_export')
+    ->execute();
+  return file_load_multiple($result->fetchCol());
+}
+
 
 /**
  * Batch API callback.
@@ -298,15 +312,3 @@ function views_data_export_view_clear($export_id) {
     ->condition('eid', $export_id)
     ->execute();
 }
-
-/**
- * Implements hook_file_presave().
- */
-function views_data_export_file_presave($file) {
-  // Ensure temporary files really are temporary.
-  // @see: https://drupal.org/node/2198399
-  if (views_data_export_is_export_file($file->uri)) {
-    // There is no FILE_STATUS_TEMPORARY.
-    $file->status = 0;
-  }
-}
