Skip to content

Commit

Permalink
feat(runtime): add cloud.storage api in cloud sdk (#1729)
Browse files Browse the repository at this point in the history
* feat(runtime): add cloud.storage api in cloud sdk

* fix cloud object prop missing in runtime
  • Loading branch information
maslow authored Dec 8, 2023
1 parent dafa12c commit 18afa9b
Show file tree
Hide file tree
Showing 9 changed files with 4,815 additions and 678 deletions.
5,140 changes: 4,497 additions & 643 deletions packages/cloud-sdk/package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions packages/cloud-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lafjs/cloud",
"version": "1.0.0-beta.13-fix",
"version": "1.0.0-beta.13-storage-pr",
"description": "The cloud sdk for laf cloud function",
"main": "dist/index.js",
"scripts": {
Expand All @@ -27,6 +27,9 @@
"typescript": "^4.9.4"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.468.0",
"@aws-sdk/client-sts": "^3.468.0",
"@aws-sdk/s3-request-presigner": "^3.468.0",
"@types/express": "^4.17.15",
"@types/ws": "^8.5.3",
"axios": "^1.2.1",
Expand All @@ -37,4 +40,4 @@
"lint-staged": {
"*.{ts,js}": "eslint --fix"
}
}
}
8 changes: 7 additions & 1 deletion packages/cloud-sdk/src/cloud.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as mongodb from 'mongodb'
import { Db } from 'database-proxy'
import { WebSocket } from 'ws'
import { FunctionContext } from './function.interface'
import { CloudStorage } from './storage'

export type InvokeFunctionType = (
name: string,
Expand All @@ -22,7 +23,7 @@ export interface MongoDriverObject {
export interface CloudSdkInterface {
/**
* Sending an HTTP request is actually an Axios instance. You can refer to the Axios documentation directly.
* @deprecated this is deprecated and will be removed in future, use the global `fetch()` directly @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
* @deprecated this is deprecated and will be removed in future, use the global `fetch()` directly @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
* @see https://axios-http.com/docs/intro
*/
fetch?: AxiosStatic
Expand Down Expand Up @@ -101,4 +102,9 @@ export interface CloudSdkInterface {
* @deprecated this is deprecated and will be removed in future, use `process.env` instead
*/
env: any

/**
* Cloud storage instance
*/
storage: CloudStorage
}
15 changes: 15 additions & 0 deletions packages/cloud-sdk/src/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ParseTokenFunctionType,
} from './cloud.interface'
import { WebSocket } from 'ws'
import { CloudStorage } from './storage'

export class Cloud implements CloudSdkInterface {
/**
Expand All @@ -29,12 +30,21 @@ export class Cloud implements CloudSdkInterface {
return this._cloud
}

/**
* Sending an HTTP request is actually an Axios instance. You can refer to the Axios documentation directly.
* @deprecated this is deprecated and will be removed in future, use the global `fetch()` directly @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
* @see https://axios-http.com/docs/intro
*/
fetch: AxiosStatic = request

database(): Db {
return this.cloud.database()
}

/**
* Invoke cloud function
* @deprecated Just import the cloud function directly, and then call it
*/
invoke: InvokeFunctionType = (name: string, param?: any) => {
return this.cloud.invoke(name, param)
}
Expand Down Expand Up @@ -63,7 +73,12 @@ export class Cloud implements CloudSdkInterface {
return this.cloud.appid
}

/**
* @deprecated this is deprecated and will be removed in future, use `process.env` instead
*/
get env() {
return this.cloud.env
}

storage: CloudStorage = new CloudStorage()
}
265 changes: 265 additions & 0 deletions packages/cloud-sdk/src/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
import * as assert from 'node:assert'
import { Readable } from 'node:stream'
import { IncomingMessage } from 'node:http'
import {
S3,
GetObjectCommandInput,
PutObjectCommandInput,
DeleteObjectCommandInput,
ListObjectsCommandInput,
PutObjectCommand,
GetObjectCommand,
} from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'

export type NodeJsRuntimeStreamingBlobPayloadOutputTypes = SdkStream<
Readable | IncomingMessage
>
export type SdkStream<BaseStream> = BaseStream & SdkStreamMixin

export interface SdkStreamMixin {
transformToByteArray: () => Promise<Uint8Array>
transformToString: (encoding?: string) => Promise<string>
transformToWebStream: () => ReadableStream
}
/**
/**
* `ICloudStorage` is an interface for cloud storage services.
*/
export class CloudStorage {
protected _externalS3Client: S3
protected _internalS3Client: S3

protected get appid() {
assert(process.env.APPID, 'APPID is required')
return process.env.APPID
}

protected getExternalClient() {
if (!this._externalS3Client) {
assert(
process.env.OSS_EXTERNAL_ENDPOINT,
'OSS_EXTERNAL_ENDPOINT is required'
)
assert(process.env.OSS_REGION, 'OSS_REGION is required')
assert(process.env.OSS_ACCESS_KEY, 'OSS_ACCESS_KEY is required')
assert(process.env.OSS_ACCESS_SECRET, 'OSS_ACCESS_SECRET is required')

this._externalS3Client = new S3({
endpoint: process.env.OSS_EXTERNAL_ENDPOINT,
region: process.env.OSS_REGION,
credentials: {
accessKeyId: process.env.OSS_ACCESS_KEY,
secretAccessKey: process.env.OSS_ACCESS_SECRET,
},
forcePathStyle: true,
})
}

return this._externalS3Client
}

protected getInternalClient() {
if (!this._internalS3Client) {
assert(
process.env.OSS_INTERNAL_ENDPOINT,
'OSS_INTERNAL_ENDPOINT is required'
)
assert(process.env.OSS_REGION, 'OSS_REGION is required')
assert(process.env.OSS_ACCESS_KEY, 'OSS_ACCESS_KEY is required')
assert(process.env.OSS_ACCESS_SECRET, 'OSS_ACCESS_SECRET is required')

this._internalS3Client = new S3({
endpoint: process.env.OSS_INTERNAL_ENDPOINT,
region: process.env.OSS_REGION,
credentials: {
accessKeyId: process.env.OSS_ACCESS_KEY,
secretAccessKey: process.env.OSS_ACCESS_SECRET,
},
forcePathStyle: true,
})
}

return this._internalS3Client
}

/**
* Get S3 client of `@aws-sdk/client-s3`, by default it returns the internal access client.
* @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/
*/
public getS3Client(options: { external?: boolean } = { external: false }) {
return options.external
? this.getExternalClient()
: this.getInternalClient()
}

/**
* Get bucket by short name
* @param bucketShortName it is the short name of the bucket, e.g. `images`, NOT `{appid}-images`
* @returns
*/
bucket(bucketShortName: string): CloudStorageBucket {
assert(bucketShortName, 'bucketShortName is required')
const name = `${this.appid}-${bucketShortName}`
return new CloudStorageBucket(this, name)
}
}

export class CloudStorageBucket {
readonly storage: CloudStorage
readonly name: string

constructor(storage: CloudStorage, name: string) {
assert(storage, 'storage is required')
assert(name, 'name is required')

this.storage = storage
this.name = name
}

/**
* Read file from bucket
* @param filename filename is the key of the object, it can contain subdirectories, e.g. `a/b/c.txt`
* @param options
* @returns
* @see https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html
*/
public async readFile(
filename: string,
options?: Omit<GetObjectCommandInput, 'Bucket' | 'Key'>
): Promise<NodeJsRuntimeStreamingBlobPayloadOutputTypes> {
assert(filename, 'filename is required')
const internal = this.storage.getS3Client()

const args: GetObjectCommandInput = {
Bucket: this.name,
Key: filename,
...options,
}
const res = await internal.getObject(args)
return res.Body as NodeJsRuntimeStreamingBlobPayloadOutputTypes
}

/**
* Write file to bucket
* @param filename filename is the key of the object, it can contain subdirectories, e.g. `a/b/c.txt`
* @param body body is the content of the object, it can be a string, a Buffer, a Readable stream, or a Blob
* @param options
* @returns
* @see https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
*/
public async writeFile(
filename: string,
body: string | Blob | Buffer | Uint8Array | Readable,
options?: Omit<PutObjectCommandInput, 'Bucket' | 'Key' | 'Body'>
) {
assert(filename, 'key is required')
assert(body, 'body is required')
const external = this.storage.getS3Client()

const args: PutObjectCommandInput = {
Bucket: this.name,
Key: filename,
Body: body,
...options,
}
const res = await external.putObject(args)
return res
}

/**
* Delete file from bucket
* @param filename filename is the key of the object, it can contain subdirectories, e.g. `a/b/c.txt`
* @param options
* @returns
* @see https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html
*/
public async deleteFile(
filename: string,
options?: Omit<DeleteObjectCommandInput, 'Bucket' | 'Key'>
) {
assert(filename, 'filename is required')
const external = this.storage.getS3Client()

const args: DeleteObjectCommandInput = {
Bucket: this.name,
Key: filename,
...options,
}
const res = await external.deleteObject(args)
return res
}

/**
* List files in bucket
* @param prefix prefix is the key prefix of the object, it can contain subdirectories, e.g. `a/b/`
* @param options
* @returns
* @see https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjects.html
*/
public async listFiles(
prefix?: string,
options?: Omit<ListObjectsCommandInput, 'Bucket' | 'Prefix'>
) {
const internal = this.storage.getS3Client()

const args: ListObjectsCommandInput = {
Bucket: this.name,
Prefix: prefix,
...options,
}
const res = await internal.listObjects(args)
return res
}

/**
* Get upload url, you can use this url to upload file directly to bucket
* @param filename filename is the key of the object, it can contain subdirectories, e.g. `a/b/c.txt`
* @param expiresIn expiresIn is the seconds of the signed url, default is `3600` seconds which is 1 hour
* @param options
* @returns
*/
public async getUploadUrl(
filename: string,
expiresIn = 3600,
options?: Omit<PutObjectCommandInput, 'Bucket' | 'Key'>
) {
assert(filename, 'filename is required')
const external = this.storage.getS3Client({ external: true })

const args = new PutObjectCommand({
Bucket: this.name,
Key: filename,
...options,
})

const url = await getSignedUrl(external, args, { expiresIn: expiresIn })
return url
}

/**
* Get download url, you can use this url to download file directly from bucket
* @param filename filename is the key of the object, it can contain subdirectories, e.g. `a/b/c.txt`
* @param expiresIn expiresIn is the seconds of the signed url, default is `3600` seconds which is 1 hour
* @param options
* @returns
*/
public async getDownloadUrl(
filename: string,
expiresIn = 3600,
options?: Omit<GetObjectCommandInput, 'Bucket' | 'Key'>
) {
assert(filename, 'filename is required')
const external = this.storage.getS3Client({ external: true })

const args = new GetObjectCommand({
Bucket: this.name,
Key: filename,
...options,
})

const url = await getSignedUrl(external, args, { expiresIn: expiresIn })
return url
}
}
2 changes: 1 addition & 1 deletion runtimes/nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@aws-sdk/client-sts": "^3.231.0",
"@aws-sdk/s3-request-presigner": "^3.231.0",
"@kubernetes/client-node": "^0.18.0",
"@lafjs/cloud": "^1.0.0-beta.13-fix",
"@lafjs/cloud": "^1.0.0-beta.13-storage-pr",
"@types/pako": "^2.0.2",
"axios": "^1.4.0",
"chalk": "^4.1.2",
Expand Down
1 change: 1 addition & 0 deletions runtimes/nodejs/src/support/cloud-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function createCloudSdk() {
get env() {
return process.env
},
storage: null,
}

/**
Expand Down
Loading

0 comments on commit 18afa9b

Please sign in to comment.