Skip to content

Commit 29ba4db

Browse files
authored
Merge pull request #615 from jbernal0019/master
Fix issue that prevented deletion of a folder's descendant folders when the raw storage was a POSIX filesystem
2 parents 66a894d + 7e2c929 commit 29ba4db

File tree

4 files changed

+98
-39
lines changed

4 files changed

+98
-39
lines changed

chris_backend/core/models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,17 @@ def _update_public_access(self, public_tf):
337337
ChrisLinkFile.objects.bulk_update(link_files, ['public'])
338338

339339

340+
@receiver(post_delete, sender=ChrisFolder)
341+
def auto_delete_folder_from_storage(sender, instance, **kwargs):
342+
storage_path = instance.path
343+
storage_manager = connect_storage(settings)
344+
try:
345+
if storage_manager.path_exists(storage_path):
346+
storage_manager.delete_path(storage_path)
347+
except Exception as e:
348+
logger.error('Storage error, detail: %s' % str(e))
349+
350+
340351
class ChrisFolderFilter(FilterSet):
341352
path = django_filters.CharFilter(field_name='path')
342353

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
2+
import logging
3+
import os
4+
from unittest import mock
5+
6+
from django.test import TestCase
7+
from django.contrib.auth.models import User
8+
9+
from core.models import ChrisFolder, ChrisFile
10+
11+
12+
class ModelTests(TestCase):
13+
14+
def setUp(self):
15+
# avoid cluttered console output (for instance logging all the http requests)
16+
logging.disable(logging.WARNING)
17+
18+
# superuser chris (owner of root folders)
19+
self.chris_username = 'chris'
20+
21+
# create normal user
22+
self.username = 'foo'
23+
self.password = 'bar'
24+
User.objects.create_user(username=self.username, password=self.password)
25+
26+
def tearDown(self):
27+
# re-enable logging
28+
logging.disable(logging.NOTSET)
29+
30+
31+
class ChrisFileModelTests(ModelTests):
32+
33+
def setUp(self):
34+
super(ChrisFileModelTests, self).setUp()
35+
36+
# create a ChrisFile instance
37+
owner = User.objects.get(username=self.username)
38+
self.upload_path = f'home/{self.username}/uploads/file1.txt'
39+
folder_path = os.path.dirname(self.upload_path)
40+
(parent_folder, _) = ChrisFolder.objects.get_or_create(path=folder_path,
41+
owner=owner)
42+
self.file = ChrisFile(parent_folder=parent_folder, owner=owner)
43+
self.file.fname.name = self.upload_path
44+
self.file.save()
45+
46+
def test_move(self):
47+
"""
48+
Test whether custom 'move' method successfully updates a ChrisFile with
49+
the correct path and parent folder.
50+
"""
51+
new_path = f'home/{self.username}/tests/file1.txt'
52+
53+
storage_manager_mock = mock.Mock()
54+
storage_manager_mock.obj_exists = mock.Mock()
55+
storage_manager_mock.copy_obj = mock.Mock()
56+
storage_manager_mock.delete_obj = mock.Mock()
57+
58+
with mock.patch('core.models.connect_storage') as connect_storage_mock:
59+
connect_storage_mock.return_value=storage_manager_mock
60+
self.file.move(new_path)
61+
storage_manager_mock.obj_exists.assert_called_with(new_path)
62+
storage_manager_mock.copy_obj.assert_called_with(self.upload_path, new_path)
63+
self.assertEqual(self.file.fname.name, new_path)
64+
self.assertEqual(self.file.parent_folder.path, f'home/{self.username}/tests')
65+
storage_manager_mock.delete_obj.assert_called_with(self.upload_path)

chris_backend/userfiles/serializers.py

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11

22
import os
33

4-
from django.conf import settings
54
from rest_framework import serializers
65

76
from core.models import ChrisFolder
8-
from core.storage import connect_storage
97
from core.serializers import ChrisFileSerializer
108
from .models import UserFile
119

@@ -46,38 +44,31 @@ def create(self, validated_data):
4644

4745
def update(self, instance, validated_data):
4846
"""
49-
Overriden to set the file's saving path and parent folder and delete the old
50-
path from storage.
47+
Overriden to grant or remove public access to the file and/or move it to a new
48+
path.
5149
"""
52-
if 'public' in validated_data:
53-
instance.public = validated_data['public']
50+
public = instance.public
51+
52+
if public and 'public' in validated_data and not validated_data['public']:
53+
instance.remove_public_link()
54+
instance.remove_public_access()
5455

5556
upload_path = validated_data.pop('upload_path', None)
5657

5758
if upload_path:
59+
if public and ('public' not in validated_data or validated_data['public']):
60+
instance.remove_public_link()
61+
5862
# user file will be stored at: SWIFT_CONTAINER_NAME/<upload_path>
5963
# where <upload_path> must start with home/
60-
old_storage_path = instance.fname.name
61-
62-
storage_manager = connect_storage(settings)
63-
if storage_manager.obj_exists(upload_path):
64-
storage_manager.delete_obj(upload_path)
65-
66-
storage_manager.copy_obj(old_storage_path, upload_path)
67-
storage_manager.delete_obj(old_storage_path)
68-
69-
folder_path = os.path.dirname(upload_path)
70-
owner = instance.owner
71-
72-
try:
73-
parent_folder = ChrisFolder.objects.get(path=folder_path)
74-
except ChrisFolder.DoesNotExist:
75-
parent_folder = ChrisFolder.objects.create(path=folder_path, owner=owner)
64+
instance.move(upload_path)
7665

77-
instance.parent_folder = parent_folder
78-
instance.fname.name = upload_path
66+
if public and ('public' not in validated_data or validated_data['public']):
67+
instance.create_public_link() # recreate public link
7968

80-
instance.save()
69+
if not public and 'public' in validated_data and validated_data['public']:
70+
instance.grant_public_access()
71+
instance.create_public_link()
8172
return instance
8273

8374
def validate_upload_path(self, upload_path):

chris_backend/userfiles/tests/test_serializers.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,12 @@ def test_create(self):
5353
user_file = userfiles_serializer.create(validated_data)
5454
self.assertEqual(user_file.fname.name, f'home/{self.username}/uploads/file1.txt')
5555
self.assertEqual(user_file.parent_folder.path, f'home/{self.username}/uploads')
56+
user_file.delete()
5657

5758
def test_update(self):
5859
"""
59-
Test whether overriden 'update' method successfully updates a UserFile with
60-
the correct path and parent folder
60+
Test whether overriden 'update' method successfully moves a UserFile to the
61+
correct path.
6162
"""
6263
owner = User.objects.get(username=self.username)
6364
upload_path = f'home/{self.username}/uploads/file1.txt'
@@ -71,18 +72,9 @@ def test_update(self):
7172
userfiles_serializer = UserFileSerializer()
7273
validated_data = {'upload_path': f'home/{self.username}/tests/file1.txt'}
7374

74-
storage_manager_mock = mock.Mock()
75-
storage_manager_mock.copy_obj = mock.Mock()
76-
storage_manager_mock.delete_obj = mock.Mock()
77-
78-
with mock.patch('userfiles.serializers.connect_storage') as connect_storage_mock:
79-
connect_storage_mock.return_value=storage_manager_mock
80-
user_file = userfiles_serializer.update(user_file, validated_data)
81-
storage_manager_mock.copy_obj.assert_called_with(upload_path, f'home/{self.username}/tests/file1.txt')
82-
self.assertEqual(user_file.fname.name,f'home/{self.username}/tests/file1.txt')
83-
self.assertEqual(user_file.parent_folder.path, f'home/{self.username}/tests')
84-
connect_storage_mock.assert_called_with(settings)
85-
storage_manager_mock.delete_obj.assert_called_with(upload_path)
75+
user_file.move = mock.Mock()
76+
user_file = userfiles_serializer.update(user_file, validated_data)
77+
user_file.move.assert_called_with(f'home/{self.username}/tests/file1.txt')
8678

8779
def test_validate_upload_path_failure_contains_commas(self):
8880
"""

0 commit comments

Comments
 (0)