Skip to content

Commit

Permalink
fix: @storagebus/s3 tests and types
Browse files Browse the repository at this point in the history
  • Loading branch information
fox1t committed Oct 25, 2023
1 parent d95ea6c commit a169fd3
Show file tree
Hide file tree
Showing 10 changed files with 641 additions and 524 deletions.
30 changes: 13 additions & 17 deletions packages/s3/package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
{
"name": "@ducktors/storagebus-s3",
"name": "@storagebus/s3",
"version": "0.11.4",
"description": "",
"license": "MIT",
"author": "Ducktors <[email protected]> (https://ducktors.dev)",
"private": false,
"files": [
"dist"
],
"files": ["dist"],
"type": "commonjs",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js"
"typescript": "./src/s3.ts",
"import": "./dist/s3.js",
"require": "./dist/s3.js"
}
},
"main": "dist/index.js",
Expand All @@ -22,27 +21,24 @@
"lint": "biome check .",
"preinstall": "npx only-allow pnpm",
"prepublish": "pnpm build",
"test": "tsx --test test/*.ts",
"test:ci": "c8 --all --src src --reporter lcov --reporter text tsx --test ./test/*.ts",
"test:watch": "tsx --watch --test ./test/*.ts"
"test": "tsx --conditions=typescript --test test/*.ts",
"test:ci": "c8 --all --src dist --reporter lcov --reporter text tsx --test ./test/*.ts",
"test:watch": "tsx --conditions=typescript --watch --test ./test/*.ts"
},
"keywords": [],
"dependencies": {
"@aws-sdk/abort-controller": "^3.272.0",
"@aws-sdk/client-s3": "^3.281.0",
"@aws-sdk/lib-storage": "^3.281.0",
"@ducktors/storagebus-abstract": "workspace:*",
"mime-types": "^2.1.35",
"tslib": "^2.5.0"
"@aws-sdk/client-s3": "^3.421.0",
"@aws-sdk/lib-storage": "^3.421.0",
"@storagebus/storage": "workspace:*",
"tslib": "^2.6.2"
},
"devDependencies": {
"@biomejs/biome": "^1.2.2",
"@ducktors/tsconfig": "^1.0.0",
"@types/mime-types": "^2.1.1",
"@types/node": "^20.6.5",
"aws-sdk-client-mock": "^2.0.1",
"c8": "^8.0.1",
"rimraf": "^5.0.1",
"s3rver": "^3.7.1",
"tsx": "^3.13.0",
"typescript": "^5.2.2"
}
Expand Down
104 changes: 104 additions & 0 deletions packages/s3/src/driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Readable } from 'node:stream'

import {
DeleteObjectCommand,
GetObjectCommand,
HeadObjectCommand,
S3Client,
} from '@aws-sdk/client-s3'
import { Upload } from '@aws-sdk/lib-storage'
import {
StorageOptions as StorageBusOptions,
Driver,
} from '@storagebus/storage'
import { ENOENT } from '@storagebus/storage/errors'

export type StorageOptions = {
bucket: string
region?: string
accessKeyId?: string
secretAccessKey?: string
endpoint?: string
} & StorageBusOptions

export function driver(options: StorageOptions): Driver {
const { bucket, region, accessKeyId, secretAccessKey, endpoint } = options

const client = new S3Client({
forcePathStyle: true,
...(region ? { region } : {}),
...(accessKeyId && secretAccessKey
? { credentials: { accessKeyId, secretAccessKey } }
: /* c8 ignore next */
{}),
...(endpoint ? { endpoint: endpoint } : {}),
})

return {
async set(data) {
const upload = new Upload({
client,
params: {
Key: data.name,
Bucket: bucket,
Body: await data.stream(),
ContentType: data.type,
},
})
await upload.done()
return data.name
},

async get(path) {
return async () => {
try {
const { Body } = await client.send(
new GetObjectCommand({
Bucket: bucket,
Key: path,
}),
)
return Body as Readable
} catch (err) {
if ((err as Error).name === 'NoSuchKey') {
throw new ENOENT(path)
}
/* c8 ignore next */
throw err
}
}
},
async metadata(path) {
try {
const { ContentType, LastModified, ContentLength } = await client.send(
new HeadObjectCommand({
Bucket: bucket,
Key: path,
}),
)

return {
type: ContentType,
lastModified: LastModified?.getTime(),
size: ContentLength,
}
} catch (err) {
return {
size: 0,
lastModified: -1,
}
}
},

async delete(path) {
await client.send(
new DeleteObjectCommand({
Bucket: bucket,
Key: path,
}),
)
},
}
}

export default { driver }
2 changes: 0 additions & 2 deletions packages/s3/src/index.ts

This file was deleted.

164 changes: 17 additions & 147 deletions packages/s3/src/s3.ts
Original file line number Diff line number Diff line change
@@ -1,153 +1,23 @@
import { Readable } from 'node:stream'
import { Storage as StorageBus } from '@storagebus/storage'
import driver, { StorageOptions } from './driver.js'

import {
CopyObjectCommand,
CopyObjectCommandInput,
DeleteObjectCommand,
DeleteObjectCommandInput,
GetObjectCommand,
GetObjectCommandInput,
HeadObjectCommand,
HeadObjectCommandInput,
S3Client,
ServerSideEncryption,
} from '@aws-sdk/client-s3'
import { Progress, Upload } from '@aws-sdk/lib-storage'
import {
AbstractStorageOptions,
Storage as AbstractStorage,
} from '@ducktors/storagebus-abstract'
import { lookup } from 'mime-types'

export type StorageOptions = {
bucket: string
region?: string
accessKeyId?: string
secretAccessKey?: string
} & AbstractStorageOptions

type EntryptionOptions = {
entryption?: {
ServerSideEncryption: `${ServerSideEncryption}`
SSEKMSKeyId?: string
}
export function createStorage(options: StorageOptions) {
return new Storage(options)
}

export type WriteOpts = {
progress?: (p: Progress) => void
} & EntryptionOptions

export class Storage extends AbstractStorage {
protected client: S3Client
protected bucket: string

export class Storage extends StorageBus {
constructor(opts: StorageOptions) {
super({ debug: opts?.debug, logger: opts?.logger })

const { bucket, region, accessKeyId, secretAccessKey } = opts

this.client = new S3Client({
...(region ? { region } : {}),
...(accessKeyId && secretAccessKey
? { credentials: { accessKeyId, secretAccessKey } }
: {}),
})

this.bucket = bucket
}

async write(
key: string,
fileReadable: Readable,
opts?: WriteOpts,
): Promise<string> {
const _key = this.sanitize(key)
const mimeType = lookup(_key)

const upload = new Upload({
client: this.client,
params: {
Key: _key,
Bucket: this.bucket,
Body: fileReadable,
...(mimeType ? { ContentType: mimeType } : {}),
...(opts?.entryption ?? {}),
},
})

if (opts?.progress) {
upload.on('httpUploadProgress', opts.progress)
}

await upload.done()
return _key
}

async exists(key: string): Promise<boolean> {
const headParams: HeadObjectCommandInput = {
Bucket: this.bucket,
Key: this.sanitize(key),
}
try {
await this.client.send(new HeadObjectCommand(headParams))
return true
} catch (err) {
if (this._debug) {
this._logger.info({ err })
}
return false
}
}

async read(key: string): Promise<Readable> {
const getParams: GetObjectCommandInput = {
Bucket: this.bucket,
Key: this.sanitize(key),
}

const { Body } = await this.client.send(new GetObjectCommand(getParams))
if (!Body) {
throw new Error(`Missing ${key} from ${this.bucket}`)
}

// in Node.js Body is a Readable
return Body as Readable
}

async remove(key: string): Promise<void> {
const deleteParams: DeleteObjectCommandInput = {
Bucket: this.bucket,
Key: this.sanitize(key),
}

await this.client.send(new DeleteObjectCommand(deleteParams))
}

async copy(
key: string,
destKey: string,
opts?: EntryptionOptions,
): Promise<string> {
const mimeType = lookup(key)
const copyParams: CopyObjectCommandInput = {
Bucket: this.bucket,
CopySource: `${this.bucket}/${this.sanitize(key)}`,
Key: this.sanitize(destKey),
...(mimeType ? { ContentType: mimeType } : {}),
...(opts?.entryption ?? {}),
}

await this.client.send(new CopyObjectCommand(copyParams))
return destKey
}

async move(
key: string,
destKey: string,
opts?: EntryptionOptions,
): Promise<string> {
await this.copy(key, destKey, opts)
await this.remove(key)
return destKey
const {
bucket,
region,
accessKeyId,
secretAccessKey,
endpoint,
...options
} = opts
super(
driver.driver({ bucket, region, accessKeyId, secretAccessKey, endpoint }),
options,
)
}
}
Loading

0 comments on commit a169fd3

Please sign in to comment.