- PVSM.RU - https://www.pvsm.ru -

Как Облако@mail.ru спасло все* мои файлы и что из этого вышло

Как Облако@mail.ru спасло все* мои файлы и что из этого вышло - 1

Однажды я увидел этот баннер и решил, что бесплатный терабайт мне совсем не помешает, тем более, что мой архив фотографий и документов как раз лежал на терабайтном винчестере. Признаюсь, я очень опасался ставить на компьютер программу с логотипом mail.ru, но стремление к халяве пересилило. Я зарегистрировался, получил место, установил клиент, настроил его и забыл.

А несколько месяцев назад произошло неизбежное — мой жёсткий диск с архивом приказал долго жить. К счастью, к тому времени все файлы были скопированы в облако и ничего не потерялось.

После покупки нового диска я заново установил облачный клиент и стал ждать, когда скачаются мои файлы. Но спустя пару минут я увидел, что на диске ничего не появилось, а вот из Облака файлы стремительно удаляются.

Как выяснилось после общения с тех. поддержкой, это стандартное поведение клиента — какую бы папку вы ни скормили ему, он начинает синхронизировать её в облако, удаляя оттуда всё, чего в папке нет.

Скачать файлы по WebDav тоже невозможно:

Как Облако@mail.ru спасло все* мои файлы и что из этого вышло - 2

Остаётся только возможность скачать файлы через веб-интерфейс. Файлы там можно скачивать по одному, а можно выбрать несколько файлов или папок и скачать их одним архивом, что довольно удобно. Единственное ограничение — архив не может превышать 4Гб.

Как Облако@mail.ru спасло все* мои файлы и что из этого вышло - 3

Я попробовал пойти этим путём, но быстро понял, что это очень неудобный вариант:

  • Ограничение в 4 гигабайта означает, что если у вас в облаке находится около терабайта, придётся качать как минимум 250 архивов.
  • Каждый архив нужно создавать вручную, выбирая папки, считая их суммарный размер и помечая те, что уже скачаны.
  • Иногда архивы не открываются по неизвестной причине.
  • Теряется структура папок.

Файлы мне всё-таки нужны, поэтому, я решил написать свой инструмент, а заодно изучить что-нибудь новое. Ну и получить удовольствие от решения задачи, разумеется.

Первым делом нужно понять, как получить список папок и файлов. Изначально я планировал просто парсить страницы, выдирать с них информацию о папках и файлах и строить дерево. Но, открыв исходный код страницы, я сразу же увидел, что весь интерфейс работы с документами строится через javascript, что, если подумать, весьма логично.

Поэтому, у меня появилось два возможных варианта решения: подключить
Selenium [1] и всё-таки строить дерево из html или разобраться с внутренним API, которое используется в скрипте.

Я выбрал второй путь, как самый разумный — зачем что-то парсить с использованием сторонних инструментов, если уже есть готовое API?
К счастью, скрипт не был обфусцирован и даже не сжат — мне были доступны исходные имена переменных и функций и комментарии разработчиков, это сильно облегчило задачу.

После нескольких минут изучения я увидел, что все доступные методы API описаны в массиве:

Как Облако@mail.ru спасло все* мои файлы и что из этого вышло - 4
Вот поэтому я и не трачу в своём коде времени на красивое форматирование — кто-нибудь его обязательно поломает.

Я рассудил, что для получения списка папок и файлов в директории нужно вызывать метод 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], которые построены как раз на этом компоненте, но там уровень абстракции довольно высок и напрямую с ним я не работал, поэтому решил, что настало время познакомиться поближе.

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

Как Облако@mail.ru спасло все* мои файлы и что из этого вышло - 5
Так выглядит приложение в процессе скачивания файлов.

Как Облако@mail.ru спасло все* мои файлы и что из этого вышло - 6
А вот так по завершении: показывается небольшая табличка (максимум 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/