Merge branch 'version-2-dev' into version-2-tabletop
This commit is contained in:
@@ -9,6 +9,7 @@ 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 { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
|
||||
|
||||
export function getArchiveObj(creatureId){
|
||||
// Build the archive document
|
||||
@@ -31,7 +32,7 @@ export function getArchiveObj(creatureId){
|
||||
return archiveCreature;
|
||||
}
|
||||
|
||||
export function archiveCreature(creatureId){
|
||||
export function archiveCreature(creatureId, userId){
|
||||
const archive = getArchiveObj(creatureId);
|
||||
const buffer = Buffer.from(JSON.stringify(archive, null, 2));
|
||||
ArchiveCreatureFiles.write(buffer, {
|
||||
@@ -43,11 +44,12 @@ export function archiveCreature(creatureId){
|
||||
creatureId: archive.creature._id,
|
||||
creatureName: archive.creature.name,
|
||||
},
|
||||
}, (error) => {
|
||||
}, (error, file) => {
|
||||
if (error){
|
||||
throw error;
|
||||
} else {
|
||||
removeCreatureWork(creatureId);
|
||||
incrementFileStorageUsed(userId, file.size);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
@@ -68,7 +70,7 @@ const archiveCreatureToFile = new ValidatedMethod({
|
||||
async run({creatureId}) {
|
||||
assertOwnership(creatureId, this.userId);
|
||||
if (Meteor.isServer){
|
||||
archiveCreature(creatureId);
|
||||
archiveCreature(creatureId, this.userId);
|
||||
} else {
|
||||
removeCreatureWork(creatureId);
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
import '/imports/api/creature/archive/methods/archiveCreatureToFile.js';
|
||||
import '/imports/api/creature/archive/methods/restoreCreatures.js';
|
||||
import '/imports/api/creature/archive/methods/restoreCreatureFromFile.js';
|
||||
import '/imports/api/creature/archive/methods/removeArchiveCreature.js';
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
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 assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
|
||||
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
|
||||
|
||||
const removeArchiveCreature = new ValidatedMethod({
|
||||
name: 'ArchiveCreatureFiles.methods.removeArchiveCreature',
|
||||
validate: new SimpleSchema({
|
||||
'fileId': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
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');
|
||||
}
|
||||
//Remove the archive once the restore succeeded
|
||||
ArchiveCreatureFiles.remove({ _id: fileId });
|
||||
// Update the user's file storage limits
|
||||
incrementFileStorageUsed(userId, -file.size);
|
||||
},
|
||||
});
|
||||
|
||||
export default removeArchiveCreature;
|
||||
@@ -8,6 +8,9 @@ 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 assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
|
||||
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
|
||||
|
||||
let migrateArchive;
|
||||
if (Meteor.isServer){
|
||||
migrateArchive = require('/imports/migrations/server/migrateArchive.js').default;
|
||||
@@ -69,13 +72,18 @@ const restoreCreaturefromFile = new ValidatedMethod({
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'You can only restore creatures you own');
|
||||
}
|
||||
|
||||
assertHasCharactersSlots(this.userId);
|
||||
|
||||
if (Meteor.isServer){
|
||||
// Read the file data
|
||||
const archive = await ArchiveCreatureFiles.readJSONFile(file);
|
||||
restoreCreature(archive);
|
||||
}
|
||||
//Remove the archive once the restore succeeded
|
||||
ArchiveCreatureFiles.remove({_id: fileId});
|
||||
ArchiveCreatureFiles.remove({ _id: fileId });
|
||||
// Update the user's file storage limits
|
||||
incrementFileStorageUsed(userId, -file.size);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
|
||||
export default function assertHasCharactersSlots(userId) {
|
||||
if (characterSlotsRemaining(userId) <= 0) {
|
||||
throw new Meteor.Error('characterSlotLimit',
|
||||
'No character slots left')
|
||||
}
|
||||
}
|
||||
|
||||
export function characterSlotsRemaining(userId) {
|
||||
let tier = getUserTier(userId);
|
||||
const currentCharacterCount = Creatures.find({
|
||||
owner: userId,
|
||||
}, {
|
||||
fields: { _id: 1 },
|
||||
}).count();
|
||||
if (tier.characterSlots === -1) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
return tier.characterSlots - currentCharacterCount;
|
||||
}
|
||||
@@ -2,9 +2,9 @@ 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 { getUserTier } from '/imports/api/users/patreon/tiers.js';
|
||||
import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js';
|
||||
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
|
||||
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
|
||||
|
||||
const insertCreature = new ValidatedMethod({
|
||||
|
||||
@@ -23,21 +23,8 @@ const insertCreature = new ValidatedMethod({
|
||||
throw new Meteor.Error('Creatures.methods.insert.denied',
|
||||
'You need to be logged in to insert a creature');
|
||||
}
|
||||
let tier = getUserTier(this.userId);
|
||||
|
||||
let currentCharacterCount = Creatures.find({
|
||||
owner: this.userId,
|
||||
}, {
|
||||
fields: {_id: 1},
|
||||
}).count();
|
||||
|
||||
if (
|
||||
tier.characterSlots !== -1 &&
|
||||
currentCharacterCount >= tier.characterSlots
|
||||
){
|
||||
throw new Meteor.Error('Creatures.methods.insert.denied',
|
||||
`You are already at your limit of ${tier.characterSlots} characters`)
|
||||
}
|
||||
assertHasCharactersSlots(this.userId);
|
||||
|
||||
// Create the creature document
|
||||
let creatureId = Creatures.insert({
|
||||
|
||||
18
app/imports/api/files/UserImages.js
Normal file
18
app/imports/api/files/UserImages.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
|
||||
|
||||
const UserImages = createS3FilesCollection({
|
||||
collectionName: 'userImages',
|
||||
storagePath: Meteor.isDevelopment ? '/DiceCloud/userImages/' : 'assets/app/userImages',
|
||||
onBeforeUpload(file) {
|
||||
// Allow upload files under 10MB
|
||||
if (file.size > 10485760) {
|
||||
return 'Please upload with size equal or less than 10MB';
|
||||
}
|
||||
// Allow common image extensions
|
||||
if (/gif|png|jpe?g|webp/i.test(file.extension || '')) {
|
||||
return 'Please upload an image file only';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default UserImages;
|
||||
@@ -213,9 +213,6 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
||||
return collection;
|
||||
}
|
||||
} else {
|
||||
if (Meteor.isServer){
|
||||
// console.log('No S3 details specified, files will be stored in the local filesystem');
|
||||
}
|
||||
createS3FilesCollection = function({
|
||||
collectionName,
|
||||
storagePath,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import '/imports/api/users/methods/deleteMyAccount.js';
|
||||
import '/imports/api/users/methods/addEmail.js';
|
||||
import '/imports/api/users/methods/removeEmail.js';
|
||||
import '/imports/api/users/methods/updateFileStorageUsed.js';
|
||||
|
||||
import { some } from 'lodash';
|
||||
const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || [];
|
||||
@@ -84,6 +85,10 @@ const userSchema = new SimpleSchema({
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
fileStorageUsed: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
profile: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
|
||||
71
app/imports/api/users/methods/updateFileStorageUsed.js
Normal file
71
app/imports/api/users/methods/updateFileStorageUsed.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
|
||||
import UserImages from '/imports/api/files/UserImages.js';
|
||||
const fileCollections = [ArchiveCreatureFiles, UserImages];
|
||||
|
||||
const updateFileStorageUsed = new ValidatedMethod({
|
||||
name: 'users.recalculateFileStorageUsed',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run() {
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) throw new Meteor.Error('No user',
|
||||
'You must be logged in to recalculate your file use');
|
||||
const user = Meteor.users.findOne(userId);
|
||||
if (!user) {
|
||||
throw new Meteor.Error('noUser', 'User not found');
|
||||
}
|
||||
updateFileStorageUsedWork(userId);
|
||||
}
|
||||
});
|
||||
|
||||
export default updateFileStorageUsed;
|
||||
|
||||
export function updateFileStorageUsedWork(userId) {
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('idRequired',
|
||||
'No user ID was provided to update file storage used')
|
||||
}
|
||||
|
||||
let sum = 0;
|
||||
fileCollections.forEach(collection => {
|
||||
collection.find({ userId }, { fields: { size: 1 } }).forEach(file => {
|
||||
sum += file.size;
|
||||
});
|
||||
});
|
||||
|
||||
Meteor.users.update(userId, {
|
||||
$set: {
|
||||
fileStorageUsed: sum,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function incrementFileStorageUsed(userId, amount) {
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('idRequired',
|
||||
'No user ID was provided to update file storage used')
|
||||
}
|
||||
|
||||
const user = Meteor.users.findOne(userId);
|
||||
if (!user) {
|
||||
throw new Meteor.Error('noUser', 'User not found');
|
||||
}
|
||||
|
||||
if (user.fileStorageUsed === undefined) {
|
||||
// The user doesn't have a current value for storage used, calculate it
|
||||
// from scratch
|
||||
updateFileStorageUsedWork(userId);
|
||||
} else {
|
||||
Meteor.users.update(userId, {
|
||||
$inc: {
|
||||
fileStorageUsed: amount,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9,18 +9,21 @@ const TIERS = Object.freeze([
|
||||
minimumEntitledCents: 0,
|
||||
invites: 0,
|
||||
characterSlots: 5,
|
||||
fileStorage: 50,
|
||||
paidBenefits: false,
|
||||
}, {
|
||||
name: 'Dreamer',
|
||||
minimumEntitledCents: 100,
|
||||
invites: 0,
|
||||
characterSlots: 5,
|
||||
fileStorage: 50,
|
||||
paidBenefits: false,
|
||||
}, {
|
||||
name: 'Wanderer',
|
||||
minimumEntitledCents: 300,
|
||||
invites: 0,
|
||||
characterSlots: 5,
|
||||
fileStorage: 50,
|
||||
paidBenefits: false,
|
||||
}, {
|
||||
//cost per user $5
|
||||
@@ -28,6 +31,7 @@ const TIERS = Object.freeze([
|
||||
minimumEntitledCents: 500,
|
||||
invites: 0,
|
||||
characterSlots: 20,
|
||||
fileStorage: 200,
|
||||
paidBenefits: true,
|
||||
}, {
|
||||
//cost per user $3.33
|
||||
@@ -35,6 +39,7 @@ const TIERS = Object.freeze([
|
||||
minimumEntitledCents: 1000,
|
||||
invites: 2,
|
||||
characterSlots: 50,
|
||||
fileStorage: 500,
|
||||
paidBenefits: true,
|
||||
}, {
|
||||
//cost per user $3.333
|
||||
@@ -42,6 +47,7 @@ const TIERS = Object.freeze([
|
||||
minimumEntitledCents: 2000,
|
||||
invites: 5,
|
||||
characterSlots: 120,
|
||||
fileStorage: 1000,
|
||||
paidBenefits: true,
|
||||
}, {
|
||||
//cost per user $3.125
|
||||
@@ -49,6 +55,7 @@ const TIERS = Object.freeze([
|
||||
minimumEntitledCents: 5000,
|
||||
invites: 15,
|
||||
characterSlots: -1, // Unlimited characters
|
||||
fileStorage: 2000,
|
||||
paidBenefits: true,
|
||||
},
|
||||
]);
|
||||
@@ -59,6 +66,7 @@ const GUEST_TIER = Object.freeze({
|
||||
guest: true,
|
||||
invites: 0,
|
||||
characterSlots: 20,
|
||||
fileStorage: 200,
|
||||
paidBenefits: true,
|
||||
});
|
||||
|
||||
@@ -68,6 +76,7 @@ const PATREON_DISABLED_TIER = Object.freeze({
|
||||
name: 'Outlander',
|
||||
invites: 0,
|
||||
characterSlots: -1, // Can have infinitely many characters
|
||||
fileStorage: 1000000, // 1TB file storage
|
||||
paidBenefits: true,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user