Перейти к содержимому

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 отправляет его автоматически).
bash
curl -sS -F "file=@photo.jpg" \
  "https://steemitimages.com/<username>/<hex-signature>"

Успешный ответ:

json
{
  "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Превышен лимит частоты загрузок

Например, загрузка с неизвестным аккаунтом возвращает:

json
{
  "success": false,
  "error": {
    "name": "NoSuchAccount",
    "message": "failed to get account: account not found: [<username>]"
  }
}

Подпись загрузок ​

Подпись загрузки охватывает данные изображения:

  1. Соедините ASCII-строку ImageSigningChallenge с «сырыми» байтами изображения.
  2. Возьмите хеш SHA-256 результата.
  3. Подпишите хеш приватным ключом, авторизованным на загружающем аккаунте (ключ из posting- или active-полномочий аккаунта).
  4. Закодируйте полученную восстанавливаемую подпись в шестнадцатеричном виде — это сегмент пути :signature.

Пример с использованием dsteem в Node.js:

javascript
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 всегда отражает фактический тип изображения.

bash
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 не задан
modecover (по умолчанию)Заполнить запрошенный прямоугольник, обрезая выступающее
fitУместить в запрошенный прямоугольник, только уменьшая
formatmatch (по умолчанию)Сохранить исходный формат
jpeg, png, webpПреобразовать в указанный формат
bash
# 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, частные и внутренние сетевые адреса отклоняются, а целевые хосты ограничены политикой разрешённого списка оператора. Запрос, целевой хост которого не разрешён, завершается ошибкой:

json
{
  "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×64
  • medium — 128×128
  • large — 512×512
  • любое другое значение или отсутствие размера — ширина 128 с естественными пропорциями изображения

Если у аккаунта не задана аватарка — или его profile_image указывает на внешний хост — вместо неё отдаётся аватарка по умолчанию. Ответы с аватарками всегда используют короткое время жизни кэша (Cache-Control: public,max-age=600), чтобы смена аватарки быстро распространялась.

bash
# 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