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

в 10:38, , рубрики: cloud.mail.ru, mit license, php, symfony, Восстановление данных, метки: ,

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

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

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

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

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

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

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

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

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

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

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

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

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

Поэтому, у меня появилось два возможных варианта решения: подключить
Selenium и всё-таки строить дерево из 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 из состава Symfony. Я уже писал консольные команды для Laravel, которые построены как раз на этом компоненте, но там уровень абстракции довольно высок и напрямую с ним я не работал, поэтому решил, что настало время познакомиться поближе.

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

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

Как Облако@mail.ru спасло все* мои файлы и что из этого вышло - 6
А вот так по завершении: показывается небольшая табличка (максимум 100 строк) с информацией о скачаных файлах. Никакой практической пользы она не несёт и сделана исключительно в образовательных целях.

В состав консольного приложения может входить несколько команд, вызываемых следующим образом: php app.php command argument --option. Но для моих целей нужна всего одна команда и я хотел бы запускать скачивание так: php app.php argument --option. Этого легко добиться при помощи инструкции из документации компонента.

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

Здесь я тоже не стал изобретать велосипед и воспользовался прекрасной библиотекой Guzzle. С её помощью очень удобно отправлять http запросы, при этом она использует интерфейсы PSR-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 они по-умолчанию выключены и их нужно включать руками.

Проще всего это сделать один раз при инициализации клиента:

$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.

Итоговый код выложен на GitHub под лицензией MIT, буду рад, если он кому-то пригодится.

Приложение далеко от идеала, его функционал ограничен, в нём совершенно точно есть баги и покрытие тестами оставляет желать лучшего, но оно на все 100% решило мою задачу, а ведь именно это требуется от MVP.

P.S. Хочу выразить-таки спасибо Mail.ru за то, что, во-первых, вместе с облачным клиентом у меня ни разу не установился «Амиго», а во-вторых, за то, что совершенно спасли меня от потери всего домашнего архива (даже не уверен, что из этого важнее). Но всё же, от греха подальше, я решил переехать в облако другой компании: 200 рублей в месяц — небольшая плата за то, чтобы мне не пришлось повторять этот аттракцион ещё раз.

* Все, которые не успело сначала удалить.

Автор: koceg

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js