[RIFT 16087] Backend changes to decouple storage semantics from user interface. Chang...
[osm/SO.git] / rwlaunchpad / plugins / rwlaunchpadtasklet / rift / package / archive.py
1
2 #
3 # Copyright 2016 RIFT.IO Inc
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 #
17
18 import io
19 import os
20 import tarfile
21 import time
22
23 from . import package
24
25 class ArchiveError(Exception):
26 pass
27
28
29 def get_size(hdl):
30 """ Get number of bytes of content within file hdl
31 Set the file position to original position before returning
32
33 Returns:
34 Number of bytes in the hdl file object
35 """
36 old_pos = hdl.tell()
37 hdl.seek(0, os.SEEK_END)
38 size = hdl.tell()
39 hdl.seek(old_pos)
40
41 return size
42
43
44 class TarPackageArchive(object):
45 """ This class represents a package stored within a tar.gz archive file """
46 def __init__(self, log, tar_file_hdl, mode="r"):
47 self._log = log
48 self._tar_filehdl = tar_file_hdl
49 self._tar_infos = {}
50
51 self._tarfile = tarfile.open(fileobj=tar_file_hdl, mode=mode)
52
53 self.load_archive()
54
55 @classmethod
56 def from_package(cls, log, pkg, tar_file_hdl, top_level_dir=None):
57 """ Creates a TarPackageArchive from a existing Package
58
59 Arguments:
60 log - logger
61 pkg - a DescriptorPackage instance
62 tar_file_hdl - a writeable file handle to write tar archive data
63 top_level_dir - (opt.) top level dir under which the archive will be extracted
64
65 Returns:
66 A TarPackageArchive instance
67 """
68
69 def set_common_tarinfo_fields(tar_info):
70 tar_info.uid = os.getuid()
71 tar_info.gid = os.getgid()
72 tar_info.mtime = time.time()
73 tar_info.uname = "rift"
74 tar_info.gname = "rift"
75
76 archive = TarPackageArchive(log, tar_file_hdl, mode='w:gz')
77
78 for pkg_file in pkg.files:
79 filename = "%s/%s" % (top_level_dir, pkg_file) if top_level_dir else pkg_file
80 tar_info = tarfile.TarInfo(name=filename)
81 tar_info.type = tarfile.REGTYPE
82 tar_info.mode = pkg.get_file_mode(pkg_file)
83 set_common_tarinfo_fields(tar_info)
84 with pkg.open(pkg_file) as pkg_file_hdl:
85 tar_info.size = get_size(pkg_file_hdl)
86 archive.tarfile.addfile(tar_info, pkg_file_hdl)
87
88 for pkg_dir in pkg.dirs:
89 dirname = "%s/%s" % (top_level_dir, pkg_dir) if top_level_dir else pkg_dir
90 tar_info = tarfile.TarInfo(name=dirname)
91 tar_info.type = tarfile.DIRTYPE
92 tar_info.mode = 0o775
93 set_common_tarinfo_fields(tar_info)
94 archive.tarfile.addfile(tar_info)
95
96 archive.load_archive()
97 archive.close()
98
99 return archive
100
101 def __repr__(self):
102 return "TarPackageArchive(%s)" % self._tar_filehdl
103
104 def __del__(self):
105 self.close()
106
107 def close(self):
108 """ Close the opened tarfile"""
109 if self._tarfile is not None:
110 self._tarfile.close()
111 self._tarfile = None
112
113 def load_archive(self):
114 self._tar_infos = {info.name: info for info in self._tarfile.getmembers() if info.name}
115
116 @property
117 def tarfile(self):
118 return self._tarfile
119
120 @property
121 def filenames(self):
122 """ The list of file members within the tar file """
123 return [name for name in self._tar_infos if tarfile.TarInfo.isfile(self._tar_infos[name])]
124
125 def open_file(self, rel_file_path):
126 """ Opens a file within the archive as read-only, byte mode.
127
128 Arguments:
129 rel_file_path - The file path within the archive to open
130
131 Returns:
132 A file like object (see tarfile.extractfile())
133
134 Raises:
135 FileNotFoundError - The file could not be found within the archive.
136 ArchiveError - The file could not be opened for some generic reason.
137 """
138 if rel_file_path not in self._tar_infos:
139 raise FileNotFoundError("Could not find %s in tar file", rel_file_path)
140
141 try:
142 return self._tarfile.extractfile(rel_file_path)
143 except tarfile.TarError as e:
144 msg = "Failed to read file {} from tarfile {}: {}".format(
145 rel_file_path, self._tar_filehdl, str(e)
146 )
147 self._log.error(msg)
148 raise ArchiveError(msg) from e
149
150 def create_package(self):
151 """ Creates a Descriptor package from the archive contents """
152 pkg = package.DescriptorPackage.from_package_files(self._log, self.open_file, self.filenames)
153 for pkg_file in self.filenames:
154 pkg.add_file(pkg_file, self._tar_infos[pkg_file].mode)
155
156 return pkg