RE:NODE

Провайдер загрузок Strapi для S3: свой endpoint и path-style

Автор Команда RE:NODE
18 мин чтения


Чтобы отправлять загрузки Strapi в S3-совместимое хранилище, которое не принадлежит Amazon, установите @strapi/provider-upload-aws-s3, укажите provider: 'aws-s3' в config/plugins.js и задайте в providerOptions.s3Options четыре вещи: учётные данные, название региона, endpoint хранилища и forcePathStyle: true. Добавьте params.Bucket для бакета и baseUrl для адреса, с которого браузеры будут загружать файлы. Вот и вся настройка. Почти все сбои после этого происходят в одном из трёх мест: стиль адресации (SDK строит имя хоста, которого не существует), публичный URL (файлы загружаются, но не отображаются) или список управления доступом, который отправляется с каждой загрузкой (хранилище его отклоняет). В этой статье мы настроим провайдер как следует, разберём каждый ключ, а затем пройдём по ошибкам в том порядке, в котором вы, скорее всего, на них наткнётесь.

Предполагается, что Strapi уже работает. Об остальных решениях для продакшена - секреты, база данных, сборка админки, обновления - читайте в статье Strapi в продакшене, где тот же провайдер описан в одном разделе. Эта статья - длинная версия того раздела.

Что делает провайдер загрузок#

Плагин Upload в Strapi - это код, на котором стоят Media Library и endpoint /api/upload. Сам плагин файлы не хранит. Он передаёт каждый файл провайдеру - небольшому пакету с тремя задачами: загрузить буфер, загрузить поток и удалить. Провайдер по умолчанию пишет в public/uploads на диске сервера. Провайдер S3 пишет вместо этого в бакет.

Путь файла важен для диагностики, поэтому держите его в голове:

  1. Редактор перетаскивает файл в Media Library. Браузер отправляет его в Strapi, а не в бакет.
  2. Strapi принимает multipart-запрос, читает файл и для изображений создаёт дополнительные размеры (варианты thumbnail_, small_, medium_ и large_ в зависимости от ваших breakpoints).
  3. Для каждого файла и каждого варианта провайдер отправляет из процесса Strapi на endpoint один запрос S3 PutObject.
  4. Strapi сохраняет получившийся публичный URL в своей базе данных, в поле url записи о файле.
  5. Когда страница или админка показывает изображение, браузер загружает этот URL напрямую из хранилища. Strapi в этом пути не участвует.
multipart uploadPutObjectfile URL in the APIGET baseUrl/keyРедакторMedia LibraryStrapiprovider aws-s3ПосетительбраузерS3 endpointpath-style
Загрузки идут через Strapi, чтение - напрямую из хранилища

Из этого следуют два вывода. Во-первых, для загрузок не нужна настройка CORS на бакете, потому что браузер в него никогда не пишет; CORS важен, только если вы позже добавите прямые загрузки из браузера через presigned URL (см. presigned URL в S3). Во-вторых, серверу со Strapi нужен доступ к endpoint хранилища, а вашим посетителям - к тому адресу, на который указывает baseUrl. Это могут быть два разных адреса, и главное в настройке - не перепутать их.

Что нужно от хранилища#

Прежде чем трогать Strapi, соберите пять значений из вашего S3-совместимого хранилища:

ЗначениеПримерДля чего
URL endpointhttps://s3.example.comendpoint
Ключ доступа и секретAKIA... и длинная строкаcredentials
Имя бакетаstrapi-mediaparams.Bucket
Название регионаus-east-1region
Публичный базовый URLhttps://s3.example.com/strapi-mediabaseUrl

Регион - то, на чём чаще всего застревают. Amazon использует его, чтобы выбрать дата-центр и построить подпись запроса. У хранилища не от Amazon выбирать регион не из чего, но SDK всё равно требует значение, потому что подпись включает регион. Правило такое: берите то, что указало хранилище, а если оно ничего не говорит, на большинстве работает любое корректное название вроде us-east-1. Важно, чтобы везде, где вы обращаетесь к хранилищу, использовалась одна и та же строка. Почему так, объясняет статья S3 path-style и virtual-hosted URL и что такое регионы.

Сначала проверьте значения чем-то другим, а не Strapi. Если AWS CLI не может показать содержимое бакета, Strapi им тоже не управится, а CLI выдаёт куда более понятную ошибку. Скажите CLI использовать адресацию path-style и выведите список:

bash
$ aws configure set default.s3.addressing_style path$ export AWS_ACCESS_KEY_ID=...$ export AWS_SECRET_ACCESS_KEY=...$ aws --endpoint-url https://s3.example.com --region us-east-1 s3 ls s3://strapi-media/

Пустой бакет ничего не выводит и завершается без ошибки - это успех. Неверный ключ даёт InvalidAccessKeyId или SignatureDoesNotMatch; неверный бакет - NoSuchBucket. Полный набор команд, если захотите пойти дальше, есть в статье S3-хранилище и AWS CLI.

Установка провайдера#

Из папки проекта Strapi:

bash
$ npm install @strapi/provider-upload-aws-s3

Несмотря на название, это провайдер для любого S3-совместимого сервиса. Отдельного «универсального S3»-пакета от Strapi нет. Существуют community-провайдеры для конкретных вендоров, но для endpoint под вашим контролем лучше официальный, потому что он следует за релизами Strapi. Мажорная версия провайдера должна совпадать с мажорной версией Strapi; пакет публикуется с тем же версионированием, что и сам Strapi, а провайдер от более старой мажорной версии - частая причина запутанных ошибок после обновления. Проверьте установленную версию командой npm ls @strapi/provider-upload-aws-s3 и сравните её с версией @strapi/strapi в package.json.

Конфигурация, ключ за ключом#

Создайте или отредактируйте config/plugins.js (или config/plugins.ts в проекте на TypeScript - структура та же, только с export default):

config/plugins.js
module.exports = ({ env }) => ({  upload: {    config: {      provider: 'aws-s3',      providerOptions: {        baseUrl: env('S3_PUBLIC_URL'),        rootPath: env('S3_ROOT_PATH', ''),        s3Options: {          credentials: {            accessKeyId: env('S3_ACCESS_KEY_ID'),            secretAccessKey: env('S3_SECRET_ACCESS_KEY'),          },          region: env('S3_REGION', 'us-east-1'),          endpoint: env('S3_ENDPOINT'),          forcePathStyle: true,          params: {            Bucket: env('S3_BUCKET'),          },        },      },      actionOptions: {        upload: {},        uploadStream: {},        delete: {},      },    },  },});

Имена внутри env() вы выбираете сами; структура вокруг них неизменна. Вот что делает каждый ключ:

КлючЧто делает
providerДолжен быть 'aws-s3', короткое имя пакета
providerOptions.baseUrlАдрес, который хранится в базе и используется браузерами
providerOptions.rootPathПрефикс, чтобы несколько приложений делили один бакет
s3Options.credentialsКлюч доступа и секрет: accessKeyId и secretAccessKey
s3Options.regionНужен SDK, даже если хранилище его игнорирует
s3Options.endpointURL хранилища; без него SDK обращается к Amazon
s3Options.forcePathStyleБакет в пути, а не в имени хоста
s3Options.params.BucketБакет, в который попадает каждый объект
actionOptionsДополнительные параметры для каждого вызова upload, uploadStream или delete

Всё, что лежит в s3Options, передаётся в конструктор S3Client из AWS SDK for JavaScript v3, поэтому имена совпадают с документацией самого SDK. Это полезно знать: в s3Options можно положить любую опцию, которую принимает клиент SDK, в том числе опции контрольных сумм, о которых речь пойдёт ниже.

О rootPath стоит сказать отдельно. Без него объекты ложатся в корень бакета с сгенерированным ключом вроде thumbnail_cat_4f2a1b.jpg. С rootPath: 'cms' ключ становится cms/thumbnail_cat_4f2a1b.jpg. Его стоит задать, если бакет делится с чем-то ещё, и в первый же день это ничего не стоит. Позднее изменение не переносит существующие объекты, а сохранённые URL продолжают указывать на старые ключи.

Переменные окружения#

Держите учётные данные не в файле конфигурации, а в .env в папке проекта, рядом с секретами Strapi:

.env
S3_ENDPOINT=https://s3.example.comS3_REGION=us-east-1S3_BUCKET=strapi-mediaS3_ACCESS_KEY_ID=...S3_SECRET_ACCESS_KEY=...S3_PUBLIC_URL=https://s3.example.com/strapi-mediaS3_ROOT_PATH=

Никогда не коммитьте этот файл. Strapi читает .env при запуске, поэтому после правки перезапустите сервер; пересборка админки не нужна, потому что эти значения использует серверный процесс, а не клиентский бандл. О привычках, которые не дают .env попасть в репозиторий, рассказывает статья переменные окружения и секреты. Если вы размещаете Strapi на RE:NODE, файловый менеджер и SFTP открывают папку проекта, так что файл можно править в браузере - см. SFTP и файловый менеджер.

Одна деталь, на которой спотыкаются: пустое значение вроде S3_ROOT_PATH= - это пустая строка, и env('S3_ROOT_PATH', '') возвращает её как есть. Здесь это нормально. Но пустой S3_ENDPOINT= - тоже пустая строка, а SDK считает это отсутствием endpoint и тихо отправляет запросы в Amazon. Если загрузки падают с сообщением InvalidAccessKeyId от хранилища, которое никогда не слышало о вашем ключе, проверьте, что переменная действительно задана и названа так, как её читает конфиг.

Почему path-style и что бывает без него#

Получив endpoint и бакет, AWS SDK строит один из двух URL. Virtual-hosted, по умолчанию, помещает бакет в имя хоста: https://strapi-media.s3.example.com/cat.jpg. Path-style помещает его в путь: https://s3.example.com/strapi-media/cat.jpg. Virtual-hosted требует DNS-записи для каждого имени бакета и сертификата, который их все покрывает. У хранилища, которое работает за одним именем хоста, нет ни того ни другого, поэтому значение SDK по умолчанию даёт имя, которое не разрешается.

Сбой узнаваем. Если forcePathStyle отсутствует или равен false, то при первой загрузке в логе Strapi будет что-то такое:

code
error: getaddrinfo ENOTFOUND strapi-media.s3.example.com

Посмотрите на имя хоста в сообщении. Если оно начинается с имени вашего бакета, SDK использует virtual-hosted, и исправление - forcePathStyle: true в s3Options. Если имя хоста просто s3.example.com, а ошибка другая, адресация верна и проблема в другом. Эта одна проверка убирает половину догадок.

Есть и более тонкий вариант. Хранилище с wildcard-DNS и wildcard-сертификатом может принимать virtual-hosted запросы, и загрузки заработают без этого параметра. Если позже вы перейдёте на хранилище без этого, та же конфигурация сломается. Задавайте forcePathStyle: true в любом случае; path-style работает на любом S3-совместимом хранилище, в том числе у Amazon, который по-прежнему принимает его для существующих бакетов.

baseUrl: почему файлы загружаются, но не отображаются#

Провайдер пишет объекты через endpoint, но URL, который он сохраняет в базе, строится из baseUrl. Они разделены намеренно: адрес, на который вы пишете, часто не тот, с которого вы хотите отдавать файлы миру. CDN, прокси с более красивым именем хоста или публичный путь на том же хосте - всё это обычные случаи.

Если baseUrl не задан, провайдер строит URL по представлению SDK о расположении бакета. С собственным endpoint и адресацией path-style это обычно endpoint плюс бакет, и это может оказаться верным, но может быть и адресом, доступным только серверу Strapi, например внутренним или по простому HTTP. Явный baseUrl убирает гадание.

При адресации path-style в базовом URL есть бакет: https://s3.example.com/strapi-media. Сохранённый URL - это база плюс ключ, так что файл с ключом thumbnail_cat_4f2a1b.jpg отдаётся с https://s3.example.com/strapi-media/thumbnail_cat_4f2a1b.jpg. Откройте этот адрес в приватном окне браузера, без учётных данных, и посмотрите, загрузится ли изображение. Эта единственная проверка разделяет три типичных сбоя:

  • Браузер предупреждает о смешанном содержимом или блокирует изображение. Админка работает по HTTPS, а сохранённый URL - обычный http://. Браузеры блокируют HTTP-изображения на HTTPS-странице. Используйте в baseUrl имя с HTTPS.
  • URL возвращает `403` или `AccessDenied`. Объект существует, но анонимное чтение запрещено. См. следующий раздел.
  • URL возвращает `404` или `NoSuchKey`. URL не совпадает с ключом. Проверьте rootPath и то, есть ли имя бакета в baseUrl.

У Media Library есть и четвёртый, специфичный для Strapi вариант сбоя: файлы открываются в новой вкладке, но миниатюры в админке сломаны. Это собственная Content Security Policy Strapi отказывается загружать изображения с хоста, о котором ей не сказали, и исправление лежит в config/middlewares.js. В руководстве по Strapi в продакшене есть точный блок strapi::security с img-src и media-src; добавьте в оба хост вашего baseUrl.

Публичные и приватные файлы, ACL#

По умолчанию провайдер загружает каждый объект с ACL public-read, благодаря чему ссылки baseUrl работают для анонимных посетителей. Для картинок маркетингового сайта это правильное поведение. Для всего, что не должно быть публичным, оно неверно, и хранилище к тому же может его отклонить.

ACL - это заголовок с правами на отдельный объект, который отправляется вместе с PutObject. Amazon отходит от них: новые бакеты Amazon по умолчанию блокируют ACL и опираются на политики бакета. Некоторые S3-совместимые хранилища их вообще не реализовали и отвечают на загрузку с ACL ошибкой AccessControlListNotSupported или общим NotImplemented. Если загрузки падают с жалобой на ACL, а учётные данные в порядке, причина в этом. Провайдер читает ACL из params, так что управлять им можно там:

config/plugins.js (the s3Options params block)
params: {  ACL: 'private',  signedUrlExpires: 15 * 60,  Bucket: env('S3_BUCKET'),},

С ACL: 'private' объекты нельзя читать анонимно, и провайдер переходит на подписанные URL: когда Strapi возвращает файл в ответе API, он подписывает URL с ограниченным сроком и возвращает его вместо обычного. signedUrlExpires - срок жизни в секундах. Две практические заметки:

  • Подписанные URL истекают, поэтому страница, которая кэширует ответ API дольше, чем signedUrlExpires, покажет сломанные изображения. Согласуйте время жизни кэша со временем жизни подписи или оставьте бакет публичным для ресурсов, которые относятся к публичным страницам.
  • Подпись охватывает имя хоста. Подписанный URL строится из endpoint, поэтому этот endpoint должен быть доступен из браузера посетителя. Внутренний адрес, который работает для сервера Strapi, подпишет URL, которые никто другой не откроет. Почему так, объясняют presigned URL в S3.

Умеет ли конкретное хранилище делать бакет или путь читаемым без ACL, определяет само хранилище, а не Strapi: одни используют политики бакета, у других есть переключатель «public» на бакет, у третьих нет ни того ни другого. Выясните, что поддерживает ваше, и проверьте анонимный доступ командой curl -I на загруженном файле, прежде чем говорить редакторам, что всё готово. То же относится к хранилищу, которое отклоняет ACL: если вы не можете отправить ACL public-read и не можете задать политику, рабочий путь - подписанные URL с ACL: 'private'. Точное поведение при null или отсутствующем ACL смотрите в README провайдера для вашей установленной версии, потому что между релизами оно менялось.

Контрольные суммы загрузок в новых версиях SDK#

Провайдер построен на AWS SDK for JavaScript v3. С начала 2025 года SDK по умолчанию добавляет к загрузкам контрольную сумму на основе CRC. Некоторые S3-совместимые серверы не принимают эти дополнительные заголовки, и сбой выглядит странно: Media Library нормально показывает объекты, нормально их удаляет, а каждая загрузка падает с SignatureDoesNotMatch, MissingContentLength или жалобой на контрольные суммы или x-amz-content-sha256.

У клиента SDK есть две опции, которые сводят новое поведение к «только когда операция этого требует». Поскольку s3Options передаётся прямо в клиент, они лежат там:

config/plugins.js (inside s3Options)
s3Options: {  credentials: { /* ... */ },  region: env('S3_REGION', 'us-east-1'),  endpoint: env('S3_ENDPOINT'),  forcePathStyle: true,  requestChecksumCalculation: 'WHEN_REQUIRED',  responseChecksumValidation: 'WHEN_REQUIRED',  params: { Bucket: env('S3_BUCKET') },},

Добавляйте их, только если видите этот симптом: они есть в релизах SDK начиная с начала 2025 года, а более старый SDK неизвестные опции игнорирует. Если установленный провайдер тянет более старый SDK, чище обновить провайдер. Та же проблема встречается и у клиента командной строки, и у других SDK, а S3 из Node.js и Python показывает эквивалентные настройки там. Полезный признак: если aws s3 cp работает с этим endpoint, а Strapi нет, прежде всего сравните версии CLI и SDK.

Проверка и как понять, что произошло на самом деле#

Когда конфигурация на месте, перезапустите Strapi и загрузите одно изображение через Media Library. Проверяйте по порядку:

  1. Лог сервера. Запустите Strapi в терминале или читайте консоль в панели. Любая ошибка провайдера пишется в момент загрузки, с настоящим сообщением. Не верьте общему уведомлению «Internal Server Error» в админке.
  2. Бакет. Выведите его список через CLI. Вы должны увидеть оригинал и, для изображений, варианты thumbnail_, small_, medium_ и large_, каждый отдельным объектом. Используйте aws --endpoint-url https://s3.example.com s3 ls s3://strapi-media/, как раньше.
  3. URL. Нажмите на файл в Media Library, скопируйте его ссылку и откройте в приватном окне.
  4. Удаление. Удалите тестовый файл в админке и снова выведите список. Вызов delete провайдера удаляет оригинал и все варианты; если объекты остались, скорее всего, у учётных данных нет права на удаление, а это легко упустить, если ключ создавали только для записи.

Шаг 4 - тот, который люди пропускают. Ключ, который может писать, но не удалять, оставляет в бакете «сирот», которых никто не видит, пока их не покажут счёт или квота.

Перенос уже существующих файлов#

Смена провайдера ничего не переносит. Файлы, загруженные раньше, по-прежнему имеют в базе URL /uploads/... и по-прежнему лежат в public/uploads. Есть три честных варианта:

  • Оставить как есть. Держите public/uploads на диске сервера, а новые файлы отправляйте в бакет. Старые ссылки продолжают работать, пока папка на месте. Это самый безопасный выбор, и для небольшой библиотеки он подходит.
  • Скопировать и переписать. Синхронизируйте public/uploads с бакетом инструментом вроде rclone или AWS CLI (см. rclone и S3-хранилище), затем обновите url (и URL по форматам в JSON-колонке formats) в таблице files на новую базу. Это изменение базы данных, поэтому сначала сделайте backup Strapi и проверьте на копии. Настоящие названия колонок берите из своей схемы, а не из руководства.
  • Экспорт и импорт. Команды strapi export и strapi import переносят контент и, с нужными флагами, медиа. Это поддерживаемый способ переноса между окружениями, но перед тем как полагаться на него при миграции, проверьте, что импорт в вашей версии пишет через настроенный провайдер.

Что бы вы ни выбрали, делайте это в тихий час и держите backup и базы, и public/uploads, пока не посмотрите на результат в админке. Миграция, проверенная только загрузкой нового изображения, старые не проверила.

Как держать бакет и базу в согласии#

Когда загрузки живут в бакете, база и бакет становятся двумя вещами, которые должны совпадать. Backup одного без другого даёт либо записи без файлов, либо файлы без записей.

  • Backup Strapi (копия базы или strapi export) хранит ссылки. Изображений в нём нет.
  • Копия бакета хранит изображения. Какие из них используются, она не знает.
  • Восстанавливайте оба из одного дня или смиритесь с тем, что часть записей будет указывать на отсутствующие файлы.

У бакета есть и своя надёжность, о которой стоит подумать. Если хранилище держит одну копию в одном месте, это не backup чего-либо, а основная копия, и ей нужен собственный backup. О том, как это рассчитать, читайте в правиле 3-2-1 для S3-хранилища, а о способе его снять - в restic и backup в S3.

Strapi с тарифом S3 на RE:NODE#

На RE:NODE Strapi - приложение в один клик на странице хостинга приложений, оно устанавливается и собирается при первом запуске сервера, с данными в SQLite на сервере. Тариф объектного хранилища S3 - это отдельный сервер, который даёт то, что нужно этой статье: S3-совместимый endpoint, ключ доступа и секретный ключ, сгенерированные для этого сервера, и уже созданный первый бакет. Он использует адресацию path-style и принимает любое название региона, поэтому forcePathStyle: true и регион вроде us-east-1 - правильные значения. Endpoint отвечает по простому HTTP на своём порту, а HTTPS идёт через слот прокси тарифа: направьте имя хоста на показанный адрес, и сертификат будет выпущен и продлеваться сам. Используйте это имя с HTTPS в endpoint и baseUrl, иначе админка заблокирует изображения как смешанное содержимое.

В тарифы S3 слоты backup не входят, а хранилище - это одна копия на NVMe в одном месте, без репликации. Для медиа, которые можно воссоздать, этого достаточно, а всё незаменимое стоит копировать ещё куда-нибудь. Слоты backup сервера Strapi покрывают папку проекта и файл SQLite, но не бакет. Работает ли анонимное чтение бакета и как его включить, эта статья обещать за вас не может; проверьте загруженный файл в приватном окне, как описано выше.

Диагностика#

СимптомВероятная причина
ENOTFOUND bucket.hostforcePathStyle отсутствует или стоит не там
InvalidAccessKeyId от вашего хранилищаEndpoint пуст, запрос ушёл в Amazon
NoSuchBucketНеверный params.Bucket или бакет не создан
AccessControlListNotSupportedХранилище отклоняет ACL; задайте ACL: 'private'
SignatureDoesNotMatch только при загрузкеНовые контрольные суммы по умолчанию; см. раздел о них
SignatureDoesNotMatch вездеНеверный секрет или прокси переписал заголовок Host
Загрузки работают, миниатюры сломаныimg-src в config/middlewares.js
Изображения блокируются в админкеbaseUrl с http:// при HTTPS-админке
413 при загрузкеЛимит тела запроса на обратном прокси, не Strapi и не хранилище
403 на публичном URLОбъект не читается публично

Несколько замечаний, которые не помещаются в таблицу. SignatureDoesNotMatch на каждом запросе значит, что учётные данные неверны или подпись ломается по дороге; обычный виновник по дороге - обратный прокси перед хранилищем, который не передаёт исходный заголовок Host, ведь подпись его охватывает. Если это происходит только при загрузках, думайте о контрольных суммах раньше, чем об учётных данных.

Что до 413, лимит - это настройка того прокси, который стоит перед Strapi. Загрузки из админки идут через него. Увеличьте там размер тела и подумайте о собственном sizeLimit Strapi в config плагина upload, чтобы редакторы получали понятное сообщение, а не оборванное соединение.

Если Strapi не запускается после изменения, с сообщением, что провайдер не удаётся загрузить, значит, пакет не установлен в той папке, из которой работает Strapi: выполните npm install там, а не на своём ноутбуке, и перезапустите. И помните: изменение config/plugins.js требует перезапуска, но и изменения Content Security Policy админки в config/middlewares.js тоже требуют перезапуска; пересборка не нужна ни в том, ни в другом случае.

FAQ#

Нужно ли настраивать CORS на бакете для загрузок Strapi?

Нет. Загрузки идут из браузера в Strapi и из Strapi в бакет, так что браузер в бакет не пишет и CORS не применяется. CORS нужен, только если вы добавите прямые загрузки из браузера по presigned URL или будете загружать из бакета шрифты или данные canvas.

Какое название региона использовать для хранилища не от Amazon?

То, которое указано в документации хранилища. Если там ничего нет, используйте us-east-1. SDK нужно какое-то значение для подписи запросов, а хранилище не от Amazon принимает то, что от него ждут. Держите одну и ту же строку в Strapi, CLI и всех остальных клиентах.

Можно ли использовать провайдер S3 с непубличным бакетом?

Да. Задайте ACL: 'private' в s3Options.params, и провайдер будет возвращать подписанные URL, которые истекают через signedUrlExpires секунд. Кэшируйте ответы API на меньшее время и убедитесь, что endpoint, которым подписываются URL, доступен из браузера посетителя.

Почему загрузки работают, а миниатюры в Media Library остаются пустыми?

Content Security Policy Strapi разрешает изображения только с собственного origin и из короткого списка хостов. Добавьте хост вашего baseUrl в img-src и media-src в config/middlewares.js и перезапустите.

Перенесёт ли переход на S3 мои существующие медиа?

Нет. Существующие файлы сохраняют URL /uploads/... и остаются на диске сервера. Скопируйте их в бакет и обновите сохранённые URL либо оставьте как есть и пусть только новые загрузки идут в бакет.

Удаляет ли Strapi файлы из бакета, когда я удаляю их в админке?

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

Это было полезно?