Appearance
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 resultsJob Classes
| Class | File | Purpose |
|---|---|---|
AdvUploadImagesFromZipFile | ecommercen/job/libraries/AdvUploadImagesFromZipFile.php | Base job implementation |
UploadImagesFromZipFile | application/modules/job/libraries/UploadImagesFromZipFile.php | Client-overridable subclass |
Supporting Classes
| Class | File | Role |
|---|---|---|
Tasks_model | application/modules/auth/models/Tasks_model.php | Admin task notifications |
storage() | ecommercen/helpers/storage_helper.php | Flysystem 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):
- Validate file exists and optionally check file size (if
$maxFileSizeis provided) - Open ZIP via
ZipArchive::open() - Create temp directory:
../storage/import/temp/ - Extract files: iterate all ZIP entries, filter by allowed image extensions, extract matching files to temp directory
- Copy to storage: recursively copy all files from temp to
files/products/via thestorage()helper (supports local, S3, SFTP) - Clean up: remove temp directory and all its contents
- Delete ZIP (in
finallyblock -- always runs even on error) - Return array of extracted image filenames
3. Process Each Image
processImages($images) runs within a database transaction:
For each extracted image filename:
- Query product_media:
SELECT * FROM product_media WHERE uri = ? AND type = 'image' LIMIT 1 - If match found: a. Generate new filename:
SHA256(originalName + uniqid()) + .extensionb. Move file in storage:files/products/{original}->files/products/{sha256hash}c. Updateproduct_media.urito the new hashed filename d. IncrementproductImageUpdateCounter - 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
| Column | Type | Role |
|---|---|---|
id | int | PK |
uri | varchar | Image filename (matched against ZIP contents, updated with SHA256 hash) |
type | varchar | Media type (filtered to image via ProductMediaType::Image) |
product_code_id | int | FK to product_codes.id |
is_main | boolean | Whether this is the primary product image |
File Storage
| Path | Purpose |
|---|---|
../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.zipJob Options
| Option | Type | Required | Description |
|---|---|---|---|
customerId | int | Yes | Admin user ID for task notification |
fileName | string | Yes | ZIP 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
| Constant | Value | Defined In |
|---|---|---|
IMPORT_FILES_PATH | ../storage/import/ | application/config/constants.php |
Client Extension Points
Override the job class: Create
UploadImagesFromZipFileinapplication/modules/job/libraries/extendingAdvUploadImagesFromZipFile. OverrideprocessImage()to change the matching logic orprocessImageName()to change the naming strategy.Override image matching: Override
getProductImageDataByProductImage()to match on a different column (e.g., product code instead of exact URI match).Override storage path: Override
extractPathproperty to change the target directory for product images.Override notifications: Override the task creation in
executeCommand()to customize messages or add email notification.Override file handling: Override
handleMissingImage()to log unmatched files instead of deleting them, or to attempt fuzzy matching.
Business Rules
| Rule | Description |
|---|---|
| Exact filename match | ZIP filenames must exactly match product_media.uri values |
| Image type filtering | Only files with allowed extensions are extracted; others are silently skipped |
| SHA256 rename | Matched images are renamed with SHA256(originalName + uniqid()) for cache-busting and uniqueness |
| Transactional | Image processing runs within a DB transaction; failures roll back all product_media updates |
| ZIP always deleted | The source ZIP is deleted in a finally block regardless of success or failure |
| Orphan cleanup | Unmatched images are deleted from storage after processing |
| Storage abstraction | Files are copied via the storage() helper, supporting local disk, S3, and SFTP backends |
| Admin notification | A task notification is always created with processed/not-found counts |
| Pre-requisite | Product media records must exist before this job runs (typically created via SY-17 bulk import) |
Related Flows
- SY-17 Bulk Product Import -- creates product_media records with placeholder filenames
- SY-01 Cron Job Framework -- job execution framework
- SY-25 File Upload & Storage -- upload pattern used to accept the ZIP; file copying to the product image directory uses the same storage abstraction
- SY-28 Storage Abstraction --
storage()helper used to copy extracted images to local/S3/SFTP backends - AD-02 Product Management -- admin interface where upload is triggered
- AD-55 Task Management -- notification queue pattern used by this job; the hardcoded Greek title/description is documented there