[PHP] 파일 다운로드 스크립트 (이어받기, 속도제한)

if(send_attachment('저장파일이름', "파일경로", 60 * 60 * 24, 10000) === true){
      exit;
}else{
      echo "download failed";
}

// 출처: https://gist.github.com/ssut/a3d97c7a35e5458687ed
function send_attachment($filename, $server_filename, $expires = 0, $speed_limit = 0){
      $remote = false;

      // check filename
      if (strpos($server_filename, 'http') === false) {
            if (!file_exists($server_filename) || !is_readable($server_filename)) {
                  return false;
            }
            if (($filesize = filesize($server_filename)) == 0) {
                  return false;
            }
            if (($fp = @fopen($server_filename, 'rb')) === false) {
                  return false;
            }
      } else {
            $remote = true;
            $handle = curl_init($server_filename);
            curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($handle, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($handle, CURLOPT_HEADER, true);
            curl_setopt($handle, CURLOPT_NOBODY, true);

            $response = curl_exec($handle);

            $http_code = curl_getinfo($handle, CURLINFO_HTTP_CODE);
            if ($http_code == 404) {
                  return false;
            }

            $filesize = curl_getinfo($handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
            curl_close($handle);

            $ch = curl_init($server_filename);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
      }

      $pass_remote = function ($ch, $chunk) {
            echo $chunk;
            flush();
            return strlen($chunk);
      };

      // replace special characters
      $illegal = array('\\', '/', '<', '>', '{', '}', ':', ';', '|', '"', '~', '`', '@', '#', '$', '%', '^', '&', '*', '?');
      $replace = array('', '', '(', ')', '(', ')', '_', ',', '_', '', '_', '\'', '_', '_', '_', '_', '_', '_', '', '');
      $filename = str_replace($illegal, $replace, $filename);
      $filename = preg_replace('/([\\x00-\\x1f\\x7f\\xff]+)/', '', $filename);

      // replace special spaces to normal spaces(0x20).
      $filename = trim(preg_replace('/[\\pZ\\pC]+/u', ' ', $filename));

      // remove duplicates or dots.
      $filename = trim($filename, ' .-_');
      $filename = preg_replace('/__+/', '_', $filename);
      if ($filename === '') {
            return false;
      }

      // get User-Agent from browser
      $ua = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
      $old_ie = (bool)preg_match('#MSIE [3-8]\.#', $ua);

      // add filename to header when filename only includes normal characters.
      if (preg_match('/^[a-zA-Z0-9_.-]+$/', $filename)) {
            $header = 'filename="' . $filename . '"';
      }
      // < IE 9 or < FF 5
      elseif ($old_ie || preg_match('#Firefox/(\d+)\.#', $ua, $matches) && $matches[1] < 5) {
            $header = 'filename="' . rawurlencode($filename) . '"';
      }
      // < Chrome 11
      elseif (preg_match('#Chrome/(\d+)\.#', $ua, $matches) && $matches[1] < 11) {
            $header = 'filename=' . $filename;
      }
      // < Safari 6
      elseif (preg_match('#Safari/(\d+)\.#', $ua, $matches) && $matches[1] < 6) {
            $header = 'filename=' . $filename;
      }
      // Android
      elseif (preg_match('#Android #', $ua, $matches)) {
            $header = 'filename="' . $filename . '"';
      }
      // other browsers assume that validate RFC/2231/5987 standards
      // but, add old style filename information for special circumstances
      else {
            $header = "filename*=UTF-8''" . rawurlencode($filename) . '; filename="' . rawurlencode($filename) . '"';
      }

      // cache is disallowed by client
      if (!$expires) {
            // Cannot use no-cache and pragma header when use old IE versions(<= 8) and SSL.
            if ($old_ie) {
                  header('Cache-Control: private, must-revalidate, post-check=0, pre-check=0');
                  header('Expires: Sat, 01 Jan 2000 00:00:00 GMT');
            } else {
                  header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
                  header('Expires: Sat, 01 Jan 2000 00:00:00 GMT');
            }
      }
      // cache is allowed by client
      else {
            header('Cache-Control: max-age=' . (int)$expires);
            header('Expires: ' . gmdate('D, d M Y H:i:s', time() + (int)$expires) . ' GMT');
      }

      // process range header for resume download
      if (isset($_SERVER['HTTP_RANGE']) && preg_match('/^bytes=(\d+)-/', $_SERVER['HTTP_RANGE'], $matches)) {
            $range_start = $matches[1];
            if ($range_start < 0 || $range_start > $filesize) {
                  header('HTTP/1.1 416 Requested Range Not Satisfiable');
                  return false;
            }
            header('HTTP/1.1 206 Partial Content');
            header('Content-Range: bytes ' . $range_start . '-' . ($filesize - 1) . '/' . $filesize);
            header('Content-Length: ' . ($filesize - $range_start));
            if ($remote) {
                  curl_setopt($ch, CURLOPT_WRITEFUNCTION, $pass_remote);
                  curl_setopt($ch, CURLOPT_RANGE, $range_start . '-' . ($filesize - 1));
            }
      } else {
            $range_start = 0;
            header('Content-Length: ' . $filesize);

            if ($remote) {
                  curl_setopt($ch, CURLOPT_WRITEFUNCTION, $pass_remote);
                  curl_setopt($ch, CURLOPT_RANGE, '0-' . $filesize);
            }
      }

      // send other headers.
      header('Accept-Ranges: bytes');
      header('Content-Type: application/octet-stream');
      header('Content-Disposition: attachment; ' . $header);

      // clear output buffer.
      // (blocks file broken and decrease memory usage)
      while (ob_get_level()) {
            ob_end_clean();
      }

      // send a file each 64KB and clear output buffer.
      // sometimes occurs memory leak when use readfile() function.
      $block_size = 16 * 1024;
      $speed_sleep = $speed_limit > 0 ? round(($block_size / $speed_limit / 1024) * 1000000) : 0;

      $buffer = '';
      if ($range_start > 0 && !$remote) {
            fseek($fp, $range_start);
            $alignment = (ceil($range_start / $block_size) * $block_size) - $range_start;
            if ($alignment > 0) {
                  $buffer = fread($fp, $alignment);
                  echo $buffer;
                  unset($buffer);
                  flush();
            }
      }
      while (!feof($fp) && !$remote) {
            $buffer = fread($fp, $block_size);
            echo $buffer;
            unset($buffer);
            flush();
            usleep($speed_sleep);
      }

      if ($remote && $ch) {
            curl_exec($ch);
      }

      if (!$remote) {
            fclose($fp);
      } else {
            curl_close($ch);
      }

      // true when successfully sent.
      return true;
}

Leave a Comment