From 30dab1ed6ac027ac83592478eb616c3299483484 Mon Sep 17 00:00:00 2001 From: LinuxMaster393 <73559323+linuxmaster393@users.noreply.github.com> Date: Mon, 24 Mar 2025 11:57:46 -0400 Subject: [PATCH 01/15] Write cron job to process all creatures and archive any that haven't been updated since duration configured in Meteor settings. --- .../server/cron/archiveOldCreatures.js | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 app/imports/server/cron/archiveOldCreatures.js diff --git a/app/imports/server/cron/archiveOldCreatures.js b/app/imports/server/cron/archiveOldCreatures.js new file mode 100644 index 000000000..a33b3f024 --- /dev/null +++ b/app/imports/server/cron/archiveOldCreatures.js @@ -0,0 +1,44 @@ +import Creatures from '/imports/api/creature/creatures/Creatures'; +import archiveCreature from '/imports/api/creature/archive/methods/archiveCreatureToFile'; +import { assertAdmin } from '/imports/api/sharing/sharingPermissions'; +import { SyncedCron } from 'meteor/littledata:synced-cron'; +const archiveAfterDays = !!Meteor.settings?.archiveAfterDays; + +Meteor.startup(() => { + /** + * Archive all creatures older than the configured amount of days. + */ + const archiveOldCreatures = function () { + if (typeof archiveAfterDays != 'number') { + return; + } + const now = new Date(); + const expire = new Date(now.getTime() - (archiveAfterDays * 24 * 60 * 60 * 1000)); + Creatures.find({ lastComputeTime: { $lt: expire }}, { _id: 1 }).forEach( creature => { + archiveCreature(creature._id, function (error) { + if (error) { + console.error(JSON.stringify(error, null, 2)); + } + }); + }); + } + + SyncedCron.add({ + name: 'archiveOldCreatures', + schedule: function (parser) { + return parser.text('every 1 hour'); + }, + job: archiveOldCreatures, + }); + + SyncedCron.start(); + + // Add a method to manually trigger removal + Meteor.methods({ + archiveOldCreatures() { + assertAdmin(this.userId); + this.unblock(); + archiveOldCreatures(); + }, + }); +}); From c9863ef69d90a831f1ecb39892d104e9ac999d3e Mon Sep 17 00:00:00 2001 From: LinuxMaster393 <73559323+linuxmaster393@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:47:59 -0400 Subject: [PATCH 02/15] Import the cron job, reduce iteration time for testing, add archive time to docker-compose.yml, and point Dockerfile at this repo for development. --- Dockerfile | 2 +- app/imports/server/cron/archiveOldCreatures.js | 2 +- app/server/main.js | 1 + docker-compose.yml | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3c42def26..7732e31fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ USER mt RUN curl https://install.meteor.com/ | sh WORKDIR /home/mt -RUN git clone https://github.com/ThaumRystra/DiceCloud dicecloud +RUN git clone https://github.com/LinuxMaster393/DiceCloud dicecloud WORKDIR /home/mt/dicecloud/app RUN npm install --production ENV PATH=$PATH:/home/mt/.meteor diff --git a/app/imports/server/cron/archiveOldCreatures.js b/app/imports/server/cron/archiveOldCreatures.js index a33b3f024..0c70b2f09 100644 --- a/app/imports/server/cron/archiveOldCreatures.js +++ b/app/imports/server/cron/archiveOldCreatures.js @@ -26,7 +26,7 @@ Meteor.startup(() => { SyncedCron.add({ name: 'archiveOldCreatures', schedule: function (parser) { - return parser.text('every 1 hour'); + return parser.text('every 2 minutes'); }, job: archiveOldCreatures, }); diff --git a/app/server/main.js b/app/server/main.js index eeab11f8c..e606e48a9 100644 --- a/app/server/main.js +++ b/app/server/main.js @@ -8,6 +8,7 @@ import '/imports/server/config/SyncedCronConfig'; import '/imports/server/config/redisCaching'; import '/imports/server/publications/index'; import '/imports/server/cron/deleteSoftRemovedDocuments'; +import '/imports/server/cron/archiveOldCreatures'; import '/imports/api/parenting/organizeMethods'; import '/imports/api/users/patreon/updatePatreonOnLogin'; import '/imports/migrations/server/index'; diff --git a/docker-compose.yml b/docker-compose.yml index b6f40f3ed..c86ecc0d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: - MONGO_URL=mongodb://meteor:meteor@dicecloud-db:27017 - PORT=3000 - NODE_ENV=production - - METEOR_SETTINGS={"public":{"environment":"production","disablePatreon":true}} + - METEOR_SETTINGS={"public":{"environment":"production","disablePatreon":true},"archiveAfterDays":1} - MAIL_URL=smtp://EMAIL:PASSWORD@SERVER:PORT ports: - - '3000:3000' #The internal port should match the port set in the environmental variables \ No newline at end of file + - '3000:3000' #The internal port should match the port set in the environmental variables From 9330ad4348488a4ee318c0afed0383fee2fbee66 Mon Sep 17 00:00:00 2001 From: LinuxMaster393 <73559323+linuxmaster393@users.noreply.github.com> Date: Mon, 24 Mar 2025 21:15:49 -0400 Subject: [PATCH 03/15] Added logging to check type of archiveAfterDays --- app/imports/server/cron/archiveOldCreatures.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/imports/server/cron/archiveOldCreatures.js b/app/imports/server/cron/archiveOldCreatures.js index 0c70b2f09..a5e60a2e8 100644 --- a/app/imports/server/cron/archiveOldCreatures.js +++ b/app/imports/server/cron/archiveOldCreatures.js @@ -9,6 +9,7 @@ Meteor.startup(() => { * Archive all creatures older than the configured amount of days. */ const archiveOldCreatures = function () { + console.log("archiver " + archiveAfterDays); if (typeof archiveAfterDays != 'number') { return; } From 0b6bb2d8c70ecedd173a4cce8627eb0eb0d175bc Mon Sep 17 00:00:00 2001 From: LinuxMaster393 <73559323+linuxmaster393@users.noreply.github.com> Date: Mon, 24 Mar 2025 23:48:35 -0400 Subject: [PATCH 04/15] Remove double not operator converting Meteor settings archiveAfterDays to boolean --- app/imports/server/cron/archiveOldCreatures.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/imports/server/cron/archiveOldCreatures.js b/app/imports/server/cron/archiveOldCreatures.js index a5e60a2e8..ba9e957a7 100644 --- a/app/imports/server/cron/archiveOldCreatures.js +++ b/app/imports/server/cron/archiveOldCreatures.js @@ -2,14 +2,14 @@ import Creatures from '/imports/api/creature/creatures/Creatures'; import archiveCreature from '/imports/api/creature/archive/methods/archiveCreatureToFile'; import { assertAdmin } from '/imports/api/sharing/sharingPermissions'; import { SyncedCron } from 'meteor/littledata:synced-cron'; -const archiveAfterDays = !!Meteor.settings?.archiveAfterDays; +const archiveAfterDays = Meteor.settings?.archiveAfterDays; Meteor.startup(() => { /** * Archive all creatures older than the configured amount of days. */ const archiveOldCreatures = function () { - console.log("archiver " + archiveAfterDays); + console.log("archiver " + archiveAfterDays + " type: " + typeof archiveAfterDays); if (typeof archiveAfterDays != 'number') { return; } From 7a863376fb6762c12690d2380851e27fcdb37056 Mon Sep 17 00:00:00 2001 From: LinuxMaster393 <73559323+linuxmaster393@users.noreply.github.com> Date: Tue, 25 Mar 2025 12:56:12 -0400 Subject: [PATCH 05/15] Added logging to cron job to figure out why it isn't archiving creatures. --- app/imports/server/cron/archiveOldCreatures.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/imports/server/cron/archiveOldCreatures.js b/app/imports/server/cron/archiveOldCreatures.js index ba9e957a7..f41292122 100644 --- a/app/imports/server/cron/archiveOldCreatures.js +++ b/app/imports/server/cron/archiveOldCreatures.js @@ -15,8 +15,11 @@ Meteor.startup(() => { } const now = new Date(); const expire = new Date(now.getTime() - (archiveAfterDays * 24 * 60 * 60 * 1000)); + console.log("archiving all creatures after " + expire); Creatures.find({ lastComputeTime: { $lt: expire }}, { _id: 1 }).forEach( creature => { + console.log("archiving " + creature._id); archiveCreature(creature._id, function (error) { + console.log("Archive callback") if (error) { console.error(JSON.stringify(error, null, 2)); } From aa020d928c0bb11136184163c3bb699e9fd7fa86 Mon Sep 17 00:00:00 2001 From: LinuxMaster393 <73559323+linuxmaster393@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:16:11 -0400 Subject: [PATCH 06/15] Fix key value for lastComputedAt in the auto archive cron job. --- app/imports/server/cron/archiveOldCreatures.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/imports/server/cron/archiveOldCreatures.js b/app/imports/server/cron/archiveOldCreatures.js index f41292122..85b7933dd 100644 --- a/app/imports/server/cron/archiveOldCreatures.js +++ b/app/imports/server/cron/archiveOldCreatures.js @@ -16,7 +16,7 @@ Meteor.startup(() => { const now = new Date(); const expire = new Date(now.getTime() - (archiveAfterDays * 24 * 60 * 60 * 1000)); console.log("archiving all creatures after " + expire); - Creatures.find({ lastComputeTime: { $lt: expire }}, { _id: 1 }).forEach( creature => { + Creatures.find({ lastComputedAt: { $lt: expire }}, { _id: 1 }).forEach( creature => { console.log("archiving " + creature._id); archiveCreature(creature._id, function (error) { console.log("Archive callback") From cb7e323c153b38f766ee5494434692faa20fcb26 Mon Sep 17 00:00:00 2001 From: LinuxMaster393 <73559323+linuxmaster393@users.noreply.github.com> Date: Tue, 25 Mar 2025 17:01:03 -0400 Subject: [PATCH 07/15] Add print statements to try and figure out why creatures aren't archiving --- .../api/creature/archive/methods/archiveCreatureToFile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/imports/api/creature/archive/methods/archiveCreatureToFile.js b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js index 57d0002b6..5b883d9f1 100644 --- a/app/imports/api/creature/archive/methods/archiveCreatureToFile.js +++ b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js @@ -35,6 +35,7 @@ export function getArchiveObj(creatureId) { } export const archiveCreature = Meteor.wrapAsync(function archiveCreatureFn(creatureId, callback) { + console.log("Archiving creature " + creatureId); const archive = getArchiveObj(creatureId); const buffer = Buffer.from(JSON.stringify(archive, null, 2)); ArchiveCreatureFiles.write(buffer, { From 5acdf1c6c00745317f4d042317dd9f3686e86948 Mon Sep 17 00:00:00 2001 From: LinuxMaster393 <73559323+linuxmaster393@users.noreply.github.com> Date: Tue, 25 Mar 2025 17:36:56 -0400 Subject: [PATCH 08/15] Remove the callback from archiveCreature --- app/imports/server/cron/archiveOldCreatures.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/imports/server/cron/archiveOldCreatures.js b/app/imports/server/cron/archiveOldCreatures.js index 85b7933dd..c36c8c0e9 100644 --- a/app/imports/server/cron/archiveOldCreatures.js +++ b/app/imports/server/cron/archiveOldCreatures.js @@ -18,12 +18,7 @@ Meteor.startup(() => { console.log("archiving all creatures after " + expire); Creatures.find({ lastComputedAt: { $lt: expire }}, { _id: 1 }).forEach( creature => { console.log("archiving " + creature._id); - archiveCreature(creature._id, function (error) { - console.log("Archive callback") - if (error) { - console.error(JSON.stringify(error, null, 2)); - } - }); + archiveCreature(creature._id); }); } From 7de86389c9e18e3dda551c042ef9a693a388157d Mon Sep 17 00:00:00 2001 From: LinuxMaster393 <73559323+linuxmaster393@users.noreply.github.com> Date: Fri, 28 Mar 2025 10:42:59 -0400 Subject: [PATCH 09/15] Fix import statement in archiveOldCreatures, remove debug log from archiveCreatureToFile. --- .../api/creature/archive/methods/archiveCreatureToFile.js | 1 - app/imports/server/cron/archiveOldCreatures.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/imports/api/creature/archive/methods/archiveCreatureToFile.js b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js index 5b883d9f1..57d0002b6 100644 --- a/app/imports/api/creature/archive/methods/archiveCreatureToFile.js +++ b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js @@ -35,7 +35,6 @@ export function getArchiveObj(creatureId) { } export const archiveCreature = Meteor.wrapAsync(function archiveCreatureFn(creatureId, callback) { - console.log("Archiving creature " + creatureId); const archive = getArchiveObj(creatureId); const buffer = Buffer.from(JSON.stringify(archive, null, 2)); ArchiveCreatureFiles.write(buffer, { diff --git a/app/imports/server/cron/archiveOldCreatures.js b/app/imports/server/cron/archiveOldCreatures.js index c36c8c0e9..d7272ca48 100644 --- a/app/imports/server/cron/archiveOldCreatures.js +++ b/app/imports/server/cron/archiveOldCreatures.js @@ -1,5 +1,5 @@ import Creatures from '/imports/api/creature/creatures/Creatures'; -import archiveCreature from '/imports/api/creature/archive/methods/archiveCreatureToFile'; +import { archiveCreature } from '/imports/api/creature/archive/methods/archiveCreatureToFile'; import { assertAdmin } from '/imports/api/sharing/sharingPermissions'; import { SyncedCron } from 'meteor/littledata:synced-cron'; const archiveAfterDays = Meteor.settings?.archiveAfterDays; From 7e89891abc82caa89464294478533f341abd8cde Mon Sep 17 00:00:00 2001 From: LinuxMaster393 <73559323+linuxmaster393@users.noreply.github.com> Date: Fri, 28 Mar 2025 10:47:38 -0400 Subject: [PATCH 10/15] Remove console logs from archiveOldCreatures --- app/imports/server/cron/archiveOldCreatures.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/imports/server/cron/archiveOldCreatures.js b/app/imports/server/cron/archiveOldCreatures.js index d7272ca48..fdc48729c 100644 --- a/app/imports/server/cron/archiveOldCreatures.js +++ b/app/imports/server/cron/archiveOldCreatures.js @@ -9,15 +9,12 @@ Meteor.startup(() => { * Archive all creatures older than the configured amount of days. */ const archiveOldCreatures = function () { - console.log("archiver " + archiveAfterDays + " type: " + typeof archiveAfterDays); if (typeof archiveAfterDays != 'number') { return; } const now = new Date(); const expire = new Date(now.getTime() - (archiveAfterDays * 24 * 60 * 60 * 1000)); - console.log("archiving all creatures after " + expire); Creatures.find({ lastComputedAt: { $lt: expire }}, { _id: 1 }).forEach( creature => { - console.log("archiving " + creature._id); archiveCreature(creature._id); }); } From d23b1fb90efeb0ffd515485ce76f2c1af4a992c0 Mon Sep 17 00:00:00 2001 From: LinuxMaster393 <73559323+linuxmaster393@users.noreply.github.com> Date: Fri, 28 Mar 2025 12:30:53 -0400 Subject: [PATCH 11/15] Change the cron job to run every 10 minutes, like the other job. Change the archive threshold to 100 days. --- app/imports/server/cron/archiveOldCreatures.js | 2 +- docker-compose.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/imports/server/cron/archiveOldCreatures.js b/app/imports/server/cron/archiveOldCreatures.js index fdc48729c..8380a3698 100644 --- a/app/imports/server/cron/archiveOldCreatures.js +++ b/app/imports/server/cron/archiveOldCreatures.js @@ -22,7 +22,7 @@ Meteor.startup(() => { SyncedCron.add({ name: 'archiveOldCreatures', schedule: function (parser) { - return parser.text('every 2 minutes'); + return parser.text('every 10 minutes'); }, job: archiveOldCreatures, }); diff --git a/docker-compose.yml b/docker-compose.yml index c86ecc0d0..17909a547 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,8 @@ services: - MONGO_URL=mongodb://meteor:meteor@dicecloud-db:27017 - PORT=3000 - NODE_ENV=production - - METEOR_SETTINGS={"public":{"environment":"production","disablePatreon":true},"archiveAfterDays":1} + - METEOR_SETTINGS={"public":{"environment":"production","disablePatreon":true},"archiveAfterDays":100} - MAIL_URL=smtp://EMAIL:PASSWORD@SERVER:PORT ports: - '3000:3000' #The internal port should match the port set in the environmental variables + From 611e72751f626ba51b70f7eb0b1d6c003dad45cb Mon Sep 17 00:00:00 2001 From: LinuxMaster393 <73559323+linuxmaster393@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:42:16 -0400 Subject: [PATCH 12/15] Change Dockerfile clone url to upstream url. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7732e31fd..3c42def26 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ USER mt RUN curl https://install.meteor.com/ | sh WORKDIR /home/mt -RUN git clone https://github.com/LinuxMaster393/DiceCloud dicecloud +RUN git clone https://github.com/ThaumRystra/DiceCloud dicecloud WORKDIR /home/mt/dicecloud/app RUN npm install --production ENV PATH=$PATH:/home/mt/.meteor From 46d2c679e02043c20c5146e507df9186b0872aa6 Mon Sep 17 00:00:00 2001 From: LinuxUser393 <73559323+linuxuser393@users.noreply.github.com> Date: Fri, 18 Apr 2025 17:55:48 -0400 Subject: [PATCH 13/15] Automatically archived creatures now have "meta.archive" set to true. Fixed redis-oplog submodule. --- .../api/creature/archive/methods/archiveCreatureToFile.js | 5 +++-- app/imports/server/cron/archiveOldCreatures.js | 2 +- app/packages/redis-oplog | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) create mode 160000 app/packages/redis-oplog diff --git a/app/imports/api/creature/archive/methods/archiveCreatureToFile.js b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js index 57d0002b6..c3996c9e6 100644 --- a/app/imports/api/creature/archive/methods/archiveCreatureToFile.js +++ b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js @@ -34,7 +34,7 @@ export function getArchiveObj(creatureId) { return archiveCreature; } -export const archiveCreature = Meteor.wrapAsync(function archiveCreatureFn(creatureId, callback) { +export const archiveCreature = Meteor.wrapAsync(function archiveCreatureFn(creatureId, autoArchive, callback) { const archive = getArchiveObj(creatureId); const buffer = Buffer.from(JSON.stringify(archive, null, 2)); ArchiveCreatureFiles.write(buffer, { @@ -45,6 +45,7 @@ export const archiveCreature = Meteor.wrapAsync(function archiveCreatureFn(creat schemaVersion: SCHEMA_VERSION, creatureId: archive.creature._id, creatureName: archive.creature.name, + auto: !!autoArchive, }, }, (error, fileRef) => { if (error) { @@ -90,7 +91,7 @@ const archiveCreatureToFile = new ValidatedMethod({ async run({ creatureId }) { assertOwnership(creatureId, this.userId); if (Meteor.isServer) { - archiveCreature(creatureId); + archiveCreature(creatureId, false); } else { removeCreatureWork(creatureId); } diff --git a/app/imports/server/cron/archiveOldCreatures.js b/app/imports/server/cron/archiveOldCreatures.js index 8380a3698..c7d56f427 100644 --- a/app/imports/server/cron/archiveOldCreatures.js +++ b/app/imports/server/cron/archiveOldCreatures.js @@ -15,7 +15,7 @@ Meteor.startup(() => { const now = new Date(); const expire = new Date(now.getTime() - (archiveAfterDays * 24 * 60 * 60 * 1000)); Creatures.find({ lastComputedAt: { $lt: expire }}, { _id: 1 }).forEach( creature => { - archiveCreature(creature._id); + archiveCreature(creature._id, true); }); } diff --git a/app/packages/redis-oplog b/app/packages/redis-oplog new file mode 160000 index 000000000..83e302c15 --- /dev/null +++ b/app/packages/redis-oplog @@ -0,0 +1 @@ +Subproject commit 83e302c15456d6744047c50fc8d1add9e739b001 From eae0b4a44febe515c3ed20fc82992b767966be54 Mon Sep 17 00:00:00 2001 From: LinuxUser393 <73559323+linuxuser393@users.noreply.github.com> Date: Wed, 23 Apr 2025 16:10:12 -0400 Subject: [PATCH 14/15] Updated publication backend to restore automatically archived creatures when the "singleCharacter" publication is subscribed to and the target creature was automatically archived. Automatically archiving creatures now leaves the entry in the creature database, setting the archiveId property. Updated archive deleting to check for and delete the corresponding creature entry. Updated manual creature archiving to check if the creature was auto archived, delete the creature entry, and remove the auto flag from the archive. The files page shows auto archives with a label saying such, and shows an open button instead of a restore button. Fixed issue where updating the file storage used would fail due to a server side error. --- .../archive/methods/archiveCreatureToFile.js | 40 +++++++++- .../archive/methods/removeArchiveCreature.js | 10 +++ .../methods/restoreCreatureFromFile.js | 80 +++++++++++++++++++ .../archive/methods/softArchiveCreature.js | 27 +++++++ .../api/creature/creatures/Creatures.ts | 6 ++ .../users/methods/updateFileStorageUsed.js | 10 +-- .../ui/creature/archive/ArchiveDialog.vue | 31 ++++--- .../creatureList/CreatureFolderList.vue | 6 +- .../ui/creature/creatureList/CreatureList.vue | 6 +- .../client/ui/files/ArchiveFileCard.vue | 11 ++- app/imports/client/ui/pages/Files.vue | 2 + .../server/cron/archiveOldCreatures.js | 2 +- .../server/publications/characterList.js | 5 ++ .../server/publications/singleCharacter.js | 5 ++ 14 files changed, 213 insertions(+), 28 deletions(-) create mode 100644 app/imports/api/creature/archive/methods/softArchiveCreature.js diff --git a/app/imports/api/creature/archive/methods/archiveCreatureToFile.js b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js index c3996c9e6..bce9ecd3c 100644 --- a/app/imports/api/creature/archive/methods/archiveCreatureToFile.js +++ b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js @@ -9,6 +9,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import CreatureLogs from '/imports/api/creature/log/CreatureLogs'; import Experiences from '/imports/api/creature/experience/Experiences'; import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature'; +import { toSoftArchive } from '/imports/api/creature/archive/methods/softArchiveCreature'; import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles'; import { getFilter } from '/imports/api/parenting/parentingFunctions'; @@ -53,7 +54,11 @@ export const archiveCreature = Meteor.wrapAsync(function archiveCreatureFn(creat callback(error); } else if (!Meteor.settings.useS3) { // If we aren't using s3, remove the creature and call the callback - removeCreatureWork(creatureId); + if (autoArchive) { + toSoftArchive(creatureId, fileRef._id); + } else { + removeCreatureWork(creatureId); + } callback(); } else { // Wait for s3Result event that occurs when the s3 attempt to write ends. @@ -65,7 +70,11 @@ export const archiveCreature = Meteor.wrapAsync(function archiveCreatureFn(creat ArchiveCreatureFiles.off('s3Result', resultHandler); // Remove the creature if there was no error if (!s3Error) { - removeCreatureWork(creatureId); + if (autoArchive) { + toSoftArchive(creatureId, fileRef._id); + } else { + removeCreatureWork(creatureId); + } } // Alert the callback that we're done callback(s3Error); @@ -75,6 +84,26 @@ export const archiveCreature = Meteor.wrapAsync(function archiveCreatureFn(creat }, true); }); +export function softArchiveToArchive(creatureId) { + let creature = Creatures.findOne({ _id: creatureId }); + Creatures.remove({ _id: creatureId }); + ArchiveCreatureFiles.update( + { + _id: creature.archiveId, + 'meta.creatureId': creature._id, + 'meta.auto': true, + }, + { + $set: { 'meta.auto': false }, + }, error => { + this.archiveActionLoading = false; + if (!error) return; + console.error(error); + snackbar({text: error.reason}); + } + ); +} + const archiveCreatureToFile = new ValidatedMethod({ name: 'Creatures.methods.archiveCreatureToFile', validate: new SimpleSchema({ @@ -91,7 +120,12 @@ const archiveCreatureToFile = new ValidatedMethod({ async run({ creatureId }) { assertOwnership(creatureId, this.userId); if (Meteor.isServer) { - archiveCreature(creatureId, false); + let creature = Creatures.findOne({ _id: creatureId }, { fields: { archiveId: 1 }}); + if (creature.archiveId) { + softArchiveToArchive(creatureId); + } else { + archiveCreature(creatureId, false); + } } else { removeCreatureWork(creatureId); } diff --git a/app/imports/api/creature/archive/methods/removeArchiveCreature.js b/app/imports/api/creature/archive/methods/removeArchiveCreature.js index 7018b0b13..bc57ccea3 100644 --- a/app/imports/api/creature/archive/methods/removeArchiveCreature.js +++ b/app/imports/api/creature/archive/methods/removeArchiveCreature.js @@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles'; +import Creatures from '/imports/api/creature/creatures/Creatures'; import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed'; const removeArchiveCreature = new ValidatedMethod({ @@ -30,6 +31,15 @@ const removeArchiveCreature = new ValidatedMethod({ throw new Meteor.Error('Permission denied', 'You can only restore creatures you own'); } + if (file.meta.auto) { + Creatures.remove( + { + _id: file.meta.creatureId, + owner: this.userId, + archiveId: file._id, + } + ); + } //Remove the archive once the restore succeeded ArchiveCreatureFiles.remove({ _id: fileId }); // Update the user's file storage limits diff --git a/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js b/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js index 4a8da16aa..12c4725ea 100644 --- a/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js +++ b/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js @@ -101,4 +101,84 @@ const restoreCreaturefromFile = new ValidatedMethod({ }, }); +export const restoreSoftArchive = Meteor.wrapAsync(async function restoreSoftArchiveFn(creatureId, callback) { + if (!Meteor.isServer) return; + + // Verify that the creature and archive match + const existingCreature = Creatures.findOne( + { _id: creatureId }, + { + fields: { + _id: 1, + owner: 1, + archiveId: 1, + }, + } + ); + if (!existingCreature) throw new Meteor.Error('Doesn\'t exist', + 'The creature you are trying to restore doesn\'t exist'); + + if (!existingCreature.archiveId) throw new Meteor.Error('No Linked Archive', + 'The creature doesn\'t have a linked archive.'); + + const archiveFile = ArchiveCreatureFiles.findOne({ + _id: existingCreature.archiveId, + userId: existingCreature.owner, + 'meta.auto': true, + 'meta.creatureId': creatureId, + }); + + const archive = await ArchiveCreatureFiles.readJSONFile(archiveFile); + + if (SCHEMA_VERSION < archive.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); + + // Asset that the archive is safe + verifyArchiveSafety(archive); + + 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); + } + + Creatures.update( + { _id: creatureId }, + { $unset: { + archiveId: true, + }} + ); + } catch (e) { + // If the above fails, delete the inserted creature + CreatureVariables.remove({ _creatureId: creatureId }); + CreatureProperties.remove(getFilter.descendantsOfRoot(creatureId)); + CreatureLogs.remove({ creatureId }); + Experiences.remove({ creatureId }); + Creatures.update( + { _id: creatureId }, + { $set: { + archiveId: archiveFile._id, + }} + ); + console.log(e); + throw e; + } + //Remove the archive once the restore succeeded + ArchiveCreatureFiles.remove({ _id: archiveFile._id }); + // Update the user's file storage limits + incrementFileStorageUsed(archiveFile.userId, -archiveFile.size); + callback(); +}); + export default restoreCreaturefromFile; diff --git a/app/imports/api/creature/archive/methods/softArchiveCreature.js b/app/imports/api/creature/archive/methods/softArchiveCreature.js new file mode 100644 index 000000000..e7eb31a9f --- /dev/null +++ b/app/imports/api/creature/archive/methods/softArchiveCreature.js @@ -0,0 +1,27 @@ +import Creatures from '/imports/api/creature/creatures/Creatures'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import CreatureLogs from '/imports/api/creature/log/CreatureLogs'; +import Experiences from '/imports/api/creature/experience/Experiences'; +import { getFilter } from '/imports/api/parenting/parentingFunctions'; + +function removeRelatedDocuments(creatureId) { + CreatureVariables.remove({ _creatureId: creatureId }); + CreatureProperties.remove(getFilter.descendantsOfRoot(creatureId)); + CreatureLogs.remove({ creatureId }); + Experiences.remove({ creatureId }); +} + +export function toSoftArchive(creatureId, archiveId) { + if (!Meteor.isServer) return; + + Creatures.update( + { _id: creatureId }, + { + $set: { + archiveId: archiveId, + }, + } + ); + removeRelatedDocuments(creatureId); +} diff --git a/app/imports/api/creature/creatures/Creatures.ts b/app/imports/api/creature/creatures/Creatures.ts index e6b170e61..1d1e0bf24 100644 --- a/app/imports/api/creature/creatures/Creatures.ts +++ b/app/imports/api/creature/creatures/Creatures.ts @@ -180,6 +180,12 @@ const CreatureSchema = TypedSimpleSchema.from({ type: CreatureSettingsSchema, defaultValue: {}, }, + + // Was auto archived + 'archiveId': { + type: String, + optional: true, + }, }) .extend(ColorSchema) .extend(SharingSchema); diff --git a/app/imports/api/users/methods/updateFileStorageUsed.js b/app/imports/api/users/methods/updateFileStorageUsed.js index 656915b22..d055727c1 100644 --- a/app/imports/api/users/methods/updateFileStorageUsed.js +++ b/app/imports/api/users/methods/updateFileStorageUsed.js @@ -2,7 +2,6 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles'; import UserImages from '/imports/api/files/userImages/UserImages'; -const fileCollections = [ArchiveCreatureFiles, UserImages]; const updateFileStorageUsed = new ValidatedMethod({ name: 'users.recalculateFileStorageUsed', @@ -33,10 +32,11 @@ export function updateFileStorageUsedWork(userId) { } let sum = 0; - fileCollections.forEach(collection => { - collection.find({ userId }, { fields: { size: 1 } }).forEach(file => { - sum += file.size; - }); + ArchiveCreatureFiles.find({ userId }, { fields: { size: 1 } }).forEach(file => { + sum += file.size; + }); + UserImages.find({ userId }, { fields: { size: 1 } }).forEach(file => { + sum += file.size; }); Meteor.users.update(userId, { diff --git a/app/imports/client/ui/creature/archive/ArchiveDialog.vue b/app/imports/client/ui/creature/archive/ArchiveDialog.vue index 8113ac427..67610d963 100644 --- a/app/imports/client/ui/creature/archive/ArchiveDialog.vue +++ b/app/imports/client/ui/creature/archive/ArchiveDialog.vue @@ -27,8 +27,8 @@ selection :creatures="mode === 'archive' ? CreaturesWithNoParty : archiveCreaturesWithNoParty" :folders="mode === 'archive' ? folders : archivefolders" - :selected-creature="selectedCreature" - @creature-selected="id => selectedCreature = id" + :selected-creature-id="selectedCreatureId" + @creature-selected="id => selectedCreatureId = id" /> { this.archiveActionLoading = false; if (!error) return; console.error(error); snackbar({text: error.reason}); }); - } else if (this.mode === 'restore'){ + } else if (this.mode === 'restore') { restoreCreatureFromFile.call({ - fileId: this.selectedCreature, + fileId: this.selectedCreatureId, }, error => { this.archiveActionLoading = false; if (!error) return; @@ -139,7 +140,7 @@ export default { snackbar({text: error.reason}); }); } - this.selectedCreature = null; + this.selectedCreatureId = null; } }, meteor: { @@ -194,6 +195,10 @@ export default { folder.creatures = ArchiveCreatureFiles.find( { 'meta.creatureId': {$in: folder.creatures || []}, + $or: [ + { 'meta.auto': false }, + { 'meta.auto': { $exists: false } }, + ], userId, }, { sort: {'meta.creatureName': 1}, @@ -211,6 +216,10 @@ export default { return ArchiveCreatureFiles.find( { 'meta.creatureId': {$nin: folderChars}, + $or: [ + { 'meta.auto': false }, + { 'meta.auto': { $exists: false } }, + ], userId, }, { sort: {'meta.creatureName': 1}, diff --git a/app/imports/client/ui/creature/creatureList/CreatureFolderList.vue b/app/imports/client/ui/creature/creatureList/CreatureFolderList.vue index 5c4806d59..ecf5be234 100644 --- a/app/imports/client/ui/creature/creatureList/CreatureFolderList.vue +++ b/app/imports/client/ui/creature/creatureList/CreatureFolderList.vue @@ -8,7 +8,7 @@ @@ -34,7 +34,7 @@ :creatures="folder.creatures" :folder-id="folder._id" :selection="selection" - :selected-creature="selectedCreature" + :selected-creature-id="selectedCreatureId" :dense="dense" @creature-selected="id => $emit('creature-selected', id)" /> @@ -62,7 +62,7 @@ export default { default: () => [], }, selection: Boolean, - selectedCreature: { + selectedCreatureId: { type: String, default: undefined, }, diff --git a/app/imports/client/ui/creature/creatureList/CreatureList.vue b/app/imports/client/ui/creature/creatureList/CreatureList.vue index f8869b013..7ee2eb8f3 100644 --- a/app/imports/client/ui/creature/creatureList/CreatureList.vue +++ b/app/imports/client/ui/creature/creatureList/CreatureList.vue @@ -15,7 +15,7 @@ class="creature" :model="creature" :selection="selection" - :is-selected="selectedCreature === creature._id || selectedCreatures.has(creature._id)" + :is-selected="selectedCreatureId === creature._id || selectedCreatureIds.has(creature)" v-bind="selection ? {} : {to: creature.url}" :dense="dense" :data-id="dense ? undefined : creature._id" @@ -45,11 +45,11 @@ default: null, }, selection: Boolean, - selectedCreature: { + selectedCreatureId: { type: String, default: undefined, }, - selectedCreatures: { + selectedCreatureIds: { type: Set, default: () => new Set(), }, diff --git a/app/imports/client/ui/files/ArchiveFileCard.vue b/app/imports/client/ui/files/ArchiveFileCard.vue index 9eb926ac8..95dff9b19 100644 --- a/app/imports/client/ui/files/ArchiveFileCard.vue +++ b/app/imports/client/ui/files/ArchiveFileCard.vue @@ -4,17 +4,24 @@ {{ model.meta.creatureName }} - {{ model.size }} + {{ model.size }} {{ model.meta.auto ? "(Automatically Archived)" : ""}} Restore + + Open + { f.size = prettyBytes(f.size); f.link = ArchiveCreatureFiles.link(f); + f.url = `/character/${f.meta.creatureId}/${getCreatureUrlName({ name: f.meta.creatureName })}` return f; }); }, diff --git a/app/imports/server/cron/archiveOldCreatures.js b/app/imports/server/cron/archiveOldCreatures.js index c7d56f427..2755064a8 100644 --- a/app/imports/server/cron/archiveOldCreatures.js +++ b/app/imports/server/cron/archiveOldCreatures.js @@ -14,7 +14,7 @@ Meteor.startup(() => { } const now = new Date(); const expire = new Date(now.getTime() - (archiveAfterDays * 24 * 60 * 60 * 1000)); - Creatures.find({ lastComputedAt: { $lt: expire }}, { _id: 1 }).forEach( creature => { + Creatures.find({ lastComputedAt: { $lt: expire }, archiveId: { $exists: false } }, { _id: 1 }).forEach( creature => { archiveCreature(creature._id, true); }); } diff --git a/app/imports/server/publications/characterList.js b/app/imports/server/publications/characterList.js index 6f2f1be92..50dedbc77 100644 --- a/app/imports/server/publications/characterList.js +++ b/app/imports/server/publications/characterList.js @@ -34,6 +34,11 @@ Meteor.publish('characterList', function () { avatarPicture: 1, public: 1, type: 1, + archiveId: { $cond: { + if: "$archiveId", + then: true, + else: null, + }} } } ), diff --git a/app/imports/server/publications/singleCharacter.js b/app/imports/server/publications/singleCharacter.js index c6c0581be..91d56dacf 100644 --- a/app/imports/server/publications/singleCharacter.js +++ b/app/imports/server/publications/singleCharacter.js @@ -9,6 +9,7 @@ import VERSION from '/imports/constants/VERSION'; import { loadCreature } from '/imports/api/engine/loadCreatures'; import { rebuildCreatureNestedSets } from '/imports/api/parenting/parentingFunctions'; import EngineActions from '/imports/api/engine/action/EngineActions'; +import { restoreSoftArchive } from '/imports/api/creature/archive/methods/restoreCreatureFromFile'; let schema = new SimpleSchema({ creatureId: { @@ -36,10 +37,14 @@ Meteor.publish('singleCharacter', function (creatureId) { public: 1, computeVersion: 1, tabletopId: 1, + archiveId: 1, } }); try { assertViewPermission(permissionCreature, userId) } catch (e) { return [] } + if (permissionCreature && permissionCreature.archiveId) { + restoreSoftArchive(permissionCreature._id); + } loadCreature(creatureId, self); if (permissionCreature?.computeVersion !== VERSION && computation.firstRun) { try { From ca17965640eb3453cc4394f673c7301149883c0a Mon Sep 17 00:00:00 2001 From: LinuxUser393 <73559323+linuxuser393@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:04:47 -0400 Subject: [PATCH 15/15] Fix logical error created when renaming selectedCreatures to selectedCreatureIds. --- app/imports/client/ui/creature/creatureList/CreatureList.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/imports/client/ui/creature/creatureList/CreatureList.vue b/app/imports/client/ui/creature/creatureList/CreatureList.vue index 7ee2eb8f3..d300c74fc 100644 --- a/app/imports/client/ui/creature/creatureList/CreatureList.vue +++ b/app/imports/client/ui/creature/creatureList/CreatureList.vue @@ -15,7 +15,7 @@ class="creature" :model="creature" :selection="selection" - :is-selected="selectedCreatureId === creature._id || selectedCreatureIds.has(creature)" + :is-selected="selectedCreatureId === creature._id || selectedCreatureIds.has(creature._id)" v-bind="selection ? {} : {to: creature.url}" :dense="dense" :data-id="dense ? undefined : creature._id"