attachments.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  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('attachments', function() {
  19. return Attachments.find().cursor;
  20. });
  21. } else {
  22. Meteor.subscribe('attachments');
  23. }
  24. // ---------- Deprecated fallback ---------- //
  25. const localFSStore = process.env.ATTACHMENTS_STORE_PATH;
  26. const storeName = 'attachments';
  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. DeprecatedAttachs = new FS.Collection('attachments', {
  189. stores: [store],
  190. });
  191. if (Meteor.isServer) {
  192. Meteor.startup(() => {
  193. DeprecatedAttachs.files._ensureIndex({ cardId: 1 });
  194. });
  195. DeprecatedAttachs.allow({
  196. insert(userId, doc) {
  197. return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
  198. },
  199. update(userId, doc) {
  200. return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
  201. },
  202. remove(userId, doc) {
  203. return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
  204. },
  205. // We authorize the attachment download either:
  206. // - if the board is public, everyone (even unconnected) can download it
  207. // - if the board is private, only board members can download it
  208. download(userId, doc) {
  209. const board = Boards.findOne(doc.boardId);
  210. if (board.isPublic()) {
  211. return true;
  212. } else {
  213. return board.hasMember(userId);
  214. }
  215. },
  216. fetch: ['boardId'],
  217. });
  218. }
  219. // XXX Enforce a schema for the DeprecatedAttachs CollectionFS
  220. if (Meteor.isServer) {
  221. DeprecatedAttachs.files.after.insert((userId, doc) => {
  222. // If the attachment doesn't have a source field
  223. // or its source is different than import
  224. if (!doc.source || doc.source !== 'import') {
  225. // Add activity about adding the attachment
  226. Activities.insert({
  227. userId,
  228. type: 'card',
  229. activityType: 'addAttachment',
  230. attachmentId: doc._id,
  231. boardId: doc.boardId,
  232. cardId: doc.cardId,
  233. listId: doc.listId,
  234. swimlaneId: doc.swimlaneId,
  235. });
  236. } else {
  237. // Don't add activity about adding the attachment as the activity
  238. // be imported and delete source field
  239. DeprecatedAttachs.update(
  240. {
  241. _id: doc._id,
  242. },
  243. {
  244. $unset: {
  245. source: '',
  246. },
  247. },
  248. );
  249. }
  250. });
  251. DeprecatedAttachs.files.before.remove((userId, doc) => {
  252. Activities.insert({
  253. userId,
  254. type: 'card',
  255. activityType: 'deleteAttachment',
  256. attachmentId: doc._id,
  257. boardId: doc.boardId,
  258. cardId: doc.cardId,
  259. listId: doc.listId,
  260. swimlaneId: doc.swimlaneId,
  261. });
  262. });
  263. DeprecatedAttachs.files.after.remove((userId, doc) => {
  264. Activities.remove({
  265. attachmentId: doc._id,
  266. });
  267. });
  268. }
  269. function storagePath(defaultPath) {
  270. const storePath = process.env.ATTACHMENTS_STORE_PATH;
  271. return storePath ? storePath : defaultPath;
  272. }
  273. export default Attachments;