Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions app/src/main/kotlin/org/gameyfin/app/media/ImageService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@ class ImageService(
// Always try to get existing image first to avoid detached entity issues
val existingImage = imageRepository.findByOriginalUrl(image.originalUrl)

if (existingImage != null && existingImage.contentId != null) {
// Check if the existing image has valid content
val existingImageHasValidContent = (existingImage != null && imageHasValidContent(existingImage))

// If the existing image has valid content we can just associate it instead of downloading again
if (existingImageHasValidContent && existingImage.contentId != null) {
// If we have an existing image with content, associate it with the current image
imageContentStore.associate(image, existingImage.contentId)
// Update the current image's content metadata
Expand All @@ -104,7 +108,7 @@ class ImageService(
return
}

// If no existing image or existing image has no content, download it
// If no existing image or existing image has no valid content, download it
TikaInputStream.get { URI.create(image.originalUrl).toURL().openStream() }.use { input ->
image.mimeType = tika.detect(input)
imageContentStore.setContent(image, input)
Expand Down Expand Up @@ -135,8 +139,8 @@ class ImageService(
val isImageStillInUse = gameRepository.existsByImage(imageId) || userRepository.existsByAvatar(imageId)

if (!isImageStillInUse) {
imageContentStore.unsetContent(image)
imageRepository.delete(image)
imageContentStore.unsetContent(image)
}
}

Expand All @@ -145,4 +149,9 @@ class ImageService(
imageRepository.save(image)
return imageContentStore.setContent(image, content)
}

private fun imageHasValidContent(image: Image): Boolean {
val imageContent = imageContentStore.getContent(image)
return imageContent != null && image.contentLength != null && image.contentLength!! > 0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
-- Flyway Migration: V2.1.2
-- Purpose: Remove unintended single-column uniqueness on GAME_IMAGES.IMAGES_ID
-- (leftover unique constraint / index from initial schema) and
-- replace it with a proper composite uniqueness over (GAME_ID, IMAGES_ID)
-- allowing the same image to be linked to multiple games while
-- preventing duplicate pairs.
--
-- Context Recap:
-- * Initial table GAME_IMAGES had: IMAGES_ID UNIQUE (constraint UKBDE7M3TKHIEEYBINM2ED0B6X1)
-- * V2.1.0.1 only renamed that constraint (to UQ_GAME_IMAGES_IMAGE_ID if present) – did not drop it.
-- * Attempting to drop unique index now shows: "Index ... belongs to constraint FK_GAME_IMAGES_IMAGE".
-- This means H2 re-used (or bound) the existing unique index for the foreign key, so we must drop the FK first.
-- * Prior partial execution of an earlier draft of this migration might already have created
-- composite index UX_GAME_IMAGES_GAME_IMAGE. Script is idempotent.
-- Strategy (idempotent):
-- 1. Drop foreign key FK_GAME_IMAGES_IMAGE (and legacy hashed name) to free the index.
-- 2. Drop the old unique constraint names (hashed and friendly) if they still exist.
-- 3. Drop lingering unique indexes (hashed + variants, including the one ending with _INDEX_C).
-- 4. Create a NON-UNIQUE index on IMAGES_ID.
-- 5. Create (or ensure) composite UNIQUE index (GAME_ID, IMAGES_ID).
-- 6. Recreate foreign key FK_GAME_IMAGES_IMAGE.
-- 7. (Optional) Verification queries shown in comments.

/******************************************************************************************
* 1. Drop foreign key so bound unique index can be removed
******************************************************************************************/
ALTER TABLE GAME_IMAGES
DROP CONSTRAINT IF EXISTS FK_GAME_IMAGES_IMAGE;
-- Legacy hashed name (in case rename migration not applied yet)
ALTER TABLE GAME_IMAGES
DROP CONSTRAINT IF EXISTS FK5YWV1DMXCM2VSQUEB7RHQ3JK9;

/******************************************************************************************
* 2. Drop legacy/friendly unique constraints (if still defined)
******************************************************************************************/
ALTER TABLE GAME_IMAGES
DROP CONSTRAINT IF EXISTS UKBDE7M3TKHIEEYBINM2ED0B6X1; -- original hashed name
ALTER TABLE GAME_IMAGES
DROP CONSTRAINT IF EXISTS UQ_GAME_IMAGES_IMAGE_ID;
-- friendly name

/******************************************************************************************
* 3. Drop lingering unique indexes that may remain after constraint drop
* (H2 auto-named variants; include conservative list). Safe if absent.
******************************************************************************************/
DROP INDEX IF EXISTS UKBDE7M3TKHIEEYBINM2ED0B6X1_INDEX_C;

/******************************************************************************************
* 4. Create supporting NON-UNIQUE index for IMAGES_ID (only if missing)
******************************************************************************************/
CREATE INDEX IF NOT EXISTS IDX_GAME_IMAGES_IMAGE ON GAME_IMAGES (IMAGES_ID);

/******************************************************************************************
* 5. Create / ensure composite uniqueness (prevents duplicate pairs, allows reuse of images)
******************************************************************************************/
CREATE UNIQUE INDEX IF NOT EXISTS UX_GAME_IMAGES_GAME_IMAGE ON GAME_IMAGES (GAME_ID, IMAGES_ID);

/******************************************************************************************
* 6. Recreate foreign key (H2 will use existing non-unique index or create one silently)
******************************************************************************************/
ALTER TABLE GAME_IMAGES
ADD CONSTRAINT FK_GAME_IMAGES_IMAGE FOREIGN KEY (IMAGES_ID) REFERENCES IMAGE (ID);
-- (FK to GAME side should already exist; keep idempotent recreation separate if ever needed.)

/******************************************************************************************
* 7. (Optional verification after migration)
* -- SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE FROM INFORMATION_SCHEMA.CONSTRAINTS WHERE TABLE_NAME='GAME_IMAGES';
* -- SELECT INDEX_NAME, NON_UNIQUE, COLUMN_NAME FROM INFORMATION_SCHEMA.INDEXES WHERE TABLE_NAME='GAME_IMAGES';
* Expected:
* Constraint FK_GAME_IMAGES_IMAGE present (TYPE='REFERENTIAL').
* Composite unique index UX_GAME_IMAGES_GAME_IMAGE (NON_UNIQUE=FALSE, columns GAME_ID, IMAGES_ID).
* Non-unique index IDX_GAME_IMAGES_IMAGE (NON_UNIQUE=TRUE, column IMAGES_ID).
* No remaining single-column unique index enforcing uniqueness of IMAGES_ID alone.
******************************************************************************************/
-- End of migration.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-- Flyway Migration: V2.1.2.2
-- Purpose: Remove orphan (unreferenced) IMAGE rows that are no longer linked to any
-- GAME (cover/header), GAME_IMAGES (many-to-many screenshots), or USERS (avatar).
--
-- Rationale:
-- A previous bug deleted content (files) before deleting the DB row, allowing the
-- IMAGE entity to remain referenced or resurrected. After fixing logic order, we
-- now perform a one-time cleanup of rows that have no remaining foreign key references.
--
-- Safety:
-- The DELETE only targets rows for which no referencing rows exist; it will not
-- violate FK constraints. Uses NOT EXISTS predicates (safer than NOT IN when NULLs present).
--
-- Idempotency:
-- Running this migration again (e.g., in replayed environments) is harmless because
-- once removed, those rows no longer exist.
--
-- Verification (optional; run manually):
-- SELECT COUNT(*) FROM IMAGE i
-- WHERE NOT EXISTS (SELECT 1 FROM GAME g WHERE g.COVER_IMAGE_ID = i.ID)
-- AND NOT EXISTS (SELECT 1 FROM GAME g2 WHERE g2.HEADER_IMAGE_ID = i.ID)
-- AND NOT EXISTS (SELECT 1 FROM GAME_IMAGES gi WHERE gi.IMAGES_ID = i.ID)
-- AND NOT EXISTS (SELECT 1 FROM USERS u WHERE u.AVATAR_ID = i.ID);
-- -- Expect 0 after delete.

DELETE FROM IMAGE i
WHERE NOT EXISTS (SELECT 1 FROM GAME g WHERE g.COVER_IMAGE_ID = i.ID)
AND NOT EXISTS (SELECT 1 FROM GAME g2 WHERE g2.HEADER_IMAGE_ID = i.ID)
AND NOT EXISTS (SELECT 1 FROM GAME_IMAGES gi WHERE gi.IMAGES_ID = i.ID)
AND NOT EXISTS (SELECT 1 FROM USERS u WHERE u.AVATAR_ID = i.ID);

-- End of migration.

2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
import java.nio.file.Files

group = "org.gameyfin"
version = "2.1.1"
version = "2.1.2"

allprojects {
repositories {
Expand Down
Loading