attachments.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import { FilesCollection } from 'meteor/ostrio:files';
  2. Attachments = new FilesCollection({
  3. storagePath: storagePath(),
  4. debug: false, // FIXME: Remove debug mode
  5. collectionName: 'attachments2',
  6. allowClientCode: true, // FIXME: Permissions
  7. onAfterUpload: (fileRef) => {
  8. Attachments.update({_id:fileRef._id}, {$set: {"meta.uploaded": true}});
  9. }
  10. });
  11. if (Meteor.isServer) {
  12. Meteor.startup(() => {
  13. Attachments.collection._ensureIndex({ cardId: 1 });
  14. });
  15. // TODO: Permission related
  16. // TODO: Add Activity update
  17. // TODO: publish and subscribe
  18. Meteor.publish('attachments2', function() {
  19. return Attachments.find().cursor;
  20. });
  21. } else {
  22. Meteor.subscribe('attachments2');
  23. }
  24. // ---------- Deprecated fallback ---------- //
  25. const localFSStore = process.env.ATTACHMENTS_STORE_PATH;
  26. const storeName = 'attachments2';
  27. const defaultStoreOptions = {
  28. beforeWrite: fileObj => {
  29. if (!fileObj.isImage()) {
  30. return {
  31. type: 'application/octet-stream',
  32. };
  33. }
  34. return {};
  35. },
  36. };
  37. let store;
  38. if (localFSStore) {
  39. // have to reinvent methods from FS.Store.GridFS and FS.Store.FileSystem
  40. const fs = Npm.require('fs');
  41. const path = Npm.require('path');
  42. const mongodb = Npm.require('mongodb');
  43. const Grid = Npm.require('gridfs-stream');
  44. // calulate the absolute path here, because FS.Store.FileSystem didn't expose the aboslutepath or FS.Store didn't expose api calls :(
  45. let pathname = localFSStore;
  46. /*eslint camelcase: ["error", {allow: ["__meteor_bootstrap__"]}] */
  47. if (!pathname && __meteor_bootstrap__ && __meteor_bootstrap__.serverDir) {
  48. pathname = path.join(
  49. __meteor_bootstrap__.serverDir,
  50. `../../../cfs/files/${storeName}`,
  51. );
  52. }
  53. if (!pathname)
  54. throw new Error('FS.Store.FileSystem unable to determine path');
  55. // Check if we have '~/foo/bar'
  56. if (pathname.split(path.sep)[0] === '~') {
  57. const homepath =
  58. process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
  59. if (homepath) {
  60. pathname = pathname.replace('~', homepath);
  61. } else {
  62. throw new Error('FS.Store.FileSystem unable to resolve "~" in path');
  63. }
  64. }
  65. // Set absolute path
  66. const absolutePath = path.resolve(pathname);
  67. const _FStore = new FS.Store.FileSystem(storeName, {
  68. path: localFSStore,
  69. ...defaultStoreOptions,
  70. });
  71. const GStore = {
  72. fileKey(fileObj) {
  73. const key = {
  74. _id: null,
  75. filename: null,
  76. };
  77. // If we're passed a fileObj, we retrieve the _id and filename from it.
  78. if (fileObj) {
  79. const info = fileObj._getInfo(storeName, {
  80. updateFileRecordFirst: false,
  81. });
  82. key._id = info.key || null;
  83. key.filename =
  84. info.name ||
  85. fileObj.name({ updateFileRecordFirst: false }) ||
  86. `${fileObj.collectionName}-${fileObj._id}`;
  87. }
  88. // If key._id is null at this point, createWriteStream will let GridFS generate a new ID
  89. return key;
  90. },
  91. db: undefined,
  92. mongoOptions: { useNewUrlParser: true },
  93. mongoUrl: process.env.MONGO_URL,
  94. init() {
  95. this._init(err => {
  96. this.inited = !err;
  97. });
  98. },
  99. _init(callback) {
  100. const self = this;
  101. mongodb.MongoClient.connect(self.mongoUrl, self.mongoOptions, function(
  102. err,
  103. db,
  104. ) {
  105. if (err) {
  106. return callback(err);
  107. }
  108. self.db = db;
  109. return callback(null);
  110. });
  111. return;
  112. },
  113. createReadStream(fileKey, options) {
  114. const self = this;
  115. if (!self.inited) {
  116. self.init();
  117. return undefined;
  118. }
  119. options = options || {};
  120. // Init GridFS
  121. const gfs = new Grid(self.db, mongodb);
  122. // Set the default streamning settings
  123. const settings = {
  124. _id: new mongodb.ObjectID(fileKey._id),
  125. root: `cfs_gridfs.${storeName}`,
  126. };
  127. // Check if this should be a partial read
  128. if (
  129. typeof options.start !== 'undefined' &&
  130. typeof options.end !== 'undefined'
  131. ) {
  132. // Add partial info
  133. settings.range = {
  134. startPos: options.start,
  135. endPos: options.end,
  136. };
  137. }
  138. return gfs.createReadStream(settings);
  139. },
  140. };
  141. GStore.init();
  142. const CRS = 'createReadStream';
  143. const _CRS = `_${CRS}`;
  144. const FStore = _FStore._transform;
  145. FStore[_CRS] = FStore[CRS].bind(FStore);
  146. FStore[CRS] = function(fileObj, options) {
  147. let stream;
  148. try {
  149. const localFile = path.join(
  150. absolutePath,
  151. FStore.storage.fileKey(fileObj),
  152. );
  153. const state = fs.statSync(localFile);
  154. if (state) {
  155. stream = FStore[_CRS](fileObj, options);
  156. }
  157. } catch (e) {
  158. // file is not there, try GridFS ?
  159. stream = undefined;
  160. }
  161. if (stream) return stream;
  162. else {
  163. try {
  164. const stream = GStore[CRS](GStore.fileKey(fileObj), options);
  165. return stream;
  166. } catch (e) {
  167. return undefined;
  168. }
  169. }
  170. }.bind(FStore);
  171. store = _FStore;
  172. } else {
  173. store = new FS.Store.GridFS(localFSStore ? `G${storeName}` : storeName, {
  174. // XXX Add a new store for cover thumbnails so we don't load big images in
  175. // the general board view
  176. // If the uploaded document is not an image we need to enforce browser
  177. // download instead of execution. This is particularly important for HTML
  178. // files that the browser will just execute if we don't serve them with the
  179. // appropriate `application/octet-stream` MIME header which can lead to user
  180. // data leaks. I imagine other formats (like PDF) can also be attack vectors.
  181. // See https://github.com/wekan/wekan/issues/99
  182. // XXX Should we use `beforeWrite` option of CollectionFS instead of
  183. // collection-hooks?
  184. // We should use `beforeWrite`.
  185. ...defaultStoreOptions,
  186. });
  187. }
  188. function storagePath(defaultPath) {
  189. const storePath = process.env.ATTACHMENTS_STORE_PATH;
  190. return storePath ? storePath : defaultPath;
  191. }
  192. export default Attachments;