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

Постоянные читатели блога наверное в курсе, что периодически я публикую посты-ответы на вопросы читателей. Обычно я практикую сей способ работы в блоге в том случае, если вопрос читателя:

  1. меня заинтересовал
  2. может не уложиться в пару строк типа «RTFM Необходимо сделать вот так, так или вот так«
  3. может пригодиться кому-нибудь ещё, кроме вопрошающего.

Так, например, я давал ответы на вопросы работу с примечаниями в ячейках Excel, работу с (опять же multipart/form-dataimagevenue.com и ещё пару небольших постов про работу с Сетью. Вот сегодня как раз и будет такой пост-ответ на вопрос с форума WebDelphi. Все-таки Яндекс.Фотки — это довольно популярный сервис, да и работа предполагается с Synapse, а к этой библиотеке я, как вы знаете, питаю самые теплые чувства :) В качестве ответа я приготовил небольшое приложение, которое демонстрирует все шаги по работе с API Яндекс.Фоток в т.ч. и загружает изображение на сервис в формате multipart/form-data. Рассмотрим это приложение и посмотрим, что у меня в итоге получилось.

Этот API Яндекса устарел в связи с тем, что Яндекс.Фотки переезжает на Яндекс.Диск. Материал этой статьи можно использовать в качестве примера работы с библиотекой Synapse

Само приложение выглядит следующим образом:


Каждый шаг работы с программой предполагает каких-либо действий от нас: указать файл, открыть файл и скопировать что-то, внести данные в поля и т.д. Рассмотрим каждый шаг работы подробнее.

Шаг №1. Авторизация

Про авторизацию в сервисах Яндекса я говорил, когда рассказывал про работу с Яндекс.Метрика. Однако время, а вместе с ним и Яндекс не стоят на месте и первый поисковик Рунета сменил способ авторизации в своих сервисах, а именно убрал авторизацию по логину/паролю, который мне так приглянулся. В результате пришлось в срочном порядке перечитывать документацию по OAuth Яндекса и изобретать очередной велосипедик. Раз логин/пароль более использовать нельзя, то будем использовать переход на сайт Яндекса за разрешением.  Самый простой вариант в этом случае — использовать старый недобрый TWebBrowser.

Для авторизации я соорудил вот такую простенькую форму:

т.е. ничего кроме TWebBrowser на форме-то и нету. Работает авторизация следующим способом:
у главной формы в public-секции определена вот такая переменная:

type
  Tfmain = class(TForm)
    ...
  private
  public
    Token: string;
  end;

клик по кнопке «Авторизация» выполняет следующий код:

procedure Tfmain.Button1Click(Sender: TObject);
begin
  fAuth.ShowModal;
  if fAuth.ModalResult=mrOk then
    lbAuth.Caption:=Token
end;

У формы для авторизации в OnShow сразу же выполняется отправка пользователя за подтверждением разрешения для нашего приложения:

procedure TfAuth.FormShow(Sender: TObject);
begin
  WebBrowser1.Navigate2(Format('https://oauth.yandex.ru/authorize?response_type=token&client_id=%s&display=popup',['тут_client_id_приложения']));
end;

Как регистрировать приложения в Яндексе и получать их Client_ID я рассказывал как раз тут (эта часть статьи до сих пор актуальна). Как только пользователь подтверждает, что он дает разрешение нашему приложению копаться у него в фотках и загружать новые изображения, то Яндекс начинает переправлять пользователя на redirect_uri, который мы, кстати, можем задать при регистрации приложения абсолютно произвольно. Я например, указал в качестве redirect_uri http://ya.ru.

Так вот, наша задача заключается в том, чтобы поймать момент, когда Яндекс перебросит пользователя на redirect_uri, а сам URL будет содержать в параметрах заветный токен для доступа к API. Я решил эту задачку элементарно — обработав событие  OnNavigateComplete2 вот таким образом:

procedure TfAuth.WebBrowser1NavigateComplete2(ASender: TObject;
  const pDisp: IDispatch; const URL: OleVariant);
var Params: TStringList;
begin
  if pos('access_token',URL)>0 then //URL содержит токен доступа
    begin
      Params:=TStringList.Create;
      try
        Params.Delimiter:='&';
        Params.DelimitedText:=copy(URL,pos('#',URL)+1,Length(URL)-pos('#',URL));//копируем параметры
        fMain.Token:=Params.Values['access_token'];//получаем значение токена
        ModalResult:=mrOk; //уходим отсюда
      finally
        Params.Free;
      end;
    end;
end;

Может такой подход и не совсем корректный или выглядит не эстетично, но авторизация в сервисах — это вообще отдельный разговор и сегодня этот момент скорее необходимый шажок в решении задачи, нежели сам ответ на вопрос, поэтому работает пока и то хлеб — кому надо доработают по вкусу и цвету. В итоге, получив токен мы уже можем смело двигаться в сторону Яндекс.Фоток и работать с ними.

Шаг №2. Получение сервисного документа

Разработчики Яндекса решили задачку с получением точек доступа к ресурсам пользователя довольно грамотно. Вместо того, чтобы  просить нас конструировать какие-то свои URL доступа, разбираться с параметрами и т.д. разработчики API решили просто — качаете сервисный документ (обычный XML) и получаете в нем все необходимые сведения по URL’ам коллекций, названиям и т.д. и т.п. Кроме того, эти самые URL со временем могут изменяться, поэтому начинать работу с API Яндекс.Фоток с чтения сервисного документы — это уже даже не хороший тон, а необходимость.

В моей программке получение сервисного документа происходит следующим образом:

procedure Tfmain.Button2Click(Sender: TObject);
var Redirect: string;
begin
  with THTTPSend.Create do
    begin
      Headers.Add('Authorization: OAuth '+Token);
      if HTTPMethod('GET','http://api-fotki.yandex.ru/api/me/') then
        begin
          Headers.NameValueSeparator:=':';
          Redirect:=Headers.Values['Location'];
          if Length(Redirect)>0 then
            begin
              Clear;
              if HTTPMethod('GET',Redirect) then
                begin
                  Document.SaveToFile('ServeceDocument.xml');
                  label4.Caption:='Получен'
                end
              else
                raise Exception.Create('Не удалось получить сервисный документ');
            end
          else
            raise Exception.Create('Не удалось получить адрес сервисного документа');
        end
      else
        raise Exception.Create('Не удалось получить адрес сервисного документа');
    end;
end;

Посмотрим, что здесь происходит и, что мы получаем в итоге. Так как на предыдущем этапе мы успешно получили токен доступа, то, согласно документации API Яндекс.Фоток мы можем не запрашивать у пользователя его логин, а просто выполнить специальный запрос на адрес:

http://api-fotki.yandex.ru/api/me/

В результате в заголовках ответа сервера будет содержаться адрес по которому мы можем скачать сервисный документ. Поэтому как только запрос на первый URL прошел я и читаю заголовок Location:

  Headers.NameValueSeparator:=':';
  Redirect:=Headers.Values['Location'];

Далее очищаем объект THTTPSend от данных предыдущего запроса и GET’ом на полученный из Location URL просим сервисный документ, а получив документ — сохраняем его на диск:

 Clear;
 if HTTPMethod('GET',Redirect) then
   begin
     Document.SaveToFile('ServiceDocument.xml');
     label4.Caption:='Получен'
   end

Я не стал забивать программу методами парсинга XML, т.к. это опять же не наша сегодняшняя цель. Поэтому для следующего шага работы с программой надо найти файл ServiceDocument.xml рядом с exe-файлом программы, открыть его, внимательно изучить и скопировать из него ссылку на ссылку на альбом по умолчанию — у неё параметр id равен «photo-list«.

На этом этапе работы с программой мы выполнили три шага: авторизовались, скачали сервисный документ, скопировали необходимый для работы URL. Четвертый шаг — это указание изображения для загрузки на сервер и определение его параметров (заголовка, описания и тегов), но этот шаг мы рассматривать подробно не будем, т.к. в нем нет ничего того, что может не знать человек, собравшийся копаться в API :) Поэтому сразу переходим к последнему шагу — загрузке изображения в формате multipart/form-data на Яндекс.Фотки

Шаг №3(5) Загрузка изображения в формате multipart/form-data

В качестве отправной точки я предлагаю Вам ещё раз ознакомиться со статьей Synapse. Отправка данных на сервер на примере imagevenue.com. Так как сейчас мы будем делать примерно тоже самое — составлять правильно тело документа и скидывать его на сервер.

Итак, согласно документации API Яндекс.Фоток запрос на добавление нового изображения должен выглядеть так:

POST /api/users/alekna/photos/ HTTP/1.1
Host: api-fotki.yandex.ru
Content-Type: multipart/form-data;  boundary=frekgh738gGHUehfui33qqQ
Content-length: 565928
Authorization: OAuth eb1c5..
 
--frekgh738gGHUehfui33qqQ 
Content-Disposition: form-data;  name="image"; filename="picture"
Content-Type: image/jpeg
 
{содержимое файла "picture.jpg" в бинарном виде}
 
--frekgh738gGHUehfui33qqQ
Content-Disposition: form-data; name="title"
 
Горы
--frekgh738gGHUehfui33qqQ
Content-Disposition: form-data; name="description"
 
Сан-Катальдо, Сицилия
--frekgh738gGHUehfui33qqQ
Content-Disposition: form-data; name="tags"
 
природа
--frekgh738gGHUehfui33qqQ
Content-Disposition: form-data; name="tags"
 
небо
--frekgh738gGHUehfui33qqQ
Content-Disposition: form-data; name="album"
 
urn:yandex:fotki:alekna:album:19781

Не проблема — составляем аналогичный запрос, но уже с нашей фоткой. Т.к. я не активный пользователь этого сервиса, то просто загружу фотку в дефолтный альбом, т.е. не буду использовать в запросе идентификатор альбома — сути это никак не изменит. Итак, вот метод отправки изображения:

procedure Tfmain.Button3Click(Sender: TObject);
const CR = #$0d;
      LF = #$0a;
      CRLF = CR + LF;
var Bound,s: string;
    HTTP: THTTPSend;
    PicStream: TMemoryStream;
begin
{определяем разделитель}
 Bound := IntToHex(Random(MaxInt), 8) + '_PhotoManager_boundary';
 HTTP:=THTTPSend.Create;
 try
   {вставляем заголовок авторизации и определяем Mime-тип документа}
   HTTP.Headers.Add('Authorization: OAuth '+Token);
   HTTP.MimeType:='multipart/form-data;  boundary='+Bound;
   {начинаем составлять тело документа}
   S:='--' + Bound + CRLF; //тут два дефиса перед разделителем :)
   {обязательная часть - изображение}
   s:=s+'Content-Disposition: form-data;  name="image"; filename="picture"' + CRLF;
   {тут надо бы определять Mime-тип картинки, но для примера и так пойдёт}
   s:=s+'Content-Type: image/jpeg'+CRLF+CRLF;
   PicStream:=TMemoryStream.Create;
   try
     {записываем текстовую часть тела документа}
     WriteStrToStream(HTTP.Document,AnsiToUtf8(s));//не забываем про кодировку!
     {грузим картинку в поток}
     PicStream.LoadFromFile(lbPhoto1.Caption);
     {копируем картинку в тело документа} 
     PicStream.Position:=0;
     HTTP.Document.CopyFrom(PicStream,0);
     {пропускаем одну строку и начинаем расписывать свойства изображения}
     S:=CRLF+CRLF+'--' + Bound + CRLF;//не забываем про разделитель с двумя дефисами
     {задали название}
     S:=S+'Content-Disposition: form-data; name="title"'+CRLF+CRLF;
     S:=s+edTitle.Text+CRLF+'--'+Bound+CRLF;
     {задаем описание} 
     s:=s+'Content-Disposition: form-data; name="description"'+CRLF+CRLF;
     s:=s+edDescription.Text+CRLF+'--'+Bound+CRLF;
     {задаем тэги}
     s:=s+'Content-Disposition: form-data; name="tags"'+CRLF+CRLF;
     s:=s+edTags.Text;
     {записываем текстовую часть}
     WriteStrToStream(HTTP.Document,AnsiToUtf8(s));
     {скидываем тело документа в файл - вдруг захочется посмотреть что там}
     HTTP.Document.SaveToFile('Request.txt');
     {скидываем документ на сервер}
     if HTTP.HTTPMethod('POST',edLoadURL.Text) then
       begin
         {сервер что-то ответил - сохраняем - потом посмотрим}
         HTTP.Document.SaveToFile('Response.xml');
         lbLoadResult.Caption:='Сервер вернул ответ!';
       end
     else
       raise Exception.Create('Во время отправки фотки произошла ошибка');
   finally
     PicStream.Free;
   end;
 finally
   HTTP.Free;
 end;
end;

Собственно, в коде постарался как можно более подробно расписать все, что я делал и зачем. Памятуя о том, что в прошлый раз кто-то потерял три дня жизни из-за дефисов в разделителях — на этот раз обратил внимание и на них :).

Теперь запускаем программку, проходимся по всем шагам и в итоге получаем вот такой примерно вид:


Заходим в сервис Яндекс.Фотки и наблюдаем результаты работы:

Как видите — изображение загрузилось, название, описание и теги вставились без проблем, а следовательно — задача решена.

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

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

Описание: Рассмотрены практические вопросы по разработке клиент-серверных приложений в среде Delphi 7 и Delphi 2005 с использованием СУБД MS SQL Server 2000, InterBase и Firebird. Приведена информация о теории построения реляционных баз данных и языке SQL. Освещены вопросы эксплуатации и администрирования СУБД.
купить книгу delphi на ЛитРес
Описание: Рассмотрены малоосвещенные вопросы программирования в Delphi. Описаны методы интеграции VCL и API. Показаны внутренние механизмы VCL и приведены примеры вмешательства в эти механизмы. Рассмотрено использование сокетов в Delphi: различные режимы их работы, особенности для протоколов TCP и UDP и др.
купить книгу delphi на ЛитРес
0 0 голоса
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
17 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
xpert13
xpert13
27/11/2012 13:27

Вы же программист со стажем, а пиши такой код… :(

[code]
s:=s+'Content-Disposition: form-data; name="description"'+CRLF+CRLF;
s:=s+edDescription.Text+CRLF+'--'+Bound+CRLF;
s:=s+'Content-Disposition: form-data; name="tags"'+CRLF+CRLF;
s:=s+edTags.Text;
[/code]

Я понимаю, что часто лень на такие мелочи создавать отдельный класс, но в данном случае он бы имел более элегантный вид, чем эти копи-пасты. Чему вы учите молодежь?

Pavel Lobanov
27/11/2012 18:58

Отличная статья получилась.
Всё понятно.

Андрей
Андрей
14/03/2013 17:42

ООП это конечно хорошо, но согласен с Vlad, если так оформлять все статьи то всю жизнь на это можно убить, в том числе и на написание классов для таких тривиальных задач. ДА КРАСИВО было бы — НО ЗАЧЕМ!

Вы ещё к интерфейсу предиритесь, мол, а что это стандарные батоны и эдиты используете, не красиво :)))))

Михаил
Михаил
10/07/2013 17:05

Ничего не понимаю. Делал процедуру для загрузки изображений этим способом на другой сайт, но не работает. Все делал по аналогии, HTTP.Document пустой, потом сделал для теста вот так:


Var S: String;
...
S:= 'Hello, World!';
WriteStrToStream(HTTP.Document,S);
TestStr:= ReadStrFromStream(HTTP.Document, HTTP.Document.Size);

TestStr в Debug-Ad Watch пустая (равна »). Почему?

Михаил
Михаил
10/07/2013 20:53
Ответить на  Vlad

ОХ!
Я так и думал, что я просто чего-то не знаю =) Спасибо большое!

xsi
xsi
02/10/2013 04:41

пытаюсь приспособить Ваш пример под с++ builder. авторизацию осилил, а вот с отправкой файла не получается. код:

THTTPSend *http = new THTTPSend;
http->Protocol=»1.1″;
http->TargetHost=»api-fotki.yandex.ru»;
http->MimeType=»image/jpeg»;
http->Headers->Add(«Authorization: OAuth «+token);

TMemoryStream *PicStream = new TMemoryStream;
WriteStrToStream(http->Document,AnsiToUtf8(s));

PicStream->LoadFromFile(«C:\\CIMG9656.JPG»);
PicStream->Position=0;
http->Document->CopyFrom(PicStream,0);

WriteStrToStream(http->Document,AnsiToUtf8(s));
http->Document->SaveToFile(«c:\\Request.txt»);

http->HTTPMethod(«POST»,Form1->Edit1->Text);
http->Document->SaveToFile(«c:\\Response.xml»);

в реквест.тхт сервер сбрасывает ошибку о том, что тип изображения не поддерживается. «Not an image or unsupported image format». где я накосячил?

xsi
xsi
03/10/2013 00:37
Ответить на  Vlad

увы, это не причина

xsi
xsi
04/10/2013 04:06
Ответить на  Vlad

нашел баг — когда упрощал основной неработающий пример забыл выбросить добавление s. без него работает. но, когда пытаюсь добавить через multipart/form-data то ругается на No image data (no field «image») =( при этом кодировка вроде как надо выполняется. при этом в реквесте написано всё как в примере на яндексе. блин, что ж делать-то.

xsi
xsi
04/10/2013 05:06
Ответить на  Vlad

взял картинку, вставил туда теги multipart/form-datа, сохранил как текстовик. гружу этот текстовик с тегами и телом картинки — на сервак улетает только в путь. генерю такой же текстовик программой(то что выплевывает реквест.тхт перед отправкой), открываю его notepad++, сравниваю плагином compare — разницы нет! ну и да, если записываю сгенеренный файл, в потом читаю его сразу на передачу — результат нулевой, т.е. с таким костылём не прокатывает.

xsi
xsi
04/10/2013 05:28
Ответить на  Vlad

победил проблему! оказалось что перехода на новую строку \n недостаточно, надо еще возврат каретки делать \r\n. в notepad++ при изучении начинки всё выглядит как надо, так как он, видимо, сам переносит и смотрится все цивильно, а вот если тупым виндовым блокнотом смотреть — то разница на лицо. админ, я тут нафлудил — наверно всё это можно потереть нафиг =)

и да, большое спасибо за описанный пример, мне, как не программисту, он очень помог!

xsi
xsi
29/05/2017 03:31

немного оффтопика: заметил сегодня, что вот уже почти 5 лет открыта в браузере эта полезная страница =) спасибо тебе, автор!
и еще немножко — а как уважаемый автор внедрил такой прикольный виджет поддержки сайта?