##// END OF EJS Templates
feat(ui): added ability to replace binary file through UI, added related tests. Fixes: RCCE-19
ilin.s -
r5274:6d0b768f default
parent child Browse files
Show More
@@ -446,6 +446,16 b' def includeme(config):'
446 renderer='json_ext')
446 renderer='json_ext')
447
447
448 config.add_route(
448 config.add_route(
449 name='repo_files_replace_binary',
450 pattern='/{repo_name:.*?[^/]}/replace_binary/{commit_id}/{f_path:.*}',
451 repo_route=True)
452 config.add_view(
453 RepoFilesView,
454 attr='repo_files_replace_file',
455 route_name='repo_files_replace_binary', request_method='POST',
456 renderer='json_ext')
457
458 config.add_route(
449 name='repo_files_create_file',
459 name='repo_files_create_file',
450 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
460 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
451 repo_route=True)
461 repo_route=True)
@@ -924,6 +924,26 b' class TestModifyFilesWithWebInterface(ob'
924 tip = repo.get_commit(commit_idx=-1)
924 tip = repo.get_commit(commit_idx=-1)
925 assert tip.message == 'I committed'
925 assert tip.message == 'I committed'
926
926
927 def test_replace_binary_file_view_commit_changes(self, backend, csrf_token):
928 repo = backend.create_repo()
929 backend.ensure_file(b"vcs/nodes.docx", content=b"PREVIOUS CONTENT'")
930
931 response = self.app.post(
932 route_path('repo_files_replace_binary',
933 repo_name=repo.repo_name,
934 commit_id=backend.default_head_id,
935 f_path='vcs/nodes.docx'),
936 params={
937 'message': 'I committed',
938 'csrf_token': csrf_token,
939 },
940 upload_files=[('files_upload', 'vcs/nodes.docx', b'SOME CONTENT')],
941 status=200)
942 assert_session_flash(
943 response, 'Successfully committed 1 new file')
944 tip = repo.get_commit(commit_idx=-1)
945 assert tip.message == 'I committed'
946
927 def test_edit_file_view_commit_changes_default_message(self, backend,
947 def test_edit_file_view_commit_changes_default_message(self, backend,
928 csrf_token):
948 csrf_token):
929 repo = backend.create_repo()
949 repo = backend.create_repo()
@@ -950,6 +970,48 b' class TestModifyFilesWithWebInterface(ob'
950 tip = repo.get_commit(commit_idx=-1)
970 tip = repo.get_commit(commit_idx=-1)
951 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
971 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
952
972
973 def test_replace_binary_file_content_with_content_that_not_belong_to_original_type(self, backend, csrf_token):
974 repo = backend.create_repo()
975 backend.ensure_file(b"vcs/sheet.xlsx", content=b"PREVIOUS CONTENT'")
976
977 response = self.app.post(
978 route_path('repo_files_replace_binary',
979 repo_name=repo.repo_name,
980 commit_id=backend.default_head_id,
981 f_path='vcs/sheet.xlsx'),
982 params={
983 'message': 'I committed',
984 'csrf_token': csrf_token,
985 },
986 upload_files=[('files_upload', 'vcs/sheet.docx', b'SOME CONTENT')],
987 status=200)
988 assert response.json['error'] == "file extension of uploaded file doesn't match an original file's extension"
989
990 @pytest.mark.parametrize("replacement_files, expected_error", [
991 ([], 'missing files'),
992 (
993 [('files_upload', 'vcs/node1.docx', b'SOME CONTENT'),
994 ('files_upload', 'vcs/node2.docx', b'SOME CONTENT')],
995 'too many files for replacement'),
996 ])
997 def test_replace_binary_with_wrong_amount_of_content_sources(self, replacement_files, expected_error, backend,
998 csrf_token):
999 repo = backend.create_repo()
1000 backend.ensure_file(b"vcs/node.docx", content=b"PREVIOUS CONTENT'")
1001
1002 response = self.app.post(
1003 route_path('repo_files_replace_binary',
1004 repo_name=repo.repo_name,
1005 commit_id=backend.default_head_id,
1006 f_path='vcs/node.docx'),
1007 params={
1008 'message': 'I committed',
1009 'csrf_token': csrf_token,
1010 },
1011 upload_files=replacement_files,
1012 status=200)
1013 assert response.json['error'] == expected_error
1014
953 def test_delete_file_view(self, backend):
1015 def test_delete_file_view(self, backend):
954 self.app.get(
1016 self.app.get(
955 route_path('repo_files_remove_file',
1017 route_path('repo_files_remove_file',
@@ -1341,6 +1341,9 b' class RepoFilesView(RepoAppView):'
1341
1341
1342 self._ensure_not_locked()
1342 self._ensure_not_locked()
1343
1343
1344 # Check if we need to use this page to upload binary
1345 upload_binary = str2bool(self.request.params.get('upload_binary', False))
1346
1344 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1347 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1345 if c.commit is None:
1348 if c.commit is None:
1346 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1349 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
@@ -1357,8 +1360,10 b' class RepoFilesView(RepoAppView):'
1357 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1360 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1358 self.check_branch_permission(_branch_name, commit_id=commit_id)
1361 self.check_branch_permission(_branch_name, commit_id=commit_id)
1359
1362
1360 c.default_message = (_('Added file via RhodeCode Enterprise'))
1363 c.default_message = (_('Added file via RhodeCode Enterprise')) \
1364 if not upload_binary else (_('Edited file {} via RhodeCode Enterprise').format(f_path))
1361 c.f_path = f_path.lstrip('/') # ensure not relative path
1365 c.f_path = f_path.lstrip('/') # ensure not relative path
1366 c.replace_binary = upload_binary
1362
1367
1363 return self._get_template_context(c)
1368 return self._get_template_context(c)
1364
1369
@@ -1584,3 +1589,120 b' class RepoFilesView(RepoAppView):'
1584 'error': None,
1589 'error': None,
1585 'redirect_url': default_redirect_url
1590 'redirect_url': default_redirect_url
1586 }
1591 }
1592
1593 @LoginRequired()
1594 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1595 @CSRFRequired()
1596 def repo_files_replace_file(self):
1597 _ = self.request.translate
1598 c = self.load_default_context()
1599 commit_id, f_path = self._get_commit_and_path()
1600
1601 self._ensure_not_locked()
1602
1603 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1604 if c.commit is None:
1605 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1606
1607 if self.rhodecode_vcs_repo.is_empty():
1608 default_redirect_url = h.route_path(
1609 'repo_summary', repo_name=self.db_repo_name)
1610 else:
1611 default_redirect_url = h.route_path(
1612 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1613
1614 if self.rhodecode_vcs_repo.is_empty():
1615 # for empty repository we cannot check for current branch, we rely on
1616 # c.commit.branch instead
1617 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1618 else:
1619 _branch_name, _sha_commit_id, is_head = \
1620 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1621 landing_ref=self.db_repo.landing_ref_name)
1622
1623 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1624 if error:
1625 return {
1626 'error': error,
1627 'redirect_url': default_redirect_url
1628 }
1629 error = self.check_branch_permission(_branch_name, json_mode=True)
1630 if error:
1631 return {
1632 'error': error,
1633 'redirect_url': default_redirect_url
1634 }
1635
1636 c.default_message = (_('Edited file {} via RhodeCode Enterprise').format(f_path))
1637 c.f_path = f_path
1638
1639 r_post = self.request.POST
1640
1641 message = c.default_message
1642 user_message = r_post.getall('message')
1643 if isinstance(user_message, list) and user_message:
1644 # we take the first from duplicated results if it's not empty
1645 message = user_message[0] if user_message[0] else message
1646
1647 data_for_replacement = r_post.getall('files_upload') or []
1648 if (objects_count := len(data_for_replacement)) > 1:
1649 return {
1650 'error': 'too many files for replacement',
1651 'redirect_url': default_redirect_url
1652 }
1653 elif not objects_count:
1654 return {
1655 'error': 'missing files',
1656 'redirect_url': default_redirect_url
1657 }
1658
1659 content = data_for_replacement[0].file
1660 retrieved_filename = data_for_replacement[0].filename
1661
1662 if retrieved_filename.split('.')[-1] != f_path.split('.')[-1]:
1663 return {
1664 'error': 'file extension of uploaded file doesn\'t match an original file\'s extension',
1665 'redirect_url': default_redirect_url
1666 }
1667
1668 author = self._rhodecode_db_user.full_contact
1669
1670 try:
1671 commit = ScmModel().update_binary_node(
1672 user=self._rhodecode_db_user.user_id,
1673 repo=self.db_repo,
1674 message=message,
1675 node={
1676 'content': content,
1677 'file_path': f_path.encode(),
1678 },
1679 parent_commit=c.commit,
1680 author=author,
1681 )
1682
1683 h.flash(_('Successfully committed 1 new file'), category='success')
1684
1685 default_redirect_url = h.route_path(
1686 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1687
1688 except (NodeError, NodeAlreadyExistsError) as e:
1689 error = h.escape(e)
1690 h.flash(error, category='error')
1691
1692 return {
1693 'error': error,
1694 'redirect_url': default_redirect_url
1695 }
1696 except Exception:
1697 log.exception('Error occurred during commit')
1698 error = _('Error occurred during commit')
1699 h.flash(error, category='error')
1700 return {
1701 'error': error,
1702 'redirect_url': default_redirect_url
1703 }
1704
1705 return {
1706 'error': None,
1707 'redirect_url': default_redirect_url
1708 }
@@ -255,6 +255,31 b' class ScmModel(BaseModel):'
255 all_repos, repos_path=self.repos_path, order_by=sort_key)
255 all_repos, repos_path=self.repos_path, order_by=sort_key)
256 return repo_iter
256 return repo_iter
257
257
258 @staticmethod
259 def get_parent_commits(parent_commit, scm_instance):
260 if not parent_commit:
261 parent_commit = EmptyCommit(alias=scm_instance.alias)
262
263 if isinstance(parent_commit, EmptyCommit):
264 # EmptyCommit means we're editing empty repository
265 parents = None
266 else:
267 parents = [parent_commit]
268 return parent_commit, parents
269
270 def initialize_inmemory_vars(self, user, repo, message, author):
271 """
272 Initialize node specific objects for further usage
273 """
274 user = self._get_user(user)
275 scm_instance = repo.scm_instance(cache=False)
276 message = safe_str(message)
277 commiter = user.full_contact
278 author = safe_str(author) if author else commiter
279 imc = scm_instance.in_memory_commit
280
281 return user, scm_instance, message, commiter, author, imc
282
258 def get_repo_groups(self, all_groups=None):
283 def get_repo_groups(self, all_groups=None):
259 if all_groups is None:
284 if all_groups is None:
260 all_groups = RepoGroup.query()\
285 all_groups = RepoGroup.query()\
@@ -763,24 +788,10 b' class ScmModel(BaseModel):'
763
788
764 :returns: new committed commit
789 :returns: new committed commit
765 """
790 """
766
791 user, scm_instance, message, commiter, author, imc = self.initialize_inmemory_vars(
767 user = self._get_user(user)
792 user, repo, message, author)
768 scm_instance = repo.scm_instance(cache=False)
769
770 message = safe_str(message)
771 commiter = user.full_contact
772 author = safe_str(author) if author else commiter
773
793
774 imc = scm_instance.in_memory_commit
794 parent_commit, parents = self.get_parent_commits(parent_commit, scm_instance)
775
776 if not parent_commit:
777 parent_commit = EmptyCommit(alias=scm_instance.alias)
778
779 if isinstance(parent_commit, EmptyCommit):
780 # EmptyCommit means we're editing empty repository
781 parents = None
782 else:
783 parents = [parent_commit]
784
795
785 upload_file_types = (io.BytesIO, io.BufferedRandom)
796 upload_file_types = (io.BytesIO, io.BufferedRandom)
786 processed_nodes = []
797 processed_nodes = []
@@ -827,23 +838,10 b' class ScmModel(BaseModel):'
827
838
828 def update_nodes(self, user, repo, message, nodes, parent_commit=None,
839 def update_nodes(self, user, repo, message, nodes, parent_commit=None,
829 author=None, trigger_push_hook=True):
840 author=None, trigger_push_hook=True):
830 user = self._get_user(user)
841 user, scm_instance, message, commiter, author, imc = self.initialize_inmemory_vars(
831 scm_instance = repo.scm_instance(cache=False)
842 user, repo, message, author)
832
833 message = safe_str(message)
834 commiter = user.full_contact
835 author = safe_str(author) if author else commiter
836
837 imc = scm_instance.in_memory_commit
838
843
839 if not parent_commit:
844 parent_commit, parents = self.get_parent_commits(parent_commit, scm_instance)
840 parent_commit = EmptyCommit(alias=scm_instance.alias)
841
842 if isinstance(parent_commit, EmptyCommit):
843 # EmptyCommit means we we're editing empty repository
844 parents = None
845 else:
846 parents = [parent_commit]
847
845
848 # add multiple nodes
846 # add multiple nodes
849 for _filename, data in nodes.items():
847 for _filename, data in nodes.items():
@@ -890,6 +888,39 b' class ScmModel(BaseModel):'
890
888
891 return tip
889 return tip
892
890
891 def update_binary_node(self, user, repo, message, node, parent_commit=None, author=None):
892 user, scm_instance, message, commiter, author, imc = self.initialize_inmemory_vars(
893 user, repo, message, author)
894
895 parent_commit, parents = self.get_parent_commits(parent_commit, scm_instance)
896
897 file_path = node.get('file_path')
898 if isinstance(raw_content := node.get('content'), (io.BytesIO, io.BufferedRandom)):
899 content = raw_content.read()
900 else:
901 raise Exception("Wrong content was provided")
902 file_node = FileNode(file_path, content=content)
903 imc.change(file_node)
904
905 try:
906 tip = imc.commit(message=message,
907 author=author,
908 parents=parents,
909 branch=parent_commit.branch)
910 except NodeNotChangedError:
911 raise
912 except Exception as e:
913 log.exception("Unexpected exception during call to imc.commit")
914 raise IMCCommitError(str(e))
915 finally:
916 self.mark_for_invalidation(repo.repo_name)
917
918 hooks_utils.trigger_post_push_hook(
919 username=user.username, action='push_local', hook_type='post_push',
920 repo_name=repo.repo_name, repo_type=scm_instance.alias,
921 commit_ids=[tip.raw_id])
922 return tip
923
893 def delete_nodes(self, user, repo, message, nodes, parent_commit=None,
924 def delete_nodes(self, user, repo, message, nodes, parent_commit=None,
894 author=None, trigger_push_hook=True):
925 author=None, trigger_push_hook=True):
895 """
926 """
@@ -908,8 +939,8 b' class ScmModel(BaseModel):'
908 :returns: new commit after deletion
939 :returns: new commit after deletion
909 """
940 """
910
941
911 user = self._get_user(user)
942 user, scm_instance, message, commiter, author, imc = self.initialize_inmemory_vars(
912 scm_instance = repo.scm_instance(cache=False)
943 user, repo, message, author)
913
944
914 processed_nodes = []
945 processed_nodes = []
915 for f_path in nodes:
946 for f_path in nodes:
@@ -919,20 +950,8 b' class ScmModel(BaseModel):'
919 content = nodes[f_path].get('content')
950 content = nodes[f_path].get('content')
920 processed_nodes.append((safe_bytes(f_path), content))
951 processed_nodes.append((safe_bytes(f_path), content))
921
952
922 message = safe_str(message)
953 parent_commit, parents = self.get_parent_commits(parent_commit, scm_instance)
923 commiter = user.full_contact
924 author = safe_str(author) if author else commiter
925
926 imc = scm_instance.in_memory_commit
927
954
928 if not parent_commit:
929 parent_commit = EmptyCommit(alias=scm_instance.alias)
930
931 if isinstance(parent_commit, EmptyCommit):
932 # EmptyCommit means we we're editing empty repository
933 parents = None
934 else:
935 parents = [parent_commit]
936 # add multiple nodes
955 # add multiple nodes
937 for path, content in processed_nodes:
956 for path, content in processed_nodes:
938 imc.remove(FileNode(path, content=content))
957 imc.remove(FileNode(path, content=content))
@@ -36,7 +36,7 b''
36 %if c.on_branch_head and c.branch_or_raw_id:
36 %if c.on_branch_head and c.branch_or_raw_id:
37 ## binary files are delete only
37 ## binary files are delete only
38 % if c.file.is_binary:
38 % if c.file.is_binary:
39 ${h.link_to(_('Edit'), '#Edit', class_="btn btn-default disabled tooltip", title=_('Editing binary files not allowed'))}
39 ${h.link_to(_('Replace'), h.route_path('repo_files_upload_file', repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path, _query={'upload_binary': 'true'}), class_="btn btn-default active tooltip", title=_('You can replace content of your binary file'))}
40 ${h.link_to(_('Delete'), h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _query=query),class_="btn btn-danger")}
40 ${h.link_to(_('Delete'), h.route_path('repo_files_remove_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _query=query),class_="btn btn-danger")}
41 % else:
41 % else:
42 <a class="btn btn-default" href="${h.route_path('repo_files_edit_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _query=query)}">
42 <a class="btn btn-default" href="${h.route_path('repo_files_edit_file',repo_name=c.repo_name,commit_id=c.branch_or_raw_id,f_path=c.f_path, _query=query)}">
@@ -49,7 +49,11 b''
49 </div>
49 </div>
50
50
51 <div class="edit-file-title">
51 <div class="edit-file-title">
52 <span class="title-heading">${_('Upload new file')} @ <code>${h.show_id(c.commit)}</code></span>
52 % if c.replace_binary:
53 <span class="title-heading">${_('Replace content of')} <b>${c.f_path}</b> @ <code>${h.show_id(c.commit)}</code></span>
54 % else:
55 <span class="title-heading">${_('Upload new file')} @ <code>${h.show_id(c.commit)}</code></span>
56 % endif
53 % if c.commit.branch:
57 % if c.commit.branch:
54 <span class="tag branchtag">
58 <span class="tag branchtag">
55 <i class="icon-branch"></i> ${c.commit.branch}
59 <i class="icon-branch"></i> ${c.commit.branch}
@@ -57,24 +61,27 b''
57 % endif
61 % endif
58 </div>
62 </div>
59
63
60 <% form_url = h.route_path('repo_files_upload_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path) %>
64 % if not c.replace_binary:
61 ##${h.secure_form(form_url, id='eform', enctype="multipart/form-data", request=request)}
65 <% form_url = h.route_path('repo_files_upload_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path) %>
62 <div class="edit-file-fieldset">
66 <div class="edit-file-fieldset">
63 <div class="path-items">
67 <div class="path-items">
64 <ul class="tooltip" title="Repository path to store uploaded files. To change it, navigate to different path and click upload from there.">
68 <ul class="tooltip" title="Repository path to store uploaded files. To change it, navigate to different path and click upload from there.">
65 <li class="breadcrumb-path">
69 <li class="breadcrumb-path">
66 <div>
70 <div>
67 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path='')}"><i class="icon-home"></i></a> /
71 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path='')}"><i class="icon-home"></i></a> /
68 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">${c.f_path}</a>${('/' if c.f_path else '')}
72 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">${c.f_path}</a>${('/' if c.f_path else '')}
69 </div>
73 </div>
70 </li>
74 </li>
71 <li class="location-path">
75 <li class="location-path">
72
76
73 </li>
77 </li>
74 </ul>
78 </ul>
79 </div>
80
75 </div>
81 </div>
76
82 % else:
77 </div>
83 <% form_url = h.route_path('repo_files_replace_binary', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path) %>
84 % endif
78
85
79 <div class="upload-form table">
86 <div class="upload-form table">
80 <div>
87 <div>
@@ -83,7 +90,11 b''
83 <div class="dropzone-pure">
90 <div class="dropzone-pure">
84 <div class="dz-message">
91 <div class="dz-message">
85 <i class="icon-upload" style="font-size:36px"></i></br>
92 <i class="icon-upload" style="font-size:36px"></i></br>
86 ${_("Drag'n Drop files here or")} <span class="link">${_('Choose your files')}</span>.<br>
93 % if not c.replace_binary:
94 ${_("Drag'n Drop files here or")} <span class="link">${_('Choose your files')}</span>.<br>
95 % else:
96 ${_("Drag'n Drop file here or")} <span class="link">${_('Choose your file')}</span>.<br>
97 % endif
87 </div>
98 </div>
88 </div>
99 </div>
89
100
@@ -75,6 +75,7 b' def get_url_defs():'
75 "repo_files_add_file": "/{repo_name}/add_file/{commit_id}/{f_path}",
75 "repo_files_add_file": "/{repo_name}/add_file/{commit_id}/{f_path}",
76 "repo_files_upload_file": "/{repo_name}/upload_file/{commit_id}/{f_path}",
76 "repo_files_upload_file": "/{repo_name}/upload_file/{commit_id}/{f_path}",
77 "repo_files_create_file": "/{repo_name}/create_file/{commit_id}/{f_path}",
77 "repo_files_create_file": "/{repo_name}/create_file/{commit_id}/{f_path}",
78 "repo_files_replace_binary": "/{repo_name}/replace_binary/{commit_id}/{f_path}",
78 "repo_nodetree_full": "/{repo_name}/nodetree_full/{commit_id}/{f_path}",
79 "repo_nodetree_full": "/{repo_name}/nodetree_full/{commit_id}/{f_path}",
79 "repo_nodetree_full:default_path": "/{repo_name}/nodetree_full/{commit_id}/",
80 "repo_nodetree_full:default_path": "/{repo_name}/nodetree_full/{commit_id}/",
80 "journal": ADMIN_PREFIX + "/journal",
81 "journal": ADMIN_PREFIX + "/journal",
General Comments 0
You need to be logged in to leave comments. Login now