oa_based/src/modules/netdisk/manager/manage-qiniu.service.ts

837 lines
26 KiB
TypeScript
Raw Normal View History

2024-03-08 16:24:29 +08:00
import { basename, extname } from 'node:path';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { isEmpty } from 'lodash';
import * as qiniu from 'qiniu';
import { auth, conf, rs } from 'qiniu';
import { ConfigKeyPaths } from '~/config';
import {
NETDISK_COPY_SUFFIX,
NETDISK_DELIMITER,
NETDISK_HANDLE_MAX_ITEM,
NETDISK_LIMIT
} from '~/constants/oss.constant';
import { AccountInfo } from '~/modules/user/user.model';
import { UserService } from '~/modules/user/user.service';
import { generateRandomValue } from '~/utils';
import { SFileInfo, SFileInfoDetail, SFileList } from './manage.class';
import { FileOpItem } from './manage.dto';
@Injectable()
export class QiNiuNetDiskManageService {
private config: conf.ConfigOptions;
private mac: auth.digest.Mac;
private bucketManager: rs.BucketManager;
private get qiniuConfig() {
return this.configService.get('oss', { infer: true });
}
constructor(
private configService: ConfigService<ConfigKeyPaths>,
private userService: UserService
) {
this.mac = new qiniu.auth.digest.Mac(this.qiniuConfig.accessKey, this.qiniuConfig.secretKey);
this.config = new qiniu.conf.Config({
zone: this.qiniuConfig.zone
});
// bucket manager
this.bucketManager = new qiniu.rs.BucketManager(this.mac, this.config);
}
/**
*
* @param prefix
* @param marker
* @returns iFileListResult
*/
async getFileList(prefix = '', marker = '', skey = ''): Promise<SFileList> {
// 是否需要搜索
const searching = !isEmpty(skey);
return new Promise<SFileList>((resolve, reject) => {
this.bucketManager.listPrefix(
this.qiniuConfig.bucket,
{
prefix: searching ? '' : prefix,
limit: NETDISK_LIMIT,
delimiter: searching ? '' : NETDISK_DELIMITER,
marker
},
(err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
// 如果这个nextMarker不为空那么还有未列举完毕的文件列表下次调用listPrefix的时候
// 指定options里面的marker为这个值
const fileList: SFileInfo[] = [];
// 处理目录,但只有非搜索模式下可用
if (!searching && !isEmpty(respBody.commonPrefixes)) {
// dir
for (const dirPath of respBody.commonPrefixes) {
const name = (dirPath as string).substr(0, dirPath.length - 1).replace(prefix, '');
if (isEmpty(skey) || name.includes(skey)) {
fileList.push({
name: (dirPath as string).substr(0, dirPath.length - 1).replace(prefix, ''),
type: 'dir',
id: generateRandomValue(10)
});
}
}
}
// handle items
if (!isEmpty(respBody.items)) {
// file
for (const item of respBody.items) {
// 搜索模式下处理
if (searching) {
const pathList: string[] = item.key.split(NETDISK_DELIMITER);
// dir is empty stirng, file is key string
const name = pathList.pop();
if (
item.key.endsWith(NETDISK_DELIMITER) &&
pathList[pathList.length - 1].includes(skey)
) {
// 结果是目录
const ditName = pathList.pop();
fileList.push({
id: generateRandomValue(10),
name: ditName,
type: 'dir',
belongTo: pathList.join(NETDISK_DELIMITER)
});
} else if (name.includes(skey)) {
// 文件
fileList.push({
id: generateRandomValue(10),
name,
type: 'file',
fsize: item.fsize,
mimeType: item.mimeType,
putTime: new Date(Number.parseInt(item.putTime) / 10000),
belongTo: pathList.join(NETDISK_DELIMITER)
});
}
} else {
// 正常获取列表
const fileKey = item.key.replace(prefix, '') as string;
if (!isEmpty(fileKey)) {
fileList.push({
id: generateRandomValue(10),
name: fileKey,
type: 'file',
fsize: item.fsize,
mimeType: item.mimeType,
putTime: new Date(Number.parseInt(item.putTime) / 10000)
});
}
}
}
}
resolve({
list: fileList,
marker: respBody.marker || null
});
} else {
reject(
new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`)
);
}
}
);
});
}
/**
*
*/
async getFileInfo(name: string, path: string): Promise<SFileInfoDetail> {
return new Promise((resolve, reject) => {
this.bucketManager.stat(
this.qiniuConfig.bucket,
`${path}${name}`,
(err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
const detailInfo: SFileInfoDetail = {
fsize: respBody.fsize,
hash: respBody.hash,
md5: respBody.md5,
mimeType: respBody.mimeType.split('/x-qn-meta')[0],
putTime: new Date(Number.parseInt(respBody.putTime) / 10000),
type: respBody.type,
uploader: '',
mark: respBody?.['x-qn-meta']?.['!mark'] ?? ''
};
if (!respBody.endUser) {
resolve(detailInfo);
} else {
this.userService
.getAccountInfo(Number.parseInt(respBody.endUser))
.then((user: AccountInfo) => {
if (isEmpty(user)) {
resolve(detailInfo);
} else {
detailInfo.uploader = user.username;
resolve(detailInfo);
}
});
}
} else {
reject(
new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`)
);
}
}
);
});
}
/**
* MimeType
*/
async changeFileHeaders(
name: string,
path: string,
headers: { [k: string]: string }
): Promise<void> {
return new Promise((resolve, reject) => {
this.bucketManager.changeHeaders(
this.qiniuConfig.bucket,
`${path}${name}`,
headers,
(err, _, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
resolve();
} else {
reject(
new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`)
);
}
}
);
});
}
/**
*
* @returns true创建成功
*/
async createDir(dirName: string): Promise<void> {
const safeDirName = dirName.endsWith('/') ? dirName : `${dirName}/`;
return new Promise((resolve, reject) => {
// 上传一个空文件以用于显示文件夹效果
const formUploader = new qiniu.form_up.FormUploader(this.config);
const putExtra = new qiniu.form_up.PutExtra();
formUploader.put(
this.createUploadToken(''),
safeDirName,
' ',
putExtra,
(respErr, respBody, respInfo) => {
if (respErr) {
reject(respErr);
return;
}
if (respInfo.statusCode === 200) {
resolve();
} else {
reject(
new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`)
);
}
}
);
});
}
/**
*
*/
async checkFileExist(filePath: string): Promise<boolean> {
return new Promise((resolve, reject) => {
// fix path end must a /
// 检测文件夹是否存在
this.bucketManager.stat(this.qiniuConfig.bucket, filePath, (respErr, respBody, respInfo) => {
if (respErr) {
reject(respErr);
return;
}
if (respInfo.statusCode === 200) {
// 文件夹存在
resolve(true);
} else if (respInfo.statusCode === 612) {
// 文件夹不存在
resolve(false);
} else {
reject(
new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`)
);
}
});
});
}
/**
* Upload Token,
* @returns upload token
*/
createUploadToken(endUser: string): string {
const policy = new qiniu.rs.PutPolicy({
scope: this.qiniuConfig.bucket,
insertOnly: 1,
fsizeLimit: 1024 ** 2 * 10,
endUser
});
const uploadToken = policy.uploadToken(this.mac);
return uploadToken;
}
/**
*
* @param dir
* @param name
*/
async renameFile(dir: string, name: string, toName: string): Promise<void> {
const fileName = `${dir}${name}`;
const toFileName = `${dir}${toName}`;
const op = {
force: true
};
return new Promise((resolve, reject) => {
this.bucketManager.move(
this.qiniuConfig.bucket,
fileName,
this.qiniuConfig.bucket,
toFileName,
op,
(err, respBody, respInfo) => {
if (err) {
reject(err);
} else {
if (respInfo.statusCode === 200) {
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`
)
);
}
}
}
);
});
}
/**
*
*/
async moveFile(dir: string, toDir: string, name: string): Promise<void> {
const fileName = `${dir}${name}`;
const toFileName = `${toDir}${name}`;
const op = {
force: true
};
return new Promise((resolve, reject) => {
this.bucketManager.move(
this.qiniuConfig.bucket,
fileName,
this.qiniuConfig.bucket,
toFileName,
op,
(err, respBody, respInfo) => {
if (err) {
reject(err);
} else {
if (respInfo.statusCode === 200) {
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`
)
);
}
}
}
);
});
}
/**
*
*/
async copyFile(dir: string, toDir: string, name: string): Promise<void> {
const fileName = `${dir}${name}`;
// 拼接文件名
const ext = extname(name);
const bn = basename(name, ext);
const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`;
const op = {
force: true
};
return new Promise((resolve, reject) => {
this.bucketManager.copy(
this.qiniuConfig.bucket,
fileName,
this.qiniuConfig.bucket,
toFileName,
op,
(err, respBody, respInfo) => {
if (err) {
reject(err);
} else {
if (respInfo.statusCode === 200) {
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`
)
);
}
}
}
);
});
}
/**
*
*/
async renameDir(path: string, name: string, toName: string): Promise<void> {
const dirName = `${path}${name}`;
const toDirName = `${path}${toName}`;
let hasFile = true;
let marker = '';
const op = {
force: true
};
const bucketName = this.qiniuConfig.bucket;
while (hasFile) {
await new Promise<void>((resolve, reject) => {
// 列举当前目录下的所有文件
this.bucketManager.listPrefix(
this.qiniuConfig.bucket,
{
prefix: dirName,
limit: NETDISK_HANDLE_MAX_ITEM,
marker
},
(err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
const moveOperations = respBody.items.map(item => {
const { key } = item;
const destKey = key.replace(dirName, toDirName);
return qiniu.rs.moveOp(bucketName, key, bucketName, destKey, op);
});
this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => {
if (err2) {
reject(err2);
return;
}
if (respInfo2.statusCode === 200) {
if (isEmpty(respBody.marker)) hasFile = false;
else marker = respBody.marker;
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}`
)
);
}
});
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`
)
);
}
}
);
});
}
}
/**
* url链接
* @param key
* @returns
*/
getDownloadLink(key: string): string {
if (this.qiniuConfig.access === 'public') {
return this.bucketManager.publicDownloadUrl(this.qiniuConfig.domain, key);
} else if (this.qiniuConfig.access === 'private') {
return this.bucketManager.privateDownloadUrl(
this.qiniuConfig.domain,
key,
Date.now() / 1000 + 36000
);
}
throw new Error('qiniu config access type not support');
}
/**
*
* @param dir
* @param name
*/
async deleteFile(dir: string, name: string): Promise<void> {
return new Promise((resolve, reject) => {
this.bucketManager.delete(
this.qiniuConfig.bucket,
`${dir}${name}`,
(err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
resolve();
} else {
reject(
new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`)
);
}
}
);
});
}
/**
*
* @param dir
* @param name
*/
async deleteMultiFileOrDir(fileList: FileOpItem[], dir: string): Promise<void> {
const files = fileList.filter(item => item.type === 'file');
if (files.length > 0) {
// 批处理文件
const copyOperations = files.map(item => {
const fileName = `${dir}${item.name}`;
return qiniu.rs.deleteOp(this.qiniuConfig.bucket, fileName);
});
await new Promise<void>((resolve, reject) => {
this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
resolve();
} else if (respInfo.statusCode === 298) {
reject(new Error('操作异常,但部分文件夹删除成功'));
} else {
reject(
new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`)
);
}
});
});
}
// 处理文件夹
const dirs = fileList.filter(item => item.type === 'dir');
if (dirs.length > 0) {
// 处理文件夹的复制
for (let i = 0; i < dirs.length; i++) {
const dirName = `${dir}${dirs[i].name}/`;
let hasFile = true;
let marker = '';
while (hasFile) {
await new Promise<void>((resolve, reject) => {
// 列举当前目录下的所有文件
this.bucketManager.listPrefix(
this.qiniuConfig.bucket,
{
prefix: dirName,
limit: NETDISK_HANDLE_MAX_ITEM,
marker
},
(err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
const moveOperations = respBody.items.map(item => {
const { key } = item;
return qiniu.rs.deleteOp(this.qiniuConfig.bucket, key);
});
this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => {
if (err2) {
reject(err2);
return;
}
if (respInfo2.statusCode === 200) {
if (isEmpty(respBody.marker)) hasFile = false;
else marker = respBody.marker;
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}`
)
);
}
});
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`
)
);
}
}
);
});
}
}
}
}
/**
*
*/
async copyMultiFileOrDir(fileList: FileOpItem[], dir: string, toDir: string): Promise<void> {
const files = fileList.filter(item => item.type === 'file');
const op = {
force: true
};
if (files.length > 0) {
// 批处理文件
const copyOperations = files.map(item => {
const fileName = `${dir}${item.name}`;
// 拼接文件名
const ext = extname(item.name);
const bn = basename(item.name, ext);
const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`;
return qiniu.rs.copyOp(
this.qiniuConfig.bucket,
fileName,
this.qiniuConfig.bucket,
toFileName,
op
);
});
await new Promise<void>((resolve, reject) => {
this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
resolve();
} else if (respInfo.statusCode === 298) {
reject(new Error('操作异常,但部分文件夹删除成功'));
} else {
reject(
new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`)
);
}
});
});
}
// 处理文件夹
const dirs = fileList.filter(item => item.type === 'dir');
if (dirs.length > 0) {
// 处理文件夹的复制
for (let i = 0; i < dirs.length; i++) {
const dirName = `${dir}${dirs[i].name}/`;
const copyDirName = `${toDir}${dirs[i].name}${NETDISK_COPY_SUFFIX}/`;
let hasFile = true;
let marker = '';
while (hasFile) {
await new Promise<void>((resolve, reject) => {
// 列举当前目录下的所有文件
this.bucketManager.listPrefix(
this.qiniuConfig.bucket,
{
prefix: dirName,
limit: NETDISK_HANDLE_MAX_ITEM,
marker
},
(err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
const moveOperations = respBody.items.map(item => {
const { key } = item;
const destKey = key.replace(dirName, copyDirName);
return qiniu.rs.copyOp(
this.qiniuConfig.bucket,
key,
this.qiniuConfig.bucket,
destKey,
op
);
});
this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => {
if (err2) {
reject(err2);
return;
}
if (respInfo2.statusCode === 200) {
if (isEmpty(respBody.marker)) hasFile = false;
else marker = respBody.marker;
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}`
)
);
}
});
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`
)
);
}
}
);
});
}
}
}
}
/**
*
*/
async moveMultiFileOrDir(fileList: FileOpItem[], dir: string, toDir: string): Promise<void> {
const files = fileList.filter(item => item.type === 'file');
const op = {
force: true
};
if (files.length > 0) {
// 批处理文件
const copyOperations = files.map(item => {
const fileName = `${dir}${item.name}`;
const toFileName = `${toDir}${item.name}`;
return qiniu.rs.moveOp(
this.qiniuConfig.bucket,
fileName,
this.qiniuConfig.bucket,
toFileName,
op
);
});
await new Promise<void>((resolve, reject) => {
this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
resolve();
} else if (respInfo.statusCode === 298) {
reject(new Error('操作异常,但部分文件夹删除成功'));
} else {
reject(
new Error(`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`)
);
}
});
});
}
// 处理文件夹
const dirs = fileList.filter(item => item.type === 'dir');
if (dirs.length > 0) {
// 处理文件夹的复制
for (let i = 0; i < dirs.length; i++) {
const dirName = `${dir}${dirs[i].name}/`;
const toDirName = `${toDir}${dirs[i].name}/`;
// 移动的目录不是是自己
if (toDirName.startsWith(dirName)) continue;
let hasFile = true;
let marker = '';
while (hasFile) {
await new Promise<void>((resolve, reject) => {
// 列举当前目录下的所有文件
this.bucketManager.listPrefix(
this.qiniuConfig.bucket,
{
prefix: dirName,
limit: NETDISK_HANDLE_MAX_ITEM,
marker
},
(err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
const moveOperations = respBody.items.map(item => {
const { key } = item;
const destKey = key.replace(dirName, toDirName);
return qiniu.rs.moveOp(
this.qiniuConfig.bucket,
key,
this.qiniuConfig.bucket,
destKey,
op
);
});
this.bucketManager.batch(moveOperations, (err2, respBody2, respInfo2) => {
if (err2) {
reject(err2);
return;
}
if (respInfo2.statusCode === 200) {
if (isEmpty(respBody.marker)) hasFile = false;
else marker = respBody.marker;
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}`
)
);
}
});
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`
)
);
}
}
);
});
}
}
}
}
}