Progress on file system UI

This commit is contained in:
Stefan Zermatten
2022-04-22 11:30:59 +02:00
parent b28bcbe079
commit 13b6689ba4
12 changed files with 267 additions and 54 deletions

View File

@@ -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);
}

View File

@@ -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);
},
});

View File

@@ -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',
`You are already at your limit of ${tier.characterSlots} characters`)
}
}
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;
}

View File

@@ -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({

View File

@@ -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,

View 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,
}
});
}
}

View File

@@ -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,
});

View File

@@ -10,6 +10,7 @@ Meteor.publish('user', function(){
apiKey: 1,
darkMode: 1,
subscribedLibraries: 1,
fileStorageUsed: 1,
profile: 1,
preferences: 1,
'services.patreon.id': 1,

View File

@@ -35,16 +35,15 @@
slot="actions"
text
:loading="archiveActionLoading"
:disabled="!numSelected"
:disabled="!numSelected || (mode === 'restore' && characterSlots <= 0)"
color="primary"
@click="archiveAction"
>
{{ mode === 'archive' ? 'Archive' : 'Restore' }}
<template v-if="numSelected > 1">
{{ numSelected }} characters
<template v-if="mode === 'restore' && characterSlots <= 0">
No Character Slots Left
</template>
<template v-else-if="numSelected === 1">
character
<template v-else>
{{ mode === 'archive' ? 'Archive' : 'Restore' }}
</template>
</v-btn>
<v-btn
@@ -65,8 +64,9 @@ import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolder
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 { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import { uniq, flatten } from 'lodash';
import { characterSlotsRemaining } from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
const characterTransform = function(char){
char.url = `/character/${char._id}/${char.urlName || '-'}`;
@@ -146,6 +146,10 @@ export default {
$subscribe: {
'archivedCreatures': [],
'archiveCreatureFiles': [],
'characterList': [],
},
characterSlots(){
return characterSlotsRemaining(Meteor.userId());
},
folders(){
const userId = Meteor.userId();

View File

@@ -0,0 +1,68 @@
<template>
<v-card>
<v-card-title>
{{ model.meta.creatureName }}
</v-card-title>
<v-card-subtitle>
{{ model.size }}
</v-card-subtitle>
<v-card-actions>
<v-btn
v-if="characterSlots > 0"
text
@click="restore(model._id)"
>
Restore
</v-btn>
<v-flex />
<v-btn
icon
>
<v-icon>mdi-delete</v-icon>
</v-btn>
<v-btn
icon
:href="`${model.link}?download=true`"
>
<v-icon>mdi-download</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</template>
<script lang="js">
import restoreCreatureFromFile from '/imports/api/creature/archive/methods/restoreCreatureFromFile.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import { characterSlotsRemaining } from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
export default {
props: {
model: {
type: Object,
required: true,
},
},
data(){return {
restoreLoading: false,
removeLoading: false,
}},
meteor: {
characterSlots(){
return characterSlotsRemaining(Meteor.userId());
},
},
methods: {
restore(){
this.restoreLoading = true;
restoreCreatureFromFile.call({
fileId: this.model._id,
}, error => {
this.restoreLoading = false;
if (!error) return;
console.error(error);
snackbar({text: error.reason});
});
}
},
}
</script>

View File

@@ -1,5 +1,33 @@
<template>
<v-container>
<v-row justify="center">
<v-col
cols="12"
md="4"
lg="3"
class="layout column justify-center align-center"
>
<v-progress-circular
:rotate="-90"
:size="100"
:width="15"
:value="percentFileStorageUsed"
:buffer-value="50"
color="accent"
>
{{ percentFileStorageUsed }}%
</v-progress-circular>
<div class="ma-2 mt-4">
{{ prettyBytes(storageUsed) }} / {{ prettyBytes(storageAllowed) }}
<v-btn
icon
@click="updateStorageUsed"
>
<v-icon>mdi-refresh</v-icon>
</v-btn>
</div>
</v-col>
</v-row>
<v-row>
<template v-if="archiveFiles && archiveFiles.length">
<v-col cols="12">
@@ -12,31 +40,7 @@
md="4"
lg="3"
>
<v-card>
<v-card-title>
{{ file.meta.creatureName }}
</v-card-title>
<v-card-subtitle>
{{ file.size }}
</v-card-subtitle>
<v-card-actions>
<v-btn text>
Restore
</v-btn>
<v-flex />
<v-btn
icon
>
<v-icon>mdi-delete</v-icon>
</v-btn>
<v-btn
icon
:href="`${file.link}?download=true`"
>
<v-icon>mdi-download</v-icon>
</v-btn>
</v-card-actions>
</v-card>
<archive-file-card :model="file" />
</v-col>
</template>
</v-row>
@@ -46,11 +50,31 @@
<script lang="js">
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
import prettyBytes from 'pretty-bytes';
import ArchiveFileCard from '/imports/ui/files/ArchiveFileCard.vue';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import updateFileStorageUsed from '/imports/api/users/methods/updateFileStorageUsed.js';
export default {
components: {
ArchiveFileCard,
},
data(){ return {
updateStorageUsedLoading: false,
}},
meteor: {
$subscribe: {
'archiveCreatureFiles': [],
'characterList': [],
},
storageUsed(){
return Meteor.user().fileStorageUsed || 0;
},
storageAllowed(){
return getUserTier(Meteor.userId()).fileStorage * 1000000;
},
percentFileStorageUsed(){
return Math.round((this.storageUsed / this.storageAllowed) * 100);
},
archiveFiles() {
var userId = Meteor.userId();
@@ -61,13 +85,25 @@ export default {
sort: {'size': -1},
}
).map(f => {
f.json = JSON.stringify(f, null, 2);
f.size = prettyBytes(f.size);
f.link = ArchiveCreatureFiles.link(f);
return f;
});
},
}
},
methods: {
prettyBytes(input){
return prettyBytes(input)
},
updateStorageUsed(){
this.updateStorageUsedLoading = true;
updateFileStorageUsed.call(error => {
this.updateStorageUsedLoading = false;
if (!error) return;
snackbar({text: error.reason});
});
}
},
}
</script>

2
app/package-lock.json generated
View File

@@ -2793,7 +2793,7 @@
},
"signal-exit": {
"version": "3.0.2",
"resolved": "",
"resolved": false,
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"simpl-schema": {