oss-client
Version:
Aliyun OSS(Object Storage Service) Node.js Client
1,173 lines (1,100 loc) • 32.9 kB
text/typescript
import type { Readable, Writable } from 'node:stream';
import { createReadStream, createWriteStream } from 'node:fs';
import { strict as assert } from 'node:assert';
import querystring from 'node:querystring';
import fs from 'node:fs/promises';
import mime from 'mime';
import { isReadable, isWritable } from 'is-type-of';
import type { IncomingHttpHeaders } from 'urllib';
import type {
ListObjectsQuery,
RequestOptions,
ListObjectResult,
PutObjectOptions,
PutObjectResult,
UserMeta,
DeleteObjectOptions,
DeleteObjectResult,
GetObjectOptions,
GetObjectResult,
SignatureUrlOptions,
HeadObjectOptions,
HeadObjectResult,
IObjectSimple,
GetStreamOptions,
GetStreamResult,
CopyObjectOptions,
CopyAndPutMetaResult,
ObjectMeta,
StorageType,
} from 'oss-interface';
import {
type OSSBaseClientInitOptions,
OSSBaseClient,
} from './OSSBaseClient.js';
import type {
ACLType,
AppendObjectOptions,
AppendObjectResult,
DeleteMultipleObject,
DeleteMultipleObjectOptions,
DeleteMultipleObjectResponse,
DeleteMultipleObjectXML,
DeleteObjectTaggingOptions,
DeleteObjectTaggingResult,
GetACLOptions,
GetACLResult,
GetSymlinkOptions,
GetSymlinkResult,
GutObjectTaggingOptions,
GutObjectTaggingResult,
ListV2ObjectResult,
ListV2ObjectsQuery,
OSSRequestParams,
OSSResult,
PutACLOptions,
PutACLResult,
PutObjectTaggingOptions,
PutObjectTaggingResult,
PutSymlinkOptions,
PutSymlinkResult,
RequestMethod,
} from './type/index.js';
import {
checkBucketName,
signatureForURL,
encodeCallback,
json2xml,
timestamp,
checkObjectTag,
computeSignature,
policyToJSONString,
} from './util/index.js';
export interface OSSObjectClientInitOptions extends OSSBaseClientInitOptions {
bucket?: string;
/**
* Enable cname
* @see https://help.aliyun.com/zh/oss/user-guide/map-custom-domain-names-5
*/
cname?: boolean;
}
interface XMLCommonPrefix {
Prefix: string;
}
interface XMLContent {
Key: string;
LastModified: string;
ETag: string;
Type: string;
Size: string;
StorageClass: StorageType;
Owner: { ID: string; DisplayName: string };
}
export class OSSObject extends OSSBaseClient implements IObjectSimple {
#bucket: string;
#bucketEndpoint: string;
constructor(options: OSSObjectClientInitOptions) {
if (!options.cname) {
assert.ok(options.bucket, 'bucket required');
}
if (options.bucket) {
checkBucketName(options.bucket);
}
super(options);
if (options.cname) {
// ignore bucket on cname set to true
this.#bucket = '';
this.#bucketEndpoint = this.options.endpoint;
} else {
this.#bucket = options.bucket ?? '';
const urlObject = new URL(this.options.endpoint);
urlObject.hostname = `${this.#bucket}.${urlObject.hostname}`;
this.#bucketEndpoint = urlObject.toString();
}
}
/** public methods */
/**
* AppendObject
* @see https://help.aliyun.com/zh/oss/developer-reference/appendobject
*/
async append(
name: string,
file: string | Buffer | Readable,
options?: AppendObjectOptions
): Promise<AppendObjectResult> {
const position = options?.position ?? '0';
const result = await this.#sendPutRequest(
name,
{
...options,
subResource: {
append: '',
position: `${position}`,
},
},
file,
'POST'
);
return {
...result,
nextAppendPosition: result.res.headers[
'x-oss-next-append-position'
] as string,
};
}
/**
* put an object from String(file path)/Buffer/Readable
* @param {String} name the object key
* @param {Mixed} file String(file path)/Buffer/Readable
* @param {Object} options options
* {Object} options.callback The callback parameter is composed of a JSON string encoded in Base64
* {String} options.callback.url the OSS sends a callback request to this URL
* {String} options.callback.host The host header value for initiating callback requests
* {String} options.callback.body The value of the request body when a callback is initiated
* {String} options.callback.contentType The Content-Type of the callback requests initiated
* {Object} options.callback.customValue Custom parameters are a map of key-values, e.g:
* customValue = {
* key1: 'value1',
* key2: 'value2'
* }
* @returns {Object} result
*/
async put(
name: string,
file: string | Buffer | Readable,
options?: PutObjectOptions
): Promise<PutObjectResult> {
if (typeof file === 'string' || isReadable(file) || Buffer.isBuffer(file)) {
return await this.#sendPutRequest(name, options ?? {}, file);
}
throw new TypeError('Must provide String/Buffer/ReadableStream for put.');
}
/**
* put an object from ReadableStream.
*/
async putStream(
name: string,
stream: Readable,
options?: PutObjectOptions
): Promise<PutObjectResult> {
return await this.#sendPutRequest(name, options ?? {}, stream);
}
async putMeta(
name: string,
meta: UserMeta,
options?: Omit<CopyObjectOptions, 'meta'>
) {
return await this.copy(name, name, {
meta,
...options,
});
}
/**
* GetBucket (ListObjects)
* @see https://help.aliyun.com/zh/oss/developer-reference/listobjects
*/
async list(
query?: ListObjectsQuery,
options?: RequestOptions
): Promise<ListObjectResult> {
// prefix, marker, max-keys, delimiter
const params = this.#objectRequestParams('GET', '', options);
if (query) {
params.query = query;
}
params.xmlResponse = true;
params.successStatuses = [200];
const { data, res } = await this.request(params);
let contents = data.Contents as XMLContent[] | XMLContent | undefined;
let objects: ObjectMeta[] = [];
if (contents) {
if (!Array.isArray(contents)) {
contents = [contents];
}
objects = contents.map(obj => ({
name: obj.Key,
url: this.#objectUrl(obj.Key),
lastModified: obj.LastModified,
etag: obj.ETag,
type: obj.Type,
size: Number(obj.Size),
storageClass: obj.StorageClass,
owner: {
id: obj.Owner.ID,
displayName: obj.Owner.DisplayName,
},
}));
}
let commonPrefixes = data.CommonPrefixes as
| XMLCommonPrefix[]
| XMLCommonPrefix
| undefined;
let prefixes: string[] = [];
if (commonPrefixes) {
if (!Array.isArray(commonPrefixes)) {
commonPrefixes = [commonPrefixes];
}
prefixes = commonPrefixes.map(item => item.Prefix);
}
return {
res,
objects,
prefixes: prefixes || [],
nextMarker: data.NextMarker || null,
isTruncated: data.IsTruncated === 'true',
} satisfies ListObjectResult;
}
/**
* ListObjectsV2(GetBucketV2)
* @see https://help.aliyun.com/zh/oss/developer-reference/listobjectsv2
*/
async listV2(
query?: ListV2ObjectsQuery,
options?: RequestOptions
): Promise<ListV2ObjectResult> {
const params = this.#objectRequestParams('GET', '', options);
params.query = {
'list-type': '2',
};
const continuationToken =
query?.['continuation-token'] ?? query?.continuationToken;
if (continuationToken) {
// should set subResource to add sign string
params.subResource = {
'continuation-token': continuationToken,
};
}
if (query?.prefix) {
params.query.prefix = query.prefix;
}
if (query?.delimiter) {
params.query.delimiter = query.delimiter;
}
if (query?.['max-keys']) {
params.query['max-keys'] = `${query['max-keys']}`;
}
if (query?.['start-after']) {
params.query['start-after'] = query['start-after'];
}
if (query?.['encoding-type']) {
params.query['encoding-type'] = query['encoding-type'];
}
if (query?.['fetch-owner']) {
params.query['fetch-owner'] = 'true';
}
params.xmlResponse = true;
params.successStatuses = [200];
const { data, res } = await this.request(params);
let contents = data.Contents as XMLContent[] | XMLContent | undefined;
let objects: ObjectMeta[] = [];
if (contents) {
if (!Array.isArray(contents)) {
contents = [contents];
}
objects = contents.map(obj => ({
name: obj.Key,
url: this.#objectUrl(obj.Key),
lastModified: obj.LastModified,
etag: obj.ETag,
type: obj.Type,
size: Number(obj.Size),
storageClass: obj.StorageClass,
owner: obj.Owner
? {
id: obj.Owner.ID,
displayName: obj.Owner.DisplayName,
}
: undefined,
}));
}
let commonPrefixes = data.CommonPrefixes as
| XMLCommonPrefix[]
| XMLCommonPrefix
| undefined;
let prefixes: string[] = [];
if (commonPrefixes) {
if (!Array.isArray(commonPrefixes)) {
commonPrefixes = [commonPrefixes];
}
prefixes = commonPrefixes.map(item => item.Prefix);
}
return {
res,
objects,
prefixes,
isTruncated: data.IsTruncated === 'true',
keyCount: Number.parseInt(data.KeyCount),
continuationToken: data.ContinuationToken,
nextContinuationToken: data.NextContinuationToken,
} satisfies ListV2ObjectResult;
}
/**
* GetObject
* @see https://help.aliyun.com/zh/oss/developer-reference/getobject
*/
async get(name: string, options?: GetObjectOptions): Promise<GetObjectResult>;
async get(
name: string,
file: string | Writable,
options?: GetObjectOptions
): Promise<GetObjectResult>;
async get(
name: string,
file?: string | Writable | GetObjectOptions,
options?: GetObjectOptions
): Promise<GetObjectResult> {
let writeStream: Writable | undefined;
let needDestroy = false;
if (isWritable(file)) {
writeStream = file;
} else if (typeof file === 'string') {
writeStream = createWriteStream(file);
needDestroy = true;
} else {
// get(name, options)
options = file;
}
options = this.#formatGetOptions(options);
let result: OSSResult<Buffer>;
try {
const params = this.#objectRequestParams('GET', name, options);
params.writeStream = writeStream;
params.successStatuses = [200, 206, 304];
result = await this.request<Buffer>(params);
if (needDestroy && writeStream) {
writeStream.destroy();
}
} catch (err) {
if (needDestroy && writeStream) {
writeStream.destroy();
// should delete the exists file before throw error
await fs.rm(file as string, { force: true });
}
throw err;
}
return {
res: result.res,
content: result.data,
};
}
async getStream(
name: string,
options?: GetStreamOptions
): Promise<GetStreamResult> {
options = this.#formatGetOptions(options);
const params = this.#objectRequestParams('GET', name, options);
params.streaming = true;
params.successStatuses = [200, 206, 304];
const { res } = await this.request(params);
return {
stream: res,
res,
} satisfies GetStreamResult;
}
/**
* PutObjectACL
* @see https://help.aliyun.com/zh/oss/developer-reference/putobjectacl
*/
async putACL(
name: string,
acl: ACLType,
options?: PutACLOptions
): Promise<PutACLResult> {
options = options ?? {};
if (options.subres && !options.subResource) {
options.subResource = options.subres;
}
if (!options.subResource) {
options.subResource = {};
}
options.subResource.acl = '';
if (options.versionId) {
options.subResource.versionId = options.versionId;
}
options.headers = options.headers ?? {};
options.headers['x-oss-object-acl'] = acl;
name = this.#objectName(name);
const params = this.#objectRequestParams('PUT', name, options);
params.successStatuses = [200];
const { res } = await this.request(params);
return {
res,
} satisfies PutACLResult;
}
/**
* GetObjectACL
* @see https://help.aliyun.com/zh/oss/developer-reference/getobjectacl
*/
async getACL(name: string, options?: GetACLOptions): Promise<GetACLResult> {
options = options ?? {};
if (options.subres && !options.subResource) {
options.subResource = options.subres;
delete options.subres;
}
if (!options.subResource) {
options.subResource = {};
}
options.subResource.acl = '';
if (options.versionId) {
options.subResource.versionId = options.versionId;
}
name = this.#objectName(name);
const params = this.#objectRequestParams('GET', name, options);
params.successStatuses = [200];
params.xmlResponse = true;
const { data, res } = await this.request(params);
return {
acl: data.AccessControlList.Grant,
owner: {
id: data.Owner.ID,
displayName: data.Owner.DisplayName,
},
res,
} satisfies GetACLResult;
}
// /**
// * Restore Object
// * @param {String} name the object key
// * @param {Object} options {type : Archive or ColdArchive}
// * @return {{res}} result
// */
// proto.restore = async function restore(name, options = { type: 'Archive' }) {
// options = options || {};
// options.subres = Object.assign({ restore: '' }, options.subres);
// if (options.versionId) {
// options.subres.versionId = options.versionId;
// }
// const params = this._objectRequestParams('POST', name, options);
// if (options.type === 'ColdArchive') {
// const paramsXMLObj = {
// RestoreRequest: {
// Days: options.Days ? options.Days : 2,
// JobParameters: {
// Tier: options.JobParameters ? options.JobParameters : 'Standard',
// },
// },
// };
// params.content = obj2xml(paramsXMLObj, {
// headers: true,
// });
// params.mime = 'xml';
// }
// params.successStatuses = [ 202 ];
// const result = await this.request(params);
// return {
// res: result.res,
// };
// };
/**
* DeleteObject
* @see https://help.aliyun.com/zh/oss/developer-reference/deleteobject
*/
async delete(
name: string,
options?: DeleteObjectOptions
): Promise<DeleteObjectResult> {
const requestOptions = {
timeout: options?.timeout,
subResource: {} as Record<string, string>,
};
if (options?.versionId) {
requestOptions.subResource.versionId = options.versionId;
}
const params = this.#objectRequestParams('DELETE', name, requestOptions);
params.successStatuses = [204];
const { res } = await this.request(params);
return {
res,
status: res.status,
headers: res.headers,
size: res.size,
rt: res.rt,
};
}
/**
* DeleteMultipleObjects
* @see https://help.aliyun.com/zh/oss/developer-reference/deletemultipleobjects
*/
async deleteMulti(
namesOrObjects: string[] | DeleteMultipleObject[],
options?: DeleteMultipleObjectOptions
): Promise<DeleteMultipleObjectResponse> {
const objects: DeleteMultipleObjectXML[] = [];
assert.ok(namesOrObjects.length > 0, 'namesOrObjects is empty');
for (const nameOrObject of namesOrObjects) {
if (typeof nameOrObject === 'string') {
objects.push({ Key: this.#objectName(nameOrObject) });
} else {
assert.ok(nameOrObject.key, 'key is empty');
objects.push({
Key: this.#objectName(nameOrObject.key),
VersionId: nameOrObject.versionId,
});
}
}
const xml = json2xml(
{
Delete: {
Quiet: !!options?.quiet,
Object: objects,
},
},
{ headers: true }
);
const requestOptions = {
timeout: options?.timeout,
// ?delete
subResource: { delete: '' } as Record<string, string>,
};
if (options?.versionId) {
requestOptions.subResource.versionId = options.versionId;
}
const params = this.#objectRequestParams('POST', '', requestOptions);
params.mime = 'xml';
params.content = Buffer.from(xml, 'utf8');
params.xmlResponse = true;
params.successStatuses = [200];
const { data, res } = await this.request(params);
// quiet will return null
let deleted = data?.Deleted || [];
if (deleted && !Array.isArray(deleted)) {
deleted = [deleted];
}
return {
res,
deleted,
} satisfies DeleteMultipleObjectResponse;
}
/**
* HeadObject
* @see https://help.aliyun.com/zh/oss/developer-reference/headobject
*/
async head(
name: string,
options?: HeadObjectOptions
): Promise<HeadObjectResult> {
options = options ?? {};
if (options.subres && !options.subResource) {
options.subResource = options.subres;
}
if (options.versionId) {
if (!options.subResource) {
options.subResource = {};
}
options.subResource.versionId = options.versionId;
}
const params = this.#objectRequestParams('HEAD', name, options);
params.successStatuses = [200, 304];
const { res } = await this.request(params);
const meta: UserMeta = {};
const result = {
meta,
res,
status: res.status,
} satisfies HeadObjectResult;
for (const k in res.headers) {
if (k.startsWith('x-oss-meta-')) {
const key = k.slice(11);
meta[key] = res.headers[k] as string;
}
}
return result;
}
/**
* GetObjectMeta
* @see https://help.aliyun.com/zh/oss/developer-reference/getobjectmeta
*/
async getObjectMeta(name: string, options?: HeadObjectOptions) {
options = options ?? {};
name = this.#objectName(name);
if (options.subres && !options.subResource) {
options.subResource = options.subres;
}
if (!options.subResource) {
options.subResource = {};
}
if (options.versionId) {
options.subResource.versionId = options.versionId;
}
options.subResource.objectMeta = '';
const params = this.#objectRequestParams('HEAD', name, options);
params.successStatuses = [200];
const { res } = await this.request(params);
return {
status: res.status,
res,
};
}
/**
* PutSymlink
* @see https://help.aliyun.com/zh/oss/developer-reference/putsymlink
*/
async putSymlink(
name: string,
targetName: string,
options: PutSymlinkOptions
): Promise<PutSymlinkResult> {
options = options ?? {};
if (!options.subResource) {
options.subResource = {};
}
options.subResource.symlink = '';
if (options.versionId) {
options.subResource.versionId = options.versionId;
}
options.headers = options.headers ?? {};
this.#convertMetaToHeaders(options.meta, options.headers);
targetName = this.escape(this.#objectName(targetName));
options.headers['x-oss-symlink-target'] = targetName;
if (options.storageClass) {
options.headers['x-oss-storage-class'] = options.storageClass;
}
name = this.#objectName(name);
const params = this.#objectRequestParams('PUT', name, options);
params.successStatuses = [200];
const { res } = await this.request(params);
return {
res,
};
}
/**
* GetSymlink
* @see https://help.aliyun.com/zh/oss/developer-reference/getsymlink
*/
async getSymlink(
name: string,
options?: GetSymlinkOptions
): Promise<GetSymlinkResult> {
options = options ?? {};
if (!options.subResource) {
options.subResource = {};
}
options.subResource.symlink = '';
if (options.versionId) {
options.subResource.versionId = options.versionId;
}
name = this.#objectName(name);
const params = this.#objectRequestParams('GET', name, options);
params.successStatuses = [200];
const { res } = await this.request(params);
const target = res.headers['x-oss-symlink-target'] as string;
const meta: Record<string, string> = {};
for (const k in res.headers) {
if (k.startsWith('x-oss-meta-')) {
const key = k.slice(11);
meta[key] = res.headers[k] as string;
}
}
return {
targetName: decodeURIComponent(target),
res,
meta,
};
}
/**
* PutObjectTagging
* @see https://help.aliyun.com/zh/oss/developer-reference/putobjecttagging
*/
async putObjectTagging(
name: string,
tag: Record<string, string>,
options?: PutObjectTaggingOptions
): Promise<PutObjectTaggingResult> {
checkObjectTag(tag);
options = options ?? {};
if (!options.subResource) {
options.subResource = {};
}
options.subResource.tagging = '';
if (options.versionId) {
options.subResource.versionId = options.versionId;
}
name = this.#objectName(name);
const params = this.#objectRequestParams('PUT', name, options);
params.successStatuses = [200];
const tags: { Key: string; Value: string }[] = [];
for (const key in tag) {
tags.push({ Key: key, Value: tag[key] });
}
const paramXMLObj = {
Tagging: {
TagSet: {
Tag: tags,
},
},
};
params.mime = 'xml';
params.content = Buffer.from(json2xml(paramXMLObj));
const { res } = await this.request(params);
return {
res,
status: res.status,
};
}
/**
* GetObjectTagging
* @see https://help.aliyun.com/zh/oss/developer-reference/getobjecttagging
*/
async getObjectTagging(
name: string,
options?: GutObjectTaggingOptions
): Promise<GutObjectTaggingResult> {
options = options ?? {};
if (!options.subResource) {
options.subResource = {};
}
options.subResource.tagging = '';
if (options.versionId) {
options.subResource.versionId = options.versionId;
}
name = this.#objectName(name);
const params = this.#objectRequestParams('GET', name, options);
params.successStatuses = [200];
params.xmlResponse = true;
const { res, data } = await this.request(params);
// console.log(data.toString());
let tags = data.TagSet?.Tag;
if (tags && !Array.isArray(tags)) {
tags = [tags];
}
const tag: Record<string, string> = {};
if (tags) {
for (const item of tags) {
tag[item.Key] = item.Value;
}
}
return {
status: res.status,
res,
tag,
};
}
/**
* DeleteObjectTagging
* @see https://help.aliyun.com/zh/oss/developer-reference/deleteobjecttagging
*/
async deleteObjectTagging(
name: string,
options?: DeleteObjectTaggingOptions
): Promise<DeleteObjectTaggingResult> {
options = options ?? {};
if (!options.subResource) {
options.subResource = {};
}
options.subResource.tagging = '';
if (options.versionId) {
options.subResource.versionId = options.versionId;
}
name = this.#objectName(name);
const params = this.#objectRequestParams('DELETE', name, options);
params.successStatuses = [204];
const { res } = await this.request(params);
return {
status: res.status,
res,
};
}
/**
* signatureUrl URL签名
* @see https://help.aliyun.com/zh/oss/developer-reference/signed-urls
*/
signatureUrl(name: string, options?: SignatureUrlOptions) {
options = options ?? {};
name = this.#objectName(name);
options.method = options.method ?? 'GET';
const expires = options.expires ?? 1800;
const expiresTimestamp = timestamp() + expires;
const params = {
bucket: this.#bucket,
object: name,
};
const resource = this.getResource(params);
const signRes = signatureForURL(
this.options.accessKeySecret,
options,
resource,
expiresTimestamp
);
const url = this.getRequestURL({
object: name,
subResource: {
OSSAccessKeyId: this.options.accessKeyId,
Expires: expiresTimestamp,
Signature: signRes.Signature,
...signRes.subResource,
},
});
return url;
}
async asyncSignatureUrl(name: string, options?: SignatureUrlOptions) {
return this.signatureUrl(name, options);
}
/**
* Get Object url by name
* @param {string} name - object name
* @param {string} [baseUrl] - If provide `baseUrl`, will use `baseUrl` instead the default `endpoint and bucket`.
* @returns {string} object url include bucket
*/
generateObjectUrl(name: string, baseUrl?: string) {
const urlObject = new URL(baseUrl ?? this.getRequestEndpoint());
urlObject.pathname = this.escape(this.#objectName(name));
return urlObject.toString();
}
/**
* @alias generateObjectUrl
*/
getObjectUrl(name: string, baseUrl?: string) {
return this.generateObjectUrl(name, baseUrl);
}
/**
* @param {object | string} policy specifies the validity of the fields in the request.
* @returns {object} params.OSSAccessKeyId
* params.Signature
* params.policy JSON text encoded with UTF-8 and Base64.
*/
calculatePostSignature(policy: object | string) {
if (typeof policy !== 'object' && typeof policy !== 'string') {
throw new TypeError('policy must be JSON string or Object');
}
const policyString = Buffer.from(
policyToJSONString(policy),
'utf8'
).toString('base64');
const Signature = computeSignature(
this.options.accessKeySecret,
policyString
);
return {
OSSAccessKeyId: this.options.accessKeyId,
Signature,
policy: policyString,
};
}
/**
* Copy an object from sourceName to name.
*/
async copy(
name: string,
sourceName: string,
options?: CopyObjectOptions
): Promise<CopyAndPutMetaResult>;
async copy(
name: string,
sourceName: string,
sourceBucket: string,
options?: CopyObjectOptions
): Promise<CopyAndPutMetaResult>;
async copy(
name: string,
sourceName: string,
sourceBucket?: string | CopyObjectOptions,
options?: CopyObjectOptions
): Promise<CopyAndPutMetaResult> {
if (typeof sourceBucket === 'object') {
options = sourceBucket; // 兼容旧版本,旧版本第三个参数为options
sourceBucket = undefined;
}
options = options ?? {};
options.headers = options.headers ?? {};
let hasMetadata = !!options.meta;
const REPLACE_HEADERS = new Set([
'content-type',
'content-encoding',
'content-language',
'content-disposition',
'cache-control',
'expires',
]);
for (const key in options.headers) {
const lowerCaseKey = key.toLowerCase();
options.headers[`x-oss-copy-source-${lowerCaseKey}`] =
options.headers[key];
if (REPLACE_HEADERS.has(lowerCaseKey)) {
hasMetadata = true;
}
}
if (hasMetadata) {
options.headers['x-oss-metadata-directive'] = 'REPLACE';
}
this.#convertMetaToHeaders(options.meta, options.headers);
sourceName = this.#getCopySourceName(sourceName, sourceBucket);
if (options.versionId) {
sourceName = `${sourceName}?versionId=${options.versionId}`;
}
options.headers['x-oss-copy-source'] = sourceName;
const params = this.#objectRequestParams('PUT', name, options);
params.xmlResponse = true;
params.successStatuses = [200, 304];
const { data, res } = await this.request(params);
return {
data: data
? {
etag: data.ETag ?? '',
lastModified: data.LastModified ?? '',
}
: null,
res,
} satisfies CopyAndPutMetaResult;
}
/**
* 另存为
* @see https://help.aliyun.com/zh/oss/user-guide/sys-or-saveas
*/
async processObjectSave(
sourceObject: string,
targetObject: string,
process: string,
targetBucket?: string
) {
targetObject = this.#objectName(targetObject);
const params = this.#objectRequestParams('POST', sourceObject, {
subResource: {
'x-oss-process': '',
},
});
const bucketParam = targetBucket
? `,b_${Buffer.from(targetBucket).toString('base64')}`
: '';
targetObject = Buffer.from(targetObject).toString('base64');
const content = {
'x-oss-process': `${process}|sys/saveas,o_${targetObject}${bucketParam}`,
};
params.content = Buffer.from(querystring.stringify(content));
params.successStatuses = [200];
const result = await this.request(params);
return {
res: result.res,
status: result.res.status,
};
}
/** protected methods */
protected getRequestEndpoint(): string {
return this.#bucketEndpoint;
}
/** private methods */
#getCopySourceName(sourceName: string, bucketName?: string) {
if (typeof bucketName === 'string') {
sourceName = this.#objectName(sourceName);
// eslint-disable-next-line no-negated-condition
} else if (sourceName[0] !== '/') {
bucketName = this.#bucket;
} else {
bucketName = sourceName.replace(/\/(.+?)(\/.*)/, '$1');
sourceName = sourceName.replace(/(\/.+?\/)(.*)/, '$2');
}
sourceName = encodeURIComponent(sourceName);
if (bucketName) {
checkBucketName(bucketName);
sourceName = `/${bucketName}/${sourceName}`;
}
return sourceName;
}
async #sendPutRequest(
name: string,
options: PutObjectOptions & { subResource?: Record<string, string> },
fileOrBufferOrStream: string | Buffer | Readable,
method: RequestMethod = 'PUT'
) {
options.headers = options.headers ?? {};
if (options.headers['Content-Type'] && !options.headers['content-type']) {
options.headers['content-type'] = options.headers[
'Content-Type'
] as string;
delete options.headers['Content-Type'];
}
name = this.#objectName(name);
this.#convertMetaToHeaders(options.meta, options.headers);
// don't override exists headers
if (options.callback && !options.headers['x-oss-callback']) {
const callbackOptions = encodeCallback(options.callback);
options.headers['x-oss-callback'] = callbackOptions.callback;
if (callbackOptions.callbackVar) {
options.headers['x-oss-callback-var'] = callbackOptions.callbackVar;
}
}
const params = this.#objectRequestParams(method, name, options);
if (typeof fileOrBufferOrStream === 'string') {
const stats = await fs.stat(fileOrBufferOrStream);
if (!stats.isFile()) {
throw new TypeError(`${fileOrBufferOrStream} is not file`);
}
if (!options.mime) {
const mimeFromFile = mime.getType(fileOrBufferOrStream);
if (mimeFromFile) {
options.mime = mimeFromFile;
}
}
params.stream = createReadStream(fileOrBufferOrStream);
} else if (Buffer.isBuffer(fileOrBufferOrStream)) {
params.content = fileOrBufferOrStream;
} else {
params.stream = fileOrBufferOrStream;
}
params.mime = options.mime;
params.successStatuses = [200];
const { res, data } = await this.request<Buffer>(params);
const putResult = {
name,
url: this.#objectUrl(name),
res,
data: {},
} satisfies PutObjectResult;
if (params.headers?.['x-oss-callback']) {
putResult.data = JSON.parse(data.toString());
}
return putResult;
}
#objectUrl(name: string) {
return this.getRequestURL({ object: name });
}
#formatGetOptions(options?: GetObjectOptions) {
options = options ?? {};
// 兼容老的 subres 参数
if (options.subres && !options.subResource) {
options.subResource = options.subres;
}
if (!options.subResource) {
options.subResource = {};
}
if (options.versionId) {
options.subResource.versionId = options.versionId;
}
if (options.process) {
options.subResource['x-oss-process'] = options.process;
}
return options;
}
/**
* generator request params
*/
#objectRequestParams(
method: RequestMethod,
name: string,
options?: Pick<OSSRequestParams, 'headers' | 'subResource' | 'timeout'>
) {
name = this.#objectName(name);
const params: OSSRequestParams = {
object: name,
bucket: this.#bucket,
method,
headers: options?.headers,
subResource: options?.subResource,
timeout: options?.timeout,
};
return params;
}
#objectName(name: string) {
return name.replace(/^\/+/, '');
}
#convertMetaToHeaders(
meta: UserMeta | undefined,
headers: IncomingHttpHeaders
) {
if (!meta) {
return;
}
for (const key in meta) {
headers[`x-oss-meta-${key}`] = `${meta[key]}`;
}
}
}