Имя пользователя:
Пароль:  
Помощь | Регистрация | Забыли пароль?  | Правила  

Компьютерный форум OSzone.net » Программирование, базы данных и автоматизация действий » Скриптовые языки администрирования Windows » CMD/BAT - [решено] Вывод части бинарного файла

Ответить
Настройки темы
CMD/BAT - [решено] Вывод части бинарного файла

Старожил


Сообщения: 415
Благодарности: 257

Профиль | Отправить PM | Цитировать


Приветствую уважаемых форумчан.
В ходе реализации RFC 7233 (HTTP Range Requests) для батника, запущенного в качестве CGI-скрипта, столкнулся с новой для себя задачей — вывести определённый диапазон байтов бинарного файла в stdout. Первым кандидатом на роль подходящего инструмента стал dd, и путём чтения мануалов и поиска на stackoverflow пришёл к вот этой строчке:
Код: Выделить весь код
dd if="%TargetFile%" ibs=1 skip=%~1 count=%~2
где соответственно, %1 и %2 - смещение от начала файла и размер фрагмента в байтах. Проблема в том, что при этом dd работает с размером блока, равным одному байту (а мне именно такая точность и требуется), что совершенно неприемлемо по скорости работы и нагрузке на процессор.
Прошу подсказать более подходящий инструмент для этой задачи, или объяснить что я делаю не так, в случае если неправильно использую dd.

Отправлено: 10:10, 27-11-2015

 

Ветеран


Сообщения: 1758
Благодарности: 966

Профиль | Цитировать


Обязательно батник? В powershell решается без dd:
Код: Выделить весь код
$skip = 100
$count = 150
$file = 'C:\path\to\file'

(get-content $file -encoding byte)[$skip..($skip+$count)]   # Медленный способ
[io.file]::readallbytes($file)[$skip..($skip+$count)]               # Оптимальный способ
Это сообщение посчитали полезным следующие участники:

Отправлено: 10:27, 27-11-2015 | #2



Для отключения данного рекламного блока вам необходимо зарегистрироваться или войти с учетной записью социальной сети.

Если же вы забыли свой пароль на форуме, то воспользуйтесь данной ссылкой для восстановления пароля.


Старожил


Сообщения: 415
Благодарности: 257

Профиль | Отправить PM | Цитировать


Если возможно, хотелось бы всё же избежать использования powershell, не в последнюю очередь оттого, что я с ним почти не знаком и не смогу полноценно интегрировать предложенное решение в свой скрипт. Ваш пример я так и не смог запустить с бинарным файлом (не выводит ничего), а с текстовым показывает ряд чисел вместо фрагмента содержимого. Интересует решение именно для батника, со сторонними утилитами.

Отправлено: 10:56, 27-11-2015 | #3


Ветеран


Сообщения: 1758
Благодарности: 966

Профиль | Цитировать


Не совсем понятно зачем выводить по одному байту.
Код: Выделить весь код
dd if=inputfile.bin skip=1000 count=1 bs=10240

:: 10240 bytes (10 kB) copied, 1,436 seconds
Это сообщение посчитали полезным следующие участники:

Отправлено: 14:20, 27-11-2015 | #4


Забанен


Сообщения: 793
Благодарности: 260

Профиль | Цитировать


Не знаю сколь много считывать, но как вариант:
Код: Выделить весь код
@echo off
  setlocal enabledelayedexpansion
    for /f "tokens=2" %%i in (
       'fc /b "%binfile%" "%~f0" ^| findstr /brc:"[0-9].*:"'
    ) do set "hex=!hex!%%i"
    echo !hex:~%skip%,%read%!
  endlocal
exit /b

:: ниже, под этими строками, поместить достаточно
:: "мусора", чтобы fc считал как можно больше байт
Foreigner, в случае с PS - не лучше ли через FileStream?
Это сообщение посчитали полезным следующие участники:

Отправлено: 15:38, 27-11-2015 | #5


Старожил


Сообщения: 415
Благодарности: 257

Профиль | Отправить PM | Цитировать


Цитата 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, к сожалению, те же проблемы с производительностью, файлы обычно бывают от десятка мегабайт до гигабайта, а запрашиваемая часть файла варьируется в зависимости от браузера либо другого софта, делающего запрос, но редко превышает треть или четверть общего размера.

Последний раз редактировалось Anonymоus, 27-11-2015 в 20:31.


Отправлено: 20:13, 27-11-2015 | #6


Ветеран


Сообщения: 1758
Благодарности: 966

Профиль | Цитировать


Цитата greg zakharov:
в случае с PS - не лучше ли через FileStream? »
Лучше, особенно если файл большого размера.

Цитата Anonymоus:
использовать один блок сразу нужного размера »
Да, если известен офсет и размер блока. Только вот гнутый dd через раз вылетает.

Отправлено: 20:33, 27-11-2015 | #7


Старожил


Сообщения: 415
Благодарности: 257

Профиль | Отправить PM | Цитировать


Протестировал с одним блоком нужного размера, работает действительно быстрее, но точно задать офсет не выйдет — ibs влияет и на это тоже, опять всплывает проблема с кратностью размера блока. А я уже успел обрадоваться, что решение найдено. Про вылеты вы верно заметили, бывало пару раз, я списал это как раз на работу с блоком в один байт — всё же совершенно нетипичные условия работы для этой утилиты.

Отправлено: 20:55, 27-11-2015 | #8


Старожил


Сообщения: 415
Благодарности: 257

Профиль | Отправить PM | Цитировать


Решил проблему с помощью двух вызовов dd с передачей результата через пайп, первый обеспечивает смещение, второй выдаёт данные нужной длины. Производительность приемлемая даже на больших файлах. Заодно заменил саму утилиту на эту, она не упала ни разу за всё время тестирования.
Код: Выделить весь код
dd if="%File%" bs=%Offset% skip=1 2>nul|dd bs=%Length% count=1 of="out.tmp" 2>nul

Отправлено: 05:34, 28-11-2015 | #9



Компьютерный форум OSzone.net » Программирование, базы данных и автоматизация действий » Скриптовые языки администрирования Windows » CMD/BAT - [решено] Вывод части бинарного файла

Участник сейчас на форуме Участник сейчас на форуме Участник вне форума Участник вне форума Автор темы Автор темы Шапка темы Сообщение прикреплено

Похожие темы
Название темы Автор Информация о форуме Ответов Последнее сообщение
CMD/BAT - [решено] Правка бинарного файла sov44 Скриптовые языки администрирования Windows 0 25-06-2015 13:32
VBS/WSH/JS - [решено] Поиск строки по части названия и вывод данных в ECHO Kainos Скриптовые языки администрирования Windows 14 16-04-2015 22:21
VBS/WSH/JS - поиск слова в тексте и вывод части текста после искомого слова sergey23031978 Скриптовые языки администрирования Windows 9 18-02-2015 16:54
Скрипт для разбиения файла на части Diamond AutoIt 17 17-05-2013 01:42
C/C++ - Чтение из бинарного файла формата GRD Violetta_ Программирование и базы данных 3 20-05-2012 19:45




 
Переход