--- title: بازیابی متادیتای کلی عکس tags: لینوکس فایل‌سیستم exif bash pipe category: راهنما uuid: 43d53a11-8aba-4f1f-b1d4-bdde7fcd1ffa --- مشکلی با عکس‌های آرشیوی‌ام پیش آمده که بد نیست بنویسم برای خود آینده‌ام و رهگذران عزیز. من عکس‌هایی که با گوشی‌های مختلف گرفته‌ام را به کمک Syncthing با کامپیوترم سینک می‌کنم. در حال حاضر یکسالی می‌شود که روی یک Rock64 فسقلی Libreelec ریخته‌ام و یک هارد پنج‌ترابایتی هم برای سینک به درگاه USB 3 آن متصل کرده‌ام (که ممکن است دیر یا زود به خاطر روشن و خاموش شدن متعدد عمرش را بدهد به شما). سینک‌تینگ روی راک است و فایل‌ها سنک می‌شود روی آن هارددیسک. حالا مشکلی که پیش امده این است که من گوشی جدیدی خریده‌ام و تمام فایل‌ها را روی آن به شکل ترکیبی! (دستی و سینک‌تینگ) کپی کردم. حالا تاریخ آخرین تغییر فایل‌ها به هم ریخته است و وقتی در برنامه‌ی گالری گوشی عکس‌ها را باز می‌کنم تاریخ‌ها اشتباه است. این تغییر نه تنها باعث می‌شود که سورت کردن تصاویر براحتی امکانپذیر نباشد بلکه برنامه‌هایی که از روی عکس‌ها کلیپ‌های کوچک می‌سازند به اشتباه می‌افتند. تصور بکنید در حال تماشای عکس‌های سال‌های گذشته هستیم و به کمک توالی تاریخ عکس‌ها در حال بازسازی گذر ایام در ذهنمان که به یکباره عکسی نامربوط ظاهر می‌شود که دلپذیر نیست. خلاصه برای حل این مشکل طبق معمول با یک مقدمه دست به دامن خط فرمان می‌شویم. خبر خوش اینکه در نام همه‌ی عکس‌ها روز عکاسی در فرمت ساده‌ی ISO8601 آمده است. یعنی از چپ به راست اولی تاریخ چهار رقمی و بعد ماه دو رقمی و بعد روز دو رقمی (حالا دیگری هم دارد که از بحث ما خارج است). در برخی فایل‌ها اولی چیز ثابتی آمده و بعد تاریخ که مشکلی نیست. از این گذشته عکس‌ها حاوی اطلاعات Exif هستند که تاریخ اصلی عکاسی در آن ثبت شده است. پس در بدترین حالت می‌توانیم تاریخ را از روی نام فایل‌ها بازسازی کنیم. داشتن تاریخ یک رویداد در نام فایل به قدری کاربردی است که افزودن تاریخ به فرمتISO8601 به ابتدای نام تمام فایل‌ها به یک عادت دائمی برای من تبدیل شده است. معمولا وقتی قصد کپی گرفتن از یک فایل دارم از دستور زیر استفاده می‌کنم: $ copy myfile.jpg $(date -I)_myfile.jpg یا بدون خط تیره در نام فایل: $ copy myfile.jpg $(date +%+4Y%m%d)_myfile.jpg حسن این فرمت اینست که نام فایل ابتدا با سال و سپس با ماه و روز شروع می‌شود که کار سورت کردن فایل‌ها را خیلی ساده می‌کند. ایده‌ی فعلی من برای رفع این ایراد اینست که متادیتای فایل‌هایمان را به کمک اطلاعات اگزیف (یا در نبود آن با کمک نام فایل) اصلاح کنیم و بگذاریم که سینک‌تینگ فایل‌ها را دوباره سینک کند. پیش از آن بیایید نگاهی بکنیم به این متادیتا. مهم است که بدانیم متادیتا (داده در مورد داده) چیست و کجا ذخیره می‌شود. تاریخ ساخت و تغییر و دسترسی و مانند اینها در حالت عادی داخل فایل ذخیره نمی‌شوند و هر فایل‌سیستم و سیستم‌عامل آن را جداگانه ذخیره می‌کند. فایل‌سیستم ما در این مورد ext4 و سیستم عامل هم ما مبتنی بر لینوکس است. من اطلاعی در مورد ویندوز ندارم اما در سیستم‌عامل‌های مشابه یونیکس متادیتای فایل در ساختاری بنام inode ذخیره می‌شود (از جایی که مک‌اوس هم از خانواده‌ی یونیکس است آنجا هم باید این موارد صادق باشد). به کمک فرمان `ls -i` می‌توانیم inode یکتای مختص هر فایل را ببینیم. inode اطلاعات مختلفی از جمله حق دسترسی‌ها و همچنین چندین تاریخ (timestamp) مختلف را در خود ذخیره می‌کند. برای مشکلی که ما اینجا با آن روبرو هستیم ctime یا همان `creation_time` و mtime یا همان `modification_time` مهم است. اولی تاریخ ساخت و دومی تاریخ آخرین تغییر فایل است. atime یا همان `access_time` هم وجود دارد که تاریخ آخرین دسترسی یا خوانده شدن محتوای فایل است. در [مستندات کرنل لینوکس برای ext4] جزئیات پیاده‌سازی inode در لینوکس آمده است. نکته‌ی جالبی که در مستندات یاد شده آمده است اینست که متغیری که برای ذخیره‌ی برخی از این تاریخ‌ها استفاده شده چهار بایت (سی و دو بیت) است. نحوه‌ی رایج ذخیره‌ی timestamp یا لحظه‌ی خاص در امتداد محور زمان یک عدد است که تعداد ثانیه‌های سپری شده از بامداد روز اول ژانویه‌ی سال ۱۹۷۰ به وقت گرینویچ در خود ذخیره می‌کند. منتها در سال ۲۰۳۸ تعداد این ثانیه‌ها دیگر در چهار بایت جا نمی‌شود و به اصطلاح overflow یا سرریز می‌کند. برای رفع این مشکل تمهیداتی اندیشیده شده است که شرح آن در مستندات ذکر شده آمده است. به کمک دو دستور ls و stat می‌توانیم برخی از اطلاعات inode را ببینیم. مثلا بیایید خروجی زیر را برای یکی از عکس‌های خراب من ببینیم: mx@playground[21250]:/home/mx/Sync/Everything/DCIM/Camera $ stat 20190928_200752.jpg stat 20190928_200752.jpg File: 20190928_200752.jpg Size: 1828942 Blocks: 3576 IO Block: 4096 regular file Device: 253,0 Inode: 15409043 Links: 1 Access: (0664/-rw-rw-r--) Uid: ( 1100/ mx) Gid: ( 985/ users) Access: 2024-03-01 10:48:49.262525490 +0100 Modify: 2021-10-27 23:04:21.000000000 +0200 Change: 2022-05-14 12:07:15.297708323 +0200 Birth: 2021-11-08 10:43:23.375113903 +0100 mx@playground[21249]:/home/mx/Sync/Everything/DCIM/Camera $ ls -l !$ ls -l 20190928_200752.jpg -rw-rw-r-- 1 mx users 1828942 Oct 27 2021 20190928_200752.jpg از مقایسه‌ی خروجی stat با ls پیداست که ls تاریخ آخرین تغییر را در خروجی‌اش نمایش می‌دهد (که ۲۷ اکتبر ۲۰۲۱ است). همانطور که از مقایسه‌ی نام فایل با خروجی دستوارت بالا پیداست ایرادی که برای عکس‌های ما پیش آمده اینست که به طریقی این متاداده حین کپی فایل‌ها از یک کامپیوتر (لینوکس) به دیگری (لینوکس) و در نهایت موبایل تغییر کرده است (اندروید هم لینوکس است البته در مورد گوشی سامسونگم از نوع فایل‌سیستم مطمئن نیستم). مثلا عکسی که سال ۲۰۱۹ گرفته شده است تغییر کرده است به سال ۲۰۲۱ یا ۲۰۲۳. با جستجو در فروم سینک‌تینگ متوجه شدم که سینک‌تینگ تاریخ آخرین تغییر را سینک می‌کند. بنابراین برنامه اصلاح تاریخ آخرین تغییر است. در حقیقت این مسئله به قدری رایج است که برنامه‌های cp و scp و rsync و غیره فلگی برای حفظ متادیتای فایل مرجع دارند. مثلا در مورد cp نگاهی به `man cp` به ما می‌گوید که `-p` (حرف اول preserve) باعث حفظ تاریخ‌ها و حق‌دسترسی‌های فایل مرجع می‌شود. برگردیم سروقت مشکل اصلی. از جایی که متاداده‌های inode عکس‌های ما خراب شده باید تاریخ را یا از نام فایل بگیریم یا از خود فایل. بسته به نوع فایل ممکن است اطلاعاتی اضافه بر داده‌ی خام نیز داخل آن ذخیره شده باشد که می‌شود نوع دوم متاداده. در مورد عکس‌ها (و اصوات ضبط شده در قالب WAV) این قضیه صادق است و برخی انواع فایل مانند JPG و TIF و WAV و PNG و WEBP می‌توانند متاداده با قالب Exif در خود ذخیره کنند (ممکن است انواع دیگری هم باشد که من از آن بی اطلاعم). برای دیدن متادیتای اگزیف از برنامه‌ای قدیمی به نام [exiftool] کمک می‌گیریم (که به زبان پرل توسط فیل هاروی نوشته شده است). مثلا به کمک دستور زیر می‌توان تمام تاریخ‌های ذخیره شده در یک فایل سالم را دید: $ exiftool -time:all -s 20190307_144822.jpg FileModifyDate : 2019:03:07 14:48:22+01:00 FileAccessDate : 2024:02:28 08:25:22+01:00 FileInodeChangeDate : 2024:02:28 08:25:21+01:00 ModifyDate : 2019:03:07 14:48:22 DateTimeOriginal : 2019:03:07 14:48:22 CreateDate : 2019:03:07 14:48:22 SubSecTime : 0611 SubSecTimeOriginal : 0611 SubSecTimeDigitized : 0611 TimeStamp : 2019:03:07 14:48:22.654+01:00 SubSecCreateDate : 2019:03:07 14:48:22.0611 SubSecDateTimeOriginal : 2019:03:07 14:48:22.0611 SubSecModifyDate : 2019:03:07 14:48:22.0611 می‌بینیم که «تگ»های مختلفی در اطلاعات اگزیف وجود دارد (ممکن است exiftool اطلاعات inode را هم به خروجی اضافه کرده باشد). اگر نام فایل را با تاریخ‌های ذخیره شده مقایسه کنیم هم می‌بینیم که تاریخ‌ها درست هستند (بجز تاریخ آخرین دسترسی و تاریخ آخرین تغییر inode). مثال دیگری از یک فایل خراب (دقت کنید که متادیتای کمتری دارد): $ exiftool -time:all -s 20190928_200752.jpg FileModifyDate : 2021:10:27 23:04:21+02:00 FileAccessDate : 2024:01:21 23:56:30+01:00 FileInodeChangeDate : 2022:05:14 12:07:15+02:00 ModifyDate : 2019:09:28 20:07:52 DateTimeOriginal : 2019:09:28 20:07:52 CreateDate : 2019:09:28 20:07:52 TimeStamp : 2019:09:28 20:07:52.812+02:00 در این مورد مقایسه نام فایل با FileModifyDate نشان می‌دهد تاریخ آخرین تغییر غلط است (من این فایل‌ها را از زمان عکاسی ویرایش نکرده‌ام البته شاید سهوا کپی کرده باشم). از جایی که با بررسی فایل‌های مختلف متوجه شدم مقدار CreateDate صحیح است بیایید به کمک آن اطلاعات inode عکس‌هایمان را تصحیح کنیم. برای بیرون کشیدن CreateDate از فرمت زیر کمک می‌گیریم: $ exiftool -CreateDate -S -d %Y%m%d%H%M.%S -ext jpg 20190307_144822.jpg CreateDate: 201903071448.22 علت فرمت کردن خروجی اینست که ما می‌خواهیم در گام‌های بعدی آن را به خورد touch بدهیم و این فرمتی است که touch قبول می‌کند. `man exiftool` را ببینید. روش کار به کمک دستوارت ساده‌ی یونیکس و همچنین Bash خیلی ساده است: 1. فایل‌های تمام عکس‌ها را لیست می‌کنیم 2. روی هر فایل دستور exiftool را اجرا می‌کنیم و CreateDate آنرا در خروجی چاپ می‌کنیم 3. از خروجی exiftool مقدار دقیق CreateDate را «می‌بریم» 4. به کمک xargs خروجی cut را به خورد touch می‌دهیم که زمان آخرین تغییر را با مقداری که از exiftool گرفتیم جایگزین می‌کند هر چهار مرحله‌ی بالا به هم به کمک `|` به هم وصل شده است. به `|` می‌گویند «پایپ» یا «لوله». یعنی ما دستورات مختلف (پراسس‌های مختلف) را به یک لوله به هم وصل می‌کنیم (لوله‌کشی می‌کنیم!). کار این لوله وصل کردن خروجی استاندارد هر دستور به ورودی استاندارد دستور بعدی است. لوله اختراع [Douglas McIlroy] یونیکس‌کار ۹۲ ساله‌ی آمریکایی است و یکی از معجزات یونیکس به شمار می‌رود. جالب است بدانید او در ۹۲ سالگی هنوز هم در میلینگ لیست [TUHS] فعال است. برای لیست کردن فایل‌ها در یک حلقه از حلقه‌ی for که در Bash فراهم است کمک می‌گیریم. کافیست به `man bash` نگاهی بکنیم و چند صفحه پایین برویم تا برسیم به Compound Commands و کمی پایین‌تر نحوه‌ی نوشتن for را پیدا می‌کنیم: for name [ [ in [ word ... ] ] ; ] do list ; done The list of words following in is expanded, generating a list of items. The variable name is set to each ele‐ ment of this list in turn, and list is executed each time. If the in word is omitted, the for command exe‐ cutes list once for each positional parameter that is set (see PARAMETERS below). The return status is the exit status of the last command that executes. If the expansion of the items following in results in an empty list, no commands are executed, and the return status is 0. در بدنه‌ی یک حلقه‌ی for می‌شود به آن مقدار یا لغتی که در آن لحظه به آن رسیده‌ام با بکارگیری `$name` دسترسی داشت. `name` مقداری است اختیاری که ما انتخاب می‌کنیم. باید دقت داشته باشیم که از دید for هر آنچه در برابرش ظاهر می‌شود چیزی جز یک کلمه نیست. مثلا اگر خروجی دستور ls را به آن بدهیم و نام فایل‌های ما خط فاصله داشته باشد for آنها را چند لغت جدا از هم خواهد دید. این نکته‌ی بسیار مهمی است و برای رفع آن همواره باید متغیرها را درون گیومه قرار داد. بیایید اول یک لیست ساده بنویسیم: $ for i in $(ls *.jpg | head -n3); do echo "$i"; done 20190307_144822.jpg 20190307_144826.jpg 20190313_172658.jpg این دستور عکس‌های ما را لیست می‌کند. ما فقط نام فایل را echo کردیم (دستوری که هر آنچه به آن بدهید در خروجی استاندارد خودش می‌نویسد). نام متغیر را `i` انتخاب کرده‌ایم. علاوه بر آن بجای word مقدار `$(ls *.jpg | head)` گذاشته‌ایم. Bash هر چیزی که درون `$()` بگذاریم همیشه به عنوان یک فرمان اجرا می‌کند و با خروجی آن جایگزین می‌کند. ما دستور `ls *.jpg | head -n3` داخل آن گذاشته‌ایم. ls که فایل‌های منتهی به `.jpg` را لیست می‌کند و خروجی آن را به کمک لوله می‌دهد به `head` که دستور ساده‌ایست که فقط چند خط اول را نگه می‌دارد (چون فایل‌ها زیاد است و برای مثال ما همین کافی بود). جلوتر هم طبق مستندات for دستوری که می‌خواهیم برای هر «لغت» اجرا شود آورده‌ایم که چیزی جز `echo "$i"` نیست. برای فهم بهتر در نظر بگیرید که دستور بالا دقیقا معادل دستور پایین است: $ for in in 20190307_144822.jpg 20190307_144826.jpg 20190313_172658.jpg; do echo $i; done 20190322_200524.jpg 20190322_200524.jpg 20190322_200524.jpg با این مقدمه در مورد for حالا دستور exiftool را در دل آن جای می‌دهیم: $ for i in $(ls *.jpg | head -n3); do exiftool -CreateDate -S -d %Y%m%d%H%M.%S -ext jpg "$i"; done CreateDate: 201903071448.22 CreateDate: 201903071448.26 CreateDate: 201903131726.58 به همین ترتیب ادامه می‌دهیم و اینبار خروجی را می‌بریم تا فقط تاریخ فرمت شده باقی بماند: $ for i in $(ls *.jpg | head -n3); do exiftool -CreateDate -S -d %Y%m%d%H%M.%S -ext jpg "$i" | cut -d ' ' -f 2; done 201903071448.22 201903071448.26 201903131726.58 می‌بینید که دوباره خروجی یک دستور را لوله کرده‌ایم در ورودی دستور بعدی، اینجا دستور `cut`. این دستور هر خط ورودی‌اش را به کمک یک جداکننده (اینجا خط فاصله) قیچی می‌کند و آنها را به چند ستون تبدیل می‌کند که بعد ما می‌توانیم یک ستون خاص را با پارامتر `-f` انتخاب کنیم. حالا نام هر فایل و تاریخ صحیح را داریم، ماند اصلاح فایل‌ها. برای اصلاح فایل از دستور `touch` استفاده می‌کنیم. `man touch` به ما می‌گوید که `touch` می‌توان تاریخ همین لحظه یا یک تاریخ دلخواه را در inode یک فایل بنویسد. تاریخ فرمت‌شده مطابق خواست `touch` را که قبلا ساختیم و نام فایل را هم که در متغیر `i` داریم. منتها از جایی که ما می‌خواهیم فایل‌های مختلفی را با تاریخ‌های مختلف اصلاح کنیم باید دستور touch را برای تک‌تک آنها جداگانه اجرا کنیم. به همین منظور از دستور `xargs` کمک می‌گیریم. xargs برای هر خط که در ورودی استانداردش نوشته بشود یکبار دستوری که به آن در خط فرمان داده‌ایم اجرا می‌کند. حالا فقط می‌ماند اجرای دستور نهایی. منتها برای تست اول یکبار همه را `echo` می‌کنیم: $ for i in $(ls *.jpg | head -n3); do exiftool -CreateDate -S -d %Y%m%d%H%M.%S -ext jpg "$i" | cut -d ' ' -f 2 | xargs echo touc h "$i" -t; done touch 20190307_144822.jpg -t 201903071448.22 touch 20190307_144826.jpg -t 201903071448.26 touch 20190313_172658.jpg -t 201903131726.58 این خروجی دستور نهایی است. که خوب به نظر می‌رسد. فقط کافیست `head -n3` را به همراه `echo`ی قبل از `xargs` حذف کنیم و آن را دوباره اجرا کنیم: $ for i in $(ls *.jpg); do exiftool -CreateDate -S -d %Y%m%d%H%M.%S -ext jpg "$i" | cut -d ' ' -f 2 | xargs touch "$i" -t; done و تمام. در این مقاله‌ی کوتاه نگاهی کردیم به متادیتای فایل‌ها در یونیکس و نیز متادیتای اگزیف موجود در عکس‌ها و به کمک مقادیر صحیح تگ CreateDate اگزیف تاریخ آخرین تغییر فایل‌هایمان را اصلاح کردیم تا برنامه‌های گالری عکس بتوانند عکس‌ها را با توالی صحیح نمایش بدهند. [مستندات کرنل لینوکس برای ext4]: https://www.kernel.org/doc/html/latest/filesystems/ext4/inodes.html?highlight=inode [exiftool]: https://exiftool.org/ [Douglas McIlroy]: https://en.wikipedia.org/wiki/Douglas_McIlroy [TUHS]: https://www.tuhs.org/