Сообщений 4 Оценка 62 Оценить |
Предисловие Получение данных от клиента Разделение данных на поля Заключение Cсылки |
Некоторое время назад передо мной возникла задача передачи файлов на сервер по HTTP-протоколу и их загрузки в базу данных. Чаще всего это решается использованием специальных ActiveX-компонентов, но это не всегда безопасно и не всегда это решение является рациональным. Поэтому я поставил перед собой цель реализовать это на ASP без применения дополнительных компонентов. До сих пор мне не приходилось работать с HTML-формами, содержащими файлы или двоичные данные, поэтому я начал с того, что открыл Internet Explorer и попытался найти нужную информацию в мировой паутине. Однако передо мной возникла ещё одна проблема. Дело в том, что большая часть кода системы уже была написана на JavaScript, а все статьи, которые я находил, содержали примеры на VBScript. Так как мне очень не хотелось смешивать два языка в одной системе, я решил транслировать найденные примеры в JavaScript. Естественно, я столкнулся при этом с некоторыми трудностями, возникшими при написании кода для разбора данных, пришедших от пользователя. В этой статье я хочу поделиться своими решениями с читателями журнала.
Рассмотрим, как сервер получает данные от клиента. Сообщение типа multipart/form-data состоит из нескольких частей. Части отделяются друг от друга разделителями полей (boundary). Разделитель имеет переменное значение и может меняться в зависимости от нескольких факторов. Кроме того, разделитель может быть окружен произвольным числом символов “-“. Каждая часть сообщения содержит заголовок Content-Disposition, имеющий значение form-data, атрибут наименования, определяющий имя соответствующего управляющего элемента HTML-формы, и, собственно данные, составляющие управляющий элемент. Для разделения строк данных используется комбинация CR+LF (т.е. последовательность кодов символов 13+10). Если клиент передает некоторый файл, то имя файла указывается в параметре filename заголовка Content-Disposition: form-data.
Получить всю эту информацию можно при помощи встроенного объекта Request. Однако не пытайтесь получить значения полей формы, используя привычную конструкцию Request.Form("имя_поля")(), где имя_поля – имя элемента HTML-формы. Если форма имеет тип содержимого multipart/form-data, то это выражение вернет значение undefined. Происходит это потому, что данные от клиента в этом случае поступают в бинарном виде, и должны быть прочитаны с помощью метода BinaryRead объекта Request. В качестве параметра для этого метода нужно указать количество считываемых байтов. Общее количество данных можно узнать с помощью свойства TotalBytes объекта Request.
Рассмотрим простой пример. Создадим HTML-форму, не забыв при этом указать для атрибута enctype значение multipart/form-data:
<form name="test" method="post" enctype="multipart/form-data" action="test.asp"> Поле №1: <input type="text" name="Field1"><br> Поле №2: <input type="text" name="Field2"><br> Файл №1: <input type="file" name="FileData1"><p> <input type="submit" value="Тест"> </form> |
После этого создадим файл test.asp. Напишем в нем код для определения разделителя полей. Для этого можно воспользоваться переменной среды IIS CONTENT_TYPE, в которой и содержится тип данных, включённых в запрос, а также искомый разделитель. Для наглядности выведем значения переменных и разделителя в браузер:
<% var Header = new String(Request.ServerVariables("CONTENT_TYPE")); var Boundary = Header.slice(Header.length - Header.indexOf("boundary=") - 8); Boundary = Boundary.replace(/-/g,""); Response.Write("Заголовок: " + Header); Response.Write("<hr>Разделитель: " + Boundary); %> |
Теперь необходимо получить тело запроса и выделить из него отдельные поля, соответствующие элементам HTML-формы. Необходимо учесть, что разделитель был получен нами в текстовом формате, а тело запроса в бинарном, поэтому если мы попытаемся искать наш «текстовый» разделитель в «бинарном» теле запроса, то, скорее всего, ничего не найдем. Как же быть? Так как JavaScript не умеет работать с бинарными данными, то выход один – преобразовать тело запроса в текстовый формат и искать разделитель в преобразованном запросе. Сделать такое преобразование можно с помощью объекта ADO Recordset:
<% // Преобразование бинарных данных в текстfunction BinToText(DataBin) { var objRS = Server.CreateObject("ADODB.Recordset"); objRS.Fields.Append("txt",201,Request.TotalBytes); objRS.Open(); objRS.AddNew; objRS.Fields("txt").AppendChunk(DataBin); objRS.Update; var DataTextRes = objRS("txt").Value; objRS.Close(); objRS = null; return DataTextRes; } %> |
Функция BinToText создает объект Recordset и определяет в нем поле данных типа LongVarChar, которому присваивает имя txt. В это поле затем записываются двоичные данные, переданные функции, которые сразу же читаются из него уже в текстовом виде. После добавления функции BinToText в файл test.asp можно будет вывести результат преобразования в браузер и посмотреть что получилось. Для этого к тексту, приведённому в Листинге 1, необходимо дописать следующий код:
<% var DataText = BinToText(Request.BinaryRead(Request.TotalBytes)); Response.Write("<hr>Тело запроса в текстовом виде: " + DataText); %> |
Для примера введем в Поле №1 «Интернет», а в Поле №2 «Коммуникации». Вот что мы увидим в окне браузера:
Теперь разделим полученное в текстовом виде тело запроса на отдельные поля. Для начала определимся, какая информация о полях нас интересует. Вероятно, это имя поля, данные, тип содержимого и размер данных. Для хранения этой информации определим объект FormField:
<% // Объект "Поле формы"function FormField() { this.Name = null; this.Value = null; this.ContentType = null; this.Size = 0; } %> |
Очевидно, что тип содержимого и размер важны только для поля, содержащего бинарные данные, в нашем примере это файл. Для разбиения по полям я написал отдельную функцию. Эта функция возвращает массив объектов FormField, содержащий информацию обо всех полях, полученных от клиента:
<% // Получение информации по всем полям запросаfunction GetFormFields() { // Получаем заголовокvar Header = new String(Request.ServerVariables("CONTENT_TYPE")); // Получаем разделительvar Boundary = Header.slice(Header.length - Header.indexOf("boundary=") - 8); Boundary = Boundary.replace(/-/g,""); // Получаем тело запроса в текстовом видеvar DataText = BinToText(Request.BinaryRead(Request.TotalBytes)); // Убираем первый разделитель DataText = DataText.slice(DataText.indexOf(Boundary) + Boundary.length); // Убираем последний разделитель DataText = DataText.slice(0,DataText.lastIndexOf(Boundary)); // Разделяем на поля - получаем массив строк DataPartvar DataPart = DataText.split(Boundary); // Убираем ненужные символы в начале и в конце каждого поляfor (var i=0;i<DataPart.length;i++) DataPart[i] = DataTrim(DataPart[i]); var FieldStr = ""; var FF = new Array(); // Разбираем поля по частямfor (var i=0;i<DataPart.length;i++) { FF[i] = new FormField(); FieldStr = DataPart[i]; pn = FieldStr.indexOf("name=\"") + 6; // Имя поля FF[i].Name = FieldStr.slice(pn,FieldStr.indexOf("\"",pn)); // Если это файл, то поле содержит бинарные данные pfn = FieldStr.indexOf("filename=\""); if (pfn >= 0) { pfn = pfn + 10; // Тип содержимого pct = FieldStr.indexOf("Content-Type: ",pfn) + 14; pct_end = FieldStr.indexOf("\n",pct); if (pct_end < 0) pct_end = FieldStr.length; FF[i].ContentType = FieldStr.slice(pct,pct_end); // Данные бинарные DataFile = FieldStr.slice(pct_end + 3); FF[i].Value = TextToBin(DataFile); // Размер данных FF[i].Size = DataFile.length; } else { // Данные текстовые FF[i].Value = FieldStr.slice(FieldStr.indexOf("\"",pn) + 5); } } return FF; } %> |
При этом мы использовали вспомогательную функцию DataTrim, убирающую ненужные символы в начале и в конце строки. Текст её приведён ниже:
<% function DataTrim(Str) { while ((Str.charCodeAt(1) == 13) || (Str.charCodeAt(1) == 10) || (Str.charAt(1) == "-")) Str = Str.slice(1); while ((Str.charCodeAt(Str.length - 1) == 13) || (Str.charCodeAt(Str.length - 1) == 10) || (Str.charAt(Str.length - 1) == "-")) Str = Str.slice(0,Str.length - 1); return Str; } %> |
Процесс обработки поля с содержимым файла заслуживает особого разговора. В отличие от других полей, здесь дополнительно определяются тип содержимого и размер данных, включённых в поле. Кроме этого, данные такого поля конвертируются обратно в бинарный формат для дальнейшего использования. С написанием именно этого преобразования у меня и возникли самые большие сложности, потому что, как я уже говорил, JavaScript не умеет работать с бинарными данными. Решение было найдено в использовании объекта ADO Stream для работы с потоками:
<% // Преобразование текстовых данных в бинарныеfunction TextToBin(DataText) { var TextStream = Server.CreateObject("ADODB.Stream"); var BinStream = Server.CreateObject("ADODB.Stream"); TextStream.Type = 2; TextStream.Charset = "Windows-1251"; BinStream.Type = 1; TextStream.Open; TextStream.WriteText(DataText); BinStream.Open; TextStream.Position = 0; TextStream.CopyTo(BinStream,-1); BinStream.Position = 0; var DataBinRes = BinStream.Read; TextStream.Close; BinStream.Close; TextStream = null; BinStream = null; return DataBinRes; } %> |
Работает функция TextToBin очень просто. Создаются два потока, текстовый и бинарный. В текстовый поток записываются полученные функцией данные, после чего текстовый поток копируется в бинарный. Данные читаются из бинарного потока, после прочтения поток удаляется и функция возвращает результат чтения уже в бинарном виде.
Добавим все эти функции в test.asp и напишем код, который опробует их и выведет информацию о полях запроса в браузер.
<% // Объект "Поле формы"function FormField() { ... } // Преобразование бинарных данных в текстfunction BinToText(DataBin) { ... } // Преобразование текстовых данных в бинарныеfunction TextToBin(DataText) { ... } // Функция, убирающая ненужные символы в начале и в конце строкиfunction DataTrim(Str) { ... } // Получение информации по всем полям запросаfunction GetFormFields() { ... } // Получим поля запросаvar FormFields = GetFormFields(); // Выведем информацию о полях в браузерfor (var i=0;i<FormFields.length;i++) { Response.Write("<hr>Имя поля: " + FormFields[i].Name); Response.Write("<br>Значение: " + FormFields[i].Value); Response.Write("<br>Тип содержимого: " + FormFields[i].ContentType); Response.Write("<br>Размер: " + FormFields[i].Size); } %> |
Итак, мы получили все поля с данными, поступившими от клиента, при этом использовали «чистый» JavaScript без применения каких-либо нестандартных компонентов, и тем самым достигли поставленной цели. Для удобства использования можно написать еще одну функцию, которая, аналогично Request.Form("имя_поля")(), позволяет обращаться к полям запроса по имени:
<% // Находит поле формы по имениfunction GetFormFieldByName(FF,n) { var i = 0; var found = false; var Res = null; while ((i < FF.length) && (!found)) { if (FF[i].Name == n) { Res = FF[i]; found = true; } i++; } if (Res != null) return Res; returnnew FormField(); } %> |
Функция возвращает объект FormField для поля с именем n из массива объектов полей FF. Например, следующий код получает данные поля с именем Field1:
<% var FormFields = GetFormFields(); var Data_Field1 = GetFormFieldByName(FormFields,"Field1").Value; %> |
Разработанные функции применяются для загрузки файлов на сервер в реально работающем Web-приложении с последующей записью их в базу данных. Применяемая технология показала достаточно высокое быстродействие и надежность в работе.
Сообщений 4 Оценка 62 Оценить |