fileStoreStrategy.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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") {
  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. /** return the storage name
  103. * @return the storage name
  104. */
  105. getStorageName() {
  106. }
  107. }
  108. /** Strategy to store attachments at GridFS (MongoDB) */
  109. export class FileStoreStrategyGridFs extends FileStoreStrategy {
  110. /** constructor
  111. * @param gridFsBucket use this GridFS Bucket
  112. * @param fileObj the current file object
  113. * @param versionName the current version
  114. */
  115. constructor(gridFsBucket, fileObj, versionName) {
  116. super(fileObj, versionName);
  117. this.gridFsBucket = gridFsBucket;
  118. }
  119. /** download the file
  120. * @param http the current http request
  121. * @param cacheControl cacheControl of FilesCollection
  122. */
  123. interceptDownload(http, cacheControl) {
  124. const readStream = this.getReadStream();
  125. const downloadFlag = http?.params?.query?.download;
  126. let ret = false;
  127. if (readStream) {
  128. ret = true;
  129. httpStreamOutput(readStream, this.fileObj.name, http, downloadFlag, cacheControl);
  130. }
  131. return ret;
  132. }
  133. /** after file remove */
  134. onAfterRemove() {
  135. this.unlink();
  136. super.onAfterRemove();
  137. }
  138. /** returns a read stream
  139. * @return the read stream
  140. */
  141. getReadStream() {
  142. const gfsId = this.getGridFsObjectId();
  143. let ret;
  144. if (gfsId) {
  145. ret = this.gridFsBucket.openDownloadStream(gfsId);
  146. }
  147. return ret;
  148. }
  149. /** returns a write stream
  150. * @param filePath if set, use this path
  151. * @return the write stream
  152. */
  153. getWriteStream(filePath) {
  154. const fileObj = this.fileObj;
  155. const versionName = this.versionName;
  156. const metadata = { ...fileObj.meta, versionName, fileId: fileObj._id };
  157. const ret = this.gridFsBucket.openUploadStream(this.fileObj.name, {
  158. contentType: fileObj.type || 'binary/octet-stream',
  159. metadata,
  160. });
  161. return ret;
  162. }
  163. /** writing finished
  164. * @param finishedData the data of the write stream finish event
  165. */
  166. writeStreamFinished(finishedData) {
  167. const gridFsFileIdName = this.getGridFsFileIdName();
  168. Attachments.update({ _id: this.fileObj._id }, { $set: { [gridFsFileIdName]: finishedData._id.toHexString(), } });
  169. }
  170. /** remove the file */
  171. unlink() {
  172. const gfsId = this.getGridFsObjectId();
  173. if (gfsId) {
  174. this.gridFsBucket.delete(gfsId, err => {
  175. if (err) {
  176. console.error("error on gfs bucket.delete: ", err);
  177. }
  178. });
  179. }
  180. const gridFsFileIdName = this.getGridFsFileIdName();
  181. Attachments.update({ _id: this.fileObj._id }, { $unset: { [gridFsFileIdName]: 1 } });
  182. }
  183. /** return the storage name
  184. * @return the storage name
  185. */
  186. getStorageName() {
  187. return STORAGE_NAME_GRIDFS;
  188. }
  189. /** returns the GridFS Object-Id
  190. * @return the GridFS Object-Id
  191. */
  192. getGridFsObjectId() {
  193. let ret;
  194. const gridFsFileId = this.getGridFsFileId();
  195. if (gridFsFileId) {
  196. ret = createObjectId({ gridFsFileId });
  197. }
  198. return ret;
  199. }
  200. /** returns the GridFS Object-Id
  201. * @return the GridFS Object-Id
  202. */
  203. getGridFsFileId() {
  204. const ret = (this.fileObj.versions[this.versionName].meta || {})
  205. .gridFsFileId;
  206. return ret;
  207. }
  208. /** returns the property name of gridFsFileId
  209. * @return the property name of gridFsFileId
  210. */
  211. getGridFsFileIdName() {
  212. const ret = `versions.${this.versionName}.meta.gridFsFileId`;
  213. return ret;
  214. }
  215. }
  216. /** Strategy to store attachments at filesystem */
  217. export class FileStoreStrategyFilesystem extends FileStoreStrategy {
  218. /** constructor
  219. * @param fileObj the current file object
  220. * @param versionName the current version
  221. */
  222. constructor(fileObj, versionName) {
  223. super(fileObj, versionName);
  224. }
  225. /** returns a read stream
  226. * @return the read stream
  227. */
  228. getReadStream() {
  229. const ret = fs.createReadStream(this.fileObj.versions[this.versionName].path)
  230. return ret;
  231. }
  232. /** returns a write stream
  233. * @param filePath if set, use this path
  234. * @return the write stream
  235. */
  236. getWriteStream(filePath) {
  237. if (!_.isString(filePath)) {
  238. filePath = this.fileObj.versions[this.versionName].path;
  239. }
  240. const ret = fs.createWriteStream(filePath);
  241. return ret;
  242. }
  243. /** writing finished
  244. * @param finishedData the data of the write stream finish event
  245. */
  246. writeStreamFinished(finishedData) {
  247. }
  248. /** remove the file */
  249. unlink() {
  250. const filePath = this.fileObj.versions[this.versionName].path;
  251. fs.unlink(filePath, () => {});
  252. }
  253. /** return the storage name
  254. * @return the storage name
  255. */
  256. getStorageName() {
  257. return STORAGE_NAME_FILESYSTEM;
  258. }
  259. }
  260. /** move the fileObj to another storage
  261. * @param fileObj move this fileObj to another storage
  262. * @param storageDestination the storage destination (fs or gridfs)
  263. * @param fileStoreStrategyFactory get FileStoreStrategy from this factory
  264. */
  265. export const moveToStorage = function(fileObj, storageDestination, fileStoreStrategyFactory) {
  266. Object.keys(fileObj.versions).forEach(versionName => {
  267. const strategyRead = fileStoreStrategyFactory.getFileStrategy(fileObj, versionName);
  268. const strategyWrite = fileStoreStrategyFactory.getFileStrategy(fileObj, versionName, storageDestination);
  269. if (strategyRead.constructor.name != strategyWrite.constructor.name) {
  270. const readStream = strategyRead.getReadStream();
  271. const filePath = strategyWrite.getNewPath(fileStoreStrategyFactory.storagePath);
  272. const writeStream = strategyWrite.getWriteStream(filePath);
  273. writeStream.on('error', error => {
  274. console.error('[writeStream error]: ', error, fileObjId);
  275. });
  276. readStream.on('error', error => {
  277. console.error('[readStream error]: ', error, fileObjId);
  278. });
  279. writeStream.on('finish', Meteor.bindEnvironment((finishedData) => {
  280. strategyWrite.writeStreamFinished(finishedData);
  281. }));
  282. // 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
  283. readStream.on('end', Meteor.bindEnvironment(() => {
  284. Attachments.update({ _id: fileObj._id }, { $set: {
  285. [`versions.${versionName}.storage`]: strategyWrite.getStorageName(),
  286. [`versions.${versionName}.path`]: filePath,
  287. } });
  288. strategyRead.unlink();
  289. }));
  290. readStream.pipe(writeStream);
  291. }
  292. });
  293. };