Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: migrate Client base methods to TypeScript #1153

Merged
merged 7 commits into from
May 21, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
383 changes: 383 additions & 0 deletions src/internal/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,383 @@
import * as http from 'node:http'
import * as https from 'node:https'

import _ from 'lodash'

import { CredentialProvider } from '../CredentialProvider.ts'
import * as errors from '../errors.ts'
import {
isAmazonEndpoint,
isBoolean,
isDefined,
isEmpty,
isObject,
isString,
isValidEndpoint,
isValidPort,
isVirtualHostStyle,
uriResourceEscape,
} from './helper.ts'
import type { Region } from './s3-endpoints.ts'
import { getS3Endpoint } from './s3-endpoints.ts'
import type { IRequest, RequestHeaders, Transport } from './type.ts'

// will be replaced by bundler.
const Package = { version: process.env.MINIO_JS_PACKAGE_VERSION || 'development' }

const requestOptionProperties = [
'agent',
'ca',
'cert',
'ciphers',
'clientCertEngine',
'crl',
'dhparam',
'ecdhCurve',
'family',
'honorCipherOrder',
'key',
'passphrase',
'pfx',
'rejectUnauthorized',
'secureOptions',
'secureProtocol',
'servername',
'sessionIdContext',
] as const

export interface ClientOptions {
endPoint: string
accessKey: string
secretKey: string
useSSL?: boolean
port?: number
region?: Region
transport?: Transport
sessionToken?: string
partSize?: number
pathStyle?: boolean
credentialsProvider?: CredentialProvider
s3AccelerateEndpoint?: string
transportAgent?: http.Agent
}

export type RequestOption = Partial<IRequest> & {
method: string
bucketName?: string
objectName?: string
region?: string
query?: string
pathStyle?: boolean
}

export class TypedClient {
protected transport: Transport
protected host: string
protected port: number
protected protocol: string
protected accessKey: string
protected secretKey: string
protected sessionToken?: string
protected userAgent: string
protected anonymous: boolean
protected pathStyle: boolean
protected regionMap: Record<string, string>
public region?: string
protected credentialsProvider?: CredentialProvider
partSize: number = 64 * 1024 * 1024
protected overRidePartSize?: boolean

protected maximumPartSize = 5 * 1024 * 1024 * 1024
protected maxObjectSize = 5 * 1024 * 1024 * 1024 * 1024
public enableSHA256: boolean
protected s3AccelerateEndpoint?: string
protected reqOptions: Record<string, unknown>

protected transportAgent: http.Agent

constructor(params: ClientOptions) {
// @ts-expect-error deprecated property
if (params.secure !== undefined) {
throw new Error('"secure" option deprecated, "useSSL" should be used instead')
}
// Default values if not specified.
if (params.useSSL === undefined) {
params.useSSL = true
}
if (!params.port) {
params.port = 0
}
// Validate input params.
if (!isValidEndpoint(params.endPoint)) {
throw new errors.InvalidEndpointError(`Invalid endPoint : ${params.endPoint}`)
}
if (!isValidPort(params.port)) {
throw new errors.InvalidArgumentError(`Invalid port : ${params.port}`)
}
if (!isBoolean(params.useSSL)) {
throw new errors.InvalidArgumentError(
`Invalid useSSL flag type : ${params.useSSL}, expected to be of type "boolean"`,
)
}

// Validate region only if its set.
if (params.region) {
if (!isString(params.region)) {
throw new errors.InvalidArgumentError(`Invalid region : ${params.region}`)
}
}

const host = params.endPoint.toLowerCase()
let port = params.port
let protocol: string
let transport
let transportAgent: http.Agent
// Validate if configuration is not using SSL
// for constructing relevant endpoints.
if (params.useSSL) {
// Defaults to secure.
transport = https
protocol = 'https:'
port = port || 443
transportAgent = https.globalAgent
} else {
transport = http
protocol = 'http:'
port = port || 80
transportAgent = http.globalAgent
}

// if custom transport is set, use it.
if (params.transport) {
if (!isObject(params.transport)) {
throw new errors.InvalidArgumentError(
`Invalid transport type : ${params.transport}, expected to be type "object"`,
)
}
transport = params.transport
}

// if custom transport agent is set, use it.
if (params.transportAgent) {
if (!isObject(params.transportAgent)) {
throw new errors.InvalidArgumentError(
`Invalid transportAgent type: ${params.transportAgent}, expected to be type "object"`,
)
}

transportAgent = params.transportAgent
}

// User Agent should always following the below style.
// Please open an issue to discuss any new changes here.
//
// MinIO (OS; ARCH) LIB/VER APP/VER
//
const libraryComments = `(${process.platform}; ${process.arch})`
const libraryAgent = `MinIO ${libraryComments} minio-js/${Package.version}`
// User agent block ends.

this.transport = transport
this.transportAgent = transportAgent
this.host = host
this.port = port
this.protocol = protocol
this.userAgent = `${libraryAgent}`

// Default path style is true
if (params.pathStyle === undefined) {
this.pathStyle = true
} else {
this.pathStyle = params.pathStyle
}

this.accessKey = params.accessKey ?? ''
this.secretKey = params.secretKey ?? ''
this.sessionToken = params.sessionToken
this.anonymous = !this.accessKey || !this.secretKey

if (params.credentialsProvider) {
this.credentialsProvider = params.credentialsProvider
void this.checkAndRefreshCreds()
prakashsvmx marked this conversation as resolved.
Show resolved Hide resolved
}

this.regionMap = {}
if (params.region) {
this.region = params.region
}

if (params.partSize) {
this.partSize = params.partSize
this.overRidePartSize = true
}
if (this.partSize < 5 * 1024 * 1024) {
throw new errors.InvalidArgumentError(`Part size should be greater than 5MB`)
}
if (this.partSize > 5 * 1024 * 1024 * 1024) {
throw new errors.InvalidArgumentError(`Part size should be less than 5GB`)
}

// SHA256 is enabled only for authenticated http requests. If the request is authenticated
// and the connection is https we use x-amz-content-sha256=UNSIGNED-PAYLOAD
// header for signature calculation.
this.enableSHA256 = !this.anonymous && !params.useSSL

this.s3AccelerateEndpoint = params.s3AccelerateEndpoint || undefined
this.reqOptions = {}
}

/**
* @param endPoint - valid S3 acceleration end point
*/
setS3TransferAccelerate(endPoint: string) {
this.s3AccelerateEndpoint = endPoint
}

/**
* Sets the supported request options.
*/
public setRequestOptions(options: Pick<https.RequestOptions, (typeof requestOptionProperties)[number]>) {
if (!isObject(options)) {
throw new TypeError('request options should be of type "object"')
}
this.reqOptions = _.pick(options, requestOptionProperties)
}

/**
* This is s3 Specific and does not hold validity in any other Object storage.
*/
private getAccelerateEndPointIfSet(bucketName?: string, objectName?: string) {
if (!isEmpty(this.s3AccelerateEndpoint) && !isEmpty(bucketName) && !isEmpty(objectName)) {
// http://docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html
// Disable transfer acceleration for non-compliant bucket names.
if (bucketName.includes('.')) {
throw new Error(`Transfer Acceleration is not supported for non compliant bucket:${bucketName}`)
}
// If transfer acceleration is requested set new host.
// For more details about enabling transfer acceleration read here.
// http://docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html
return this.s3AccelerateEndpoint
}
return false
}

/**
* returns options object that can be used with http.request()
* Takes care of constructing virtual-host-style or path-style hostname
*/
protected getRequestOptions(opts: RequestOption): IRequest & { host: string; headers: Record<string, string> } {
const method = opts.method
const region = opts.region
const bucketName = opts.bucketName
let objectName = opts.objectName
const headers = opts.headers
const query = opts.query

let reqOptions = {
method,
headers: {} as RequestHeaders,
protocol: this.protocol,
// If custom transportAgent was supplied earlier, we'll inject it here
agent: this.transportAgent,
}

// Verify if virtual host supported.
let virtualHostStyle
if (bucketName) {
virtualHostStyle = isVirtualHostStyle(this.host, this.protocol, bucketName, this.pathStyle)
}

let path = '/'
let host = this.host

let port: undefined | number
if (this.port) {
port = this.port
}

if (objectName) {
objectName = uriResourceEscape(objectName)
}

// For Amazon S3 endpoint, get endpoint based on region.
if (isAmazonEndpoint(host)) {
const accelerateEndPoint = this.getAccelerateEndPointIfSet(bucketName, objectName)
if (accelerateEndPoint) {
host = `${accelerateEndPoint}`
} else {
host = getS3Endpoint(region!)

Check warning on line 308 in src/internal/client.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion
}
}

if (virtualHostStyle && !opts.pathStyle) {
// For all hosts which support virtual host style, `bucketName`
// is part of the hostname in the following format:
//
// var host = 'bucketName.example.com'
//
if (bucketName) {
host = `${bucketName}.${host}`
}
if (objectName) {
path = `/${objectName}`
}
} else {
// For all S3 compatible storage services we will fallback to
// path style requests, where `bucketName` is part of the URI
// path.
if (bucketName) {
path = `/${bucketName}`
}
if (objectName) {
path = `/${bucketName}/${objectName}`
}
}

if (query) {
path += `?${query}`
}
reqOptions.headers.host = host
if ((reqOptions.protocol === 'http:' && port !== 80) || (reqOptions.protocol === 'https:' && port !== 443)) {
reqOptions.headers.host = `${host}:${port}`
}
reqOptions.headers['user-agent'] = this.userAgent
if (headers) {
// have all header keys in lower case - to make signing easy
for (const [k, v] of Object.entries(headers)) {
reqOptions.headers[k.toLowerCase()] = v
}
}

// Use any request option specified in minioClient.setRequestOptions()
reqOptions = Object.assign({}, this.reqOptions, reqOptions)

return {
...reqOptions,
headers: _.mapValues(_.pickBy(reqOptions.headers, isDefined), (v) => v.toString()),
host,
port,
path,
} satisfies https.RequestOptions
}

public async setCredentialsProvider(credentialsProvider: CredentialProvider) {
if (!(credentialsProvider instanceof CredentialProvider)) {
throw new Error('Unable to get credentials. Expected instance of CredentialProvider')
}
this.credentialsProvider = credentialsProvider
await this.checkAndRefreshCreds()
}

private async checkAndRefreshCreds() {
if (this.credentialsProvider) {
try {
const credentialsConf = await this.credentialsProvider.getCredentials()
this.accessKey = credentialsConf.getAccessKey()
this.secretKey = credentialsConf.getSecretKey()
this.sessionToken = credentialsConf.getSessionToken()
} catch (e) {
throw new Error(`Unable to get credentials: ${e}`, { cause: e })
}
}
}
}
4 changes: 4 additions & 0 deletions src/internal/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ export function isEmptyObject(o: Record<string, unknown>): boolean {
return Object.values(o).filter((x) => x !== undefined).length !== 0
}

export function isDefined<T>(o: T): o is Exclude<T, null | undefined> {
return o !== null && o !== undefined
}

/**
* check if arg is a valid date
*/
Expand Down