Інфраструктура · медіа

keepix.cloud — медіа-хостинг, який повертає готові адреси

Медіа-сервіс для застосунків, вітрин і маркетплейсів: один підписаний запит на вході — іменовані WebP-варіанти та драбинка HLS на виході, віддача з власного піддомену клієнта і вічний кеш. Мультиорендність, потарифна тарифікація до сотих цента, метадані знімаються на прийманні, а стирання всього, що стосується людини, відповідає числом. Спроєктували й побудували цілком — від конвеєра приймання до кабінету.

Категорія
Інфраструктура · медіа
Оцінка вартості
від €24 000
Терміни
≈ 10–14 тижнів
keepix.cloudkeepix.cloud
Головна сторінка проєкту — keepix.cloud

01Огляд проєкту

keepix.cloud — медіа-хостинг для продуктів, яким доводиться віддавати чужі файли: каталожні фотографії вітрини, картинки у відгуках маркетплейсу, відеоогляди в застосунку. Оригінал іде одним підписаним запитом, і у відповіді вже лежать готові адреси: іменований набір WebP-варіантів для картинки, драбинка HLS для відео. Файли віддаються з власного піддомену клієнта — читач бачить його бренд і не бачить нас.

Задум у тому, що медіа — не функція, яку пишуть один раз на проєкт. Кожен продукт, де щось завантажують, зрештою пише те саме наново: масштабування, зміну формату, схему імен, політику кешу, знімання метаданих, шлях «видалити все про цю людину». Написане вп'яте — вп'яте написане погано, а те, що ламається (витекла GPS-мітка, SVG, який виявився скриптом), ламається мовчки: без помилки й без рядка в журналі.

Тому сервіс побудований навколо однієї думки: адресу оголошує сервіс, а клієнт її зберігає, а не обчислює. Саме це рішення прибирає найчастіший спосіб зламати медіа — сторінку, яка відмальовується бездоганно, а на місці товару має дірку.

Ми спроєктували й побудували продукт цілком: конвеєр приймання, модель варіантів, відеодрабинку, мультиорендність із власними доменами та ключами підпису, потарифну тарифікацію, кабінет, публічний договір API і документацію. Нижче — яке завдання це розв'язує, як воно влаштоване і які інженерні рішення ми ухвалили б так само.

02Контекст і завдання

Зберегти завантажену картинку легко. Тримати медіа-шлях правильним роками, під навантаженням, для кількох продуктів і на чужих файлах — ні. Коли ми розібрали завдання, граблі вилізли швидко.

  • Довільна ширина — пастка. Адреса на кшталт ?w=473 має вигляд гнучкої, а насправді дає дві біди одразу: кеш, який ніколи не прогрівається, бо кожна сторінка просить трохи інше число, і безкоштовний спосіб зайняти процесор, попросивши тисячу ширин поспіль.
  • Обчислена адреса ламається мовчки. Правило «id → шард → шлях», зашите в код клієнта, живе рівно до першої зміни розкладки на тому боці. Після неї сторінка так само відмальовується, а в товару просто дірка замість фотографії — ані помилки, ані запису в журналі.
  • Метадані — витік, якого ніхто не помічає. Координати чужого дому в EXIF фотографії з відгуку не піднімають помилки, не ламають сторінку й не доходять до моніторингу. Це витік рівно тоді, коли хтось подивиться.
  • SVG — не картинка. Це виконувана розмітка. Прийнятий як зображення й відданий з домену клієнта, скрипт усередині нього виконається від імені цього домену.
  • Видалення має бути доказовим. Запит на стирання, на який відповіли позначкою «видалено» в рядку, поки байти лежать на диску, — це видалення, якого не було.
  • Відео — це набір, а не файл. Одне завантаження зобов'язане стати придатною до відтворення драбинкою, кадром-постером і запасним варіантом, і нічого з цього не має відбуватися всередині запиту, який прийняв файл.
  • Рахунок має бути чесним на малих числах. Гігабайт може коштувати менше за цент; округлення до цілих центів з'їдає весь потарифний рахунок і перетворює його на вигадку.

Була й ще одна вимога, яка визначила весь задум: сервіс не повинен ходити в інтернет узагалі. Медіа-хостинг, який тягне картинку з чужої адреси за клієнта, знає, хто і що попросив, і передає це знання тому CDN, яким скористався. keepix не знає жодної сторонньої адреси — і це властивість коду, а не обіцянка на сторінці тарифів.

03Цілі проєкту

Завдання перетворилося на список цілей, яких ми трималися на кожному кроці:

  • Один запит на вхід — готові адреси на вихід. Клієнт надсилає оригінал і одразу отримує адреси, які треба зберегти: без другого виклику й без очікування завдання для картинок.
  • Іменований і скінченний набір варіантів. Три ширини для картинок і чотири форми для відео, оголошені для кожного виду файлу, — щоб кеш був теплим, а процесор не здавався в оренду.
  • Власний домен клієнта. Файли віддаються з img.його-домен, сервісу в адресному рядку не видно.
  • Кеш, який можна тримати вічно. В адресі стоїть хеш вмісту, тому заміна файлу змінює адресу й доходить до читача одразу.
  • Безпека, вбудована у шлях. Тип визначається розбором, антивірус відмовляє, а не пропускає, кожна картинка перекодовується, метадані знімаються, SVG не приймається й не віддається.
  • Доказове стирання. Один запит за власним owner_ref клієнта і відповідь, яка називає число об'єктів і файлів, що пішли.
  • Мультиорендність зі справжньою ізоляцією. Власні домени, власні види й варіанти, власні стелі, два ключі підпису, щоб ротація не була простоєм.
  • Потарифний рахунок, який можна перевірити. Зберігання середнім, а не піком, трафік — реально відданими байтами, операції — поштучно, з розбивкою по днях у кабінеті.
  • API, який визнає, що зробив. Кожна відповідь каже, що побудовано і що пропущено, а для перевірки перенесення є суха звірка пачками.

04Що ми зробили

keepix розбирається на кілька частин, у кожної одна робота:

  • Приймання. Один підписаний ендпоінт приймає оригінал з його видом, ключем і списком потрібних варіантів. Картинки повертаються готовими в тій самій відповіді, відео йде в чергу — і відповідь про це каже.
  • Конвеєр обробки. Визначення типу, антивірус, перекодування, знімання метаданих, збирання варіантів — у фіксованому порядку, який не можна пропустити параметром запиту.
  • Віддача. Окремий хост без сесій, без JavaScript і без жодного Set-Cookie: файли віддає вебсервер прямо з диска, з річним незмінним кешем.
  • API орендаря. Підпис HMAC-SHA256, ендпоінти на отримання адрес, звіряння пачками, вивантаження опису, видалення об'єкта і стирання всього, що належить людині.
  • Кабінет. Теки, перетягування мишею, пошук за назвою, описом і мітками — і витрати з розбивкою по днях. Працює повністю без JavaScript: скрипт додає зручність, а не можливість працювати.
  • Тарифікація. Пакет плюс потарифні витрати понад нього, у сотих цента, причому на безкоштовному тарифі приймання зупиняється, а не починає мовчки витрачати гроші.
  • Сток. Шар-маркетплейс, де орендарі купують і продають свої роботи, на тому самому сховищі й тій самій віддачі.

Далі — про рішення всередині цих частин: ті, які дорого змінювати потім.

05Архітектура рішення

Сервіс — це три простори з фізично різними входами, і цей поділ тримає весь задум:

  • Публічна віддача — API немає взагалі. Файл шукає на диску вебсервер. Не знайшов — 404, а не «зберу на льоту». Виконуваного коду на цьому шляху немає зовсім, і саме тому хост віддачі може нести default-src 'none'; sandbox усерйоз.
  • Приватний API орендаря. Кожен запит підписаний; підпис накриває метод, шлях разом із рядком запиту, час і хеш тіла. Підписати шлях без запиту означало б залишити kind і key поза підписом — підмінними, і ніхто б цього не помітив.
  • Адмінський API для панелі керування — на своєму вході й зі своїми обліковими даними.

Відповіді завжди у JSON і завжди англійською, з одним формулюванням на код відмови: та сама поломка читається однаково в журналі будь-якого клієнта, хоч би якою мовою говорив його продукт.

Усередині сховище ділиться на холодне дерево оригіналів, яке назовні не віддається ніколи, і віддаване дерево варіантів. Зберігання оригіналу — це те, що дозволяє зібрати новий варіант через рік, не просячи клієнта завантажити файл наново; і це ж робить правило «ми не віддаємо надісланий файл» абсолютним, а не «як правило».

06Життя файлу

Файл проходить фіксовану послідовність, і порядок тут — це і є сенс:

  • Тип визначається розбором — за вмістом, а не за розширенням і не за тим, що сказав клієнт. Розширення вихідного файлу призначаємо ми.
  • Антивірус працює і може відмовити. Якщо сканер недоступний, приймання відповідає 503, а не пропускає файл. Пропустити мовчки, поки перевірка лежить, — це брехня про зроблену роботу, і така брехня зринає значно пізніше.
  • Картинка перекодовується. Жоден байт надісланого файлу назовні не йде.
  • Метадані не переживають перекодування. EXIF і GPS зникають через будову шляху, а не тому, що той, хто завантажував, згадав їх зняти.
  • Збираються варіанти, і їхні адреси повертаються в тій самій відповіді.

SVG не приймається і не віддається — ніколи, ані на вході, ані на виході. Перевірка йде двічі, за оголошеним типом вмісту і за початком файлу, бо для SVG визначник типу часто просто каже «текст». Назовні йдуть WebP, JPEG, PNG, MP4, плейлист HLS і його сегменти; усе інше — відмова 415.

Приймання ідемпотентне: той самий sha256 під тим самим ключем не робить роботу наново й повертає ті самі адреси. Повтор після обірваного з'єднання тому безкоштовний — а це важливо, коли файл на два гігабайти, а мережа — телефон.

07Іменовані варіанти замість довільної ширини

У картинок три іменовані ширини — thumb 400, card 1000, full 1400, усі WebP якості 82, — і варіант просять на ім'я. Набір скінченний і оголошений для кожного виду файлу. Це рішення, на яке спирається весь сервіс: воно прибирає цілий клас проблем, а не одну помилку.

Два правила описують, коли варіанта законно може не бути, і обидва існують, щоб не брехати читачеві:

  • Ми не розтягуємо. Якщо оригінал вужчий за ширину варіанта, варіант лишається в розмірі оригіналу, і відповідь чесно про це каже: у полях w і h стоять справжні числа. Розтягнута копія має вигляд нашої провини — і була б нею.
  • Однакові копії не зберігаються двічі. Якщо дві ширини дадуть рівно той самий файл, друга адреса не створюється. Два побайтово однакові файли за двома адресами — це два завантаження того самого на чийсь телефон.

Звідси випливає, що відповідь треба читати, а не припускати: попросили два варіанти — можна отримати один, з блоком skipped і поясненням. Ми зробили це нормальною очікуваною відповіддю, а не краєм: інтеграція, написана під оптимістичну форму, ламається на першій же маленькій картинці, яку завантажить користувач.

Додати новий варіант пізніше можна без повторного завантаження: він збирається з оригіналу в холодному дереві. Заради цього оригінал і зберігається.

08Відео: драбинка HLS, постер і превʼю

Одне завантаження відео перетворюється на набір, бо саме набір потрібен програвачу: драбинка HLS до трьох щаблів якості (640, 1280 і 1920 px), постер — кадр із 10% тривалості, беззвучне превʼю перших секунд для наведення й автовідтворення у стрічці та прогресивний MP4 як запасний варіант для старих клієнтів.

Щабель ширший за оригінал не робиться — з тієї самої причини, з якої не розтягується картинка: збільшена копія має вигляд нашої провини. Постер береться з десятої частки тривалості, а не з першого кадру, бо перший кадр справжнього відео дуже часто чорний.

Обробка відео не відбувається всередині запиту, що прийняв файл: вона йде в чергу, і відповідь про це каже. Відповідь приймання чесна щодо того, що готове зараз, а що в роботі, — клієнт може зберегти готове й спитати пізніше, а не тримати екран завантаження користувача на часі перекодування.

09Власний домен і вічний кеш

Файли віддаються з власного піддомену орендаря — img.його-домен, — який він спрямовує на сервіс A-записом. Читач бачить його бренд і його адресу і не бачить keepix узагалі. На безкоштовному тарифі домен спільний; обидва шляхи працюють однаково.

Доменів в орендаря може бути скільки завгодно, але канонічний рівно один, і це обмеження навмисне: за двох канонічних в одного файлу з'являються дві адреси, тобто два кеші й ті самі байти, завантажені двічі на телефон одного читача.

Заголовки віддачі однакові для всіх орендарів і не змінюються: річний immutable-кеш, nosniff, default-src 'none'; sandbox, дозвільний CORS на читання і жодного Set-Cookie. Вічний кеш безпечний саме тому, що в адресі стоїть хеш вмісту: змінилися байти — змінилася адреса, і замінена картинка доходить до читача одразу, а не після закінчення TTL.

Саме тому адресу не можна обчислити на боці клієнта. Її повертає сервіс, клієнт кладе її поруч із записом, до якого файл належить, і питає наново через resolve, якщо запис загубився.

10Приймання: тип, антивірус, метадані

На сервісі лежать файли, які надіслали люди: фотографія у відгуку, відеоогляд. Правила приймання написані заради них, а не заради форми, і кожне існує тому, що альтернатива відмовляє тихо.

Визначення типу розбором, а не за розширенням, — не педантизм: розширення це єдине, що той, хто завантажує, контролює повністю. Відмова за недоступного сканера коштує доступності й купує гарантію, що «перевірено» означає перевірено. Перекодування кожної картинки означає, що підготовлений файл ніколи не доїде до читача в початковому вигляді. Знімання метаданих усередині перекодування означає, що про нього не можна забути.

На боці віддачі гарантії структурні, а не процедурні. У хоста віддачі немає ні сесій, ні JavaScript, ні виконуваного коду на шляху; вміст іде з політикою безпеки, за якої браузер не запустив би нічого, навіть якби небезпечне все-таки пройшло. Два незалежні шари — бо цікаві поломки це ті, де перший шар був налаштований неправильно і ніхто не помітив.

Журнал віддачі з адресами читачів живе 24 години. Довше він сервісу ні для чого не потрібен, а отже, тримати його довше — просте накопичення чужих слідів.

11Стирання всього, що стосується людини

У кожного об'єкта є owner_ref — те, як сам орендар називає власника файлу. У каталожних картинок він порожній: вони не про людину. У фотографій у відгуках і користувацького відео він обов'язковий.

Один запит стирає все під ним, і відповідь називає число: скільки об'єктів і скільки файлів пішло. Заради цього числа ендпоінт і існує. Гейт персональних даних має перевіряти виконання, а не намір: рядок із позначкою «видалено» за живих байтів на диску — це видалення, якого не було, і воно так і не відбудеться, поки хтось не подивиться.

Стирання зносить записи й байти одразу, а не за розкладом, і забирає з собою відкладені копії попередніх версій. Це ж робить keepix придатним для продукту з власними зобов'язаннями щодо персональних даних: код видалення на його боці може викликати стирання прямо в потоці й прочитати число, а не сподіватися, що до цього дійде якесь нічне завдання.

З ключами підпису те саме. В орендаря завжди два в роботі, бо за одного ключа ротація означає простій, а простій — причина, з якої ротацію відкладають назавжди. Секрет показується один раз при випуску й у панель не повертається: можливість підписати запит — це право писати в чуже сховище.

12Підписаний API і звіряння

Кожен приватний запит несе чотири заголовки — орендар, ідентифікатор ключа, час і підпис, — а підпис це HMAC-SHA256 за методом, повним шляхом із рядком запиту, часом і хешем тіла. Вікно — 300 секунд. Однаковий підписаний запит, повторений тієї самої секунди, дістає відмову як повтор: це захист, що працює як задумано, і договір каже про це прямо, а не залишає інтегратору з'ясовувати самому.

Набір ендпоінтів невеликий, і кожен відповідає на справжнє експлуатаційне питання:

  • put — прийняти оригінал; put-variant — прийняти вже готову копію без обробки, для переїзду з тим, що в клієнта є. Перекодувати готове вдруге — втрачати якість просто так.
  • resolve і resolve-batch (до 1000 ключів) — які адреси є в цього ключа. Це інструмент лагодження: загубився запис — питають, а не вгадують.
  • reconcile — суха перевірка, до 1000 записів за раз, нічого не змінює і відповідає по кожному запису ok / missing / mismatch / pending плюс відбиток усієї пачки.
  • inventory — вивантаження, яке веде звірку у зворотний бік, список сервісу проти списку клієнта, з курсором за самим ключем, щоб сторінка не з'їжджала, якщо щось приймається під час обходу.
  • object, erase, quota, ping і непідписаний healthz, який каже, що фронт живий, і нічого більше — ані версії, ані стану бази.

Звіряння заслуговує на окреме слово, бо саме воно виправдовує весь договір. Після великого перенесення значення має рівно одне питання: чи все доїхало. Відповідати на нього по одному файлу — це стільки запитів, скільки файлів, тобто ніхто цього не робить; і тоді перенесення оголошують закінченим на підставі того, що скрипт завантаження не впав. Звіряння пачками робить чесну відповідь достатньо дешевою, щоб її справді отримали.

13Кабінет, витрати й тарифікація

Кабінет — це бібліотека без жодного рядка коду з боку користувача: теки, перетягування мишею, пошук за назвою, описом і мітками. Він працює повністю без JavaScript — скрипт додає зручність, а не можливість працювати. Це не ностальгія, а найдешевший спосіб гарантувати, що панель лишиться працездатною на поганому зв'язку і перевірною без браузерного рушія.

Витрати рахуються з початку місяця й розбиваються по днях: скільки зберігання, скільки трафіку віддачі й скільки операцій припало на кожну дату, скільки пакета з'їдено і що прийшло понад нього. Розбивка важливіша за підсумок — рахунок, який не можна віднести до дня, це рахунок, з яким не можна сперечатися.

Три рішення в тарифікації варто назвати:

  • Зберігання — середнє за період, а не пік. Один день із великим завантаженням не повинен оцінювати весь місяць.
  • Трафік віддачі — ті самі байти, що пішли читачам, із журналу сервера, а не «розмір файлу × кількість запитів»: друге завищує кожен діапазонний запит і кожне покинуте завантаження.
  • Ставки тримаються в сотих цента. Гігабайт може коштувати менше за цент; округлення до цілих центів з'їло б увесь потарифний рахунок і тихо перетворило б рахунок на вигадку.

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

14Технологічний стек

Стек обраний під сервіс, у якого гарячий шлях — це вебсервер, що віддає файл:

  • PHP 8.4 для API і панелі — зрілий, достатньо швидкий на цьому шляху і дешевий в експлуатації, що важливо для продукту з безкоштовним тарифом.
  • Окремий хост віддачі, що віддає прямо з диска, без прикладного коду на шляху запиту.
  • WebP для картинок із фіксованою якістю, H.264 + AAC в HLS для відео і прогресивний MP4 як запасний.
  • Черга для відео, щоб перекодування ніколи не сиділо всередині запиту на завантаження.
  • Підпис HMAC-SHA256 із двома живими ключами на орендаря і вікном у 300 секунд.
  • Холодне дерево оригіналів поза шляхом віддачі — те, що й уможливлює нові варіанти та перезбирання пізніше.

Сервіс навмисне консервативний у залежностях. Він не робить вихідних запитів, не користується чужим CDN і не зберігає про користувачів клієнта нічого, крім орендаря, ключа, виду, кількості байтів і варіантів. Кожна залежність, якої немає, — це залежність, яка не зламає вам медіа у вівторок.

15Дизайн і UX

У публічного сайту було одне завдання: зробити інфраструктурний продукт читабельним для інженера, який його підключатиме. Тому на першому екрані стоїть справжній запит і справжня відповідь — адреси, що повертаються у JSON, — а не абстракція замість них. Інтегратор вирішує секунд за тридцять, чи підходить йому медіа-сервіс, і вирішує він за формою відповіді.

Решта сайту написана так само. Кожна можливість названа разом зі своїм обмеженням: іменовані варіанти — і чому відмовлено в довільній ширині; вічний кеш — і хеш в адресі, який робить його безпечним; стирання — і число, яким воно відповідає. Документація, що перелічує лише можливості, залишає читачеві з'ясовувати краї в бою.

У кабінеті той самий принцип перетворюється на роботу без JavaScript і на розбивку витрат по днях: на екрані немає нічого, чого читач не може перевірити, і нічого, що перестає працювати, якщо скрипт не завантажився.

16Як ми працювали

Проєкт ішов етапами, кожен закінчувався чимось показовим:

  • Спочатку договір. Ми написали договір API — схему підпису, ендпоінти, коди відмови, форми відповідей — до реалізації: договір це те, під що пишуть клієнти, і те, що дорого змінювати.
  • Приймання і варіанти. Конвеєр у фіксованому порядку, з двома правилами про нерозтягування й недублювання, вбудованими від самого початку.
  • Віддача. Окремий хост, набір заголовків, адресація за хешем вмісту, що робить вічний кеш безпечним.
  • Відео. Драбинка, постер і превʼю, і черга, яка тримає перекодування поза запитом завантаження.
  • Мультиорендність. Домени, види, варіанти, стелі й модель двох ключів.
  • Тарифікація і кабінет. Потарифний рахунок із розбивкою по днях, а поверх нього — бібліотека.
  • Справжнє перенесення. Ми перевезли на сервіс живий каталог і звірили його пачками — так ендпоінт звіряння й отримав свою форму.

Останній етап коштував більше за будь-який обсяг внутрішнього тестування. Перенесення сотень тисяч справжніх файлів знайшло те, чого не знаходить набір тестів: об'єкти, які сервіс прийняв і загубив, записи з розбіжною збереженою адресою і різницю між перевіркою, що звіряє хеші, і перевіркою, що лише підтверджує наявність рядка. Перевірка, якій нема чого перевіряти, відповідає «все зійшлося» — тому слабкий режим у нас називає себе вголос.

17Результат

Вийшов медіа-хостинг, на який продукт може спертися: один підписаний запит на вході, готові адреси на виході, віддача з домену клієнта і кеш, який можна тримати вічно.

1 запит
оригінал усередину — готові адреси назад
0
вихідних запитів: сервіс не знає жодної сторонньої адреси
24 год
стільки живе журнал віддачі з адресами читачів
  • Іменовані варіанти й драбинка HLS, зібрані один раз і кешовані вічно за адресою з хешем вмісту.
  • Власний піддомен клієнта: читач бачить його бренд, а не наш.
  • Безпека, вбудована у шлях приймання — розібраний тип, антивірус, що відмовляє, перекодування, зняті метадані, SVG відкинуто з обох боків.
  • Стирання, що відповідає числом, — гейт персональних даних перевіряє виконання, а не намір.
  • Мультиорендність із власними доменами, стелями й ротацією двох ключів без простою.
  • Потарифний рахунок у сотих цента з розбивкою по днях і безкоштовний тариф, який зупиняється, а не витрачає.

18Висновки

keepix — випадок, коли більша частина інженерії пішла в рішення, чого сервіс робити відмовиться. Жодних довільних ширин, жодних обчислюваних адрес, жодного SVG, жодних вихідних запитів, жодних початкових байтів на шляху віддачі, жодного рахунку на безкоштовному тарифі. Кожне з цих «ні» прибирає цілий клас поломок, а не обробляє одну.

Головний урок, який ми понесемо в наступний інфраструктурний проєкт: робіть чесну відповідь дешевою. Звіряння пачками, витрати по днях, стирання, що повертає число, відповідь приймання, яка називає пропущене, — в усіх випадках альтернативою була не хибна відповідь, а відсутність відповіді, і саме на відсутність команди тихо погоджуються.

Якщо вам потрібен медіа-сервіс під власним доменом, конвеєр файлів, який зобов'язаний бути безпечним із чужими завантаженнями, або API-first продукт, де договір важливіший за екрани, — таку роботу ми робимо від початку до кінця.

Потрібен схожий продукт?

Розкажіть про завдання — запропонуємо архітектуру та оцінку. Консультація безкоштовна.

На які запити відповідає ця сторінка

розробка медіа-хостингу на замовлення, створити власний image cdn, сервіс ресайзу зображень розробка, api варіантів webp, хостинг hls відео для застосунку, зберігання картинок на власному домені, мультиорендне медіа-сховище розробка, підписаний api завантаження hmac, знімання exif під час завантаження, svg завантаження вразливість, видалення файлів користувача gdpr api, вічний кеш картинок хеш вмісту, конвеєр перекодування відео розробка, обробка користувацьких завантажень, потарифна тарифікація saas розробка, keepix cloud, keepix відгуки, keepix api документація, скільки коштує розробка медіа-хостингу, альтернатива cdn для картинок маркетплейсу.