Как быстро пролетела первая неделя отпуска :). Последние три дня так вообще махом потому как довольно плотно “завис” над API Google Docs и обновлениями в блоге. Ну, с обновлениями познакомлю всех немного позже, как только все эти обновы заработают в полную силу, а пока по теме поста.
Введение
Итак, сам по себе API Google Docs (он же API списка документов Google) по сложности не отличается от многих других API Google – те же данные в формате XML, те же GET, POST, PUT-запросы, однако серьезные различия есть в объемах перекачиваемых данных. Одно дело, когда мы качаем из Сети XML-файл размером в 200-300 Кб, а другое – когда вместе с этими данными нам надо получить файл или наоборот – забросить в аккаунт файл объемом, скажем, в несколько мегабайт. А если канал слабый? А ну как “заглючит” чего-нибудь в момент аплоада? Подводных камней достаточно и надо их как-то обходить, искать решения. А с последним обновлениям сервиса так вообще все пользователи получили возможность закачивать в аккаунт файлы любых форматов. Благо разработчики API предусмотрели такую замечательную возможность как возобновляемые загрузки (resumable upload). Вот над этой возможностью я и работал последние три дня. Причем пришлось реализовывать работу сразу с двумя библиотеками – Indy и Synapse.Сначала кратко в чем суть возобновляемых загрузок.
Описание Resumable Uploads от Google
Судя по информации Google, впервые подобный механизм был разработан теми же программистами, которые работали над Google Gears. Смысл механизма в следующем:
- Мы инициализируем сессию загрузки, отправляя на специальный URL сервиса пустой POST-запрос или POST-запрос, содержащий только мета-данный по загружаемому файлу.
- В ответ сервер отвечает нам 200 кодом (“Ok”), а в заголовках будет содержаться редирект (заголовок “Location”) – это будет URL на который мы будем отправлять данные.
- Далее мы отправляем на полученный URL только PUT-запросы с некоторым объемом данных файла. Например, файл в 100 Мб можно разбить на 100 частей и слать в одном PUT’е по 1 Мб данных.
- Сервер может принять сразу все отправленные данные, а может только часть, например из 1 Мб загрузить всего 256 Кб, а остальное “отбросить” на следующий запрос. Поэтому мы обязаны контролировать в ответах сервера содержимое заголовка ”Range”.
- При загрузке больших файлов сервер может менять URL назначение, присылая новый заголовок Location. Поэтому мы также должны в каждом ответе просматривать наличие заголовка Location и в случае его обнаружения менять URL для следующего PUT-запроса.
- В процессе загрузки могут возникать перебои как на стороне сервера (в этом случае возвращается ошибка с 503 кодом) так и а стороне клиента (ошибка с 404 и 410 кодом). В этом случае необходимо отправлять специальный PUT-запрос в ответ на который сервер вернет нам в заголовке Range точный размер загруженных данных – это значение будет использоваться для возобновления загрузки.
- В случае успешной загрузки сервер возвращает 201 код, в случае не полной загрузке данных, но рабочей сессии – 308 код.
Вот такой “хитрый” и удобный механизм. Позволяет даже на самом “дохлом” соединении закачивать на сервер файлы любых объемов. Для работы с возобновляемыми загрузками в Google Docs нам потребуются следующие данные:
- Код авторизации в сервисе. Так как OAuth 2.0 для этого API я проверять не стал, то воспользовался простым протоколом ClientLogin.
- Версия API. На данный момент механизм возобновляемых загрузок поддерживается третьей версией API.
- MIME-тип загружаемого файла.
- Ну и, конечно, первоначальный URL для инициализации сессии.
Теперь, что касается библиотек.
Библиотеки для работы с HTTP
Что касается реализации механизма resumable upload с использованием Synapse, то здесь никаких проблем не возникло в принципе так как с этой библиотекой я работаю уже достаточно продолжительное время и уж что-что, а работу с модулем HTTPSend.pas изучил вдоль и поперек. Единственная небольшая проблема возникла при определении MIME-типа по расширению файла. Наиболее подходящая функция нашлась в модуле MIMEPart.pas, но и её пришлось доработать для нормальной работы. В остальном все было достаточно просто.
Совсем другое дело было, когда дело коснулось реализации работы с Indy.
Я не буду огульно ругать разработчиков библиотеки за “глючность”, т.к. ошибок как таковых-то и не было – после того как потратил почти 5 часов на то, чтобы просто понять КАК все устроено в Indy модуль заработал как часы. Но вот, что в очередной раз меня НЕ порадовала так это:
1. Непомерная громоздкость библиотеки. После работы с Synapse где все функции на виду, а модули имеют понятные внятные названия, догадаться, что функция для определения MIME-типа по расширению файла лежит в модуле idGlobalProtocols.pas и перенесена туда (судя по информации, полученной из форумов) из модуля idGlobals.pas – было как-то не весело. Но это не критично – нашел и слава богу.
2. Что за дурацкая обработка исключений в Indy? Понятно, что в случае действительно исключительных ситуаций надо выдавать сообщение, но на кой это делать каждый раз, когда работа идет нормально? Опять же пример: по п.2 (см. выше) работы с механизмом resumable upload мне требуется получить заголовок Location из ответа сервера и при этом не дать “прыгнуть” idHTTP на этот адрес иначе вся работа насмарку – создается пустой файл в аккаунте с которым уже ничего не сделать. Естественно, сделать это можно выставив у idHTTP свойство HandleRedirects=false. Я САМ указываю, что буду контролировать перенаправления. На кой чёрт при ответе сервера “200 Ok” компонент плюется Exception’ами, прерывая выполнение метода? Что за дурость? Ладно 404 код, черт с ним и на 410 и на 5хх путь будет, но 200!!! Чтобы отключить такую обработку надо либо заморачиваться над событиями либо заворачивать каждый запрос в try…except…end и анализировать код сервера.
В общем работа с Indy в очередной раз не принесла никакого морального удовлетворения. Да большая, да есть много разных фич, да входит в состав Delphi “из коробки”, но удобство ИМХО оставляет желать лучшего.
Что касается реализации то рассмотрим пример реализации класса Delphi с использованием Synapse, а если у вас возникнут вопросы по реализации на Indy – задавайте их в комментариях и я постараюсь на них ответить.
Реализация механизма resumable upload в Delphi
Итак. Для работы нам понадобится ключ для авторизации в сервисе, который можно получить, используя, например, наш компонент GoogleLogin.
В качестве URL для инициализации сессии загрузки будем использовать URL для загрузки в корень списка документов – он постоянный и имеет вид:
https://docs.google.com/feeds/upload/create-session/default/private/full
Mime-тип файла будем указывать вручную, но если Вы хотите, то можете легко перенести код функции GetMimeTypeFromFile() из Indy в свой модуль - поверьте, там реализация элементарная и затруднений не вызовет даже у новичка.
Создадим новое приложение в Delphi и добавим новый модуль, который назовем GResumableUpload – в нем и будем реализовывать необходимый класс.
В Uses подключим следующие модули: httpsend, synautil, ssl_openssl.
Объявим в модуле следующие константы:
const cResumableURL ='https://docs.google.com/feeds/upload/create-session/default/private/full'; cChunkSize = 262144; //размер блока, пересылаемых за один запрос данных (256 Кб) cRangeHeader = ' bytes %d-%d/%d'; //шаблон для заголовка Content-Range cQueryRange = 'bytes */100';//шаблон для заголовка Content-Range в случае запроса состояния загрузки cGDataVersion = '3.0';//версия протокола API
Что касается константы cChunkSize, то вы в праве использовать любое значение. Однако Google рекомендует выбирать размер блока кратного 256 Кб.
Теперь рассмотрим все необходимые типы данных для работы.
Типы данных
type TUploadParams = record FileName : string;//полное наименование файла, включая путь к нему FileURL : string;//если закачка прерывалась, то поле должно содержать URL для закачки XMLContent: string;//содержит XML-данные, если загрузка прошла успешно end;
Эта запись для передачи параметров загрузки в класс. Анализируя поле FileURL мы будем выстраивать всю дальнейшую работу с загрузкой — либо начинать новую сессию, либо возобновлять загрузку. Можно было бы обойтись и без записи, но в моих планах немного расширить возможности класса для загрузки и сделать также реализации обновления контента, мета-данных документа и т.д. И через запись необходимые параметры можно будет передавать более удобно.
TStatus = (teUnknown, teWork, teAbortByUser, teAbortByServer, teDone); TOnProgress = procedure(TotalBytes, SendBytes: int64) of object; TOnUploadStatus = procedure(Status: TStatus; ResponseParams:TUploadParams) of object;
Состояние сессии загрузки может быть:
- В работе – данные продолжают передаваться на сервер (teWork)
- Прервана пользователем (teAbortByUser)
- Прервана сервером — истекло время сессии, переданы ошибочные данные (teAbortByServer)
- Выполнена (teDone)
Также определены два типа событий – первый передает данные о прогрессе загрузки, а второй используется для анализа текущего состояния загрузки.
Сам класс для реализации загрузок получился следующим:
type TResumableClass = class private FHTTP: THTTPSend; FAuthKey: string; FFileStream: TFileStream; FInterrupted: boolean; FTerminate: boolean; FTransmitted: integer; FMimeType: string; FParams: TUploadParams; FOnProgress: TOnProgress; FOnThreadStatus: TOnUploadStatus; function OpenSession: string; procedure PrepareHeaders(const OpenSession: boolean); procedure SetNewChunk; function GetSentCount(const RangeHeader: string):int64; function PrepareData: boolean; function QueryStatus: boolean; procedure SendFile; procedure DoProgress; procedure DoStatus; public constructor Create; destructor Destroy; override; procedure UploadFile(FileName, MimeType: string);overload; procedure UploadFile(UploadParams: TUploadParams);overload; property AuthKey: string read FAuthKey write FAuthKey; procedure Abort; property OnProgress: TOnProgress read FOnProgress write FOnProgress; property OnThreadStatus: TOnUploadStatus read FOnThreadStatus write FOnThreadStatus; end;
Рассмотрим основные методы класса, реализующие механизм resumable upload.
Функция OpenSession
Открывает новую сессию для загрузки файла:
function TResumableClass.OpenSession: string; begin Result := ''; try PrepareHeaders(True); FHTTP.Headers.Add('Content-Length: 0'); if FHTTP.HTTPMethod('POST',cResumableURL) then begin if FHTTP.ResultCode=200 then begin HeadersToList(FHTTP.Headers); Result:=FHTTP.Headers.Values['Location']; end else raise Exception.CreateFmt('Ошибка открытия сессии: %d - %s',[FHTTP.ResultCode, FHTTP.ResultString]); end else raise Exception.Create('Ошибка отправки запроса'); finally FHTTP.Clear; end; end;
В этой функции отправляется пустой POST-запрос на URL открытия сессии. Здесь стоит обратить внимание на то, что по в методе HTTPMethod заголовок Content-Length отправляется только в том случае, если тело запроса содержит хотя бы один байт данных. Поэтому в функции этот заголовок добавляется вручную. Если не добавить, то получим ответ сервера 411 — требуется указать размер.
Функция QueryStatus
Запрашивает данные о прерванной сессии и в случае, если данные успешно получены, возвращает True:
function TResumableClass.QueryStatus: boolean; begin PrepareHeaders(false); FHTTP.Headers.Add('Content-Range: '+cQueryRange); Result:=FHTTP.HTTPMethod('PUT',FParams.FileURL) and (FHTTP.ResultCode=308); end;
Здесь PrepareHeaders — это вспомогательная функция, вставляющая необходимые заголовки в запрос в зависимости от его (запроса) зазначения:
procedure TResumableClass.PrepareHeaders(const OpenSession: boolean); begin FHTTP.Headers.Add('Authorization: GoogleLogin auth=' + FAuthKey); FHTTP.Headers.Add('GData-Version: ' + cGDataVersion); FHTTP.MimeType := FMimeType; if OpenSession then begin FHTTP.Headers.Add('Slug: ' +EncodeURL(UTF8Encode(ExtractFileName(FParams.FileName)))); FHTTP.Headers.Add('X-Upload-Content-Type: ' + FMimeType); FHTTP.Headers.Add('X-Upload-Content-Length: ' + IntToStr(FFileStream.Size)); end end;
Процедура SendFile
Отправляет файл на сервер:
procedure TResumableClass.SendFile; begin if not FileExists(FParams.FileName) then raise Exception.Create('Файл не найден!'); if Assigned(FFileStream) then FFileStream.Free; FFileStream:=TFileStream.Create(FParams.FileName, fmShareDenyNone); if not PrepareData then raise Exception.Create('Переданы не все данные для загрузки файла'); if length(FParams.FileURL)=0 then begin FParams.FileURL := OpenSession; FTransmitted := 0; end else begin if not QueryStatus then raise Exception.Create('Невозможно восстановить сессию!'); end; if length(FParams.FileURL) > 0 then begin FHTTP.Clear; FHTTP.MimeType := FMimeType; FFileStream.Position := soFromBeginning; try repeat FHTTP.Clear; SetNewChunk; PrepareHeaders(false); FHTTP.Headers.Add('Content-Range: '+Format(cRangeHeader, [FTransmitted, (FHTTP.Document.Size + FTransmitted - 1), FFileStream.Size])); FHTTP.HTTPMethod('PUT',FParams.FileURL); DoProgress; DoStatus; until (FTransmitted = FFileStream.Size) or (FHTTP.ResultCode <> 308)or FTerminate; except raise Exception.Create('Внутренняя ошибка загрузки файла'); end; end else raise Exception.Create('Внутренняя ошибка загрузки файла'); end;
И, наконец, чтобы отправить файл, используя класс можно воспользоваться следующим кодом:
var Resumable: TResumableClass; begin Resumable:=TResumableClass.Create; Resumable.AuthKey:='Ключ доступа к данным'; Resumable.UploadFile('Файл для отправки', 'MIME-тип'); end;
Вот, что можно очень кратко рассказать про реализацию Google Resumable Upload в Delphi. В заключение скажу, что представленный выше класс реализует только основы механизма, т.е. файл грузится в корень аккаунта Google Docs, загружается непосредственно контент документа, исключая отправку мета-данных. Думаю, что, разобравшись с работой этого класса вы сможете самостоятельно реализовать все дополнительные возможности загрузки файлов в том числе не только загрузку, но и редактирование данных документа. Ну, а если не сможете, то следите за обновлениями в блоге.
Книжная полка
Описание: Рассмотрены практические вопросы по разработке клиент-серверных приложений в среде Delphi 7 и Delphi 2005 с использованием СУБД MS SQL Server 2000, InterBase и Firebird. Приведена информация о теории построения реляционных баз данных и языке SQL. Освещены вопросы эксплуатации и администрирования СУБД.
|
||
Название: О чем не пишут в книгах по Delphi
Описание: Рассмотрены малоосвещенные вопросы программирования в Delphi. Описаны методы интеграции VCL и API. Показаны внутренние механизмы VCL и приведены примеры вмешательства в эти механизмы. Рассмотрено использование сокетов в Delphi: различные режимы их работы, особенности для протоколов TCP и UDP и др.
|
Спасибо, интересная статья!
Было бы еще интересно о SkyDrive на синапсе посмотреть.
А то что то получается работать с SkyDrive только через Indy и Clever Internet Suite.
VolRus, не за что :) По поводу SkyDrive ничего сказать не могу т.к. никогда с этим сервисом не работал, но, думаю, что работу с ним на Synapse тоже можно выстроить. Может получиться немного сложнее, чем с Clever Internet Suite, но работать будет. В чем конкретно заключается проблема использования Synapse?
В обработке кукисов проблема. В Indy и CIS есть CookieManager а в Synapse вроде бы нету такого менеджера. И при авторизации он не те кукисы отправляет серверу. Спецификацию по кукисах я читал но так пока не научился вручную управлять ими. Да и снифить Synapse под HTTPS не получается. Программа HTTP Debugger Pro например не видит его соединения под HTTPS. Может подскажете хороший снифер под HTTP/HTTPS.
VolRus, в Synapse за куки отвечает список Cookies:TStringList. Причм как на отправку, так и на получение. То есть примерно так: 1. Отправляете запрос на сервер. Сервер выдает куки, которые сохраняются в Cookies. 2. Если не почистить список и отослать второй запрос, то куки, полученные в п.1. автоматом сбрасываются на сервер. Поэтому при работе с Synapse следует учитывать, что библиотека эта проста и такой навороченной «автоматики» как в Indy там нету (что кстати по мне так удобно и практично). Если надо самостоятельно управлять куками — после очередного запроса анализируйте список Cookies, копируйте в другое место, чистите и т.д. Так же само,… Подробнее »