Раздаем файлы с Google Drive посредством nginx +29


Предыстория


Так уж случилось, что нужно мне было где-то хранить более 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 я пока не нашел, писать же какие-то врапперы для этого проекта неоправданно трудозатратно.


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




К сожалению, не доступен сервер mySQL