Data Recovery Case Studies and Technical Write-Ups
Real recovery workflows, tools, and lessons from failed hard drives, damaged optical media, and other data loss cases.
Weak-Head Seagate Drive Recovery
Controlled HDDSuperClone imaging, DMDE filesystem recovery, and EXIF-based sorting of carved photos on an unstable 1TB drive.
Unreadable CD Batch Recovery
Recovering data from 23 damaged CDs using multiple optical drives, ddrescue mapfiles, and DMDE.
MacWrite II Floppy Recovery + Custom Carver
Imaging 1990s Mac floppies with ddrescue, then writing a custom file carver to extract MacWrite II manuscripts that no modern recovery tool could read.
Parsing Classic Mac HFS Trees by Hand
When DMDE and The Sleuth Kit refused to read these 1990s Mac floppies, we wrote a parser for the HFS catalog and extent B-trees to rebuild the real file tree — folders, names, and fragmented files included.
The Drive That Died Every 8 Seconds
A 3TB Seagate that locked up 8 seconds after going idle — too fast to type a command. We built a USB-relay SATA power switch and an imager that arms itself before the drive appears, then recovered 99.997% of 2.67TB over roughly 900 automated power cycles.
Recovering a Weak-Head Seagate Drive with Controlled HDDSuperClone Imaging
This is a technical write-up of a real recovery workflow used on an unstable 1TB Seagate drive that would reset under sustained reads. The goal was to safely extract photos for a photographer while minimizing drive stress.
The drive would not show up in Windows on the Device Manager, and required using a SATA PCIe card to be able to hot swap the drive in after BIOS since it would lock up BIOS during hardware checks.
The drive would show up in output of lsblk and thus we took the following steps to recover data for our customer.
Tools used: Linux, smartctl, hdparm, HDDSuperClone, DMDE, PhotoRec, ExifTool.
Symptoms and initial triage
The drive presented as readable for short operations (SMART and small reads), but would become unstable under sustained sequential reads.
ddrescue would stall, and imaging attempts with HDDSuperClone would trigger firmware resets and capacity/identify glitches. The error message read Source drive reports wrong size / size changed. No new clicking sounds were observed.
Quick verification commands
$ sudo smartctl -a /dev/sdX
$ sudo hdparm -I /dev/sdX
$ sudo dd if=/dev/sdX of=/dev/null bs=512 count=1
A key indicator was SMART responsiveness: after stressful imaging sessions, smartctl became slow, but returned to fast responses
after a longer cool-down. This strongly suggested thermal / sustained-load sensitivity consistent with a weak head (not a dead motor, not obvious
mechanical crash, and not immediate firmware lockup).
Why sustained imaging was failing
In this case, the drive tolerated only a limited amount of continuous reading before entering heavy internal retries and eventually resetting. The practical takeaway: avoid long, continuous reads. Use controlled reads of small sections of the drive and allow true rest between runs.
Controlled micro-burst imaging with HDDsuperclone
HDDSuperClone was used to image in small, controlled segments. The critical technique was to increase segment size gradually, only when the drive remained stable and SMART stayed responsive.
Segment sizing approach
Start conservative and increase in measured steps. Example working sizes observed during this recovery:
- 512000 sectors (~250MB)
- 640000 sectors (~320MB)
- 768000 sectors (~384MB)
If hesitation increases, SMART slows, or resets occur, drop back to the last stable size.
Operational pattern used between runs
After each successful segment, the drive was placed into standby to stop spindle rotation and park heads cleanly. This provided real rest compared to simply waiting while the drive continued spinning.
$ sudo hdparm -Y /dev/sdX
$ sudo hdparm -C /dev/sdX
The workflow was: run a segment, stop, disconnect the device handle, issue standby (hdparm -Y), rest 10–15 minutes, then resume.
After several larger segments, take a longer cool-down.
Recovering from the image with DMDE (filesystem first)
Everything from here works on the image file, never the drive. Once the micro-burst process had produced the best image it could, the physical disk was powered down and set aside — there was no reason to risk another read.
The right first move on an NTFS drive is not carving — it's reading the filesystem. When the file table is intact, a filesystem-aware tool recovers files with their original names, folder structure, and timestamps, and reassembles fragmented files correctly. A signature carver can't do any of that. We loaded the image into DMDE and worked from the partition structure:
- Open the image in DMDE (Open Disk → the
.imgfile) and let it detect the partitions. - Open the NTFS volume and run a scan so DMDE rebuilds the directory tree, including deleted entries the MFT still references.
- Recover the wanted files and folders to a separate healthy destination drive, preserving the original structure.
This is the part that mattered to the customer: the bulk of their photos came back already organized into the folders they originally used, with real filenames — no reconstruction required. Only after this pass did we look at carving, and only for what the filesystem could no longer account for.
Carving the remainder with PhotoRec (deleted / unreferenced photos)
Carving is a last resort, not a starting point — but it earns its place after the filesystem pass. Some photos had been deleted long ago, or sat in regions the damaged metadata no longer pointed to, so DMDE couldn't list them. To recover those, we carved the unallocated space of the same image with PhotoRec. It's a supplement to the DMDE results, not a replacement:
$ sudo photorec badimage.img
In the PhotoRec UI, choose File Opt, press s to disable all, then enable only the photo types you care about
(JPEG/JPG and optionally PNG/RAW). This reduces junk output and speeds up the carve.
PhotoRec filenames: f######## vs t########
PhotoRec assigns generated filenames. Two common patterns:
f########.jpg– a carved file recovered by signature scant########.jpg– commonly thumbnails / small previews (often safe to separate)
If you want to isolate thumbnails for review later:
$ mkdir -p thumbs
$ mv t*.jpg thumbs/ 2>/dev/null || trueSorting recovered photos for a photographer (EXIF-based)
The carved photos from the previous step come out as anonymous f########.jpg files with no names or folders. The DMDE-recovered set already
kept its structure, so this step applies only to the carved leftovers: sorting them by EXIF date is the fastest way to fold them back into the timeline.
Install ExifTool:
$ sudo apt install exiftoolWe can check the contents of the metadata to see what value we want to use to sort, the best value was the Created Time value:
$ exiftool f6121520.jpg The following script was created to sort recovered photos by date based on the exif data:
#!/usr/bin/env python3
import argparse
import json
import os
import shutil
import subprocess
from pathlib import Path
from typing import Dict, List
def find_recup_dirs(src_root: Path) -> List[Path]:
if not src_root.is_dir():
raise FileNotFoundError(f"Source root not found or not a directory: {src_root}")
return sorted([p for p in src_root.iterdir() if p.is_dir() and p.name.startswith("recup")])
def iter_files_under(dirs: List[Path]) -> List[Path]:
files: List[Path] = []
for d in dirs:
for p in d.rglob("*"):
if p.is_file():
files.append(p)
return files
def chunked(lst: List[Path], n: int) -> List[List[Path]]:
return [lst[i : i + n] for i in range(0, len(lst), n)]
def exiftool_create_month(files: List[Path]) -> Dict[str, str]:
"""
Returns mapping: SourceFile -> "YYYY-MM"
Missing CreateDate will simply not exist in mapping.
Uses exiftool date formatting to output YYYY-MM directly.
"""
if not files:
return {}
cmd = ["exiftool", "-json", "-CreateDate", "-d", "%Y-%m"] + [str(f) for f in files]
try:
proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
except FileNotFoundError:
raise RuntimeError("exiftool not found. Install it first (e.g., sudo apt install libimage-exiftool-perl).")
if proc.returncode != 0 and not proc.stdout.strip():
raise RuntimeError(f"exiftool failed:\n{proc.stderr.strip()}")
try:
data = json.loads(proc.stdout) if proc.stdout.strip() else []
except json.JSONDecodeError as e:
raise RuntimeError(f"Failed to parse exiftool JSON output: {e}\nStderr:\n{proc.stderr.strip()}")
out: Dict[str, str] = {}
for item in data:
src = item.get("SourceFile")
month = item.get("CreateDate") # already formatted like YYYY-MM due to -d
if src and month and isinstance(month, str) and len(month) == 7 and month[4] == "-":
out[src] = month
return out
def safe_dest_path(dest_dir: Path, filename: str) -> Path:
dest_dir.mkdir(parents=True, exist_ok=True)
base = Path(filename).stem
ext = Path(filename).suffix
candidate = dest_dir / (base + ext)
if not candidate.exists():
return candidate
i = 1
while True:
candidate = dest_dir / f"{base}_{i}{ext}"
if not candidate.exists():
return candidate
i += 1
def copy_file(src: Path, dest_dir: Path, dry_run: bool) -> Path:
dest = safe_dest_path(dest_dir, src.name)
if dry_run:
return dest
shutil.copy2(src, dest)
return dest
def move_file(src: Path, dest_dir: Path, dry_run: bool) -> Path:
dest = safe_dest_path(dest_dir, src.name)
if dry_run:
return dest
dest_dir.mkdir(parents=True, exist_ok=True)
shutil.move(str(src), str(dest))
return dest
def main():
ap = argparse.ArgumentParser(description="Sort PhotoRec recovered files into YYYY-MM folders using ExifTool CreateDate.")
ap.add_argument("--src-root", default="/mnt/samsung/photorec_out", help="Root that contains recup* directories.")
ap.add_argument("--dest-root", default="/mnt/samsung/recoveredphotos", help="Destination base directory.")
ap.add_argument("--chunk-size", type=int, default=500, help="How many files to send to exiftool per batch.")
ap.add_argument("--dry-run", action="store_true", help="Print what would happen, but don't copy/move anything.")
args = ap.parse_args()
src_root = Path(args.src_root)
dest_root = Path(args.dest_root)
thumbs_dir = dest_root / "thumbnails"
misc_dir = dest_root / "Misc"
recup_dirs = find_recup_dirs(src_root)
if not recup_dirs:
print(f"No recup* directories found under: {src_root}")
return
all_files = iter_files_under(recup_dirs)
if not all_files:
print(f"No files found under recup* directories in: {src_root}")
return
# Separate thumbnails (filename starts with 't')
thumbs = [p for p in all_files if p.name.startswith("t")]
normal = [p for p in all_files if not p.name.startswith("t")]
print(f"Found recup dirs: {len(recup_dirs)}")
print(f"Total files: {len(all_files)} | thumbnails (move): {len(thumbs)} | normal (copy): {len(normal)}")
if args.dry_run:
print("DRY RUN enabled: no changes will be made.\n")
# Move thumbnails
moved_thumbs = 0
for p in thumbs:
dest = move_file(p, thumbs_dir, args.dry_run)
moved_thumbs += 1
if moved_thumbs <= 10 or moved_thumbs % 500 == 0:
print(f"[THUMB] {p} -> {dest}")
if moved_thumbs > 10:
print(f"[THUMB] ... moved {moved_thumbs} thumbnails total")
# Copy normal files based on CreateDate -> YYYY-MM
copied = 0
to_misc = 0
for batch in chunked(normal, args.chunk_size):
month_map = exiftool_create_month(batch)
for p in batch:
month = month_map.get(str(p))
target_dir = (dest_root / month) if month else misc_dir
dest = copy_file(p, target_dir, args.dry_run)
copied += 1
if not month:
to_misc += 1
if copied <= 10 or copied % 1000 == 0:
label = month if month else "Misc"
print(f"[COPY:{label}] {p} -> {dest}")
if copied > 10:
print(f"[COPY] ... copied {copied} files total")
print("\nDone.")
print(f"Thumbnails moved: {moved_thumbs} -> {thumbs_dir}")
print(f"Normal copied: {copied} -> {dest_root}")
print(f"Missing CreateDate (copied to Misc): {to_misc} -> {misc_dir}")
if __name__ == "__main__":
main()
We use the chmod command to make the script executable, test it with the dry run parameter, and after validation we can run this to sort the photos by exif data datetime.
$ chmod +x sort_recup_photos.py
$ sudo ./sort_recup_photos.py --dry-run
$ sudo ./sort_recup_photos.py
Takeaways
- Weak-head drives can be readable in short bursts while failing under sustained reads.
- SMART responsiveness is a practical indicator of fatigue; longer cool-downs can restore stability.
- Using
hdparm -Ybetween runs provides real rest without hard power pulls. - Recover from the image, never the drive — and read the filesystem first (DMDE) so files keep their names, folders, and fragmentation; carve only the unallocated remainder.
- For carved photos that have no metadata, EXIF-date sorting quickly restores usable organization.
Recovering Data from 23 Unreadable CDs Using Multi-Drive ddrescue and DMDE
This is a real-world workflow used to recover data from a large batch of CDs that would not read in standard environments. The customer had 23 discs containing photos and files accumulated over years, most of which failed to open or would hang during access.
Instead of treating each disc as a full imaging job, we built a workflow that prioritized quick wins, reduced unnecessary stress on marginal media, and used multiple optical drives to maximize read success.
Tools used: Linux, ddrescue, DMDE, multiple SATA and USB optical drives.
Initial Triage with DMDE
Before committing to full imaging, each CD was tested in DMDE to quickly determine whether files were accessible without heavy read operations.
cd Downloads/dmde
sudo ./dmdeThis step allowed us to:
- Identify discs that could be recovered immediately
- Avoid unnecessary ddrescue runs on readable media
- Reduce wear on already fragile discs
A small number of discs were successfully recovered at this stage, eliminating hours of imaging work.
Building a Multi-Drive Recovery Setup
Optical drives vary significantly in how they handle damaged or degraded discs. Instead of relying on a single drive, we created an “assembly line” using multiple drives simultaneously.
- Older SATA DVD-RW drives (2008–2013 era)
- USB DVD drives
- USB Blu-ray drives
Each drive had different read tolerances. Some would fail instantly on a disc that another drive could partially read.
Devices appeared as:
/dev/sr0
/dev/sr1
/dev/sr2
/dev/sr3This allowed us to rotate discs between drives without losing progress.
Imaging Workflow with ddrescue
Each disc was labeled physically and matched to an image file and log file inside a customer directory:
mkdir ~/mike
cd ~/mikeInitial imaging always started with a no-retry pass to quickly capture readable sectors:
sudo ddrescue -b 2048 -n /dev/sr0 ddcd24.img ddcd24.logIf the drive stalled or slowed significantly, the same job was resumed on another drive using the same mapfile:
sudo ddrescue -b 2048 -n /dev/sr1 ddcd24.img ddcd24.log
sudo ddrescue -b 2048 -n /dev/sr2 ddcd24.img ddcd24.logThis is a key technique. ddrescue’s mapfile allows seamless continuation across different drives, effectively combining their strengths.
Final pass on best-performing drive
Once the majority of readable sectors were captured, the best-performing drive was selected for retry attempts:
sudo ddrescue -b 2048 -r 3 /dev/sr3 ddcd24.img ddcd24.logLimiting retries is important. CDs degrade quickly under repeated reads, and excessive retries can make things worse.
Why Multiple Drives Matter
One of the biggest takeaways from this job is how inconsistent optical drives are.
- Some drives read through scratches better
- Some handle dye degradation better
- Some fail quickly but others recover slowly
Rotating drives is often the difference between partial recovery and near-complete recovery.
Extracting Files with DMDE
Once imaging completed, DMDE was used to scan raw images and extract files.
In many cases, filesystem metadata was incomplete or damaged, so raw scanning was required.
JPEG files were recovered and organized into folders that matched the original disc labels written by the customer years ago.
This small step makes a big difference for usability. Instead of a flat dump of files, the customer received structured data that reflected how they originally organized their discs.
Final Delivery
All recovered files were consolidated and transferred to a USB drive for the customer.
- Recovered images organized by disc
- Readable files separated from partial or corrupted files
- Delivered on a single accessible device
Out of 23 discs, the majority yielded recoverable data, including photos that had not been accessible for years.
Key Takeaways
- Always triage first. Not every disc needs full imaging
- Use ddrescue mapfiles to continue across multiple drives
- Older optical drives can outperform newer ones on damaged media
- Limit retries to avoid further degradation
- Organizing output improves customer experience significantly
If you have old CDs that won’t read, especially photo archives, there is often still recoverable data even when they appear completely dead.
Recovering MacWrite II Manuscripts from 1990s Mac Floppies with ddrescue and a Custom File Carver
A local Minnesota author brought us a shoebox of old Macintosh floppy disks holding decades of unpublished writing — essays, drafts, and full manuscripts. The work had been saved in MacWrite II, a word processor discontinued in the 1990s, and nothing on a modern computer could open the files. This is a technical write-up of how we imaged the disks, why every off-the-shelf carving tool failed, and how we built a custom file carver to pull the documents back out.
To protect the customer’s privacy, the author is not named and none of the recovered writing is reproduced here. The technical details below use only file structure, byte signatures, and our own tooling.
Tools used: Linux, GNU ddrescue, file/strings/hexdump, binwalk, foremost, PhotoRec, LibreOffice (libmwaw), and a custom Python carver.
The media and the problem
The source media was a set of 3.5" Mac-formatted (HFS) floppies read through a USB floppy drive. Two challenges stacked on top of each other:
- The diskettes were 25–30 years old, so some sectors were weak or unreadable and needed careful imaging before anything else.
- Even after imaging, the documents were in MacWrite II format. On classic Mac OS, a file’s type was stored in filesystem metadata (the resource fork / Finder type & creator codes), not in the data itself, so these files have no extension and modern apps don’t recognize them.
The plan was therefore two stages: first make safe, complete images of each disk, then solve the file format separately against those images.
Stage 1 — Imaging the floppies with ddrescue
Aging floppies should be read as few times as possible. We used GNU ddrescue, which captures all the easy sectors first and uses a mapfile (log file) so later passes only revisit the bad spots instead of re-reading the whole disk. Each disk got its own image and mapfile so progress was never lost.
First (fast) pass — grab everything readable
The first pass uses -n (no-scrape) to skip the slow sector-scraping phase and capture the bulk of the disk quickly, and -d for direct
device access so we see true read errors rather than cached results. Floppies use a 512-byte sector, set with -b 512.
$ mkdir ~/floppy && cd ~/floppy
$ sudo ddrescue -d -n -b 512 -v /dev/sdd image1.img log1.logSecond (retry) pass — work the bad sectors
The second pass reuses the same mapfile and adds limited retries with -r3 (retry bad sectors three times). Because the mapfile already records
what succeeded, ddrescue only re-attempts the sectors that previously failed.
$ sudo ddrescue -d -r3 -b 512 -v /dev/sdd image1.img log1.log
That two-pass pattern was repeated for every disk in the box (image1.img through image9.img, each with its own logN.log
mapfile). Keeping a separate mapfile per disk means any disk can be re-imaged or resumed later without touching the others.
One stubborn disk
Most disks imaged cleanly. One refused to read on the first floppy drive, so we moved it to a second drive (it enumerated as /dev/sdc) and pushed
the retry count higher:
$ sudo ddrescue -d -n -b 512 -v /dev/sdc image5.img log5.log
$ sudo ddrescue -d -r7 -b 512 -v /dev/sdc image5.img log5.logThat disk yielded nothing — it was physically dead/blank and produced an empty image. The honest outcome: of nine diskettes, eight contained recoverable data and one did not. Swapping to a second drive is the same trick that pays off with optical media — different hardware has different read tolerances.
Stage 2 — Why standard recovery tools came up empty
With the images in hand, we ran the usual identification and carving tools against them. First, basic inspection:
$ file image1.img
$ hexdump -C image1.img | head -50
$ strings image1.img | less
$ binwalk image1.img
strings proved there was real text on the disks — readable sentences scrolled by — so the data was there. But the structured carvers found nothing:
$ foremost -i image1.img -o foremost_1
$ photorec image1.imgBoth came back with no documents. The reason is fundamental to how carvers work. Tools like foremost, scalpel, and PhotoRec identify files using either a header + footer signature or a fixed/embedded length. MacWrite II documents have a recognizable header, but:
- there is no end-of-file marker (no footer to match), and
- there is no length field anywhere in the header.
With no footer and no size, a generic carver has no way to know where each document ends, so it skips the format entirely. To recover these, we had to learn the format ourselves and write a purpose-built carver.
Reverse-engineering the MacWrite II format
We had a lucky advantage: the customer also had a folder of intact MacWrite II files from the same era, which gave us 233 known-good samples to study. Three properties held across every single one.
1. A confirmed type, and a strong header signature
The classic Mac type/creator codes — type MW2D, creator MWII — confirmed these were MacWrite II documents. In the raw data fork, every
file began with the same 16-byte signature:
00 2E 00 2E 00 04 00 00 00 48 00 48 00 00 00 002. Lengths are always a multiple of 256 bytes
No sample ever broke this rule. That single fact is the lever that makes clean carving possible without a length field.
3. The importer tolerates trailing slack
LibreOffice still ships libmwaw, an import filter that understands MacWrite II. Crucially, it parses the document’s internal structure and ignores
any extra bytes after the real content — so a carve that grabs a little too much still opens perfectly.
The carving algorithm
Putting those three facts together produced a simple, safe strategy:
- Scan the image for every occurrence of the 16-byte header.
- Carve from each header to the start of the next header (or end of image), capped at a sane maximum.
- Round the carved length down to a 256-byte boundary and trim trailing zero padding.
Because a real file’s length is always a multiple of 256 and is never larger than the gap to the next file, rounding down can never cut into real content; it only trims slack. Any extra bytes that do get included are harmless, since libmwaw stops at the true end of the document. The core of it:
# 16-byte signature shared by every MacWrite II sample
MAGIC = bytes.fromhex("002e002e000400000048004800000000")
ALIGN = 256 # every real file length is a multiple of this
def find_headers(data):
offs, pos = [], 0
while True:
i = data.find(MAGIC, pos)
if i < 0:
break
offs.append(i)
pos = i + 1
return offs
def carve_one(data, start, end, max_size):
end = min(end, start + max_size, len(data))
blob = data[start:end]
n = (len(blob) // ALIGN) * ALIGN # round down to 256
return blob[:n].rstrip(b"\x00") or blob[:ALIGN]This assumes each document is stored contiguously on the disk, which is the normal case on floppies and small HFS volumes.
The complete carver, including the conversion and Mac-Roman fallback described below, is open source on GitHub: Champlin-Guys-Data-Recovery-Scripts / macwrite_carver.
Two-tier recovery: formatting first, words always
For each carved blob the tool tries two methods, in order, so we always get the maximum out of every file:
- .docx via LibreOffice/libmwaw — full text and original formatting. This is the preferred result.
- .txt via direct Mac-Roman extraction — a fallback for files libmwaw can’t fully decode (an older format variant, or a fragmented file). It decodes the data fork as Mac-Roman, so curly quotes and accented characters survive, and keeps only the runs that look like real prose.
# headless conversion with LibreOffice's MacWrite filter
$ soffice --headless --convert-to docx:"MS Word 2007 XML" \
--outdir out/ carved_document.mw2
The carver runs that conversion automatically, checks whether the resulting document actually contains text, and if not, writes the recovered plain text instead.
Every carved document ends up as either a formatted .docx or, at worst, a readable .txt — nothing is silently dropped.
Proving it actually works
Before trusting it on the real disks, we validated the boundary logic against ground truth. We built a synthetic disk image out of five known-good documents (ranging from 2 KB to 647 KB), deliberately surrounded by leading junk, random zero-padding slack, and a block of unrelated data wedged between two of them — the kinds of conditions a carver has to survive. The carver pulled all five back out, and the recovered text matched the originals byte-for-byte.
Results on the customer’s disks: across the eight readable floppies, the carver recovered 56 MacWrite II documents — 34 with full
formatting as .docx, and 22 as recovered text where the importer couldn’t decode the variant. Several were complete manuscripts hundreds of thousands of
characters long. Net unrecoverable documents: zero.
Takeaways
- Image fragile media first with ddrescue and a per-disk mapfile; do a fast
-npass, then a limited-rretry pass. - When a disk won’t read, try a second drive before giving up — read tolerances vary between hardware.
- “No carver supports it” usually means the format lacks a footer or length field — not that the data is gone.
- A handful of known-good samples can reveal the one invariant (here, the 256-byte alignment) that makes safe carving possible.
- Always validate a custom carver against ground truth before trusting it on irreplaceable originals.
Have old floppies, Zip disks, or files in a format nothing will open anymore? Even when standard tools say “unsupported,” the data is often still recoverable with the right approach. We’re local to Champlin, Minnesota and happy to take a look.
Parsing Classic Mac HFS Directory and Extent Trees to Recover Files DMDE and The Sleuth Kit Can't Read
This is a follow-up to our MacWrite II floppy recovery case. The carver pulled the words back out of those 1990s Mac disks, but as anonymous blobs — no filenames, no folder structure, and a known weak spot on fragmented files. To recover the actual file tree the author had organized their manuscripts into, we went a layer deeper and parsed the classic Mac HFS filesystem itself: its Master Directory Block, Catalog B-tree, and Extents-Overflow B-tree.
As before, to protect the customer's privacy the author is not named and none of the recovered writing is reproduced here. Everything below is filesystem structure and our own tooling.
Tools used: Linux, The Sleuth Kit, the Linux hfs kernel module, hexdump, and a custom Python HFS parser (open-sourced below).
Where the carver left off
Signature carving recovers a file's bytes but knows nothing about the filesystem, so every document came out named something like
image2_off0007a400.mw2, flattened into one folder, with fragmented files at risk of coming out incomplete. The author, though, had filed
decades of work into a real hierarchy — draft folders, version folders, a working set versus a submission set. That organization is itself recovered
information worth keeping. The natural next step was to read the disk the way a Mac of the era did: through its directory.
Why DMDE and The Sleuth Kit came up empty
In plain English: every disk format stamps a little label near its front saying what kind it is — like the edition notice on a book's
copyright page. Recovery tools read that label first and bail out if they don't recognize it. These floppies use an old Apple format (its label
reads BD), and the modern tools are only built for the newer Apple format (label H+). So they took one look at the old
label and said "not my format" — even though the files were sitting right there. We just had to write something that reads the old label.
These floppies are formatted with classic HFS — the Hierarchical File System Apple shipped before HFS+. Pointed at the raw images, the usual filesystem tools wouldn't list a tree at all. The Sleuth Kit is explicit about why:
$ fsstat -f hfs image1.img
Invalid magic value (HFS file systems (other than wrappers
HFS+/HFSX file systems) are not supported)
The Sleuth Kit only implements HFS+/HFSX; DMDE likewise didn't recognize the structures and showed no file list. The reasons are simple once you look at
the disk: a bare floppy has no partition map, and a classic-HFS volume's signature is BD (0x4244) at byte
offset 1024 — not the H+ that HFS+ tools look for.
$ xxd -s 1024 -l 16 image1.img
00000400: 4244 a0c2 6797 b92f 3f41 0100 0007 0003 BD..g../?A......
The Linux hfs kernel module can mount classic HFS read-only, but it needs root and is unforgiving of the damaged, partially-imaged
disks in this batch. So we wrote a small parser that reads the on-disk structures directly and tolerates missing sectors.
How an HFS volume is laid out
In plain English: think of the disk like a book. The very front has a short title page (it names the disk and points to where the table of contents lives). Then there's the table of contents — the list of every file and folder, what it's called, and which pages hold it. And because some files got split across non-adjacent pages, there's a small "continued on page…" index for the leftovers. Read those three things in order and you can find every file on the disk. The rest of this section is just naming those three parts the way HFS does.
Classic HFS keeps its entire directory in two special files — B-trees — whose locations are recorded in a header near the front of the volume:
- Boot blocks (the first two 512-byte sectors).
- The Master Directory Block (MDB) at sector 2: volume name, allocation-block size, file/folder counts, and the extent records that say where the two special files live.
- The Catalog file — a B-tree of every folder and file: names, parent IDs, type/creator codes, and fork sizes.
- The Extents-Overflow file — a B-tree that records extra fragments for any file too chopped-up to describe in the catalog alone.
A file's contents are stored in extents: runs of contiguous allocation blocks listed as (start block, block count). The catalog record holds the first three extents inline; if a file is more fragmented than that, the rest live in the Extents-Overflow B-tree. Recover the file tree, and then follow those extents, and you have the files.
Step 1 — Read the Master Directory Block
The MDB gives the allocation-block geometry and, at the end, the extent records for the Catalog and Extents-Overflow files. Reading one of the customer's disks revealed a detail that matters: the Catalog file was split into two extents — blocks 22–43 and again far away at blocks 2507–2528. A reader that only honors the first extent would silently read half a directory. That fragmentation is a plausible reason off-the-shelf tools stumble here.
Volume name : 'LEFT COAST'
Alloc blocks: 2874 x 512B
Catalog : 22528 bytes, extents [(22, 22), (2507, 22)] <- fragmented
Extents file: 11264 bytes, extents [(0, 22)]With the block size (512 bytes here) and the first-block offset from the MDB, an allocation block number converts straight to a byte offset in the image, and the special files can be reassembled by concatenating their extents in order.
Step 2 — Walk the Catalog B-tree
In plain English: a "B-tree" is just a way of keeping a big list sorted and quick to search — think of an old library card catalog or the tabbed dividers in a filing cabinet, where a top drawer tells you which lower drawer to open. HFS uses one to track every file and folder. "Walking" it simply means reading the cards in order, one after another, until you've seen them all: read a card, note the filename and where its data lives, move to the next card, repeat. The whole directory on one of these floppies was only about forty of those little cards. The one quirk is that each drawer lists its cards in an index at the back, written in reverse — so you peek at the back of the drawer to find where each card starts. Mechanical, just unfamiliar.
An HFS B-tree is a series of fixed-size nodes (512 bytes on these disks). Node 0 is a header that points to the chain of leaf nodes, and the leaves hold the actual records in order. Each node ends with a small table of offsets — stored back-to-front — that marks where each record begins, so walking a node means reading that table and slicing out the records:
# records are located by an offset table at the END of the node,
# written in reverse: the last 2 bytes point at record 0, etc.
def node_records(node):
nrecs = int.from_bytes(node[10:12], "big")
ns = len(node)
recs = []
for i in range(nrecs):
start = int.from_bytes(node[ns-2*(i+1):ns-2*(i+1)+2], "big")
end = int.from_bytes(node[ns-2*(i+2):ns-2*(i+2)+2], "big")
recs.append(node[start:end])
return recsEach catalog record is a key plus a data payload. The key carries the parent folder's ID and the item's name; the payload says whether it's a folder or a file and, for files, the Mac type/creator codes and the data + resource fork extents. Collecting every leaf record and linking children to parents by ID rebuilds the entire tree, names and all:
$ python3 hfsrec.py image2.img list
Volume name : 'west coast'
...
ODYSSEY/ [dir cnid=17 valence=8]
left coast preview [MW2D/MWII] data=500736 rsrc=0 cnid=22 *fragmented*
nw sequence-y! [MW2D/MWII] data=96000 rsrc=0 cnid=23 *fragmented*
w.c. misc/ [dir cnid=28 valence=20]
chop draft [MW2D/MWII] data=249088 rsrc=0 cnid=32
notes-- border to xxx [MW2D/MWII] data=199424 rsrc=0 cnid=41The recovered file and folder counts matched each volume's own header tally exactly — a built-in sanity check that the walk was complete.
Step 3 — Follow the extents, including the overflow tree
In plain English: an "extent" is just one unbroken chunk of a file. A small file is one chunk; but as a disk fills up, the system tucks new files into whatever gaps are free, so a big file can end up scattered into several chunks — like a long document photocopied onto pages that got filed across different drawers. The table of contents lists the first three chunks of each file right next to its name; if a file has more than three, the extra locations are written in that separate "continued on page…" list. Miss that second list and you get a file that's cut short. Reading both lists is how we put scattered files back together completely — and it's exactly the step that simpler tools skip.
Reading a file means walking its extent list and concatenating those allocation blocks. The catch is the heavily-fragmented files: the catalog only holds
three extents inline, and if that doesn't cover the file's length, the remaining fragments are in the Extents-Overflow B-tree, keyed by file ID. The
left coast preview document above is a clean example — its three inline extents stopped short of its real size, and the missing fourth fragment
was waiting in the overflow tree:
data extents (from catalog): [(100, 932), (2694, 20), (2754, 9)] = 491,776 bytes
logical size: 500,736 bytes
-> short by 8,960 bytes, so consult the extents-overflow B-tree:
full extent list: [(100, 932), (2694, 20), (2754, 9), (2856, 17)]
reassembled size: 500,736 bytes (exact)That fourth fragment is exactly what a first-extent-only reader — or a contiguous-assumption carver — would drop. Following the overflow tree puts the file back together to the byte.
Paying off against the format work
With a real file tree in hand, the MacWrite conversion got better in two concrete ways. First, we could select documents by their catalog
type code (MW2D) instead of scanning for a byte signature — which caught a MacWrite II header variant that began
FF FF FF FC rather than the 00 2E 00 2E the carver keyed on. A pure signature scan had been skipping those files entirely.
Second, fragmented documents now arrived whole, so they converted cleanly instead of truncating. Each document was handed to LibreOffice's
libmwaw filter for a formatted .docx, with a Mac-Roman text fallback — and now landed in a folder tree that mirrored how the
author had filed it.
Being honest about the truncated disks
A few of the floppies imaged only partially — one came in at 786 KB and another at 721 KB against the full 1.44 MB. The parser is built to tolerate this: reads past the end of an image return zero-fill plus a warning rather than crashing, so everything that is present comes out. But it also tells the truth about what isn't. Several files' allocation blocks sat entirely beyond the point where imaging stopped, so their data was simply not in the image — confirmed by checking that the reassembled forks were 100% zeros. Those are genuinely unrecoverable from these partial images; the only way to get them is a fresh, more complete read of the physical disk.
Result: across the eight readable floppies the parser rebuilt the complete folder tree for every fully-imaged volume, with file and folder
counts matching each volume header. Combined with the format conversion, the author got their manuscripts back as named, foldered .docx and
.txt files — not anonymous blobs.
Takeaways
- "Unsupported filesystem" often means a tool only handles the modern variant. Classic HFS (signature
BD, no partition map) is well documented and parseable by hand when HFS+ tools refuse it. - An HFS volume's whole directory lives in two B-trees named by the Master Directory Block — read those and you have the file tree.
- Always follow all the extents. The catalog holds three inline; the rest are in the Extents-Overflow tree, and that's exactly where fragmented files lose data to naive readers.
- Recovering the directory makes everything downstream better: real filenames, real folders, type-code selection, and complete fragmented files.
- When an image is truncated, verify recovered data is non-zero before claiming success — and say plainly when a file simply isn't in the image.
The HFS catalog/extent parser and the catalog-driven MacWrite converter are open source on GitHub: Champlin-Guys-Data-Recovery-Scripts / hfs_recover.
Have old Mac floppies, Zip disks, or drives in a filesystem nothing will mount anymore? Even when modern tools say "unsupported," the directory is usually still there to be read. We're local to Champlin, Minnesota and happy to take a look.
The Drive That Died Every 8 Seconds: Building a SATA Power Switch to Recover 2.67TB
A customer brought in a 3TB Seagate ST3000DM001 that no software could image. The drive read perfectly — full speed, no bad sectors, no clicking — and then locked up roughly 8 seconds after I/O stopped. Every recovery tool we tried lost the drive before it could finish starting.
Getting the data off required building a hardware power switch for the SATA rail, a controller for it, and an imager that arms itself before the drive appears on the bus. The result: 4,719 of 4,726 files recovered — 2,671.9 GB of 2,672.0 GB, or 99.997% — across roughly 900 automated power cycles.
Up front, because it shapes everything below: a professional lab with firmware tooling could have addressed the underlying fault directly. The customer had a firm budget and no interest in a four-figure lab referral, which made the real choice "try something creative or return the drive unrecovered." More on that below.
To protect the customer's privacy, no filenames, folder names, or file contents appear anywhere in this write-up. Everything below is drive behavior and our own tooling.
Tools used: Linux, ddrescue, ddrescuelog, smartctl, a DCTTech USB HID relay board, and two scripts we wrote for this job.
Symptoms: a drive that fails when you leave it alone
Most failing drives get worse the harder you push them. This one was the opposite, and that inversion is what made it interesting.
On connection the drive enumerated normally and reported its full 3TB. Reads ran at 110–130 MB/s with zero errors. Then, consistently, it stopped answering. The kernel log showed the drive going busy and libata working through its reset ladder:
ata6: link is slow to respond, please be patient (ready=0)
ata6.00: qc timeout after 5000 msecs (cmd 0xec)
ata6.00: failed to IDENTIFY (I/O error, err_mask=0x4)
ata6.00: revalidation failed (errno=-5)
ata6: limiting SATA link speed to 3.0 GbpsThose messages look like a dying drive, but they are the consequence of the lockup, not evidence of mechanical failure. SMART told a different story: zero reallocated sectors at the time of triage, no spin retries, no reported uncorrectable errors, and a normal temperature. The motor and heads were fine. The firmware was hanging.
Timing the failure
We measured the window across several attaches. Idle, the drive lasted about 8 seconds — repeatable to within a fraction of a second across three separate connections. That number never moved.
The critical detail: the 8-second countdown restarts on every successful read, not on every read attempt. A drive that is being read continuously stays alive. A drive sitting idle — or one whose reads have started failing — does not.
So we tested the opposite of the usual approach and read the drive continuously instead of gently. Under sustained sequential reads it survived 128 seconds — sixteen times longer than idle. Later measurements under load put the range at roughly 100–170 seconds.
Worth being precise about one thing: this was sustained reading, never writing. We never write to a customer's drive. A bad read is recoverable; a bad write is not. Everything described here is read-only against the source, with all output going to a separate destination disk.
Why ordinary imaging could not work
Two consequences fall out of an 8-second idle window, and together they rule out every normal workflow.
You cannot type fast enough
By the time a drive enumerates, you notice it, and you type a ddrescue command with the right source, destination, mapfile, and flags, the 8 seconds are long gone. The imager has to already be running and waiting before the drive exists.
A slow patch kills the drive on its own
Because the countdown resets on successful reads, any region that reads slowly starves the timer and the drive locks up — with no bad sector involved. The drive can kill itself on perfectly good media simply by reading it too slowly.
Recovering from a lockup requires physical power removal
Once the drive latched, no software reset brought it back reliably. A bus reset was not enough; the power rail had to drop. On a single-pass recovery that means a human unplugging and replugging a SATA power connector. We estimated this job would need several hundred cycles. Doing that by hand was not realistic, and every manual replug risks a static discharge or a bent connector on a drive we only get one shot at.
The recovery therefore needed three things that don't come in a box: a way to cut and restore drive power under software control, a way to start imaging within milliseconds of the drive appearing, and a loop that resumes exactly where the last cycle stopped without re-reading anything.
Why not just send it to a lab?
This is the fair question, and it deserves a straight answer.
A BSY lockup like this is a firmware fault, and firmware faults have a proper tool. Professional lab equipment — PC-3000, DFL, and similar — can talk to a drive's service area directly, work around a hung firmware module, and in many cases simply stop the drive from latching in the first place. On a job like this that hardware would have been faster and gentler than what we did. If you own that equipment and your first reaction to this write-up is "I'd have fixed the firmware," you're right, and we'd have made the same call in your shoes.
What that tooling also is, is expensive — both to buy and to charge for. Sending a drive to a lab that has it typically means a four-figure quote, and the customer here had a clear budget ceiling and told us plainly they were not willing to spend that. They weren't interested in a lab referral. The realistic choice was not "our approach versus a PC-3000." It was our approach versus the customer walking away with nothing.
Being told "we can't afford the proper route, do what you can" is not a constraint we resent — it's what gave us the room to try something unorthodox on a drive that had no better option. If the attempt had failed, the customer was no worse off than the day they walked in. It didn't fail, and that's the only reason this write-up exists.
We'd also say this plainly: what follows is not a replacement for proper lab tooling, and we don't present it as one. It's what you can build when the drive's fault happens to be one that a power cycle clears, the customer can't fund the better path, and the alternative is losing the data entirely. Different job, different fault, different answer — some drives genuinely do need the service area rewritten, and no amount of clever scripting substitutes for that.
Building an automated SATA power switch
The hardware is a cheap USB HID relay board — the DCTTech USBRelay family, USB ID 16c0:05df, and the many clones of it. It presents as
a plain HID device with no vendor driver, so Linux exposes it through hidraw with no special setup:
$ lsusb
Bus 001 Device 005: ID 16c0:05df Van Ooijen Technische Informatica HID device
$ cat /sys/class/hidraw/hidraw5/device/uevent | grep HID_NAME
HID_NAME=www.dcttech.com USBRelay2The drive's SATA power lead runs through one relay contact. Energizing the coil opens the circuit and the drive goes dead; de-energizing restores power. That gives software control over the one thing that reliably clears the firmware lockup.
Talking to the board without vendor software
The manufacturer ships a Windows library built on hid.dll and setupapi.dll. None of that exists on a Linux recovery
bench, so we wrote relayctl.py against the kernel's hidraw interface instead. The board's wire protocol is simple and
well documented by several open-source projects — 8-byte HID feature reports:
FF nn relay nn ON FD nn relay nn OFF
FE 00 all relays ON FC 00 all relays OFF
GET_FEATURE returns: bytes 0-4 = module ID, byte 7 = relay state bitmask
relayctl.py is deliberately stdlib-only — no hidapi, no pip packages. It runs as root next to ddrescue during multi-hour
unattended jobs, and a dependency that breaks after a distro upgrade would take the whole imaging loop down with it.
$ ./relayctl.py list
/dev/hidraw5 (hidraw5) www.dcttech.com USBRelay2
id='BITFT' state=0x00 1=off 2=off 3=off
$ ./relayctl.py on 1 # cut power to the patient
$ ./relayctl.py off 1 # restore powerProving the relay actually cuts power
This step matters more than it sounds. A drive that latches and then recovers on its own looks exactly like a drive you successfully power-cycled. Watching the device node disappear proves nothing, because a locked-up drive drops off the bus too.
The test that cannot be fooled is a sustained absence: a drive with no power cannot come back, so if it stays gone for 90 seconds
the relay is genuinely switching it. We confirmed it a second way on a healthy test drive, using the drive's own firmware counters — SMART's
Power_Cycle_Count, Power-Off_Retract_Count, and Start_Stop_Count each rose by exactly 8 over an 8-cycle
soak. That is the drive itself confirming it lost power.
We tested the whole rig on a scrap drive before it ever touched the customer's. Thirteen power cycles, zero failures, and two timings that shaped everything after: about 13 seconds from power-on to a readable device, and about 12 seconds for the kernel to drop the node after power is cut.
That second number is a trap worth naming. Cutting SATA power is instant, but Linux does not remove the device node until its link-loss handling completes. Restore power before that finishes and the stale node is still sitting there — so an imager watching for the drive to "appear" matches immediately and starts reading hardware that is still spinning up. The off-period has to outlast the node removal, not just the firmware lockup.
An imager that arms before the drive exists
The second script, arm_and_image.sh, is the other half. Rather than being launched at a drive, it watches /sys/block for
a device whose model string matches the patient, and fires ddrescue within about 50 milliseconds of it appearing. Then it loops: cut power, wait,
restore, catch the drive, read until it stops, repeat.
ddrescue's mapfile is what makes the loop safe. Every cycle resumes exactly where the previous one ended and never re-reads a sector already recovered, so hundreds of interruptions cost nothing but time.
sudo ./arm_and_image.sh <case> ST3000DM001 <image-name> \
--domain domain-priority.map \
--relay-channel 1 --relay-normally-closed \
--max-run-secs 60 --min-read-rate offSafety checks that run every single cycle
Because device letters move between attaches, the script re-verifies its assumptions on every cycle rather than trusting what was true at startup:
- the source must match the expected model string, read fresh from sysfs
- the source must not be mounted, and must not be the system disk
- the destination must live under the designated recovery volume
- source and destination must not be the same path
- it refuses to start a brand-new image when it expected to resume an existing one
That last check exists because we tripped over it. A mistyped case name sent one run into a fresh empty image in a new folder instead of resuming the real one — and it looked completely normal in the log. Reads succeeded, throughput was fine, the percentage climbed. Only the destination path was wrong. The script now refuses to begin a new image unless explicitly told to.
Lowering the SCSI timeout
One setting is load-bearing and easy to miss. Linux defaults to a 30-second SCSI command timeout, which is far longer than this drive's 8-second window. A single stalled read would block ddrescue for 30 seconds — during which ddrescue's own timeout and rate guards cannot act, because they are only evaluated between reads. Dropping it to 3 seconds makes a hung read fail fast enough for the other guards to work:
echo 3 > /sys/block/$DEV/device/timeoutThis resets to 30 every time the drive re-enumerates, so it has to be written on every cycle rather than once at startup.
Three things the drive taught us mid-recovery
The first version of this loop worked but was slow. Three findings roughly tripled throughput, and each one contradicted an assumption we started with.
A 3-second power cut is not enough
Three seconds cleared the lockup once, on a drive that had already been sitting idle for minutes. From a running state it was not enough — the drive came back half-reset, failed IDENTIFY, and libata walked the link speed down from 6.0 to 3.0 to 1.5 Gbps without it ever answering. Every clean recovery used a 10-second cut. We set the default to the number the evidence supported rather than the best case we had seen.
A guard against stalling was causing the stalls
ddrescue's --min-read-rate marks a slow zone and seeks past it, and we had it enabled specifically to stop the drive starving its own
timer on a slow patch. The logs said otherwise: four of five cycles ended with exactly one slow read, with the drive running at
84–112 MB/s immediately before. Full speed, one slow read, dead.
The seek that skips the slow zone was itself taking longer than the 8-second budget. The guard meant to prevent the lockup had started causing it. Turning it off nearly doubled the data recovered per power cycle:
--min-read-rate=1MiB off
mean read window 27.6 s 42.0 s
data per power cycle 1.36 GB 2.63 GB
effective rate 14.1 MB/s 24.0 MB/sCorrelation alone could not prove which way the causation ran — a drive starting to lock up would also produce a slow read. So we measured it properly: same drive, same domain, back to back, four cycles each.
You cannot signal a process stuck in a kernel read
To stop a cycle cleanly before the drive locked up, we wrapped ddrescue in a timer that sends it an interrupt. It frequently did nothing. When a read is issued to a drive that stops answering, the process blocks in uninterruptible sleep, where no signal reaches it — not the interrupt, not even SIGKILL. It stays there until the kernel finishes its own reset sequence, which on this drive meant overruns of up to 126 seconds past a 60-second limit.
No ddrescue flag fixes that, because the problem is below ddrescue. The only reliable way to end an uninterruptible read is to make the I/O fail — and we already had a relay wired to the power rail. The imager now watches its own child, and if ddrescue has not exited ~10 seconds past its deadline, it cuts power. The blocked read errors out immediately, the process becomes responsive and exits, and ddrescue flushes its mapfile intact on the way out. Verified by hand first: a ddrescue that had ignored an interrupt for over a minute died instantly when power dropped, and lost no progress.
Reading the most valuable data first
On a drive this fragile, the order you read things in matters. Every power cycle costs an emergency head retract, and there is no guarantee the drive survives to the end of the job — so the goal is to have the most value already banked at any moment you might have to stop.
Rather than imaging the disk end to end, we parsed the NTFS Master File Table out of the image, mapped each file to its extents on disk, and generated ddrescue domain mapfiles that restrict reading to just those regions. That let us image specific folders first, then work outward.
A finding that changed how we sequence these jobs: small files are dramatically cheaper per power cycle than large ones. Bands of files under 500 MB averaged about 6.5 GB recovered per cycle, against roughly 2.6 GB for scattered large files — densely packed small files mean far less seeking inside each window. Working smallest-first maximises both the number of files recovered early and raw throughput.
There was one more thing the image needed before any recovery software could read it. The Master File Table was complete and the boot sector intact, but roughly 0.4 MB of directory index blocks were still missing — so folders enumerated as empty and the file tree appeared to barely exist. Imaging that 0.4 MB took one second, and took the browsable tree from a handful of entries to all 4,734 files. It was by far the highest value-per-second work of the whole job.
Worth knowing if you hit this yourself: the volume's "dirty" flag was also set, which makes many tools refuse to open the filesystem at all until forced. That alone can make a perfectly good image look unreadable.
Results
The finished recovery, measured file by file against the Master File Table rather than by percentage complete:
files on the volume 4,726
fully recovered 4,719 (99.85%)
data recovered 2,671.9 GB of 2,672.0 GB (99.997%)
unreadable media 9,216 bytesOf the seven files not fully recovered, four are NTFS internal journal files that no one needs. The remaining three lost data to genuinely dead sectors: two lost 512 bytes each out of files of 1.77 GB and 2.59 GB and play back perfectly, and one lost 302 KB.
That last file is worth a note, because it is the one case where recovery hit a real limit. All of its damage sits in the opening 0.1% of the file — the container header and index — so 99.9% of the video data is present and intact, but a player cannot parse its way in to reach it. That is a video repair problem now, not an imaging one. No further passes over the drive will change it; those sectors are gone.
We also learned to check whether a gap actually matters before spending drive life on it. Two files with small holes played back cleanly before we did anything about them. The one that genuinely needed help could not be fixed by imaging at all. Testing playback first would have saved cycles on a drive that had a finite number left.
Total cost to the drive: roughly 900 power cycles over about two days. Its healthy read window degraded over that time from a steady 44 seconds to an erratic 10–70 seconds, but it never developed the runaway bad-sector growth that would have ended the job — total unreadable media across 2.67TB came to just over 9 KB.
What we'd tell another technician
- Time the failure before theorising about it. "Dies after 8 seconds idle, survives 128 seconds under continuous reads" is a completely different fault from "dies under load," and it points at firmware rather than mechanics.
- Don't trust a device node as evidence of power state. A locked-up drive disappears too. Prove a power cut with a sustained absence, or with the drive's own SMART power-cycle counters.
- Check whether your safety guards have become the problem. The rate guard we added to prevent lockups was causing them, and only an A/B measurement showed it.
- A process stuck in a kernel read cannot be signalled. If you need to interrupt I/O to a drive that has stopped answering, removing power is the only thing that reliably works.
- Read smallest files first. More files recovered early, and better throughput per power cycle.
- Verify per file, not per percentage. "89% of the disk imaged" sounds worse than it is when the rest is free space, and "100% of the domain" can still hide a file that was never in scope. Check the file list.
- Never write to the source. All of the above is read-only against the customer's drive, with output on a separate disk.
Have a drive that locks up, disappears mid-copy, or that other shops have already called unrecoverable? Behavior like this is often firmware rather than physical damage, and the data is usually still there. We're local to Champlin, Minnesota and happy to take a look.