Командную строку фотографа-линуксоида — на пенсию!

Я увлекаюсь фотографией ещё со Смены-8М. Тогда были длительные ожидания пятницы или субботы (печать обычно шла в ночь на выходные), а перед этим оочень долгие ожидания фотоплёнки, химикатов, фотобумаги (ибо дефицит). Теперь же я вырос, стал большим и ленивым. Моя мыльница почти всегда со мной: либо в рюкзаке, либо где-то в кармане. Фотографирую всё, что вызвало интерес. При этом за день может быть одна фотография (шёл с работы), а может быть сразу много (целенаправленно вышел на прогулку). И если с целенаправленным случаем я скорее всего по приходу домой фотографии солью и разберу, то в единичных случаях я забуду и потом окажется что надо рассортировать фотографии сделанные в десяток разных дней. В последнее время выбираться целенаправленно получалось всё меньше, поэтому росло количество одиночных фотографий. И вот в один из таких дней, вдохновившись прошлогодней статьёй, я решил упростить себе хобби. Поскольку на компьютере стоит Linux (openSUSE 12.1), то нерешаемых проблем быть не должно — подумал я. А захотелось мне чтоб оно само копировало и чтоб никуда тыкать не нужно было. Ну, а поскольку я ненастоящий линуксоид (первый и последний скрипт был на третьем курсе 0x0C лет назад), сразу скажу — не всё получилось.

Храню я фотографии в одном месте, отдельный каталог с датой под отдельное событие, даже если там одна фотография: «2009.05.20 Ночной Питер», «2011.08.20 Водопад Лавна», «2012.07.24 Дуся спит». Уже приходят мысли о том, что нужен ещё как минимум один уровень — год, но пока ещё терплю. Мои или не мои фотографии (в случае коллективных походов) — мне не важно, всё лежать будет в одном каталоге по событию. Свои фотографии если надо будет я ручками найду.
Для автоматической сортировки необходимо отследить момент подключения нужной карты памяти и запустить скрипт сортировки. В Linux за железо отвечает демон udev. Поэтому для начала научимся обращаться с ним.

udev
udev следит за оборудованием и для каждого устройства создаёт свою ноду в каталоге /dev. Это удобно, но есть маленький нюанс: устройства одного класса будут именоваться последовательно в зависимости от порядка их подключения. Поэтому первоначальный вариант — нажать кнопку на скрипте, который бы всё скопировал куда надо — не подходит (мало ли какие ещё окажутся диски подключены, а чтоб отследить конкретный диск придётся усложнить скрипт, да и не хотелось после подключения карты ещё и жать куда-то). Но его можно настроить так, чтоб конкретный диск монтировался в нужную точку — это уже хорошо, но не достаточно. Его самый большой плюс: запуск произвольного скрипта по каким-то событиям подходящим под фильтр. Для начала посмотрим к каким атрибутам и событиям мы можем привязаться для однозначного определения факта вставки карты памяти в карт-ридер. Конечной целью данного раздела является файл udev-правил соответствия карт-ридеру.
Для просмотра характеристик устройств можно использовать программу udevadm. Но ей требуется имя устройства. Поэтому сначала необходимо определить имя диска в карт-ридере. Воспользуемся самым простым способом. Вначале смотрим какие диски у нас в системе уже есть:
>ls -1 /dev/sd*
/dev/sda
/dev/sda1
/dev/sdb
/dev/sdb1
/dev/sdc
/dev/sdc1
/dev/sdd
/dev/sdd1
/dev/sde
/dev/sde1
/dev/sdf
/dev/sdg
/dev/sdh
/dev/sdi

Вставляем карточку в ридер и повторяем команду:
>ls -1 /dev/sd*
/dev/sda
/dev/sda1
/dev/sdb
/dev/sdb1
/dev/sdc
/dev/sdc1
/dev/sdd
/dev/sdd1
/dev/sde
/dev/sde1
/dev/sdf
/dev/sdg
/dev/sdh
/dev/sdh1
/dev/sdi

Видно что вставленная карточка прячется под именем /dev/sdh1. По секрету скажу что последние 4 диска (sdf, sdg, sdh, sdi) — это всё карт-ридер, и определить его диски можно было выполнив ту же команду до и после подключения ридера к компу, даже без карты памяти (до меня это немного позже дошло, когда уже определил имя тома описанным способом).

Теперь смотрим характеристики этого карт-ридера и карточки. Нужно найти что-нибудь за что можно будет уцепиться для более-менее однозначного определения факта появления карты в ридере. Тут нам и понадобится любое из его имён дисков. Данная команда выведет список атрибутов всех устройств начиная с указанного по имени и до корня, в udev-подобном формате:
Вывод udevadm
>udevadm info -a -n /dev/sdh1

Udevadm info starts with the device specified by the devpath and then
walks up the chain of parent devices. It prints for every device
found, all possible attributes in the udev rules key format.
A rule to match, can be composed by the attributes of the device
and the attributes from one single parent device.

  looking at device '/devices/pci0000:00/0000:00:02.1/usb1/1-1/1-1:1.0/host10/target10:0:0/10:0:0:2/block/sdh/sdh1':
    KERNEL=="sdh1"
    SUBSYSTEM=="block"
    DRIVER==""
    ATTR{partition}=="1"
    ATTR{start}=="2048"
    ATTR{size}=="153600"
    ATTR{ro}=="0"
    ATTR{alignment_offset}=="0"
    ATTR{discard_alignment}=="0"
    ATTR{stat}=="     146        4      738      319        0        0        0        0        0      319      319"
    ATTR{inflight}=="       0        0"

  looking at parent device '/devices/pci0000:00/0000:00:02.1/usb1/1-1/1-1:1.0/host10/target10:0:0/10:0:0:2/block/sdh':
    KERNELS=="sdh"
    SUBSYSTEMS=="block"
    DRIVERS==""
    ATTRS{range}=="16"
    ATTRS{ext_range}=="256"
    ATTRS{removable}=="1"
    ATTRS{ro}=="0"
    ATTRS{size}=="7745536"
    ATTRS{alignment_offset}=="0"
    ATTRS{discard_alignment}=="0"
    ATTRS{capability}=="51"
    ATTRS{stat}=="    1352     1239    73856     8882        4       18       22      735        0     3608     9615"
    ATTRS{inflight}=="       0        0"
    ATTRS{events}=="media_change"
    ATTRS{events_async}==""
    ATTRS{events_poll_msecs}=="-1"

  looking at parent device '/devices/pci0000:00/0000:00:02.1/usb1/1-1/1-1:1.0/host14/target14:0:0/14:0:0:2':
    KERNELS=="14:0:0:2"
    SUBSYSTEMS=="scsi"
    DRIVERS=="sd"
    ATTRS{device_blocked}=="0"
    ATTRS{type}=="0"
    ATTRS{scsi_level}=="0"
    ATTRS{vendor}=="Generic-"
    ATTRS{model}=="SD/MMC          "
    ATTRS{rev}=="1.00"
    ATTRS{state}=="running"
    ATTRS{timeout}=="30"
    ATTRS{iocounterbits}=="32"
    ATTRS{iorequest_cnt}=="0x220"
    ATTRS{iodone_cnt}=="0x220"
    ATTRS{ioerr_cnt}=="0x21f"
    ATTRS{evt_media_change}=="0"
    ATTRS{queue_depth}=="1"
    ATTRS{queue_type}=="none"
    ATTRS{max_sectors}=="240"

  looking at parent device '/devices/pci0000:00/0000:00:02.1/usb1/1-1/1-1:1.0/host14/target14:0:0':
    KERNELS=="target14:0:0"
    SUBSYSTEMS=="scsi"
    DRIVERS==""

  looking at parent device '/devices/pci0000:00/0000:00:02.1/usb1/1-1/1-1:1.0/host14':
    KERNELS=="host14"
    SUBSYSTEMS=="scsi"
    DRIVERS==""

  looking at parent device '/devices/pci0000:00/0000:00:02.1/usb1/1-1/1-1:1.0':
    KERNELS=="1-1:1.0"
    SUBSYSTEMS=="usb"
    DRIVERS=="usb-storage"
    ATTRS{bInterfaceNumber}=="00"
    ATTRS{bAlternateSetting}==" 0"
    ATTRS{bNumEndpoints}=="02"
    ATTRS{bInterfaceClass}=="08"
    ATTRS{bInterfaceSubClass}=="06"
    ATTRS{bInterfaceProtocol}=="50"
    ATTRS{supports_autosuspend}=="1"
    ATTRS{interface}=="Bulk-In, Bulk-Out, Interface"

  looking at parent device '/devices/pci0000:00/0000:00:02.1/usb1/1-1':
    KERNELS=="1-1"
    SUBSYSTEMS=="usb"
    DRIVERS=="usb"
    ATTRS{configuration}=="CARD READER"
    ATTRS{bNumInterfaces}==" 1"
    ATTRS{bConfigurationValue}=="1"
    ATTRS{bmAttributes}=="80"
    ATTRS{bMaxPower}=="500mA"
    ATTRS{urbnum}=="10885"
    ATTRS{idVendor}=="0bda"
    ATTRS{idProduct}=="0151"
    ATTRS{bcdDevice}=="5195"
    ATTRS{bDeviceClass}=="00"
    ATTRS{bDeviceSubClass}=="00"
    ATTRS{bDeviceProtocol}=="00"
    ATTRS{bNumConfigurations}=="1"
    ATTRS{bMaxPacketSize0}=="64"
    ATTRS{speed}=="480"
    ATTRS{busnum}=="1"
    ATTRS{devnum}=="15"
    ATTRS{devpath}=="1"
    ATTRS{version}==" 2.00"
    ATTRS{maxchild}=="0"
    ATTRS{quirks}=="0x0"
    ATTRS{avoid_reset_quirk}=="0"
    ATTRS{authorized}=="1"
    ATTRS{manufacturer}=="Generic"
    ATTRS{product}=="USB2.0-CRW"
    ATTRS{serial}=="20060413092100000"

  looking at parent device '/devices/pci0000:00/0000:00:02.1/usb1':
    KERNELS=="usb1"
    SUBSYSTEMS=="usb"
    DRIVERS=="usb"
    ATTRS{configuration}==""
    ATTRS{bNumInterfaces}==" 1"
    ATTRS{bConfigurationValue}=="1"
    ATTRS{bmAttributes}=="e0"
    ATTRS{bMaxPower}=="  0mA"
    ATTRS{urbnum}=="222"
    ATTRS{idVendor}=="1d6b"
    ATTRS{idProduct}=="0002"
    ATTRS{bcdDevice}=="0301"
    ATTRS{bDeviceClass}=="09"
    ATTRS{bDeviceSubClass}=="00"
    ATTRS{bDeviceProtocol}=="00"
    ATTRS{bNumConfigurations}=="1"
    ATTRS{bMaxPacketSize0}=="64"
    ATTRS{speed}=="480"
    ATTRS{busnum}=="1"
    ATTRS{devnum}=="1"
    ATTRS{devpath}=="0"
    ATTRS{version}==" 2.00"
    ATTRS{maxchild}=="6"
    ATTRS{quirks}=="0x0"
    ATTRS{avoid_reset_quirk}=="0"
    ATTRS{authorized}=="1"
    ATTRS{manufacturer}=="Linux 3.1.10-1.16-desktop ehci_hcd"
    ATTRS{product}=="EHCI Host Controller"
    ATTRS{serial}=="0000:00:02.1"
    ATTRS{authorized_default}=="1"

  looking at parent device '/devices/pci0000:00/0000:00:02.1':
    KERNELS=="0000:00:02.1"
    SUBSYSTEMS=="pci"
    DRIVERS=="ehci_hcd"
    ATTRS{vendor}=="0x10de"
    ATTRS{device}=="0x077c"
    ATTRS{subsystem_vendor}=="0x1043"
    ATTRS{subsystem_device}=="0x82e7"
    ATTRS{class}=="0x0c0320"
    ATTRS{irq}=="22"
    ATTRS{local_cpus}=="00000000,00000000,00000000,0000000f"
    ATTRS{local_cpulist}=="0-3"
    ATTRS{numa_node}=="0"
    ATTRS{dma_mask_bits}=="32"
    ATTRS{consistent_dma_mask_bits}=="32"
    ATTRS{enable}=="1"
    ATTRS{broken_parity_status}=="0"
    ATTRS{msi_bus}==""
    ATTRS{companion}==""
    ATTRS{uframe_periodic_max}=="100"

  looking at parent device '/devices/pci0000:00':
    KERNELS=="pci0000:00"
    SUBSYSTEMS==""
    DRIVERS==""

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

Правила будем писать для подключения первого раздела карты памяти (чтоб сразу отсечь карты без разделов, хотя я и не пытался такие сделать, и фотоаппарат пишет исключительно в первый раздел). У самого тома sdh1 уникальных свойств не много. Тут разве что имя устройства KERNEL==«sdh1» и подсистема этого устройства SUBSYSTEM==«block». Но такими свойствами будет обладать любая флешка. Да и, как я говорил ранее, не факт что в следующий раз система назовёт наш том sdh1, и вдруг я куплю себе другой фотоаппарат с xD-Picture или вообще CompactFlash (а это будут sdf, sdg, sdi) — не хотелось бы из-за этого переписывать правила. На наше счастье udev поддерживает джокеры и поэтому в этой части для правила возьмём выражение KERNEL==«sd?1» (или даже «sd*»), что будет означать первый том подключаемого диска, а под какой конкретно он будет буквой — нам не важно, скрипт всё равно получит имя полностью без джокера.

Перейдём к следующему устройству. /devices/pci0000:00/0000:00:02.1/usb1/1-1/1-1:1.0/host14/target14:0:0/14:0:0:2/block/sdh тут нового и запоминающегося, пожалуй, только ATTRS{events}==«media_change», но опять же данный атрибут будет у любой флешки.

Следующее устройство /devices/pci0000:00/0000:00:02.1/usb1/1-1/1-1:1.0/host14/target14:0:0/14:0:0:2 и тут уже более конкретный вариант ATTRS{model}==«SD/MMC », но привязавшись к этому атрибуту мы будем реагировать только на SD-карты и игнорировать остальные возможные варианты карт памяти. В следующих трёх устройствах мы опять не находим ничего интересного. У следующего устройства /devices/pci0000:00/0000:00:02.1/usb1/1-1 уже интереснее:
SUBSYSTEMS=="usb"
DRIVERS=="usb"
ATTRS{configuration}=="CARD READER"
ATTRS{idVendor}=="0bda"
ATTRS{idProduct}=="0151"
ATTRS{product}=="USB2.0-CRW"
ATTRS{serial}=="20060413092100000"

Судя по арибутам, это сам карт-ридер и именно он-то нам и нужен (оставшиеся устройства уже имеют отношение к шине USB). Итого в правилах соответствий мы будем опираться на атрибуты карт-ридера и первого тома карты памяти:
ACTION=="add", KERNEL=="sd?1", SUBSYSTEMS=="usb", ATTRS{configuration}=="CARD READER", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="0151"

И вставили ещё событие (ACTION==«add») добавления устройства, т.к. копирование после того, как карту вынули из ридера невозможно чисто физически. Я тут подумал: ещё есть атрибут ATTR{ro}==«0» — его тоже можно в правила дописать, а иначе если диск защищён от записи, то ничего не удастся переместить (но скопировать можно будет, хоть и может быть слегка разбросано по лишним директориям в зависимости от шаблона имён типов файлов у фотоаппарата), но я пока не буду себе этого делать.

Как вы уже наверно обратили внимание для самого устройства применяются ключи ATTR/KERNEL/SUBSYSTEM/DRIVER, а для его родителей уже с буквой S на конце: ATTRS/KERNELS/SUBSYSTEMS/DRIVERS.

У меня есть ещё один ридер для microSD карт, так у него в configuration пустая строка и привязаться можно только по idVendor и idProduct. В одной из предыдущих версиях файла правил udev'а (скрипт я начал писать где-то в середине августа, и не обновлял систему до октября и где-то в этот промежуток и был добавлен этот атрибут, да и вообще изменились многие атрибуты для этого ридера) использовались атрибуты vendor и device, которые (слегка изменив, поскольку они исчезли) я оставил, хотя особой нужды теперь в них нет. В этом варианте фильтра он будет действовать только для этой модели карт-ридера и подключение какой-то другой модели не вызовет никакого эффекта.

Фильтры мы определили, а действия ещё не назначили. Т.к. нам надо выполнить определённый скрипт, то дописываем к фильтру действие:
RUN+="/root/bin/PhotoSort.sh %k"

%k в параметрах скрипта — имя тома, сгенерированное системой (то, что выводится в ключе KERNEL диска — sdh1); /root/bin/PhotoSort.sh — имя нашего будущего скрипта.

Окончательный файл правил для udev выглядит так:
>cat /etc/udev/rules.d/99-lumix.rules
#Автоматическое копирование медиа с карты памяти
ACTION=="add", KERNEL=="sd?1", SUBSYSTEMS=="usb", ATTRS{configuration}=="CARD READER", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="0151", RUN+="/root/bin/PhotoSort.sh %k"
#предыдущий вариант через производителя и модель
#ACTION=="add", KERNEL=="sd?1", SUBSYSTEMS=="pci", ATTR{events}=="media_change", ATTRS{vendor}=="0x10de", ATTRS{device}=="0x077e", RUN+="/root/bin/PhotoSort.sh %k"

Теперь сохраняем этот текст в каталоге /etc/udev/rules.d/ (нужны права root) под произвольным именем (у меня 99-lumix.rules). Демон udev обрабатывает файлы из этого каталога в алфавитном порядке, пока не найдёт подходящий по условиям фильтр. После нахождения соответствующего правила дальнейшая обработка файлов не производится. Поэтому номер для нашего правила можно ставить маленький-раньше обработается и не перекроется каким-нибудь другим правилом.

Если хочется следить за всеми подключаемыми флешками и картами, то можно опереться на уровень ниже после самого ридера и цепляться по SUBSYSTEMS==«usb», DRIVERS==«usb-storage» — они точно должны быть и у карт и у флешек. Но я пока слежу только за ридером — для остального не было нужды.

bash
А теперь пишем скрипт для сортировки фотографий с подключенной карты. Первоначальный вариант поддерживал сортировку только диска целиком. В процессе эксплуатации захотелось ещё обрабатывать и уже имеющиеся каталоги (подкопилось несортированных), посему пришлось немного изменить и дописать. Здесь будет рассмотрена вторая версия, которая может сортировать медиа-файлы (расширения медиа-файлов заданы в самом скрипте в параметрах команды find) и с диска и из каталога.

Сначала кратко про алгоритм работы данного скрипта. Определяем что будем обрабатывать — том или каталог. Если том, то его надо в начале примонтировать (а если точка монтирования уже занята, а она используется только этим скриптом, то скрипт завершит свою работу), а в конце не забыть отмонтировать. После монтирования проверяется наличие скрытого файла .PhotoSort в котором хранятся настройки для данной карточки. Его отсутствие служит сигналом, что была вставлена какая-то обычная карточка и с ней ничего делать не нужно. В этом файле можно задать имя диска (требуется только для логирования — об'яснения в конце) и каталог для сохранения файлов, если стандартный не подходит (думал сделать для мобильника отдельное хранилище). Каталог для экспорта можно ещё указать вторым параметром к скрипту в командной строке или в файле правил udev.

Затем все медиа-файлы из заданного и вложенных каталогов (потому что фотоаппараты сохраняют фотографии в разных каталогах, заранее нам не известных) необходимо отсортировать по времени их создания. Здесь кроется подвох: в Linux нет такого понятия как время создания файла, а есть только время последнего доступа к файлу и при манипуляциях с картинками оно обновляется, поэтому опираться на него нельзя. По имени файла тоже нельзя: DSC08655.JPG сделанный 02.05 должен идти после MOV08554.MPG от 29.04, который в свою очередь должен быть обработан после P1170007.JPG от 19.04. Особенно остро эта проблема встаёт если у нас есть несколько фотоаппаратов от разных производителей, которые делали снимки одного события (aka «гулянка»). На помощь нам приходит EXIF — помимо всего прочего в нём есть атрибут DateTimeOriginal и CreateDate (между ними есть какая-то разница: по крайней мере первого у видеофайлов нет и если он по каким-то причинам не прочитался, то читаться будет второй) и эти атрибуты в обычных ситуациях не меняются. Но как считать файлы со всех вложенных каталогов, начиная с заданного, отсортированные по этим атрибутам — я не знаю, поэтому в цикле все файлы переименовываются (это умеет делать и сам exiftool, но для лога я оставил цикл), добавляя время создания снимка или видео к имени (чтобы потом можно было сортировать в хронологическом порядке по имени файла).

Второй цикл распределяет файлы по каталогам: новый каталог создаётся когда перерыв между текущим файлом и предыдущим становится больше пяти часов (5*60*60) — именно для этого мы и сортировали файлы в хронологическом порядке. Пять часов — цифра с потолка. У меня ещё не было снимков с одного события чтоб перерыв между кадрами был больше этого времени. Были экскурсии, с долгими переездами между POI, но дорога занимала меньше пяти часов, а если больше, то это был уже другой город. Были свадьбы и праздники переходящие в другие сутки, и хоть дата менялась, но событие оставалось тем же. Так что, пока перерыв небольшой сохраняем в тот же каталог, что и предыдущий файл. Имя каталога задаётся как YYYYMMDD_HH — часы (берутся от первого файла нового события) в имя каталога добавлены из-за двух событий одного дня, иначе при переходе ко второму дню получим то же имя каталога, и хоть его создание завершится с ошибкой, но дальнейшие сохранения файлов будут осуществляться в тот же каталог.

В лог пишется всё происходящее (так я проверял функционирование скрипта), а так же команды для возврата файлов обратно на диск (для отладки было полезно — выполнял кусок файла и тестовые медиа-файлы возвращались обратно на карту к своим старым именам). Дополнительные обработки (поворот, уменьшение, подписывание) — не использую, всё равно потом их смотреть, удалять и всё это делать (а подпись на фотографиях я не ставлю). Место, где это можно сделать обозначено комментарием в тексте скрипта.
Сам скрипт:
>cat /root/bin/PhotoSort.sh
#!/bin/bash
#/root/bin/PhotoSort.sh

#requires: bash,coreutils,findutils,exiftool,sed,util-linux

#cat /etc/udev/rules/99-lumix.rules
##Автоматическое копирование медиа с карты памяти
#ACTION=="add", KERNEL=="sd?1", SUBSYSTEMS=="usb", ATTRS{configuration}=="CARD READER", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="0151", RUN+="/root/bin/PhotoSort.sh %k"
##предыдущий вариант через производителя и модель
##ACTION=="add", KERNEL=="sd?1", SUBSYSTEMS=="pci", ATTR{events}=="media_change", ATTRS{vendor}=="0x10de", ATTRS{device}=="0x077e", RUN+="/root/bin/PhotoSort.sh %k"

# Для определения условий к правилу udev
#udevadm info -a -p $(udeadm info -q path -n /dev/sd*) или udevadm info -a -n /dev/sd*
#http://www.arccomm.ru/OpenSource/Dev/udev.html

if [[ -z "$1" ]]
    then
    echo Сортирует изображения и видео из всех каталогов внутри указанного во вложенные каталоги указанного,
    echo основываясь на времени создания файлов
    echo $(basename "$0") Source [DestDir]
    echo Source - каталог или имя диска,содержимое которого необходимо рассортировать
    echo DestDir - целевой каталог,в который будут помещены файлы
    echo Если сортируется диск,то передаётся имя тома
    echo Если сортируется диск,то в корне должен быть файл .PhotoCopy
    echo В данном файле в первой строке хранится абсолютный путь для сохранения фотографий
    echo     Во второй строке - имя флешки \(для лога\)
    echo Примеры:
    echo $(basename "$0") sdd1 - Попытка рассортировать том /dev/sdd1.
    echo     Сортировка будет выполнена только если в корне диска sdd1 есть файл с настройками .PhotoCopy
    echo     При этом данный файл может быть пустым
    echo $(basename "$0") . ~/Photo - Рассортирует файлы из текущего \(.\) и всех вложенных в него  каталогов
    echo     во вложенные в ~/Photo/ каталоги
    exit 1 #неправильно заданы параметры скрипта
fi

if [[ ${1:0:1} == "/" || ${1:0:1} == "." || ${1:0:1} == "~" ]]
    then # передали каталог
    if [[ -d "$1" ]]
        then
        disk="$1"
            else
            echo Исходный каталог недоступен
            exit 2 # ненайден каталог для сортировки
    fi
        else # передали диск
# Передали том
        dev="/dev/$1"
        if [[ ! -e "$dev" ]]
            then # ничего не делать,если нет этого раздела на диске(этого файла не будет при извлечении флешки,но событие будет вызвано)
            exit 0
        fi
# Куда монтировать флешку
        disk="/mnt/photo"
fi

# Куда сохранять фотографии
if [[ ! -z "$2" ]]
    then # Если передан вторым параметром
    photo="$2"
        else
        photo="/mnt/temp/Photo" # по умолчанию,должен существовать
fi
sphoto="" # из файла настроек
# Директория в которую будут сохраняться файлы по датам
photodir=""
# лог-файл: возможно его надо бросать в исходный каталог или целевой-есть варианты
log="/var/log/photosort.log"
# Время создания последнего обработанного файла
lastfiletime=0
# Время создания текущего файла
curfiletime=0
# для посыла сообщения-не сработало
#export XAUTHORITY="/home/%username%/.Xauthority"
#export DISPLAY=:0.0
#notify-send Photoes "FlashCard found"

# Проверить не смонтирован ли куда-нить каталог? Сделано из-за того,что если что дважды в одну точку не примонтироваться
if [[ -n "$dev" ]]
    then
    grep -q "$disk" /etc/mtab
    if [[ $? -eq 0 ]] # если найдена хоть одна строка,то grep вернёт 0
        then # уже примонтировано-ничего не делать
        echo "#=- $(date -u +%Y.%m.%d\ %T) Точка монтирования занята -=#"
        exit 10 # ничего не делали,потому что к каталогу что-то было примонтировано
    fi
    # Монтируем
    if [[ ! -d "$disk" ]]
        then # если нет каталога для монтирования-создаём
        mkdir "$disk" &>>"$log"
        echo "#" Создана точка монтирования "$disk" >> "$log"
    fi
    mount -t vfat -o noatime,rw,noexec,users,iocharset=utf8 "$dev" "$disk" &>> "$log"
    if [[ ! -e "$disk"/.PhotoCopy ]]
        then # нет файла с настройками в корне флешки,не надо ничего делать
        umount "$disk" &>> "$log"
        exit 0 # ничего не делали,поскольку файла с настройками в корне диска нет,а значит ничего не надо было делать
    fi
# Вариант:если передан скрипту каталог для сохранения $2,то использовать его и игнорировать что указано в настройках
# Из минусов-можно указать произвольный путь(хоть /dev)и файлы будут туда скопированы(запускается от root)
    sphoto=$(head -n1 "$disk"/.PhotoCopy) # в первой строке файла должен быть указан абсолютный путь к каталогу,в который будут складироваться фотографии с данного диска
    if [[ ${sphoto:0:1} == "/" && -d "$sphoto" ]]
        then # Каталог для сохранения фотографий,как задано в файле существует,поэтому используем его
        photo="$sphoto"
    fi
    sphoto=$(tail -n1 "$disk"/.PhotoCopy) # в последней строке файла хранится имя диска
    echo $(date -u +%Y.%m.%d\ %T) Вставлен новый диск "$sphoto" >> "$log"
        else
        if [[ ! -z "$2" ]]
            then
            photo="$2" # куда сохранять картинки
        fi
        log="./PhotoSort.log" # куда писать отчёт о содеянном
        echo "#" $(date -u +%Y.%m.%d\ %T) Сохраняем в каталог "$photo" >> "$log"
        echo cd $(pwd) >> "$log" # сохраняем текущий каталог для того,чтоб при попытке вернуть всё взад можно было восстановиться с относительными путями
fi # монтирование диска и обработка файла настроек
# NB!: если файлы разных типов по-разному начинаются,то могут копироваться как попало
# Поэтому переименовываем файлы,чтоб в имени сначала была дата их создания,
# исключая файлы с подчёркиванием(возможно это уже переименованные скриптом файлы)
# и файлы с пробелами(тут могут быть проблемы при обработке-надо экранировать пробелы)
for file in $(find "$disk" -type f \( -name '*.JPG' -o -name '*.MOV' -o -name '*.MPG' -o -name '*.THM' -o -name '*.MP4' -o -name '*.AVI' \) -and -not -name '*_*' -and -not -name '* *')
    do
# exiftool вернёт строку вида:
# Date/Time Original              : 2011:07:30 15:35:52
# необходимо оставить только цифры 20110730153552
    curfiletime=$(exiftool -DateTimeOriginal "$file" | cut -d: -f2- | sed 's/[:\ ]//g') # ДатаВремя этого файла
    if [[ $curfiletime == "" ]]
        then
        curfiletime=$(exiftool -CreateDate "$file" | cut -d: -f2- | sed 's/[:\ ]//g') # ДатаВремя этого файла
    fi
    mv "$file" $(dirname "$file")/"$curfiletime"_$(basename "$file") &>> "$log" # переименовываем
    echo mv $(dirname "$file")/"$curfiletime"_$(basename "$file") "$file" >> "$log" # преобразование в обратную сторону
    done
# Теперь можно обрабатывать все файлы
# (если будет вставлен диск не из фотоаппарата,то всё содержимое будет разбросано по разным каталогам-необходимо фильтровать желания-либо в файл настроек добавить каталог,откуда брать файлы,либо список исключений)
for file in $(find "$disk" -type f -name '*.JPG' -o -name '*.MOV' -o -name '*.MPG' -o -name '*.THM' -o -name '*.MP4' -o -name '*.AVI' | sort)
    do
# exiftool возвращает дату,где все части друг от друга отделены двоеточиями и надо в дате заменить двоеточия на тире,ну и обрезать название тега
    curfiletime=$(exiftool -DateTimeOriginal "$file" | sed -r 's/^.+: ([0-9]+):([0-9]+):([0-9]+) ([0-9]+):([0-9]+):([0-9]+)/\1-\2-\3 \4:\5:\6/g')
    if [[ $curfiletime == "" ]]
        then
        curfiletime=$(exiftool -CreateDate "$file" | sed -r 's/^.+: ([0-9]+):([0-9]+):([0-9]+) ([0-9]+):([0-9]+):([0-9]+)/\1-\2-\3 \4:\5:\6/g')
    fi
    curfiletime=$(date -d "$curfiletime" +%s) # преобразовать в секунды с начала времён
    if (( $curfiletime - $lastfiletime > 5*60*60 )) # следующий файл по времени создания слишком позже сделан:через 5*60*60 секунд,на основе чего и предполагаем что это уже другая серия снимков
        then
        photodir=$(date -d @$curfiletime +%Y.%m.%d_%H) # Таким будет новый каталог под снимки
        if [[ ! -d "$photo"/"$photodir" ]] # если нет такого каталога-создаём
            then
            mkdir "$photo"/"$photodir" &>> "$log"
            chown nobody:users "$photo"/"$photodir" &>> "$log"
            chmod 0777 "$photo"/"$photodir" &>> "$log"
            echo "#" $(date -u +%Y.%m.%d\ %T) Создан новый каталог "$photodir" >> "$log"
        fi
        lastfiletime="$curfiletime"
    fi
    echo "#" $(date -u +%Y.%m.%d\ %T) Копирование файла "$file" в "$photo"/"$photodir"/$(basename "$file") >> "$log"
    echo copy "$photo"/"$photodir"/$(basename "$file") "$file" >> "$log"
# Тут же можно устроить переименовывание файла во что-то более благозвучное(дата/время создания,по gps определить ближайший город)
    mv "$file" "$photo"/"$photodir"/ &>> "$log"
    chown nobody:users "$photo"/"$photodir"/$(basename "$file") &>> "$log"
    chmod 0666 "$photo"/"$photodir"/$(basename "$file") &>> "$log"
    done
if [[ -n "$dev" ]]
    then
    echo $(date -u +%Y.%m.%d\ %T) Диск изъят >> "$log"
# Отмонтируем диск обратно
    umount "$disk" &>> "$log"
fi
exit 0

Пояснения к некоторым моментам: exiftool возвращает строки вида «Date/Time Original: 2011:07:30 15:35:52» эту строку необходимо преобразовать в «20110730153552» (обратите внимание: в интернете полно примеров, где разделитель не двоеточие, а вертикальная черта — смотрите что у вас):

curfiletime=$(exiftool -DateTimeOriginal "$file" | cut -d: -f2- | sed 's/[:\ ]//g')

cut — вырежет всё что после первого двоеточия, а sed удалит все пробелы и двоеточия из получившейся строки. Уверен что можно обойтись только sed'ом, но с RegExp'ами не очень дружон.

curfiletime=$(exiftool -DateTimeOriginal "$file" | sed -r 's/^.+: ([0-9]+):([0-9]+):([0-9]+) ([0-9]+):([0-9]+):([0-9]+)/\1-\2-\3 \4:\5:\6/g')

Ту же строку преобразует в «2011-07-30 15:35:52». Опять же, возможно существует более элегантное решение. Это чтоб date мог обработать как дату (в произвольном формате строку ему нельзя подсунуть).

Использование
Скрипт обрабатывает указанный каталог и все вложенные в него (для карты памяти это будет её корень и всё, что глубже) как один.

./PhotoSort.sh sdh1
таким образом скрипт вызывается из udev'ом для сортировки диска sdh1. В этом случае будет проверяться наличие файла с настройками в его корне. Диск нам передали или каталог определяется по первому символу: если это будут ~ / или. — значит, каталог, иначе — диск.

./PhotoSort.sh ~/AllFromParty
а так можно рассортировать все фото- и видео-файлы из всех вложенных в ~/AllFromParty каталогов (включая и его самого). Сохраняться всё будет в каталог по-умолчанию, заданный в скрипте (photo="/mnt/temp/Photo"). Если в исходном каталоге лежали файлы с нескольких фотоаппаратов, то может быть некоторый рассинхрон в именах файлов из-за того, что время между камерами не синхронизировано, а то и вовсе никогда не выставлялось. Что может вызвать распределение файлов из разных фотоаппаратов в разные каталоги. Тогда перед выполнением сортировки необходимо скорректировать время в тегах EXIF: exiftool "-DateTimeOriginal+=00.00.0000 02:37:30" *.JPG — сдвинет время в теге DateTimeoriginal на 2:37:30 в будущее по всем файлам JPG из текущего каталога.

./PhotoSort.sh ~/AllFromParty /media/backup/Photoes
то же самое, что и выше, но сохраняться всё будет в каталоге /media/backup/Photoes

Оставшиеся хотелки
  1. Нотификацию по завершению импорта
    Вообще нотификация есть: когда экспорт завершён, KDE сообщает о том, что подключили диск, но хотелось бы услышать это нормальными буквами всем залогиненным пользователям. Тот пример, что я нашёл, у меня не заработал (кусок от него остался в тексте).
  2. Все настройки скрипта хочется хранить в отдельном файле, а не в коде. Придётся учиться разбирать ini-файлы.
  3. Время жизни скрипта ограничено одной минутой. Это самый неприятный момент. Хоть везде и пишут что надо через RUN+= запускать скрипт, но по факту он потом просто висит замороженным где-то в бэкграунде. Может быть это не в udev'е дело, может KDE вмешивается, но как решить эту проблему — я не знаю. Если файлов скопилось очень много, приходится вытаскивать и вставлять карту обратно.
  4. В файле настроек .PhotoSort можно указать любой каталог и ничто не помешает udev'у скопировать туда фотографии. Пользователь потом в жизни их не найдёт, а root может быть когда-нибудь случайно наткнётся на них. С этим надо что-то делать. Может выполнять некоторые действия (перемещение, создание каталогов) от обычного пользователя или проверять права и владельца целевого каталога.
  5. Пока скрипт не отработает, dosfslabel ничего не сможет прочитать. Наверно udev что-то ещё потом для него делает. Только из-за этого в файле с настройками появилась строка с именем диска.
  6. Развить файл настроек флешки: чтоб можно было задавать каталоги исключений или каталоги с фотографиями на экспорт. А то было глупостью подключать флешку с играми от Caanoo — так появилась обработка файла .PhotoSort с настройками.
  7. Временное отключение скрипта по удержанию клавиши ну или другим каким-то способом, а то сейчас приходится ставить RO на карте памяти.
  8. Ваши пожелания. В самом деле, может я не вижу ещё какой-то задачи, которую можно было б попутно решить в рамках данного скрипта?

Список использованной литературы
Искусство написания Bash-скриптов
HOWTO: udev и автомонтирование носителей
udev. Как установить свои правила (другая такая же ссылка)
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 17

    +7
     ls -l /dev/disk/by-id/
    lrwxrwxrwx 1 root root    9 Ноя 10 04:04 ata-Hitachi_HDS5C3030ALA630_MJ1311YNG5H6EA -> ../../sdd
    lrwxrwxrwx 1 root root    9 Ноя 10 04:04 ata-Hitachi_HDS723030ALA640_MK0311YHG46TNA -> ../../sdg
    lrwxrwxrwx 1 root root   10 Ноя 10 04:04 ata-Hitachi_HDS723030ALA640_MK0311YHG46TNA-part1 -> ../../sdg1
    lrwxrwxrwx 1 root root    9 Ноя 10 04:04 ata-INTEL_SSDSA2CT040G3_CVPR113101TB040AGN -> ../../sde
    lrwxrwxrwx 1 root root   10 Ноя 10 04:04 ata-INTEL_SSDSA2CT040G3_CVPR113101TB040AGN-part1 -> ../../sde1
    lrwxrwxrwx 1 root root    9 Ноя 10 04:04 ata-SAMSUNG_HD204UI_S2H7J1SZ910745 -> ../../sdf
    lrwxrwxrwx 1 root root   10 Ноя 10 04:04 ata-SAMSUNG_HD204UI_S2H7J1SZ910745-part1 -> ../../sdf1
    lrwxrwxrwx 1 root root    9 Ноя 10 04:04 ata-ST31500541AS_5XW035A8 -> ../../sdh
    lrwxrwxrwx 1 root root   10 Ноя 10 04:04 ata-ST31500541AS_5XW035A8-part1 -> ../../sdh1
    lrwxrwxrwx 1 root root    9 Ноя 10 04:04 ata-WDC_WD1500HLFS-01G6U1_WD-WXC0CA9S7468 -> ../../sda
    lrwxrwxrwx 1 root root   10 Ноя 10 04:04 ata-WDC_WD1500HLFS-01G6U1_WD-WXC0CA9S7468-part1 -> ../../sda1
    lrwxrwxrwx 1 root root   10 Ноя 10 04:04 ata-WDC_WD1500HLFS-01G6U1_WD-WXC0CA9S7468-part2 -> ../../sda2
    lrwxrwxrwx 1 root root    9 Ноя 10 04:04 ata-WDC_WD15EARS-00S8B1_WD-WCAVY3586255 -> ../../sdi
    lrwxrwxrwx 1 root root   10 Ноя 10 04:04 ata-WDC_WD15EARS-00S8B1_WD-WCAVY3586255-part1 -> ../../sdi1
    lrwxrwxrwx 1 root root    9 Ноя 10 04:04 ata-WDC_WD2003FYYS-02W0B1_WD-WCAY00134250 -> ../../sdb
    lrwxrwxrwx 1 root root   10 Ноя 10 04:04 ata-WDC_WD2003FYYS-02W0B1_WD-WCAY00134250-part1 -> ../../sdb1
    lrwxrwxrwx 1 root root    9 Ноя 10 04:04 ata-WDC_WD20EARS-00MVWB0_WD-WMAZ20249517 -> ../../sdc
    lrwxrwxrwx 1 root root   10 Ноя 10 04:04 ata-WDC_WD20EARS-00MVWB0_WD-WMAZ20249517-part1 -> ../../sdc1
    
      0
      lrwxrwxrwx 1 root root 9 нояб. 16 00:21 usb-Generic-_Compact_Flash_20060413092100000-0:0 -> ../../sdf
      lrwxrwxrwx 1 root root 9 нояб. 16 00:21 usb-Generic-_MS_MS-Pro_20060413092100000-0:3 -> ../../sdi
      lrwxrwxrwx 1 root root 9 нояб. 16 00:21 usb-Generic-_SD_MMC_20060413092100000-0:2 -> ../../sdh
      lrwxrwxrwx 1 root root 9 нояб. 16 00:21 usb-Generic-_SM_xD-Picture_20060413092100000-0:1 -> ../../sdg
      Ага, спасибо.
        +1
        Поговорили…
      +1
      Я для себя тупо делал проверку папки DCIM в корне флешки, и не важно, что и куда оно вставлено. Если есть такая папка (вроде по стандарту для цифровых камер положено), то значит можно копировать.
        0
        Я тут ещё про телефон думал, а он у меня кладёт в mobile/picture.
        0
        Чо-то как-то название статьи не соответствует содержимому… скрипт на bash — разве не набор команд командной строки?
          0
          Не совсем. Скрипт — это скрипт! А насчет «удобных средств» я своей жене сделал на bash-е «скриптовое средство» от которого она в полном восторге: есть папка на рабочем столе и есть рядом скрипт автоматического ужимания фоток: 1) закидывает она в папку любые фотки 2) кликает на скрипт 3) через несколько секунд/минут можно юзать «готовые для web» фотки.

          Делюсь:
          #!/bin/sh
          #make_small.sh
          #########
          cd /home/valya/Desktop/fotoz_2_small || exit 1
          mkdir -p 800x600
          for f in *.[Jj][pP][gG] *.[gG][iI][fF] *.[pP][nN][gG] ; do
            if [ ! -f "800x600/${f}" ] ; then
              convert "${f}" -resize 800x800 "800x600/${f}"
            fi
          done
          


          NB: /home/valya/Desktop/fotoz_2_small замените на свой путь к папке, само собой!
          0
          В прошлогодней статье предлагалось каждый раз пользоваться командной строкой, а тут один раз написал и забыл.
            +1
            А через год забыл, что писал, как писал, и как оно вообще работает. А оно всё работает!
            Вот так линукс расслабляет :)
              0
              Собственно, так оно и происходит. Решил какую-то проблему и забыл до следующего глобального обновления (кто ж знал что за полтора месяца атрибуты карт-ридера поменяются в корне).
          0
          1. нотификация —
          $ export DISPLAY=:0.0
          $ mv /from/file /to/file && notify-send 'Move succeed' || notify-send 'Move failed'
          

          не уверен, но в KDE наверное можно использовать kdialog

          2. создать файл myapp.cfg с содержимым — key=«value»

          $ source myapp.cfg
          $ echo key
          value
          


          3. очень странно, не видел никаких ограничений на врем выполнения скрипта udev'ом, скорей всего скрипт на чем то спотыкается и не может завершиться, попробуйте в начале скрипта поставить set -x и ход выполнения сбросить в лог RUN+="/root/bin/PhotoSort.sh %k >> /tmp/PhotoSort.log"

          4. я организовал определение флэшки через UUID ее раздела, определить можно через blkid, и если это наша флешка, то что-то делаем с ней

          5. использовать UUID

          6.…

          7. зачем?
            0
            1. Нотификации я добился. notify-send, наверно, в KDE не работает совсем. А xmessage и kdialog работают. Теперь другая напасть: скрипт ожидает ответа от kdialog.
            Судя по инструкции (шикарная, кстати) данная команда должна была отобразить в области уведомлений окно и через 10 секунд скрыться. Так оно и происходит, если запускать от своего имени (почти так, первое сообщение не показывается, только раздражает иконку информации, а на второй раз — открывает окошко, поэтому там два сообщения: старое и новое), но как только запускаешь от root'а как я выше привёл, то окно совершенно другого вида посередь экрана (верхнее окно) и скрипт останавливается и ждёт пока не кликнешь в это окошко или таймер не закончится:


            2. Спасибо, как-нить займусь этим.

            3. А вот не работает. Как я понял перенаправлять надо stderr, а значит надо было писать RUN+="/root/bin/PhotoSort.sh %k 2>> /tmp/file", но это всё равно не помогает — не пишет он ничего и даже файл не пытается создать. Как я понимаю, это из-за того, что скрипт получает такую строку: sdh1 2>> /tmp/file и перенаправления потоков не происходит.
            Зато пока я экспериментировал с kdialog я проверил что выполняется он точно 60 секунд (выставив таймер на 70 секунд), а потом управление захватывает KDE. Я поставил echo «kdialog» >> /tmp/file сразу после вызова этого kdialog и ничего не получил в этом файле. Т.е. после того, как прошло 70 секунд и управление должно было вернуться в скрипт, там нас никто не ждал.

            4. Про UUID спасибо, что-то мысль такая мелькнула, но не задержалась в голове. Это не избавляет нас от лишних движений по регистрации карты памяти (ну или в моём случае — создание файла в корне).

            7. Из-за этого и появился файл в корне флешки, как индикатор того, что надо что-то делать. А так жмёшь кнопу, вставляешь флешку и она копируется, не жмёшь — не копируется. У меня тут раздвоение личности: хочется и с произвольной флешки скопировать, а с другой стороны — оно мне надо с произвольной-то?
              0
              Чтоб скрипт не ждал — приставляем &:
              notify-send 'Move failed' &
                0
                Зашибись. Осталось только чтоб он в Системные уведомления это слал.
            0
            Опять же, возможно существует более элегантное решение.

            Вы правы, существует. Воспользуйтесь exiv2.
            Она позволяет простенько считывать, редактировать, удалять и пр. действия с мета-информацией exif.
            В том числе у неё есть команда rename, которая переменовывает файлы, основываясь на Exif.Photo.DateTimeOriginal или (при отсутствии) — на Exif.Image.DateTime в соответствии с форматом, который вы укажите в ключе '-r' (который аналогичен стандартному формату strftime). По умолчанию формат %Y%m%d_%H%M%S, что меня вполне устраивает, так что ключём даже не пользуюсь.
            Итак, шелл-функция из моей библиотеки скриптов для переименовывания файлов на основе их exif данных такая:
            # sort photos by exif date
            # $* - filenames to rename
            # example: exif_date_sort ./* # sort all files in directory
            # author: japdoll, 2008
            exif_date_sort()
            {
              until [ -z "$1" ]
              do
                exiv2 rename -- "./$1"
                shift
              done
              return 0
            }
            

            Only users with full accounts can post comments. Log in, please.