-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
641 additions
and
524 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
|
@@ -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" | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
} | ||
} |
Oops, something went wrong.