mirror of https://github.com/wekan/wekan
The Open Source kanban (built with Meteor). Keep variable/table/field names camelCase. For translations, only add Pull Request changes to wekan/i18n/en.i18n.json , other translations are done at https://transifex.com/wekan/wekan only.
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
361 lines
12 KiB
361 lines
12 KiB
/**
|
|
* Notes a details about a storage adapter failure within the file record
|
|
* @param {string} storeName
|
|
* @param {number} maxTries
|
|
* @return {undefined}
|
|
* @todo deprecate this
|
|
*/
|
|
FS.File.prototype.logCopyFailure = function(storeName, maxTries) {
|
|
var self = this;
|
|
|
|
// hasStored will update from the fileRecord
|
|
if (self.hasStored(storeName)) {
|
|
throw new Error("logCopyFailure: invalid storeName");
|
|
}
|
|
|
|
// Make sure we have a temporary file saved since we will be
|
|
// trying the save again.
|
|
FS.TempStore.ensureForFile(self);
|
|
|
|
var now = new Date();
|
|
var currentCount = (self.failures && self.failures.copies && self.failures.copies[storeName] && typeof self.failures.copies[storeName].count === "number") ? self.failures.copies[storeName].count : 0;
|
|
maxTries = maxTries || 5;
|
|
|
|
var modifier = {};
|
|
modifier.$set = {};
|
|
modifier.$set['failures.copies.' + storeName + '.lastAttempt'] = now;
|
|
if (currentCount === 0) {
|
|
modifier.$set['failures.copies.' + storeName + '.firstAttempt'] = now;
|
|
}
|
|
modifier.$set['failures.copies.' + storeName + '.count'] = currentCount + 1;
|
|
modifier.$set['failures.copies.' + storeName + '.doneTrying'] = (currentCount + 1 >= maxTries);
|
|
self.update(modifier);
|
|
};
|
|
|
|
/**
|
|
* Has this store permanently failed?
|
|
* @param {String} storeName The name of the store
|
|
* @return {boolean} Has this store failed permanently?
|
|
* @todo deprecate this
|
|
*/
|
|
FS.File.prototype.failedPermanently = function(storeName) {
|
|
var self = this;
|
|
return !!(self.failures &&
|
|
self.failures.copies &&
|
|
self.failures.copies[storeName] &&
|
|
self.failures.copies[storeName].doneTrying);
|
|
};
|
|
|
|
/**
|
|
* @method FS.File.prototype.createReadStream
|
|
* @public
|
|
* @param {String} [storeName]
|
|
* @returns {stream.Readable} Readable NodeJS stream
|
|
*
|
|
* Returns a readable stream. Where the stream reads from depends on the FS.File instance and whether you pass a store name.
|
|
*
|
|
* * If you pass a `storeName`, a readable stream for the file data saved in that store is returned.
|
|
* * If you don't pass a `storeName` and data is attached to the FS.File instance (on `data` property, which must be a DataMan instance), then a readable stream for the attached data is returned.
|
|
* * If you don't pass a `storeName` and there is no data attached to the FS.File instance, a readable stream for the file data currently in the temporary store (`FS.TempStore`) is returned.
|
|
*
|
|
*/
|
|
FS.File.prototype.createReadStream = function(storeName) {
|
|
var self = this;
|
|
|
|
// If we dont have a store name but got Buffer data?
|
|
if (!storeName && self.data) {
|
|
FS.debug && console.log("fileObj.createReadStream creating read stream for attached data");
|
|
// Stream from attached data if present
|
|
return self.data.createReadStream();
|
|
} else if (!storeName && FS.TempStore && FS.TempStore.exists(self)) {
|
|
FS.debug && console.log("fileObj.createReadStream creating read stream for temp store");
|
|
// Stream from temp store - its a bit slower than regular streams?
|
|
return FS.TempStore.createReadStream(self);
|
|
} else {
|
|
// Stream from the store using storage adapter
|
|
if (self.isMounted()) {
|
|
var storage = self.collection.storesLookup[storeName] || self.collection.primaryStore;
|
|
FS.debug && console.log("fileObj.createReadStream creating read stream for store", storage.name);
|
|
// return stream
|
|
return storage.adapter.createReadStream(self);
|
|
} else {
|
|
throw new Meteor.Error('File not mounted');
|
|
}
|
|
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @method FS.File.prototype.createWriteStream
|
|
* @public
|
|
* @param {String} [storeName]
|
|
* @returns {stream.Writeable} Writeable NodeJS stream
|
|
*
|
|
* Returns a writeable stream. Where the stream writes to depends on whether you pass in a store name.
|
|
*
|
|
* * If you pass a `storeName`, a writeable stream for (over)writing the file data in that store is returned.
|
|
* * If you don't pass a `storeName`, a writeable stream for writing to the temp store for this file is returned.
|
|
*
|
|
*/
|
|
FS.File.prototype.createWriteStream = function(storeName) {
|
|
var self = this;
|
|
|
|
// We have to have a mounted file in order for this to work
|
|
if (self.isMounted()) {
|
|
if (!storeName && FS.TempStore && FS.FileWorker) {
|
|
// If we have worker installed - we pass the file to FS.TempStore
|
|
// We dont need the storeName since all stores will be generated from
|
|
// TempStore.
|
|
// This should trigger FS.FileWorker at some point?
|
|
FS.TempStore.createWriteStream(self);
|
|
} else {
|
|
// Stream directly to the store using storage adapter
|
|
var storage = self.collection.storesLookup[storeName] || self.collection.primaryStore;
|
|
return storage.adapter.createWriteStream(self);
|
|
}
|
|
} else {
|
|
throw new Meteor.Error('File not mounted');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @method FS.File.prototype.copy Makes a copy of the file and underlying data in all stores.
|
|
* @public
|
|
* @returns {FS.File} The new FS.File instance
|
|
*/
|
|
FS.File.prototype.copy = function() {
|
|
var self = this;
|
|
|
|
if (!self.isMounted()) {
|
|
throw new Error("Cannot copy a file that is not associated with a collection");
|
|
}
|
|
|
|
// Get the file record
|
|
var fileRecord = self.collection.files.findOne({_id: self._id}, {transform: null}) || {};
|
|
|
|
// Remove _id and copy keys from the file record
|
|
delete fileRecord._id;
|
|
|
|
// Insert directly; we don't have access to "original" in this case
|
|
var newId = self.collection.files.insert(fileRecord);
|
|
|
|
var newFile = self.collection.findOne(newId);
|
|
|
|
// Copy underlying files in the stores
|
|
var mod, oldKey;
|
|
for (var name in newFile.copies) {
|
|
if (newFile.copies.hasOwnProperty(name)) {
|
|
oldKey = newFile.copies[name].key;
|
|
if (oldKey) {
|
|
// We need to ask the adapter for the true oldKey because
|
|
// right now gridfs does some extra stuff.
|
|
// TODO GridFS should probably set the full key object
|
|
// (with _id and filename) into `copies.key`
|
|
// so that copies.key can be passed directly to
|
|
// createReadStreamForFileKey
|
|
var sourceFileStorage = self.collection.storesLookup[name];
|
|
if (!sourceFileStorage) {
|
|
throw new Error(name + " is not a valid store name");
|
|
}
|
|
oldKey = sourceFileStorage.adapter.fileKey(self);
|
|
// delete so that new fileKey will be generated in copyStoreData
|
|
delete newFile.copies[name].key;
|
|
mod = mod || {};
|
|
mod["copies." + name + ".key"] = copyStoreData(newFile, name, oldKey);
|
|
}
|
|
}
|
|
}
|
|
// Update keys in the filerecord
|
|
if (mod) {
|
|
newFile.update({$set: mod});
|
|
}
|
|
|
|
return newFile;
|
|
};
|
|
|
|
Meteor.methods({
|
|
// Does a HEAD request to URL to get the type, updatedAt,
|
|
// and size prior to actually downloading the data.
|
|
// That way we can do filter checks without actually downloading.
|
|
'_cfs_getUrlInfo': function (url, options) {
|
|
check(url, String);
|
|
check(options, Object);
|
|
|
|
this.unblock();
|
|
|
|
var response = HTTP.call("HEAD", url, options);
|
|
var headers = response.headers;
|
|
var result = {};
|
|
|
|
if (headers['content-type']) {
|
|
result.type = headers['content-type'];
|
|
}
|
|
|
|
if (headers['content-length']) {
|
|
result.size = +headers['content-length'];
|
|
}
|
|
|
|
if (headers['last-modified']) {
|
|
result.updatedAt = new Date(headers['last-modified']);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
// Helper function that checks whether given fileId from collectionName
|
|
// Is fully uploaded to specify storeName.
|
|
'_cfs_returnWhenStored' : function (collectionName, fileId, storeName) {
|
|
check(collectionName, String);
|
|
check(fileId, String);
|
|
check(storeName, String);
|
|
|
|
var collection = FS._collections[collectionName];
|
|
if (!collection) {
|
|
return Meteor.Error('_cfs_returnWhenStored: FSCollection name not exists');
|
|
}
|
|
|
|
var file = collection.findOne({_id: fileId});
|
|
if (!file) {
|
|
return Meteor.Error('_cfs_returnWhenStored: FSFile not exists');
|
|
}
|
|
return file.hasStored(storeName);
|
|
}
|
|
});
|
|
|
|
// TODO maybe this should be in cfs-storage-adapter
|
|
function _copyStoreData(fileObj, storeName, sourceKey, callback) {
|
|
if (!fileObj.isMounted()) {
|
|
throw new Error("Cannot copy store data for a file that is not associated with a collection");
|
|
}
|
|
|
|
var storage = fileObj.collection.storesLookup[storeName];
|
|
if (!storage) {
|
|
throw new Error(storeName + " is not a valid store name");
|
|
}
|
|
|
|
// We want to prevent beforeWrite and transformWrite from running, so
|
|
// we interact directly with the store.
|
|
var destinationKey = storage.adapter.fileKey(fileObj);
|
|
var readStream = storage.adapter.createReadStreamForFileKey(sourceKey);
|
|
var writeStream = storage.adapter.createWriteStreamForFileKey(destinationKey);
|
|
|
|
writeStream.once('stored', function(result) {
|
|
callback(null, result.fileKey);
|
|
});
|
|
|
|
writeStream.once('error', function(error) {
|
|
callback(error);
|
|
});
|
|
|
|
readStream.pipe(writeStream);
|
|
}
|
|
var copyStoreData = Meteor.wrapAsync(_copyStoreData);
|
|
|
|
/**
|
|
* @method FS.File.prototype.copyData Copies the content of a store directly into another store.
|
|
* @public
|
|
* @param {string} sourceStoreName
|
|
* @param {string} targetStoreName
|
|
* @param {boolean=} move
|
|
*/
|
|
FS.File.prototype.copyData = function(sourceStoreName, targetStoreName, move){
|
|
|
|
move = !!move;
|
|
/**
|
|
* @type {Object.<string,*>}
|
|
*/
|
|
var sourceStoreValues = this.copies[sourceStoreName];
|
|
/**
|
|
* @type {string}
|
|
*/
|
|
var copyKey = cloneDataToStore(this, sourceStoreName, targetStoreName, move);
|
|
/**
|
|
* @type {Object.<string,*>}
|
|
*/
|
|
var targetStoreValues = {};
|
|
for (var v in sourceStoreValues) {
|
|
if (sourceStoreValues.hasOwnProperty(v)) {
|
|
targetStoreValues[v] = sourceStoreValues[v]
|
|
}
|
|
}
|
|
targetStoreValues.key = copyKey;
|
|
targetStoreValues.createdAt = new Date();
|
|
targetStoreValues.updatedAt = new Date();
|
|
/**
|
|
*
|
|
* @type {modifier}
|
|
*/
|
|
var modifier = {};
|
|
modifier.$set = {};
|
|
modifier.$set["copies."+targetStoreName] = targetStoreValues;
|
|
if(move){
|
|
modifier.$unset = {};
|
|
modifier.$unset["copies."+sourceStoreName] = "";
|
|
}
|
|
this.update(modifier);
|
|
};
|
|
/**
|
|
* @method FS.File.prototype.moveData Moves the content of a store directly into another store.
|
|
* @public
|
|
* @param {string} sourceStoreName
|
|
* @param {string} targetStoreName
|
|
*/
|
|
FS.File.prototype.moveData = function(sourceStoreName, targetStoreName){
|
|
this.copyData(sourceStoreName, targetStoreName, true);
|
|
};
|
|
// TODO maybe this should be in cfs-storage-adapter
|
|
/**
|
|
*
|
|
* @param {FS.File} fileObj
|
|
* @param {string} sourceStoreName
|
|
* @param {string} targetStoreName
|
|
* @param {boolean} move
|
|
* @param callback
|
|
* @private
|
|
*/
|
|
function _copyDataFromStoreToStore(fileObj, sourceStoreName, targetStoreName, move, callback) {
|
|
if (!fileObj.isMounted()) {
|
|
throw new Error("Cannot copy store data for a file that is not associated with a collection");
|
|
}
|
|
/**
|
|
* @type {FS.StorageAdapter}
|
|
*/
|
|
var sourceStorage = fileObj.collection.storesLookup[sourceStoreName];
|
|
/**
|
|
* @type {FS.StorageAdapter}
|
|
*/
|
|
var targetStorage = fileObj.collection.storesLookup[targetStoreName];
|
|
|
|
if (!sourceStorage) {
|
|
throw new Error(sourceStoreName + " is not a valid store name");
|
|
}
|
|
if (!targetStorage) {
|
|
throw new Error(targetStorage + " is not a valid store name");
|
|
}
|
|
|
|
// We want to prevent beforeWrite and transformWrite from running, so
|
|
// we interact directly with the store.
|
|
var sourceKey = sourceStorage.adapter.fileKey(fileObj);
|
|
var targetKey = targetStorage.adapter.fileKey(fileObj);
|
|
var readStream = sourceStorage.adapter.createReadStreamForFileKey(sourceKey);
|
|
var writeStream = targetStorage.adapter.createWriteStreamForFileKey(targetKey);
|
|
|
|
|
|
writeStream.safeOnce('stored', function(result) {
|
|
if(move && sourceStorage.adapter.remove(fileObj)===false){
|
|
callback("Copied to store:" + targetStoreName
|
|
+ " with fileKey: "
|
|
+ result.fileKey
|
|
+ ", but could not delete from source store: "
|
|
+ sourceStoreName);
|
|
}else{
|
|
callback(null, result.fileKey);
|
|
}
|
|
});
|
|
|
|
writeStream.once('error', function(error) {
|
|
callback(error);
|
|
});
|
|
|
|
readStream.pipe(writeStream);
|
|
}
|
|
var cloneDataToStore = Meteor.wrapAsync(_copyDataFromStoreToStore);
|
|
|