diff --git a/melamine/classes.py b/melamine/classes.py index 90fe6f6..b272334 100644 --- a/melamine/classes.py +++ b/melamine/classes.py @@ -60,9 +60,13 @@ class ShredDir(AsyncObject): yield from item.enumerate_mount_points() yield self.mount_point - async def shred(self, hash: bool = False, dryrun: bool = False) -> bool: + async def shred(self, hash: bool = False, dryrun: bool = False, ignoredirs: Set[AsyncPath] = None) -> bool: + if ignoredirs is None: + ignoredirs = set() tasks = [] for item in self.contents: + if any(str(item).startswith(str(path)) for path in ignoredirs): + continue tasks.append(item.shred(hash, dryrun)) try: if all(await asyncio.gather(*tasks)): @@ -105,9 +109,15 @@ class ShredFile(AsyncObject): self.fs_handler = await mount_to_fs_handler(self.mount_point) self.hardlinks = None - async def shred(self, hash: bool = False, dryrun: bool = False) -> Union[bool, bytes]: + async def shred( + self, hash: bool = False, dryrun: bool = False, ignoredirs: Set[AsyncPath] = None + ) -> Union[bool, bytes]: """Shred the file with a single file descriptor.""" + if ignoredirs is None: + ignoredirs = set() try: + if any(str(path).startswith(str(path)) for path in ignoredirs): + return False logger.info(f"Shredding: {self.absolute_path}") async with aiofiles.open(self.absolute_path, "rb+") as file: @@ -161,7 +171,13 @@ class ShredFile(AsyncObject): if self.hardlinks: log_buf = f"[5/4] Unlinking {len(self.hardlinks)} hardlinks" if not dryrun: - tasks = [link.unlink() for link in self.hardlinks if link.name != "." and link.name != ".."] + tasks = [] + for link in self.hardlinks: + if link.name == "." and link.name == "..": + continue + if any(str(await link.absolute()).startswith(str(path)) for path in ignoredirs): + return False + tasks.append(link.unlink()) done, _ = await asyncio.wait(tasks) for task in done: e = task.exception() diff --git a/melamine/shred.py b/melamine/shred.py index 316d04c..0e06b99 100644 --- a/melamine/shred.py +++ b/melamine/shred.py @@ -2,6 +2,8 @@ import argparse import asyncio from collections import defaultdict +from aiopath import AsyncPath + from .classes import get_all_hardlinks from .classes import ShredDir from .classes import ShredFile @@ -30,13 +32,17 @@ async def main(job: argparse.Namespace) -> bool: # Try to delete hardlinks based on the filesystem type job.paths = await get_all_hardlinks(new_paths) + tasks = [path.absolute() for path in job.ignoredir] + tasks.append(AsyncPath("/proc").absolute()) + job.ignoredir = set(await asyncio.gather(*tasks)) + # Shred all physical files including hardlinks for path in job.paths: tasks = [] if isinstance(path, ShredFile): - tasks.append(path.shred(hash=job.exhaustive, dryrun=job.dryrun)) + tasks.append(path.shred(hash=job.exhaustive, dryrun=job.dryrun, ignoredirs=job.ignoredir)) elif isinstance(path, ShredDir): - tasks.append(path.shred(hash=job.exhaustive, dryrun=job.dryrun)) + tasks.append(path.shred(hash=job.exhaustive, dryrun=job.dryrun, ignoredirs=job.ignoredir)) done, _ = await asyncio.wait(tasks) for task in done: e = task.exception() @@ -54,9 +60,9 @@ async def main(job: argparse.Namespace) -> bool: # on an entire filesystem might be a bit burdensome semaphore = asyncio.Semaphore(1024) - async def check_inode_and_unlink(item): + async def check_inode_and_unlink(item, inodes): async with semaphore: - if await item.stat().st_ino in inodes_in_mount_points[item.mount_point]: + if await item.stat().st_ino in inodes: log_buf = f"Deleting hardlink: {item.path}" if not job.dryrun: log_buf = "DRY RUN " + log_buf @@ -67,7 +73,9 @@ async def main(job: argparse.Namespace) -> bool: # checking for . and .. should not be neccessary w/ rglob tasks = [] async for item in mount_point.rglob("*"): - tasks.append(check_inode_and_unlink(item)) + if any(str(item).startswith(path) for path in job.ignoredir): + continue + tasks.append(check_inode_and_unlink(item, inodes)) done, _ = await asyncio.wait(tasks) for task in done: e = task.exception()