уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

Как быстро пролетела первая неделя отпуска :). Последние три дня так вообще махом потому как довольно плотно “завис” над 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. Смысл механизма в следующем:

  1. Мы инициализируем сессию загрузки, отправляя на специальный URL сервиса пустой POST-запрос или POST-запрос, содержащий только мета-данный по загружаемому файлу.
  2. В ответ сервер отвечает нам 200 кодом (“Ok”), а в заголовках будет содержаться редирект (заголовок “Location”) – это будет URL на который мы будем отправлять данные.
  3. Далее мы отправляем на полученный URL только PUT-запросы с некоторым объемом данных файла. Например, файл в 100 Мб можно разбить на 100 частей и слать в одном PUT’е по 1 Мб данных.
  4. Сервер может принять сразу все отправленные данные, а может только часть, например из 1 Мб загрузить всего 256 Кб, а остальное “отбросить” на следующий запрос. Поэтому мы обязаны контролировать в ответах сервера содержимое заголовка ”Range”.
  5. При загрузке больших файлов сервер может менять URL назначение, присылая новый заголовок Location. Поэтому мы также должны в каждом ответе просматривать наличие заголовка Location и в случае его обнаружения менять URL для следующего PUT-запроса.
  6. В процессе загрузки могут возникать перебои как на стороне сервера (в этом случае возвращается ошибка с 503 кодом) так и а стороне клиента (ошибка с 404 и 410 кодом). В этом случае необходимо отправлять специальный PUT-запрос в ответ на который сервер вернет нам в заголовке Range точный размер загруженных данных – это значение будет использоваться для возобновления загрузки.
  7. В случае успешной загрузки сервер возвращает 201 код, в случае не полной загрузке данных, но рабочей сессии – 308 код.

Вот такой “хитрый” и удобный механизм. Позволяет даже на самом “дохлом” соединении закачивать на сервер файлы любых объемов. Для работы с возобновляемыми загрузками в Google Docs нам потребуются следующие данные:

  1. Код авторизации в сервисе. Так как OAuth 2.0 для этого API я проверять не стал, то воспользовался простым протоколом ClientLogin.
  2. Версия API. На данный момент механизм возобновляемых загрузок поддерживается третьей версией API.
  3. MIME-тип загружаемого файла.
  4. Ну и, конечно, первоначальный 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;

Состояние сессии загрузки может быть:

  1. В работе – данные продолжают передаваться на сервер (teWork)
  2. Прервана пользователем (teAbortByUser)
  3. Прервана сервером — истекло время сессии, переданы ошибочные данные (teAbortByServer)
  4. Выполнена (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, загружается непосредственно контент документа, исключая отправку мета-данных. Думаю, что, разобравшись с работой этого класса вы сможете самостоятельно реализовать все дополнительные возможности загрузки файлов в том числе не только загрузку, но и редактирование данных документа. Ну, а если не сможете, то следите за обновлениями в блоге.

Скачать исходник: Исходники —> API онлайн-сервисов —> Google API

Книжная полка

Описание: Рассмотрены практические вопросы по разработке клиент-серверных приложений в среде Delphi 7 и Delphi 2005 с использованием СУБД MS SQL Server 2000, InterBase и Firebird. Приведена информация о теории построения реляционных баз данных и языке SQL. Освещены вопросы эксплуатации и администрирования СУБД.
купить книгу delphi на ЛитРес
Описание: Рассмотрены малоосвещенные вопросы программирования в Delphi. Описаны методы интеграции VCL и API. Показаны внутренние механизмы VCL и приведены примеры вмешательства в эти механизмы. Рассмотрено использование сокетов в Delphi: различные режимы их работы, особенности для протоколов TCP и UDP и др.
купить книгу delphi на ЛитРес
5 1 голос
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
4 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
VolRus
VolRus
08/07/2011 20:39

Спасибо, интересная статья!
Было бы еще интересно о SkyDrive на синапсе посмотреть.
А то что то получается работать с SkyDrive только через Indy и Clever Internet Suite.

VolRus
VolRus
08/07/2011 23:27

В обработке кукисов проблема. В Indy и CIS есть CookieManager а в Synapse вроде бы нету такого менеджера. И при авторизации он не те кукисы отправляет серверу. Спецификацию по кукисах я читал но так пока не научился вручную управлять ими. Да и снифить Synapse под HTTPS не получается. Программа HTTP Debugger Pro например не видит его соединения под HTTPS. Может подскажете хороший снифер под HTTP/HTTPS.