attachments.js 7.6 KB

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