mirror of
https://github.com/wekan/wekan.git
synced 2025-09-22 01:50:48 +02:00
Ref: Attachments to use modern gridfs (rm localstore)
This commit is contained in:
parent
52a63c5423
commit
966541704c
1 changed files with 62 additions and 228 deletions
|
@ -1,220 +1,20 @@
|
||||||
export const AttachmentStorage = new Mongo.Collection(
|
import { Meteor } from 'meteor/meteor';
|
||||||
'cfs_gridfs.attachments.files',
|
import { FilesCollection } from 'meteor/ostrio:files';
|
||||||
);
|
import { createBucket } from './lib/grid/createBucket';
|
||||||
export const AvatarStorage = new Mongo.Collection('cfs_gridfs.avatars.files');
|
import { createOnAfterUpload } from './lib/fsHooks/createOnAfterUpload';
|
||||||
|
import { createInterceptDownload } from './lib/fsHooks/createInterceptDownload';
|
||||||
|
import { createOnAfterRemove } from './lib/fsHooks/createOnAfterRemove';
|
||||||
|
|
||||||
const localFSStore = process.env.ATTACHMENTS_STORE_PATH;
|
const attachmentBucket = createBucket('attachments');
|
||||||
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) {
|
// XXX Enforce a schema for the Attachments FilesCollection
|
||||||
pathname = path.join(
|
// see: https://github.com/VeliovGroup/Meteor-Files/wiki/Schema
|
||||||
__meteor_bootstrap__.serverDir,
|
|
||||||
`../../../cfs/files/${storeName}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pathname)
|
export const Attachments = new FilesCollection({
|
||||||
throw new Error('FS.Store.FileSystem unable to determine path');
|
debug: false, // Change to `true` for debugging
|
||||||
|
collectionName: 'attachments',
|
||||||
// Check if we have '~/foo/bar'
|
allowClientCode: false,
|
||||||
if (pathname.split(path.sep)[0] === '~') {
|
onAfterUpload(doc) {
|
||||||
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) => {
|
|
||||||
// If the attachment doesn't have a source field
|
// If the attachment doesn't have a source field
|
||||||
// or its source is different than import
|
// or its source is different than import
|
||||||
if (!doc.source || doc.source !== '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) => {
|
Meteor.startup(() => {
|
||||||
Activities.insert({
|
Attachments.collection._ensureIndex({ cardId: 1 });
|
||||||
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,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue