Release 2.1.2 (#735)

* Fix multiple db issues (#734)

* Bump version to 2.1.2-preview

* Fix game image uniqueness (#733)

* Fix faulty image cleanup (#733)

* Fix faulty image cleanup (#733)

* Adjust version to contain preview suffix

* Bump gradle/actions from 4 to 5 (#737)

Bumps [gradle/actions](https://github.com/gradle/actions) from 4 to 5.
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](https://github.com/gradle/actions/compare/v4...v5)

---
updated-dependencies:
- dependency-name: gradle/actions
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Simon
2025-10-08 09:18:01 +02:00
committed by GitHub
parent 33b8b8c1b8
commit e3c3303600
5 changed files with 123 additions and 6 deletions
+2 -2
View File
@@ -79,7 +79,7 @@ jobs:
distribution: 'temurin' distribution: 'temurin'
java-version: '21' java-version: '21'
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v5
- name: Run production build - name: Run production build
env: env:
@@ -135,7 +135,7 @@ jobs:
java-version: '21' java-version: '21'
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v5
- name: Build and push Plugin-API - name: Build and push Plugin-API
run: ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache run: ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache
@@ -94,7 +94,11 @@ class ImageService(
// Always try to get existing image first to avoid detached entity issues // Always try to get existing image first to avoid detached entity issues
val existingImage = imageRepository.findByOriginalUrl(image.originalUrl) 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 // If we have an existing image with content, associate it with the current image
imageContentStore.associate(image, existingImage.contentId) imageContentStore.associate(image, existingImage.contentId)
// Update the current image's content metadata // Update the current image's content metadata
@@ -104,7 +108,7 @@ class ImageService(
return 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 -> TikaInputStream.get { URI.create(image.originalUrl).toURL().openStream() }.use { input ->
image.mimeType = tika.detect(input) image.mimeType = tika.detect(input)
imageContentStore.setContent(image, input) imageContentStore.setContent(image, input)
@@ -135,8 +139,8 @@ class ImageService(
val isImageStillInUse = gameRepository.existsByImage(imageId) || userRepository.existsByAvatar(imageId) val isImageStillInUse = gameRepository.existsByImage(imageId) || userRepository.existsByAvatar(imageId)
if (!isImageStillInUse) { if (!isImageStillInUse) {
imageContentStore.unsetContent(image)
imageRepository.delete(image) imageRepository.delete(image)
imageContentStore.unsetContent(image)
} }
} }
@@ -145,4 +149,9 @@ class ImageService(
imageRepository.save(image) imageRepository.save(image)
return imageContentStore.setContent(image, content) 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
}
} }
@@ -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.
@@ -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.
+1 -1
View File
@@ -6,7 +6,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
import java.nio.file.Files import java.nio.file.Files
group = "org.gameyfin" group = "org.gameyfin"
version = "2.1.1" version = "2.1.2-preview"
allprojects { allprojects {
repositories { repositories {