Роздаємо файли з Google Drive за допомогою nginx

Передісторія

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

Дякую за увагу. Сподіваюся, моя історія була хоч трохи вам цікава чи корисна.

Джерело: habr.com

Додати коментар або відгук