fileStoreStrategy.js 15 KB

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