Как написать собственное приложение с REST API Яндекс.Диска
Всем привет! Меня зовут Клеменс Ауэр, я занимаюсь разработкой десктопной версии Яндекс.Диска. Когда появился новый REST API, я был настолько впечатлен открывшимися возможностями, что в свободное время начал писать на его основе SDK для нового языка Swift. Я решил рассказать о своем опыте работы новым API и выступил с небольшим докладом о том, как просто начать с его помощью писать собственное приложение. Доклад был на английском, но по-русски я тоже говорю, хотя не так хорошо. На основе своего выступления я подготовил сегодняшний пост, с которым мне помогали мои коллеги.Начать я хотел бы с некоторых общих вещей о Диске — скорее всего, большую часть из этого вы уже слышали. Затем я расскажу, почему же меня так впечатлил новый API и чем он отличается от WebDAV. Ну, а ближе к концу я поделюсь опытом того, как начать разрабатывать под новый API, и разберу несколько примеров своего кода. Его будет совсем немного, но с новым API, чтобы заставить что-то работать, код писать практически не требуется.
WebDAV vs. RESTНаверное, многие из вас уже задались вопросом, в чем же разница между WebDAV и REST и каковы преимущества последнего? На первый взгляд разницы нет никакой: и там и там можно заливать и скачивать файлы, создавать папки, перемещать объекты, переносить их в корзину и удалять окончательно, создавать списки и т. д. В общем, основная функциональность полностью совпадает. Если сравнивать работу с публичными файлами, то через WebDAV вы можете делать файлы публичными и снова делать их приватными. С папками то же самое. То есть вы можете получить ссылку на файл или папку, а позже сделать ее недействительной. REST API добавляет к этому новую функциональность. Например, вы можете просмотреть метаданные публичных файлов. Имея лишь ссылку на файл, одним запросом к API вы сможете узнать его размер и имя. Если это папка, можно даже просмотреть структуру ее содержимого. И конечно, все можно сохранять к себе на Диск. Это был один из самых популярных фичереквестов после выпуска наших SDK. И вот наконец мы это реализовали. В REST API появилась поддержка работы с корзиной. Теперь посмотреть содержание корзины, очистить ее или восстановить нужные файлы можно не только через веб-интерфейс. Да, кое-чего действительно не хватает. Например, не стали добавлять базовую аутентификацию и не планируем этого делать. Кроме того, обращаться к новому API можно только через HTTPS. Это сделано ради безопасности пользователя. Когда я делал доклад, получить через REST API информацию о свободном и занятом месте на диске пользователя было невозможно. Поэтому в таблице напротив этого пункта стоит крестик. Однако сейчас эта функция уже появилась. Многие знают, что через WebDAV можно добавлять файлы без загрузки, через хэш-суммы. На стороне клиента вычисляются MD5, SHA, к ним добавляется размер файла, и эта информация передается на сервер. Если файл с такими же хэш-суммами и размером уже есть в хранилище, загрузки не происходит, файл просто добавляется на ваш Диск. К сожалению, в REST API этого пока нет.Когда вы работаете через OAuth, все, что у вас есть от пользователя, — это токен. Но узнать побольше о пользователе иногда бывает полезно: какой у него логин, e-mail и т. д. Получить эти данные можно через отдельный API, описание которого тут. С помощью этого API можно получить гораздо больше информации, чем через WebDAV.
Приступаем к разработке В первую очередь нужно зарегистрировать приложение и создать аккаунт разработчика. Вы же не хотите, чтобы в результате ваших экспериментов что-нибудь случилось с файлами на вашем личном Диске. Далее вам нужно получить токен и немного разобраться с Полигоном. Вот и все, можно начинать писать код.Процесс регистрации приложения достаточно прост: вы идете на страницу https://oauth.yandex.ru/client/new и вводите название и описание приложения. Название в дальнейшем будет использоваться в качестве наименования папки вашего приложения в Диске пользователя. Там же нужно указать, какие доступы вам потребуются: доступ к папке приложений на Диске, права чтение или запись всего Диска пользователя. Если поставить галочку в чекбоксе «Клиент для разработки», поле callback UI будет заполнено дефолтным URL, который никуда не редиректит, а просто показывает вам токен. Мобильные разработчики, скорее всего, будут использовать специальный URL handler и схему приложения. Поэтому в это поле можно ввести просто что-нибудь вроде my_application:/authoruze, а Яндекс.Диск средиректит на ваше приложение, и оно появится уже с одним из параметров вашего токена.
После регистрации приложения все совсем просто. Вы получаете ID приложения и пароль. Самое важное — это ID, так как он нужен для выдачи токенов пользователям. Пароль тоже важен, но это зависит от того, как вы используете API, как работаете с OAuth. Для веб-сервисов пароль может оказаться даже важней.
Скорее всего, потребуется сразу несколько таких аккаунтов, ведь вам нужно будет тестировать возможности передачи файлов между аккаунтами. Кроме того, в наши дни желательно использовать автотесты, для них может потребоваться еще пара аккаунтов. Просто отправляйте инвайты со своего личного аккаунта. В этом есть приятный бонус, ведь за каждый аккаунт вы получите по 500 мегабайт на своем Диске.
Получить токен вы можете либо на Полигоне, либо по этой ссылке: https://oauth.yandex.ru/authorize? response_type_token&client_id=, подставив ID, который вы получили при регистрации.
На Полигоне вы можете поэкспериментировать со всеми функциями нового API. При этом сервис обновляется одновременно с ним. Так что, даже если документация отстает на неделю или месяц, все новые функции уже будут на Полигоне. Можно сразу посмотреть, как каждая новая фича работает в боевых условиях. Там же есть таблица с HTTP-кодами ответов, где указано, что означают те или иные сообщения об ошибках или успешном завершении операций. Это гораздо лучше даже самой подробной документации.
Когда все приготовления окончены, можно начинать писать код. Как я уже говорил, сам я начал писать SDK для Swift.
import Foundation
public class Disk { … }
let disk = Disk (token: «d8edc4f3a698473fbc87634c41b2ca81») var fileURL: NSURL
disk.uploadURL (fileURL, toPath: fileURL.lastPathComponent, overwrite: true) { // handle errors }
disk.deletePath (fileURL.lastPathComponent, permanently: false) { // handle response } Рассказывать о нем в полной мере не имеет смысла, так что для демонстрации я убрал самое сложное и сократил объем кода до 200 строк. Заставить что-то работать очень просто. public class Disk { public let token: String public let baseURL = «https://cloud-api.yandex.net:443»
var additionalHTTPHeaders: [String: String] { return [ «Accept» : «application/json», «Authorization» : «OAuth \(token)», «User-Agent» : «Mobile Camp Demo» ] } ] В этом примере я использую чистый JSON, но можно также использовать JSON+HAL. Обязательно нужно прописать авторизацию. Большинство вопросов о Яндекс.Диске касаются не самого API, а OAuth. Оказывается, что это вызывает гораздо больше сложностей при работе с Диском, чем сам API. Так что, если вы разобрались с OAuth, вы практически у цели. В User-Agent можно прописать все что угодно, например какой-нибудь идентификатор вашего приложения. public lazy var session: NSURLSession = { let config = NSURLSessionConfiguration.defaultSessionConfiguration () config.HTTPAdditionalHeaders = self.additionalHTTPHeaders return NSURLSession (configuration: config) }() public init (token: String) { self.token = token Кроме того, нужно инициализировать что-то вроде сессии, которую можно использовать для HTTP-запросов и т. п. Когда у вас будет готово какое-то более-менее серьезное приложение, можно добавить еще одну сессию, например для бэкграундных трансферов. Но по сути все JSON-запросы делают обычные запросы данных, так что при желании можно ограничиться и одной сессией. Понадобится еще небольшой инициализатор. Мы работаем с JSON, так что нам придется много сеарилизовать и десерилизовать JSON-объекты. extension Disk { class func JSONDictionaryWithData (data: NSData!, onError:(NSError!)→Void) → NSDictionary? {
var error: NSError? let root = NSJSONSerialization.JSONObjectWithData (data, options: nil, error: &error) as? NSDictionary if root == nil { onError (NSError (…)) return nil }
return root } } К сожалению, каким бы клевым ни был Swift, это все-таки язык со статической типизацией, так что при использовании API Objective-C приходится делать много подобных вещей. Допустим, возвращаются какие-то данные и вы хотите получить их как NSDictionary. Это генерирует много дополнительного кода, так что, как мне подсказывает опыт, вы начнете писать всякие вспомогательные штуки типа JSONDictionaryWithData. Таким образом, вы получаете данные, делаете запрос, обрабатываете ошибки, а затем возвращаете уже нужный объект. Работая с подобным API, нужно делать много запросов. Так что я также имплементировал что-то вроде JSON-тасков с методами: GET, POST, DELETE и т. д. extension NSURLSession { func jsonTaskWithMethod (method: String, url: NSURL!, onError: ((NSError!) → Void)!, onCompletion: ((NSDictionary, NSHTTPURLResponse) → Void)!) → NSURLSessionDataTask! { let request = NSMutableURLRequest (URL: url) request.HTTPMethod = method
return dataTaskWithRequest (request) { (data, response, error) → Void in let jsonRoot = Disk.JSONDictionaryWithData (data, onError: onError) if let jsonRoot = jsonRoot { switch response.statusCode { case 400…599: return onError (…) default: return onCompletion (jsonRoot, response) } } else { return // handler already called from JSONDictionaryWithData } } } } И еще URL с обработчиком ошибок и завершений. Swift очень эффективен при работе с замыканиями, там это один из базовых типов. По сравнению с питоном замыкания тут хорошо интегрированы синтаксически, работать с ними гораздо приятнее. Кроме того, можно сделать специальную зону для трехсотых и четырехсотых кодов прямо в этих вспомогательных блоках.Давайте еще посмотрим, как происходит загрузка файла. Я сделал пару скриншотов прямо с Полигона. Верхний — про параметры. Тут у нас путь на Диске, куда будут сохраняться файлы, а также пометка о том, нужно ли файл перезаписывать. В ответ вы получите от Диска что-то вроде этого:
Работая с REST API, вы будете сталкиваться с такой конструкцией чаще всего.Как же происходит загрузка файла? Вы делаете запрос, сервис в ответ передает вам эту конструкцию. Загрузить файл вы можете на выданный URL при помощи указанного HTTP-метода. Отпадает необходимость обращаться к WebDAV-прокси, вы работаете напрямую с бэкэндом хранилища. Помимо всего прочего это хорошо сказывается на пропускной способности и скорости. Если проведете замеры, увидите разницу.
extension Disk { public func uploadURL (fileURL: NSURL, toPath path: String, overwrite: Bool?, handler:(NSError!) → Void) → Void { var url = »\(baseURL)/v1/disk/resources/upload? path=\(path.urlEncoded ())»
if let overwrite = overwrite { url += »&overwrite=\(overwrite)» } let error = { handler ($0) }
session.jsonTaskWithMethod («GET», url: NSURL (string: url), onError: error) { (jsonRoot, response)→Void in let (href, method, templated) = Disk.hrefMethodTemplatedWithDictionary (jsonRoot)
let request = NSMutableURLRequest (URL: NSURL (string: href)) request.HTTPMethod = method
self.session.uploadTaskWithRequest (request, fromFile: fileURL) { (data, response, trasferError)→Void in return error (trasferError) }.resume () }.resume () } } В JSONDictionary полезно завести topper, тоже неплохой тип из Swift. Вот и вся загрузка. Просто выстраиваете URL, указываете, хотите ли вы делать перезапись, создаете обработчик ошибок. Это просто замыкание, которое оборачивает обычный обработчик в случае ошибки. Особенно интересно создавать подобные вещи, когда у вас больше одного параметра. Типы возвращаемых значений в этом случае немного сложнее.Скачивание файлов практически не отличается. Нужно лишь заменить upload на download.
Перейдем к удалению файлов. Снова обратимся к Полигону. Опять видим путь, какой-то параметр и ответ в виде все той же конструкции. Отличие — в другом HTTP-коде.
Удаление не всегда проходит моментально. Если процесс запущен, но еще не закончен, вы получите ошибку 202. Статус выполнения можно проверять отдельным запросом. Я забыл упомянуть, что в REST API предусмотрены асинхронные операции. Так что многие операции типа перемещения и удаления могут проводиться сервером асинхронно, так как на их выполнение может уходить несколько секунд или даже больше. WebDAV в таких случаях выдает только сообщения об ошибках или необходимости подождать. public enum DeletionResult { case Done case InProcess (href: String, method: String, templated: Bool) case Failed (NSError!) }
extension Disk { public func deletePath (path: String, permanently: Bool?, handler:(result: DeletionResult) → Void) → Void { var url = »\(baseURL)/v1/disk/resources? path=\(path.urlEncoded ())» if let permanently = permanently { url += »&permanently=\(permanently)» } let error = { handler (result: .Failed ($0)) } session.jsonTaskWithMethod («DELETE», url: NSURL (string: url), onError: error) { (jsonRoot, response)→Void in switch response.statusCode { case 202: return handler (result: .InProcess (Disk.hrefMethodTemplatedWithDictionary (jsonRoot))) case 204: return handler (result: .Done) default: return error (NSError (…)) } }.resume () } } По сути функция удаления не отличается от заливки и скачивания. Очень удобно, что у нас есть тип enum, который может принимать дополнительные объекты. Применение enum в качестве типа возвращаемых данных для обработчика дает неплохое преимущество. Вы пишете код, используете тип возвращаемых данных enum, присоединяете к нему какие-то объекты. Чем это хорошо? Единственный способ работы с enum — применение switch, а это подразумевает, что вы покрываете все вероятные сценарии в Swift. Таким образом, используя enum, вы автоматически вынуждаете делать обработку большинства возможных событий.Выводы Что я усвоил, занимаясь всем этим, какой опыт получил? В первую очередь, я понял, что Swift — действительно клевый язык с большим будущим. Особенно тип enum вместе со switch. Удобно использовать и базовые типы, например замыкания. По сути это безымянные функции, которые можно создавать в коде где угодно. Можно просто встроить такую функцию в пирамиду функций, вернуть ее и т. д. С ее помощью можно даже применять приемы функционального программирования. Одна из основных проблем Swift сегодня — строгость в отношении безопасности типов. Например, когда вы начинаете вызывать из Swift код на Objective-C, тип id может доставить вам проблемы. Его нельзя использовать просто так, нужно явно указать, как именно его использовать: как Dictionary, myType или что-нибудь еще. Так что, если вы применяете подобные структуры, вам придется уделять много внимания приведению типов, что сделает код немного объемнее. C другой стороны, на сегодня не существует нативных библиотек для Swift, доступна только небольшая основа. Скорее всего, дело в том, что ресурсы Apple ограничены, они пока не могут повторить то, что сделала Microsoft при запуске .NET: огромные библиотеки, десять тысяч объектов и имплементация всего того, что уже было у них в системе. Я надеюсь, что со временем ситуация будет меняться. По большому счету безопасность типов — это хорошо, компилятор лучше понимает, что может пойти не так. Еще один нюанс — изменчивость языка. Те, кто уже пробовал с ним работать, наверное, заметили, что с каждым обновлением Xcode что-то меняется. Старый код перестает компилироваться, даже те примеры кода, которые демонстрировала Apple. Вылезают странные ошибки, падения. Иногда это немного расстраивает. Вылетает даже сама среда разработки. Нет смысла гуглить или искать в Яндексе, вы найдете только устаревшую информацию. Сейчас один из главных навыков при работе со Swift — привычка набирать devforums.apple.com. Там идут открытые обсуждения, никто не боится раскрыть какие-нибудь секреты и т. п. Там действительно есть шанс получить ответ на свой вопрос. Если вы не нашли ответа в уже существующих топиках, просто создайте свой, кто-нибудь да ответит. Может быть, не сразу, но за пару дней точно. Это самый полезный ресурс на данный момент.Что касается REST API, то главный совет тут — используйте Полигон, он прекрасен. На создание документации ушло немало сил, но поддерживать ее в актуальном состоянии еще сложнее. А Полигон актуален всегда. Как только выходит новая версия, вся информация о ней уже там. Кроме того, REST API достаточно быстр. Он действительно заметно быстрее WebDAV. А работать с OAuth приятно всегда. Да, это требует писать больше кода, но, скорее всего, у вас уже есть какая-нибудь имплементация, так что особого значения это не имеет. У REST API многое еще впереди. В API еще остались белые пятна, но у нас есть множество идей, реализацией которых мои коллеги активно занимаются.
Подводя итог, я хочу посоветовать заниматься разработкой через тестирование. Это сохранит вам много времени. Особенно когда вы пишете что-то вроде SDK. Гораздо легче написать небольшой тест для имплементируемой вами функции, чем пытаться вызывать ее из приложения. Просто пишите тесты, вызывайте свои функции, проверяйте результаты. В наше время даже разработка под асинхронные API не вызывает проблем. Xcode позволяет создавать асинхронные тесты: вы задаете условия и запускаете тест, а обработчик говорит, успешно ли все выполнено. Тестирование асинхронных API там устроено очень просто. Кроме того, это помогает находить баги. Ошибки делают все, и мы не исключение, поэтому мы рады каждому багрепорту. Вы можете использовать ФОС на странице API Диска. Обычно мы быстро все чиним — в зависимости от сложности и критичности проблемы, конечно.