569 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
App,
Config,
Init,
Logger,
Provide,
Scope,
ScopeEnum,
} from '@midwayjs/decorator';
import { Mode, CoolFileConfig, MODETYPE, CLOUDTYPE } from './interface';
import { CoolCommException } from '@cool-midway/core';
import * as moment from 'moment';
import { v1 as uuid } from 'uuid';
import * as path from 'path';
import * as fs from 'fs';
import { ILogger, IMidwayApplication } from '@midwayjs/core';
import * as _ from 'lodash';
import * as OSS from 'ali-oss';
import * as crypto from 'crypto';
import * as STS from 'qcloud-cos-sts';
import * as download from 'download';
import * as COS from 'cos-nodejs-sdk-v5';
import * as QINIU from 'qiniu';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
/**
* 文件上传
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class CoolFile {
@Config('cool.file')
config: CoolFileConfig;
@Logger()
coreLogger: ILogger;
client: OSS & COS & QINIU.auth.digest.Mac & S3Client;
@App()
app: IMidwayApplication;
@Init()
async init(config: CoolFileConfig) {
const filePath = path.join(this.app.getBaseDir(), '..', 'public');
const uploadsPath = path.join(filePath, 'uploads');
const tempPath = path.join(filePath, 'temp');
if (!fs.existsSync(uploadsPath)) {
fs.mkdirSync(uploadsPath);
}
if (!fs.existsSync(tempPath)) {
fs.mkdirSync(tempPath);
}
if (config) {
this.config = config;
}
const { mode, oss, cos, qiniu, aws } = this.config;
if (mode == MODETYPE.CLOUD) {
if (oss) {
const { accessKeyId, accessKeySecret, bucket, endpoint } = oss;
this.client = new OSS({
region: endpoint.split('.')[0],
accessKeyId,
accessKeySecret,
bucket,
});
}
if (cos) {
const { accessKeyId, accessKeySecret } = cos;
this.client = new COS({
SecretId: accessKeyId,
SecretKey: accessKeySecret,
});
}
if (qiniu) {
const { accessKeyId, accessKeySecret } = qiniu;
this.client = new QINIU.auth.digest.Mac(accessKeyId, accessKeySecret);
}
if (aws) {
const {
accessKeyId,
secretAccessKey,
region,
publicDomain,
forcePathStyle,
} = aws;
this.client = new S3Client({
region,
credentials: { accessKeyId, secretAccessKey },
// 支持自定义s3服务如minio等
endpoint: publicDomain
? publicDomain
: `https://s3.${region}.amazonaws.com`,
// minio 使用域名时forcePathStyle为true时不增加二级域名而是 xx.com/bucket的形式
forcePathStyle: forcePathStyle ? forcePathStyle : false,
});
}
}
}
/**
* 上传模式
* @returns 上传模式
*/
async getMode(): Promise<Mode> {
const { mode, oss, cos, qiniu, aws } = this.config;
if (mode == MODETYPE.LOCAL) {
return {
mode: MODETYPE.LOCAL,
type: MODETYPE.LOCAL,
};
}
if (oss) {
return {
mode: MODETYPE.CLOUD,
type: CLOUDTYPE.OSS,
};
}
if (cos) {
return {
mode: MODETYPE.CLOUD,
type: CLOUDTYPE.COS,
};
}
if (qiniu) {
return {
mode: MODETYPE.CLOUD,
type: CLOUDTYPE.QINIU,
};
}
if (aws) {
return {
mode: MODETYPE.CLOUD,
type: CLOUDTYPE.AWS,
};
}
}
/**
* 获得原始操作对象
* @returns
*/
getMetaFileObj(): OSS & COS & QINIU.auth.digest.Mac & S3Client {
return this.client;
}
/**
* 下载并上传
* @param url
* @param fileName 文件名
*/
async downAndUpload(url: string, fileName?: string) {
const { mode, oss, cos, qiniu, aws, domain } = this.config;
let extend = '';
if (url.includes('.')) {
const urlArr = url.split('.');
extend = '.' + urlArr[urlArr.length - 1].split('?')[0];
}
const data = url.includes('http')
? await download(url)
: fs.readFileSync(url);
const isCloud = mode == MODETYPE.CLOUD;
// 创建文件夹
const dirPath = path.join(
this.app.getBaseDir(),
'..',
`public/${isCloud ? 'temp' : 'uploads'}/${moment().format('YYYYMMDD')}`
);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
const uuidStr = uuid();
const name = `uploads/${moment().format('YYYYMMDD')}/${
fileName ? fileName : uuidStr + extend
}`;
if (isCloud) {
if (oss) {
const ossClient: OSS = this.getMetaFileObj();
const { url } = await ossClient.put(name, data);
return oss.publicDomain ? `${oss.publicDomain}/${name}` : url;
}
if (cos) {
const cosClient: COS = this.getMetaFileObj();
await cosClient.putObject({
Bucket: cos.bucket,
Region: cos.region,
Key: name,
Body: data,
});
return cos.publicDomain + '/' + name;
}
if (aws) {
const { bucket, fields, region, publicDomain } = aws;
const uploadParams = {
Bucket: bucket,
Key: name,
Body: data,
ACL: fields ? fields.acl : 'public-read',
};
const command = new PutObjectCommand(uploadParams);
await this.client.send(command);
return publicDomain
? `${publicDomain}/${name}`
: `https://${bucket}.s3.${region}.amazonaws.com/${name}`;
}
if (qiniu) {
let uploadToken = (await this.qiniu())['token'];
const formUploader = new QINIU.form_up.FormUploader();
const putExtra = new QINIU.form_up.PutExtra();
return new Promise((resolve, reject) => {
formUploader.put(
uploadToken,
name,
data,
putExtra,
(respErr, respBody, respInfo) => {
if (respErr) {
throw respErr;
}
if (respInfo.statusCode == 200) {
resolve(qiniu.publicDomain + '/' + name);
}
}
);
});
}
} else {
fs.writeFileSync(
`${dirPath}/${fileName ? fileName : uuidStr + extend}`,
data
);
return `${domain}/public/${name}`;
}
}
/**
* 指定Key(路径)上传
* @param filePath 文件路径
* @param key 路径一致会覆盖源文件
*/
async uploadWithKey(filePath, key) {
const { mode, oss, cos, qiniu, aws } = this.config;
const data = fs.readFileSync(filePath);
if (mode == MODETYPE.LOCAL) {
fs.writeFileSync(path.join(this.app.getBaseDir(), '..', key), data);
return this.config.domain + key;
}
if (mode == MODETYPE.CLOUD) {
if (oss) {
const ossClient: OSS = this.getMetaFileObj();
const { url } = await ossClient.put(key, data);
return oss.publicDomain ? `${oss.publicDomain}/${key}` : url;
}
if (cos) {
const cosClient: COS = this.getMetaFileObj();
await cosClient.putObject({
Bucket: cos.bucket,
Region: cos.region,
Key: key,
Body: data,
});
return cos.publicDomain + '/' + key;
}
if (qiniu) {
let uploadToken = (await this.qiniu())['token'];
const formUploader = new QINIU.form_up.FormUploader();
const putExtra = new QINIU.form_up.PutExtra();
return new Promise((resolve, reject) => {
formUploader.put(
uploadToken,
key,
data,
putExtra,
(respErr, respBody, respInfo) => {
if (respErr) {
throw respErr;
}
if (respInfo.statusCode == 200) {
resolve(qiniu.publicDomain + '/' + key);
}
}
);
});
}
if (aws) {
const { bucket, fields, region, publicDomain } = aws;
const uploadParams = {
Bucket: bucket,
Key: key,
Body: data,
ACL: fields ? fields.acl : 'public-read',
};
const command = new PutObjectCommand(uploadParams);
await this.client.send(command);
return publicDomain
? `${publicDomain}/${key}`
: `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
}
}
}
/**
* 上传文件
* @param ctx
* @param key 文件路径
*/
async upload(ctx) {
const { mode, oss, cos, qiniu, aws } = this.config;
if (mode == MODETYPE.LOCAL) {
return await this.local(ctx);
}
if (mode == MODETYPE.CLOUD) {
if (oss) {
return await this.oss(ctx);
}
if (cos) {
return await this.cos(ctx);
}
if (qiniu) {
return await this.qiniu(ctx);
}
if (aws) {
return await this.aws(ctx);
}
}
}
/**
* aws 文件上传
* @param ctx
*/
private async aws(ctx) {
let {
bucket,
fields = {},
conditions = [],
expires = 3600,
} = this.config.aws;
const { key } = ctx.request.body;
if (!conditions) {
conditions = [{ acl: 'public-read' }, { bucket }];
}
if (_.isEmpty(fields)) {
fields = {
acl: 'public-read',
};
}
const result = await createPresignedPost(this.client, {
Bucket: bucket,
Key: key,
Conditions: conditions,
Fields: fields,
Expires: expires,
});
return result;
}
/**
* 七牛上传
* @param ctx
* @returns
*/
private async qiniu(ctx?) {
const {
bucket,
publicDomain,
region,
uploadUrl = `https://upload-${region}.qiniup.com/`,
fileKey = 'file',
} = this.config.qiniu;
let options = {
scope: bucket,
};
const putPolicy = new QINIU.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(this.client);
return new Promise((resolve, reject) => {
resolve({
uploadUrl,
publicDomain,
token: uploadToken,
fileKey,
});
});
}
/**
* OSS 文件上传
* @param ctx
*/
private async oss(ctx) {
const {
accessKeyId,
accessKeySecret,
bucket,
endpoint,
expAfter = 300000,
maxSize = 200 * 1024 * 1024,
host,
publicDomain,
} = this.config.oss;
const oss = {
bucket,
region: endpoint.split('.')[0], // 我的是 hangzhou
accessKeyId,
accessKeySecret,
expAfter, // 签名失效时间,毫秒
maxSize, // 文件最大的 size
};
const newHost = host ? host : `https://${bucket}.${endpoint}`;
const expireTime = new Date().getTime() + oss.expAfter;
const expiration = new Date(expireTime).toISOString();
const policyString = JSON.stringify({
expiration,
conditions: [
['content-length-range', 0, oss.maxSize], // 设置上传文件的大小限制,200mb
],
});
const policy = Buffer.from(policyString).toString('base64');
const signature = crypto
.createHmac('sha1', oss.accessKeySecret)
.update(policy)
.digest('base64');
return {
signature,
policy,
host: newHost,
publicDomain,
OSSAccessKeyId: accessKeyId,
success_action_status: 200,
};
}
/**
* COS 文件上传
* @param ctx
*/
private async cos(ctx) {
const {
accessKeyId,
accessKeySecret,
bucket,
region,
publicDomain,
durationSeconds = 1800,
allowPrefix = '_ALLOW_DIR_/*',
allowActions = [
// 所有 action 请看文档 https://cloud.tencent.com/document/product/436/31923
// 简单上传
'name/cos:PutObject',
'name/cos:PostObject',
// 分片上传
'name/cos:InitiateMultipartUpload',
'name/cos:ListMultipartUploads',
'name/cos:ListParts',
'name/cos:UploadPart',
'name/cos:CompleteMultipartUpload',
],
} = this.config.cos;
// 配置参数
let config = {
secretId: accessKeyId,
secretKey: accessKeySecret,
durationSeconds,
bucket: bucket,
region: region,
// 允许操作(上传)的对象前缀,可以根据自己网站的用户登录态判断允许上传的目录,例子: user1/* 或者 * 或者a.jpg
// 请注意当使用 * 时可能存在安全风险详情请参阅https://cloud.tencent.com/document/product/436/40265
allowPrefix,
// 密钥的权限列表
allowActions,
};
// 获取临时密钥
let LongBucketName = config.bucket;
let ShortBucketName = LongBucketName.substring(
0,
LongBucketName.lastIndexOf('-')
);
let AppId = LongBucketName.substring(LongBucketName.lastIndexOf('-') + 1);
let policy = {
version: '2.0',
statement: [
{
action: config.allowActions,
effect: 'allow',
resource: [
'qcs::cos:' +
config.region +
':uid/' +
AppId +
':prefix//' +
AppId +
'/' +
ShortBucketName +
'/' +
config.allowPrefix,
],
},
],
};
return new Promise((resolve, reject) => {
STS.getCredential(
{
secretId: config.secretId,
secretKey: config.secretKey,
durationSeconds: config.durationSeconds,
policy: policy,
},
(err, tempKeys) => {
if (err) {
reject(err);
}
if (tempKeys) {
tempKeys.startTime = Math.round(Date.now() / 1000);
}
resolve({
...tempKeys,
url: publicDomain,
});
}
);
});
}
/**
* 本地上传
* @param ctx
* @returns
*/
private async local(ctx) {
try {
const { key } = ctx.fields;
if (_.isEmpty(ctx.files)) {
throw new CoolCommException('上传文件为空');
}
const file = ctx.files[0];
const extension = file.filename.split('.').pop();
const name =
moment().format('YYYYMMDD') + '/' + (key || `${uuid()}.${extension}`);
const target = path.join(
this.app.getBaseDir(),
'..',
`public/uploads/${name}`
);
const dirPath = path.join(
this.app.getBaseDir(),
'..',
`public/uploads/${moment().format('YYYYMMDD')}`
);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
const data = fs.readFileSync(file.data);
fs.writeFileSync(target, data);
return this.config.domain + '/public/uploads/' + name;
} catch (err) {
this.coreLogger.error(err);
throw new CoolCommException('上传失败');
}
}
}