Предыстория


Так уж случилось, что нужно мне было где-то хранить более 1.5тб данных, да еще и обеспечить возможность скачивания их обычными пользователями по прямой ссылке. Поскольку традиционно такие объемы памяти идут уже на VDS, стоимость аренды которых не слишком вкладывается в бюджет проекта из категории «от нечего делать», а из исходных данных у меня был VPS 400GB SSD, куда при всем желании 1.5тб картинок без lossless сжатия поместить не удастся.


И тут я вспомнил про то, что если удалить с гугл-диска хлам, вроде программ, которые запустятся только на Windows XP, и прочие вещи, которые кочуют у меня с носителя на носитель с тех пор, когда интернет был не таким быстрым и вовсе не безлимитным (например, те 10-20 версий virtual box вряд ли имели какую-то ценность, кроме ностальгической), то все должно очень даже хорошо вместиться. Сказано — сделано. И вот, пробиваясь через лимит на количество запросов к api (кстати, техподдержка без проблем увеличила квоту запросов на пользователя за 100 секунд до 10 000) данные резво потекли в место своей дальнейшей дислокации.


Все вроде бы и хорошо, но теперь это нужно донести до конечного пользователя. Да еще и без всяких там редиректов на другие ресурсы, а чтоб человек просто нажал кнопочку «Download» и стал счастливым обладателем заветного файла.


Тут я, ей-богу, пустился во все тяжкие. Сначала это был скрипт на AmPHP, но меня не устроила создаваемая им нагрузка (резкий скачек на старте до 100% потребления ядра). Потом в дело пошел враппер curl для ReactPHP, который вполне вписывался в мои пожелания по поедаемому количеству тактов CPU, но давал скорость вовсе не такую, как мне хотелось (оказалось, что можно просто уменьшить интервал вызова curl_multi_select, но тогда мы имеем аналогичную первому варианту прожорливость). Пробовал даже написать маленький сервис на Rust, и работал он довольно резво (удивительно даже то, что он работал, с моими то познаниями), но хотелось большего, да и кастомизировать его было как-то непросто. Кроме того все эти решения как-то странно буфферизировали ответ, а мне хотелось отслеживать момент когда закончилась загрузка файла с наибольшей точностью.


В общем какое-то время это косо-криво, но работало. Пока однажды мне не пришла в голову замечательная по своей бредовости идея: nginx в теории может то, что я хочу, работает резво, да еще позволяет всякие извращения с конфигурированием. Надо попробовать — вдруг получится? И спустя пол дня настойчивых поисков родилось стабильно работающее уже несколько месяцев решение, которое отвечало всем моим требованиям.


Настраиваем NGINX


# Первым делом создадим в конфигах нашего сайта отдельную локацию.
location ~* ^/google_drive/(.+)$ {

    # И закроем её от посторонних глаз (рук, ног и прочих частей тела).
    internal;

    # Ограничим пользователям скорость до разумных пределов (я за равноправие).
    limit_rate 1m;

    # А чтоб nginx мог найти сервера google drive укажем ему адрес резолвера.
    resolver 8.8.8.8;

    # Cоберем путь к нашему файлу (мы потом передадим его заголовками).
    set $download_url https://www.googleapis.com/drive/v3/files/$upstream_http_file_id?alt=media;

    # А так же Content-Disposition заголовок, имя файла мы передадим опять же в заголовках.
    set $content_disposition 'attachment; filename="$upstream_http_filename"';

    # Запретим буфферизировать ответ на диск.
    proxy_max_temp_file_size 0;

    # И, что немаловажно, передадим заголовок с токеном (не знаю почему, но в заголовках из $http_upstream токен передать не получилось. Вернее передать получилось, но скорей всего его где-то нужно экранировать, потому что гугл отдает ошибку авторизации).
    proxy_set_header Authorization 'Bearer $1';

    # И все, осталось отправить запрос гуглу по ранее собранному нами адресу.
    proxy_pass $download_url;

    # А чтоб у пользователя при скачивании отобразилось правильное имя файла мы добавим соответствующий заголовок.
    add_header Content-Disposition $content_disposition;

    # Опционально можно поубирать ненужные нам заголовки от гугла.
    proxy_hide_header Content-Disposition;
    proxy_hide_header Alt-Svc;
    proxy_hide_header Expires;
    proxy_hide_header Cache-Control;
    proxy_hide_header Vary;
    proxy_hide_header X-Goog-Hash;
    proxy_hide_header X-GUploader-UploadID;
}

Краткую версию без комментариев можно увидеть под спойлером
location ~* ^/google_drive/(.+)$ {
    internal;
    limit_rate 1m;
    resolver 8.8.8.8;
    
    set $download_url https://www.googleapis.com/drive/v3/files/$upstream_http_file_id?alt=media;
    set $content_disposition 'attachment; filename="$upstream_http_filename"';
    
    proxy_max_temp_file_size 0;
    proxy_set_header Authorization 'Bearer $1';
    proxy_pass $download_url;
    
    add_header Content-Disposition $content_disposition;
    
    proxy_hide_header Content-Disposition;
    proxy_hide_header Alt-Svc;
    proxy_hide_header Expires;
    proxy_hide_header Cache-Control;
    proxy_hide_header Vary;
    proxy_hide_header X-Goog-Hash;
    proxy_hide_header X-GUploader-UploadID;
}



Пишем скрипт для управления всем этим счастьем


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


<?php

# Токен для Google Drive Api.
define('TOKEN', '*****');

# ID файла на гугл диске
$fileId = 'abcdefghijklmnopqrstuvwxyz1234567890';

# Опционально, но так как мы не передаем никаких данных - почему бы и нет?
http_response_code(204);

# Зададим заголовок c ID файла (в конфигах nginx мы потом получим его как $upstream_http_file_id).
header('File-Id: ' . $fileId);
# И заголовок с именем файла (соответственно $upstream_http_filename).
header('Filename: ' . 'test.zip');
# Внутренний редирект. А еще в адресе мы передадим токен, тот самый, что мы получаем из $1 в nginx.
header('X-Accel-Redirect: ' . rawurlencode('/google_drive/' . TOKEN));

Итоги


В целом данный способ позволяет довольно легко организовать раздачу файлов пользователям с любого облачного хранилища. Да хоть из telegram или VK, (при условии, что размер файла не превышает допустимый размер у этого хранилища). У меня была идея, подобная этой, но к сожалению у меня попадаются файлы вплоть до 2гб, а способа или модуля для склейки ответов из upstream я пока не нашел, писать же какие-то врапперы для этого проекта неоправданно трудозатратно.


Спасибо за внимание. Надеюсь, моя история была хоть немного вам интересна или полезна.

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


  1. BerkutEagle
    19.07.2019 16:27

    Не рассматривали вариант монтирования диска в vps, через fuse например, и раздачи файлов как обычной статики?


    1. NiceDay Автор
      19.07.2019 17:30
      +1

      Пробовал ocamlfuse — не понравилось.
      Больше задержка и потребление CPU в районе 25% ядра на один запрос (с nginx 1-2%).


      1. avg
        19.07.2019 23:25

        Use rclone, Luke!


        1. NiceDay Автор
          20.07.2019 02:51

          Да сейчас уже поздно, текущее решение хорошо работает. Дальше интересно копнуть в сторону телеграма: попробовать дробить файлы на куски и хранить в каком-нибудь приватном канале, а потом склеивать nginx'ом, но без lua (с lua любой дурак сможет). Проблема, естественно, с последним пунктом.


  1. dchusovitin
    19.07.2019 16:58
    +1

    Статью надо было назвать «Использование X-Accel-Redirect в Nginx». PHP и fpm тут вообще ни к месту, гораздо интереснее было бы использовать Lua, либо другой язык сценариев, которые поддерживаются в Nginx. Для всего этого есть Object Storage, S3 подобные хранилища, в которых хранение 1.5 ТБ будет стоить не так уж дорого.


    1. NiceDay Автор
      19.07.2019 17:57

      Ну, тут не только X-Accel-Redirect все же.
      Да, php и fpm сугубо опциональны. Можно было хоть псевдокодом написать.


      Насчет хранилищ — да, все верно. Может, даже дешевле гуглдиска. Но тогда бы не было этой статьи :).


    1. NiceDay Автор
      19.07.2019 18:06

      Ну и гуглдиск за деньги это использую лично я. У кого студенческий, это выйдет бесплатно. У кого файлы меньше 50мб — можно любой объем (покуда совесть позволяет) хоть в телеграме хранить и по bot api раздавать, до 2гб по user api.


  1. eigrad
    19.07.2019 23:27

    Статья — ок, но открой для себя rclone serve :-).


    1. NiceDay Автор
      20.07.2019 02:37

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


  1. limassolsk
    20.07.2019 00:49

    Стоимость хранения 1TB в месяц:

    Amazon s3 — 23 usd — aws.amazon.com/ru/s3/pricing
    Scaleway (s3-совместимый) — 10 euro — www.scaleway.com/en/pricing/#object-storage
    Hetzner — 9.48 euro (incl. 20 % VAT) — www.hetzner.com/storage/storage-box
    Google Drive (2 ТБ) — 700 рублей — one.google.com/storage?i=m
    Yandex Disk — 300 рублей — disk.yandex.ru/tuning

    Спасибо за статью, не думал, что можно использовать Google Drive/Yandex Disk для раздачи файлов на сайте. Понятное дело, что для коммерческого проекта это может не очень подходить, но для домашнего — вполне норм.


    1. NiceDay Автор
      20.07.2019 02:43

      Для коммерческого проекта, я думаю, вообще не стоит. Но коммерческий проект и предполагает финансовые вложения. А у меня был оплачен гуглдиск на ближайший год и хотелось развернуться на том, что есть. И еще надо куда-то было девать любовь к экспериментам :)


    1. onlinehead
      20.07.2019 13:00
      +1

      Тут еще стоит добавить, что если прям хранить и иногда вытаскивать, можно использовать Google Cloud Coldline Storage, который в отличии от S3 Cold Storage умеет отдавать данные без предзапроса и стоит 4 доллара за терабайт (для europe-west-1 региона) + Get/Head запросы по 10 центов за 10к запросов.
      Самое неприятное — стоимость трафика, 12 центов за гигабайт исходящего. Но на фоне остальных Object Storage не так уж и плохо. Но, конечно, google drive и ya.drive дешевле.


  1. Zoomerman
    20.07.2019 02:55

    А есть данные по латенси у такого решения?


    1. NiceDay Автор
      20.07.2019 22:36

      Латенси не очень, до 5 секунд. Примерно как в интерфейсе гуглдиска. Ассеты, например, раздавать не выйдет.


    1. bewza
      21.07.2019 14:27

      Можно пропустить через Cloudflare или подобные сервисы.
      Первый запрос из зоны будет долгим, потом, из кеша CDN — достаточно быстро.
      Прогреть кеш, чтобы небыло долгого первого запроса — можно через сторонние сервисы.


  1. algotrader2013
    20.07.2019 11:26
    +1

    Интересно, а какие в принципе есть подводные камни злоупотребления гугл диском. К примеру, gsuite enterprise за $25 за юзера в месяц дает безлимитный диск каждому юзеру. Из того, что проверял, большие файлы закачиваются туда со скоростью 500Мбит/сек из европейского дата-центра, и после заливки файлов на 1ТБ скорость (пока) не упала.

    Возникает вопрос, а что, если, к примеру, приспособить его под вечное хранилище бекапов или логов, и лить по несколько ТБ каждый день? Прийдет день, когда скорость опустится до условных 56Кбит, или старые файлы начнут исчезать, или просто позвонят, и скажут, что разорвут договор в одностороннем порядке, если не прекратить?


    1. fogx
      20.07.2019 14:35

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


  1. sergey-b
    21.07.2019 06:26

    Поясните, пожалуйста, что за трюк с PHP? Почему нельзя все одним конфигом nginx-а сделать?


    1. NiceDay Автор
      21.07.2019 15:41

      Случайно ответил чуть ниже.


      1. sergey-b
        21.07.2019 15:43

        Я тоже иногда промахиваюсь с ответами. Особенно, когда с телефона комментирую.


  1. NiceDay Автор
    21.07.2019 14:26

    Точно, вот что я упустил рассказать.

    Во-первых время жизни токена Google Drive — 1 час. И нужно как-то провернуть его ротацию. На голых конфигах это сделать не получится. Можно с помощью luа или используя вот такой вот подход, как в посте, но нам нужно проверить не истекло ли время действия токена и, в случае чего, дернуть гугл, чтоб его обновить. А потом этот токен мы уже передадим nginx'y.

    Во-вторых через X-Accel-Redirect удобно делать динамические ссылки и прятать реальные id файла на гуглдиске.
    Например, мы хотим, чтоб загрузка у нас была по ссылке /dl/{long long random string}, которая проживет примерно час и только для какого-нибудь конкретного пользователя. Мы можем написать обвязку (не обязательно на php), все проверить и, в случае успеха, отдать файл через X-Accel-Redirect.

    Условно как-то так
    $condition = /*
        такой файл вообще существует
        AND пользователь имеет право скачать этот файл
        AND время действия динамической ссылки не истекло
        AND ссылка вообще запрашивалась именно этим пользователем
        AND еще куча проверок на наш вкус
    */;
        
    if (!$condition) {
        http_response_code(404);
        echo 'Не удалось найти файл' . PHP_EOL;
        exit();
    }
    
    http_response_code(204);
    header('File-Id: ' . $fileId);
    header('Filename: ' . 'test.zip');
    header('X-Accel-Redirect: ' . rawurlencode('/google_drive/' . TOKEN));


  1. knightswhosayni
    22.07.2019 14:56

    'loseless' думаю имелось ввиду 'lossless' :)


    1. NiceDay Автор
      22.07.2019 14:57

      Спасибо, поправил.


  1. SergNF
    22.07.2019 15:12

    раздачу файлов пользователям с любого облачного хранилища

    Можно с помощью «этого» подключиться к облаку mail.ru на устройстве с линукс?
    Древний arm (armv5eabi), на котором не смог (после n-ой итерации плюнул искать «старые версии всех компонентов») собрать ocamlfuse и т.п.
    Уже и не нужен fuse и т.п. — хоть как-то скачать/закачать в консоли. :(


    1. NiceDay Автор
      22.07.2019 15:19

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

      Зато их облако поддерживает webdav, можно поискать библиотеку или хоть курлом дергать, по аналогии с www.qed42.com/blog/using-curl-commands-webdav и fritool.ru/curl-for-webdav