47 lines
2.0 KiB
Python
47 lines
2.0 KiB
Python
"""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)]
|
||
folder = disk_path(file_location(item.space, item.parent, item.name)) if item.is_folder else None
|
||
with transaction.atomic():
|
||
item.delete()
|
||
def remove_files():
|
||
for path in files:
|
||
path.unlink(missing_ok=True)
|
||
if folder and folder.is_dir() and not folder.is_symlink():
|
||
# Only remove empty directories, never user data untracked by SQL.
|
||
for candidate in sorted(folder.rglob('*'), key=lambda p:len(p.parts), reverse=True):
|
||
if candidate.is_dir() and not candidate.is_symlink():
|
||
try: candidate.rmdir()
|
||
except OSError: pass
|
||
try: folder.rmdir()
|
||
except OSError: pass
|
||
transaction.on_commit(remove_files)
|