1 """Copyright 2019 Deutsche Telekom.
3 Licensed under the Apache License, Version 2.0 (the "License");
4 you may not use this file except in compliance with the License.
5 You may obtain a copy of the License at
7 http://www.apache.org/licenses/LICENSE-2.0
9 Unless required by applicable law or agreed to in writing, software
10 distributed under the License is distributed on an "AS IS" BASIS,
11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 See the License for the specific language governing permissions and
13 limitations under the License.
18 from abc import ABC, abstractmethod
19 from io import BytesIO
20 from pathlib import Path
21 from zipfile import ZipFile, is_zipfile
23 from manager.configuration import config
24 from manager.errors import ArtifactNotFoundError, ArtifactOverwriteError, InvalidRequestError
27 class Repository(ABC):
28 """Abstract repository class.
30 Defines repository methods.
34 def upload_blueprint(self, file: bytes, name: str, version: str) -> None:
35 """Store blueprint file in the repository.
37 :param file: File to save
38 :param name: Blueprint name
39 :param version: Blueprint version
43 def download_blueprint(self, name: str, version: str) -> bytes:
44 """Download blueprint file from repository.
46 :param name: Blueprint name
47 :param version: Blueprint version
48 :return: Zipped Blueprint file bytes
52 def remove_blueprint(self, name: str, version: str) -> None:
53 """Remove blueprint file from repository.
55 :param name: Blueprint name
56 :param version: Blueprint version
60 class FileRepository(Repository):
61 """Store blueprints on local directory."""
65 def __init__(self, base_path: Path) -> None:
66 """Initialize the repository while passing the needed path.
68 :param base_path: Local OS path on which blueprint files reside.
70 self.base_path = base_path
72 def __remove_directory_tree(self, full_path: str) -> None:
73 """Remove specified path.
75 :param full_path: Full path to a directory.
76 :raises: FileNotFoundError
79 shutil.rmtree(full_path, ignore_errors=False)
81 raise ArtifactNotFoundError
83 def __create_directory_tree(self, full_path: str, mode: int = 0o744, retry_on_error: bool = True) -> None:
84 """Create directory or overwrite existing one.
86 This method will handle a directory tree creation. If there is a collision
87 in directory structure - old directory tree will be removed
88 and creation will be attempted one more time. If the creation fails for the second time
89 the exception will be raised.
91 :param full_path: Full directory tree path (eg. one/two/tree) as string.
92 :param mode: Permission mask for the directories.
93 :param retry_on_error: Flag that indicates if there should be a attempt to retry the operation.
96 os.makedirs(full_path, mode=mode)
97 except FileExistsError:
98 # In this case we know that cba of same name and version need to be overwritten
100 self.__remove_directory_tree(full_path)
101 self.__create_directory_tree(full_path, mode=mode, retry_on_error=False)
103 # This way we won't try for ever if something goes wrong
104 raise ArtifactOverwriteError
106 def upload_blueprint(self, cba_bytes: bytes, name: str, version: str) -> None:
107 """Store blueprint file in the repository.
109 :param cba_bytes: Bytes to save
110 :param name: Blueprint name
111 :param version: Blueprint version
113 temporary_file: BytesIO = BytesIO(cba_bytes)
115 if not is_zipfile(temporary_file):
116 raise InvalidRequestError
118 target_path: str = str(Path(self.base_path.absolute(), name, version))
119 self.__create_directory_tree(target_path)
121 with ZipFile(temporary_file, "r") as zip_file: # type: ZipFile
122 zip_file.extractall(target_path)
124 def download_blueprint(self, name: str, version: str) -> bytes:
125 """Download blueprint file from repository.
127 This method does the in-memory zipping the files and returns bytes
129 :param name: Blueprint name
130 :param version: Blueprint version
131 :return: Zipped Blueprint file bytes
133 temporary_file: BytesIO = BytesIO()
134 files_path: str = str(Path(self.base_path.absolute(), name, version))
135 if not os.path.exists(files_path):
136 raise ArtifactNotFoundError
138 with ZipFile(temporary_file, "w") as zip_file: # type: ZipFile
139 for directory_name, subdirectory_names, filenames in os.walk(files_path): # type: str, list, list
140 for filename in filenames: # type: str
141 zip_file.write(Path(directory_name, filename))
143 # Rewind the fake file to allow reading
144 temporary_file.seek(0)
146 zip_as_bytes: bytes = temporary_file.read()
147 temporary_file.close()
150 def remove_blueprint(self, name: str, version: str) -> None:
151 """Remove blueprint file from repository.
153 :param name: Blueprint name
154 :param version: Blueprint version
155 :raises: FileNotFoundError
157 files_path: str = str(Path(self.base_path.absolute(), name, version))
158 self.__remove_directory_tree(files_path)
161 class RepositoryStrategy(ABC):
164 It has only one public method `get_repository`, which returns valid repository
165 instance for the the configuration value.
166 You can create many Repository subclasses, but repository clients doesn't have
167 to know which one you use.
171 def get_reporitory(cls) -> Repository:
172 """Get the valid repository instance for the configuration value.
174 Currently it returns FileRepository because it is an only Repository implementation.
176 return FileRepository(Path(config["artifactManagerServer"]["fileRepositoryBasePath"]))