fix-multi-image-plan-00.md 3.3 KB

Why duplicate media files (image-1.jpg, image-2.jpg, …) appear on import

The cause is in includes/class-studiou-wc-product-manage-product-import.php, not in the view.

Where the upload happens

  • Variable product rowprocess_variable_product() calls upload_image_from_url() at line 287-292.
  • Each variation rowprocess_variation() calls upload_image_from_url() again at line 411-416.

Every CSV row that has a non-empty Obrázky triggers an independent call.

What upload_image_from_url() does (line 464-511)

  1. download_url($url, 30) — downloads the remote file to a fresh temp file (PHP's tmpfile() name, not the basename of your URL).
  2. Builds $file_array['name'] = basename($url).
  3. Calls media_handle_sideload($file_array, 0) — and there is no check for whether the same source URL is already in the Media Library.

Why the -1, -2, -3 suffix gets added

media_handle_sideload()wp_handle_sideload()wp_unique_filename(). WordPress sees that uploads/YYYY/MM/image.jpg already exists from a previous row's import, so it renames the new upload to image-1.jpg, then image-2.jpg, etc. Each gets its own attachment post + physical file on disk.

So with a typical variable product CSV

One variable parent + N variations, all pointing at the same image.jpg:

  • Row 1 (variable) → uploads image.jpg (attachment #1)
  • Row 2 (variation) → re-downloads → image-1.jpg (attachment #2)
  • Row 3 (variation) → re-downloads → image-2.jpg (attachment #3)
  • …and so on.

That's the exact <thumbnail-file>-<ordinal>.jpg pattern observed.

Contributing factors

  • No URL-based dedup cache anywhere in the importer. There's no lookup like "does an attachment already exist whose guid / _source_url / _wp_attached_file matches this remote URL?"
  • No in-run dedup either — even within a single batch, two rows with the same URL each trigger a fresh download_url() + media_handle_sideload().
  • basename($url) is used as the local filename, so the collision detection only catches exact same-filename uploads in the same month folder. A URL like …/img.jpg?v=2 still produces img.jpg, so collisions are likely.
  • The batch architecture (5 rows per AJAX request) doesn't share any cache between batches either — even an in-memory array($url => $attachment_id) would die when the request ends.

Side effect worth noting

Because each variation gets its own uploaded image, $variation->set_image_id($image_id) at line 414 points each variation at a different attachment ID — even though all the variations were supposed to share the parent's image. So this isn't just file clutter; the parent and every variation are individually referencing distinct copies of what is logically the same image.

Fix shape (not applied — diagnosis only)

A cache array<url, attachment_id> keyed by source URL, checked before download_url(), that:

  1. First queries existing attachments by guid / a custom _source_url meta / matching _wp_attached_file basename, and
  2. If not found, performs the sideload and stores the resulting ID under the URL key (persisting the source URL in postmeta so future imports across requests hit the same attachment).

That would collapse N+1 duplicate uploads per shared URL down to one.