Реалізація автоматичного скачування та завантаження зображень товарів

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація автоматичного скачування та завантаження зображень товарів
Середня
~3-5 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація автоматичного завантаження та завивання зображень товарів

При масовому імпорті товарів зображення — найбільш обсяжна та трудомістка частина. Постачальник надсилає посилання або шляхи в прайсі, а магазин повинен завантажити, оптимізувати та зберегти файли у власному сховищі. Робити це вручну при каталозі від 1 000 позицій нереально.

Звідки беруться посилання на зображення

  • У CSV/Excel — колонка з URL або відносним шляхом: https://supplier.ua/images/ABC-123_1.jpg
  • У XML/YML — теги <picture> або <image>
  • У API-відповіді — масив images: [{url, sort, is_main}]
  • На FTP постачальника — файли в директорії, назва файлу = артикул

Архітектура конвеєру завантаження

Import Job
  └─> parse product data
  └─> enqueue ImageDownloadJob(sku, urls[])
        └─> download each URL (HTTP)
        └─> validate (mime, size)
        └─> optimize (resize, convert to WebP)
        └─> upload to storage (S3 / local)
        └─> save to product_images table

Завантаження зображень винесено в окремий Job, щоб не блокувати основний імпорт.

Завантаження з повторними спробами

class ImageDownloadJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue;

    public int  $tries   = 3;
    public int  $backoff = 30;
    public int  $timeout = 60;

    public function __construct(
        public readonly int    $productId,
        public readonly array  $urls,
    ) {}

    public function handle(ImageProcessor $processor): void
    {
        foreach ($this->urls as $index => $url) {
            try {
                $tmpPath = $processor->download($url);
                $stored  = $processor->processAndStore($tmpPath, $this->productId, $index);

                ProductImage::updateOrCreate(
                    ['product_id' => $this->productId, 'sort' => $index],
                    ['path' => $stored, 'is_main' => $index === 0, 'source_url' => $url]
                );
            } catch (\Exception $e) {
                Log::warning("Image download failed: {$url} — {$e->getMessage()}");
            }
        }
    }
}

Валідація завантаженого файлу

class ImageProcessor
{
    private const ALLOWED_MIME = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
    private const MAX_SIZE     = 20 * 1024 * 1024; // 20 МБ

    public function download(string $url): string
    {
        $response = $this->client->get($url, ['timeout' => 30, 'stream' => true]);

        $tmpPath = tempnam(sys_get_temp_dir(), 'img_');
        $body    = $response->getBody();
        $size    = 0;
        $fp      = fopen($tmpPath, 'wb');

        while (!$body->eof()) {
            $chunk  = $body->read(8192);
            $size  += strlen($chunk);
            if ($size > self::MAX_SIZE) {
                fclose($fp);
                unlink($tmpPath);
                throw new \RuntimeException("Image too large: {$url}");
            }
            fwrite($fp, $chunk);
        }
        fclose($fp);

        $mime = mime_content_type($tmpPath);
        if (!in_array($mime, self::ALLOWED_MIME)) {
            unlink($tmpPath);
            throw new \RuntimeException("Invalid MIME type: {$mime} for {$url}");
        }

        return $tmpPath;
    }
}

Оптимізація та конвертація

Використовуємо intervention/image (v3) для ресайзу та Spatie image-optimizer для стиснення:

public function processAndStore(string $tmpPath, int $productId, int $sort): string
{
    $manager = new \Intervention\Image\ImageManager(
        new \Intervention\Image\Drivers\Gd\Driver()
    );

    $image = $manager->read($tmpPath);

    // Генеруємо кілька розмірів
    $variants = [
        'full'      => [1200, 1200],
        'catalog'   => [400,  400],
        'thumbnail' => [100,  100],
    ];

    $paths = [];
    foreach ($variants as $name => [$w, $h]) {
        $resized  = clone $image;
        $resized->coverDown($w, $h);

        $filename = "products/{$productId}/{$sort}_{$name}.webp";
        $encoded  = $resized->toWebp(quality: 85);

        Storage::disk('public')->put($filename, $encoded);
        $paths[$name] = $filename;
    }

    unlink($tmpPath);
    return json_encode($paths);
}

coverDown обрізає зображення по центру, зберігаючи пропорції — стандарт для каталожних фото.

Дедублікація: не завантажувати повторно

Якщо постачальник прислав той же URL — не тратити трафік і час:

$existing = ProductImage::where([
    'product_id' => $productId,
    'source_url' => $url,
])->first();

if ($existing && Storage::exists($existing->path)) {
    continue; // вже є, пропустити
}

Для більш надійної дедублікації — зберігати хеш контенту (SHA-256 перших 4 КБ): один і той же файл з різних URL не завантажиться двічі.

Завантаження з FTP постачальника

class FtpImageSource
{
    public function syncForProduct(string $sku): array
    {
        $ftp   = ftp_connect($this->host);
        ftp_login($ftp, $this->user, $this->pass);
        $files = ftp_nlist($ftp, $this->baseDir);

        $matched = array_filter($files, fn($f) => str_contains($f, $sku));
        $urls    = [];

        foreach ($matched as $remotePath) {
            $tmp = tempnam(sys_get_temp_dir(), 'ftpimg_');
            ftp_get($ftp, $tmp, $remotePath, FTP_BINARY);
            $urls[] = $tmp; // шлях до локального файлу
        }

        ftp_close($ftp);
        return $urls;
    }
}

Обробка 404 та зламаних посилань

Постачальники періодично видаляють або переносять зображення. Стратегія:

  1. При 404 — логувати, пропустити, не видаляти вже збережене зображення
  2. Після 3 неудачних спроб — помічати source_url як dead = true
  3. Раз на тиждень — звіт по зламаних посиланнях з пропозицією завантажити зображення вручну

Паралелізм та черги

Параметр Значення
Чергу images (окремо від default)
Воркерів на чергу 4–8
Таймаут завдання 60 сек
Розмір чанку URL у Job 10 штук
Повторних спроб 3

При 10 000 зображень з 4 воркерами час повного завантаження — близько 20–40 хвилин (залежить від швидкості хостингу постачальника).

Тривалість реалізації

  • Завантаження по HTTP, валідація, конвертація в WebP, збереження — 2 дні
  • Кілька розмірів, дедублікація, моніторинг мертвих посилань — +1–2 дні
  • FTP-джерело + паралельна чергу + панель прогресу — +1 день