Skip to content

Image Upload from ZIP

Flow ID: SY-18 | Module(s): job, eshop | Complexity: Medium Last Updated: 2026-04-06

Business Overview

The ZIP image upload job allows merchants to bulk-update product images by uploading a ZIP archive containing image files. The system extracts the archive, matches each image filename against existing product_media records, renames matched files with a SHA256 hash for cache-busting, and copies them to the product image storage directory. This is the companion to the bulk product import flow (SY-17), enabling merchants to associate images with products that were created with image filenames in the spreadsheet.

What the user experiences:

  • Upload a ZIP file via the admin interface
  • File is stored in ../storage/import/
  • Job runs asynchronously in the background
  • On completion, an admin task notification reports how many images were processed and how many were not found

Matching logic: Image filenames in the ZIP must exactly match the uri column in the product_media table. This means the spreadsheet import (SY-17) must first create product_media records with the expected filenames, then this job replaces those placeholder names with the actual uploaded files.


Architecture

Admin Upload -> storage/import/{fileName}.zip
  |
  +--> AdvUploadImagesFromZipFile (JobCommand)
         |
         +--> loadProductImagesZipFile($path)
         |      ZipArchive::open()
         |      extractZipFile() -- filter by allowed image types
         |      copyFilesFromFolders() -- copy to files/products/ via storage()
         |      removeDirectory() -- clean up temp dir
         |      unlink() -- delete ZIP
         |
         +--> processImages($images)
         |      For each image filename:
         |        getProductImageDataByProductImage() -- query product_media by uri
         |        processImageName() -- SHA256 rename + storage move
         |        updateProductImage() -- update product_media.uri
         |        OR handleMissingImage() -- delete orphan + increment counter
         |
         +--> tasks_model::addTask()  -- notify admin of results

Job Classes

ClassFilePurpose
AdvUploadImagesFromZipFileecommercen/job/libraries/AdvUploadImagesFromZipFile.phpBase job implementation
UploadImagesFromZipFileapplication/modules/job/libraries/UploadImagesFromZipFile.phpClient-overridable subclass

Supporting Classes

ClassFileRole
Tasks_modelapplication/modules/auth/models/Tasks_model.phpAdmin task notifications
storage()ecommercen/helpers/storage_helper.phpFlysystem storage abstraction
ProductMediaType(enum)Image type constant for product_media.type

Code Flow

1. Job Initialization

constructor:
  load auth/tasks_model
  read imagesAllowedTypes from config
  set extractPath = 'files/products/'

2. Extract ZIP Archive

loadProductImagesZipFile($filePath):

  1. Validate file exists and optionally check file size (if $maxFileSize is provided)
  2. Open ZIP via ZipArchive::open()
  3. Create temp directory: ../storage/import/temp/
  4. Extract files: iterate all ZIP entries, filter by allowed image extensions, extract matching files to temp directory
  5. Copy to storage: recursively copy all files from temp to files/products/ via the storage() helper (supports local, S3, SFTP)
  6. Clean up: remove temp directory and all its contents
  7. Delete ZIP (in finally block -- always runs even on error)
  8. Return array of extracted image filenames

3. Process Each Image

processImages($images) runs within a database transaction:

For each extracted image filename:

  1. Query product_media: SELECT * FROM product_media WHERE uri = ? AND type = 'image' LIMIT 1
  2. If match found: a. Generate new filename: SHA256(originalName + uniqid()) + .extension b. Move file in storage: files/products/{original} -> files/products/{sha256hash} c. Update product_media.uri to the new hashed filename d. Increment productImageUpdateCounter
  3. If no match: a. Delete the extracted image from storage b. Increment productImageNotFoundCounter

4. Notify Admin

Creates a task notification with:

  • Title: "Image import completed" (hardcoded Greek)
  • Description: "{X} images imported and {Y} images not imported because matching products were not found"
  • Assigned to the admin user who initiated the upload

Data Model

Primary Table: product_media

ColumnTypeRole
idintPK
urivarcharImage filename (matched against ZIP contents, updated with SHA256 hash)
typevarcharMedia type (filtered to image via ProductMediaType::Image)
product_code_idintFK to product_codes.id
is_mainbooleanWhether this is the primary product image

File Storage

PathPurpose
../storage/import/Upload directory for ZIP files (IMPORT_FILES_PATH)
../storage/import/temp/Temporary extraction directory (created and destroyed per job run)
files/products/Final product image storage (accessed via storage() helper)

Configuration

Job Scheduling

This job is NOT cron-scheduled. It is dispatched on-demand from the admin panel:

php
// Triggered via admin interface, not via jobs.php
// CLI: php cli.php job/run/UploadImagesFromZipFile --customerId=1 --fileName=images_20240101.zip

Job Options

OptionTypeRequiredDescription
customerIdintYesAdmin user ID for task notification
fileNamestringYesZIP file name in ../storage/import/

Image Type Filtering

Allowed image types are read from the imagesAllowedTypes config item (pipe-separated extensions, e.g., jpg|jpeg|png|gif|webp). Files in the ZIP with non-matching extensions are silently skipped during extraction.

Constants

ConstantValueDefined In
IMPORT_FILES_PATH../storage/import/application/config/constants.php

Client Extension Points

  1. Override the job class: Create UploadImagesFromZipFile in application/modules/job/libraries/ extending AdvUploadImagesFromZipFile. Override processImage() to change the matching logic or processImageName() to change the naming strategy.

  2. Override image matching: Override getProductImageDataByProductImage() to match on a different column (e.g., product code instead of exact URI match).

  3. Override storage path: Override extractPath property to change the target directory for product images.

  4. Override notifications: Override the task creation in executeCommand() to customize messages or add email notification.

  5. Override file handling: Override handleMissingImage() to log unmatched files instead of deleting them, or to attempt fuzzy matching.


Business Rules

RuleDescription
Exact filename matchZIP filenames must exactly match product_media.uri values
Image type filteringOnly files with allowed extensions are extracted; others are silently skipped
SHA256 renameMatched images are renamed with SHA256(originalName + uniqid()) for cache-busting and uniqueness
TransactionalImage processing runs within a DB transaction; failures roll back all product_media updates
ZIP always deletedThe source ZIP is deleted in a finally block regardless of success or failure
Orphan cleanupUnmatched images are deleted from storage after processing
Storage abstractionFiles are copied via the storage() helper, supporting local disk, S3, and SFTP backends
Admin notificationA task notification is always created with processed/not-found counts
Pre-requisiteProduct media records must exist before this job runs (typically created via SY-17 bulk import)