Skip to content

서버 404 에러로 인한 무한 삭제 요청 문제를 해결한다#58

Merged
komodgn merged 2 commits intodevelopfrom
fix/#56
Jan 6, 2026
Merged

서버 404 에러로 인한 무한 삭제 요청 문제를 해결한다#58
komodgn merged 2 commits intodevelopfrom
fix/#56

Conversation

@komodgn
Copy link
Owner

@komodgn komodgn commented Jan 6, 2026

개요

  • 서버의 파일명 인코딩 이슈로 인한 404 에러와 이로 인한 무한 삭제 루프를 해결했습니다.
  • 이미지 접근 권한 변경에 대응하기 위해 로컬 DB 구조를 개선했습니다.

주요 변경 사항

  • DB 스키마 변경: AnalyzedImageEntityfile_name 컬럼 추가 (서버 식별자 통일)
  • 동기화 로직 개선:
    • 업로드 시점에 실제 파일명을 DB에 저장
    • 삭제 요청 시 MediaStore 조회 대신 DB에 저장된 파일명 사용 (권한 상실 대응)
  • 이미지 업로드 로직 개선: supervisorScopechunked 적용
    • supervisorScope를 사용하여 특정 이미지 업로드 실패가 전체 프로세스로 전파되지 않도록 격리 (개별 실패와 상관없이 전체 업로드 시도 보장)
    • 실패한 항목은 로직상 누락되지 않고(분석된 이미지 리스트에 추가하지 않음) 다음 동기화 요청 시 재시도될 수 있도록 설계

📄 결과

삭제 로직 성공 확인

Image

서버 파일명 확인

Image

Summary by CodeRabbit

  • Refactor
    • Improved image upload performance through optimized chunk processing
    • Enhanced file management with improved database reliability for tracking analyzed images
    • Strengthened data consistency during image analysis operations

✏️ Tip: You can customize this high-level summary in your review settings.

- 서버 측 404 에러 반환으로 인해 deletePaths에 계속 남아있어 매번 삭제 요청하는 문제 해결
- 이미지가 삭제되거나 권한이 변경되었을 경우를 대비해 DB에 업로드 시점에 파일명 저장
- 이후 삭제 요청 시 해당 파일명 사용
- supervisorScope와 chunked 처리를 통해 대량의 이미지 업로드 성능을 개선하고 예외 처리 강화
@coderabbitai
Copy link

coderabbitai bot commented Jan 6, 2026

📝 Walkthrough

Walkthrough

The changes implement file name tracking for uploaded images across the application. A new fileName field is added to the database schema (version 3→4), with corresponding DAO methods and entity updates. The repository refactors upload/delete/analysis flows to track file name-path pairs and uses parallel coroutine execution for chunk uploads, wrapping analysis completion in a database transaction.

Changes

Cohort / File(s) Summary
Database Schema & Migration
core/room/impl/src/main/java/com/example/metasearch/core/room/impl/database/AppDatabase.kt, core/room/api/src/main/java/com/example/metasearch/core/room/api/entity/AnalyzedImageEntity.kt
Schema version bumped from 3 to 4; new fileName: String field added to AnalyzedImageEntity with @ColumnInfo(name = "file_name"); migration adds TEXT column with default 'unknown.jpg'
Database Module & DAO
core/room/impl/src/main/java/com/example/metasearch/core/room/impl/di/DatabaseModule.kt, core/room/api/src/main/java/com/example/metasearch/core/room/api/dao/AnalyzedImageDao.kt
Migration chain updated to include MIGRATION_3_4; three new DAO methods added: insertAllPaths(), getFileNameByPath(), and transaction wrapper runInTransaction()
Repository Implementation
core/data/impl/src/main/java/com/example/metasearch/core/data/impl/repository/ImageAnalysisRepositoryImpl.kt
Upload tracking refactored from List<String> to List<Pair<String, String>> (path, fileName); chunk uploads parallelized with async/awaitAll; deletion flow retrieves saved fileName from DAO; analysis completion wrapped in runInTransaction; logging updated for new structures

Sequence Diagram

sequenceDiagram
    actor User
    participant Repo as ImageAnalysisRepository
    participant Upload as Upload Service
    participant DAO as AnalyzedImageDao
    participant DB as AppDatabase
    
    User->>Repo: uploadImage(uri, fileName)
    activate Repo
    Repo->>Repo: splitIntoChunks(uri)
    
    par Parallel Uploads
        Repo->>Upload: uploadChunk(chunk1)
        Upload-->>Repo: uri1
        Repo->>Upload: uploadChunk(chunk2)
        Upload-->>Repo: uri2
        Repo->>Upload: uploadChunk(chunkN)
        Upload-->>Repo: uriN
    end
    
    Repo->>Repo: collect Pair<uri, fileName>
    Note over Repo: List<Pair<String, String>>
    
    Repo->>DAO: runInTransaction()
    activate DAO
    
    DAO->>DAO: finishAnalysis(pairs)
    DAO->>DB: insertAllPaths(entities)
    DB-->>DAO: ✓ persisted
    
    DAO-->>Repo: ✓ transaction complete
    deactivate DAO
    
    Repo-->>User: ✓ image analysis complete
    deactivate Repo
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Possibly related PRs

  • PR #43: Directly modifies ImageAnalysisRepositoryImpl to preserve and track original file names during upload and deletion flows, aligned with the filename persistence introduced here.
  • PR #10: Updates the same Room database module components (AnalyzedImageEntity, AnalyzedImageDao, AppDatabase) that are extended in this PR's schema migration.
  • PR #57: Modifies the deletion flow in ImageAnalysisRepositoryImpl to construct delete requests with file name information, building on the fileName tracking refactored here.

Poem

🐰 Hoppy files with names now saved,
Chunks upload in parallel waves,
Database migrates, column blooms,
Transactions keep the data rooms,
File names tracked from upload to end!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title describes addressing infinite deletion requests due to server 404 errors, but the main changes involve database schema expansion, parallel upload processing, and file name persistence strategy rather than solving the 404 error directly.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI Agents
In
@core/data/impl/src/main/java/com/example/metasearch/core/data/impl/repository/ImageAnalysisRepositoryImpl.kt:
- Around line 172-201: The current use of analyzedImageDao.runInTransaction does
not provide real transactional guarantees; replace that block with a call to the
Room database's withTransaction (e.g., database.withTransaction { ... }) so the
personIndexDataSource.setLastPersonIndex, personRepository add/get methods and
analyzedImageDao.insertAllPaths run inside a single transaction; move the entire
block currently inside analyzedImageDao.runInTransaction into
database.withTransaction { } and ensure you call DAO methods (analyzedImageDao)
and repository operations from within that transaction context so failures roll
back atomically.
- Line 129: The fallback to "unknown.jpg" when
galleryRepository.getFileName(uri) returns null can mask frequent failures;
update ImageAnalysisRepositoryImpl where fileName is assigned to detect the null
case from getFileName(uri) and (1) log a warning including the uri and context
using the existing logger, (2) attempt a better deterministic fallback (e.g.,
derive a name from the uri path or timestamp) before using "unknown.jpg", and
(3) ensure any upload/delete code that relies on this name treats "unknown.jpg"
as exceptional; reference getFileName, galleryRepository, and the fileName usage
so you can add logging and the alternative fallback in the same assignment spot.

In
@core/room/api/src/main/java/com/example/metasearch/core/room/api/dao/AnalyzedImageDao.kt:
- Around line 27-30: The runInTransaction(suspend () -> Unit) DAO method is
ineffective because Room’s @Transaction does not wrap lambdas; remove the
runInTransaction method from AnalyzedImageDao and instead perform transactional
work with AppDatabase.withTransaction from the repository layer (inject
AppDatabase into ImageAnalysisRepositoryImpl and wrap the existing transaction
logic in database.withTransaction { ... }) or alternatively replace the DAO
helper with a concrete @Transaction DAO method that directly calls the required
DAO operations (e.g., a new @Transaction method that orchestrates the multiple
DAO calls).

In
@core/room/impl/src/main/java/com/example/metasearch/core/room/impl/di/DatabaseModule.kt:
- Line 29: The migration chain is incomplete: update the call to addMigrations
on the Room database builder to include AppDatabase.MIGRATION_2_3 between
MIGRATION_1_2 and MIGRATION_3_4 (i.e., .addMigrations(AppDatabase.MIGRATION_1_2,
AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4)); if MIGRATION_2_3
intentionally doesn’t exist, either implement AppDatabase.MIGRATION_2_3 with the
schema changes from v2→v3 or add a clear comment/validation that no production
DBs are at version 2 and handle/throw a descriptive error during initialization.
🧹 Nitpick comments (1)
core/data/impl/src/main/java/com/example/metasearch/core/data/impl/repository/ImageAnalysisRepositoryImpl.kt (1)

129-129: Consider sanitizing fileName to prevent potential multipart issues.

File names retrieved from the gallery or MediaStore may contain special characters, path separators, or Unicode that could cause issues when used in multipart form data or on the server side.

🔎 Suggested fileName sanitization

Add a helper function to sanitize file names:

private fun sanitizeFileName(fileName: String): String {
    return fileName
        .replace(Regex("[/\\\\:*?\"<>|]"), "_")
        .take(255) // Most filesystems limit to 255 chars
}

Use it when retrieving file names:

val fileName = sanitizeFileName(galleryRepository.getFileName(uri) ?: "unknown.jpg")

Also applies to: 218-222

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b2e171 and a6a925e.

📒 Files selected for processing (5)
  • core/data/impl/src/main/java/com/example/metasearch/core/data/impl/repository/ImageAnalysisRepositoryImpl.kt
  • core/room/api/src/main/java/com/example/metasearch/core/room/api/dao/AnalyzedImageDao.kt
  • core/room/api/src/main/java/com/example/metasearch/core/room/api/entity/AnalyzedImageEntity.kt
  • core/room/impl/src/main/java/com/example/metasearch/core/room/impl/database/AppDatabase.kt
  • core/room/impl/src/main/java/com/example/metasearch/core/room/impl/di/DatabaseModule.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci-build
🔇 Additional comments (7)
core/room/api/src/main/java/com/example/metasearch/core/room/api/entity/AnalyzedImageEntity.kt (1)

16-17: LGTM!

The new fileName field is correctly defined with proper Room annotations. The non-null type aligns with the migration's DEFAULT value, ensuring data integrity for existing rows.

core/room/impl/src/main/java/com/example/metasearch/core/room/impl/database/AppDatabase.kt (1)

30-34: LGTM!

The migration correctly adds the file_name column with a NOT NULL constraint and a sensible default value for existing rows. This ensures backward compatibility while maintaining data integrity.

core/room/api/src/main/java/com/example/metasearch/core/room/api/dao/AnalyzedImageDao.kt (2)

15-16: LGTM!

Batch insert method with REPLACE strategy correctly handles bulk insertion of analyzed image paths.


24-25: LGTM!

The query correctly retrieves the stored file name for a given path. The nullable return type appropriately handles cases where the path doesn't exist, and LIMIT 1 optimizes the query.

core/data/impl/src/main/java/com/example/metasearch/core/data/impl/repository/ImageAnalysisRepositoryImpl.kt (3)

78-87: Good refactoring to track fileName alongside path.

The change from List<String> to List<Pair<String, String>> enables proper tracking of server file names, which is the key fix for the infinite delete loop issue.


101-118: Excellent fix for the 404 infinite delete loop.

Using the saved fileName from the database instead of querying MediaStore is the correct solution. This handles both the encoding issue and the permission loss scenario mentioned in the PR objectives.

The fallback to "unknown.jpg" provides safety, though it's worth noting this could theoretically delete the wrong file if a previous migration failed and left actual filenames as "unknown.jpg".


122-133: Good performance improvement with parallel chunk upload.

Using supervisorScope with async/awaitAll correctly parallelizes the chunk uploads while ensuring that one failure doesn't cancel other uploads. The filterNotNull() properly handles partial success.

): List<Pair<String, String>> = supervisorScope {
chunk.map { uri ->
async {
val fileName = galleryRepository.getFileName(uri) ?: "unknown.jpg"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the implementation of getFileName
ast-grep --pattern $'fun getFileName($$$): String? {
  $$$
}'

Repository: komodgn/meta-android

Length of output: 46


🏁 Script executed:

# Also search for getFileName method definitions more broadly
rg -t kt 'fun getFileName' -A 10

Repository: komodgn/meta-android

Length of output: 90


🏁 Script executed:

# Check if there's a GalleryRepository interface definition
rg -t kt 'interface GalleryRepository|class.*GalleryRepository' -A 20

Repository: komodgn/meta-android

Length of output: 90


🏁 Script executed:

# Search for getFileName without type constraints
rg 'fun getFileName' -A 10

Repository: komodgn/meta-android

Length of output: 3032


🏁 Script executed:

# Find all Kotlin files and their locations
fd -e kt -e java . core/

Repository: komodgn/meta-android

Length of output: 7521


🏁 Script executed:

# Look at the ImageAnalysisRepositoryImpl file directly
cat -n core/data/impl/src/main/java/com/example/metasearch/core/data/impl/repository/ImageAnalysisRepositoryImpl.kt | head -150 | tail -50

Repository: komodgn/meta-android

Length of output: 2537


getFileName can legitimately return null—ensure null uploads don't accumulate.

The implementation queries MediaStore.Images.Media.DISPLAY_NAME, which can return null if the query fails or the URI yields no results (e.g., invalid URI, deleted file, missing display name, or permission issues). The fallback to "unknown.jpg" is a reasonable defensive mechanism, and this pattern is consistently applied elsewhere in the code (e.g., line 105 in the delete operation). However, if null returns occur frequently in normal operation with valid URIs, files would be uploaded without proper names, defeating the purpose of the fix. Monitor logs to ensure this fallback is rare and investigate if it occurs unexpectedly.

🤖 Prompt for AI Agents
In
@core/data/impl/src/main/java/com/example/metasearch/core/data/impl/repository/ImageAnalysisRepositoryImpl.kt
around line 129, The fallback to "unknown.jpg" when
galleryRepository.getFileName(uri) returns null can mask frequent failures;
update ImageAnalysisRepositoryImpl where fileName is assigned to detect the null
case from getFileName(uri) and (1) log a warning including the uri and context
using the existing logger, (2) attempt a better deterministic fallback (e.g.,
derive a name from the uri path or timestamp) before using "unknown.jpg", and
(3) ensure any upload/delete code that relies on this name treats "unknown.jpg"
as exceptional; reference getFileName, galleryRepository, and the fileName usage
so you can add logging and the alternative fallback in the same assignment spot.

Comment on lines +172 to 201
analyzedImageDao.runInTransaction {
if (newMax > lastIndex) {
personIndexDataSource.setLastPersonIndex(newMax)
}

response.images.forEach { person ->
runCatching {
if (person.isFaceExit && person.imageName != null && person.imageBytes != null) {
val decodedBytes = decode(person.imageBytes, android.util.Base64.DEFAULT)

val existingPersonId = personRepository.getPersonIdByImageName(person.imageName!!)

if (existingPersonId != null) {
Log.d(tag, "기존 인물 매핑 성공: ${person.imageName} -> ID:$existingPersonId")
personRepository.addFaceToExistingPerson(existingPersonId, person.imageName!!, decodedBytes)
} else {
Log.d(tag, "새로운 인물 생성: ${person.imageName}")
personRepository.addAnalyzedPerson(person.imageName!!, decodedBytes)
}
} else {
Log.d(tag, "새로운 인물 생성: ${person.imageName}")
personRepository.addAnalyzedPerson(person.imageName!!, decodedBytes)
Log.w(tag, "저장 스킵됨: 얼굴없음(${!person.isFaceExit}) 또는 데이터가 null임")
}
} else {
Log.w(tag, "저장 스킵됨: 얼굴없음(${!person.isFaceExit}) 또는 데이터가 null임")
}
}.onFailure { e -> Log.e(tag, "인물 개별 저장 실패: ${person.imageName}", e) }
}.onFailure { e -> Log.e(tag, "인물 개별 저장 실패: ${person.imageName}", e) }
}

val entries = successfulPaths.map { (path, fileName) ->
AnalyzedImageEntity(imagePath = path, fileName = fileName)
}
analyzedImageDao.insertAllPaths(entries)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Transaction will not work as implemented.

The analyzedImageDao.runInTransaction call does not provide actual transactional guarantees (see the DAO review comment). This means that if any operation within this block fails, the other operations may still commit, potentially causing data inconsistency between:

  • Person index updates
  • Person/face additions
  • Analyzed image path insertions

This issue is addressed in the review comment for AnalyzedImageDao.kt. Please apply the suggested fix using database.withTransaction { }.

🤖 Prompt for AI Agents
In
@core/data/impl/src/main/java/com/example/metasearch/core/data/impl/repository/ImageAnalysisRepositoryImpl.kt
around lines 172-201, The current use of analyzedImageDao.runInTransaction does
not provide real transactional guarantees; replace that block with a call to the
Room database's withTransaction (e.g., database.withTransaction { ... }) so the
personIndexDataSource.setLastPersonIndex, personRepository add/get methods and
analyzedImageDao.insertAllPaths run inside a single transaction; move the entire
block currently inside analyzedImageDao.runInTransaction into
database.withTransaction { } and ensure you call DAO methods (analyzedImageDao)
and repository operations from within that transaction context so failures roll
back atomically.

Comment on lines +27 to +30
@Transaction
suspend fun runInTransaction(action: suspend () -> Unit) {
action()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Room @transaction does not wrap arbitrary lambda blocks.

The @Transaction annotation on runInTransaction will not provide transactional semantics for the action lambda. Room's @Transaction only works for database operations directly invoked within the annotated DAO method—it does not wrap arbitrary suspend lambdas passed as parameters.

To execute a transaction with multiple operations, use database.withTransaction { } from the repository layer or define a concrete @Transaction DAO method that calls multiple DAO operations directly.

🔎 Recommended fix using RoomDatabase.withTransaction

Remove this method from the DAO:

-    @Transaction
-    suspend fun runInTransaction(action: suspend () -> Unit) {
-        action()
-    }

In the repository (ImageAnalysisRepositoryImpl.kt), inject the AppDatabase instance and use:

// Add to constructor
private val database: AppDatabase

// In processAnalysisFinish
database.withTransaction {
    // existing transaction logic
}

Or define a proper @Transaction DAO method that directly invokes other DAO operations.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
@core/room/api/src/main/java/com/example/metasearch/core/room/api/dao/AnalyzedImageDao.kt
around lines 27-30, The runInTransaction(suspend () -> Unit) DAO method is
ineffective because Room’s @Transaction does not wrap lambdas; remove the
runInTransaction method from AnalyzedImageDao and instead perform transactional
work with AppDatabase.withTransaction from the repository layer (inject
AppDatabase into ImageAnalysisRepositoryImpl and wrap the existing transaction
logic in database.withTransaction { ... }) or alternatively replace the DAO
helper with a concrete @Transaction DAO method that directly calls the required
DAO operations (e.g., a new @Transaction method that orchestrates the multiple
DAO calls).

DATABASE_NAME,
).build()
)
.addMigrations(AppDatabase.MIGRATION_1_2, AppDatabase.MIGRATION_3_4)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for MIGRATION_2_3 and migration definitions
rg -n "MIGRATION_2_3|MIGRATION_[0-9]_[0-9]" --type kotlin -A 2

Repository: komodgn/meta-android

Length of output: 1473


🏁 Script executed:

# Find AppDatabase file to check all migration definitions
fd -e "kt" "AppDatabase" --exec grep -l "MIGRATION" {} \;

Repository: komodgn/meta-android

Length of output: 156


🏁 Script executed:

# Get the DatabaseModule.kt file context around line 29
fd -e "kt" "DatabaseModule" --exec cat -n {} \;

Repository: komodgn/meta-android

Length of output: 1722


🏁 Script executed:

# Get AppDatabase class definition to see version annotation
cat -n core/room/impl/src/main/java/com/example/metasearch/core/room/impl/database/AppDatabase.kt | head -40

Repository: komodgn/meta-android

Length of output: 1748


🏁 Script executed:

# Search for any reference to database version or migration fallback strategies
rg -n "version|Schema|fallbackToDestructiveMigration" --type kotlin core/room/impl/src/main/java/com/example/metasearch/core/room/impl/database/

Repository: komodgn/meta-android

Length of output: 295


Add missing MIGRATION_2_3 to complete the migration chain.

The database schema version is 4, but the migration path is broken: MIGRATION_1_2 (1→2) and MIGRATION_3_4 (3→4) are defined, but MIGRATION_2_3 is missing. Any production database at version 2 will fail to migrate to version 4. Define MIGRATION_2_3 or document why the gap exists and that no databases are at version 2.

🤖 Prompt for AI Agents
In
@core/room/impl/src/main/java/com/example/metasearch/core/room/impl/di/DatabaseModule.kt
around line 29, The migration chain is incomplete: update the call to
addMigrations on the Room database builder to include AppDatabase.MIGRATION_2_3
between MIGRATION_1_2 and MIGRATION_3_4 (i.e.,
.addMigrations(AppDatabase.MIGRATION_1_2, AppDatabase.MIGRATION_2_3,
AppDatabase.MIGRATION_3_4)); if MIGRATION_2_3 intentionally doesn’t exist,
either implement AppDatabase.MIGRATION_2_3 with the schema changes from v2→v3 or
add a clear comment/validation that no production DBs are at version 2 and
handle/throw a descriptive error during initialization.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Android CI Summary

Step Results:

  • Debug Build: ✅ Success (3m 55s)
  • Code Style Check: ✅ Success (3m 54s)

Total Time: 7m 49s

🎉 All steps completed successfully!

@komodgn komodgn merged commit 3df5ee6 into develop Jan 6, 2026
2 checks passed
@komodgn komodgn deleted the fix/#56 branch January 6, 2026 07:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments