Цитата Foreigner:
Не совсем понятно зачем выводить по одному байту »
|
Согласно
спецификации. От клиента к серверу в заголовках запроса может придти любой диапазон байтов, и не обязательно, что он будет кратным использованному размеру блока для dd. Соответственно, обеспечить точный вывод запрошенного клиентом я могу только при блоке в 1 байт, но при этом очень проседает производительность. Текущая версия реализации выглядит так:
Код скрипта
Код:

@Echo Off
:: Обрабатываем аргументы
Set Data=%*
:Next
For /F "tokens=1,* delims=;" %%A In ("%Data%") Do (
For /F "tokens=1,2 delims==" %%C In ("%%A") Do (Set %%C=%%D)
If Not "%%B"=="" Set Data=%%B&GoTo :Next
)
:: Обрабатываем get\post параметры
rem Выпилено на время теста, работаем только с заголовками запроса
:: Обрабатываем куки
If Not Defined H-Cookie Set "H-Cookie=dummy"]
::
Set H-Cookie|Findstr /C:"<" /C:">" /C:"&" /C:"|" /C:^"\^"^" /C:"(" /C:")" /C:"\^">nul&&(Call HTTP302Header err_badcookie&Exit)
For %%A In ("%H-Cookie:;=" "%") Do For /F "tokens=1,* delims==" %%A In ("%%~A") Do Set "%%A=%%B"
:: Проверяем авторизацию и доступ
If Not Defined Code Exit /B 3 &:: err_nosession
For %%A In (User Access LastSeen) Do Set "%%A="
For /F "tokens=1-3 delims=|" %%A In ('Call GetUserInfo %H-REMOTE_ADDR% %Code%') Do (
Set "UserName=%%A"
Set "Access=%%B"
Set "LastLogin=%%C"
)
If Not Defined Access Exit /B 3 &:: err_nosession
If %Access% LSS 5 Exit /B 5 &:: err_noaccess
:: ДЛЯ ОТЛАДКИ
:: В рабочей копии скрипта все эти данные будут взяты из БД
Set File=D:\Projects\cmdserv\wwwroot\test.mp3
For %%A In ("%File%") Do (
Set name=%%~nxA
Set size=%%~zA
)
Set timestamp=1442271745
:: Приводим имя файла к рабочей кодировке, для отладки, в продакшене имя сразу будет в юникоде
For /F "eol= delims=" %%A In ('Echo %name%^|recode cp866..utf8') Do Set "nameUTF8=%%A"
:: Конвертируем время для Last-Modified
For /F "delims=" %%A In ('UnixTime2HTTPDate %timestamp%') Do (Set timestamp=%%A)
:: Получаем текущее время
For /F "delims=" %%A In ('GetHTTPDate') Do (Set datetime=%%A)
:: Получаем MIME-тип, таблица для сопоставления по умолчанию указана в переменных окружения враппера
For /F "delims=" %%A In ('GetMIMEType "%name%"') Do Set "mimetype=%%A"
:: Если указан Range переходим к обработчику запроса
If Defined H-Range GoTo :Range
:: Отдаём заголовки и файл, если не поступало запросов на частичную отдачу
Echo HTTP/1.1 200 OK
Echo Server: OWS/%__OWS_version%
Echo Date: %datetime%
Echo Accept-Ranges: bytes
Echo Content-Type: %mimetype%
Echo Content-Disposition: attachment; filename="%nameUTF8%"; size=%size%; creation-date=%timestamp%; modification-date=%timestamp%; read-date=%datetime%
Echo Last-Modified: %timestamp%
Echo Content-Length: %size%
Echo Connection: close
Echo.
Type "%File%"
Exit
:Range
SetLocal EnableDelayedExpansion
:: Случайная строка для использования в качестве разделителя
For /F "delims=" %%A In ('GetRandomName charset=3 length=16') Do (Set boundary=%%A)
:: Максимальное значение диапазона = размер-1байт
For /F "delims=" %%- In ('cc %size%-1') Do Set MaxRange=%%-
:: Имя временного файла, используемого если в одном запросе несколько диапазонов
Set TmpFile="%Temp%\~part%boundary%.tmp"
:: Выделяем диапазоны из заголовка, если тип не bytes - отдаём HTTP 416
For /F "tokens=1,* delims==" %%A In ("%H-Range%") Do If /I "%%A"=="bytes" (Set "Ranges=%%B") Else (Call :Err416)
Set Parts=0
For %%A In (%Ranges%) Do (
Set rejected=false
Set Range="%%A"
For /F "tokens=1,2 delims=-" %%B In ("!Range:-="-"!") Do (
Set "rMin=%%~B"
Set "rMax=%%~C"
:: Разворачиваем диапазоны с пустыми значениями
If "!rMax!"=="" Set rMax=%MaxRange%
cc !rMax! ^>= %size%|ExitCode&&Set rMax=%MaxRange%
If "!rMin!"=="" For /F "delims=" %%- In ('cc %MaxRange%-!rMax!+1') Do (
Set rMax=%MaxRange%
Set rMin=%%-
)
)
:: Проверки
If Not Defined rMin Set rejected=true
If Not Defined rMax Set rejected=true
cc !rMin! ^<= !rMax!|ExitCode||Set rejected=true
:: Если диапазон проходит по всем условиям, записываем аргументы для функции, выдающей часть файла
If "!rejected!"=="false" (
Set /A Parts+=1
Set $Range=!rMin!-!rMax!/%size%
Set Skip=!rMin!
For /F "delims=" %%- In ('cc !rMax!+1-!rMin!') Do Set Length=%%-
Set Part[!Parts!]="!Skip!" "!Length!" "!$Range!" "%boundary%" "!Parts!"
)
)
:: Есть заголовок Range, но ни один из диапазонов невалиден? Отдаём HTTP 416
If "%Parts%"=="0" Call :Err416
Echo HTTP/1.1 206 Partial Content
Echo Server: OWS/%__OWS_version%
Echo Date: %datetime%
Echo Last-Modified: %timestamp%
Echo Accept-Ranges: bytes
If "%Parts%"=="1" (
Echo Content-Type: %mimetype%
) Else (
Echo Content-Type: multipart/byteranges; boundary=%boundary%
)
If "%Parts%"=="1" (
Echo Content-Length: !Length!
Echo Content-Range: bytes !rMin!-!rMax!/%size%
Echo.
:: Нет смысла играться со временным файлом, если диапазон один - выводим напрямую
Call :PartialContent !Part[1]! 1
) Else (
For /L %%A In (1,1,!Parts!) Do (
Call :PartialContent !Part[%%A]! !Parts!>>%TmpFile%
)
:: Подсчитываем размер получившихся частей ВМЕСТЕ с заголовками и разделителями
Call :PartsSize %TmpFile%
Echo.
rem Выводим контент и удаляем временный файл
Type "%TmpFile%"
Del "%TmpFile%" 2>nul
)
EndLocal
Exit
:: Выдаём часть файла согласно аргументам
:PartialContent (skip, length, range, boundary, part, parts)
rem Echo Skip: "%~1", Length: "%~2", Range: "%~3" --%~4-- [%~5 of %~6]
If "%~6"=="1" (
dd if="%File%" ibs=1 skip=%~1 count=%~2 2>nul
Exit /B
)
Echo --%~4
Echo Content-Type: %mimetype%
Echo Content-Range: bytes %~3
Echo.
dd if="%File%" ibs=1 skip=%~1 count=%~2 2>nul
:: Если это последний блок, пишем финальный разделитель
If "%~5"=="%~6" <nul Set /P "Echo=--%~4--"
Exit /B
:PartsSize (file)
For %%A In ("%~1") Do Echo Content-Length: %%~zA
Exit /B
:Err416
Echo HTTP/1.1 416 Range Not Satisfiable
Echo Server: OWS/%__OWS_version%
Echo Date: %datetime%
Echo Accept-Ranges: bytes
Echo Content-Range: bytes */%size%
Echo.
Exit
Если не найду другого решения, буду писать алгоритм выбора размера блока для dd, отдавать часть максимально большими блоками, а остаток точно доводить побайтово.
UPD: Прошу прощения, я не совсем верно вас понял при первом прочтении сообщения. Вы предложили использовать один блок сразу нужного размера, и это до меня только сейчас дошло. Да, это может сработать, сейчас протестирую.
greg zakharov, к сожалению, те же проблемы с производительностью, файлы обычно бывают от десятка мегабайт до гигабайта, а запрашиваемая часть файла варьируется в зависимости от браузера либо другого софта, делающего запрос, но редко превышает треть или четверть общего размера.