Pull to refresh

Решение проблемы оперативного ресайза изображений

Reading time 5 min
Views 18K
Бывают случаи, когда нужно на сайте вывести уменьшенную копию изображения. Возможные решения:

1. В теге изображения указываем другие размеры:
<img src='/path/to/image.jpg' width: 100px height:150px>
Очевидно, что в этом случае грузится слишком много не нужного трафика. Не у всех же скоростной интернет, да и глупо это как-то.

2. Заполнитель сайта делает ресайз самостоятельно и загружает обе картинки.
Крайне муторный путь. Но не удивлюсь, если такое где-нибудь практикуется.

3. В административной части при загрузке картинки делается ресайз и обработанные изображения помещаются в нужные папки — xs, s, l, xl и в нужном месте вставляется требуемая картинка
Не всегда размеры, бывают подходящими под задачу — иногда приходится возвращаться к п1. (но уже с меньшими потерями)

4. Прописывать путь к картинке вместе с указанием размера: path/to/100x150/image.jpg. Натравить nginx на такие пути, если картинки нет — то ресайзить и сохранять. (скриптом)

Недостаток такого подхода может быть чрезмерная нагрузка на сервер, в случае если злоумышленник решит создать скрипт в котором будет циклически подставлять различные пути, заставляя сервер ресазить. При желании можно положить сервак, либо забить все свободное место

5. Написать view helper такой как getImage(100,150,'image.jpg') который проверит наличие изображения и в случае его отсутствия, отресайзит оригинальное и выдаст нужный путь.

Недостатком такого подхода является излишний код, и в случае удаления оригинального изображения, отрезайзенные изображения останутся храниться на жестком диске

6. Это способ, который, как мне кажется, решает проблемы, которые я нашел.

/tmp/
/origin/    
/crop/100x150/
/crop/200x300/


origin — папка для хранения оригинальных загруженных изображений,
crop — это папка в которой хранятся папки для ресайзнутых изображений.
tmp — папка для временных данных

Демон заглядывает в папку origin, сравнивает дату ее последнего изменения с запомненной датой, и если дата изменения более поздняя чем ту, которую он помнит — то демон запускает режим синхронизации.

Для этого он проходится по папке crop — вытаскивает от туда папки, названия которых совпадают с 000х000 и сравнивает файлы из этой папки с файлами из оригинальной (только названия). Если в кропнутой есть лишние — удаляет их, если недостает — запускает imagic и конвертирует. Размер берет из имени папки.
В tmp/temp_file сохраняет последнюю отметку времени изменения.

Приведу пример реализации этого демона. Написал я его на python. Но сразу предупреждаю, что в python я новичок, поэтому возможны кривые решения. На мой взгляд лучший способ изучить язык — написать на нем хороший проект. Этот демон — часть проекта. Если вы, увидите недоработки — прошу об этом сообщить. Для демонизации используется пакет python-daemon, а на сервере должен быть установлен imagic.

# -*- coding: utf-8 -*-
import logging
import time
import os
import re
from daemon import runner
from sets import Set

_PATH = os.path.abspath(os.path.dirname(__file__))
LOG_PATH = os.path.abspath(os.path.join(_PATH, '../', 'files', 'logs'))
PID_PATH = os.path.abspath(os.path.join(_PATH, '../', 'files', 'tmp'))


class Azazel():
    '''
    демон азазель, синхронизирует и ресайзит изображения.
    проверяет изменение в папке origin (новые и удаленные изображения)
    и раскидывает кропнутые изображения по нужным папкам

    нужные папки - это папки формата 000x000 находящиеся в папке crop
                и папки в из дополнительного списка add_sync_folders
    настройка
        origin_folder - оригинальная папка
        sync_folder куда нужно синхронизировать
        add_sync_folders - дополнительные папки
        pidfile_path - путь к pid файлу
        file_save_last_sync_data файл в котором хранится отметка о последней
                                 синхронизации демоном
        LOG_PATH - путь к лог файлу
        PID_PATH - путь к pid файлу
    '''

    #папка куда нужно синхронизировать
    sync_folder = os.path.abspath(
        os.path.join(_PATH, '../', 'files', 'media', 'crop'))
    # файл в котором храним время
    # последней синхронизации
    file_save_last_sync_data = PID_PATH + "/last_update"
    # папка откуда берем изображения,
    # которые нужно синхронизировать
    origin_folder = os.path.abspath(
        os.path.join(_PATH, '../', 'files', 'media', 'origin'))
    #дополнительные папки, куда нужно
    #синхронизировать изображения
    add_sync_folders = [
        os.path.abspath(os.path.join(_PATH, '../', 'files', 'tmp', '100x100')),
    ]
    pidfile_path = PID_PATH + '/azazel.pid'
    stdin_path = '/dev/null'
    stdout_path = '/dev/tty'
    stderr_path = '/dev/tty'
    pidfile_timeout = 5

    def __init__(self):
        self.last_time_update = self.get_last_sync_date()

    def get_last_update_origin_folder(self):
        '''
        считываем время последнего изменения оригинальной папки
        '''
        last_update = str(os.path.getmtime(self.origin_folder))
        return last_update

    def set_last_update(self, time):
        '''
        записываем в файл время последней синхронизации
        '''
        # сохраним отметку синхронизации
        self.last_time_update = time
        # запишем в файл
        f = open(self.file_save_last_sync_data, 'w')
        f.write(str(time))
        f.close()

    def get_last_sync_date(self):
        '''
        берем время последней синхронизации из файла
        '''
        f = open(self.file_save_last_sync_data, 'r')
        last_sync = f.read()
        f.close()
        return last_sync

    def get_folders_to_sync(self):
        '''
        получаем список папок, куда нужно синхронизавть.
        список берем из dir (где храняться кропнутые изображения)
        добавляем дополнительные папки
        (мало-ли, если придется синхронизировать куда-нибудь кроме этого)
        из списка берем только те которые удовлетвояют виду 000x000
        '''
        folders = map(
            lambda folder_name: self.sync_folder + '/' + folder_name,
            os.listdir(self.sync_folder)) + self.add_sync_folders
        logger.info("folders list:" + str(folders))
        return filter(
            lambda folder: re.match('^\d{,4}x\d{,4}$', folder.split('/')[-1]),
            folders)

    def folder_sync(self, folder):
        '''
        синхронизатор папки. Для папки, мы проходимся и смотрим
        какие файлы появились в оригинальной и их нужно синхронизировать,
        какие нужно удалить - если они есть в синхронизируемой, но отсутсвуют
        в оригинальной
        '''
        logger.info("обработка для папки :" + folder)
        # файлы в папке
        folder_image_list = os.listdir(folder)
        # файлы которые есть в origin_image_list но нет в folder_image_list
        new_files = Set(self.origin_image_list) - Set(folder_image_list)
        logger.info("новые файлы :" + str(new_files))
        # добавляем
        for file_to_sync in new_files:
            #получаем размеры нужного изображения из имени папки
            size = folder.split('/')[-1]
            command = 'convert "%s" -resize %s "%s"' % (
                    self.origin_folder + '/' + file_to_sync,
                    size,
                    folder + '/' + file_to_sync,
                  )
            logger.info(command)
            result = os.system(command)
            logger.info(result)
        # файлы которые есть в folder_image_list но нет в origin_image_list
        delte_files = Set(folder_image_list) - Set(self.origin_image_list)
        logger.info("устаревшие файлы:" + str(delte_files))
        # удаляем
        for file_to_sync in delte_files:
            logger.info('delete ' + folder + '/' + file_to_sync)
            os.remove(folder + '/' + file_to_sync)

    def sunc_folders(self):
        '''
        проходимся по всем папкам и для кадой запускаем синхронизатор
        '''
        #запоминаем список файлов  в оригинальной директории
        self.origin_image_list = os.listdir(self.origin_folder)
        #проходим по каждой папке и запускаем синхронизатор
        for folder in self.get_folders_to_sync():
            self.folder_sync(folder)

    def run(self):
        logger.info("start daemon azazel")
        self.sunc_folders()
        while True:
            last_update = self.get_last_update_origin_folder()
            if self.last_time_update < last_update:
                logger.info("обнаружено изменение")
                self.sunc_folders()
                self.set_last_update(last_update)
        time.sleep(10)

daemon = Azazel()
logger = logging.getLogger("DaemonLog")
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
handler = logging.FileHandler(LOG_PATH + "/azazel.log")
handler.setFormatter(formatter)
logger.addHandler(handler)

daemon_runner = runner.DaemonRunner(daemon)

daemon_runner.daemon_context.files_preserve = [handler.stream]
daemon_runner.do_action()


— Демон работает прекрасно, проблем не наблюдал, кроме идеологических — возник вопрос, ведь если я не так часто гружу фотографии, зачем нужен демон, и я его переписал. Я учел комментарии и обсуждения и написал как «задачу на celery» которая запускается, при загрузке фотографии. Как-бы общая демонизация осталась, но в ином плане.
Tags:
Hubs:
+2
Comments 48
Comments Comments 48

Articles