Skip to content

Library scan silently soft-deletes ALL assets when exclusionPatterns contains "*.*" (silent data-loss) #28178

@McBenac

Description

@McBenac

I have searched the existing issues, both open and closed, to make sure this is not a duplicate report.

  • Yes

The bug

A library scan with *.* in exclusionPatterns soft-deletes the entire library.

globToSqlPattern() converts /path/*.* into SQL LIKE '/path/%.%'. SQL % matches /, so the pattern matches every file with a dot anywhere in its absolute path — /path/2020/x.jpg, /path/2021/sub/y.cr2, all of it.

detectOfflineExternalAssets() then sets isOffline=true, deletedAt=NOW() for every match in one transaction, before any disk crawl. Files stay on disk; assets go to trash. After 30 days they're gone.

Only one log line is emitted:

[LibraryService] N asset(s) out of N were offlined due to import paths and/or exclusion pattern(s) in library <id>

No warning. No error. No prompt.

Three occurrences on this deployment: 2025-11-21, 2026-04-04, 2026-04-30 — across v2.2.3, v2.3.1, v2.6.3. Most recent: 176,528 assets soft-deleted in a single second. Recovery is manual SQL.

#24005 reports the same code path with a different trigger (sequential **/YYYY/** removals). It was closed as duplicate of #14995, which is unrelated (trash-empty handling). This issue isolates the cleaner trigger: *.*.

The OS that Immich Server is running on

Ubuntu 24.04.4 LTS

Version of Immich Server

v2.6.3

Version of Immich Mobile App

N/a

Platform with the issue

  • Server
  • Web
  • Mobile

Device make and model

Intel NUC5i5RYH

Your docker-compose.yml content

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    volumes:
      - ${UPLOAD_LOCATION}:/data
      - /etc/localtime:/etc/localtime:ro
      - /mnt/photos:/Library:ro
    env_file:
      - .env
    ports:
      - '2283:2283'
    depends_on:
      - redis
      - database

  redis:
    container_name: immich_redis
    image: docker.io/valkey/valkey:8

  database:
    container_name: immich_postgres
    image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
    env_file:
      - .env

volumes:
  model-cache:

Your .env content

UPLOAD_LOCATION=/immich/library
DB_DATA_LOCATION=./postgres
TZ=Europe/Stockholm
IMMICH_VERSION=v2.6.3
DB_PASSWORD=<redacted>
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
LOG_LEVEL=verbose
IMMICH_MACHINE_LEARNING_URL=http://machine-learning-host:3003

Reproduction steps

  1. Create an external library with importPaths: ["/Library"] and assets in subdirectories (/Library/2020/x.jpg, /Library/2021/y.cr2).
  2. Run a library scan. All assets import.
  3. Add /Library/*.* to exclusionPatterns to exclude loose root-level files.
  4. Trigger a library scan.
  5. Every asset is marked offline. UI shows an empty library.

To verify the SQL match:

SELECT COUNT(*) FROM asset WHERE "originalPath" LIKE '/Library/%.%';
-- Returns the full asset count, because % matches /.
-- On the affected deployment: 180,100 of 180,100.

Relevant log output

[LibraryService] 176528 asset(s) out of 181684 were offlined due to import paths and/or exclusion pattern(s) in library <library-uuid-redacted>


That is the entire output at `LOG_LEVEL=verbose`. No stack trace.

Database after the event:


SELECT date_trunc('hour', "deletedAt") AS deleted_hour, COUNT(*)
FROM asset
WHERE "libraryId" IS NOT NULL AND "deletedAt" IS NOT NULL
GROUP BY deleted_hour ORDER BY deleted_hour DESC LIMIT 5;

-- 2026-04-30 22:00:00+00 | 176528   <- bulk soft-delete in single second

Additional information

Code path (referenced in server/src/repositories/asset.repository.ts detectOfflineExternalAssets() ~line 1055)

.where((eb) =>
  eb.or([
    eb.not(eb.or(paths.map((path) => eb('originalPath', 'like', path)))),
    eb.or(exclusions.map((path) => eb('originalPath', 'like', path))),
  ])
)
.set({ isOffline: true, deletedAt: new Date() })

The bulk-update runs before any disk verification. If exclusions matches everything, the whole library goes to trash atomically.

Glob → SQL LIKE semantics differ

Shell *.* matches foo.txt but not dir/foo.txt* doesn't cross /.
SQL %.% matches both — % matches any character including /.

globToSqlPattern() doesn't translate this. The conversion silently widens the match.

Suggested fixes

  1. Refuse to soft-delete if a single scan would offline > X% of a library. Default 50%, configurable.
  2. Document that *.* in exclusionPatterns uses SQL LIKE, not shell glob — it matches every file with a dot anywhere in the path.
  3. Fix globToSqlPattern() so % doesn't cross /. Mirrors shell glob.
  4. Log which pattern matched how many assets at debug level.

Severity

Silent data-loss. One INFO log line, no warning, no prompt. Soft-deleted assets are gone after 30 days unless manually rescued. A scan should not be able to wipe a library without explicit confirmation. Three occurrences on this deployment across three Immich versions in six months.

Why this isn't a duplicate of #14995

#24005 was closed as a duplicate of #14995. This is NOT a duplicate. The two are different bugs:

I filed #24005 in January — no response. Three more incidents on this deployment since, most recent 2026-04-30 (176k assets soft-deleted on v2.6.3). Silent data-loss bug, still in production. You want paying customers, and I'll be happy to do so if bug reports are understood instead of closed, just because they are created with AI aid.

Available on request

  • Verbose logs bracketing each recurrence
  • DB exports for the affected library
  • Exact exclusionPatterns / importPaths that triggered each event
  • Repro on a fresh test library

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions