test
This commit is contained in:
parent
17afb70810
commit
26e0e05754
@ -137,6 +137,23 @@ def trash_location(item):
|
||||
return f'.gooddrive-trash/{item.pk}/{item.name}'
|
||||
|
||||
|
||||
def cleanup_empty_trash_container(item):
|
||||
"""Remove only this item's empty UUID container after restoration.
|
||||
|
||||
A non-empty directory is deliberately kept: it may contain unrelated data
|
||||
and must never be removed recursively.
|
||||
"""
|
||||
container = disk_path(f'.gooddrive-trash/{item.pk}')
|
||||
try:
|
||||
container.rmdir()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError:
|
||||
# Non-empty or inaccessible: leave it intact. A later trash operation
|
||||
# will refuse to overwrite any content that remains here.
|
||||
pass
|
||||
|
||||
|
||||
def move_to_trash(item, *, migrate_existing=False):
|
||||
"""Atomic disk/SQL move. Preserve the original name/parent for restoration."""
|
||||
from django.utils import timezone
|
||||
@ -155,6 +172,11 @@ def move_to_trash(item, *, migrate_existing=False):
|
||||
raise FileNotFoundError('Élément absent du stockage, déplacement impossible.')
|
||||
if target.exists() or target.is_symlink():
|
||||
raise NameConflict('La destination de la corbeille est déjà utilisée.')
|
||||
# Previous versions left the UUID container behind when restoring a file.
|
||||
# Remove only empty legacy containers so the same item can be trashed again.
|
||||
cleanup_empty_trash_container(item)
|
||||
if target.parent.exists() or target.parent.is_symlink():
|
||||
raise NameConflict('Le dossier de corbeille de cet élément contient encore des données.')
|
||||
target.parent.mkdir(parents=True, exist_ok=False)
|
||||
moved = False
|
||||
try:
|
||||
|
||||
@ -75,6 +75,54 @@ class TrashIsolationTests(TestCase):
|
||||
child.refresh_from_db()
|
||||
self.assertEqual(disk_path(child.storage_location).read_bytes(), b'present')
|
||||
|
||||
def test_file_can_be_trashed_again_after_restore(self):
|
||||
item = self.new_file(name='cycle.txt', content=b'never lose me')
|
||||
container = disk_path(f'.gooddrive-trash/{item.pk}')
|
||||
for _ in range(3):
|
||||
response = self.client.post(f'/api/drive/items/{item.pk}/trash/')
|
||||
self.assertEqual(response.status_code, 200, response.data)
|
||||
item.refresh_from_db()
|
||||
self.assertTrue(container.is_dir())
|
||||
self.assertEqual(disk_path(item.storage_location).read_bytes(), b'never lose me')
|
||||
response = self.client.post(f'/api/drive/items/{item.pk}/restore/')
|
||||
self.assertEqual(response.status_code, 200, response.data)
|
||||
item.refresh_from_db()
|
||||
self.assertIsNone(item.trashed_at)
|
||||
self.assertFalse(container.exists(), 'La restauration doit nettoyer le dossier ID vide')
|
||||
self.assertEqual(disk_path(item.storage_location).read_bytes(), b'never lose me')
|
||||
|
||||
def test_stale_empty_id_folder_from_old_restore_is_reused(self):
|
||||
item = self.new_file(name='existing.txt')
|
||||
container = disk_path(f'.gooddrive-trash/{item.pk}')
|
||||
container.mkdir(parents=True)
|
||||
response = self.client.post(f'/api/drive/items/{item.pk}/trash/')
|
||||
self.assertEqual(response.status_code, 200, response.data)
|
||||
item.refresh_from_db()
|
||||
self.assertEqual(disk_path(item.storage_location).read_bytes(), b'original')
|
||||
|
||||
def test_nonempty_legacy_container_is_never_overwritten(self):
|
||||
item = self.new_file(name='important.txt', content=b'original')
|
||||
container = disk_path(f'.gooddrive-trash/{item.pk}')
|
||||
container.mkdir(parents=True)
|
||||
(container / 'untracked.txt').write_bytes(b'KEEP')
|
||||
response = self.client.post(f'/api/drive/items/{item.pk}/trash/')
|
||||
self.assertEqual(response.status_code, 409)
|
||||
self.assertEqual((container / 'untracked.txt').read_bytes(), b'KEEP')
|
||||
item.refresh_from_db()
|
||||
self.assertIsNone(item.trashed_at)
|
||||
self.assertEqual(disk_path(item.storage_location).read_bytes(), b'original')
|
||||
|
||||
def test_folder_can_be_trashed_again_after_restore(self):
|
||||
response = self.client.post('/api/drive/folders/', {'name': 'Recettes', 'space': self.space.pk}, format='json')
|
||||
self.assertEqual(response.status_code, 201, response.data)
|
||||
item = DriveItem.objects.get(pk=response.data['id'])
|
||||
container = disk_path(f'.gooddrive-trash/{item.pk}')
|
||||
for _ in range(2):
|
||||
self.assertEqual(self.client.post(f'/api/drive/items/{item.pk}/trash/').status_code, 200)
|
||||
self.assertTrue(container.is_dir())
|
||||
self.assertEqual(self.client.post(f'/api/drive/items/{item.pk}/restore/').status_code, 200)
|
||||
self.assertFalse(container.exists())
|
||||
|
||||
def test_migrate_existing_trashed_without_resetting_retention(self):
|
||||
item = self.new_file()
|
||||
when = timezone.now() - timedelta(days=4)
|
||||
|
||||
@ -10,7 +10,7 @@ from .spaces import ensure_space, ensure_private_space, is_admin, resolve_space,
|
||||
from .permissions import has_space_right, item_rights
|
||||
from .quotas import quota_state
|
||||
from .services import error, item_info, get_item, chain, readable, writable, valid_name, target_parent, writable_destination
|
||||
from .storage_layout import assert_available, prepare_parent, relocate_item, move_to_trash, NameConflict
|
||||
from .storage_layout import assert_available, prepare_parent, relocate_item, move_to_trash, cleanup_empty_trash_container, NameConflict
|
||||
from .trash_cleanup import delete_trashed_item, has_active_uploads_in_tree
|
||||
|
||||
|
||||
@ -209,6 +209,9 @@ def restore_item(request,item_id):
|
||||
relocate_item(item, parent, name)
|
||||
item.trashed_at = None
|
||||
item.save(update_fields=['trashed_at','updated_at'])
|
||||
# The restored item is no longer in its .gooddrive-trash/<ID>/ folder.
|
||||
# Leaving that empty directory prevents the next trash operation.
|
||||
cleanup_empty_trash_container(item)
|
||||
except (NameConflict,FileExistsError,OSError,ValueError,ValidationError) as exc:
|
||||
return error(f'Restauration impossible : {exc}',409)
|
||||
return Response(item_info(item,request.user))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user