Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/gitdb/db/loose.py: 61%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors
2#
3# This module is part of GitDB and is released under
4# the New BSD License: https://opensource.org/license/bsd-3-clause/
5from gitdb.db.base import (
6 FileDBBase,
7 ObjectDBR,
8 ObjectDBW
9)
11from gitdb.exc import (
12 BadObject,
13 AmbiguousObjectName
14)
16from gitdb.stream import (
17 DecompressMemMapReader,
18 FDCompressedSha1Writer,
19 FDStream,
20 Sha1Writer
21)
23from gitdb.base import (
24 OStream,
25 OInfo
26)
28from gitdb.util import (
29 file_contents_ro_filepath,
30 ENOENT,
31 hex_to_bin,
32 bin_to_hex,
33 exists,
34 chmod,
35 isfile,
36 remove,
37 rename,
38 dirname,
39 basename,
40 join
41)
43from gitdb.fun import (
44 chunk_size,
45 loose_object_header_info,
46 write_object,
47 stream_copy
48)
50from gitdb.utils.encoding import force_bytes
52import tempfile
53import os
54import sys
57__all__ = ('LooseObjectDB', )
60class LooseObjectDB(FileDBBase, ObjectDBR, ObjectDBW):
62 """A database which operates on loose object files"""
64 # CONFIGURATION
65 # chunks in which data will be copied between streams
66 stream_chunk_size = chunk_size
68 # On windows we need to keep it writable, otherwise it cannot be removed
69 # either
70 new_objects_mode = int("444", 8)
71 if os.name == 'nt':
72 new_objects_mode = int("644", 8)
74 def __init__(self, root_path):
75 super().__init__(root_path)
76 self._hexsha_to_file = dict()
77 # Additional Flags - might be set to 0 after the first failure
78 # Depending on the root, this might work for some mounts, for others not, which
79 # is why it is per instance
80 self._fd_open_flags = getattr(os, 'O_NOATIME', 0)
82 #{ Interface
83 def object_path(self, hexsha):
84 """
85 :return: path at which the object with the given hexsha would be stored,
86 relative to the database root"""
87 return join(hexsha[:2], hexsha[2:])
89 def readable_db_object_path(self, hexsha):
90 """
91 :return: readable object path to the object identified by hexsha
92 :raise BadObject: If the object file does not exist"""
93 try:
94 return self._hexsha_to_file[hexsha]
95 except KeyError:
96 pass
97 # END ignore cache misses
99 # try filesystem
100 path = self.db_path(self.object_path(hexsha))
101 if exists(path):
102 self._hexsha_to_file[hexsha] = path
103 return path
104 # END handle cache
105 raise BadObject(hexsha)
107 def partial_to_complete_sha_hex(self, partial_hexsha):
108 """:return: 20 byte binary sha1 string which matches the given name uniquely
109 :param name: hexadecimal partial name (bytes or ascii string)
110 :raise AmbiguousObjectName:
111 :raise BadObject: """
112 candidate = None
113 for binsha in self.sha_iter():
114 if bin_to_hex(binsha).startswith(force_bytes(partial_hexsha)):
115 # it can't ever find the same object twice
116 if candidate is not None:
117 raise AmbiguousObjectName(partial_hexsha)
118 candidate = binsha
119 # END for each object
120 if candidate is None:
121 raise BadObject(partial_hexsha)
122 return candidate
124 #} END interface
126 def _map_loose_object(self, sha):
127 """
128 :return: memory map of that file to allow random read access
129 :raise BadObject: if object could not be located"""
130 db_path = self.db_path(self.object_path(bin_to_hex(sha)))
131 try:
132 return file_contents_ro_filepath(db_path, flags=self._fd_open_flags)
133 except OSError as e:
134 if e.errno != ENOENT:
135 # try again without noatime
136 try:
137 return file_contents_ro_filepath(db_path)
138 except OSError as new_e:
139 raise BadObject(sha) from new_e
140 # didn't work because of our flag, don't try it again
141 self._fd_open_flags = 0
142 else:
143 raise BadObject(sha) from e
144 # END handle error
145 # END exception handling
147 def set_ostream(self, stream):
148 """:raise TypeError: if the stream does not support the Sha1Writer interface"""
149 if stream is not None and not isinstance(stream, Sha1Writer):
150 raise TypeError("Output stream musst support the %s interface" % Sha1Writer.__name__)
151 return super().set_ostream(stream)
153 def info(self, sha):
154 m = self._map_loose_object(sha)
155 try:
156 typ, size = loose_object_header_info(m)
157 return OInfo(sha, typ, size)
158 finally:
159 if hasattr(m, 'close'):
160 m.close()
161 # END assure release of system resources
163 def stream(self, sha):
164 m = self._map_loose_object(sha)
165 type, size, stream = DecompressMemMapReader.new(m, close_on_deletion=True)
166 return OStream(sha, type, size, stream)
168 def has_object(self, sha):
169 try:
170 self.readable_db_object_path(bin_to_hex(sha))
171 return True
172 except BadObject:
173 return False
174 # END check existence
176 def store(self, istream):
177 """note: The sha we produce will be hex by nature"""
178 tmp_path = None
179 writer = self.ostream()
180 if writer is None:
181 # open a tmp file to write the data to
182 fd, tmp_path = tempfile.mkstemp(prefix='obj', dir=self._root_path)
184 if istream.binsha is None:
185 writer = FDCompressedSha1Writer(fd)
186 else:
187 writer = FDStream(fd)
188 # END handle direct stream copies
189 # END handle custom writer
191 try:
192 try:
193 if istream.binsha is not None:
194 # copy as much as possible, the actual uncompressed item size might
195 # be smaller than the compressed version
196 stream_copy(istream.read, writer.write, sys.maxsize, self.stream_chunk_size)
197 else:
198 # write object with header, we have to make a new one
199 write_object(istream.type, istream.size, istream.read, writer.write,
200 chunk_size=self.stream_chunk_size)
201 # END handle direct stream copies
202 finally:
203 if tmp_path:
204 writer.close()
205 # END assure target stream is closed
206 except:
207 if tmp_path:
208 os.remove(tmp_path)
209 raise
210 # END assure tmpfile removal on error
212 hexsha = None
213 if istream.binsha:
214 hexsha = istream.hexsha
215 else:
216 hexsha = writer.sha(as_hex=True)
217 # END handle sha
219 if tmp_path:
220 obj_path = self.db_path(self.object_path(hexsha))
221 obj_dir = dirname(obj_path)
222 os.makedirs(obj_dir, exist_ok=True)
223 # END handle destination directory
224 # rename onto existing doesn't work on NTFS
225 if isfile(obj_path):
226 remove(tmp_path)
227 else:
228 rename(tmp_path, obj_path)
229 # end rename only if needed
231 # make sure its readable for all ! It started out as rw-- tmp file
232 # but needs to be rwrr
233 chmod(obj_path, self.new_objects_mode)
234 # END handle dry_run
236 istream.binsha = hex_to_bin(hexsha)
237 return istream
239 def sha_iter(self):
240 # find all files which look like an object, extract sha from there
241 for root, dirs, files in os.walk(self.root_path()):
242 root_base = basename(root)
243 if len(root_base) != 2:
244 continue
246 for f in files:
247 if len(f) != 38:
248 continue
249 yield hex_to_bin(root_base + f)
250 # END for each file
251 # END for each walk iteration
253 def size(self):
254 return len(tuple(self.sha_iter()))