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 @@