fix(app-lib): delay account skin data deletion to next reboot

This gives users an opportunity to not unexpectedly lose skin data in
case they log off on accident.
This commit is contained in:
Alejandro González
2025-05-18 23:42:15 +02:00
parent 949078e572
commit a6627ff828
5 changed files with 103 additions and 5 deletions

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "DELETE FROM default_minecraft_capes WHERE minecraft_user_uuid NOT IN (SELECT uuid FROM minecraft_users)",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "e9449930a74c6a6151c3d868042b878b6789927df5adf50986fe642c8afcb681"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "DELETE FROM custom_minecraft_skins WHERE minecraft_user_uuid NOT IN (SELECT uuid FROM minecraft_users)",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "fd494269d944b179ade61876669d1821b46d60f5cb79685c489aaebf13b35d24"
}

View File

@@ -2,11 +2,34 @@ CREATE TABLE default_minecraft_capes (
minecraft_user_uuid TEXT NOT NULL,
id TEXT NOT NULL,
PRIMARY KEY (minecraft_user_uuid, id),
FOREIGN KEY (minecraft_user_uuid) REFERENCES minecraft_users(uuid)
ON DELETE CASCADE ON UPDATE CASCADE
PRIMARY KEY (minecraft_user_uuid, id)
);
-- Emulate a ON UPDATE CASCADE foreign key constraint for the user UUID on the default_minecraft_capes table,
-- but allowing deletion of the user UUID in the minecraft_users table. This allows the application to temporarily
-- keep skin state around for logged-out users, allowing them to retain their skins under the right conditions
CREATE TRIGGER default_minecraft_capes_user_uuid_insert_check
BEFORE INSERT ON default_minecraft_capes FOR EACH ROW
BEGIN
SELECT CASE WHEN NOT EXISTS (
SELECT 1 FROM minecraft_users WHERE uuid = NEW.minecraft_user_uuid
) THEN RAISE(ABORT, 'Cannot add a default cape for an unknown Minecraft user UUID') END;
END;
CREATE TRIGGER default_minecraft_capes_user_uuid_update_check
BEFORE UPDATE ON default_minecraft_capes FOR EACH ROW
BEGIN
SELECT CASE WHEN NOT EXISTS (
SELECT 1 FROM minecraft_users WHERE uuid = NEW.minecraft_user_uuid
) THEN RAISE(ABORT, 'Cannot change a default cape to refer to an unknown Minecraft user UUID') END;
END;
CREATE TRIGGER default_minecraft_capes_user_uuid_update_cascade
AFTER UPDATE OF uuid ON minecraft_users FOR EACH ROW
BEGIN
UPDATE default_minecraft_capes SET minecraft_user_uuid = NEW.uuid WHERE minecraft_user_uuid = OLD.uuid;
END;
CREATE TABLE custom_minecraft_skins (
minecraft_user_uuid TEXT NOT NULL,
texture_key TEXT NOT NULL,
@@ -14,12 +37,33 @@ CREATE TABLE custom_minecraft_skins (
cape_id TEXT,
PRIMARY KEY (minecraft_user_uuid, texture_key, variant, cape_id),
FOREIGN KEY (minecraft_user_uuid) REFERENCES minecraft_users(uuid)
ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (texture_key) REFERENCES custom_minecraft_skin_textures(texture_key)
ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED
);
-- Similar partial foreign key emulation as above
CREATE TRIGGER custom_minecraft_skins_user_uuid_insert_check
BEFORE INSERT ON custom_minecraft_skins FOR EACH ROW
BEGIN
SELECT CASE WHEN NOT EXISTS (
SELECT 1 FROM minecraft_users WHERE uuid = NEW.minecraft_user_uuid
) THEN RAISE(ABORT, 'Cannot add a custom skin for an unknown Minecraft user UUID') END;
END;
CREATE TRIGGER custom_minecraft_skins_user_uuid_update_check
BEFORE UPDATE ON custom_minecraft_skins FOR EACH ROW
BEGIN
SELECT CASE WHEN NOT EXISTS (
SELECT 1 FROM minecraft_users WHERE uuid = NEW.minecraft_user_uuid
) THEN RAISE(ABORT, 'Cannot change a custom skin to refer to an unknown Minecraft user UUID') END;
END;
CREATE TRIGGER custom_minecraft_skins_user_uuid_update_cascade
AFTER UPDATE OF uuid ON minecraft_users FOR EACH ROW
BEGIN
UPDATE custom_minecraft_skins SET minecraft_user_uuid = NEW.uuid WHERE minecraft_user_uuid = OLD.uuid;
END;
CREATE TABLE custom_minecraft_skin_textures (
texture_key TEXT NOT NULL,
texture PNG BLOB NOT NULL,

View File

@@ -24,6 +24,8 @@ pub async fn set(settings: Settings) -> crate::Result<()> {
#[tracing::instrument]
pub async fn cancel_directory_change() -> crate::Result<()> {
// This is called to handle state initialization errors due to folder migrations
// failing, so fetching a DB connection pool from `State::get` is not reliable here
let pool = crate::state::db::connect().await?;
let mut settings = Settings::get(&pool).await?;

View File

@@ -36,5 +36,33 @@ pub(crate) async fn connect() -> crate::Result<Pool<Sqlite>> {
sqlx::migrate!().run(&pool).await?;
if let Err(err) = stale_data_cleanup(&pool).await {
tracing::warn!(
"Failed to clean up stale data from state database: {err}"
);
}
Ok(pool)
}
/// Cleans up data from the database that is no longer referenced, but must be
/// kept around for a little while to allow users to recover from accidental
/// deletions.
async fn stale_data_cleanup(pool: &Pool<Sqlite>) -> crate::Result<()> {
let mut tx = pool.begin().await?;
sqlx::query!(
"DELETE FROM default_minecraft_capes WHERE minecraft_user_uuid NOT IN (SELECT uuid FROM minecraft_users)"
)
.execute(&mut *tx)
.await?;
sqlx::query!(
"DELETE FROM custom_minecraft_skins WHERE minecraft_user_uuid NOT IN (SELECT uuid FROM minecraft_users)"
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}