Skip to content

Conversation

@Freika
Copy link
Owner

@Freika Freika commented Aug 23, 2025

Summary by CodeRabbit

  • New Features

    • Import names now auto-append a timestamp to avoid duplicates.
    • Imports list styling refreshed: zebra-striped rows, hover state, updated action buttons.
  • Bug Fixes

    • Improved detection for several import file types.
    • Safer handling when import source is missing: consistent MIME/orientation defaults and clearer error handling.
  • Documentation

    • CHANGELOG updated for v0.30.11.
  • Chores

    • Database migration to allow nullable import source; tests and fixtures updated.

@coderabbitai
Copy link

coderabbitai bot commented Aug 23, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Make Import.source nullable and remove its default; controller now ensures imported filenames are unique by appending timestamps on collisions; source detection is conditional and persisted by the create service with expanded importer mappings; several services and serializers handle nil sources; UI classes, tests, fixtures, schema, and changelog updated.

Changes

Cohort / File(s) Summary of changes
Import model & DB default
app/models/import.rb, db/migrate/20250823125940_remove_default_from_imports_source.rb, db/schema.rb
Allow Import.source to be nil via enum allow_nil: true; add migration to remove default 0 for imports.source; schema version and column default updated.
Import creation & detection flow
app/services/imports/create.rb, app/services/imports/source_detector.rb, app/services/imports/watcher.rb
Gate source detection with should_detect_source?; persist detected source early; add importer mappings and guards (raise on nil/unsupported); whitespace cleanup in source detector; watcher handles nil source with fallback MIME type.
Controller: import naming & detection removal
app/controllers/imports_controller.rb
Remove automatic source-detection path; add generate_unique_import_name(original_name) to append timestamp when a name collision occurs; create imports without auto-detect.
Photo serialization & thumbnails
app/serializers/api/photo_serializer.rb, app/services/photos/thumbnail.rb
Api::PhotoSerializer#orientation returns "landscape" by default for non-API or nil sources; Photos::Thumbnail#call raises if source is nil.
Views / UI
app/views/imports/index.html.erb
Table styling updated (table-zebra, row hover); action buttons changed to btn btn-outline ... classes.
Tests, factories, fixtures
spec/factories/imports.rb, spec/fixtures/files/google/*, spec/services/imports/create_spec.rb, spec/services/google_maps/phone_takeout_importer_spec.rb, spec/services/imports/source_detector_spec.rb, spec/services/users/import_data/imports_spec.rb
Factory no longer sets default source; fixtures replaced with phone-takeout_w_3_duplicates.json; tests adjusted to reflect nil/persisted source behavior and new fixture.
Minor formatting
app/controllers/settings/background_jobs_controller.rb
Whitespace-only formatting change in index action.
Services: imports/watcher & imports/watcher mappings
app/services/imports/watcher.rb
Safe navigation and explicit nil branch in mime_type; existing source mappings retained; unsupported non-nil sources still raise.
Documentation / version
CHANGELOG.md, .app_version
Add release block for 0.30.11 describing unique-name behavior and import-detection fix; bump .app_version to 0.30.11.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Controller as ImportsController
  participant Storage as ActiveStorage
  participant DB as DB (imports)
  participant Service as Imports::Create
  participant Detector as SourceDetector
  participant Importer as Importer (various)

  User->>Controller: upload signed_id
  Controller->>Storage: find blob by signed_id
  Storage-->>Controller: blob (filename)
  Controller->>Controller: generate_unique_import_name(filename)
  Controller->>DB: create Import (name, source may be nil)
  Controller-->>User: respond (accepted)

  Service->>DB: load Import, download blob -> temp_file
  Service->>Service: should_detect_source?
  alt detection needed (source nil or allowed)
    Service->>Detector: detect_source_from_file(temp_file)
    Detector-->>Service: detected_source
    Service->>DB: update import.source = detected_source
  else API sources (immich_api/photoprism_api)
    Service->>Service: keep existing source
  end
  Service->>Service: importer = lookup_by(source) (raise if nil/unsupported)
  Service->>Importer: import(temp_file)
  Importer-->>Service: result
  Service-->>User: complete/queued
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • 0.30.11 #1676 — Modifies the same import-related code paths (imports_controller, Import.source enum/migration, Imports::Create, watcher, tests/fixtures) and is likely directly related or overlapping.

Poem

I stamped a name with ticking time,
So twins won’t clash in upload line.
We sniff the file when sources hide,
Or leave it nil and let logic guide.
A rabbit cheers: "Landscape wins!" 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 625b8e6 and 86bfe1b.

📒 Files selected for processing (2)
  • .app_version (1 hunks)
  • CHANGELOG.md (1 hunks)
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 3

🧹 Nitpick comments (19)
app/controllers/settings/background_jobs_controller.rb (3)

14-15: flash.now is discarded on redirect; avoid duplicate notices.

You both set flash.now[:notice] and pass notice: to redirect_to. The flash.now message won’t survive the redirect, and the user will only see the redirect notice. Drop the flash.now assignment to avoid confusion.

Apply this minimal diff:

-    flash.now[:notice] = 'Job was successfully created.'
...
-    redirect_to redirect_path, notice: 'Job was successfully created.'
+    redirect_to redirect_path, notice: 'Job was successfully created.'

Optional: centralize the message via I18n (t('background_jobs.create.success')) to keep copy consistent.

Also applies to: 24-24


5-7: Deduplicate the “import jobs” special-casing via a constant; reduces drift across auth and redirect.

You special-case the same two job names in both the before_action and the redirect logic. Extracting them to a single constant keeps behavior aligned if/when the set changes.

Proposed refactor:

 class Settings::BackgroundJobsController < ApplicationController
+  IMPORT_JOBS = %w[start_immich_import start_photoprism_import].freeze
   before_action :authenticate_self_hosted!
-  before_action :authenticate_admin!, unless: lambda {
-    %w[start_immich_import start_photoprism_import].include?(params[:job_name])
-  }
+  before_action :authenticate_admin!, unless: -> { IMPORT_JOBS.include?(params[:job_name]) }
 ...
-    redirect_path =
-      case params[:job_name]
-      when 'start_immich_import', 'start_photoprism_import'
-        imports_path
-      else
-        settings_background_jobs_path
-      end
+    redirect_path = IMPORT_JOBS.include?(params[:job_name]) ? imports_path : settings_background_jobs_path

Follow-up thought: if more job-specific redirects appear, consider a small map (JOB_REDIRECTS) for O(1) lookups rather than branching.

Also applies to: 16-22


11-13: Validate job_name against a known registry to prevent enqueuing unknown/unsupported jobs.

Right now any params[:job_name] is accepted. If EnqueueBackgroundJob dispatches dynamically, this could enqueue unexpected work or fail noisily. Guard with an allowlist/registry (possibly the same source EnqueueBackgroundJob uses) and return 422/400 when invalid.

If you want, I can scan the repo to locate where valid job names are defined and wire a shared registry. For now, here’s a minimal controller-side guard you can adapt:

   def create
-    EnqueueBackgroundJob.perform_later(params[:job_name], current_user.id)
+    job_name = params[:job_name].to_s
+    unless job_name.present?
+      return redirect_back fallback_location: settings_background_jobs_path, alert: 'Missing job name.'
+    end
+    # TODO: replace ALLOWED_JOB_NAMES with a shared registry
+    if defined?(ALLOWED_JOB_NAMES) && !ALLOWED_JOB_NAMES.include?(job_name)
+      return redirect_back fallback_location: settings_background_jobs_path, alert: 'Unsupported job.'
+    end
+    EnqueueBackgroundJob.perform_later(job_name, current_user.id)

Would you like me to open a follow-up to introduce a shared job registry and specs?

CHANGELOG.md (2)

11-11: Tighten wording and clarify behavior for duplicate import names

Current text is grammatically off and a bit vague. Consider rephrasing and giving an example of the timestamp format for predictability.

Apply:

-- If user already have import with the same name, it will be appended with timestamp during the import process.
+- If a user already has an import with the same name, Dawarich will append a timestamp to make it unique during the import process (e.g., `my_import_2025-08-23T14-55-53Z`).

9-9: Call out the schema/config change: Import.source can be nil and default removed

This PR removes the default for imports.source and allows Import.source to be nil. That’s user-facing (and potentially breaking) behavior and should be documented under “Changed” to help self-hosters/operators.

Apply:

 ## Changed
+- Import source no longer defaults to a value; `Import.source` may now be `nil` (migration included). Source auto-detection moved from controller to service and runs only when appropriate.
spec/services/google_maps/phone_takeout_importer_spec.rb (2)

39-49: Avoid relying on implicit record order in expectations

Point.all[6] and Point.last rely on implicit ordering which isn’t guaranteed. Sort explicitly for deterministic tests.

Apply:

-          expect(Point.all[6].lat).to eq(27.696576)
-          expect(Point.all[6].lon).to eq(-97.376949)
-          expect(Point.all[6].timestamp).to eq(1_693_180_140)
+          ordered = Point.order(:timestamp)
+          expect(ordered[6].lat).to eq(27.696576)
+          expect(ordered[6].lon).to eq(-97.376949)
+          expect(ordered[6].timestamp).to eq(1_693_180_140)

-          expect(Point.last.lat).to eq(27.709617)
-          expect(Point.last.lon).to eq(-97.375988)
-          expect(Point.last.timestamp).to eq(1_693_180_320)
+          expect(ordered.last.lat).to eq(27.709617)
+          expect(ordered.last.lon).to eq(-97.375988)
+          expect(ordered.last.timestamp).to eq(1_693_180_320)

17-20: Ensure duplicates are dropped and remove redundant file attachment

To prevent regressions in your phone-takeout importer spec:

  • Add an explicit assertion that the importer dedupes points by the intended key (e.g. timestamp/lat/lon), not just that the total count changes.
  • Remove the redundant import.file.attach in the before block and rely on the file: passed into the factory, preventing extra ActiveStorage blobs.

Locations to update in spec/services/google_maps/phone_takeout_importer_spec.rb:

  • Around line 19: after creating/importing, assert uniqueness.
  • Around lines 10–13: drop the before do … import.file.attach(...) end block.

Suggested example diff:

@@ spec/services/google_maps/phone_takeout_importer_spec.rb
-  before do
-    import.file.attach(
-      io: File.open(file_path),
-      filename: 'phone_takeout.json',
-      content_type: 'application/json'
-    )
-  end

   let(:file_path) { Rails.root.join('spec/fixtures/files/google/phone-takeout_w_3_duplicates.json') }
   let(:file)      { Rack::Test::UploadedFile.new(file_path, 'application/json') }
   let(:import)    { create(:import, user: user, name: 'phone_takeout.json', file: file) }

   it 'drops duplicate points' do
-    expect { parser }.to change { Point.count }.by(4)
-    parser
+    expect { parser }.to change { Point.count }.by(4)
+    parser
+    # ensure all remaining points are unique by the dedupe key
+    unique_count = Point.select(:lat, :lon, :timestamp).distinct.count
+    expect(unique_count).to eq(Point.count)
   end
app/services/imports/source_detector.rb (4)

63-70: Guard against nil header reads and minor style polish

File#read can return nil for empty files; we should avoid calling methods on nil further down. Also, return an empty string when header is missing to keep downstream checks safe.

Apply:

-  def self.new_from_file_header(file_path)
+  def self.new_from_file_header(file_path)
     filename = File.basename(file_path)
-
-    # For detection, read only first 2KB to optimize performance
-    header_content = File.open(file_path, 'rb') { |f| f.read(2048) }
-
-    new(header_content, filename, file_path)
+    # For detection, read only first 2KB to optimize performance
+    header_content = File.open(file_path, 'rb') { |f| f.read(2048) }.to_s
+    new(header_content, filename, file_path)
   end

101-116: Fix potential nil dereference and address RuboCop alignment in GPX detection

If content_to_check is nil, strip.start_with? will raise. Also aligns if/else/end with RuboCop and improves readability.

Apply:

   def gpx_file?
     return false unless filename

     # Must have .gpx extension AND contain GPX XML structure
     return false unless filename.downcase.end_with?('.gpx')
-
-    # Check content for GPX structure
-    content_to_check = if file_path && File.exist?(file_path)
-                        # Read first 1KB for GPX detection
-                        File.open(file_path, 'rb') { |f| f.read(1024) }
-                      else
-                        file_content
-                      end
-
-    content_to_check.strip.start_with?('<?xml') && content_to_check.include?('<gpx')
+    # Check content for GPX structure
+    content_to_check =
+      if file_path && File.exist?(file_path)
+        # Read first 1KB for GPX detection
+        File.open(file_path, 'rb') { |f| f.read(1024) }
+      else
+        file_content
+      end
+
+    content = content_to_check.to_s
+    content.start_with?('<?xml') && content.include?('<gpx')
   end

118-133: OwnTracks detection: use binary-safe reads, guard nil, and align branches

Same rationale as GPX: avoid nil dereference, align if/else, and prefer binary read to avoid encoding issues on some platforms.

Apply:

   def owntracks_file?
     return false unless filename

     # Check for .rec extension first (fastest check)
     return true if filename.downcase.end_with?('.rec')
-
-    # Check for specific OwnTracks line format in content
-    content_to_check = if file_path && File.exist?(file_path)
-                        # For OwnTracks, read first few lines only
-                        File.open(file_path, 'r') { |f| f.read(2048) }
-                      else
-                        file_content
-                      end
-
-    content_to_check.lines.any? { |line| line.include?('"_type":"location"') }
+    # Check for specific OwnTracks line format in content
+    content_to_check =
+      if file_path && File.exist?(file_path)
+        # For OwnTracks, read first few lines only
+        File.open(file_path, 'rb') { |f| f.read(2048) }
+      else
+        file_content
+      end
+
+    content_to_check.to_s.lines.any? { |line| line.include?('"_type":"location"') }
   end

72-88: Optional: avoid full-file JSON load during detection for very large inputs

Oj.load_file will load the entire file. For large exports, consider a streaming heuristic first (e.g., read and inspect the first N KB for top-level keys like semanticSegments, timelineObjects, locations) before falling back to full parse. This keeps source detection cheap for giant files.

If you want, I can draft a streaming-first detector using Oj.saj/sc_parse to look for known keys without loading the whole JSON.

db/migrate/20250823125940_remove_default_from_imports_source.rb (1)

1-5: Add frozen string literal pragma and consider data backfill

Style: add # frozen_string_literal: true for consistency.

Semantics: this drops the default going forward but leaves existing source = 0 rows untouched. If 0 previously mapped to a concrete source (e.g., google_semantic_history) rather than “unknown”, you might want a follow-up data migration or rake task to nullify incorrect legacy defaults created under the old behavior.

Apply:

+ # frozen_string_literal: true
 class RemoveDefaultFromImportsSource < ActiveRecord::Migration[8.0]
   def change
     change_column_default :imports, :source, from: 0, to: nil
   end
 end

If you choose to backfill, a reversible migration or a one-off rake task like:

# Example (adjust the condition to your enum mapping):
# Rails.logger.warn("Nullifying legacy defaulted sources (source=0) created after 2025-08-01")
# Import.where(source: 0).update_all(source: nil)

Let me know if you want me to draft a safe, filterable data migration that avoids clobbering known-correct sources.

spec/factories/imports.rb (1)

7-7: Add source-specific traits; consider making the generated name source-agnostic

  • To keep factories explicit and avoid coupling to a specific provider in the filename, add traits for common sources and consider a neutral default name.

Example traits (outside selected lines):

factory :import do
  user
  sequence(:name) { |n| "import_#{n}.json" }

  trait(:google_semantic_history) { source { 'google_semantic_history' } }
  trait(:owntracks)               { source { 'owntracks' } }
  trait(:google_records)          { source { 'google_records' } }
  trait(:google_phone_takeout)    { source { 'google_phone_takeout' } }
  trait(:gpx)                     { source { 'gpx' } }
  trait(:geojson)                 { source { 'geojson' } }
  trait(:immich_api)              { source { 'immich_api' } }
  trait(:photoprism_api)          { source { 'photoprism_api' } }
  trait(:user_data_archive)       { source { 'user_data_archive' } }
end
spec/services/users/import_data/imports_spec.rb (1)

224-225: Expectation update to nil is correct; also update the adjacent outdated inline comment

  • Switching the expected source for “Missing Source Import” to nil matches the removal of the default and allow_nil: true.
  • Minor nit: the comment above still says “source defaults to first enum,” which is no longer true.

Apply:

-        # Verify only the valid imports were created (name is required, source defaults to first enum)
+        # Verify only the valid imports were created (name is required; source can be nil)

Optional: to reduce DB-specific brittleness, assert booleans as truthy/falsey instead of 1/0 (not required for this PR).

spec/services/imports/create_spec.rb (1)

24-28: Rename to reflect behavior; consider adding a nil→detected persistence case

  • The example “updates the import source” reads like it should change the value. Here it verifies that it remains owntracks (which is fine; the service persists a resolved/explicit source).

Rename and add a complementary case:

-      it 'updates the import source' do
+      it 'persists the resolved import source (does not overwrite explicit value)' do
         service.call

         expect(import.reload.source).to eq('owntracks')
       end

Additional spec (outside selected lines) to exercise the new detection + persistence path when source starts nil:

it 'detects and persists source when import.source is nil' do
  import = create(:import, source: nil, name: '2024-03.rec')
  file_path = Rails.root.join('spec/fixtures/files/owntracks/2024-03.rec')
  import.file.attach(io: File.open(file_path), filename: '2024-03.rec', content_type: 'application/octet-stream')

  described_class.new(user, import).call

  expect(import.reload.source).to eq('owntracks')
end
app/views/imports/index.html.erb (2)

39-39: Zebra table class: minor cleanup and consistency

Looks good to switch to table table-zebra. Minor nit: you’ve got overflow-x-auto both on the wrapper div (Line 38) and on the table. The wrapper alone is sufficient and avoids redundant styling.

Apply this diff to simplify:

-        <table class="table table-zebra overflow-x-auto">
+        <table class="table table-zebra">

60-64: Hide empty source parentheses when source is nil

Now that Import.source can be nil, the UI renders empty parentheses like (). Consider conditionally rendering the source (and its surrounding parentheses) only when present.

Suggested change:

-                  (<%= import.source %>)
+                  <%= "(#{import.source})" if import.source.present? %>
app/services/imports/create.rb (1)

19-26: Clarify detection policy: this overrides non-API sources

This branch forces re-detection for any non-API source (e.g., gpx, geojson, owntracks) even if a value is already set on import.source. If that’s intentional, all good; if not, tighten the condition to only detect when import.source is nil or explicitly unknown.

If the intent is “detect only when nil or unknown,” consider:

-    source = if import.source.nil? || should_detect_source?
+    source = if import.source.nil?
                detect_source_from_file(temp_file_path)
              else
                import.source
              end

Or rename should_detect_source? to override_non_api_source? to better reflect current semantics.

app/controllers/imports_controller.rb (1)

120-120: Style: trailing whitespace

RuboCop flagged trailing whitespace here. The refactor above removes it; otherwise, delete the extraneous spaces.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 758b31d and 6491d04.

📒 Files selected for processing (15)
  • CHANGELOG.md (1 hunks)
  • app/controllers/imports_controller.rb (1 hunks)
  • app/controllers/settings/background_jobs_controller.rb (1 hunks)
  • app/models/import.rb (1 hunks)
  • app/services/imports/create.rb (2 hunks)
  • app/services/imports/source_detector.rb (3 hunks)
  • app/views/imports/index.html.erb (3 hunks)
  • db/migrate/20250823125940_remove_default_from_imports_source.rb (1 hunks)
  • db/schema.rb (2 hunks)
  • spec/factories/imports.rb (1 hunks)
  • spec/fixtures/files/google/phone-takeout_w_3_duplicates.json (0 hunks)
  • spec/services/google_maps/phone_takeout_importer_spec.rb (1 hunks)
  • spec/services/imports/create_spec.rb (3 hunks)
  • spec/services/imports/source_detector_spec.rb (2 hunks)
  • spec/services/users/import_data/imports_spec.rb (1 hunks)
💤 Files with no reviewable changes (1)
  • spec/fixtures/files/google/phone-takeout_w_3_duplicates.json
🧰 Additional context used
🪛 RuboCop (1.76.1)
db/schema.rb

[convention] 13-13: Use underscores(_) as thousands separator and separate every 3 digits with them.

(Style/NumericLiterals)


[convention] 102-102: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)

app/services/imports/source_detector.rb

[convention] 110-110: Use 2 (not 1) spaces for indentation.

(Layout/IndentationWidth)


[convention] 111-111: Align else with if.

(Layout/ElseAlignment)


[warning] 113-113: end at 113, 22 is not aligned with if at 108, 23.

(Layout/EndAlignment)


[convention] 127-127: Use 2 (not 1) spaces for indentation.

(Layout/IndentationWidth)


[convention] 128-128: Align else with if.

(Layout/ElseAlignment)


[warning] 130-130: end at 130, 22 is not aligned with if at 125, 23.

(Layout/EndAlignment)

db/migrate/20250823125940_remove_default_from_imports_source.rb

[convention] 1-1: Missing frozen string literal comment.

(Style/FrozenStringLiteralComment)

app/controllers/imports_controller.rb

[convention] 120-120: Trailing whitespace detected.

(Layout/TrailingWhitespace)

🔇 Additional comments (14)
app/controllers/settings/background_jobs_controller.rb (1)

9-9: Whitespace-only tweak in index is fine.

No functional change; reads clean.

db/schema.rb (1)

13-13: Schema version bump looks consistent with the new migration

No action needed.

spec/factories/imports.rb (1)

7-7: Factory default removal for source is correct and aligned with enum allow_nil

Commenting out the default source matches the migration and model change to allow nils. This will surface tests that were accidentally depending on the implicit default.

spec/services/imports/create_spec.rb (2)

72-72: Fixture path and filename update are consistent

Switching to phone-takeout_w_3_duplicates.json and matching the attached filename keeps the spec coherent with the new fixture.

Minor consistency suggestion (outside selected lines): a few contexts define file = Rack::Test::UploadedFile.new(...) but still attach via File.open. Prefer attaching the file to avoid open file handle leaks and keep style uniform:

# before
import.file.attach(io: File.open(file_path), filename: '2024-03.rec', content_type: 'application/octet-stream')

# after (uses the defined `file`)
import.file.attach(file)

Also applies to: 75-76


203-203: Whitespace-only change

No action needed.

spec/services/imports/source_detector_spec.rb (2)

27-27: Updated fixture improves coverage without altering intent

Using phone-takeout_w_3_duplicates.json keeps detection focused while aligning with importer scenarios that handle duplicates. Looks good.


134-134: Whitespace-only change

No action needed.

app/views/imports/index.html.erb (3)

58-59: Row hover styling: LGTM

Adding class="hover" on the <tr> pairs well with table-zebra in DaisyUI and improves affordance without affecting behavior.


76-76: Download button: good use of Content-Disposition and filename

Using rails_blob_path(..., disposition: 'attachment', download: import.name) is solid and will prompt a user-friendly filename. No action needed.


78-78: Delete button: LGTM

Confirmation + turbo_method: :delete looks correct and consistent with the rest of the app.

app/services/imports/create.rb (3)

25-25: Persisting source before import: LGTM

Saving the resolved source prior to invoking the importer improves observability and aligns persisted state with runtime behavior.


104-108: Source detector call: LGTM

Delegation remains clean; no issues spotted.


99-103: Ignore the symbol vs. string concern – Rails enums return strings

Rails’ enum :source accessor always returns a string (e.g. "immich_api"), so %w[immich_api photoprism_api].include?(import.source) already works as intended. No change is needed here.

Likely an incorrect or invalid review comment.

app/controllers/imports_controller.rb (1)

105-107: Using a helper for unique names: good separation

Deferring name generation to a dedicated helper keeps create_import_from_signed_id tidy. Nice.

Comment on lines +114 to 124
def generate_unique_import_name(original_name)
return original_name unless current_user.imports.exists?(name: original_name)

Imports::SourceDetector.new_from_file_header(temp_file_path).detect_source
rescue StandardError => e
Rails.logger.warn "Failed to auto-detect import source for #{file_attachment.filename}: #{e.message}"
nil
ensure
# Cleanup temp file
if temp_file_path && File.exist?(temp_file_path)
File.unlink(temp_file_path)
end
# Extract filename and extension
basename = File.basename(original_name, File.extname(original_name))
extension = File.extname(original_name)

# Add current datetime
timestamp = Time.current.strftime('%Y%m%d_%H%M%S')
"#{basename}_#{timestamp}#{extension}"
end
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Strengthen uniqueness: second-level timestamp can collide; also trim trailing whitespace

If a user uploads multiple files with the same original name within the same second, both will receive the same timestamped name, causing collisions. Use microseconds and re-check in a loop (or add a short counter suffix) to guarantee uniqueness. Also, RuboCop flagged trailing whitespace (Line 120/121).

Apply this diff to harden uniqueness and remove trailing spaces:

-  def generate_unique_import_name(original_name)
-    return original_name unless current_user.imports.exists?(name: original_name)
-
-    # Extract filename and extension
-    basename = File.basename(original_name, File.extname(original_name))
-    extension = File.extname(original_name)
-    
-    # Add current datetime
-    timestamp = Time.current.strftime('%Y%m%d_%H%M%S')
-    "#{basename}_#{timestamp}#{extension}"
-  end
+  def generate_unique_import_name(original_name)
+    # Fast path if unused
+    return original_name unless current_user.imports.exists?(name: original_name)
+
+    base = File.basename(original_name, File.extname(original_name))
+    ext  = File.extname(original_name)
+
+    # Use microsecond precision and retry if needed to guarantee uniqueness
+    timestamp = Time.current.utc.strftime('%Y%m%d_%H%M%S%6N')
+    candidate = "#{base}_#{timestamp}#{ext}"
+    i = 1
+    while current_user.imports.exists?(name: candidate)
+      candidate = "#{base}_#{timestamp}_#{i}#{ext}"
+      i += 1
+    end
+    candidate
+  end

If there’s a unique DB index on imports.name, we’re covered at the database layer too. Check with:


🏁 Script executed:

#!/bin/bash
rg -nP --type=ruby -C2 'add_index\s+:imports\b.*\b:name\b.*\bunique:\s*true' db

Length of output: 81


🏁 Script executed:

#!/bin/bash
# Search for a unique DB index on imports.name in migrations
rg -nP --type=ruby -C2 'add_index\s+:imports\b.*\b:name\b.*unique:\s*true' db/migrate

# Search for inline t.index declarations in migrations
rg -nP --type=ruby -C2 't\.index\s+:name\b.*unique:\s*true' db/migrate

# Verify schema.rb for any index definitions on imports.name
if [ -f db/schema.rb ]; then
  rg -nP 'add_index\s+"imports", \["name"\], unique: true' db/schema.rb
  rg -nP 't\.index\s+:name, unique: true' db/schema.rb
fi

Length of output: 313


Ensure true name uniqueness and clean up whitespace
I didn’t find a unique database index on imports.name, so name collisions could still occur if two imports are created within the same microsecond. Please:

  • Harden the Ruby helper to use microsecond precision with a retry counter fallback for guaranteed uniqueness.
  • Remove the RuboCop-flagged trailing whitespace.
  • Add a unique index on imports.name at the database level (e.g. via a Rails migration) to enforce this constraint.

Apply these changes:

   # app/controllers/imports_controller.rb

-  def generate_unique_import_name(original_name)
-    return original_name unless current_user.imports.exists?(name: original_name)
-
-    # Extract filename and extension
-    basename = File.basename(original_name, File.extname(original_name))
-    extension = File.extname(original_name)
-    
-    # Add current datetime
-    timestamp = Time.current.strftime('%Y%m%d_%H%M%S')
-    "#{basename}_#{timestamp}#{extension}"
-  end
+  def generate_unique_import_name(original_name)
+    # Fast path if the name isn’t taken
+    return original_name unless current_user.imports.exists?(name: original_name)
+
+    base      = File.basename(original_name, File.extname(original_name))
+    ext       = File.extname(original_name)
+    timestamp = Time.current.utc.strftime('%Y%m%d_%H%M%S%6N')
+
+    candidate = "#{base}_#{timestamp}#{ext}"
+    counter   = 1
+    while current_user.imports.exists?(name: candidate)
+      candidate = "#{base}_#{timestamp}_#{counter}#{ext}"
+      counter += 1
+    end
+
+    candidate
+  end

And add a migration such as:

class AddUniqueIndexToImportsName < ActiveRecord::Migration[6.1]
  def change
    add_index :imports, :name, unique: true
  end
end

This combination guarantees both application-level and database-level uniqueness.

🧰 Tools
🪛 RuboCop (1.76.1)

[convention] 120-120: Trailing whitespace detected.

(Layout/TrailingWhitespace)

🤖 Prompt for AI Agents
In app/controllers/imports_controller.rb around lines 114 to 124, the helper
generate_unique_import_name can still collide and contains trailing whitespace;
update it to strip any surrounding whitespace from original_name, use
Time.current.strftime with microsecond precision (e.g. '%Y%m%d_%H%M%S_%6N') and
attempt a looped retry with an incrementing counter appended if a uniqueness
check still collides, returning a guaranteed-unique name; remove the trailing
whitespace in the file; and add a Rails migration to add a unique database index
on imports.name (e.g. add_index :imports, :name, unique: true) so uniqueness is
enforced at the DB level.

google_phone_takeout: 3, gpx: 4, immich_api: 5, geojson: 6, photoprism_api: 7,
user_data_archive: 8
}
}, allow_nil: true
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Enum now allows nil; verify downstream assumptions and queries

Allowing source to be nil matches the migration and tests. Please double-check:

  • Any validations, scopes, or queries that previously assumed presence (e.g., where(source: ...) without handling nil, or UI filters) still behave correctly.
  • Background jobs and services that branch on source tolerate nil (they should, given should_detect_source? gating in the service).

Run this quick scan to find potential assumptions:


🏁 Script executed:

#!/bin/bash
# Find validations or code paths that assume presence of `source`
rg -n -C2 --type ruby '\bvalidates\s+:source|\bsource\.\w+\?|where\([^)]*source:|\bgroup\(:source\)|pluck\(:source\)' -g '!**/spec/**'

# Find places that compare or branch on source without nil guards
rg -n -C2 --type ruby '\bif\s+.*source\b|\bcase\s+.*source\b'

Length of output: 2961


Handle nil source in downstream code paths

The source enum now allows nil; several services, serializers, and helpers assume a non-nil value and will raise or return undesirable nils. Please address the following critical locations:

  • app/services/imports/watcher.rb (around line 73)
    Calling source.to_sym on nil triggers a NoMethodError. Add a nil guard (e.g. case source&.to_sym) or early return, and define a fallback MIME type.

  • app/services/imports/create.rb (importer method at lines 51–54)
    case source.to_s yields "" for nil, so no branch matches and the method returns nil. Either provide a default importer or raise a clear error for unsupported sources.

  • app/services/photos/thumbnail.rb (around lines 31–33 and 49–50)
    case source and if source == 'immich' silently skip for nil, leading to nil URLs or missing headers. Ensure there’s a sensible default path or explicit handling for a nil source.

  • app/serializers/api/photo_serializer.rb (around lines 66–68)
    orientation returns nil when source is nil. Supply a default orientation (e.g. 'landscape') or guard against nil.

  • app/helpers/trips_helper.rb (around lines 19–21)
    photo_search_url returns nil for a nil source, breaking UI filters. Handle the nil case with a default search URL or an error.

These code paths must be updated to either guard against nil or explicitly handle it to avoid runtime errors and ensure consistent behavior.

🤖 Prompt for AI Agents
In app/models/import.rb around line 24, allow_nil on the source enum exposes nil
downstream; update the listed callers to guard or handle nil: in
app/services/imports/watcher.rb (≈line 73) use a nil-safe check (e.g. case
source&.to_sym or early return) and provide a fallback MIME type; in
app/services/imports/create.rb (importer method lines 51–54) ensure nil maps to
a default importer or raise a clear "unsupported source" error instead of
returning nil; in app/services/photos/thumbnail.rb (≈lines 31–33 and 49–50) add
explicit handling for nil source (default thumbnail path or headers) so
URLs/headers are not nil; in app/serializers/api/photo_serializer.rb (≈lines
66–68) return a default orientation like 'landscape' when source is nil; in
app/helpers/trips_helper.rb (≈lines 19–21) make photo_search_url return a
sensible default URL or fallback filter when source is nil. Ensure each change
either short-circuits on nil with a clear fallback or raises a descriptive error
so downstream code never operates on an unexpected nil source.

Comment on lines +102 to 104
t.integer "source"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

imports.source default removed — confirm model/validations align

The column now has no default (nullable). Ensure:

  • enum :source, ... is declared with allow_nil: true (it is in the PR).
  • No presence: true validations on source.
  • UI/forms tolerate nil.

Run:


🏁 Script executed:

#!/bin/bash
rg -nP 'enum\s+:source' -C2 app/models
rg -nP 'validates\s+:source,\s+presence:\s+true' -n app
rg -nP 'f\.select.*source' -n app/views app/helpers || true

Length of output: 774


🏁 Script executed:

#!/bin/bash
# Verify allow_nil on enum :source in import and place models
rg -nP 'enum\s+:source.*allow_nil' -n app/models || true

# Check form helpers referencing source
rg -nP 'f\.(?:select|input|collection_select).*:source' -n app/views app/helpers || true

Length of output: 147


🏁 Script executed:

#!/bin/bash
# Show the full enum declaration in Import model
sed -n '15,40p' app/models/import.rb

# Check for allow_nil usage in both models
grep -R -n "allow_nil" app/models/import.rb app/models/place.rb || true

# Show the full enum declaration in Place model
sed -n '10,25p' app/models/place.rb

Length of output: 1182


Place model’s source enum needs allow_nil: true

The Import model correctly declares enum :source, …, allow_nil: true, but the Place model’s enum does not. Without allow_nil: true, assigning or persisting a nil source will raise an ArgumentError (since the DB default was removed), leading to errors when creating or updating Place records without a source value.

Please update:

  • app/models/place.rb (around line 15):

    - enum :source, { manual: 0, photon: 1 }
    + enum :source, { manual: 0, photon: 1 }, allow_nil: true

This ensures the model aligns with the nullable column and prevents runtime exceptions when source is unset.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
t.integer "source"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
# app/models/place.rb
class Place < ApplicationRecord
# …
# Allow the source enum to accept nil, matching the nullable DB column
enum :source, { manual: 0, photon: 1 }, allow_nil: true
# …
end
🧰 Tools
🪛 RuboCop (1.76.1)

[convention] 102-102: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 103-103: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)


[convention] 104-104: Prefer single-quoted strings when you don't need string interpolation or special symbols.

(Style/StringLiterals)

🤖 Prompt for AI Agents
In app/models/place.rb around line 15, the Place model's enum :source is missing
allow_nil: true which causes ArgumentError when source is nil; update the enum
declaration to include allow_nil: true (e.g. enum source: { ... }, _prefix: ...,
allow_nil: true) so the model accepts nil values consistent with the nullable DB
column and prevents runtime exceptions when creating or updating Place without a
source.

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: 0

🧹 Nitpick comments (5)
app/helpers/trips_helper.rb (1)

24-26: Remove redundant else-nil; let Ruby’s implicit nil return keep behavior and satisfy RuboCop.

The explicit else returning nil is unnecessary and flagged by RuboCop (Style/EmptyElse). Dropping it keeps semantics identical and avoids the warning.

-    else
-      nil # return nil for nil or unknown source
     end
app/services/photos/thumbnail.rb (1)

14-14: Avoid double-raising; call the error helper directly.

unsupported_source_error already raises. Calling raise on its result is redundant and obscures the exception origin.

-    raise unsupported_source_error unless SUPPORTED_SOURCES.include?(source)
+    unsupported_source_error unless SUPPORTED_SOURCES.include?(source)
app/serializers/api/photo_serializer.rb (1)

61-63: Harden type derivation to avoid NoMethodError when type is absent.

If both photo['type'] and photo['Type'] are nil, downcase will raise. Coerce to string first.

-    (photo['type'] || photo['Type']).downcase
+    (photo['type'] || photo['Type']).to_s.downcase
app/services/imports/create.rb (2)

51-53: Remove return raise anti-pattern.

return raise is redundant; raise already exits control flow. Prefer plain raise for clarity.

-    return raise ArgumentError, 'Import source cannot be nil' if source.nil?
+    raise ArgumentError, 'Import source cannot be nil' if source.nil?

19-26: Minor: consider normalizing source to string once.

Downstream you case on source.to_s; consider normalizing when assigning to reduce repeated coercions and type ambiguity.

-    source = if import.source.nil? || should_detect_source?
-               detect_source_from_file(temp_file_path)
-             else
-               import.source
-             end
+    source = if import.source.nil? || should_detect_source?
+               detect_source_from_file(temp_file_path).to_s
+             else
+               import.source.to_s
+             end

This also aligns with the should_detect_source? fix to consistently treat sources as strings.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6491d04 and a6f4a93.

📒 Files selected for processing (5)
  • app/helpers/trips_helper.rb (1 hunks)
  • app/serializers/api/photo_serializer.rb (1 hunks)
  • app/services/imports/create.rb (3 hunks)
  • app/services/imports/watcher.rb (1 hunks)
  • app/services/photos/thumbnail.rb (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/services/photos/thumbnail.rb (1)
app/services/imports/watcher.rb (1)
  • source (54-70)
app/services/imports/create.rb (3)
app/services/imports/watcher.rb (1)
  • source (54-70)
app/controllers/settings/users_controller.rb (1)
  • import (56-84)
app/services/imports/source_detector.rb (1)
  • new_from_file_header (63-70)
🪛 RuboCop (1.76.1)
app/helpers/trips_helper.rb

[convention] 24-24: Redundant else-clause.

(Style/EmptyElse)

🔇 Additional comments (4)
app/services/photos/thumbnail.rb (1)

13-13: Good nil-guard to prevent downstream NoMethodError.

Early failure with a clear message is appropriate here, given subsequent use of public_send on source.

app/serializers/api/photo_serializer.rb (1)

71-73: Sensible default orientation for unknown/nil sources.

Defaulting to 'landscape' makes the API consistent and eliminates nil consumers must special-case.

app/services/imports/watcher.rb (1)

73-81: LGTM: nil-safe MIME resolution with a safe fallback.

Switching to source&.to_sym and adding a nil branch prevents crashes when source is intentionally unset, while keeping strict errors for truly unsupported values.

app/services/imports/create.rb (1)

101-104: No action needed: import.source is always a String
ActiveRecord’s built-in enum feature casts any symbol you assign (e.g. source: :immich_api) to its string key on read, so import.source will always return e.g. "immich_api", never a :immich_api symbol. Consequently,

!%w[immich_api photoprism_api].include?(import.source)

correctly handles all cases and adding to_s is redundant (stackoverflow.com).
No changes or additional test coverage are required.

Likely an incorrect or invalid review comment.

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: 1

🧹 Nitpick comments (1)
app/services/imports/create.rb (1)

51-53: Good nil-guard; make the error more actionable.

Include the import id in the message to speed up debugging.

-    raise ArgumentError, 'Import source cannot be nil' if source.nil?
+    raise ArgumentError, "Import source cannot be nil for import ##{import.id}" if source.nil?
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a6f4a93 and d400d3c.

📒 Files selected for processing (2)
  • app/services/imports/create.rb (3 hunks)
  • app/services/photos/thumbnail.rb (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/services/photos/thumbnail.rb
🧰 Additional context used
🧬 Code graph analysis (1)
app/services/imports/create.rb (4)
app/services/imports/watcher.rb (1)
  • source (54-70)
app/controllers/settings/users_controller.rb (1)
  • import (56-84)
app/services/users/import_data.rb (1)
  • import (43-63)
app/services/imports/source_detector.rb (1)
  • new_from_file_header (63-70)
🔇 Additional comments (2)
app/services/imports/create.rb (2)

106-110: LGTM on detector usage.

Using the header-only detector keeps I/O bounded; exceptions bubble up to the existing rescue. No issues here.


101-105: Ignore the symbol/string mismatch warning

The Rails enum :source definition returns the attribute value as a String (e.g. "immich_api"), so calling .include?(import.source) against an array of strings works correctly. There is no symbol/string mismatch, and the existing guard behaves as intended.

Optional: you may still choose to extract %w[immich_api photoprism_api] into a frozen API_SOURCES constant for clarity and reuse, but no change to .include? is required.

Likely an incorrect or invalid review comment.

Comment on lines +19 to +25
source = if import.source.nil? || should_detect_source?
detect_source_from_file(temp_file_path)
else
import.source
end

import.update!(source: source)
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Avoid overriding explicit sources and lazily download only when detection is needed.

  • Current condition re-detects for every non-API source, potentially overriding a deliberately set import.source. Confirm this is intended.
  • The file is downloaded before deciding whether detection is needed; for API sources this can be unnecessary or even fail if no file is attached.

Replace this block to defer downloading until needed and not override when detection is skipped:

-    source = if import.source.nil? || should_detect_source?
-               detect_source_from_file(temp_file_path)
-             else
-               import.source
-             end
-
-    import.update!(source: source)
+    source = import.source
+    temp_file_path = nil
+    if source.nil? || should_detect_source?
+      temp_file_path = Imports::SecureFileDownloader.new(import.file).download_to_temp_file
+      source = detect_source_from_file(temp_file_path)
+    end
+    import.update!(source: source)

Note: With this change you should remove the unconditional download earlier in the method (Line 17) so the file is not fetched for API imports.

Please verify Photos::Importer accepts a nil temp_file_path:


🏁 Script executed:

#!/bin/bash
# Locate Photos::Importer initializer to confirm file arg is optional or unused.
rg -nP --type rb -C3 'class\s+Photos::Importer\b|def\s+initialize\(' app

Length of output: 101


🏁 Script executed:

#!/bin/bash
# Locate Photos::Importer class definition
rg -n 'class Photos::Importer' -g '*.rb' app

# Inspect the initialize method signature in that file
rg -n -C3 'def initialize' -g '*.rb' app

Length of output: 46105


Defer file download and preserve explicit sources

The current implementation always downloads the import file before checking if source detection is needed and may overwrite an explicitly set import.source. To avoid unnecessary downloads (especially for API imports without attachments) and to respect deliberate source assignments, update the code in app/services/imports/create.rb as follows:

• Only download when detection is required (i.e., when import.source is nil or should_detect_source? returns true).
• Preserve an existing import.source when detection is skipped.
• Remove the earlier unconditional download (around Line 17) so API imports without files won’t trigger a fetch.
• Note: Photos::Importer#initialize(import, user_id, file_path = nil) already accepts a nil file_path, so deferring download will not break its usage.

Locations to update:

  • app/services/imports/create.rb (around Lines 17–25)

Proposed diff:

-    # existing unconditional download at Line 17 – remove this:
-    temp_file_path = Imports::SecureFileDownloader.new(import.file).download_to_temp_file
-
-    source = if import.source.nil? || should_detect_source?
-               detect_source_from_file(temp_file_path)
-             else
-               import.source
-             end
-
-    import.update!(source: source)
+    source       = import.source
+    temp_file_path = nil
+
+    if source.nil? || should_detect_source?
+      # Only download when we need to detect
+      temp_file_path = Imports::SecureFileDownloader.new(import.file).download_to_temp_file
+      source = detect_source_from_file(temp_file_path)
+    end
+
+    import.update!(source: source)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
source = if import.source.nil? || should_detect_source?
detect_source_from_file(temp_file_path)
else
import.source
end
import.update!(source: source)
source = import.source
temp_file_path = nil
if source.nil? || should_detect_source?
# Only download when we need to detect
temp_file_path = Imports::SecureFileDownloader.new(import.file).download_to_temp_file
source = detect_source_from_file(temp_file_path)
end
import.update!(source: source)
🤖 Prompt for AI Agents
In app/services/imports/create.rb around lines 17 to 25, remove the
unconditional file download and instead only download the file when detection is
required (i.e., when import.source.nil? || should_detect_source? is true); if
detection is required, download to temp_file_path, run
detect_source_from_file(temp_file_path) and set source from that detection,
otherwise preserve the existing import.source (do not overwrite it); ensure
import.update!(source: source) is only called with the selected source and that
Photos::Importer is invoked with file_path set to the temp path only when a file
was downloaded (pass nil otherwise).

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: 1

🧹 Nitpick comments (4)
app/services/photos/thumbnail.rb (4)

41-43: Harden URL joining to avoid accidental double slashes

If source_url ends with a slash, concatenation can produce // which some proxies don’t normalize consistently. Safer join:

def request_url
  base = source_url.to_s.chomp('/')
  path = source_path.to_s.sub(%r{\A/+}, '')
  "#{base}/#{path}"
end

36-37: Guard against missing Photoprism preview token

If the cache returns nil, the generated URL will contain a blank token segment and likely 404. Fail fast with a clear error or attempt a refresh/fallback.

when 'photoprism'
  preview_token = Rails.cache.read("#{Photoprism::CachePreviewToken::TOKEN_CACHE_KEY}_#{user.id}")
  raise ArgumentError, 'Missing Photoprism preview token' if preview_token.blank?
  "/api/v1/t/#{id}/#{preview_token}/tile_500"

If there’s a refresh helper (e.g., Photoprism::CachePreviewToken.fetch(user.id)), use it instead of raising.


23-29: Validate presence of expected settings methods to avoid NoMethodError

public_send("#{source}_url") and public_send("#{source}_api_key") will raise if the settings object doesn’t expose those keys for a given user/source combination. Given source is now nullable across the app, it’s worth guarding here too.

def source_url
  key = "#{source}_url"
  settings = user.safe_settings
  raise ArgumentError, "Missing setting: #{key}" unless settings.respond_to?(key)
  settings.public_send(key)
end

def source_api_key
  key = "#{source}_api_key"
  settings = user.safe_settings
  return nil unless settings.respond_to?(key) # only immich uses it now
  settings.public_send(key)
end

Add a small spec asserting a clear error when settings are misconfigured.


13-14: No symbol sources detected—normalization remains optional

I ran a project-wide search for calls like Photos::Thumbnail.new(..., :some_source) and found no instances of symbol literals being passed as the source argument. While converting incoming values with source.to_s is still a harmless future-proofing step, it isn’t strictly required based on current usage.

• Verification command used:

rg -nP 'Photos::Thumbnail\.new\([^,]+,\s*:[A-Za-z_][A-Za-z0-9_]*' -C2

– no matches found.

Optional refactor (unchanged from original suggestion):

-    raise ArgumentError, 'Photo source cannot be nil' if source.nil?
-    unsupported_source_error unless SUPPORTED_SOURCES.include?(source)
+    raise ArgumentError, 'Photo source cannot be nil' if source.nil?
+    @source = source.to_s
+    unsupported_source_error unless SUPPORTED_SOURCES.include?(source)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d400d3c and 625b8e6.

📒 Files selected for processing (2)
  • CHANGELOG.md (1 hunks)
  • app/services/photos/thumbnail.rb (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md
🧰 Additional context used
🧬 Code graph analysis (1)
app/services/photos/thumbnail.rb (1)
app/services/imports/watcher.rb (1)
  • source (54-70)
🪛 RuboCop (1.76.1)
app/services/photos/thumbnail.rb

[convention] 13-13: Add empty line after guard clause.

(Layout/EmptyLineAfterGuardClause)

🔇 Additional comments (1)
app/services/photos/thumbnail.rb (1)

13-13: RuboCop guard-clause empty-line hint looks like a false positive

There’s already an empty line after the guard section (Line 15). With consecutive guard clauses, many teams prefer no empty line between guards. I’d keep as-is and ignore the cop here unless your style guide mandates spacing between consecutive guards.

raise ArgumentError, 'Photo source cannot be nil' if source.nil?
unsupported_source_error unless SUPPORTED_SOURCES.include?(source)

HTTParty.get(request_url, headers: headers)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add timeouts and response/error handling for external HTTP call

Unbounded network calls can hang threads and degrade the app. Also, failures should surface as explicit errors. Add a sane timeout and check response.success?; log and/or raise on failure.

Here’s a targeted revision (illustrative; outside the changed lines):

def call
  raise ArgumentError, 'Photo source cannot be nil' if source.nil?
  @source = source.to_s
  unsupported_source_error unless SUPPORTED_SOURCES.include?(source)

  response = HTTParty.get(request_url, headers: headers, timeout: 5)
  return response if response.success?

  Rails.logger.warn("Thumbnail fetch failed for #{source} id=#{id} code=#{response.code} url=#{request_url}")
  raise StandardError, "Failed to fetch thumbnail from #{source} (HTTP #{response.code})"
rescue HTTParty::Error, SocketError, Net::OpenTimeout, Net::ReadTimeout => e
  Rails.logger.error("Thumbnail fetch error for #{source} id=#{id}: #{e.class} #{e.message}")
  raise
end
🤖 Prompt for AI Agents
In app/services/photos/thumbnail.rb around line 16, the HTTParty.get call is
unbounded and doesn't check the response; update the call to include a
reasonable timeout (e.g. timeout: 5) and then verify response.success? — if not,
log a warning with source, id, response.code and request_url and raise a
descriptive error; also rescue HTTParty::Error, SocketError, Net::OpenTimeout,
Net::ReadTimeout (and similar network errors), log the exception type/message as
an error and re-raise so failures surface instead of silently hanging.

@Freika Freika merged commit 4b80f77 into master Aug 23, 2025
7 of 9 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Aug 26, 2025
@coderabbitai coderabbitai bot mentioned this pull request Sep 13, 2025
@coderabbitai coderabbitai bot mentioned this pull request Sep 23, 2025
1 task
@coderabbitai coderabbitai bot mentioned this pull request Dec 26, 2025
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.

2 participants