ImageHoster
ImageHoster — сервис изображений, стоящий за приложениями Steem. Он хранит загруженные изображения, получает и преобразует изображения из одобренных источников через прокси-эндпоинт, а также отдаёт аватарки пользователей. Загруженные и проксированные изображения хранит сам сервис — данные изображений в блокчейн не записываются.
Базовый URL сервиса: https://steemitimages.com
Все примеры ниже используют этот базовый URL.
Соглашения
Эндпоинты, возвращающие JSON, при успехе отвечают
"success": true.Большинство ошибок по вине пользователя (неверный ввод, отсутствующие файлы, неудачная подпись) намеренно возвращают HTTP 200 с JSON-телом, описывающим ошибку:
json{ "success": false, "error": { "name": "ErrorCode", "message": "Human-readable message" } }Эндпоинты, возвращающие изображения, отвечают «сырыми» байтами изображения и
Content-Typeвидаimage/*.Поддерживаемые форматы изображений — JPEG, PNG, GIF и WebP.
Загрузка изображения
POST /:username/:signatureЗагружает изображение и возвращает его канонический URL.
- Запрос отправляется как
multipart/form-data; сервис читает первую файловую часть, найденную в форме (имя поля может быть любым). :username— аккаунт Steem, загружающий изображение.:signature— подпись поверх данных изображения — см. Подпись загрузок.- Требуется заголовок
Content-Length(curlотправляет его автоматически).
curl -sS -F "file=@photo.jpg" \
"https://steemitimages.com/<username>/<hex-signature>"Успешный ответ:
{
"success": true,
"url": "https://cdn.steemitimages.com/<DKEY>/<filename>"
}Возвращаемый url — каноническое расположение сохранённого изображения. Загрузки не должны превышать настроенный в сервисе максимальный размер изображения (небольшой запас поверх него для multipart-обрамления допускается), а имена файлов очищаются на стороне сервера.
Ответы с ошибками используют описанный выше JSON-контракт. Частые имена ошибок:
| Ошибка | Значение |
|---|---|
BadRequest | Некорректный multipart-запрос, отсутствующие или недопустимые заголовки, недопустимое имя файла |
LengthRequired | Отсутствует заголовок Content-Length |
PayloadTooLarge | Изображение превышает максимально допустимый размер |
FileMissing | В multipart-форме нет файловой части |
InvalidImage | Файл не является поддерживаемым типом изображения |
InvalidSignature | Проверка подписи не пройдена |
NoSuchAccount | Аккаунт Steem не найден |
Deplorable | Репутация аккаунта ниже требуемого порога |
Blacklisted | Аккаунт в чёрном списке |
QoutaExceeded | Превышен лимит частоты загрузок |
Например, загрузка с неизвестным аккаунтом возвращает:
{
"success": false,
"error": {
"name": "NoSuchAccount",
"message": "failed to get account: account not found: [<username>]"
}
}Подпись загрузок
Подпись загрузки охватывает данные изображения:
- Соедините ASCII-строку
ImageSigningChallengeс «сырыми» байтами изображения. - Возьмите хеш SHA-256 результата.
- Подпишите хеш приватным ключом, авторизованным на загружающем аккаунте (ключ из posting- или active-полномочий аккаунта).
- Закодируйте полученную восстанавливаемую подпись в шестнадцатеричном виде — это сегмент пути
:signature.
Пример с использованием dsteem в Node.js:
const dsteem = require('dsteem')
const crypto = require('crypto')
const fs = require('fs')
const [wif, file] = process.argv.slice(2)
if (!wif || !file) {
process.stderr.write(`Usage: ./sign.js <posting_wif> <file>\n`)
process.exit(1)
}
const data = fs.readFileSync(file)
const key = dsteem.PrivateKey.fromString(wif)
const imageHash = crypto.createHash('sha256')
.update('ImageSigningChallenge')
.update(data)
.digest()
process.stdout.write(key.sign(imageHash).toString() + '\n')Получение загруженного изображения
GET /:hash
GET /:hash/:filenameСкачивает ранее загруженное изображение по его ключу (:hash — ключ из ответа на загрузку). Сегмент :filename необязателен и служит лишь для читаемости — заголовок Content-Type всегда отражает фактический тип изображения.
curl -O https://steemitimages.com/DQmZi174Xz96UrRVBMNRHb6A2FfU3z1HRPwPPQCgSMgdiUT/test.jpgОтветы содержат Cache-Control: public,max-age=29030400,immutable — содержимое изображения под данным ключом никогда не меняется, поэтому клиенты и CDN могут кэшировать его агрессивно.
Проксирование и преобразование изображения
GET /p/:urlСкачивает изображение по URL-источнику, при необходимости преобразует его, кэширует результат и отдаёт его. :url — абсолютный URL-источник (HTTP/HTTPS), закодированный в Base58.
Необязательные параметры запроса:
| Параметр | Значения | Поведение |
|---|---|---|
width | положительное целое | Масштабировать до этой ширины; высота выводится с сохранением пропорций, если height не задан |
height | положительное целое | Масштабировать до этой высоты; ширина выводится с сохранением пропорций, если width не задан |
mode | cover (по умолчанию) | Заполнить запрошенный прямоугольник, обрезая выступающее |
fit | Уместить в запрошенный прямоугольник, только уменьшая | |
format | match (по умолчанию) | Сохранить исходный формат |
jpeg, png, webp | Преобразовать в указанный формат |
# Base58-encode the source URL, then request a 200px-wide PNG copy:
curl -o out.png "https://steemitimages.com/p/<base58-url>?width=200&format=png"Кэширование: преобразование, отданное из кэша, содержит Cache-Control: public,max-age=29030400,immutable; первый запрос преобразования содержит Cache-Control: public,max-age=600.
Анимированные GIF: GIF передаётся побайтово без изменений, только если он запрошен без width/height (с mode=fit и сохранением исходного формата). Любой запрос с размерами пропускает изображение через конвейер преобразований, который сводит анимированный GIF к его статичному первому кадру. Чтобы сохранить анимацию, запрашивайте GIF без параметров размера.
Политика доступа: исходящие прокси-запросы ограничены — принимаются только URL-источники HTTP/HTTPS, частные и внутренние сетевые адреса отклоняются, а целевые хосты ограничены политикой разрешённого списка оператора. Запрос, целевой хост которого не разрешён, завершается ошибкой:
{
"success": false,
"error": {
"name": "TargetHostNotAllowed",
"message": "target URL host is not allowed"
}
}Другие имена ошибок этого эндпоинта включают InvalidProxyUrl (не удалось декодировать Base58 или разобрать URL), InvalidImage (неподдерживаемый тип содержимого источника), PayloadTooLarge (изображение источника слишком велико) и NotFound (URL указывает на несуществующий ключ загрузки).
Аватарки пользователей
GET /u/:username/avatar
GET /u/:username/avatar/:sizeОтдаёт аватарку аккаунта Steem, взятую из ончейн-метаданных профиля аккаунта и переданную через тот же конвейер прокси/преобразования.
Размеры:
small— 64×64medium— 128×128large— 512×512- любое другое значение или отсутствие размера — ширина 128 с естественными пропорциями изображения
Если у аккаунта не задана аватарка — или его profile_image указывает на внешний хост — вместо неё отдаётся аватарка по умолчанию. Ответы с аватарками всегда используют короткое время жизни кэша (Cache-Control: public,max-age=600), чтобы смена аватарки быстро распространялась.
# 64x64 avatar for the @steemit account
curl -o avatar.png https://steemitimages.com/u/steemit/avatar/small
# 128px-wide avatar with natural aspect ratio
curl -o avatar.png https://steemitimages.com/u/steemit/avatar