Компания
510,95
рейтинг
22 октября 2014 в 15:34

Разработка → Как написать собственное приложение с 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 Диска. Обычно мы быстро все чиним — в зависимости от сложности и критичности проблемы, конечно.
Автор: @aucl
Яндекс
рейтинг 510,95

Комментарии (12)

  • +1
    Сколько не глядел не могу понять, можно ли дать приложению доступ только к одной папке?
    • 0
      Приложение может запросить права на папку Приложения или на запись/чтение всего Диска.
      • +1
        Только работает это криво, писал в поддержку яндекс.диска. Но, что-то толком и не ответили.
        Если выбрать доступ только к каталогу приложения — то нельзя посмотреть квоту на диске (не хватает прав). Получается для того, чтобы узнать квоту, мне нужно просить у пользователя доступ на чтение всего диска.
        Вот к примеру, у меня приложение для бэкапа которое может заливать к примеру файл на несколько гиг, то мне нужно либо включать доступ к чтению всего диска, либо заливать файл на авось, и после заливки узнавать, что места нет.

        Также не ясно когда появится поддержка upload по частям (что есть и в dropbox, и в google drive, и т.п.). То же касается скачивания, не всего файла, а частей (к примеру, не обязательно скачивать весь zip файл, если нужно оттуда извлечь только один файл).

        И еще хотелось бы, чтобы если запрос идет к app:/ то и пути к файлам и каталогам получать относительно app:/. Зачем приложению знать полный путь, если оно имеет доступ только к своему каталогу.
        • 0
          про квоты, это баг!.. мы это исправим.
          • 0
            Спасибо, будем ждать.

            И еще хотел предложить, чтобы добавили в информацию о диске (сейчас это только квоты) имя пользователя, и возможно ссылка с приглашением. Да я в курсе, что можно узнать имя пользователя используя API Логин (хотя в доках REST API об этом ни слова). Но это выглядит, как костыль, так как нужно делать отдельный запрос к другому хосту. Т.е. все данные мы получаем с cloud-api.yandex.net (и соответственно можно использовать Keep-Alive соединение), а для запроса имени нужно делать отдельный запрос на login.yandex.ru.
            • 0
              OAuth же тоже отдельно. такие запросы не так часто делаете что Keep-Alive было важно?!
              • 0
                Ну я же написал, что это не критично, но как по мне более логично, если запрашиваешь информацию о диске, то получаешь и имя владельца, как сделано у всех основных конкурентов (Dropbox, Google Drive, Box, Copy и т.п.). У того же Google тоже куча разных API, но если используешь API диска, то можно получать всю нужную информацию с этого API. А не использовать для этого еще другие API.
  • 0
    Не так давно написал bash-скрипт, использующий OAuth-авторизацию, curl и REST API Яндекс.Диска для резервного копирования: исходники на гитхаб, описание в блоге. Буду рад, если кому-нибудь пригодится, а так же конструктивной критике и замечаниям. Цель была больше образовательная, чем практическая — хотелось использовать обычный shell и curl, тем более последнего для работы с REST API более чем достаточно.
    • +2
      спасибо большое за информацию, мы с командой ознакомимся с вашим решением
  • +1
    А я очень давно столкнулся с проблемой разлогинивания — как?
    Я хочу, чтобы юзер мог нажать на кнопку «Sign out», и при этом произошло примерно следующее:
    * Программа сделала запрос на уничтожение токена
    * Программа удалила токен из внутреннего хранилища
    * Пользователь перенаправлен на страницу входа в Яндекс.Диск, а она знает, что пользователь разлогинен и показывается в первоначальном виде.

    Сейчас метода API для первого пункта нет (не было по крайней мере пару месяцев назад), а на номере три даже без явного указания tokena в url все равно происходит автоматическая авторизация (чтобы этого не происходило, нужно выйти из Яндекс.Паспорта).

    Это не особо популярная хотелка? =\ Вроде бы в других сервисах, использующих OAuth, бывает способ выйти…
    • 0
      ну да, это к сожалению пока ещё не возможно ( api.yandex.ru/oauth/ ), но поговорим с командой OAuth о разработке этой фичи.
  • 0
    Было бы очень интересно почитать о проблемах, с которыми вы столкнулись при разработке как API так и клиента. Наверняка кто-то сломал голову, думая о разрешении конфликтов, к чему Вы в итоге пришли и какие планы? Как и какие проблемы решали и решили ли при отслеживании изменений файлов и папок. Как происходит процедура синхронизации с учетом того, что пользователь в этот момент может сделать что угодно как с файлами, так и убить приложение. И так далее.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка