fileStoreStrategy.js 13 KB

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