Archive now uses file system instead of collection
This commit is contained in:
@@ -53,3 +53,4 @@ akryum:vue-component
|
||||
akryum:vue-sass
|
||||
percolate:migrations
|
||||
meteortesting:mocha
|
||||
ostrio:files
|
||||
|
||||
@@ -85,6 +85,8 @@ oauth@1.3.2
|
||||
oauth2@1.3.0
|
||||
ongoworks:speakingurl@9.0.0
|
||||
ordered-dict@1.1.0
|
||||
ostrio:cookies@2.7.0
|
||||
ostrio:files@2.0.1
|
||||
patreon-oauth@0.1.0
|
||||
peerlibrary:assert@0.3.0
|
||||
peerlibrary:check-extension@0.7.0
|
||||
|
||||
18
app/imports/api/creature/archive/ArchiveCreatureFiles.js
Normal file
18
app/imports/api/creature/archive/ArchiveCreatureFiles.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { FilesCollection } from 'meteor/ostrio:files';
|
||||
|
||||
const ArchiveCreatureFiles = new FilesCollection({
|
||||
collectionName: 'archiveCreatureFiles',
|
||||
allowClientCode: false, // Disallow remove files from Client
|
||||
storagePath: '/DiceCloud/uploads/',
|
||||
onBeforeUpload(file) {
|
||||
// Allow upload files under 10MB, and only in json format
|
||||
if (file.size > 10485760) {
|
||||
return 'Please upload with size equal or less than 10MB';
|
||||
}
|
||||
if (!/json/i.test(file.extension)){
|
||||
return 'Please upload only a JSON file';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default ArchiveCreatureFiles;
|
||||
@@ -0,0 +1,70 @@
|
||||
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import Experiences from '/imports/api/creature/experience/Experiences.js';
|
||||
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
|
||||
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
|
||||
|
||||
export function getArchiveObj(creatureId){
|
||||
// Build the archive document
|
||||
const creature = Creatures.findOne(creatureId);
|
||||
const properties = CreatureProperties.find({'ancestors.id': creatureId}).fetch();
|
||||
const experiences = Experiences.find({creatureId}).fetch();
|
||||
const logs = CreatureLogs.find({creatureId}).fetch();
|
||||
let archiveCreature = {
|
||||
meta: {
|
||||
archiveDate: new Date(),
|
||||
schemaVersion: SCHEMA_VERSION,
|
||||
},
|
||||
creature,
|
||||
properties,
|
||||
experiences,
|
||||
logs,
|
||||
};
|
||||
|
||||
return archiveCreature;
|
||||
}
|
||||
|
||||
export function writeArchiveCreatureFile(archive){
|
||||
const buffer = Buffer.from(JSON.stringify(archive, null, 2));
|
||||
return ArchiveCreatureFiles.write(buffer, {
|
||||
fileName: `${archive.creature.name || archive.creature._id}.json`,
|
||||
type: 'application/json',
|
||||
userId: archive.creature.owner,
|
||||
meta: {
|
||||
schemaVersion: SCHEMA_VERSION,
|
||||
creatureId: archive.creature._id,
|
||||
creatureName: archive.creature.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const archiveCreatureToFile = new ValidatedMethod({
|
||||
name: 'Creatures.methods.archiveCreatureToFile',
|
||||
validate: new SimpleSchema({
|
||||
'creatureId': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
async run({creatureId}) {
|
||||
assertOwnership(creatureId, this.userId);
|
||||
if (Meteor.isServer){
|
||||
const archive = getArchiveObj(creatureId);
|
||||
writeArchiveCreatureFile(archive);
|
||||
}
|
||||
removeCreatureWork(creatureId);
|
||||
},
|
||||
});
|
||||
|
||||
export default archiveCreatureToFile;
|
||||
@@ -1,2 +1,4 @@
|
||||
import '/imports/api/creature/archive/methods/archiveCreatures.js';
|
||||
import '/imports/api/creature/archive/methods/archiveCreatureToFile.js';
|
||||
import '/imports/api/creature/archive/methods/restoreCreatures.js';
|
||||
import '/imports/api/creature/archive/methods/restoreCreatureFromFile.js';
|
||||
|
||||
6
app/imports/api/creature/archive/methods/readFile.js
Normal file
6
app/imports/api/creature/archive/methods/readFile.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
// Read a file and return the result
|
||||
export default function read(file){
|
||||
return fs.readFile(file.path, 'utf8');
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import Experiences from '/imports/api/creature/experience/Experiences.js';
|
||||
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
|
||||
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
|
||||
import readFile from '/imports/api/creature/archive/methods/readFile.js';
|
||||
|
||||
function restoreCreature(file, archive){
|
||||
if (SCHEMA_VERSION < file.meta.schemaVersion){
|
||||
throw new Meteor.Error('Incompatible',
|
||||
'The archive file is from a newer version. Update required to read.')
|
||||
}
|
||||
|
||||
// Migrate and verify the archive meets the current schema
|
||||
// migrateArchive(archive, file.meta.schemaVersion);
|
||||
|
||||
// Insert the creature sub documents
|
||||
// They still have their original _id's
|
||||
Creatures.insert(archive.creature);
|
||||
try {
|
||||
// Add all the properties
|
||||
if (archive.properties && archive.properties.length){
|
||||
CreatureProperties.batchInsert(archive.properties);
|
||||
}
|
||||
if (archive.experiences && archive.experiences.length){
|
||||
Experiences.batchInsert(archive.experiences);
|
||||
}
|
||||
if (archive.logs && archive.logs.length){
|
||||
CreatureLogs.batchInsert(archive.logs);
|
||||
}
|
||||
} catch (e) {
|
||||
// If the above fails, delete the inserted creature
|
||||
removeCreatureWork(archive.creature._id);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const restoreCreaturefromFile = new ValidatedMethod({
|
||||
name: 'Creatures.methods.restoreCreaturefromFile',
|
||||
validate: new SimpleSchema({
|
||||
'fileId': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
async run({fileId}) {
|
||||
// fetch the file
|
||||
const file = ArchiveCreatureFiles.findOne({_id: fileId}).get();
|
||||
if (!file){
|
||||
throw new Meteor.Error('File not found',
|
||||
'The requested creature archive does not exist');
|
||||
}
|
||||
// Assert ownership
|
||||
const userId = file?.userId;
|
||||
if (!userId || userId !== this.userId){
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'You can only restore creatures you own');
|
||||
}
|
||||
if (Meteor.isServer){
|
||||
// Read the file data
|
||||
const string = await readFile(file);
|
||||
const archive = JSON.parse(string);
|
||||
restoreCreature(file, archive);
|
||||
//Remove the archive once the restore succeeded
|
||||
ArchiveCreatureFiles.remove({_id: fileId})
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default restoreCreaturefromFile;
|
||||
@@ -19,8 +19,14 @@ Migrations.add({
|
||||
});
|
||||
|
||||
function migrate({reversed} = {}){
|
||||
console.log('unarchiving all characters from database archive');
|
||||
// TODO
|
||||
console.log('migrating creature properties');
|
||||
migrateCollection({collection: CreatureProperties, reversed});
|
||||
console.log('migrating library nodes')
|
||||
migrateCollection({collection: LibraryNodes, reversed});
|
||||
console.log('archiving characters to file system archive');
|
||||
// TODO
|
||||
}
|
||||
|
||||
function migrateCollection({collection, reversed}){
|
||||
|
||||
7
app/imports/server/publications/archiveFiles.js
Normal file
7
app/imports/server/publications/archiveFiles.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
|
||||
|
||||
Meteor.publish('archiveCreatureFiles', function () {
|
||||
return ArchiveCreatureFiles.find({
|
||||
userId: this.userId,
|
||||
}).cursor;
|
||||
});
|
||||
@@ -10,3 +10,4 @@ import '/imports/server/publications/slotFillers.js';
|
||||
import '/imports/server/publications/ownedDocuments.js';
|
||||
import '/imports/server/publications/archivedCreatures.js';
|
||||
import '/imports/server/publications/searchLibraryNodes.js';
|
||||
import '/imports/server/publications/archiveFiles.js';
|
||||
|
||||
@@ -60,12 +60,13 @@
|
||||
<script lang="js">
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js';
|
||||
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
|
||||
import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolderList.vue';
|
||||
import archiveCreatures from '/imports/api/creature/archive/methods/archiveCreatures.js';
|
||||
import restoreCreatures from '/imports/api/creature/archive/methods/restoreCreatures.js';
|
||||
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
|
||||
import archiveCreatureToFile from '/imports/api/creature/archive/methods/archiveCreatureToFile.js';
|
||||
import restoreCreatureFromFile from '/imports/api/creature/archive/methods/restoreCreatureFromFile.js';
|
||||
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import { uniq, flatten } from 'lodash';
|
||||
|
||||
const characterTransform = function(char){
|
||||
char.url = `/character/${char._id}/${char.urlName || '-'}`;
|
||||
@@ -73,6 +74,15 @@ const characterTransform = function(char){
|
||||
return char;
|
||||
};
|
||||
|
||||
const fileTransform = function(file){
|
||||
return {
|
||||
_id: file._id,
|
||||
name: file.meta.creatureName,
|
||||
owner: file.userId,
|
||||
creatureId: file.meta.creatureId,
|
||||
};
|
||||
}
|
||||
|
||||
const creatureFields = {
|
||||
'color': 1,
|
||||
'avatarPicture': 1,
|
||||
@@ -111,8 +121,8 @@ export default {
|
||||
if (!this.selectedCreature) return;
|
||||
this.archiveActionLoading = true;
|
||||
if (this.mode === 'archive'){
|
||||
archiveCreatures.call({
|
||||
creatureIds: [this.selectedCreature],
|
||||
archiveCreatureToFile.call({
|
||||
creatureId: this.selectedCreature,
|
||||
}, error => {
|
||||
this.archiveActionLoading = false;
|
||||
if (!error) return;
|
||||
@@ -120,11 +130,8 @@ export default {
|
||||
snackbar({text: error.reason});
|
||||
});
|
||||
} else if (this.mode === 'restore'){
|
||||
let archiveId = ArchivedCreatures.findOne({
|
||||
'creature._id': this.selectedCreature
|
||||
})._id;
|
||||
restoreCreatures.call({
|
||||
archiveIds: [archiveId],
|
||||
restoreCreatureFromFile.call({
|
||||
fileId: this.selectedCreature,
|
||||
}, error => {
|
||||
this.archiveActionLoading = false;
|
||||
if (!error) return;
|
||||
@@ -138,6 +145,7 @@ export default {
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'archivedCreatures': [],
|
||||
'archiveCreatureFiles': [],
|
||||
},
|
||||
folders(){
|
||||
const userId = Meteor.userId();
|
||||
@@ -162,7 +170,7 @@ export default {
|
||||
CreaturesWithNoParty() {
|
||||
var userId = Meteor.userId();
|
||||
var charArrays = CreatureFolders.find({owner: userId}).map(p => p.creatures);
|
||||
var folderChars = _.uniq(_.flatten(charArrays));
|
||||
var folderChars = uniq(flatten(charArrays));
|
||||
return Creatures.find(
|
||||
{
|
||||
_id: {$nin: folderChars},
|
||||
@@ -179,15 +187,14 @@ export default {
|
||||
{owner: userId},
|
||||
{sort: {order: 1}},
|
||||
).map(folder => {
|
||||
folder.creatures = ArchivedCreatures.find(
|
||||
folder.creatures = ArchiveCreatureFiles.find(
|
||||
{
|
||||
'creature._id': {$in: folder.creatures || []},
|
||||
owner: userId,
|
||||
'meta.creatureId': {$in: folder.creatures || []},
|
||||
userId,
|
||||
}, {
|
||||
sort: {'creature.name': 1},
|
||||
fields: {creature: 1},
|
||||
sort: {'meta.creatureName': 1},
|
||||
}
|
||||
).map(arc => characterTransform(arc.creature));
|
||||
).map(fileTransform);
|
||||
return folder;
|
||||
});
|
||||
folders = folders.filter(folder => !!folder.creatures.length);
|
||||
@@ -196,16 +203,15 @@ export default {
|
||||
archiveCreaturesWithNoParty() {
|
||||
var userId = Meteor.userId();
|
||||
var charArrays = CreatureFolders.find({owner: userId}).map(p => p.creatures);
|
||||
var folderChars = _.uniq(_.flatten(charArrays));
|
||||
return ArchivedCreatures.find(
|
||||
var folderChars = uniq(flatten(charArrays));
|
||||
return ArchiveCreatureFiles.find(
|
||||
{
|
||||
'creature._id': {$nin: folderChars},
|
||||
owner: userId,
|
||||
'meta.creatureId': {$nin: folderChars},
|
||||
userId,
|
||||
}, {
|
||||
sort: {'creature.name': 1},
|
||||
fields: {creature: 1},
|
||||
sort: {'meta.creatureName': 1},
|
||||
}
|
||||
).map(arc => characterTransform(arc.creature));
|
||||
).map(fileTransform);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user