fileStoreStrategy.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. import fs from 'fs';
  2. import path from 'path';
  3. import { createObjectId } from './grid/createObjectId';
  4. import { httpStreamOutput } from './httpStream.js'
  5. export const STORAGE_NAME_FILESYSTEM = "fs";
  6. export const STORAGE_NAME_GRIDFS = "gridfs";
  7. /** Factory for FileStoreStrategy */
  8. export default class FileStoreStrategyFactory {
  9. /** constructor
  10. * @param classFileStoreStrategyFilesystem use this strategy for filesystem storage
  11. * @param storagePath file storage path
  12. * @param classFileStoreStrategyGridFs use this strategy for GridFS storage
  13. * @param gridFsBucket use this GridFS Bucket as GridFS Storage
  14. */
  15. constructor(classFileStoreStrategyFilesystem, storagePath, classFileStoreStrategyGridFs, gridFsBucket) {
  16. this.classFileStoreStrategyFilesystem = classFileStoreStrategyFilesystem;
  17. this.storagePath = storagePath;
  18. this.classFileStoreStrategyGridFs = classFileStoreStrategyGridFs;
  19. this.gridFsBucket = gridFsBucket;
  20. }
  21. /** returns the right FileStoreStrategy
  22. * @param fileObj the current file object
  23. * @param versionName the current version
  24. * @param use this storage, or if not set, get the storage from fileObj
  25. */
  26. getFileStrategy(fileObj, versionName, storage) {
  27. if (!storage) {
  28. storage = fileObj.versions[versionName].storage;
  29. if (!storage) {
  30. if (fileObj.meta.source == "import" || fileObj.versions[versionName].meta.gridFsFileId) {
  31. // uploaded by import, so it's in GridFS (MongoDB)
  32. storage = STORAGE_NAME_GRIDFS;
  33. } else {
  34. // newly uploaded, so it's at the filesystem
  35. storage = STORAGE_NAME_FILESYSTEM;
  36. }
  37. }
  38. }
  39. let ret;
  40. if ([STORAGE_NAME_FILESYSTEM, STORAGE_NAME_GRIDFS].includes(storage)) {
  41. if (storage == STORAGE_NAME_FILESYSTEM) {
  42. ret = new this.classFileStoreStrategyFilesystem(fileObj, versionName);
  43. } else if (storage == STORAGE_NAME_GRIDFS) {
  44. ret = new this.classFileStoreStrategyGridFs(this.gridFsBucket, fileObj, versionName);
  45. }
  46. }
  47. return ret;
  48. }
  49. }
  50. /** Strategy to store files */
  51. class FileStoreStrategy {
  52. /** constructor
  53. * @param fileObj the current file object
  54. * @param versionName the current version
  55. */
  56. constructor(fileObj, versionName) {
  57. this.fileObj = fileObj;
  58. this.versionName = versionName;
  59. }
  60. /** after successfull upload */
  61. onAfterUpload() {
  62. }
  63. /** download the file
  64. * @param http the current http request
  65. * @param cacheControl cacheControl of FilesCollection
  66. */
  67. interceptDownload(http, cacheControl) {
  68. }
  69. /** after file remove */
  70. onAfterRemove() {
  71. }
  72. /** returns a read stream
  73. * @return the read stream
  74. */
  75. getReadStream() {
  76. }
  77. /** returns a write stream
  78. * @param filePath if set, use this path
  79. * @return the write stream
  80. */
  81. getWriteStream(filePath) {
  82. }
  83. /** writing finished
  84. * @param finishedData the data of the write stream finish event
  85. */
  86. writeStreamFinished(finishedData) {
  87. }
  88. /** returns the new file path
  89. * @param storagePath use this storage path
  90. * @return the new file path
  91. */
  92. getNewPath(storagePath, name) {
  93. if (!_.isString(name)) {
  94. name = this.fileObj.name;
  95. }
  96. const ret = path.join(storagePath, this.fileObj._id + "-" + this.versionName + "-" + name);
  97. return ret;
  98. }
  99. /** remove the file */
  100. unlink() {
  101. }
  102. /** rename the file (physical)
  103. * @li at database the filename is updated after this method
  104. * @param newFilePath the new file path
  105. */
  106. rename(newFilePath) {
  107. }
  108. /** return the storage name
  109. * @return the storage name
  110. */
  111. getStorageName() {
  112. }
  113. }
  114. /** Strategy to store attachments at GridFS (MongoDB) */
  115. export class FileStoreStrategyGridFs extends FileStoreStrategy {
  116. /** constructor
  117. * @param gridFsBucket use this GridFS Bucket
  118. * @param fileObj the current file object
  119. * @param versionName the current version
  120. */
  121. constructor(gridFsBucket, fileObj, versionName) {
  122. super(fileObj, versionName);
  123. this.gridFsBucket = gridFsBucket;
  124. }
  125. /** download the file
  126. * @param http the current http request
  127. * @param cacheControl cacheControl of FilesCollection
  128. */
  129. interceptDownload(http, cacheControl) {
  130. const readStream = this.getReadStream();
  131. const downloadFlag = http?.params?.query?.download;
  132. let ret = false;
  133. if (readStream) {
  134. ret = true;
  135. httpStreamOutput(readStream, this.fileObj.name, http, downloadFlag, cacheControl);
  136. }
  137. return ret;
  138. }
  139. /** after file remove */
  140. onAfterRemove() {
  141. this.unlink();
  142. super.onAfterRemove();
  143. }
  144. /** returns a read stream
  145. * @return the read stream
  146. */
  147. getReadStream() {
  148. const gfsId = this.getGridFsObjectId();
  149. let ret;
  150. if (gfsId) {
  151. ret = this.gridFsBucket.openDownloadStream(gfsId);
  152. }
  153. return ret;
  154. }
  155. /** returns a write stream
  156. * @param filePath if set, use this path
  157. * @return the write stream
  158. */
  159. getWriteStream(filePath) {
  160. const fileObj = this.fileObj;
  161. const versionName = this.versionName;
  162. const metadata = { ...fileObj.meta, versionName, fileId: fileObj._id };
  163. const ret = this.gridFsBucket.openUploadStream(this.fileObj.name, {
  164. contentType: fileObj.type || 'binary/octet-stream',
  165. metadata,
  166. });
  167. return ret;
  168. }
  169. /** writing finished
  170. * @param finishedData the data of the write stream finish event
  171. */
  172. writeStreamFinished(finishedData) {
  173. const gridFsFileIdName = this.getGridFsFileIdName();
  174. Attachments.update({ _id: this.fileObj._id }, { $set: { [gridFsFileIdName]: finishedData._id.toHexString(), } });
  175. }
  176. /** remove the file */
  177. unlink() {
  178. const gfsId = this.getGridFsObjectId();
  179. if (gfsId) {
  180. this.gridFsBucket.delete(gfsId, err => {
  181. if (err) {
  182. console.error("error on gfs bucket.delete: ", err);
  183. }
  184. });
  185. }
  186. const gridFsFileIdName = this.getGridFsFileIdName();
  187. Attachments.update({ _id: this.fileObj._id }, { $unset: { [gridFsFileIdName]: 1 } });
  188. }
  189. /** return the storage name
  190. * @return the storage name
  191. */
  192. getStorageName() {
  193. return STORAGE_NAME_GRIDFS;
  194. }
  195. /** returns the GridFS Object-Id
  196. * @return the GridFS Object-Id
  197. */
  198. getGridFsObjectId() {
  199. let ret;
  200. const gridFsFileId = this.getGridFsFileId();
  201. if (gridFsFileId) {
  202. ret = createObjectId({ gridFsFileId });
  203. }
  204. return ret;
  205. }
  206. /** returns the GridFS Object-Id
  207. * @return the GridFS Object-Id
  208. */
  209. getGridFsFileId() {
  210. const ret = (this.fileObj.versions[this.versionName].meta || {})
  211. .gridFsFileId;
  212. return ret;
  213. }
  214. /** returns the property name of gridFsFileId
  215. * @return the property name of gridFsFileId
  216. */
  217. getGridFsFileIdName() {
  218. const ret = `versions.${this.versionName}.meta.gridFsFileId`;
  219. return ret;
  220. }
  221. }
  222. /** Strategy to store attachments at filesystem */
  223. export class FileStoreStrategyFilesystem extends FileStoreStrategy {
  224. /** constructor
  225. * @param fileObj the current file object
  226. * @param versionName the current version
  227. */
  228. constructor(fileObj, versionName) {
  229. super(fileObj, versionName);
  230. }
  231. /** returns a read stream
  232. * @return the read stream
  233. */
  234. getReadStream() {
  235. const ret = fs.createReadStream(this.fileObj.versions[this.versionName].path)
  236. return ret;
  237. }
  238. /** returns a write stream
  239. * @param filePath if set, use this path
  240. * @return the write stream
  241. */
  242. getWriteStream(filePath) {
  243. if (!_.isString(filePath)) {
  244. filePath = this.fileObj.versions[this.versionName].path;
  245. }
  246. const ret = fs.createWriteStream(filePath);
  247. return ret;
  248. }
  249. /** writing finished
  250. * @param finishedData the data of the write stream finish event
  251. */
  252. writeStreamFinished(finishedData) {
  253. }
  254. /** remove the file */
  255. unlink() {
  256. const filePath = this.fileObj.versions[this.versionName].path;
  257. fs.unlink(filePath, () => {});
  258. }
  259. /** rename the file (physical)
  260. * @li at database the filename is updated after this method
  261. * @param newFilePath the new file path
  262. */
  263. rename(newFilePath) {
  264. fs.renameSync(this.fileObj.versions[this.versionName].path, newFilePath);
  265. }
  266. /** return the storage name
  267. * @return the storage name
  268. */
  269. getStorageName() {
  270. return STORAGE_NAME_FILESYSTEM;
  271. }
  272. }
  273. /** move the fileObj to another storage
  274. * @param fileObj move this fileObj to another storage
  275. * @param storageDestination the storage destination (fs or gridfs)
  276. * @param fileStoreStrategyFactory get FileStoreStrategy from this factory
  277. */
  278. export const moveToStorage = function(fileObj, storageDestination, fileStoreStrategyFactory) {
  279. Object.keys(fileObj.versions).forEach(versionName => {
  280. const strategyRead = fileStoreStrategyFactory.getFileStrategy(fileObj, versionName);
  281. const strategyWrite = fileStoreStrategyFactory.getFileStrategy(fileObj, versionName, storageDestination);
  282. if (strategyRead.constructor.name != strategyWrite.constructor.name) {
  283. const readStream = strategyRead.getReadStream();
  284. const filePath = strategyWrite.getNewPath(fileStoreStrategyFactory.storagePath);
  285. const writeStream = strategyWrite.getWriteStream(filePath);
  286. writeStream.on('error', error => {
  287. console.error('[writeStream error]: ', error, fileObj._id);
  288. });
  289. readStream.on('error', error => {
  290. console.error('[readStream error]: ', error, fileObj._id);
  291. });
  292. writeStream.on('finish', Meteor.bindEnvironment((finishedData) => {
  293. strategyWrite.writeStreamFinished(finishedData);
  294. }));
  295. // https://forums.meteor.com/t/meteor-code-must-always-run-within-a-fiber-try-wrapping-callbacks-that-you-pass-to-non-meteor-libraries-with-meteor-bindenvironmen/40099/8
  296. readStream.on('end', Meteor.bindEnvironment(() => {
  297. Attachments.update({ _id: fileObj._id }, { $set: {
  298. [`versions.${versionName}.storage`]: strategyWrite.getStorageName(),
  299. [`versions.${versionName}.path`]: filePath,
  300. } });
  301. strategyRead.unlink();
  302. }));
  303. readStream.pipe(writeStream);
  304. }
  305. });
  306. };
  307. export const copyFile = function(fileObj, newCardId, fileStoreStrategyFactory) {
  308. const versionName = "original";
  309. const strategyRead = fileStoreStrategyFactory.getFileStrategy(fileObj, versionName);
  310. const readStream = strategyRead.getReadStream();
  311. const strategyWrite = fileStoreStrategyFactory.getFileStrategy(fileObj, versionName, STORAGE_NAME_FILESYSTEM);
  312. const tempPath = path.join(fileStoreStrategyFactory.storagePath, Random.id() + "-" + versionName + "-" + fileObj.name);
  313. const writeStream = strategyWrite.getWriteStream(tempPath);
  314. writeStream.on('error', error => {
  315. console.error('[writeStream error]: ', error, fileObj._id);
  316. });
  317. readStream.on('error', error => {
  318. console.error('[readStream error]: ', error, fileObj._id);
  319. });
  320. // https://forums.meteor.com/t/meteor-code-must-always-run-within-a-fiber-try-wrapping-callbacks-that-you-pass-to-non-meteor-libraries-with-meteor-bindenvironmen/40099/8
  321. readStream.on('end', Meteor.bindEnvironment(() => {
  322. const fileId = Random.id();
  323. Attachments.addFile(
  324. tempPath,
  325. {
  326. fileName: fileObj.name,
  327. type: fileObj.type,
  328. meta: {
  329. boardId: fileObj.meta.boardId,
  330. cardId: newCardId,
  331. listId: fileObj.meta.listId,
  332. swimlaneId: fileObj.meta.swimlaneId,
  333. source: 'copy',
  334. copyFrom: fileObj._id,
  335. copyStorage: strategyRead.getStorageName(),
  336. },
  337. userId: fileObj.userId,
  338. size: fileObj.fileSize,
  339. fileId,
  340. },
  341. (err, fileRef) => {
  342. if (err) {
  343. console.log(err);
  344. } else {
  345. // Set the userId again
  346. Attachments.update({ _id: fileRef._id }, { $set: { userId: fileObj.userId } });
  347. }
  348. },
  349. true,
  350. );
  351. }));
  352. readStream.pipe(writeStream);
  353. };
  354. export const rename = function(fileObj, newName, fileStoreStrategyFactory) {
  355. Object.keys(fileObj.versions).forEach(versionName => {
  356. const strategy = fileStoreStrategyFactory.getFileStrategy(fileObj, versionName);
  357. const newFilePath = strategy.getNewPath(fileStoreStrategyFactory.storagePath, newName);
  358. strategy.rename(newFilePath);
  359. Attachments.update({ _id: fileObj._id }, { $set: {
  360. "name": newName,
  361. [`versions.${versionName}.path`]: newFilePath,
  362. } });
  363. });
  364. };