2
0

old-attachments-migration.js 6.0 KB

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