Pull to refresh

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

PHP *Symfony *
Sandbox


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

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

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

В конце статьи есть UPD, UPD2, UPD3 и UPD4, в котором описаны причины такого поведения.
TL;DR: ложная тревога, с файлами и синхронизацией всё в порядке, а вот пользовательский интерфейс и работу тех. поддержки нужно дорабатывать.


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

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



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



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

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

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

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

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

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


Так выглядит приложение в процессе скачивания файлов.


А вот так по завершении: показывается небольшая табличка (максимум 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 \GuzzleHttp\Client(['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 рублей в месяц — небольшая плата за то, чтобы мне не пришлось повторять этот аттракцион ещё раз.

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

UPD: Общение с тех. поддержкой.

[[[[ У меня возникают проблемы с синхронизацией..., другая проблема, Форма обратной связи ]]]]
Добрый день.
Я заменил жесткий диск, на котором располагалась папка облака. Старый диск сломался, поэтому перенести данные с него нет возможности. В веб-интерфейсе все мои данные на месте.
Когда я создал пустую папку на новом диске и настроил её в приложении, при синхронизации начали удаляться файлы в облаке.
Как мне настроить приложение на компьютере, чтобы оно считало основной копией веб, а не пустую папку — то есть начало бы скачивать файлы на компьютер, а не удалять их в облаке.
Пробовал скачивать файлы через браузер, но это нереально — их очень много.


support@cloud.mail.ru 29.12.15
Здравствуйте.

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

По умолчанию между web-интерфейсом и приложением на компьютере
осуществляется полная двусторонняя синхронизация — если вы удаляете файл из
Облака в web-интерфейсе, то файл удаляется и в приложении, так же и
наоборот: удаляя файл в приложении, вы удаляете файл и в Облаке.

Вы можете настроить выборочную синхронизацию в ПК-клиенте Облака. Для этого
кликните на иконке Облака (в системном трее) правой кнопкой мыши и
перейдите в раздел «Выбрать папки».
В открывшемся окне снимите галочки напротив тех папок, синхронизацию для
которых вы хотите отменить и нажмите «Выбрать».
Если ранее папка была синхронизирована, то она будет удалена с вашего
компьютера, но в web-интерфейсе Облака папка, а также все содержащиеся в
ней файлы, сохранятся.
Чтобы вновь включить синхронизацию для удаленной ранее папки, кликните на
иконку приложения Облака правой кнопкой мыши, нажмите «Выбрать папки» и
установите галочку напротив имени необходимой папки.

Вы также можете временно отключить синхронизацию. Для этого кликните на
иконку приложения Облака правой кнопкой мыши и выберите «Приостановить
синхронизацию».

Подробнее о синхронизации Вы можете прочитать в системе Помощи
help.mail.ru/cloud_web/synch


Алексей Уколов 29.12.15
Возможно, я не совсем явно обозначил свою проблему, попробую перефразировать.

Все мои файлы на данный момент есть в облаке. Я купил новый HDD и хочу эти файлы на него скачать. Но когда я создал на нем пустую папку и указал её в приложении, вместо скачивания файлов из облака на компьютер, файлы начали удаляться из облака.

Как мне запустить процесс в обратном направлении — скачать всё из облака на компьютер, не используя веб-интерфейс.

Если это невозможно сделать через приложение, есть ли какие-то альтернативные инструменты? WebDav, как я понимаю, еще не реализован?


support@cloud.mail.ru 29.12.15
Здравствуйте.

На текущий момент данная функциональность отсутствует.

Ваше замечание передано разработчикам.


UPD2: Проблема до сих пор воспроизводится, представители Облака@mail.ru говорят, что это нетипичное поведение и проблема локальная, в комментариях есть сообщения, что синхронизация с пустой папкой работает как нужно.
Добавил видеопример: youtu.be/dTF9UCdN2S8
Прошу прощения за вотермарки и общее качество, просто proof of concept.

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

UPD4: Bulldozavr написал, что, похоже, это удаляются системные файлы Thumbs.db и desktop.ini. Я запустил синхронизацию и не стал её останавливать — действительно, периодически в статусе было видно такие файлы (но из-за того, что ширина контекстного меню ограничена, а доступного файла лога нет, пользователю убедиться, что удаляются только эти файлы невозможно).
Спустя пару минут все системные файлы, судя по всему, удалились и началась закачка из облака на компьютер, как и должно быть.
Вердикт следующий: синхронизация работает нормально, хоть и пугает поначалу пользователя удалением файлов из облака; тех. поддержка работает плохо.
Ну а мой опыт написания консольных приложений на php уже никуда не денется :)
Tags:
Hubs:
Total votes 80: ↑72 and ↓8 +64
Views 111K
Comments Comments 118