שחזור קבצים שנמחקו בטעות עם rm -rf
ביצעתם rm -rf בטעות? הפסיקו מיד כל כתיבה לדיסק כדי למנוע דריסת נתונים. מדריך מעשי לשחזור קבצים במערכות ext4, כולל שלבי ביצוע remount לקריאה בלבד ושימוש ב-rescue mode.
מה לעשות בשישים השניות הראשונות
שני גורמים קובעים אם תצליחו לשחזר קבצים שנמחקו עם rm -rf, ושניהם מתרחשים עוד לפני שתפתחו מנוע חיפוש. הפסיקו לכתוב למערכת הקבצים הזו. לאחר מכן, הוציאו אותה משימוש על ידי ביצוע unmount או על ידי טעינה מחדש (remount) במצב לקריאה בלבד (read-only).
rm אינו מוחק דבר. הוא מסיר את רשומת הספרייה, ולאחר מכן מסמן את ה-inode ואת בלוקי הנתונים של הקובץ כפנויים. הבייטים עדיין נמצאים על ההתקן. הם יישארו שם עד שמקצה הבלוקים יקצה את הבלוקים הללו למשימה אחרת, ואותה משימה תכתוב עליהם. בכל שנייה שמערכת הקבצים נשארת טעונה ופעילה, daemon כלשהו כותב שורת לוג או מסד נתונים מבצע flush לדף, וכל כתיבה כזו עלולה לנחות על הבלוקים שאתם מנסים להציל.
לכן, הפקודות הראשונות הן אלו שעוצרות כתיבה, ולא אלו שמשחזרות קבצים.
sudo systemctl stop nginx postgresql
sudo umount /mnt/dataאם umount משיב umount: /mnt/data: target is busy., מצאו מה מחזיק את מערכת הקבצים פתוחה.
sudo fuser -vm /mnt/data
sudo lsof +D /mnt/dataאם אינכם מצליחים לשחרר אותה, בצעו remount במצב לקריאה בלבד. טעינה לקריאה בלבד עוצרת הקצאות חדשות, וזהו רוב מה שאתם צריכים.
sudo mount -o remount,ro /mnt/dataאם הנתיב שנמחק היה על מערכת הקבצים הראשית (root), המשימה קשה יותר. sudo mount -o remount,ro / בדרך כלל ייכשל עם mount: /: cannot remount /dev/vda1 read-only., כיוון שתהליכים רצים מחזיקים קבצים פתוחים לכתיבה וה-kernel לא יכפה את סגירתם. ב-VPS, הפתרון המעשי הוא מצב ה-rescue או ה-recovery של ספק השירות: הוא מעלה מערכת live נפרדת כשהדיסק שלכם מחובר אך לא טעון. כל פקודה להלן תרוץ אז מול התקן שאף אחד לא כותב אליו.
כלל אחד חל על כל המדריך הזה. לעולם אל תכתבו קבצים משוחזרים, image של דיסק, או כלי שהותקן זה עתה על מערכת הקבצים שממנה אתם מבצעים את השחזור. חברו כונן שני, או שלחו את הפלט למכונה אחרת באמצעות SSH.
מדוע שחזור לאחר rm -rf ב-ext4 הוא לרוב חסר סיכוי
תאמו ציפיות לפני התקנת רכיבים כלשהם. ודאו באיזו מערכת קבצים אתם משתמשים:
lsblk -fב-ext4, שהיא ברירת המחדל כמעט בכל תמונת VPS, מיקום הנתונים של קובץ נשמר ב-inode שלו כעץ extent. כל extent הוא רשומה המציינת שבלוק לוגי N של הקובץ מתחיל בבלוק פיזי M ונמשך לאורך L בלוקים. קבצים קטנים שומרים עד ארבע רשומות כאלו בתוך ה-inode עצמו. קבצים גדולים יותר מצביעים על בלוקים נוספים המכילים את שאר העץ.
כאשר הקישור האחרון לקובץ מוסר, ext4 עוברת על העץ, מחזירה כל extent למקצה הבלוקים (block allocator) ומנקה את העץ מה-inode. ה-inode מסומן כפנוי ומקבל חותמת זמן של מחיקה. הנתונים עצמם נותרים ללא שינוי. הרישום היחיד למיקום הנתונים הללו נמחק.
זהו ההבדל מ-ext3, שבה inode שנמחק שמר מספיק מידע כדי שכלי כמו ext3grep יוכל לעקוב אחריו. עדיין ניתן להציג רשימת inodes שנמחקו ב-ext4:
sudo debugfs -R lsdel /dev/vdb1debugfs פותח את ההתקן במצב קריאה בלבד אלא אם מעבירים את הדגל -w, לכן הפעולה בטוחה בהתקן שאינו מותקן (unmounted) ואין עלות לנסות. ה-inodes יוצגו ברשימה. הניסיון לשלוף את התוכן שלהם הוא המקום שבו התהליך נעצר, כיוון שמפת הבלוקים שה-inode החזיק נוקתה, ולכן ל-dump אין לאן להמשיך.
שני כלים מנסים לעקוף זאת באמצעות קריאת ה-journal. ה-journal הוא טבעת בגודל קבוע ש-ext4 משתמשת בה כדי לשמור על עקביות מטא-דאטה במקרה של קריסה, והוא עשוי להכיל עותק ישן של ה-inode מלפני המחיקה. extundelete ו-ext4magic מחפשים בו. בדקו באיזה גודל אתם עובדים:
sudo dumpe2fs -h /dev/vdb1 | grep -i journalה-journal מכיל מטא-דאטה בלבד והוא קטן, לכן פעולות כתיבה רגילות עוברות דרכו במהירות. בשרת פעיל, חלון הזמן שבו ה-inode שלפני המחיקה עדיין קיים נמדד בדקות. אף אחד מהכלים אינו מתוחזק באופן פעיל, ואף אחד מהם אינו ארוז בכל ההפצות. התייחסו לשניהם כאל ניסיון בעל סיכוי נמוך, הריצו אותם מול התקן שאינו מותקן או מול תמונת דיסק, ואל תופתעו אם הם לא יחזירו דבר.
אם lsblk -f מדווח על xfs, המצב אינו טוב יותר, כיוון שאין אפשרות שחזור נתונים נתמכת גם ב-XFS. סדר האפשרויות להלן אינו משתנה.
האם הקובץ עדיין פתוח בתהליך פעיל?
זוהי דרך השחזור היחידה בדף זה בעלת סיכויי הצלחה גבוהים, וזו הסיבה לכך שאין לבצע restart לשירות שהשתמש בקובץ.
קובץ נמחק באמת רק כאשר שני מונים מגיעים לאפס: מספר רשומות הספרייה המצביעות על ה-inode שלו, ומספר מתארי הקבצים (file descriptors) הפתוחים. rm מביא את המונה הראשון לאפס. אם תהליך כלשהו עדיין מחזיק את הקובץ פתוח, המונה השני אינו אפס, ולכן ה-inode והבלוקים שלו עדיין מוקצים והנתונים עדיין ניתנים לקריאה.
מצאו קבצים פתוחים שמספר הקישורים אליהם ירד לאפס:
sudo lsof +L1+L1 מציג רשימת קבצים פתוחים עם מספר קישורים קטן מ-1. כל התאמה מציגה את התהליך, את מספר מתאר הקובץ, NLINK של 0, ונתיב שמסתיים ב-(deleted). קחו את ה-PID ואת מספר המתאר כדי לגשת ל-/proc:
sudo ls -l /proc/1234/fdרשומה נראית כמו 3 -> /var/log/app/events.log (deleted). קישור זה עדיין מגיע לנתונים. העתיקו אותם למערכת קבצים אחרת:
sudo cp /proc/1234/fd/3 /mnt/rescue/events.logהשתמשו ב-cp, לא ב-mv. פתיחת /proc/1234/fd/3 מעניקה לכם נקודת אחיזה חדשה על אותו inode שמתחילה ב-offset אפס, כך שתקבלו את הקובץ המלא במקום את החלק שאחרי המיקום הנוכחי של הכותב.
ישנן שתי מגבלות שכדאי להכיר. עץ ספריות שנמחק לא חוזר בדרך זו, כיוון שרק הקבצים הבודדים שהיו פתוחים בתהליך נשמרים. כמו כן, קובץ מסד נתונים שהועתק בזמן שהמנוע נמצא באמצע כתיבה הוא עותק במצב crash-consistent, לכן תכננו להריץ עליו את כלי השחזור של המנוע עצמו במקום להתייחס אליו כאל קובץ תקין. רשומות ש-lsof מציג עם mem במקום מספר מתאר הן ממופות זיכרון (memory mapped), ולאלו אין רשומת /proc/<pid>/fd שניתן להעתיק ממנה.
האם יש ברשותך snapshot ב-btrfs, ZFS או LVM?
אם מערכת הקבצים מבצעת snapshots, הקבצים שנמחקו כבר נמצאים בתוך אחד מהם, ללא שינוי. זה עוזר רק אם ה-snapshot היה קיים לפני המחיקה. שום דבר שתצור כעת לא יחזיר את המצב לאחור.
btrfs שומרת snapshots כ-subvolumes:
sudo btrfs subvolume list /עיין ב-snapshot והעתק את הנתיבים הדרושים לך באמצעות cp -a. עדיף להעתיק נתיבים בודדים מאשר לבצע rollback ל-subvolume שלם, כיוון ש-rollback מוחק גם את כל מה שנכתב מאז שה-snapshot נוצר.
ZFS חושפת כל snapshot כספרייה לקריאה בלבד:
zfs list -t snapshot
ls /tank/data/.zfs/snapshot/הספרייה .zfs היא מוסתרת ולא תופיע ב-ls רגיל של שורש ה-dataset, אך ניתן להיכנס אליה לפי שם. העתק משם את הקבצים. הפקודה zfs rollback מחזירה את ה-dataset כולו לאחור ומשמידה כל snapshot חדש יותר מזה שתציין, לכן השתמש בה כמוצא אחרון בלבד.
snapshots של LVM הם כרכים מסוג copy-on-write בעלי גודל קבוע:
sudo lvs
sudo mount -o ro /dev/vg0/data-snap /mnt/snapעגן (mount) את הכרך לקריאה בלבד והעתק את הקבצים. בדוק את lvs לפני שתסתמך עליו, כיוון ש-snapshot של LVM שממלא את השטח שהוקצה לו מוכרז כלא תקין על ידי ה-kernel, וברגע שזה קורה, תוכנו אובד.
snapshot אינו גיבוי. הוא נמצא על אותו דיסק או באותו pool כמו המקור, ולכן הוא חולק כל כשל שיש למקור. הוא כלי מצוין לביטול טעות שקרתה לפני שתי דקות, וזה בדיוק התפקיד הנדרש כאן.
שחזור נתונים באמצעות PhotoRec, מול קובץ image בלבד ולעולם לא מול הדיסק החי
אם אף אחת מהשיטות לעיל אינה רלוונטית, האפשרות האחרונה היא carving: סריקת ההתקן ברמת ה-raw לאיתור רצפי בתים המציינים התחלה של סוג קובץ מוכר, ולאחר מכן כתיבת כל מה שבא אחריהם. פעולת ה-carving קוראת נתוני קבצים בלבד. שמות קבצים, מבנה ספריות, חותמות זמן ובעלות הם כולם metadata של מערכת הקבצים, וזהו ה-metadata ש-rm השמיד, לכן שום דבר מזה לא ישוחזר. אתם תקבלו קבצים בשם f0384512.jpg בתוך ספריית פלט ממוספרת, ותצטרכו למיין אותם ידנית.
שני כללים קובעים אם תהליך זה יצליח.
ראשית, צרו image של ההתקן לפני שתפעילו עליו כל כלי אחר. ב-Debian וב-Ubuntu החבילה היא gddrescue והקובץ הבינארי שהיא מתקינה הוא ddrescue.
sudo apt update && sudo apt install -y gddrescue testdisk
sudo ddrescue -n /dev/vdb1 /mnt/rescue/vdb1.img /mnt/rescue/vdb1.map/mnt/rescue חייב להיות על התקן אחר, עם שטח פנוי לפחות בגודל המחיצה. lsblk -b מציג את הגדלים המדויקים בבתים. קובץ ה-map מאפשר להמשיך העתקה שנקטעה במקום להתחיל מהתחלה. ברגע שיש לכם את ה-image, תוכלו לנסות כלי שני על אותם בתים בדיוק, דבר שלא ניתן לעשות אם הכלי הראשון דרס את הדיסק.
שנית, כוונו את כלי השחזור אל קובץ ה-image.
sudo photorec /d /mnt/rescue/recup /mnt/rescue/vdb1.imgphotorec פותח תפריט טקסטואלי. בחרו את המחיצה, לאחר מכן את סוג מערכת הקבצים, את חתימות הקבצים לחיפוש, ולבסוף את ספריית היעד. צמצמו את רשימת החתימות לסוגי הקבצים שאיבדתם בפועל לפני שתתחילו, כיוון שרשימת ברירת המחדל מוצאת הכול ומייצרת עשרות אלפי מקטעים שתצטרכו למיין.
testdisk, מאותה חבילה, כולל פונקציית undelete משלו, והוא תומך ב-FAT, exFAT, NTFS ו-ext2 בלבד. ב-ext4 האפשרות שנותרה היא photorec.
צפו לכך שקבצים מקוטעים ישוחזרו פגומים. ה-carving מניח שהבלוקים של הקובץ רציפים, לכן קובץ שה-allocator פיצל על פני הדיסק ישוחזר בצורה שגויה או שלא יזוהה כלל. קובצי מדיה משתחזרים בצורה סבירה כיוון שיש להם headers חזקים. טקסט פשוט, קובצי תצורה וקוד מקור משתחזרים בצורה גרועה, כיוון שאין חתימת בתים שמציינת התחלה של shell script.
הרווח התועה: כיצד נמחק הנתיב השגוי
כמעט כל תאונה מסוג rm -rf היא בעיה של ה-shell. הפקודה rm מקבלת רשימה של נתיבים ומוחקת כל אחד מהם בתורו. היא לעולם לא "רואה" למה התכוונת.
המקרה הקלאסי הוא רווח בודד:
rm -rf /home/deploy/app /old
rm -rf /home/deploy/app/oldהשורה הראשונה כוללת שני ארגומנטים. היא מוחקת את היישום, ולאחר מכן היא מוחקת את /old. אם /old אינו קיים, rm לא מדפיס דבר, כיוון ש--f מדכא את השגיאה על קובץ חסר. שתיקה אינה אישור.
הצורה השנייה היא משתנה ללא גרשיים המכיל רווח:
dir="/srv/my app"
rm -rf $dirה-shell מפצל את הערך לפי רווחים, ולכן rm מקבל את /srv/my ואת app כשני נתיבים נפרדים. כתיבה בתור rm -rf "$dir" הופכת זאת לנתיב אחד.
הצורה השלישית היא משתנה ריק, בדרך כלל בגלל שהפקודה שהייתה אמורה למלא אותו נכשלה:
rm -rf "$TARGET"/*כאשר TARGET אינו מוגדר, הביטוי מתרחב ל-rm -rf /*. הגרסה של GNU ל-rm מסרבת לצורה החשופה: rm -rf / מדפיס rm: it is dangerous to operate recursively on '/' ועוצר. צורת ה-glob אינה נהנית מהגנה כזו, כיוון שה-shell מחליף את /* ברשימה של נתיבים אמיתיים ברמת השורש לפני ש-rm בכלל רץ, ו-/ אינו אחד מהם, לכן מנגנון ההגנה לעולם לא מופעל.
הרגלים שמונעים את התקלה הבאה
- הקפידו על גרשיים סביב כל משתנה המשמש כנתיב. כתבו
"$dir"בכל פעם, כולל בתוך בדיקות ולולאות. - עצרו במקרה של משתנה ריק.
rm -rf "${TARGET:?TARGET is not set}"/*גורם ל-shell לעצור עם הודעה מטעמכם לפני ש-rmמתחיל, בכל פעם ש-TARGETאינו מוגדר או ריק. הציבו אתset -euo pipefailבראש כל סקריפט שמבצע מחיקה. - הוסיפו את
--one-file-system. דגל זה מורה ל-rmלדלג על כל ספרייה הנמצאת במערכת קבצים שונה מזו של הארגומנט שסיפקתם, כך שמחיקה רקורסיבית לא תוכל לזלוג לתוך כונן גיבוי מחובר או bind mount. - אל תמחקו כ-root. חשבון שירות יכול להשמיד רק את מה שבבעלותו, וזהו כל הטיעון בעד הרצת כל שירות כמשתמש לא מורשה נפרד. אם אינכם בטוחים מה חשבון מסוים יכול להשיג, קריאת סיביות ההרשאות בפלט של ls תענה על כך בפקודה אחת.
- הדפיסו את הרשימה לפני שאתם פועלים לפיה. בתוך סקריפט, בנו את הנתיבים, בצעו להם
printf '%s\n', קראו את הפלט, ורק אז מחקו במעבר שני. - שמרו פקודת אשפה בהישג יד.
sudo apt install trash-cliמספקת לכם אתtrash-put,trash-list,trash-restoreו-trash-empty. קבצים שנמחקו עוברים ל-~/.local/share/Trash, ו-trash-empty 30מנקה כל פריט ישן יותר משלושים יום.
יצירת alias ל-rm בתור trash-put נשמעת כמו הצעד הבא המתבקש, אך זו מלכודת. ה-alias מקבע רפלקס שנכשל בשרת הבא שבו הוא לא קיים, ו-aliases אינם חלים בתוך סקריפטים, ששם מתרחשות הטעויות היקרות. הקלידו trash-put במכוון.
השחזור היחיד שתמיד עובד
כל מה שצוין לעיל הוא בגדר סיכוי. גיבוי אינו סיכוי.
שני דברים הופכים גיבוי לאמיתי. הוא רץ לפי לוח זמנים בלי שתצטרך לזכור זאת, וביצעת ממנו שחזור לפחות פעם אחת. מאגר שאיש מעולם לא שחזר ממנו הוא בגדר אמונה בלבד, כיוון שהגורמים שהופכים אותו ללא שמיש (נתיב שגוי ברשימת ההכללה, או סיסמת מאגר שאף אחד לא רשם) מתגלים רק ביום שבו אתה זקוק לו.
עם restic, שחזור מתבצע בשתי פקודות.
restic -r /srv/restic-repo snapshots
restic -r /srv/restic-repo restore latest --target /mnt/rescue --include /srv/appdataבצע שחזור לתוך תיקייה ריקה במקום לדרוס את הנתיב החי, כך שתוכל להשוות בין השניים לפני שדבר מה עובר למקומו. הגדרת גיבויי restic ב-VPS מכסה את הגדרת המאגר ואת ה-systemd timer שמריץ אותו.
עם Borg:
borg list /srv/borg-repo
borg extract /srv/borg-repo::daily-2026-08-08 srv/appdataנתיבים בתוך ארכיון Borg נשמרים ללא הלוכסן המוביל, לכן srv/appdata תואם ו-/srv/appdata אינו תואם דבר. borg extract כותב לתוך ספריית העבודה הנוכחית, לכן cd לתיקיית עבודה זמנית תחילה.
אם טרם בחרת ביניהם, השוואה בין restic ל-Borg מכסה דה-דופליקציה ומאגרים מסוג append-only, שהיא התכונה המונעת משרת שנפרץ למחוק את היסטוריית הגיבויים של עצמו. כל אחד מהכלים הללו מצוין. התשובה השגויה היא לא להריץ אף אחד מהם.
שרת חדש הוא הרגע הזול ביותר להגדיר זאת, לפני שיש עליו משהו ששווה להפסיד. עשר הדקות הראשונות ב-VPS חדש הוא המקום שבו עבודה זו צריכה להתבצע, לצד הגדרות ה-SSH וה-firewall.
לאחר מכן, הוסף אירוע חוזר ביומן שלך: שחזר ספרייה אחת מהמאגר לתוך /tmp בכל חודש וקרא את הקבצים. הרגל בודד זה שווה יותר מכל הכלים שבדף זה.
FAQ
האם ניתן לשחזר קובץ שנמחק ב-ext4?
בדרך כלל לא. כאשר הקישור האחרון לקובץ מוסר, ext4 מוחק את עץ ה-extent מה-inode, כך ששום דבר על הדיסק לא מתעד היכן המידע היה מאוחסן. extundelete ו-ext4magic מחפשים ב-journal של ext4 עותק ישן של ה-inode, מה שעוזר רק אם המחיקה התרחשה לפני דקות ספורות ומערכת הקבצים נותרה שקטה מאז. אף אחד מהפרויקטים הללו אינו מתוחזק באופן פעיל. יש להריץ אותם על התקן שאינו מחובר (unmounted) או על image של דיסק, לעולם לא על מערכת קבצים פעילה, ויש לבדוק תחילה מול מה אתם עובדים באמצעות sudo dumpe2fs -h /dev/vdb1 | grep -i journal.
שירות עדיין מחזיק קובץ מחוק פתוח. האם ניתן להחזיר אותו?
כן, וזהו התרחיש הטוב ביותר. כל עוד תהליך מחזיק קובץ פתוח, ה-inode ובלוקי המידע שלו נשארים מוקצים, ולכן המידע עדיין קריא. אל תפעילו מחדש את השירות, כיוון שסגירת ה-descriptor האחרון משלימה את המחיקה. הריצו את sudo lsof +L1 כדי להציג קבצים פתוחים עם מונה קישורים של 0, רשמו את ה-PID ואת מספר ה-file descriptor, ולאחר מכן בצעו העתקה דרך /proc עם sudo cp /proc/1234/fd/3 /mnt/rescue/events.log. כתבו את העותק למערכת קבצים אחרת. רשומות המוצגות עם mem במקום מספר descriptor הן ממופות לזיכרון (memory mapped) ואין להן נתיב /proc/<pid>/fd להעתקה.
מדוע כדאי ליצור image לדיסק במקום להריץ את כלי השחזור עליו ישירות?
מכיוון שכל כלי חייב לכתוב את הפלט שלו למקום כלשהו, וכתיבה למערכת הקבצים שאתם משחזרים עלולה לדרוס את הבלוקים הפנויים שעדיין מכילים את המידע שלכם. העתיקו תחילה את המחיצה להתקן אחר בעזרת sudo ddrescue -n /dev/vdb1 /mnt/rescue/vdb1.img /mnt/rescue/vdb1.map, ולאחר מכן כוונו את photorec לקובץ ה-image. ה-image מאפשר לכם גם לנסות כלי שני מאוחר יותר על אותם בתים בדיוק, דבר שאינו אפשרי ברגע שמשהו דרס את המקור.
האם הפקודה rm -rf / עדיין משמידה מערכת Linux?
הפקודה כשלעצמה לא. ה-rm של GNU מסרב לבצע זאת ומדפיס rm: it is dangerous to operate recursively on '/'. הצורות המסוכנות הן אלו שמגיעות בדרך אחרת. rm -rf "$TARGET"/* כאשר TARGET אינו מוגדר מתרחב ל-rm -rf /*, וה-shell מעביר ל-rm רשימה של ספריות שורש אמיתיות, שאף אחת מהן אינה /, ולכן מנגנון ההגנה לעולם לא מופעל. כתבו "${TARGET:?TARGET is not set}" במקום זאת, וה-shell יעצור לפני ש-rm ירוץ.
האם snapshot של מערכת קבצים הוא גיבוי?
לא. snapshot של btrfs או ZFS נמצא על אותו pool שבו נמצא המידע שהוא מגן עליו, כך שדיסק תקול או pool שנהרס לוקחים את שניהם יחד. ל-snapshot של LVM יש בעיה נוספת של גודל קבוע: ברגע שהוא מתמלא, ה-kernel מבטל אותו ותוכנו אובד. snapshots מצוינים לביטול מחיקה שבוצעה לפני שתי דקות. לכל מטרה אחרת, שמרו מאגר גיבוי על חומרה נפרדת.