"""Permanent deletion shared by the trash API and scheduled retention cleanup.""" from django.db import transaction from .models import DriveItem, UploadSession from .services import descendant_files from .storage_layout import disk_path, file_location def has_active_uploads_in_tree(item): """Do not purge directories while a resumable upload targets a descendant.""" if not item.is_folder: return False pending = [item.pk] while pending: batch, pending = pending[:250], pending[250:] if UploadSession.objects.filter(parent_id__in=batch).exists(): return True pending.extend(DriveItem.objects.filter(parent_id__in=batch, is_folder=True).values_list('pk', flat=True)) return False def delete_trashed_item(item): """Delete the SQL tree, then its tracked files after commit. Untracked files and directories are not erased. Failures resolving a stored path abort before the SQL delete; missing physical files are tolerated. """ if item.trashed_at is None: raise ValueError('L’élément doit être dans la corbeille.') if has_active_uploads_in_tree(item): raise ValueError('Un transfert est encore en cours dans ce dossier.') files = [disk_path(key) for key in descendant_files(item)] # A child may have been trashed separately, into another isolated directory. folders = [] queue = [item.pk] if item.is_folder else [] if item.is_folder: folders.append(disk_path(item.storage_location or file_location(item.space, item.parent, item.name))) while queue: batch, queue = queue[:300], queue[300:] children = list(DriveItem.objects.filter(parent_id__in=batch, is_folder=True).select_related('parent','space')) queue.extend(child.pk for child in children) for child in children: folders.append(disk_path(child.storage_location or file_location(child.space, child.parent, child.name))) with transaction.atomic(): item.delete() def remove_files(): for path in files: path.unlink(missing_ok=True) for folder in sorted(set(folders), key=lambda p:len(p.parts), reverse=True): if folder.is_dir() and not folder.is_symlink(): # Never recursively erase untracked files. try: folder.rmdir() except OSError: pass # Remove empty UUID trash containers (never the shared .trash root). trash_root = disk_path('.gooddrive-trash') for path in [*files, *folders]: parent = path.parent while parent != trash_root and parent.is_relative_to(trash_root): try: parent.rmdir() except OSError: break parent = parent.parent transaction.on_commit(remove_files)