Skip to content

Commit

Permalink
feat: optimize storage getters
Browse files Browse the repository at this point in the history
  • Loading branch information
frytg committed Dec 18, 2024
1 parent 0420014 commit 78bf945
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 27 deletions.
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://biomejs.dev/schemas/1.9.3/schema.json",
"files": {
"maxSize": 2097152,
"ignore": ["*.vue", "vendor"]
"ignore": ["*.vue", "coverage", "node_modules", "vendor"]
},
"formatter": {
"indentStyle": "tab",
Expand Down
5 changes: 5 additions & 0 deletions storage-s3/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Storage S3 Changelog

## 2024-12-18 - 0.0.1

- feat: added basic setup
140 changes: 114 additions & 26 deletions storage-s3/storage.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,64 @@
/**
* Storage module for S3 with common operations using MinIO.
* @module
*/

// import packages
import { Buffer } from 'node:buffer'
import process from 'node:process'
import { getRequiredEnv } from '@frytg/check-required-env/get'
// @deno-types="minio/dist/esm/minio.d.mts"
import type { BucketItem, BucketItemStat, BucketStream, Client } from 'minio'

// load utils
import { minioClient } from './s3.ts'

// get object from s3
export const getObject = (path: string, parseJson = false, throwError = true) =>
/**
* The MinIO client.
* @type {Client}
*/
export const client: Client = minioClient

/**
* Retrieves an object from S3.
* @param {string} path - The path to the object in S3.
* @param {object} options - The options for the operation.
* @param {boolean} options.parseJson - Whether to parse the object as JSON. Defaults to `false`.
* @param {boolean} options.throwError - Whether to throw an error if the object does not exist. Defaults to `true`.
* @param {string} options.bucketName - The name of the bucket to retrieve the object from. Defaults to env `S3_BUCKET_NAME`.
* @returns {Promise<Buffer | string | null>}
*
* @see https://min.io/docs/minio/linux/developers/javascript/API.html#getObject
*
* @example
* ```ts
* import { getObject } from '@frytg/storage-s3'
*
* const object = await getObject('path/to/object.json', { parseJson: true })
* console.log(object)
* ```
*/
export const getObject = (
path: string,
{
parseJson = false,
throwError = true,
bucketName = getRequiredEnv('S3_BUCKET_NAME'),
}: {
parseJson?: boolean
throwError?: boolean
bucketName?: string
},
): Promise<Buffer | string | object | null> =>
new Promise((resolve, reject) => {
const chunks = []
const chunks: Buffer[] = []
minioClient
.getObject(process.env.S3_BUCKET_NAME, path)
.getObject(bucketName, path)
.then((stream) => {
stream.on('data', (chunk) => chunks.push(chunk))
stream.on('end', () => {
const buffer = Buffer.concat(chunks)
if (parseJson) {
resolve(JSON.parse(buffer.toString('utf-8')))
} else {
resolve(buffer)
}
if (parseJson) resolve(JSON.parse(buffer.toString('utf-8')))
else resolve(buffer)
})
stream.on('error', reject)
})
Expand All @@ -29,41 +68,90 @@ export const getObject = (path: string, parseJson = false, throwError = true) =>
})
})

// upload object to s3
export const uploadObject = async (path: string, data) => {
/**
* Uploads an object to S3.
* @param {string} path - The path to the object in S3.
* @param {Buffer | string} data - The data to upload. Can be a Buffer or a string. If JSON is passed, it will be stringified.
* @param {object} options - The options for the operation.
* @param {string} options.bucketName - The name of the bucket to upload the object to. Defaults to env `S3_BUCKET_NAME`.
* @returns {Promise<void>}
*
* @see https://min.io/docs/minio/linux/developers/javascript/API.html#putObject
*/
export const uploadObject = async (
path: string,
data: Buffer | string,
{
bucketName = getRequiredEnv('S3_BUCKET_NAME'),
}: {
bucketName?: string
},
): Promise<void> => {
// convert data to string if it's an object or array
let dataString = data
if (typeof data === 'object' || Array.isArray(data)) {
dataString = JSON.stringify(data, null, 2)
}

// upload object
await minioClient.putObject(process.env.S3_BUCKET_NAME, path, dataString)
await minioClient.putObject(bucketName, path, dataString)
}

// check if object exists in s3
export const objectExists = async (path: string) => {
/**
* Checks if an object exists in S3.
* @param {string} path - The path to the object in S3.
* @param {object} options - The options for the operation.
* @param {string} options.bucketName - The name of the bucket to check for the object. Defaults to env `S3_BUCKET_NAME`.
* @returns {Promise<BucketItemStat | false>}
*
* @see https://min.io/docs/minio/linux/developers/javascript/API.html#statObject
*/
export const objectExists = async (
path: string,
{
bucketName = getRequiredEnv('S3_BUCKET_NAME'),
}: {
bucketName?: string
},
): Promise<BucketItemStat | Promise<false>> => {
try {
const result = await minioClient.statObject(process.env.S3_BUCKET_NAME, path)
// explicitly awaiting the result to avoid unhandled promise rejection
const result: BucketItemStat = await minioClient.statObject(bucketName, path)
return result
} catch (_error) {
return false
}
}

// convert a readable stream to a string
const readableStreamToString = (stream: ReadableStream) => {
return new Promise((resolve, reject) => {
const chunks = []
// convert a readable stream to a string (for listObjects)
const readableStreamForListObjects = (stream: BucketStream<BucketItem>): Promise<BucketItem[]> =>
new Promise((resolve, reject) => {
const chunks: BucketItem[] = []
stream.on('data', (chunk) => chunks.push(chunk))
stream.on('end', () => resolve(chunks))
stream.on('error', reject)
})
}

// list objects in s3 with v2
export const listObjects = async (prefix: string, recursive = false) => {
const result = await minioClient.listObjectsV2(process.env.S3_BUCKET_NAME, prefix, recursive)
const data = await readableStreamToString(result)
return data
/**
* Lists objects in S3.
* @param {string} prefix - The prefix to filter the objects by.
* @param {object} options - The options for the operation.
* @param {boolean} options.recursive - Whether to list recursively. Defaults to `false`.
* @param {string} options.bucketName - The name of the bucket to list the objects from. Defaults to env `S3_BUCKET_NAME`.
* @returns {Promise<BucketItem[]>}
*
* @see https://min.io/docs/minio/linux/developers/javascript/API.html#listObjectsV2
*/
export const listObjects = async (
prefix: string,
{
recursive = false,
bucketName = getRequiredEnv('S3_BUCKET_NAME'),
}: {
recursive?: boolean
bucketName?: string
},
): Promise<BucketItem[]> => {
const result: BucketStream<BucketItem> = await minioClient.listObjectsV2(bucketName, prefix, recursive)
return readableStreamForListObjects(result)
}

0 comments on commit 78bf945

Please sign in to comment.