Задача
СкопированоРано или поздно практически перед каждым разработчиком встаёт задача использования сервиса коротких ссылок. Чаще всего такой сервис нужен для публикации постов в социальных сетях или сбора маркетинговой статистики. У разработчика есть два пути — использовать готовый сервис или написать свой. Я выбираю второй вариант!
Описанная в рецепте серверная часть позволяет реализовать такой сервис, клиентская часть всё ещё остаётся за вами. Для генерации ссылки вам нужно будет отправить запрос на сервер, в ответ вам вернётся сгенерированная короткая ссылка.
Использовать я буду Nginx. Подробнее об этом веб-сервере вы можете почитать в статье «Веб-сервер Nginx».
Задача, которая стоит передо мной, — генерация короткого варианта ссылки на главную страницу с набором GET-параметров. Страница лежит на моём же домене. Пример:
- Длинная ссылка:
https
.: / / mysite . dev / ? source = twitter&site = doka&p = docker - Короткая ссылка:
https
.: / / mysite . dev / get / 72cbb2d3
Готовое решение
СкопированоУстановим модуль NJS для Nginx по инструкции из документации. Подробнее о модуле можно почитать в официальной документации или в статье «Магия вне Хогвартса: NJS».
Включим модуль NJS и подключим нужный скрипт, добавив соответствующие строки в основной файл конфигурации:
load_module modules/ngx_http_js_module.so;http { js_import scripts/short_link.js;}
load_module modules/ngx_http_js_module.so; http { js_import scripts/short_link.js; }
Добавим строчки в конфигурацию для вашего сайта Nginx:
server { location / { error_page 404 /404/index.html; try_files $uri $uri/ /index.html =404; } location /add { js_content short_link.add; } location /get { js_content short_link.get; }}
server { location / { error_page 404 /404/index.html; try_files $uri $uri/ /index.html =404; } location /add { js_content short_link.add; } location /get { js_content short_link.get; } }
Добавим в поддиректорию scripts в директории nginx следующий скрипт:
const fs = require('fs')// Формирует длинную ссылку на основе аргументов GET-запросаfunction getLink(r) { // Формирование аргументов GET-запроса const arguments = []; for (const key in r.args) { arguments.push(`${key}=${r.args[key]}`) } // Можно использовать любой адрес, такая ссылка для примера return `https://mysite.dev/?${arguments.join('&')}`}// Сохраняет короткую ссылку в файлasync function add(r) { const link = getLink(r) const hash = await crypto.subtle.digest('SHA-512', JSON.stringify(r.args)) const shortHash = Buffer.from(hash).toString('hex').slice(0, 8) const path = `/links/${shortHash}.json` // Формирует заголовок для ответа r.headersOut['Content-Type'] = "application/json charset=utf-8" try { // Если файл уже есть (ссылка уже была сформирована), то возвращает соответствующий ответ fs.accessSync(path, fs.constants.R_OK | fs.constants.W_OK) r.return(200, `{ "status": "Already exists", "hash": "${shortHash}" }`) } catch (e) { // Запись ссылки в файл const json = `{ "url": "${link}" }` fs.writeFileSync(`${path}`, json) // Возврат хэш-части короткой ссылки в ответе на запрос r.return(201, `{ "status": "Created", "hash": "${shortHash}" }`) }}// Преобразует короткую ссылку в длинную и перенаправляет браузер по нейasync function get(r) { // Открывает файл со ссылкой const filePath = `/links/${r.uri.replace('/get/', '')}.json` const file = fs.readFileSync(filePath, { encoding: 'utf8' }) const json = JSON.parse(file) // Переадресация браузера r.return(301, json.url)}export default { add, get}
const fs = require('fs') // Формирует длинную ссылку на основе аргументов GET-запроса function getLink(r) { // Формирование аргументов GET-запроса const arguments = []; for (const key in r.args) { arguments.push(`${key}=${r.args[key]}`) } // Можно использовать любой адрес, такая ссылка для примера return `https://mysite.dev/?${arguments.join('&')}` } // Сохраняет короткую ссылку в файл async function add(r) { const link = getLink(r) const hash = await crypto.subtle.digest('SHA-512', JSON.stringify(r.args)) const shortHash = Buffer.from(hash).toString('hex').slice(0, 8) const path = `/links/${shortHash}.json` // Формирует заголовок для ответа r.headersOut['Content-Type'] = "application/json charset=utf-8" try { // Если файл уже есть (ссылка уже была сформирована), то возвращает соответствующий ответ fs.accessSync(path, fs.constants.R_OK | fs.constants.W_OK) r.return(200, `{ "status": "Already exists", "hash": "${shortHash}" }`) } catch (e) { // Запись ссылки в файл const json = `{ "url": "${link}" }` fs.writeFileSync(`${path}`, json) // Возврат хэш-части короткой ссылки в ответе на запрос r.return(201, `{ "status": "Created", "hash": "${shortHash}" }`) } } // Преобразует короткую ссылку в длинную и перенаправляет браузер по ней async function get(r) { // Открывает файл со ссылкой const filePath = `/links/${r.uri.replace('/get/', '')}.json` const file = fs.readFileSync(filePath, { encoding: 'utf8' }) const json = JSON.parse(file) // Переадресация браузера r.return(301, json.url) } export default { add, get }
Все файлы со ссылками будут лежать в папке /links, не забудьте выставить ей правильные права доступа. Нужно:
sudo chown -R nginx:nginx /linkssudo chmod -R 755 /links
sudo chown -R nginx:nginx /links sudo chmod -R 755 /links
Пример сформированного автоматически файла 07b0c246.json с информацией о переадресации короткой ссылки:
{ "url": "https://mysite.dev/article=docker" }
{ "url": "https://mysite.dev/article=docker" }
Разбор решения
СкопированоКонструирование ссылок
СкопированоКакая задача сразу встаёт перед разработчиком при создании сервиса коротких ссылок? Нужно выбрать способ создания коротких ссылок. Можно использовать заранее заготовленные короткие ссылки. Например, для своей статьи на Доке можно сделать ссылку https
. Такой подход позволяет сделать ссылки понятными человеку, что очень удобно. Однако перечень «разумных» названий будет быстро исчерпан, необходимо заранее продумать о масштабировании. Зато такой подход легко реализовать на уровне сервера обычным перенаправлением. Для Nginx перенаправление можно прописать прямо в секции server
файла конфигурации:
...server { rewrite ^/docker https://doka.guide/tools/docker/ permanent;}
... server { rewrite ^/docker https://doka.guide/tools/docker/ permanent; }
Второй подход — генерировать ссылки автоматически при создании. В этом случае необходимо определиться с количеством ссылок, на которое вы можете рассчитывать:
- выбираете количество необходимых вам комбинаций (например, 1000);
- выбираете набор символов, которые вы будете использовать, исходя из кодировки или иных соображений (например, цифры, то есть 10 символов);
- выбираете нужное количество разрядов, которые понадобятся для перекрытия диапазона, исходя из формулы: количество символов из набора возводим в степень количества разрядов (у нас получается 3).
Здесь используется формула из комбинаторики для выборки с возвращением. Например, у нас используется два разряда. Для каждого разряда мы можем использовать два символа — 0 или 1. Это значит, что для одного символа старшего разряда мы можем использовать два символа в младшем разряде и для второго — два. Следовательно, общее количество комбинаций, которые мы можем использовать — четыре. Если разрядов становится три, то таких комбинаций становится в два раза больше, то есть восемь. Иными словами мы можем использовать формулу возведения в степень, где в показателе степени будет количество разрядов, а в основании — количество доступных символов. В коде на JavaScript для случая десяти символов и трёх разрядов это можно выразить так:
const N = 10 * 10 * 10 = 10 ** 3
const N = 10 * 10 * 10 = 10 ** 3
Затем генерируете для каждой ссылки нужное количество символов из набора, озаботившись предварительно проверкой на уникальность. Проверку на уникальность можно заменить генерацией хеша по запрашиваемой ссылке.
Подходы к реализации
СкопированоЕсли вы — единственный человек, который генерирует ссылки, ссылок у вас немного, а генерировать эти ссылки нужно не часто, вполне может подойти способ с ручной настройкой сервера.
Если пользователей много, и/или вы хотите генерировать ссылки автоматически, код придётся написать. И сразу возникает вопрос, где хранить информацию о ссылках? Можно в оперативной памяти, можно в файлах, можно в базе данных. Первый вариант очень ненадёжен, его рассматривать не будем. Третий вариант требует настройки базы данных и написания кода для работы с ней, давайте его тоже опустим.
Ссылки можно хранить в одном файле в виде, например, JSON-объекта или простых строчек в формате «ключ-значение». Ссылки можно хранить в отдельных директориях или файлах. Мой вариант — использовать отдельные файлы в формате JSON.
В отличие от варианта с базой данных, есть определённые ограничения на количество поддиректорий, которые могут быть созданы внутри директории (например, для ext4 максимальное количество — 65535). Также есть ограничение на максимальное количество файлов на диске (для ext4 максимальное количество ограничено ~4 миллиардами). Внимательно следите, чтобы не выйти за пределы диапазонов.