@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-совместимого APIcredentials— опционально: ключи доступа. Если не указаны, 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 правил на бакет; лимит задает провайдер, а не адаптер.
