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

в 19:21, , рубрики: bash scripting, exif, exiftool, linux, udev, Программирование, фотография, Фототехника, метки: , , , , ,

Я увлекаюсь фотографией ещё со Смены-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. Как установить свои правила (другая такая же ссылка)

Автор: vlivyur

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js