mirror of
https://github.com/cool-team-official/cool-admin-midway-packages.git
synced 2025-12-15 08:15:36 +00:00
569 lines
15 KiB
TypeScript
569 lines
15 KiB
TypeScript
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('上传失败');
|
||
}
|
||
}
|
||
}
|