diff --git a/lib/exe/fetch.php b/lib/exe/fetch.php
index 46010917c7de2017465c0a0b2ad9de15ac1668b9..64bd35729a080978daff3c8de97a112516c528ce 100644
--- a/lib/exe/fetch.php
+++ b/lib/exe/fetch.php
@@ -14,6 +14,7 @@
   require_once(DOKU_INC.'inc/auth.php');
   //close sesseion
   session_write_close();
+  if(!defined('CHUNK_SIZE')) define('CHUNK_SIZE',16*1024);
 
   $mimetypes = getMimeTypes();
 
@@ -53,8 +54,8 @@
       exit;
     }
     $FILE  = mediaFN($MEDIA);
-  } 
-  
+  }
+
   //check file existance
   if(!@file_exists($FILE)){
     header("HTTP/1.0 404 Not Found");
@@ -68,35 +69,123 @@
     $FILE = get_resized($FILE,$EXT,$WIDTH,$HEIGHT);
   }
 
+  // finally send the file to the client
+  sendFile($FILE,$MIME);
 
-  //FIXME set sane cachecontrol headers
-  //FIXME handle conditional and partial requests
+/* ------------------------------------------------------------------------ */
 
-  //send file
-  header("Content-Type: $MIME");
-  header('Last-Modified: '.date('r',filemtime($FILE)));
-  header('Content-Length: '.filesize($FILE));
-  header('Cache-Control: private, must-revalidate, post-check=0, pre-check=0');
+/**
+ * Set headers and send the file to the client
+ *
+ * @author Andreas Gohr <andi@splitbrain.org>
+ */
+function sendFile($file,$mime){
+  // send headers
+  header("Content-Type: $mime");
+  http_conditionalRequest(filemtime($file));
+  list($start,$len) = http_rangeRequest(filesize($file));
+  header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+  header('Pragma: public');
+  header('Accept-Ranges: bytes');
 
   //application mime type is downloadable
-  if(substr($MIME,0,11) == 'application'){
-    header('Content-Disposition: attachment; filename="'.basename($FILE).'"');
+  if(substr($mime,0,11) == 'application'){
+    header('Content-Disposition: attachment; filename="'.basename($file).'";');
   }
 
-  $fp = @fopen($FILE,"rb");
+  // send file contents
+  $fp = @fopen($file,"rb");
   if($fp){
-    while (!feof($fp)) {
+    fseek($fp,$start); //seek to start of range
+
+    $chunk = ($len > CHUNK_SIZE) ? CHUNK_SIZE : $len;
+    while (!feof($fp) && $chunk > 0) {
       @set_time_limit(); // large files can take a lot of time
-      print fread($fp, 16*1024);
+      print fread($fp, $chunk);
       flush();
+      $len -= $chunk;
+      $chunk = ($len > CHUNK_SIZE) ? CHUNK_SIZE : $len;
     }
     fclose($fp);
   }else{
     header("HTTP/1.0 500 Internal Server Error");
-    print "Could not read $FILE - bad permissions?";
+    print "Could not read $file - bad permissions?";
   }
+}
 
-/* ------------------------------------------------------------------------ */
+/**
+ * Checks and sets headers to handle range requets
+ *
+ * @author  Andreas Gohr <andi@splitbrain.org>
+ * @returns array The start byte and the amount of bytes to send
+ */
+function http_rangeRequest($size){
+  if(!isset($_SERVER['HTTP_RANGE'])){
+    // no range requested - send the whole file
+    header("Content-Length: $size");
+    return array(0,$size);
+  }
+
+  $t = explode('=', $_SERVER['HTTP_RANGE']);
+  if (!$t[0]=='bytes') {
+    // we only understand byte ranges - send the whole file
+    header("Content-Length: $size");
+    return array(0,$size);
+  }
+
+  $r = explode('-', $t[1]);
+  $start = (int)$r[0];
+  $end = (int)$r[1];
+  if (!$end) $end = $size - 1;
+  if ($start > $end || $start > $size || $end > $size){
+    header('HTTP/1.1 416 Requested Range Not Satisfiable');
+    print 'Bad Range Request!';
+    exit;
+  }
+
+  $tot = $end - $start + 1;
+  header('HTTP/1.1 206 Partial Content');
+  header("Content-Range: bytes {$start}-{$end}/{$size}");
+  header("Content-Length: $tot");
+
+  return array($start,$tot);
+}
+
+/**
+ * Checks and sets HTTP headers for conditional HTTP requests
+ *
+ * @author Simon Willison <swillison@gmail.com>
+ * @link   http://simon.incutio.com/archive/2003/04/23/conditionalGet
+ */
+function http_conditionalRequest($timestamp){
+    // A PHP implementation of conditional get, see 
+    //   http://fishbowl.pastiche.org/archives/001132.html
+    $last_modified = substr(date('r', $timestamp), 0, -5).'GMT';
+    $etag = '"'.md5($last_modified).'"';
+    // Send the headers
+    header("Last-Modified: $last_modified");
+    header("ETag: $etag");
+    // See if the client has provided the required headers
+    $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ?
+        stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']) :
+        false;
+    $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ?
+        stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) : 
+        false;
+    if (!$if_modified_since && !$if_none_match) {
+        return;
+    }
+    // At least one of the headers is there - check them
+    if ($if_none_match && $if_none_match != $etag) {
+        return; // etag is there but doesn't match
+    }
+    if ($if_modified_since && $if_modified_since != $last_modified) {
+        return; // if-modified-since is there but doesn't match
+    }
+    // Nothing has changed since their last request - serve a 304 and exit
+    header('HTTP/1.0 304 Not Modified');
+    exit;
+}
 
 /**
  * Resizes the given image to the given size
@@ -165,7 +254,7 @@ function get_from_URL($url,$ext,$cache){
         return false;
       }
   }
-      
+
   //if cache exists use it else
   if($mtime) return $local;
 
@@ -286,7 +375,7 @@ function resize_imageGD($ext,$from,$from_w,$from_h,$to,$to_w,$to_h){
  * @param  int $mem  Size of memory you want to allocate in bytes
  * @param  int $used already allocated memory (see above)
  * @author Filip Oscadal <webmaster@illusionsoftworks.cz>
- * @author Andreas Gohr <andi@splitbrain.org> 
+ * @author Andreas Gohr <andi@splitbrain.org>
  */
 function is_mem_available($mem,$bytes=1048576){
   $limit = trim(ini_get('memory_limit'));