- PVSM.RU - https://www.pvsm.ru -
Однажды я увидел этот баннер и решил, что бесплатный терабайт мне совсем не помешает, тем более, что мой архив фотографий и документов как раз лежал на терабайтном винчестере. Признаюсь, я очень опасался ставить на компьютер программу с логотипом mail.ru, но стремление к халяве пересилило. Я зарегистрировался, получил место, установил клиент, настроил его и забыл.
А несколько месяцев назад произошло неизбежное — мой жёсткий диск с архивом приказал долго жить. К счастью, к тому времени все файлы были скопированы в облако и ничего не потерялось.
После покупки нового диска я заново установил облачный клиент и стал ждать, когда скачаются мои файлы. Но спустя пару минут я увидел, что на диске ничего не появилось, а вот из Облака файлы стремительно удаляются.
Как выяснилось после общения с тех. поддержкой, это стандартное поведение клиента — какую бы папку вы ни скормили ему, он начинает синхронизировать её в облако, удаляя оттуда всё, чего в папке нет.
Скачать файлы по WebDav тоже невозможно:
Остаётся только возможность скачать файлы через веб-интерфейс. Файлы там можно скачивать по одному, а можно выбрать несколько файлов или папок и скачать их одним архивом, что довольно удобно. Единственное ограничение — архив не может превышать 4Гб.
Я попробовал пойти этим путём, но быстро понял, что это очень неудобный вариант:
Файлы мне всё-таки нужны, поэтому, я решил написать свой инструмент, а заодно изучить что-нибудь новое. Ну и получить удовольствие от решения задачи, разумеется.
Первым делом нужно понять, как получить список папок и файлов. Изначально я планировал просто парсить страницы, выдирать с них информацию о папках и файлах и строить дерево. Но, открыв исходный код страницы, я сразу же увидел, что весь интерфейс работы с документами строится через javascript, что, если подумать, весьма логично.
Поэтому, у меня появилось два возможных варианта решения: подключить
Selenium [1] и всё-таки строить дерево из html или разобраться с внутренним API, которое используется в скрипте.
Я выбрал второй путь, как самый разумный — зачем что-то парсить с использованием сторонних инструментов, если уже есть готовое API?
К счастью, скрипт не был обфусцирован и даже не сжат — мне были доступны исходные имена переменных и функций и комментарии разработчиков, это сильно облегчило задачу.
После нескольких минут изучения я увидел, что все доступные методы API описаны в массиве:
Вот поэтому я и не трачу в своём коде времени на красивое форматирование — кто-нибудь его обязательно поломает.
Я рассудил, что для получения списка папок и файлов в директории нужно вызывать метод folder. Для этого нужно отправить get-запрос на адрес https://cloud.mail.ru/api/v2/folder
.
Открываем страницу в браузере и видим такой ответ:
{"body":"user","time":1457097026874,"status":403}
Очевидно, нужно авторизоваться на портале. Авторизуюсь, повторяю запрос и вижу другую ошибку:
{"email":"me@mail.ru","body":"token","time":1457097187300,"status":403
Ничего удивительного, для выполнения запросов к API требуется токен. В списке методов есть два подходящих: tokens/csrf
и tokens/download
.
При запросе https://cloud.mail.ru/api/v2/tokens/download
отдаётся точно такая же ошибка токена, а значит нам нужен именно csrf-токен.
Запрашиваем его, добавляем в вызов метода folder параметр ?token=X9ccJNwYeowQTakZC1yGHsWzb7q6bTpP
и получаем новую ошибку:
{"email":"me@mail.ru","body":{"error":"invalid args"},"time":1457097695182,"status":400}
Здесь мне пришлось снова читать исходники, чтобы выяснить какие аргументы принимает этот метод. Оказалось, что нужно указать папку, содержимое которой мы хотим получить в параметре home.
Итак, в ответ на запрос по url https://cloud.mail.ru/api/v2/folder?token=X9ccJNwYeowQTakZC1yGHsWzb7q6bTpP&home=/
возвращается вот такой объект:
{
"email": "me@mail.ru",
"body": {
"count": {"folders": 1, "files": 1},
"tree": "363831373562653330303030",
"name": "/",
"grev": 17,
"size": 978473730,
"sort": {"order": "asc", "type": "name"},
"kind": "folder",
"rev": 9,
"type": "folder",
"home": "/",
"list": [{
"count": {"folders": 1, "files": 3},
"tree": "363831373562653330303030",
"name": "Фотографии",
"grev": 17,
"size": 492119223,
"kind": "folder",
"rev": 16,
"type": "folder",
"home": "/Фотографии"
}, {
"mtime": 1456774311,
"virus_scan": "pass",
"name": "Полет.mp4",
"size": 486354507,
"hash": "C2AD142BDF1E4F9FD50E06026BCA578198BFC36E",
"kind": "file",
"type": "file",
"home": "/Полет.mp4"
}]
},
"time": 1457097848869,
"status": 200
}
Информация о файлах и директориях — то, что нужно!
Работоспособность API подтверждена, схема его работы понятная — можно приступать к написанию программы. Я решил писать консольное приложение на php, поскольку хорошо знаю этот язык. Для этой задачи идеально подходит компонент Console [2] из состава Symfony. Я уже писал консольные команды для Laravel [3], которые построены как раз на этом компоненте, но там уровень абстракции довольно высок и напрямую с ним я не работал, поэтому решил, что настало время познакомиться поближе.
Не буду пересказывать документацию, она довольно подробная и очень простая. Ничего не зная о компоненте, за несколько часов я написал вот такие нехитрые интерфейсы:
Так выглядит приложение в процессе скачивания файлов.
А вот так по завершении: показывается небольшая табличка (максимум 100 строк) с информацией о скачаных файлах. Никакой практической пользы она не несёт и сделана исключительно в образовательных целях.
В состав консольного приложения может входить несколько команд, вызываемых следующим образом: php app.php command argument --option
. Но для моих целей нужна всего одна команда и я хотел бы запускать скачивание так: php app.php argument --option
. Этого легко добиться при помощи инструкции [4] из документации компонента.
Итак, консольное приложение готово, оно выводит информацию из заранее заготовленных фикстур и даже покрыто тестами [5]. Самое время реализовать непосредственно получение информации о файлах и папках из облака.
Здесь я тоже не стал изобретать велосипед и воспользовался прекрасной библиотекой Guzzle [6]. С её помощью очень удобно отправлять http запросы, при этом она использует интерфейсы PSR-7 [7].
При авторизации с главной страницы mail.ru отправляется post-запрос на адрес https://auth.mail.ru/cgi-bin/auth
, содержащий поля Login и Password.
/**
* @throws InvalidCredentials
*/
private function auth()
{
$expectedTitle = sprintf('Входящие - %s - Почта Mail.Ru', $this->login);
$authResponse = $this->http->post(
static::AUTH_DOMAIN . '/cgi-bin/auth',
[
'form_params' => [
'Login' => $this->login,
'Password' => $this->password,
]
]
);
try {
// http://php.net/manual/en/domdocument.loadhtml.php#95463
libxml_use_internal_errors(true);
$this->dom->loadHTML($authResponse->getBody());
$actualTitle = $this->dom->getElementsByTagName('title')->item(0)->textContent;
} catch (Exception $e) {
throw new InvalidCredentials;
}
if ($actualTitle !== $expectedTitle) {
throw new InvalidCredentials;
}
}
Поскольку в ответ на запрос авторизации возвращается несколько редиректов, которые в итоге приводят в почтовый ящик пользователя, я решил просто проверять заголовок страницы, чтобы определить успешно ли прошла авторизация.
Проверка простая настолько, что сейчас она проваливается если в почтовом ящике есть непрочитанные письма — их количество выводится в заголовке страницы. Но я ящиком не пользуюсь, поэтому для моих целей этого достаточно.
Далее я попробовал запросить csrf-токен, но с удивлением получил уже знакомую ошибку:
{"status":403,"body":"user"}
Я занялся отладкой запросов и увидел, что авторизация происходит успешно, но, тем не менее, токен мне не отдавался. Очень похоже на проблему с куками и действительно, оказывается, в Guzzle они по-умолчанию выключены и их нужно включать руками [8].
Проще всего это сделать один раз при инициализации клиента:
$client = new GuzzleHttpClient(['cookies' => true]);
Ещё одним параметром инициализации является 'debug' => true, с ним отладка запросов почти безболезненна.
Настроив куки, я снова попробовал получить токен и получил в ответ ошибку авторизации, с которой до этого не сталкивался:
{"email":"me@mail.ru","body":"nosdc","time":1457097187300,"status":403}
После чтения исходников и мониторинга процесса авторизации я увидел, что sdc — это ещё одна кука, которая получается отдельным запросом при старте приложения: https://auth.mail.ru/sdc?from=https://cloud.mail.ru/home
.
Я добавил этот запрос после запроса авторизации и наконец-то смог получить токен. Ну а дальше дело техники — запрашивать содержимое корневой папки и рекурсивно содержимое её подпапок, и дерево готово.
Как оказалось, дерево в итоге даже не понадобилось — каждый файл хранит полный путь от корня, поэтому для скачивания достаточно плоского списка.
Механизм скачивания немного хитрый: нужно сначала запросить рекомендуемый шард (что-то похожее на https://cloclo28.datacloudmail.ru/get/
) и только потом скачивать файл.
Учитывая, что адреса шардов отличаются только цифрой, думаю, можно было бы не заморачиваться и захардкодить адрес, но если уж делать, то делать до конца!
Для получения массива шардов нужно выполнить метод dispatcher (https://cloud.mail.ru/api/v2/dispatcher?token=X9ccJNwYeowQTakZC1yGHsWzb7q6bTpP
):
{
"email": "me@mail.ru",
"body": {
"video": [{"count": "3", "url": "https://cloclo22.datacloudmail.ru/video/"}],
"view_direct": [{"count": "250", "url": "http://cloclo18.cloud.mail.ru/docdl/"}],
"weblink_view": [{"count": "50", "url": "https://cloclo18.datacloudmail.ru/weblink/view/"}],
"weblink_video": [{"count": "3", "url": "https://cloclo18.datacloudmail.ru/videowl/"}],
"weblink_get": [{"count": 1, "url": "https://cloclo27.cldmail.ru/2yoHNmAc9HVQzZU1hcyM/G"}],
"weblink_thumbnails": [{"count": "50", "url": "https://cloclo3.datacloudmail.ru/weblink/thumb/"}],
"auth": [{"count": "500", "url": "https://swa.mail.ru/cgi-bin/auth"}],
"view": [{"count": "250", "url": "https://cloclo2.datacloudmail.ru/view/"}],
"get": [{"count": "100", "url": "https://cloclo27.datacloudmail.ru/get/"}],
"upload": [{"count": "25", "url": "https://cloclo22-upload.cloud.mail.ru/upload/"}],
"thumbnails": [{"count": "250", "url": "https://cloclo3.cloud.mail.ru/thumb/"}]
},
"time": 1457101607726,
"status": 200
}
Нас интересует массив, хранящийся в get.
Выбираем случайный элемент из массива шардов, добавляем к нему адрес файла и ссылка для скачивания готова!
Для экономии памяти можно сразу при создании запроса указать, куда Guzzle должен записать ответ, для этого используется параметр sink [9].
Итоговый код выложен на GitHub [10] под лицензией MIT, буду рад, если он кому-то пригодится.
Приложение далеко от идеала, его функционал ограничен, в нём совершенно точно есть баги и покрытие тестами оставляет желать лучшего, но оно на все 100% решило мою задачу, а ведь именно это требуется от MVP [11].
P.S. Хочу выразить-таки спасибо Mail.ru за то, что, во-первых, вместе с облачным клиентом у меня ни разу не установился «Амиго», а во-вторых, за то, что совершенно спасли меня от потери всего домашнего архива (даже не уверен, что из этого важнее). Но всё же, от греха подальше, я решил переехать в облако другой компании: 200 рублей в месяц — небольшая плата за то, чтобы мне не пришлось повторять этот аттракцион ещё раз.
* Все, которые не успело сначала удалить.
Автор: koceg
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/114459
Ссылки в тексте:
[1] Selenium: http://docs.seleniumhq.org/projects/webdriver/
[2] Console: http://symfony.com/doc/current/components/console/index.html
[3] Laravel: https://laravel.com/
[4] инструкции: http://symfony.com/doc/current/components/console/single_command_tool.html
[5] тестами: https://phpunit.de/
[6] Guzzle: http://docs.guzzlephp.org/en/latest/
[7] PSR-7: http://www.php-fig.org/psr/psr-7/
[8] включать руками: http://docs.guzzlephp.org/en/latest/quickstart.html#cookies
[9] sink: http://docs.guzzlephp.org/en/latest/request-options.html#sink
[10] GitHub: https://github.com/alexey-m-ukolov/cloud.mail.ru-downloader
[11] MVP: https://en.wikipedia.org/wiki/Minimum_viable_product
[12] Источник: https://habrahabr.ru/post/278849/
Нажмите здесь для печати.