Browse Source

Ref: Attachments to use modern gridfs (rm localstore)

David Arnold 4 years ago
parent
commit
966541704c
1 changed files with 65 additions and 231 deletions
  1. 65 231
      models/attachments.js

+ 65 - 231
models/attachments.js

@@ -1,220 +1,20 @@
-export const AttachmentStorage = new Mongo.Collection(
-  'cfs_gridfs.attachments.files',
-);
-export const AvatarStorage = new Mongo.Collection('cfs_gridfs.avatars.files');
-
-const localFSStore = process.env.ATTACHMENTS_STORE_PATH;
-const storeName = 'attachments';
-const defaultStoreOptions = {
-  beforeWrite: fileObj => {
-    if (!fileObj.isImage()) {
-      return {
-        type: 'application/octet-stream',
-      };
-    }
-    return {};
-  },
-};
-let store;
-if (localFSStore) {
-  // have to reinvent methods from FS.Store.GridFS and FS.Store.FileSystem
-  const fs = Npm.require('fs');
-  const path = Npm.require('path');
-  const mongodb = Npm.require('mongodb');
-  const Grid = Npm.require('gridfs-stream');
-  // calulate the absolute path here, because FS.Store.FileSystem didn't expose the aboslutepath or FS.Store didn't expose api calls :(
-  let pathname = localFSStore;
-  /*eslint camelcase: ["error", {allow: ["__meteor_bootstrap__"]}] */
-
-  if (!pathname && __meteor_bootstrap__ && __meteor_bootstrap__.serverDir) {
-    pathname = path.join(
-      __meteor_bootstrap__.serverDir,
-      `../../../cfs/files/${storeName}`,
-    );
-  }
-
-  if (!pathname)
-    throw new Error('FS.Store.FileSystem unable to determine path');
-
-  // Check if we have '~/foo/bar'
-  if (pathname.split(path.sep)[0] === '~') {
-    const homepath =
-      process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
-    if (homepath) {
-      pathname = pathname.replace('~', homepath);
-    } else {
-      throw new Error('FS.Store.FileSystem unable to resolve "~" in path');
-    }
-  }
-
-  // Set absolute path
-  const absolutePath = path.resolve(pathname);
-
-  const _FStore = new FS.Store.FileSystem(storeName, {
-    path: localFSStore,
-    ...defaultStoreOptions,
-  });
-  const GStore = {
-    fileKey(fileObj) {
-      const key = {
-        _id: null,
-        filename: null,
-      };
-
-      // If we're passed a fileObj, we retrieve the _id and filename from it.
-      if (fileObj) {
-        const info = fileObj._getInfo(storeName, {
-          updateFileRecordFirst: false,
-        });
-        key._id = info.key || null;
-        key.filename =
-          info.name ||
-          fileObj.name({ updateFileRecordFirst: false }) ||
-          `${fileObj.collectionName}-${fileObj._id}`;
-      }
-
-      // If key._id is null at this point, createWriteStream will let GridFS generate a new ID
-      return key;
-    },
-    db: undefined,
-    mongoOptions: { useNewUrlParser: true },
-    mongoUrl: process.env.MONGO_URL,
-    init() {
-      this._init(err => {
-        this.inited = !err;
-      });
-    },
-    _init(callback) {
-      const self = this;
-      mongodb.MongoClient.connect(self.mongoUrl, self.mongoOptions, function(
-        err,
-        db,
-      ) {
-        if (err) {
-          return callback(err);
-        }
-        self.db = db;
-        return callback(null);
-      });
-      return;
-    },
-    createReadStream(fileKey, options) {
-      const self = this;
-      if (!self.inited) {
-        self.init();
-        return undefined;
-      }
-      options = options || {};
-
-      // Init GridFS
-      const gfs = new Grid(self.db, mongodb);
-
-      // Set the default streamning settings
-      const settings = {
-        _id: new mongodb.ObjectID(fileKey._id),
-        root: `cfs_gridfs.${storeName}`,
-      };
-
-      // Check if this should be a partial read
-      if (
-        typeof options.start !== 'undefined' &&
-        typeof options.end !== 'undefined'
-      ) {
-        // Add partial info
-        settings.range = {
-          startPos: options.start,
-          endPos: options.end,
-        };
-      }
-      return gfs.createReadStream(settings);
-    },
-  };
-  GStore.init();
-  const CRS = 'createReadStream';
-  const _CRS = `_${CRS}`;
-  const FStore = _FStore._transform;
-  FStore[_CRS] = FStore[CRS].bind(FStore);
-  FStore[CRS] = function(fileObj, options) {
-    let stream;
-    try {
-      const localFile = path.join(
-        absolutePath,
-        FStore.storage.fileKey(fileObj),
-      );
-      const state = fs.statSync(localFile);
-      if (state) {
-        stream = FStore[_CRS](fileObj, options);
-      }
-    } catch (e) {
-      // file is not there, try GridFS ?
-      stream = undefined;
-    }
-    if (stream) return stream;
-    else {
-      try {
-        const stream = GStore[CRS](GStore.fileKey(fileObj), options);
-        return stream;
-      } catch (e) {
-        return undefined;
-      }
-    }
-  }.bind(FStore);
-  store = _FStore;
-} else {
-  store = new FS.Store.GridFS(localFSStore ? `G${storeName}` : storeName, {
-    // XXX Add a new store for cover thumbnails so we don't load big images in
-    // the general board view
-    // If the uploaded document is not an image we need to enforce browser
-    // download instead of execution. This is particularly important for HTML
-    // files that the browser will just execute if we don't serve them with the
-    // appropriate `application/octet-stream` MIME header which can lead to user
-    // data leaks. I imagine other formats (like PDF) can also be attack vectors.
-    // See https://github.com/wekan/wekan/issues/99
-    // XXX Should we use `beforeWrite` option of CollectionFS instead of
-    // collection-hooks?
-    // We should use `beforeWrite`.
-    ...defaultStoreOptions,
-  });
-}
-Attachments = new FS.Collection('attachments', {
-  stores: [store],
-});
-
-if (Meteor.isServer) {
-  Meteor.startup(() => {
-    Attachments.files._ensureIndex({ cardId: 1 });
-  });
-
-  Attachments.allow({
-    insert(userId, doc) {
-      return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
-    },
-    update(userId, doc) {
-      return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
-    },
-    remove(userId, doc) {
-      return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
-    },
-    // We authorize the attachment download either:
-    // - if the board is public, everyone (even unconnected) can download it
-    // - if the board is private, only board members can download it
-    download(userId, doc) {
-      const board = Boards.findOne(doc.boardId);
-      if (board.isPublic()) {
-        return true;
-      } else {
-        return board.hasMember(userId);
-      }
-    },
-
-    fetch: ['boardId'],
-  });
-}
-
-// XXX Enforce a schema for the Attachments CollectionFS
-
-if (Meteor.isServer) {
-  Attachments.files.after.insert((userId, doc) => {
+import { Meteor } from 'meteor/meteor';
+import { FilesCollection } from 'meteor/ostrio:files';
+import { createBucket } from './lib/grid/createBucket';
+import { createOnAfterUpload } from './lib/fsHooks/createOnAfterUpload';
+import { createInterceptDownload } from './lib/fsHooks/createInterceptDownload';
+import { createOnAfterRemove } from './lib/fsHooks/createOnAfterRemove';
+
+const attachmentBucket = createBucket('attachments');
+
+// XXX Enforce a schema for the Attachments FilesCollection
+// see: https://github.com/VeliovGroup/Meteor-Files/wiki/Schema
+
+export const Attachments = new FilesCollection({
+  debug: false, // Change to `true` for debugging
+  collectionName: 'attachments',
+  allowClientCode: false,
+  onAfterUpload(doc) {
     // If the attachment doesn't have a source field
     // or its source is different than import
     if (!doc.source || doc.source !== 'import') {
@@ -246,22 +46,56 @@ if (Meteor.isServer) {
         },
       );
     }
+    createOnAfterUpload(attachmentBucket)(doc);
+  },
+  interceptDownload: createInterceptDownload(attachmentBucket),
+  onAfterRemove(docs) {
+    docs.forEach(function(doc) {
+      Activities.insert({
+        userId: doc.userId,
+        type: 'card',
+        activityType: 'deleteAttachment',
+        attachmentId: doc._id,
+        // this preserves the name so that notifications can be meaningful after
+        // this file is removed
+        attachmentName: doc.original.name,
+        boardId: doc.boardId,
+        cardId: doc.cardId,
+        listId: doc.listId,
+        swimlaneId: doc.swimlaneId,
+      });
+    });
+    createOnAfterRemove(attachmentBucket)(docs);
+  },
+  // We authorize the attachment download either:
+  // - if the board is public, everyone (even unconnected) can download it
+  // - if the board is private, only board members can download it
+  downloadCallback(doc) {
+    const board = Boards.findOne(doc.boardId);
+    if (board.isPublic()) {
+      return true;
+    } else {
+      return board.hasMember(this.userId);
+    }
+  },
+});
+
+if (Meteor.isServer) {
+  Attachments.allow({
+    insert(userId, doc) {
+      return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+    },
+    update(userId, doc) {
+      return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+    },
+    remove(userId, doc) {
+      return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+    },
+    fetch: ['boardId'],
   });
 
-  Attachments.files.before.remove((userId, doc) => {
-    Activities.insert({
-      userId,
-      type: 'card',
-      activityType: 'deleteAttachment',
-      attachmentId: doc._id,
-      // this preserves the name so that notifications can be meaningful after
-      // this file is removed
-      attachmentName: doc.original.name,
-      boardId: doc.boardId,
-      cardId: doc.cardId,
-      listId: doc.listId,
-      swimlaneId: doc.swimlaneId,
-    });
+  Meteor.startup(() => {
+    Attachments.collection._ensureIndex({ cardId: 1 });
   });
 }