|
@@ -1,14 +1,175 @@
|
|
|
import { Config, Inject, Provide } from '@midwayjs/decorator';
|
|
|
-import { existsSync, mkdirSync, renameSync } from 'fs';
|
|
|
+import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, unlinkSync } from 'fs';
|
|
|
import { Context } from '@midwayjs/koa';
|
|
|
-import { dirname, extname, sep } from 'path';
|
|
|
+import { dirname, extname, join, sep } from 'path';
|
|
|
+import { ReturnModelType, mongoose } from '@typegoose/typegoose';
|
|
|
+import { isObject, get, pick, flattenDeep } from 'lodash';
|
|
|
+import { GetModel } from '../util/getModel';
|
|
|
+import { InjectEntityModel } from '@midwayjs/typegoose';
|
|
|
+import { UseFile } from '../entity/useFile.entity';
|
|
|
+interface ScanModelFileValue {
|
|
|
+ model: any;
|
|
|
+ projection: object;
|
|
|
+}
|
|
|
+interface ScanModelFileResult {
|
|
|
+ [key: string]: ScanModelFileValue;
|
|
|
+}
|
|
|
+const limit = 50;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 文件上传相关服务
|
|
|
+ */
|
|
|
@Provide()
|
|
|
export class FileService {
|
|
|
@Inject()
|
|
|
ctx: Context;
|
|
|
@Config('koa.globalPrefix')
|
|
|
globalPrefix: string;
|
|
|
+ @Config('upload')
|
|
|
+ uploadConfig;
|
|
|
+ @InjectEntityModel(UseFile)
|
|
|
+ UseFileModel: ReturnModelType<typeof UseFile>;
|
|
|
+
|
|
|
+ // #region 递归清理未被使用的上传文件
|
|
|
+ /**
|
|
|
+ * 删除不在文件使用表登记的文件
|
|
|
+ */
|
|
|
+ async deleteNoUseFile() {
|
|
|
+ const realDir = this.uploadConfig.realdir;
|
|
|
+ this.recursionFindFile(realDir);
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ *
|
|
|
+ * @param basePath 基础路径
|
|
|
+ * @param list 基础路径下的所有内容
|
|
|
+ */
|
|
|
+ async recursionFindFile(basePath) {
|
|
|
+ const dirExists = existsSync(basePath);
|
|
|
+ if (!dirExists) return;
|
|
|
+ const list = readdirSync(basePath);
|
|
|
+ for (const f of list) {
|
|
|
+ const thisPath = join(basePath, f);
|
|
|
+ // 文件夹就继续递归找
|
|
|
+ if (this.isDir(thisPath)) this.recursionFindFile(thisPath);
|
|
|
+ else if (this.isFile(thisPath)) {
|
|
|
+ // 文件,需要到表里找是否存在,存在保留,不存在就删除
|
|
|
+ const shortPath = this.realPathTurnToShortPath(thisPath);
|
|
|
+ const count = await this.UseFileModel.count({ uri: shortPath });
|
|
|
+ if (count <= 0) this.toUnlink(thisPath);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 真实路径=>短地址
|
|
|
+ * @param realPath 文件真实路径
|
|
|
+ * @returns string 短地址
|
|
|
+ */
|
|
|
+ realPathTurnToShortPath(realPath: string) {
|
|
|
+ const realDir = this.uploadConfig.realdir;
|
|
|
+ let shortPath = realPath.replace(realDir, `${this.globalPrefix}/files`);
|
|
|
+ while (shortPath.includes('\\')) {
|
|
|
+ shortPath = shortPath.replace('\\', '/');
|
|
|
+ }
|
|
|
+ return shortPath;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除文件
|
|
|
+ * @param path 文件路径
|
|
|
+ */
|
|
|
+ toUnlink(path) {
|
|
|
+ unlinkSync(path);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断路径是否存在
|
|
|
+ * @param path 路径
|
|
|
+ * @returns boolean: true-存在/false-不存在
|
|
|
+ */
|
|
|
+ isExists(path) {
|
|
|
+ return existsSync(path);
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 判断是否是文件夹
|
|
|
+ * @param path 路径
|
|
|
+ * @returns boolean: true-是文件夹
|
|
|
+ */
|
|
|
+ isDir(path) {
|
|
|
+ const f = lstatSync(path);
|
|
|
+ return f.isDirectory();
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 判断是否是文件
|
|
|
+ * @param path 路径
|
|
|
+ * @returns boolean: true-是文件
|
|
|
+ */
|
|
|
+ isFile(path) {
|
|
|
+ const f = lstatSync(path);
|
|
|
+ return f.isFile();
|
|
|
+ }
|
|
|
+ // #endregion
|
|
|
+
|
|
|
+ // #region 写入文件统计地址表
|
|
|
+ /**文件统计地址表重写, 由 scanModelHaveFile()提供参数*/
|
|
|
+ async setFileUriFromDefaultDataBase(scanResult: ScanModelFileResult) {
|
|
|
+ // 清空表内容,重写
|
|
|
+ await this.UseFileModel.deleteMany({});
|
|
|
+ for (const mn in scanResult) {
|
|
|
+ const obj = scanResult[mn];
|
|
|
+ const { model, projection } = obj;
|
|
|
+ // 获取表总数量
|
|
|
+ const total = await model.count();
|
|
|
+ // 计算循环次数
|
|
|
+ const skip = 0;
|
|
|
+ const times = Math.ceil(total / limit);
|
|
|
+ for (let i = 0; i < times; i++) {
|
|
|
+ // 查数据
|
|
|
+ const r = await model.find({}, projection).skip(skip).limit(limit).lean();
|
|
|
+ // 整理批量添加数据格式
|
|
|
+ let filesUri = r.map(i => {
|
|
|
+ const arr = [];
|
|
|
+ for (const prop in projection) {
|
|
|
+ const targetVal = i[prop];
|
|
|
+ const uris = targetVal.map(ti => pick(ti, 'uri'));
|
|
|
+ arr.push(...uris);
|
|
|
+ }
|
|
|
+ return arr;
|
|
|
+ });
|
|
|
+ filesUri = flattenDeep(filesUri);
|
|
|
+ //批量添加
|
|
|
+ await this.UseFileModel.insertMany(filesUri);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**扫描注册的model中是否有File类型字段,以Object的形式提取出来*/
|
|
|
+ scanModelHaveFile(): ScanModelFileResult {
|
|
|
+ const modelNames = this.uploadConfig.modelNames || [];
|
|
|
+ const models = modelNames.map(i => GetModel(i));
|
|
|
+ const result: ScanModelFileResult = {};
|
|
|
+ for (const model of models) {
|
|
|
+ const s = model['schema'];
|
|
|
+ const fields: object = s['tree'];
|
|
|
+ const projection = {};
|
|
|
+ for (const f in fields) {
|
|
|
+ const field = fields[f];
|
|
|
+ if (isObject(field)) {
|
|
|
+ const type = get(field, 'type');
|
|
|
+ if (type && type === mongoose.Schema.Types['File']) {
|
|
|
+ projection[f] = 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (Object.keys(projection).length > 0) {
|
|
|
+ result[model.modelName] = { model, projection };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ // #endregion
|
|
|
|
|
|
+ // #region 上传部分
|
|
|
+ /**文件真实短地址 */
|
|
|
getFileShortRealPath() {
|
|
|
let originalUrl = this.ctx.request.originalUrl;
|
|
|
//先去掉请求前缀
|
|
@@ -60,4 +221,5 @@ export class FileService {
|
|
|
const secondstr = second < 10 ? '0' + second : second;
|
|
|
return `${y}${mstr}${dstr}${hstr}${minutestr}${secondstr}`;
|
|
|
}
|
|
|
+ // #endregion
|
|
|
}
|