file.service.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import { Config, Inject, Provide } from '@midwayjs/decorator';
  2. import { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, unlinkSync } from 'fs';
  3. import { Context } from '@midwayjs/koa';
  4. import { dirname, extname, join, sep } from 'path';
  5. import { ReturnModelType, mongoose } from '@typegoose/typegoose';
  6. import { isObject, get, pick, flattenDeep } from 'lodash';
  7. import { GetModel } from '../util/getModel';
  8. import { InjectEntityModel } from '@midwayjs/typegoose';
  9. import { UseFile } from '../entity/useFile.entity';
  10. interface ScanModelFileValue {
  11. model: any;
  12. projection: object;
  13. }
  14. interface ScanModelFileResult {
  15. [key: string]: ScanModelFileValue;
  16. }
  17. const limit = 50;
  18. /**
  19. * 文件上传相关服务
  20. */
  21. @Provide()
  22. export class FileService {
  23. @Inject()
  24. ctx: Context;
  25. @Config('koa.globalPrefix')
  26. globalPrefix: string;
  27. @Config('upload')
  28. uploadConfig;
  29. @InjectEntityModel(UseFile)
  30. UseFileModel: ReturnModelType<typeof UseFile>;
  31. // #region 递归清理未被使用的上传文件
  32. /**
  33. * 删除不在文件使用表登记的文件
  34. */
  35. async deleteNoUseFile() {
  36. const realDir = this.uploadConfig.realdir;
  37. this.recursionFindFile(realDir);
  38. }
  39. /**
  40. *
  41. * @param basePath 基础路径
  42. * @param list 基础路径下的所有内容
  43. */
  44. async recursionFindFile(basePath) {
  45. const dirExists = existsSync(basePath);
  46. if (!dirExists) return;
  47. const list = readdirSync(basePath);
  48. for (const f of list) {
  49. const thisPath = join(basePath, f);
  50. // 文件夹就继续递归找
  51. if (this.isDir(thisPath)) this.recursionFindFile(thisPath);
  52. else if (this.isFile(thisPath)) {
  53. // 文件,需要到表里找是否存在,存在保留,不存在就删除
  54. const shortPath = this.realPathTurnToShortPath(thisPath);
  55. const count = await this.UseFileModel.count({ uri: shortPath });
  56. if (count <= 0) this.toUnlink(thisPath);
  57. }
  58. }
  59. }
  60. /**
  61. * 真实路径=>短地址
  62. * @param realPath 文件真实路径
  63. * @returns string 短地址
  64. */
  65. realPathTurnToShortPath(realPath: string) {
  66. const realDir = this.uploadConfig.realdir;
  67. let shortPath = realPath.replace(realDir, `${this.globalPrefix}/files`);
  68. while (shortPath.includes('\\')) {
  69. shortPath = shortPath.replace('\\', '/');
  70. }
  71. return shortPath;
  72. }
  73. /**
  74. * 删除文件
  75. * @param path 文件路径
  76. */
  77. toUnlink(path) {
  78. unlinkSync(path);
  79. }
  80. /**
  81. * 判断路径是否存在
  82. * @param path 路径
  83. * @returns boolean: true-存在/false-不存在
  84. */
  85. isExists(path) {
  86. return existsSync(path);
  87. }
  88. /**
  89. * 判断是否是文件夹
  90. * @param path 路径
  91. * @returns boolean: true-是文件夹
  92. */
  93. isDir(path) {
  94. const f = lstatSync(path);
  95. return f.isDirectory();
  96. }
  97. /**
  98. * 判断是否是文件
  99. * @param path 路径
  100. * @returns boolean: true-是文件
  101. */
  102. isFile(path) {
  103. const f = lstatSync(path);
  104. return f.isFile();
  105. }
  106. // #endregion
  107. // #region 写入文件统计地址表
  108. /**文件统计地址表重写, 由 scanModelHaveFile()提供参数*/
  109. async setFileUriFromDefaultDataBase(scanResult: ScanModelFileResult) {
  110. // 清空表内容,重写
  111. await this.UseFileModel.deleteMany({});
  112. for (const mn in scanResult) {
  113. const obj = scanResult[mn];
  114. const { model, projection } = obj;
  115. // 获取表总数量
  116. const total = await model.count();
  117. // 计算循环次数
  118. const skip = 0;
  119. const times = Math.ceil(total / limit);
  120. for (let i = 0; i < times; i++) {
  121. // 查数据
  122. const r = await model.find({}, projection).skip(skip).limit(limit).lean();
  123. // 整理批量添加数据格式
  124. let filesUri = r.map(i => {
  125. const arr = [];
  126. for (const prop in projection) {
  127. const targetVal = i[prop];
  128. const uris = targetVal.map(ti => pick(ti, 'uri'));
  129. arr.push(...uris);
  130. }
  131. return arr;
  132. });
  133. filesUri = flattenDeep(filesUri);
  134. //批量添加
  135. await this.UseFileModel.insertMany(filesUri);
  136. }
  137. }
  138. }
  139. /**扫描注册的model中是否有File类型字段,以Object的形式提取出来*/
  140. scanModelHaveFile(): ScanModelFileResult {
  141. const modelNames = this.uploadConfig.modelNames || [];
  142. const models = modelNames.map(i => GetModel(i));
  143. const result: ScanModelFileResult = {};
  144. for (const model of models) {
  145. const s = model['schema'];
  146. const fields: object = s['tree'];
  147. const projection = {};
  148. for (const f in fields) {
  149. const field = fields[f];
  150. if (isObject(field)) {
  151. const type = get(field, 'type');
  152. if (type && type === mongoose.Schema.Types['File']) {
  153. projection[f] = 1;
  154. }
  155. }
  156. }
  157. if (Object.keys(projection).length > 0) {
  158. result[model.modelName] = { model, projection };
  159. }
  160. }
  161. return result;
  162. }
  163. // #endregion
  164. // #region 上传部分
  165. /**文件真实短地址 */
  166. getFileShortRealPath() {
  167. let originalUrl = this.ctx.request.originalUrl;
  168. //先去掉请求前缀
  169. originalUrl = originalUrl.replace(this.globalPrefix, '');
  170. // 再去掉files
  171. originalUrl = originalUrl.replace('/files', '');
  172. const arr = originalUrl.split('/');
  173. // 首行为空去掉
  174. arr.splice(0, 1);
  175. // 最后一个数据为文件,其余的为路径拼成一起就行
  176. return arr.join(sep);
  177. }
  178. /**
  179. * 移动文件
  180. * @param tempPath 临时上传文件位置
  181. * @param path 实际文件应处位置
  182. */
  183. moveFile(tempPath: string, path: string) {
  184. renameSync(tempPath, path);
  185. }
  186. // 创建文件夹
  187. mkdir(dn: string) {
  188. if (existsSync(dn)) {
  189. return true;
  190. }
  191. if (this.mkdir(dirname(dn))) {
  192. mkdirSync(dn);
  193. return true;
  194. }
  195. }
  196. /**获取文件名后缀 */
  197. getExt(name: string) {
  198. return extname(name);
  199. }
  200. /**获取年月日时分秒, 格式: 年月日时分秒 */
  201. getNowDateTime() {
  202. const date = new Date();
  203. const y = date.getFullYear();
  204. const m = date.getMonth() + 1;
  205. const mstr = m < 10 ? '0' + m : m;
  206. const d = date.getDate();
  207. const dstr = d < 10 ? '0' + d : d;
  208. const h = date.getHours();
  209. const hstr = h < 10 ? '0' + h : h;
  210. const minute = date.getMinutes();
  211. const minutestr = minute < 10 ? '0' + minute : minute;
  212. const second = date.getSeconds();
  213. const secondstr = second < 10 ? '0' + second : second;
  214. return `${y}${mstr}${dstr}${hstr}${minutestr}${secondstr}`;
  215. }
  216. // #endregion
  217. }