summaryrefslogtreecommitdiffstats
path: root/ms/artifact-manager/manager/utils.py
blob: da4cd9f9d03208f286fabaa7dc416a2debd2bb52 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
"""Copyright 2019 Deutsche Telekom.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

import os
import shutil
from abc import ABC, abstractmethod
from io import BytesIO
from pathlib import Path
from zipfile import ZipFile, is_zipfile

from manager.configuration import config
from manager.errors import ArtifactNotFoundError, ArtifactOverwriteError, InvalidRequestError


class Repository(ABC):
    """Abstract repository class.

    Defines repository methods.
    """

    @abstractmethod
    def upload_blueprint(self, file: bytes, name: str, version: str) -> None:
        """Store blueprint file in the repository.

        :param file: File to save
        :param name: Blueprint name
        :param version: Blueprint version
        """

    @abstractmethod
    def download_blueprint(self, name: str, version: str) -> bytes:
        """Download blueprint file from repository.

        :param name: Blueprint name
        :param version: Blueprint version
        :return: Zipped Blueprint file bytes
        """

    @abstractmethod
    def remove_blueprint(self, name: str, version: str) -> None:
        """Remove blueprint file from repository.

        :param name: Blueprint name
        :param version: Blueprint version
        """


class FileRepository(Repository):
    """Store blueprints on local directory."""

    base_path = None

    def __init__(self, base_path: Path) -> None:
        """Initialize the repository while passing the needed path.

        :param base_path: Local OS path on which blueprint files reside.
        """
        self.base_path = base_path

    def __remove_directory_tree(self, full_path: str) -> None:
        """Remove specified path.

        :param full_path: Full path to a directory.
        :raises: FileNotFoundError
        """
        try:
            shutil.rmtree(full_path, ignore_errors=False)
        except OSError:
            raise ArtifactNotFoundError

    def __create_directory_tree(self, full_path: str, mode: int = 0o744, retry_on_error: bool = True) -> None:
        """Create directory or overwrite existing one.

        This method will handle a directory tree creation. If there is a collision
        in directory structure - old directory tree will be removed
        and creation will be attempted one more time. If the creation fails for the second time
        the exception will be raised.

        :param full_path: Full directory tree path (eg. one/two/tree) as string.
        :param mode: Permission mask for the directories.
        :param retry_on_error: Flag that indicates if there should be a attempt to retry the operation.
        """
        try:
            os.makedirs(full_path, mode=mode)
        except FileExistsError:
            # In this case we know that cba of same name and version need to be overwritten
            if retry_on_error:
                self.__remove_directory_tree(full_path)
                self.__create_directory_tree(full_path, mode=mode, retry_on_error=False)
            else:
                # This way we won't try for ever if something goes wrong
                raise ArtifactOverwriteError

    def upload_blueprint(self, cba_bytes: bytes, name: str, version: str) -> None:
        """Store blueprint file in the repository.

        :param cba_bytes: Bytes to save
        :param name: Blueprint name
        :param version: Blueprint version
        """
        temporary_file: BytesIO = BytesIO(cba_bytes)

        if not is_zipfile(temporary_file):
            raise InvalidRequestError

        target_path: str = str(Path(self.base_path.absolute(), name, version))
        self.__create_directory_tree(target_path)

        with ZipFile(temporary_file, "r") as zip_file:  # type: ZipFile
            zip_file.extractall(target_path)

    def download_blueprint(self, name: str, version: str) -> bytes:
        """Download blueprint file from repository.

        This method does the in-memory zipping the files and returns bytes

        :param name: Blueprint name
        :param version: Blueprint version
        :return: Zipped Blueprint file bytes
        """
        temporary_file: BytesIO = BytesIO()
        files_path: str = str(Path(self.base_path.absolute(), name, version))
        if not os.path.exists(files_path):
            raise ArtifactNotFoundError

        with ZipFile(temporary_file, "w") as zip_file:  # type: ZipFile
            for directory_name, subdirectory_names, filenames in os.walk(files_path):  # type: str, list, list
                for filename in filenames:  # type: str
                    zip_file.write(Path(directory_name, filename))

        # Rewind the fake file to allow reading
        temporary_file.seek(0)

        zip_as_bytes: bytes = temporary_file.read()
        temporary_file.close()
        return zip_as_bytes

    def remove_blueprint(self, name: str, version: str) -> None:
        """Remove blueprint file from repository.

        :param name: Blueprint name
        :param version: Blueprint version
        :raises: FileNotFoundError
        """
        files_path: str = str(Path(self.base_path.absolute(), name, version))
        self.__remove_directory_tree(files_path)


class RepositoryStrategy(ABC):
    """Strategy class.

    It has only one public method `get_repository`, which returns valid repository
    instance for the the configuration value.
    You can create many Repository subclasses, but repository clients doesn't have
    to know which one you use.
    """

    @classmethod
    def get_reporitory(cls) -> Repository:
        """Get the valid repository instance for the configuration value.

        Currently it returns FileRepository because it is an only Repository implementation.
        """
        return FileRepository(Path(config["artifactManagerServer"]["fileRepositoryBasePath"]))