diff --git a/rhodecode/api/tests/test_api.py b/rhodecode/api/tests/test_api.py index d861bfc5..1c39aa0e 100644 --- a/rhodecode/api/tests/test_api.py +++ b/rhodecode/api/tests/test_api.py @@ -16,11 +16,13 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ import json +import os import pytest from rhodecode.api.utils import Optional, OAttr from rhodecode.api.tests.utils import build_data, api_call, assert_error, assert_ok +from rhodecode.lib.utils import get_rhodecode_repo_store_path from rhodecode.model.db import Repository from rhodecode.model.meta import Session @@ -156,6 +158,42 @@ class TestApi(object): assert json_body["result"]["archived"] == is_archived + def test_api_delete_repo_with_backup(self, backend): + repo_name = self._create_repo(backend, is_archived=False) + + id_, params = build_data(apikey=self.apikey, method="delete_repo", repoid=repo_name) + response = api_call(self.app, params) + + assert response.status == "200 OK" + + json_body = json.loads(response.body) + assert json_body["result"]["success"] + assert f"Deleted repository `{repo_name}`" == json_body["result"]["msg"] + assert repo_name in self._list_removed_repos_folders_only() + + def test_api_delete_repo_without_backup(self, backend): + repo_name = self._create_repo(backend, is_archived=False) + + id_, params = build_data(apikey=self.apikey, method="delete_repo", repoid=repo_name, fs_backup=False) + response = api_call(self.app, params) + + assert response.status == "200 OK" + + json_body = json.loads(response.body) + assert json_body["result"]["success"] + assert f"Deleted repository `{repo_name}`" == json_body["result"]["msg"] + assert repo_name not in self._list_removed_repos_folders_only() + + def _list_removed_repos_folders_only(self): + temp_folder_with_repos = get_rhodecode_repo_store_path() + + res = [] + for r in os.listdir(temp_folder_with_repos): + if r.startswith("rm__"): + removed_repo_name = r.split("__", 2)[-1] + res.append(removed_repo_name) + return res + def _create_repo(self, backend, is_archived): repo = backend.create_repo() repo_name = repo.repo_name diff --git a/rhodecode/api/views/repo_api.py b/rhodecode/api/views/repo_api.py index b4398312..3f2f57ef 100644 --- a/rhodecode/api/views/repo_api.py +++ b/rhodecode/api/views/repo_api.py @@ -1331,7 +1331,7 @@ def fork_repo( @jsonrpc_method() -def delete_repo(request, apiuser, repoid, forks=Optional("")): +def delete_repo(request, apiuser, repoid, fs_backup=True, forks=Optional("")): """ Deletes a repository. @@ -1345,6 +1345,8 @@ def delete_repo(request, apiuser, repoid, forks=Optional("")): :type apiuser: AuthUser :param repoid: Set the repository name or repository ID. :type repoid: str or int + :param fs_backup: + :type fs_backup: bool :param forks: Set to `detach` or `delete` forks from the |repo|. :type forks: Optional(str) @@ -1378,7 +1380,7 @@ def delete_repo(request, apiuser, repoid, forks=Optional("")): raise JSONRPCError(f"Cannot delete `{repo.repo_name}` it still contains attached forks") old_data = repo.get_api_data() - RepoModel().delete(repo, forks_action=ForksAction(str_fork_action)) + RepoModel().delete(repo, forks_action=ForksAction(str_fork_action), fs_backup=fs_backup) repo = audit_logger.RepoWrap(repo_id=None, repo_name=repo.repo_name) diff --git a/rhodecode/apps/repository/views/repo_settings_advanced.py b/rhodecode/apps/repository/views/repo_settings_advanced.py index 82b45f0b..3b5dc9c1 100644 --- a/rhodecode/apps/repository/views/repo_settings_advanced.py +++ b/rhodecode/apps/repository/views/repo_settings_advanced.py @@ -27,6 +27,7 @@ from rhodecode.lib import helpers as h from rhodecode.lib import audit_logger from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired, HasRepoPermissionAny from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError, AttachedArtifactsError +from rhodecode.lib.type_utils import str2bool from rhodecode.lib.utils2 import safe_int from rhodecode.lib.vcs import RepositoryError from rhodecode.model.db import Session, UserFollowing, User, Repository @@ -143,7 +144,10 @@ class RepoSettingsAdvancedView(RepoAppView): delete_cache = True self._invalidate_remote_cache(delete=delete_cache) - delete_res = RepoModel().delete(self.db_repo) + backup = str2bool( + self.request.POST.get("backup", "false") + ) # if this option is absent, this means that on FE side checkbox was unchecked + delete_res = RepoModel().delete(self.db_repo, fs_backup=backup) fork_action = delete_res.fork_action_result.action fork_cnt = delete_res.fork_action_result.fork_count diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py index 9f24367d..33b611fc 100644 --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -790,6 +790,7 @@ class RepoModel(BaseModel): fs_remove: bool = True, cur_user: User = None, call_events: bool = True, + fs_backup: bool = True, ) -> DeleteRepoResult: """ Delete a given repository. @@ -797,11 +798,10 @@ class RepoModel(BaseModel): :param repo: :param forks_action: - NOTE: used only in API to enforce action; - If the repository is private, then forks will be removed. - If the repository is public, then forks will be detached. - - :param fs_remove: remove(archive) repo from filesystem + NOTE: used only in API to enforce action; + If the repository is private, then forks will be removed. + If the repository is public, then forks will be detached. + :param fs_backup: flag that tells if the repo folder on filesystem should be renamed in a special format or removed completely """ repo = self._get_repo(repo) if not repo: @@ -835,7 +835,7 @@ class RepoModel(BaseModel): try: self.sa.delete(repo) if fs_remove: - self._delete_filesystem_repo(repo) + self._delete_filesystem_repo(repo, backup=fs_backup) else: log.debug("skipping removal from filesystem") old_repo_dict.update( @@ -1113,7 +1113,7 @@ class RepoModel(BaseModel): raise Exception("Was trying to rename to already existing dir %s" % new_path) shutil.move(old_path, new_path) - def _delete_filesystem_repo(self, repo): + def _delete_filesystem_repo(self, repo, backup=True): """ removes repo from filesystem, the removal is actually made by added rm__ prefix into dir, and rename internal .hg/.git dirs so this @@ -1121,9 +1121,9 @@ class RepoModel(BaseModel): by reverting the renames on this repository :param repo: repo object + :param backup: flag that tells if the repo folder on filesystem should be renamed in a special format or removed completely """ rm_path = os.path.join(self.repos_path, repo.repo_name) - repo_group = repo.group log.info("delete_filesystem_repo: removing repository %s", rm_path) # disable hg/git internal that it doesn't get detected as repo alias = repo.repo_type @@ -1132,23 +1132,36 @@ class RepoModel(BaseModel): config.set("extensions", "largefiles", "") bare = getattr(repo.scm_instance(config=config), "bare", False) - # skip this for bare git repos - if not bare: - # disable VCS repo - vcs_path = os.path.join(rm_path, ".%s" % alias) - if os.path.exists(vcs_path): - shutil.move(vcs_path, os.path.join(rm_path, "rm__.%s" % alias)) + if backup: + # skip this for bare git repos + if not bare: + # disable VCS repo + vcs_path = os.path.join(rm_path, ".%s" % alias) + if os.path.exists(vcs_path): + shutil.move(vcs_path, os.path.join(rm_path, "rm__.%s" % alias)) - _now = datetime.datetime.now() - _ms = str(_now.microsecond).rjust(6, "0") - _d = "rm__{}__{}".format(_now.strftime("%Y%m%d_%H%M%S_" + _ms), repo.just_name) - if repo_group: - # if repository is in group, prefix the removal path with the group - args = repo_group.full_path_splitted + [_d] - _d = os.path.join(*args) + repo_group = repo.group - if os.path.isdir(rm_path): - shutil.move(rm_path, os.path.join(self.repos_path, _d)) + _now = datetime.datetime.now() + _ms = str(_now.microsecond).rjust(6, "0") + _d = "rm__{}__{}".format(_now.strftime("%Y%m%d_%H%M%S_" + _ms), repo.just_name) + if repo_group: + # if repository is in group, prefix the removal path with the group + args = repo_group.full_path_splitted + [_d] + _d = os.path.join(*args) + + if os.path.isdir(rm_path): + shutil.move(rm_path, os.path.join(self.repos_path, _d)) + else: + # skip this for bare git repos + if not bare: + # disable VCS repo + vcs_path = os.path.join(rm_path, ".%s" % alias) + if os.path.exists(vcs_path): + shutil.rmtree(vcs_path) + + if os.path.exists(rm_path): + shutil.rmtree(rm_path) # finally cleanup diff-cache if it exists cached_diffs_dir = repo.cached_diffs_dir diff --git a/rhodecode/templates/admin/repos/repo_edit_advanced.mako b/rhodecode/templates/admin/repos/repo_edit_advanced.mako index e3ade58b..d2925688 100644 --- a/rhodecode/templates/admin/repos/repo_edit_advanced.mako +++ b/rhodecode/templates/admin/repos/repo_edit_advanced.mako @@ -247,6 +247,12 @@
+ +
+
${_('This repository will be renamed in a special way in order to make it inaccessible to RhodeCode Enterprise and its VCS systems. If you need to fully delete it from the file system, please do it manually, or with rhodecode-cleanup-repos command available in rhodecode-tools.')} @@ -311,5 +317,19 @@ $("#id_fork_of").select2({ } }) }); + +// hide/show archive help field +const $checkbox = $('#backup'); +const $helpField = $('#archive_help_field'); + +function syncHelpVisibility() { + $checkbox.val($checkbox.is(':checked') ? 'true' : 'false'); + $helpField.toggle($checkbox.is(':checked')); +} + +$checkbox.on('change', syncHelpVisibility) + +syncHelpVisibility(); + diff --git a/rhodecode/templates/data_table/_dt_elements.mako b/rhodecode/templates/data_table/_dt_elements.mako index cde395da..5b13c975 100644 --- a/rhodecode/templates/data_table/_dt_elements.mako +++ b/rhodecode/templates/data_table/_dt_elements.mako @@ -174,14 +174,6 @@ ${h.style_metatag(tag_type, tag)|n,trim} Edit
-
- ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=repo_name), request=request)} - - ${h.end_form()} -
diff --git a/rhodecode/tests/models/test_repos.py b/rhodecode/tests/models/test_repos.py index 1afd08e0..6d33038b 100644 --- a/rhodecode/tests/models/test_repos.py +++ b/rhodecode/tests/models/test_repos.py @@ -23,7 +23,7 @@ import tempfile from rhodecode.lib.exceptions import AttachedForksError -from rhodecode.lib.utils import make_db_config +from rhodecode.lib.utils import make_db_config, get_rhodecode_repo_store_path from rhodecode.model.db import Repository from rhodecode.model.meta import Session from rhodecode.model.repo import RepoModel, ForksAction @@ -42,6 +42,34 @@ class TestRepoModel(object): assert Repository.get_by_repo_name(repo_name=backend.repo_name) is None assert repo.repo_name not in repos + def test_remove_repo_with_backup(self, backend): + repo = backend.create_repo() + Session().commit() + RepoModel().delete(repo=repo) + Session().commit() + + assert Repository.get_by_repo_name(repo_name=backend.repo_name) is None + assert repo.repo_name in self._list_removed_repos_folders_only() + + def test_remove_repo_without_backup(self, backend): + repo = backend.create_repo() + Session().commit() + RepoModel().delete(repo=repo, fs_backup=False) + Session().commit() + + assert Repository.get_by_repo_name(repo_name=backend.repo_name) is None + assert repo.repo_name not in self._list_removed_repos_folders_only() + + def _list_removed_repos_folders_only(self): + temp_folder_with_repos = get_rhodecode_repo_store_path() + + res = [] + for r in os.listdir(temp_folder_with_repos): + if r.startswith("rm__"): + removed_repo_name = r.split("__", 2)[-1] + res.append(removed_repo_name) + return res + def test_remove_repo_forks_are_detached(self, backend): repo = backend.create_repo() Session().commit()