minio
Version:
S3 Compatible Cloud Storage client
355 lines (312 loc) • 9.65 kB
text/typescript
import * as fs from 'node:fs'
import * as path from 'node:path'
import * as querystring from 'query-string'
import * as errors from './errors.ts'
import {
getEncryptionHeaders,
isEmpty,
isEmptyObject,
isNumber,
isObject,
isString,
isValidBucketName,
isValidObjectName,
} from './internal/helper.ts'
import type { Encryption, ObjectMetaData, RequestHeaders } from './internal/type.ts'
import { RETENTION_MODES } from './internal/type.ts'
export { ENCRYPTION_TYPES, LEGAL_HOLD_STATUS, RETENTION_MODES, RETENTION_VALIDITY_UNITS } from './internal/type.ts'
export const DEFAULT_REGION = 'us-east-1'
export const PRESIGN_EXPIRY_DAYS_MAX = 24 * 60 * 60 * 7 // 7 days in seconds
export interface ICopySourceOptions {
Bucket: string
Object: string
/**
* Valid versionId
*/
VersionID?: string
/**
* Etag to match
*/
MatchETag?: string
/**
* Etag to exclude
*/
NoMatchETag?: string
/**
* Modified Date of the object/part. UTC Date in string format
*/
MatchModifiedSince?: string | null
/**
* Modified Date of the object/part to exclude UTC Date in string format
*/
MatchUnmodifiedSince?: string | null
/**
* true or false Object range to match
*/
MatchRange?: boolean
Start?: number
End?: number
Encryption?: Encryption
}
export class CopySourceOptions {
public readonly Bucket: string
public readonly Object: string
public readonly VersionID: string
public MatchETag: string
private readonly NoMatchETag: string
private readonly MatchModifiedSince: string | null
private readonly MatchUnmodifiedSince: string | null
public readonly MatchRange: boolean
public readonly Start: number
public readonly End: number
private readonly Encryption?: Encryption
constructor({
Bucket,
Object,
VersionID = '',
MatchETag = '',
NoMatchETag = '',
MatchModifiedSince = null,
MatchUnmodifiedSince = null,
MatchRange = false,
Start = 0,
End = 0,
Encryption = undefined,
}: ICopySourceOptions) {
this.Bucket = Bucket
this.Object = Object
this.VersionID = VersionID
this.MatchETag = MatchETag
this.NoMatchETag = NoMatchETag
this.MatchModifiedSince = MatchModifiedSince
this.MatchUnmodifiedSince = MatchUnmodifiedSince
this.MatchRange = MatchRange
this.Start = Start
this.End = End
this.Encryption = Encryption
}
validate() {
if (!isValidBucketName(this.Bucket)) {
throw new errors.InvalidBucketNameError('Invalid Source bucket name: ' + this.Bucket)
}
if (!isValidObjectName(this.Object)) {
throw new errors.InvalidObjectNameError(`Invalid Source object name: ${this.Object}`)
}
if ((this.MatchRange && this.Start !== -1 && this.End !== -1 && this.Start > this.End) || this.Start < 0) {
throw new errors.InvalidObjectNameError('Source start must be non-negative, and start must be at most end.')
} else if ((this.MatchRange && !isNumber(this.Start)) || !isNumber(this.End)) {
throw new errors.InvalidObjectNameError(
'MatchRange is specified. But Invalid Start and End values are specified.',
)
}
return true
}
getHeaders(): RequestHeaders {
const headerOptions: RequestHeaders = {}
headerOptions['x-amz-copy-source'] = encodeURI(this.Bucket + '/' + this.Object)
if (!isEmpty(this.VersionID)) {
headerOptions['x-amz-copy-source'] = `${encodeURI(this.Bucket + '/' + this.Object)}?versionId=${this.VersionID}`
}
if (!isEmpty(this.MatchETag)) {
headerOptions['x-amz-copy-source-if-match'] = this.MatchETag
}
if (!isEmpty(this.NoMatchETag)) {
headerOptions['x-amz-copy-source-if-none-match'] = this.NoMatchETag
}
if (!isEmpty(this.MatchModifiedSince)) {
headerOptions['x-amz-copy-source-if-modified-since'] = this.MatchModifiedSince
}
if (!isEmpty(this.MatchUnmodifiedSince)) {
headerOptions['x-amz-copy-source-if-unmodified-since'] = this.MatchUnmodifiedSince
}
return headerOptions
}
}
/**
* @deprecated use nodejs fs module
*/
export function removeDirAndFiles(dirPath: string, removeSelf = true) {
if (removeSelf) {
return fs.rmSync(dirPath, { recursive: true, force: true })
}
fs.readdirSync(dirPath).forEach((item) => {
fs.rmSync(path.join(dirPath, item), { recursive: true, force: true })
})
}
export interface ICopyDestinationOptions {
/**
* Bucket name
*/
Bucket: string
/**
* Object Name for the destination (composed/copied) object defaults
*/
Object: string
/**
* Encryption configuration defaults to {}
* @default {}
*/
Encryption?: Encryption
UserMetadata?: ObjectMetaData
/**
* query-string encoded string or Record<string, string> Object
*/
UserTags?: Record<string, string> | string
LegalHold?: 'on' | 'off'
/**
* UTC Date String
*/
RetainUntilDate?: string
Mode?: RETENTION_MODES
MetadataDirective?: 'COPY' | 'REPLACE'
/**
* Extra headers for the target object
*/
Headers?: Record<string, string>
}
export class CopyDestinationOptions {
public readonly Bucket: string
public readonly Object: string
private readonly Encryption?: Encryption
private readonly UserMetadata?: ObjectMetaData
private readonly UserTags?: Record<string, string> | string
private readonly LegalHold?: 'on' | 'off'
private readonly RetainUntilDate?: string
private readonly Mode?: RETENTION_MODES
private readonly MetadataDirective?: string
private readonly Headers?: Record<string, string>
constructor({
Bucket,
Object,
Encryption,
UserMetadata,
UserTags,
LegalHold,
RetainUntilDate,
Mode,
MetadataDirective,
Headers,
}: ICopyDestinationOptions) {
this.Bucket = Bucket
this.Object = Object
this.Encryption = Encryption ?? undefined // null input will become undefined, easy for runtime assert
this.UserMetadata = UserMetadata
this.UserTags = UserTags
this.LegalHold = LegalHold
this.Mode = Mode // retention mode
this.RetainUntilDate = RetainUntilDate
this.MetadataDirective = MetadataDirective
this.Headers = Headers
}
getHeaders(): RequestHeaders {
const replaceDirective = 'REPLACE'
const headerOptions: RequestHeaders = {}
const userTags = this.UserTags
if (!isEmpty(userTags)) {
headerOptions['X-Amz-Tagging-Directive'] = replaceDirective
headerOptions['X-Amz-Tagging'] = isObject(userTags)
? querystring.stringify(userTags)
: isString(userTags)
? userTags
: ''
}
if (this.Mode) {
headerOptions['X-Amz-Object-Lock-Mode'] = this.Mode // GOVERNANCE or COMPLIANCE
}
if (this.RetainUntilDate) {
headerOptions['X-Amz-Object-Lock-Retain-Until-Date'] = this.RetainUntilDate // needs to be UTC.
}
if (this.LegalHold) {
headerOptions['X-Amz-Object-Lock-Legal-Hold'] = this.LegalHold // ON or OFF
}
if (this.UserMetadata) {
for (const [key, value] of Object.entries(this.UserMetadata)) {
headerOptions[`X-Amz-Meta-${key}`] = value.toString()
}
}
if (this.MetadataDirective) {
headerOptions[`X-Amz-Metadata-Directive`] = this.MetadataDirective
}
if (this.Encryption) {
const encryptionHeaders = getEncryptionHeaders(this.Encryption)
for (const [key, value] of Object.entries(encryptionHeaders)) {
headerOptions[key] = value
}
}
if (this.Headers) {
for (const [key, value] of Object.entries(this.Headers)) {
headerOptions[key] = value
}
}
return headerOptions
}
validate() {
if (!isValidBucketName(this.Bucket)) {
throw new errors.InvalidBucketNameError('Invalid Destination bucket name: ' + this.Bucket)
}
if (!isValidObjectName(this.Object)) {
throw new errors.InvalidObjectNameError(`Invalid Destination object name: ${this.Object}`)
}
if (!isEmpty(this.UserMetadata) && !isObject(this.UserMetadata)) {
throw new errors.InvalidObjectNameError(`Destination UserMetadata should be an object with key value pairs`)
}
if (!isEmpty(this.Mode) && ![RETENTION_MODES.GOVERNANCE, RETENTION_MODES.COMPLIANCE].includes(this.Mode)) {
throw new errors.InvalidObjectNameError(
`Invalid Mode specified for destination object it should be one of [GOVERNANCE,COMPLIANCE]`,
)
}
if (this.Encryption !== undefined && isEmptyObject(this.Encryption)) {
throw new errors.InvalidObjectNameError(`Invalid Encryption configuration for destination object `)
}
return true
}
}
/**
* maybe this should be a generic type for Records, leave it for later refactor
*/
export class SelectResults {
private records?: unknown
private response?: unknown
private stats?: string
private progress?: unknown
constructor({
records, // parsed data as stream
response, // original response stream
stats, // stats as xml
progress, // stats as xml
}: {
records?: unknown
response?: unknown
stats?: string
progress?: unknown
}) {
this.records = records
this.response = response
this.stats = stats
this.progress = progress
}
setStats(stats: string) {
this.stats = stats
}
getStats() {
return this.stats
}
setProgress(progress: unknown) {
this.progress = progress
}
getProgress() {
return this.progress
}
setResponse(response: unknown) {
this.response = response
}
getResponse() {
return this.response
}
setRecords(records: unknown) {
this.records = records
}
getRecords(): unknown {
return this.records
}
}