diff --git a/plugins/views_data_export_plugin_display_export.inc b/plugins/views_data_export_plugin_display_export.inc
index 741d488..ce2c6d7 100644
--- a/plugins/views_data_export_plugin_display_export.inc
+++ b/plugins/views_data_export_plugin_display_export.inc
@@ -404,6 +404,9 @@ class views_data_export_plugin_display_export extends views_plugin_display_feed
         break;
 
       case VIEWS_DATA_EXPORT_FOOTER:
+        // Update the temporary file size, otherwise we would get a problematic
+        // "Content-Length: 0" HTTP header, that may break the export download.
+        $this->outputfile_update_size();
         $sandbox['finished'] = 1;
         $state->batch_state = VIEWS_DATA_EXPORT_FINISHED;
         break;
@@ -574,9 +577,28 @@ class views_data_export_plugin_display_export extends views_plugin_display_feed
     if (!$this->view->init_style()) {
       $this->view->build_info['fail'] = TRUE;
     }
-    // Set the headers.
-    $this->add_http_headers();
-    file_transfer($this->outputfile_path(), array());
+
+    $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();
   }
 
   /**
@@ -716,6 +738,18 @@ 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);
+      file_save($file);
+    }
+  }
+
   function abort_export($errors) {
     // Just cause the next batch to do the clean-up
     if (!is_array($errors)) {
diff --git a/tests/access.test b/tests/access.test
new file mode 100644
index 0000000..1b58fa9
--- /dev/null
+++ b/tests/access.test
@@ -0,0 +1,182 @@
+<?php
+
+/**
+ * Test class for access checks for VDE downloads.
+ *
+ * Views Data Export enforces that a previously exported file may only be
+ * re-downloaded by the user that created the export. We test for that with
+ * this class.
+ */
+class ViewsDataExportAccessTest extends ViewsDataExportBaseTest {
+
+  protected $profile = 'testing';
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Access to temp files',
+      'description' => 'Check access to created export files.',
+      'group' => 'Views Data Export',
+    );
+  }
+
+  /**
+   * Test that VDE export can only be downloaded by the user that created them.
+   */
+  public function testExportedTempFileAccess() {
+    $this->admin_user1 = $this->drupalCreateUser();
+    $this->admin_user2 = $this->drupalCreateUser();
+
+    // 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);
+
+    $this->drupalLogin($this->admin_user1);
+    $this->assertBatchedExportEqual($path, $expected, 'Batched access export matched expected output.');
+
+    // Assert that we can re-download directly.
+    // 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.');
+
+    // 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.');
+  }
+
+  /**
+   * 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 0f7479c..d896842 100644
--- a/views_data_export.info
+++ b/views_data_export.info
@@ -14,6 +14,7 @@ files[] = plugins/views_data_export_plugin_style_export_xml.inc
 
 ; Tests
 files[] = "tests/base.test"
+files[] = "tests/access.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 92d9bf2..1861b14 100644
--- a/views_data_export.module
+++ b/views_data_export.module
@@ -31,6 +31,36 @@ 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.
+ *
+ * @param string $uri
+ *   A file URI.
+ *
+ * @return bool
+ *   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;
+}
+
+/**
  * Implementation of hook_theme().
  */
 function views_data_export_theme() {
@@ -275,7 +305,7 @@ function views_data_export_view_clear($export_id) {
 function views_data_export_file_presave($file) {
   // Ensure temporary files really are temporary.
   // @see: https://drupal.org/node/2198399
-  if (strpos($file->filename, 'views_data_export') === 0) {
+  if (views_data_export_is_export_file($file->uri)) {
     // There is no FILE_STATUS_TEMPORARY.
     $file->status = 0;
   }
