Skip to content

Commit b00ce7c

Browse files
committed
Update command, add tests
1 parent 3cefffe commit b00ce7c

File tree

2 files changed

+105
-3
lines changed

2 files changed

+105
-3
lines changed

osf/management/commands/force_archive.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,11 @@ def handle_file_operation(file_tree, reg, file_obj, log, obj_cache):
329329
elif log.action == 'osf_storage_file_updated':
330330
return modify_file_tree_recursive(reg._id, file_tree, file_obj, cached=bool(file_obj._id in obj_cache), revert=True)
331331
elif log.action == 'addon_file_moved':
332-
parent = BaseFileNode.objects.get(_id__in=obj_cache, name='/{}'.format(log.params['source']['materialized']).rstrip('/').split('/')[-2])
332+
if '/' not in log.params['source']['materialized'].rstrip('/'):
333+
# Root dir — parent is the root storage folder (name='')
334+
parent = BaseFileNode.objects.get(_id__in=obj_cache, name='')
335+
else:
336+
parent = BaseFileNode.objects.get(_id__in=obj_cache, name='/{}'.format(log.params['source']['materialized']).rstrip('/').split('/')[-2])
333337
return modify_file_tree_recursive(reg._id, file_tree, file_obj, cached=bool(file_obj._id in obj_cache), move_under=parent)
334338
elif log.action == 'addon_file_renamed':
335339
return modify_file_tree_recursive(reg._id, file_tree, file_obj, cached=bool(file_obj._id in obj_cache), rename=log.params['source']['name'])
@@ -356,7 +360,8 @@ def revert_log_actions(file_tree, reg, obj_cache, permissible_addons):
356360

357361
def build_file_tree(reg, node_settings, *args, **kwargs):
358362
n = reg.registered_from
359-
obj_cache = set(n.files.values_list('_id', flat=True))
363+
ct_id = ContentType.objects.get_for_model(n.__class__()).id
364+
obj_cache = set(BaseFileNode.objects.filter(target_object_id=n.id, target_content_type_id=ct_id).values_list('_id', flat=True))
360365

361366
def _recurse(file_obj, node):
362367
ct_id = ContentType.objects.get_for_model(node.__class__()).id

osf_tests/management_commands/test_force_archive.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from addons.osfstorage.models import OsfStorageFile, OsfStorageFolder
55
from osf.models import NodeLog, BaseFileNode
66
from osf.models.files import TrashedFileNode, TrashedFolder
7-
from osf.management.commands.force_archive import get_file_obj_from_log, build_file_tree, DEFAULT_PERMISSIBLE_ADDONS
7+
from osf.management.commands.force_archive import get_file_obj_from_log, build_file_tree, handle_file_operation, DEFAULT_PERMISSIBLE_ADDONS
88
from osf_tests.factories import NodeFactory, RegistrationFactory
99

1010

@@ -192,3 +192,100 @@ def get_root(self):
192192
names = [child['object'].name for child in tree['children']]
193193
assert 'file1.txt' in names
194194
assert 'file2.txt' not in names
195+
196+
@pytest.mark.django_db
197+
def test_obj_cache_includes_folders(self, node, reg, permissible_addons):
198+
"""
199+
Regression: n.files is a GenericRelation to OsfStorageFile only, so folder _ids were
200+
never in obj_cache. The fix uses BaseFileNode.objects.filter(...) which includes folders.
201+
"""
202+
from django.contrib.contenttypes.models import ContentType
203+
204+
folder = OsfStorageFolder.create(target=node, name='myfolder')
205+
folder.save()
206+
root_folder = OsfStorageFolder.create(target=node, name='')
207+
root_folder.save()
208+
209+
# Demonstrate the BUG: n.files (GenericRelation to OsfStorageFile) omits folders
210+
old_obj_cache = set(node.files.values_list('_id', flat=True))
211+
assert folder._id not in old_obj_cache, 'Folders must NOT appear via n.files (demonstrating the bug)'
212+
assert root_folder._id not in old_obj_cache, 'Root folder must NOT appear via n.files (demonstrating the bug)'
213+
214+
# Demonstrate the FIX: BaseFileNode.objects.filter(...) includes files AND folders
215+
ct_id = ContentType.objects.get_for_model(node.__class__()).id
216+
new_obj_cache = set(
217+
BaseFileNode.objects.filter(
218+
target_object_id=node.id,
219+
target_content_type_id=ct_id,
220+
).values_list('_id', flat=True)
221+
)
222+
assert folder._id in new_obj_cache, 'Folders must appear in fixed obj_cache'
223+
assert root_folder._id in new_obj_cache, 'Root folder must appear in fixed obj_cache'
224+
225+
226+
class TestHandleFileOperation:
227+
228+
@pytest.fixture
229+
def node(self):
230+
return NodeFactory(title='Test Node', category='project')
231+
232+
@pytest.fixture
233+
def reg(self, node):
234+
return RegistrationFactory(project=node, registered_date=timezone.now())
235+
236+
@pytest.mark.django_db
237+
def test_addon_file_moved_from_root_dir(self, node, reg):
238+
"""
239+
Regression: when materialized='/' (root dir moved between nodes), the old code did:
240+
'/{}'.format('/').rstrip('/') -> ''
241+
''.split('/') -> [''] (only 1 element)
242+
[''][-2] -> IndexError: list index out of range
243+
The fix detects the root-dir case and looks up the folder by name='' directly.
244+
"""
245+
from django.contrib.contenttypes.models import ContentType
246+
247+
root_folder = OsfStorageFolder.create(target=node, name='')
248+
root_folder.save()
249+
file = OsfStorageFile.create(target=node, name='file.txt')
250+
file.save()
251+
file.move_under(root_folder)
252+
253+
ct_id = ContentType.objects.get_for_model(node.__class__()).id
254+
obj_cache = set(
255+
BaseFileNode.objects.filter(
256+
target_object_id=node.id,
257+
target_content_type_id=ct_id,
258+
).values_list('_id', flat=True)
259+
)
260+
261+
file_tree = {
262+
'object': root_folder,
263+
'name': '',
264+
'deleted': False,
265+
'version': None,
266+
'children': [
267+
{'object': file, 'name': 'file.txt', 'deleted': False, 'version': None, 'children': []}
268+
]
269+
}
270+
271+
# materialized='/' is the actual crash case: moving a root dir between nodes
272+
log = NodeLog.objects.create(
273+
node=node,
274+
action='addon_file_moved',
275+
params={
276+
'source': {
277+
'materialized': '/', # root dir: triggers IndexError in old code
278+
'name': '',
279+
},
280+
'destination': {
281+
'materialized': '/',
282+
'name': '',
283+
}
284+
},
285+
date=timezone.now(),
286+
)
287+
288+
# Old code: '/{}'.format('/').rstrip('/') = '' -> ''.split('/')[-2] -> IndexError
289+
# Fixed code: detects no '/' in materialized.rstrip('/') and uses name='' directly
290+
result_tree, noop = handle_file_operation(file_tree, reg, file, log, obj_cache)
291+
assert result_tree is not None

0 commit comments

Comments
 (0)