diff --git a/biscd/biscd/models/project.py b/biscd/biscd/models/project.py index 9f8392b..a6ef6ad 100644 --- a/biscd/biscd/models/project.py +++ b/biscd/biscd/models/project.py @@ -4,7 +4,7 @@ from shutil import rmtree import string import random from werkzeug.utils import secure_filename -from git import Repo, InvalidGitRepositoryError, GitCommandError, cmd as git_cmd +from git import Repo, InvalidGitRepositoryError, GitCommandError from flask import current_app as app from biscd import config @@ -42,20 +42,28 @@ class Project(YamlSerializable): return hash(self.name) @property - def absulute_path(self): + def absolute_production_path(self): if not hasattr(self, 'relative_production_path') or not self.relative_production_path: return None - return Path(config['production_path'].get()) / self.relative_production_path + production_path = Path(config['production_path'].get()) + absolute_path = (production_path / self.relative_production_path).resolve() + try: + if str(absolute_path.relative_to(production_path)) != self.relative_production_path: + raise ValueError + except ValueError as v_e: + raise ValueError(f"{self.name}'s relative_production_path is not within the " \ + f"application's production path! {self.relative_production_path}") from v_e + return absolute_path def update(self): """Update project's local repoistory""" # pylint: disable=no-member self._make_sure_production_path_exists() - path = self.absulute_path - app.logger.info(f"Updating {path}") + path = self.absolute_production_path + app.logger.info("Updating project '%s' in %s", self.name, path) try: repo = Repo(path) - app.logger.info('Already under git control') + app.logger.info('%s was already under git control', self.name) except InvalidGitRepositoryError: repo = self._clone_repo(path) # If repo is not type Repo it must be an error and thus returned directly @@ -63,11 +71,11 @@ class Project(YamlSerializable): return repo remotes = repo.remotes if not any(remotes): - app.logger.error(f"Repo {path} doesn't have any remotes.") + app.logger.error("%s's repo %s doesn't have any remotes.", self.name, path) return ["Repo doesn't have any remotes. please fix manually", 'error'] response = remotes[0].pull() for item in response: - # TODO: is this nessecary? + #TODO: is this nessecary? app.logger.debug(item) response = self._checkout_branch(repo, self.branch) @@ -95,40 +103,65 @@ class Project(YamlSerializable): 'error'] return None + def delete(self): + if self.production_path_exists(): + result = self.delete_files() + if result and result[1] in ['warning', 'error']: + return result + super().delete() + return [f"{self.name} has been deleted", 'success'] + def delete_files(self): + if not hasattr(self, 'relative_production_path') or not self.relative_production_path: + return ['Project does not have a production path', 'warning'] if self.production_path_exists(): - path = self.absulute_path + path = self.absolute_production_path app.logger.info(f"Deleting {path}") rmtree(path) + path = Path(self.relative_production_path).parent + while len(path.parts) > 0 and self._delete_dir_if_empty(path): + path = path.parent self.relative_production_path = None self.save() + @classmethod + def _delete_dir_if_empty(cls, relative_path): + production_path = config['production_path'].get() + absolute_path = (production_path / relative_path).resolve() + if absolute_path.is_dir() and not any(absolute_path.iterdir()): + if absolute_path.relative_to(production_path) == relative_path: + absolute_path.rmdir() + app.logger.debug('Deleted empty dir %s', absolute_path) + return True + raise ValueError("Trying to delete a directory outdize of production path! : " + + str(absolute_path)) + return False + def production_path_exists(self): """Return True if project's the production directory path exists""" - prod_path_dir = Path(config['production_path'].get()) if hasattr(self, 'relative_production_path') and self.relative_production_path: - full_path = prod_path_dir / self.relative_production_path - return full_path.is_dir() + absolute_path = self.absolute_production_path + return absolute_path.is_dir() return False def _make_sure_production_path_exists(self): - prod_path_dir = Path(config['production_path'].get()) if hasattr(self, 'relative_production_path') and self.relative_production_path: - full_path = prod_path_dir / self.relative_production_path - if not full_path.is_dir(): - full_path.mkdir(parents=True) + absolute_path = self.absolute_production_path + if not absolute_path.is_dir(): + absolute_path.mkdir(parents=True) else: - def generate_full_path(): + prod_path_dir = Path(config['production_path'].get()) + def generate_absolute_path(): rand_num = ''.join(random.choices(string.digits, k=6)) return prod_path_dir / (secure_filename(self.name) + rand_num) - full_path = generate_full_path() + absolute_path = generate_absolute_path() # Make sure path is unique - while full_path.is_dir(): - full_path = generate_full_path() + while absolute_path.is_dir(): + absolute_path = generate_absolute_path() - full_path.mkdir(parents=True) - self.relative_production_path = str(full_path.relative_to(prod_path_dir)) + absolute_path.mkdir(parents=True) + self.relative_production_path = str(absolute_path.relative_to(prod_path_dir)) self.save() def user_access(self, user): diff --git a/biscd/biscd/models/yaml_serializable.py b/biscd/biscd/models/yaml_serializable.py index 684a347..4ae2316 100644 --- a/biscd/biscd/models/yaml_serializable.py +++ b/biscd/biscd/models/yaml_serializable.py @@ -1,5 +1,6 @@ from abc import ABCMeta, abstractmethod from flask import abort +from git import exc import yaml import inspect @@ -52,6 +53,20 @@ class YamlSerializable(RecursiveProperty): print(ymlsls) self._save_all_to_file(ymlsls) + def delete(self): + # pylint: disable=no-member + if self.name is None: + raise TypeError("Name cannot be None") + ymlsls = self._get_all_from_file() + try: + ymlsl = next((ymlsl for ymlsl in ymlsls if [*ymlsl][0] == self.name)) + ymlsls.remove(ymlsl) + #ymlsl = next((ymlsl for ymlsl in ymlsls if [*ymlsl][0] == self.name), None) + except KeyError as key_error: + raise ValueError( + f"{type(self).__name__} {self.name} does not exist") from key_error + self._save_all_to_file(ymlsls) + @classmethod def first_or_404(cls, **kwargs): ymlsl = cls.first(**kwargs) diff --git a/biscd/biscd/routes.py b/biscd/biscd/routes.py index 7c02060..e2ab720 100644 --- a/biscd/biscd/routes.py +++ b/biscd/biscd/routes.py @@ -122,3 +122,13 @@ def project_delete_files(project_name): result = project.delete_files() flash_result(result) return redirect(url_for('project_dashboard', project_name=project.name)) + +@app.route('/project//delete', methods=['GET']) +@login_required +def project_delete(project_name): + project = Project.first_or_404(name=project_name) + if project.user_access(current_user) != 'Owner': + abort(401) + result = project.delete() + flash_result(result) + return redirect(url_for('index'))