npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@opsregistry/adapter-s3

v0.0.6

Published

Runtime adapter for S3-compatible object storage (Yandex Object Storage, Timeweb S3, etc.)

Readme

@opsregistry/adapter-s3

Runtime-адаптер для S3-совместимых объектных хранилищ (Yandex Object Storage, Timeweb S3, MinIO и любых других S3-совместимых провайдеров).

Пакет реализует операции opsregistry:

  • storage.object.uploadFile — загрузка одного файла (S3 PutObject)
  • storage.object.uploadStream — загрузка stream без сборки всего файла в памяти
  • storage.object.uploadMultipartStream — multipart-загрузка stream для крупных файлов
  • storage.object.getStream — чтение объекта stream-ом
  • storage.object.head — metadata объекта без чтения тела
  • storage.object.deleteFile — удаление одного файла (S3 DeleteObject)
  • storage.object.list — список объектов в бакете (S3 ListObjectsV2)
  • storage.website.put — запись static website configuration бакета (S3 PutBucketWebsite)
  • storage.website.get — чтение static website configuration бакета (S3 GetBucketWebsite)
  • storage.website.delete — удаление static website configuration бакета (S3 DeleteBucketWebsite)
  • syncDir — утилита синхронизации локальной директории с бакетом (композитная операция)

Установка

bun add @opsregistry/adapter-s3

Использование

Подключение

import {
  uploadFile,
  uploadStream,
  uploadMultipartStream,
  getObjectStream,
  headObject,
  deleteFile,
  listObjects,
  syncDir,
  putBucketWebsite,
  getBucketWebsite,
  deleteBucketWebsite,
} from "@opsregistry/adapter-s3";
import type { S3BucketRef } from "@opsregistry/contracts/storage/object-storage-common";

Конфигурация бакета

Все операции принимают S3BucketRef — описание целевого бакета:

const bucketRef: S3BucketRef = {
  bucket: "my-bucket",
  region: "ru-central1",
  endpointUrl: "https://storage.yandexcloud.net",
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY_ID!,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
  },
  keyPrefix: "storefront/", // опционально: префикс для всех ключей
};

Параметры:

  • bucket — имя бакета
  • region — регион (например ru-central1 для Yandex Object Storage, ru-1 для Timeweb S3)
  • endpointUrl — URL endpoint S3-совместимого API
  • credentials — опционально: ключи доступа. Если не указаны, S3Client использует цепочку провайдеров AWS SDK (env, IAM и т.д.)
  • keyPrefix — опционально: префикс, автоматически добавляемый ко всем ключам объектов

uploadFile

Загрузка одного файла в бакет:

const result = await uploadFile({
  bucketRef,
  targetKey: "images/photo.jpg",
  fileContent: "...", // строка или Uint8Array/Buffer
  options: {
    contentType: "image/jpeg",
    cacheControl: "public, max-age=31536000",
    // S3 object-level redirect. AWS S3 supports it, but some
    // S3-compatible providers reject this header.
    websiteRedirectLocation: "/new-path/photo/",
    metadata: {
      "x-source": "my-app",
    },
  },
});

console.log(result);
// { operationCode: 'storage.object.uploadFile', key: 'storefront/images/photo.jpg', etag: '"abc123"' }

uploadStream

Загрузка AsyncIterable<Uint8Array> через обычный S3 PutObject. Подходит для объектов умеренного размера, когда провайдер принимает streaming body.

import { createReadStream } from "node:fs";

const result = await uploadStream({
  bucketRef,
  targetKey: "cad/products/42/model/rev_3.sldasm",
  body: createReadStream("/tmp/rev_3.sldasm"),
  options: {
    contentType: "application/octet-stream",
  },
});

console.log(result);
// { operationCode: 'storage.object.uploadStream', key: 'storefront/cad/products/42/model/rev_3.sldasm', etag: '"abc123"' }

uploadMultipartStream

Multipart-загрузка AsyncIterable<Uint8Array>. Helper режет входной stream на parts, держит в памяти только текущий part, завершает multipart upload при успехе и вызывает abort при ошибке.

const result = await uploadMultipartStream({
  bucketRef,
  targetKey: "cad/products/42/model/rev_4.sldasm",
  body: createReadStream("/tmp/rev_4.sldasm"),
  options: {
    contentType: "application/octet-stream",
    partSize: 64 * 1024 * 1024,
    onProgress: (event) => {
      if (event.type === "part:done") {
        console.log(`part ${event.partNumber} uploaded`);
      }
    },
  },
});

console.log(result);
// { operationCode: 'storage.object.uploadMultipartStream', key: 'storefront/cad/products/42/model/rev_4.sldasm', etag: '"..."', parts: 3, byteLength: 123456789, uploadId: '...' }

getObjectStream И headObject

const head = await headObject({
  bucketRef,
  key: "cad/products/42/model/rev_4.sldasm",
});
console.log(head.contentLength, head.contentType);

const object = await getObjectStream({
  bucketRef,
  key: "cad/products/42/model/rev_4.sldasm",
});

// object.body — stream/body из AWS SDK. В Node runtime обычно это Readable.

Static website configuration

Запись настроек static website hosting для бакета:

await putBucketWebsite({
  bucketRef,
  configuration: {
    indexDocument: "index.html",
    errorDocument: "404.html",
    routingRules: [
      {
        condition: {
          keyPrefixEquals: "old/catalog/",
          httpErrorCodeReturnedEquals: "404",
        },
        redirect: {
          httpRedirectCode: "301",
          replaceKeyPrefixWith: "new/catalog/",
        },
      },
      {
        condition: {
          keyPrefixEquals: "old/product.html",
        },
        redirect: {
          httpRedirectCode: "301",
          replaceKeyWith: "new/product/",
        },
      },
    ],
  },
});

Чтение и удаление website configuration:

const current = await getBucketWebsite({ bucketRef });
await deleteBucketWebsite({ bucketRef });

routingRules — стандартная S3 website feature, но S3-совместимые провайдеры отличаются по полноте поддержки. Например, object-level websiteRedirectLocation может быть недоступен, даже если bucket-level website routing rules работают.

deleteFile

Удаление одного файла из бакета:

const result = await deleteFile({
  bucketRef,
  key: "images/photo.jpg",
});

console.log(result);
// { operationCode: 'storage.object.deleteFile', key: 'storefront/images/photo.jpg', deleted: true }

listObjects

Получение списка объектов в бакете:

const result = await listObjects({
  bucketRef,
  options: {
    prefix: "images/", // фильтр по префиксу
    maxKeys: 50, // макс. количество (по умолчанию 100)
    continuationToken: "...", // для пагинации
  },
});

console.log(result);
// {
//   operationCode: 'storage.object.list',
//   objects: [
//     { key: 'storefront/images/photo.jpg', size: 1024, etag: '"abc"', lastModified: '2025-01-01T00:00:00.000Z', storageClass: 'STANDARD' }
//   ],
//   nextContinuationToken: '...', // если isTruncated === true
//   isTruncated: false
// }

syncDir (утилита)

Синхронизация локальной директории с бакетом. Комбинирует listObjects + uploadFile + deleteFile:

const result = await syncDir(bucketRef, "/path/to/local/dist", {
  deleteRemote: true, // удалять файлы из бакета, которых нет локально
  include: /\.(html|css|js|png)$/, // только эти расширения
  exclude: /\.map$/, // исключить source maps
  onProgress: (event) => {
    switch (event.type) {
      case "scan:start":
        console.log("Сканирование...");
        break;
      case "scan:local":
        console.log(`Найдено ${event.total} локальных файлов`);
        break;
      case "upload:start":
        console.log(`Загрузка ${event.key}...`);
        break;
      case "upload:done":
        console.log(`✅ ${event.key} загружен`);
        break;
      case "skip:done":
        console.log(`⏭ ${event.key} не изменился`);
        break;
      case "delete:start":
        console.log(`Удаление ${event.key}...`);
        break;
      case "complete":
        console.log(
          `Готово: ${event.uploaded} загружено, ${event.skipped} пропущено, ${event.deleted} удалено`,
        );
        break;
    }
  },
});

console.log(result);
// { uploaded: 42, skipped: 120, deleted: 3 }

syncDir пропускает загрузку неизмененных файлов. Для сравнения используется размер объекта и ETag из ListObjectsV2: для обычных PutObject-загрузок S3-совместимые хранилища возвращают в ETag MD5 содержимого. Если ETag не похож на MD5, например у multipart-объекта, файл будет загружен заново, потому что сравнение только по размеру небезопасно.

Примеры провайдеров

Yandex Object Storage

const yandexBucket: S3BucketRef = {
  bucket: "my-bucket",
  region: "ru-central1",
  endpointUrl: "https://storage.yandexcloud.net",
  credentials: {
    accessKeyId: "YCA...",
    secretAccessKey: "YCM...",
  },
};

Timeweb S3

const timewebBucket: S3BucketRef = {
  bucket: "my-bucket",
  region: "ru-1",
  endpointUrl: "https://s3.timeweb.com",
  credentials: {
    accessKeyId: "...",
    secretAccessKey: "...",
  },
};

MinIO (локальный)

const minioBucket: S3BucketRef = {
  bucket: "my-bucket",
  region: "us-east-1",
  endpointUrl: "http://localhost:9000",
  credentials: {
    accessKeyId: "minioadmin",
    secretAccessKey: "minioadmin",
  },
};

Важно

  • Секреты передаются только в runtime и не должны попадать в registry.
  • Ключи доступа храните в .env приложения-потребителя.
  • keyPrefix удобно использовать для разделения окружений в одном бакете: staging/, production/.
  • syncDir читает файлы как Buffer, поэтому подходит для статических сайтов с изображениями, шрифтами и favicon.
  • Адаптер по умолчанию использует forcePathStyle: true для совместимости со всеми S3-провайдерами (включая MinIO и Timeweb). Если провайдеру нужен virtual-hosted style, передайте forcePathStyle: false в bucketRef.
  • keyPrefix нормализуется автоматически: new, /new и new/ превращаются в один безопасный префикс new/.
  • При deleteRemote: true удаляются только объекты внутри keyPrefix; адаптер проходит все страницы ListObjectsV2, а не только первые 1000 объектов.
  • Для static website routing rules у многих S3 API действует лимит около 50 правил на бакет; лимит задает провайдер, а не адаптер.