swift.py 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037
  1. # swift.py -- Repo implementation atop OpenStack SWIFT
  2. # Copyright (C) 2013 eNovance SAS <licensing@enovance.com>
  3. #
  4. # Author: Fabien Boucher <fabien.boucher@enovance.com>
  5. #
  6. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
  7. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  8. # General Public License as public by the Free Software Foundation; version 2.0
  9. # or (at your option) any later version. You can redistribute it and/or
  10. # modify it under the terms of either of these two licenses.
  11. #
  12. # Unless required by applicable law or agreed to in writing, software
  13. # distributed under the License is distributed on an "AS IS" BASIS,
  14. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. # See the License for the specific language governing permissions and
  16. # limitations under the License.
  17. #
  18. # You should have received a copy of the licenses; if not, see
  19. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  20. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  21. # License, Version 2.0.
  22. #
  23. """Repo implementation atop OpenStack SWIFT."""
  24. # TODO: Refactor to share more code with dulwich/repo.py.
  25. # TODO(fbo): Second attempt to _send() must be notified via real log
  26. # TODO(fbo): More logs for operations
  27. import json
  28. import os
  29. import posixpath
  30. import stat
  31. import sys
  32. import tempfile
  33. import urllib.parse as urlparse
  34. import zlib
  35. from configparser import ConfigParser
  36. from io import BytesIO
  37. from typing import Optional
  38. from geventhttpclient import HTTPClient
  39. from ..greenthreads import GreenThreadsMissingObjectFinder
  40. from ..lru_cache import LRUSizeCache
  41. from ..object_store import INFODIR, PACKDIR, PackBasedObjectStore
  42. from ..objects import S_ISGITLINK, Blob, Commit, Tag, Tree
  43. from ..pack import (
  44. Pack,
  45. PackData,
  46. PackIndexer,
  47. PackStreamCopier,
  48. _compute_object_size,
  49. compute_file_sha,
  50. iter_sha1,
  51. load_pack_index_file,
  52. read_pack_header,
  53. unpack_object,
  54. write_pack_header,
  55. write_pack_index_v2,
  56. write_pack_object,
  57. )
  58. from ..protocol import TCP_GIT_PORT
  59. from ..refs import InfoRefsContainer, read_info_refs, split_peeled_refs, write_info_refs
  60. from ..repo import OBJECTDIR, BaseRepo
  61. from ..server import Backend, TCPGitServer
  62. """
  63. # Configuration file sample
  64. [swift]
  65. # Authentication URL (Keystone or Swift)
  66. auth_url = http://127.0.0.1:5000/v2.0
  67. # Authentication version to use
  68. auth_ver = 2
  69. # The tenant and username separated by a semicolon
  70. username = admin;admin
  71. # The user password
  72. password = pass
  73. # The Object storage region to use (auth v2) (Default RegionOne)
  74. region_name = RegionOne
  75. # The Object storage endpoint URL to use (auth v2) (Default internalURL)
  76. endpoint_type = internalURL
  77. # Concurrency to use for parallel tasks (Default 10)
  78. concurrency = 10
  79. # Size of the HTTP pool (Default 10)
  80. http_pool_length = 10
  81. # Timeout delay for HTTP connections (Default 20)
  82. http_timeout = 20
  83. # Chunk size to read from pack (Bytes) (Default 12228)
  84. chunk_length = 12228
  85. # Cache size (MBytes) (Default 20)
  86. cache_length = 20
  87. """
  88. class PackInfoMissingObjectFinder(GreenThreadsMissingObjectFinder):
  89. def next(self):
  90. while True:
  91. if not self.objects_to_send:
  92. return None
  93. (sha, name, leaf) = self.objects_to_send.pop()
  94. if sha not in self.sha_done:
  95. break
  96. if not leaf:
  97. info = self.object_store.pack_info_get(sha)
  98. if info[0] == Commit.type_num:
  99. self.add_todo([(info[2], "", False)])
  100. elif info[0] == Tree.type_num:
  101. self.add_todo([tuple(i) for i in info[1]])
  102. elif info[0] == Tag.type_num:
  103. self.add_todo([(info[1], None, False)])
  104. if sha in self._tagged:
  105. self.add_todo([(self._tagged[sha], None, True)])
  106. self.sha_done.add(sha)
  107. self.progress("counting objects: %d\r" % len(self.sha_done))
  108. return (sha, name)
  109. def load_conf(path=None, file=None):
  110. """Load configuration in global var CONF.
  111. Args:
  112. path: The path to the configuration file
  113. file: If provided read instead the file like object
  114. """
  115. conf = ConfigParser()
  116. if file:
  117. try:
  118. conf.read_file(file, path)
  119. except AttributeError:
  120. # read_file only exists in Python3
  121. conf.readfp(file)
  122. return conf
  123. confpath = None
  124. if not path:
  125. try:
  126. confpath = os.environ["DULWICH_SWIFT_CFG"]
  127. except KeyError as exc:
  128. raise Exception("You need to specify a configuration file") from exc
  129. else:
  130. confpath = path
  131. if not os.path.isfile(confpath):
  132. raise Exception(f"Unable to read configuration file {confpath}")
  133. conf.read(confpath)
  134. return conf
  135. def swift_load_pack_index(scon, filename):
  136. """Read a pack index file from Swift.
  137. Args:
  138. scon: a `SwiftConnector` instance
  139. filename: Path to the index file objectise
  140. Returns: a `PackIndexer` instance
  141. """
  142. with scon.get_object(filename) as f:
  143. return load_pack_index_file(filename, f)
  144. def pack_info_create(pack_data, pack_index):
  145. pack = Pack.from_objects(pack_data, pack_index)
  146. info = {}
  147. for obj in pack.iterobjects():
  148. # Commit
  149. if obj.type_num == Commit.type_num:
  150. info[obj.id] = (obj.type_num, obj.parents, obj.tree)
  151. # Tree
  152. elif obj.type_num == Tree.type_num:
  153. shas = [
  154. (s, n, not stat.S_ISDIR(m))
  155. for n, m, s in obj.items()
  156. if not S_ISGITLINK(m)
  157. ]
  158. info[obj.id] = (obj.type_num, shas)
  159. # Blob
  160. elif obj.type_num == Blob.type_num:
  161. info[obj.id] = None
  162. # Tag
  163. elif obj.type_num == Tag.type_num:
  164. info[obj.id] = (obj.type_num, obj.object[1])
  165. return zlib.compress(json.dumps(info))
  166. def load_pack_info(filename, scon=None, file=None):
  167. if not file:
  168. f = scon.get_object(filename)
  169. else:
  170. f = file
  171. if not f:
  172. return None
  173. try:
  174. return json.loads(zlib.decompress(f.read()))
  175. finally:
  176. f.close()
  177. class SwiftException(Exception):
  178. pass
  179. class SwiftConnector:
  180. """A Connector to swift that manage authentication and errors catching."""
  181. def __init__(self, root, conf) -> None:
  182. """Initialize a SwiftConnector.
  183. Args:
  184. root: The swift container that will act as Git bare repository
  185. conf: A ConfigParser Object
  186. """
  187. self.conf = conf
  188. self.auth_ver = self.conf.get("swift", "auth_ver")
  189. if self.auth_ver not in ["1", "2"]:
  190. raise NotImplementedError("Wrong authentication version use either 1 or 2")
  191. self.auth_url = self.conf.get("swift", "auth_url")
  192. self.user = self.conf.get("swift", "username")
  193. self.password = self.conf.get("swift", "password")
  194. self.concurrency = self.conf.getint("swift", "concurrency") or 10
  195. self.http_timeout = self.conf.getint("swift", "http_timeout") or 20
  196. self.http_pool_length = self.conf.getint("swift", "http_pool_length") or 10
  197. self.region_name = self.conf.get("swift", "region_name") or "RegionOne"
  198. self.endpoint_type = self.conf.get("swift", "endpoint_type") or "internalURL"
  199. self.cache_length = self.conf.getint("swift", "cache_length") or 20
  200. self.chunk_length = self.conf.getint("swift", "chunk_length") or 12228
  201. self.root = root
  202. block_size = 1024 * 12 # 12KB
  203. if self.auth_ver == "1":
  204. self.storage_url, self.token = self.swift_auth_v1()
  205. else:
  206. self.storage_url, self.token = self.swift_auth_v2()
  207. token_header = {"X-Auth-Token": str(self.token)}
  208. self.httpclient = HTTPClient.from_url(
  209. str(self.storage_url),
  210. concurrency=self.http_pool_length,
  211. block_size=block_size,
  212. connection_timeout=self.http_timeout,
  213. network_timeout=self.http_timeout,
  214. headers=token_header,
  215. )
  216. self.base_path = str(
  217. posixpath.join(urlparse.urlparse(self.storage_url).path, self.root)
  218. )
  219. def swift_auth_v1(self):
  220. self.user = self.user.replace(";", ":")
  221. auth_httpclient = HTTPClient.from_url(
  222. self.auth_url,
  223. connection_timeout=self.http_timeout,
  224. network_timeout=self.http_timeout,
  225. )
  226. headers = {"X-Auth-User": self.user, "X-Auth-Key": self.password}
  227. path = urlparse.urlparse(self.auth_url).path
  228. ret = auth_httpclient.request("GET", path, headers=headers)
  229. # Should do something with redirections (301 in my case)
  230. if ret.status_code < 200 or ret.status_code >= 300:
  231. raise SwiftException(
  232. "AUTH v1.0 request failed on "
  233. + f"{self.auth_url} with error code {ret.status_code} ({ret.items()!s})"
  234. )
  235. storage_url = ret["X-Storage-Url"]
  236. token = ret["X-Auth-Token"]
  237. return storage_url, token
  238. def swift_auth_v2(self):
  239. self.tenant, self.user = self.user.split(";")
  240. auth_dict = {}
  241. auth_dict["auth"] = {
  242. "passwordCredentials": {
  243. "username": self.user,
  244. "password": self.password,
  245. },
  246. "tenantName": self.tenant,
  247. }
  248. auth_json = json.dumps(auth_dict)
  249. headers = {"Content-Type": "application/json"}
  250. auth_httpclient = HTTPClient.from_url(
  251. self.auth_url,
  252. connection_timeout=self.http_timeout,
  253. network_timeout=self.http_timeout,
  254. )
  255. path = urlparse.urlparse(self.auth_url).path
  256. if not path.endswith("tokens"):
  257. path = posixpath.join(path, "tokens")
  258. ret = auth_httpclient.request("POST", path, body=auth_json, headers=headers)
  259. if ret.status_code < 200 or ret.status_code >= 300:
  260. raise SwiftException(
  261. "AUTH v2.0 request failed on "
  262. + f"{str(auth_httpclient.get_base_url()) + path} with error code {ret.status_code} ({ret.items()!s})"
  263. )
  264. auth_ret_json = json.loads(ret.read())
  265. token = auth_ret_json["access"]["token"]["id"]
  266. catalogs = auth_ret_json["access"]["serviceCatalog"]
  267. object_store = next(
  268. o_store for o_store in catalogs if o_store["type"] == "object-store"
  269. )
  270. endpoints = object_store["endpoints"]
  271. endpoint = next(
  272. endp for endp in endpoints if endp["region"] == self.region_name
  273. )
  274. return endpoint[self.endpoint_type], token
  275. def test_root_exists(self) -> Optional[bool]:
  276. """Check that Swift container exist.
  277. Returns: True if exist or None it not
  278. """
  279. ret = self.httpclient.request("HEAD", self.base_path)
  280. if ret.status_code == 404:
  281. return None
  282. if ret.status_code < 200 or ret.status_code > 300:
  283. raise SwiftException(
  284. f"HEAD request failed with error code {ret.status_code}"
  285. )
  286. return True
  287. def create_root(self) -> None:
  288. """Create the Swift container.
  289. Raises:
  290. SwiftException: if unable to create
  291. """
  292. if not self.test_root_exists():
  293. ret = self.httpclient.request("PUT", self.base_path)
  294. if ret.status_code < 200 or ret.status_code > 300:
  295. raise SwiftException(
  296. f"PUT request failed with error code {ret.status_code}"
  297. )
  298. def get_container_objects(self):
  299. """Retrieve objects list in a container.
  300. Returns: A list of dict that describe objects
  301. or None if container does not exist
  302. """
  303. qs = "?format=json"
  304. path = self.base_path + qs
  305. ret = self.httpclient.request("GET", path)
  306. if ret.status_code == 404:
  307. return None
  308. if ret.status_code < 200 or ret.status_code > 300:
  309. raise SwiftException(
  310. f"GET request failed with error code {ret.status_code}"
  311. )
  312. content = ret.read()
  313. return json.loads(content)
  314. def get_object_stat(self, name):
  315. """Retrieve object stat.
  316. Args:
  317. name: The object name
  318. Returns:
  319. A dict that describe the object or None if object does not exist
  320. """
  321. path = self.base_path + "/" + name
  322. ret = self.httpclient.request("HEAD", path)
  323. if ret.status_code == 404:
  324. return None
  325. if ret.status_code < 200 or ret.status_code > 300:
  326. raise SwiftException(
  327. f"HEAD request failed with error code {ret.status_code}"
  328. )
  329. resp_headers = {}
  330. for header, value in ret.items():
  331. resp_headers[header.lower()] = value
  332. return resp_headers
  333. def put_object(self, name, content) -> None:
  334. """Put an object.
  335. Args:
  336. name: The object name
  337. content: A file object
  338. Raises:
  339. SwiftException: if unable to create
  340. """
  341. content.seek(0)
  342. data = content.read()
  343. path = self.base_path + "/" + name
  344. headers = {"Content-Length": str(len(data))}
  345. def _send():
  346. ret = self.httpclient.request("PUT", path, body=data, headers=headers)
  347. return ret
  348. try:
  349. # Sometime got Broken Pipe - Dirty workaround
  350. ret = _send()
  351. except Exception:
  352. # Second attempt work
  353. ret = _send()
  354. if ret.status_code < 200 or ret.status_code > 300:
  355. raise SwiftException(
  356. f"PUT request failed with error code {ret.status_code}"
  357. )
  358. def get_object(self, name, range=None):
  359. """Retrieve an object.
  360. Args:
  361. name: The object name
  362. range: A string range like "0-10" to
  363. retrieve specified bytes in object content
  364. Returns:
  365. A file like instance or bytestring if range is specified
  366. """
  367. headers = {}
  368. if range:
  369. headers["Range"] = f"bytes={range}"
  370. path = self.base_path + "/" + name
  371. ret = self.httpclient.request("GET", path, headers=headers)
  372. if ret.status_code == 404:
  373. return None
  374. if ret.status_code < 200 or ret.status_code > 300:
  375. raise SwiftException(
  376. f"GET request failed with error code {ret.status_code}"
  377. )
  378. content = ret.read()
  379. if range:
  380. return content
  381. return BytesIO(content)
  382. def del_object(self, name) -> None:
  383. """Delete an object.
  384. Args:
  385. name: The object name
  386. Raises:
  387. SwiftException: if unable to delete
  388. """
  389. path = self.base_path + "/" + name
  390. ret = self.httpclient.request("DELETE", path)
  391. if ret.status_code < 200 or ret.status_code > 300:
  392. raise SwiftException(
  393. f"DELETE request failed with error code {ret.status_code}"
  394. )
  395. def del_root(self) -> None:
  396. """Delete the root container by removing container content.
  397. Raises:
  398. SwiftException: if unable to delete
  399. """
  400. for obj in self.get_container_objects():
  401. self.del_object(obj["name"])
  402. ret = self.httpclient.request("DELETE", self.base_path)
  403. if ret.status_code < 200 or ret.status_code > 300:
  404. raise SwiftException(
  405. f"DELETE request failed with error code {ret.status_code}"
  406. )
  407. class SwiftPackReader:
  408. """A SwiftPackReader that mimic read and sync method.
  409. The reader allows to read a specified amount of bytes from
  410. a given offset of a Swift object. A read offset is kept internally.
  411. The reader will read from Swift a specified amount of data to complete
  412. its internal buffer. chunk_length specify the amount of data
  413. to read from Swift.
  414. """
  415. def __init__(self, scon, filename, pack_length) -> None:
  416. """Initialize a SwiftPackReader.
  417. Args:
  418. scon: a `SwiftConnector` instance
  419. filename: the pack filename
  420. pack_length: The size of the pack object
  421. """
  422. self.scon = scon
  423. self.filename = filename
  424. self.pack_length = pack_length
  425. self.offset = 0
  426. self.base_offset = 0
  427. self.buff = b""
  428. self.buff_length = self.scon.chunk_length
  429. def _read(self, more=False) -> None:
  430. if more:
  431. self.buff_length = self.buff_length * 2
  432. offset = self.base_offset
  433. r = min(self.base_offset + self.buff_length, self.pack_length)
  434. ret = self.scon.get_object(self.filename, range=f"{offset}-{r}")
  435. self.buff = ret
  436. def read(self, length):
  437. """Read a specified amount of Bytes form the pack object.
  438. Args:
  439. length: amount of bytes to read
  440. Returns:
  441. a bytestring
  442. """
  443. end = self.offset + length
  444. if self.base_offset + end > self.pack_length:
  445. data = self.buff[self.offset :]
  446. self.offset = end
  447. return data
  448. if end > len(self.buff):
  449. # Need to read more from swift
  450. self._read(more=True)
  451. return self.read(length)
  452. data = self.buff[self.offset : end]
  453. self.offset = end
  454. return data
  455. def seek(self, offset) -> None:
  456. """Seek to a specified offset.
  457. Args:
  458. offset: the offset to seek to
  459. """
  460. self.base_offset = offset
  461. self._read()
  462. self.offset = 0
  463. def read_checksum(self):
  464. """Read the checksum from the pack.
  465. Returns: the checksum bytestring
  466. """
  467. return self.scon.get_object(self.filename, range="-20")
  468. class SwiftPackData(PackData):
  469. """The data contained in a packfile.
  470. We use the SwiftPackReader to read bytes from packs stored in Swift
  471. using the Range header feature of Swift.
  472. """
  473. def __init__(self, scon, filename) -> None:
  474. """Initialize a SwiftPackReader.
  475. Args:
  476. scon: a `SwiftConnector` instance
  477. filename: the pack filename
  478. """
  479. self.scon = scon
  480. self._filename = filename
  481. self._header_size = 12
  482. headers = self.scon.get_object_stat(self._filename)
  483. self.pack_length = int(headers["content-length"])
  484. pack_reader = SwiftPackReader(self.scon, self._filename, self.pack_length)
  485. (version, self._num_objects) = read_pack_header(pack_reader.read)
  486. self._offset_cache = LRUSizeCache(
  487. 1024 * 1024 * self.scon.cache_length,
  488. compute_size=_compute_object_size,
  489. )
  490. self.pack = None
  491. def get_object_at(self, offset):
  492. if offset in self._offset_cache:
  493. return self._offset_cache[offset]
  494. assert offset >= self._header_size
  495. pack_reader = SwiftPackReader(self.scon, self._filename, self.pack_length)
  496. pack_reader.seek(offset)
  497. unpacked, _ = unpack_object(pack_reader.read)
  498. return (unpacked.pack_type_num, unpacked._obj())
  499. def get_stored_checksum(self):
  500. pack_reader = SwiftPackReader(self.scon, self._filename, self.pack_length)
  501. return pack_reader.read_checksum()
  502. def close(self) -> None:
  503. pass
  504. class SwiftPack(Pack):
  505. """A Git pack object.
  506. Same implementation as pack.Pack except that _idx_load and
  507. _data_load are bounded to Swift version of load_pack_index and
  508. PackData.
  509. """
  510. def __init__(self, *args, **kwargs) -> None:
  511. self.scon = kwargs["scon"]
  512. del kwargs["scon"]
  513. super().__init__(*args, **kwargs)
  514. self._pack_info_path = self._basename + ".info"
  515. self._pack_info = None
  516. self._pack_info_load = lambda: load_pack_info(self._pack_info_path, self.scon)
  517. self._idx_load = lambda: swift_load_pack_index(self.scon, self._idx_path)
  518. self._data_load = lambda: SwiftPackData(self.scon, self._data_path)
  519. @property
  520. def pack_info(self):
  521. """The pack data object being used."""
  522. if self._pack_info is None:
  523. self._pack_info = self._pack_info_load()
  524. return self._pack_info
  525. class SwiftObjectStore(PackBasedObjectStore):
  526. """A Swift Object Store.
  527. Allow to manage a bare Git repository from Openstack Swift.
  528. This object store only supports pack files and not loose objects.
  529. """
  530. def __init__(self, scon) -> None:
  531. """Open a Swift object store.
  532. Args:
  533. scon: A `SwiftConnector` instance
  534. """
  535. super().__init__()
  536. self.scon = scon
  537. self.root = self.scon.root
  538. self.pack_dir = posixpath.join(OBJECTDIR, PACKDIR)
  539. self._alternates = None
  540. def _update_pack_cache(self):
  541. objects = self.scon.get_container_objects()
  542. pack_files = [
  543. o["name"].replace(".pack", "")
  544. for o in objects
  545. if o["name"].endswith(".pack")
  546. ]
  547. ret = []
  548. for basename in pack_files:
  549. pack = SwiftPack(basename, scon=self.scon)
  550. self._pack_cache[basename] = pack
  551. ret.append(pack)
  552. return ret
  553. def _iter_loose_objects(self):
  554. """Loose objects are not supported by this repository."""
  555. return []
  556. def pack_info_get(self, sha):
  557. for pack in self.packs:
  558. if sha in pack:
  559. return pack.pack_info[sha]
  560. def _collect_ancestors(self, heads, common=set()):
  561. def _find_parents(commit):
  562. for pack in self.packs:
  563. if commit in pack:
  564. try:
  565. parents = pack.pack_info[commit][1]
  566. except KeyError:
  567. # Seems to have no parents
  568. return []
  569. return parents
  570. bases = set()
  571. commits = set()
  572. queue = []
  573. queue.extend(heads)
  574. while queue:
  575. e = queue.pop(0)
  576. if e in common:
  577. bases.add(e)
  578. elif e not in commits:
  579. commits.add(e)
  580. parents = _find_parents(e)
  581. queue.extend(parents)
  582. return (commits, bases)
  583. def add_pack(self):
  584. """Add a new pack to this object store.
  585. Returns: Fileobject to write to and a commit function to
  586. call when the pack is finished.
  587. """
  588. f = BytesIO()
  589. def commit():
  590. f.seek(0)
  591. pack = PackData(file=f, filename="")
  592. entries = pack.sorted_entries()
  593. if entries:
  594. basename = posixpath.join(
  595. self.pack_dir,
  596. f"pack-{iter_sha1(entry[0] for entry in entries)}",
  597. )
  598. index = BytesIO()
  599. write_pack_index_v2(index, entries, pack.get_stored_checksum())
  600. self.scon.put_object(basename + ".pack", f)
  601. f.close()
  602. self.scon.put_object(basename + ".idx", index)
  603. index.close()
  604. final_pack = SwiftPack(basename, scon=self.scon)
  605. final_pack.check_length_and_checksum()
  606. self._add_cached_pack(basename, final_pack)
  607. return final_pack
  608. else:
  609. return None
  610. def abort() -> None:
  611. pass
  612. return f, commit, abort
  613. def add_object(self, obj) -> None:
  614. self.add_objects(
  615. [
  616. (obj, None),
  617. ]
  618. )
  619. def _pack_cache_stale(self) -> bool:
  620. return False
  621. def _get_loose_object(self, sha) -> None:
  622. return None
  623. def add_thin_pack(self, read_all, read_some):
  624. """Read a thin pack.
  625. Read it from a stream and complete it in a temporary file.
  626. Then the pack and the corresponding index file are uploaded to Swift.
  627. """
  628. fd, path = tempfile.mkstemp(prefix="tmp_pack_")
  629. f = os.fdopen(fd, "w+b")
  630. try:
  631. indexer = PackIndexer(f, resolve_ext_ref=self.get_raw)
  632. copier = PackStreamCopier(read_all, read_some, f, delta_iter=indexer)
  633. copier.verify()
  634. return self._complete_thin_pack(f, path, copier, indexer)
  635. finally:
  636. f.close()
  637. os.unlink(path)
  638. def _complete_thin_pack(self, f, path, copier, indexer):
  639. entries = list(indexer)
  640. # Update the header with the new number of objects.
  641. f.seek(0)
  642. write_pack_header(f, len(entries) + len(indexer.ext_refs()))
  643. # Must flush before reading (http://bugs.python.org/issue3207)
  644. f.flush()
  645. # Rescan the rest of the pack, computing the SHA with the new header.
  646. new_sha = compute_file_sha(f, end_ofs=-20)
  647. # Must reposition before writing (http://bugs.python.org/issue3207)
  648. f.seek(0, os.SEEK_CUR)
  649. # Complete the pack.
  650. for ext_sha in indexer.ext_refs():
  651. assert len(ext_sha) == 20
  652. type_num, data = self.get_raw(ext_sha)
  653. offset = f.tell()
  654. crc32 = write_pack_object(f, type_num, data, sha=new_sha)
  655. entries.append((ext_sha, offset, crc32))
  656. pack_sha = new_sha.digest()
  657. f.write(pack_sha)
  658. f.flush()
  659. # Move the pack in.
  660. entries.sort()
  661. pack_base_name = posixpath.join(
  662. self.pack_dir,
  663. "pack-" + os.fsdecode(iter_sha1(e[0] for e in entries)),
  664. )
  665. self.scon.put_object(pack_base_name + ".pack", f)
  666. # Write the index.
  667. filename = pack_base_name + ".idx"
  668. index_file = BytesIO()
  669. write_pack_index_v2(index_file, entries, pack_sha)
  670. self.scon.put_object(filename, index_file)
  671. # Write pack info.
  672. f.seek(0)
  673. pack_data = PackData(filename="", file=f)
  674. index_file.seek(0)
  675. pack_index = load_pack_index_file("", index_file)
  676. serialized_pack_info = pack_info_create(pack_data, pack_index)
  677. f.close()
  678. index_file.close()
  679. pack_info_file = BytesIO(serialized_pack_info)
  680. filename = pack_base_name + ".info"
  681. self.scon.put_object(filename, pack_info_file)
  682. pack_info_file.close()
  683. # Add the pack to the store and return it.
  684. final_pack = SwiftPack(pack_base_name, scon=self.scon)
  685. final_pack.check_length_and_checksum()
  686. self._add_cached_pack(pack_base_name, final_pack)
  687. return final_pack
  688. class SwiftInfoRefsContainer(InfoRefsContainer):
  689. """Manage references in info/refs object."""
  690. def __init__(self, scon, store) -> None:
  691. self.scon = scon
  692. self.filename = "info/refs"
  693. self.store = store
  694. f = self.scon.get_object(self.filename)
  695. if not f:
  696. f = BytesIO(b"")
  697. super().__init__(f)
  698. def _load_check_ref(self, name, old_ref):
  699. self._check_refname(name)
  700. f = self.scon.get_object(self.filename)
  701. if not f:
  702. return {}
  703. refs = read_info_refs(f)
  704. (refs, peeled) = split_peeled_refs(refs)
  705. if old_ref is not None:
  706. if refs[name] != old_ref:
  707. return False
  708. return refs
  709. def _write_refs(self, refs) -> None:
  710. f = BytesIO()
  711. f.writelines(write_info_refs(refs, self.store))
  712. self.scon.put_object(self.filename, f)
  713. def set_if_equals(
  714. self,
  715. name,
  716. old_ref,
  717. new_ref,
  718. committer=None,
  719. timestamp=None,
  720. timezone=None,
  721. message=None,
  722. ) -> bool:
  723. """Set a refname to new_ref only if it currently equals old_ref."""
  724. if name == "HEAD":
  725. return True
  726. refs = self._load_check_ref(name, old_ref)
  727. if not isinstance(refs, dict):
  728. return False
  729. refs[name] = new_ref
  730. self._write_refs(refs)
  731. self._refs[name] = new_ref
  732. return True
  733. def remove_if_equals(
  734. self, name, old_ref, committer=None, timestamp=None, timezone=None, message=None
  735. ) -> bool:
  736. """Remove a refname only if it currently equals old_ref."""
  737. if name == "HEAD":
  738. return True
  739. refs = self._load_check_ref(name, old_ref)
  740. if not isinstance(refs, dict):
  741. return False
  742. del refs[name]
  743. self._write_refs(refs)
  744. del self._refs[name]
  745. return True
  746. def allkeys(self):
  747. try:
  748. self._refs["HEAD"] = self._refs["refs/heads/master"]
  749. except KeyError:
  750. pass
  751. return self._refs.keys()
  752. class SwiftRepo(BaseRepo):
  753. def __init__(self, root, conf) -> None:
  754. """Init a Git bare Repository on top of a Swift container.
  755. References are managed in info/refs objects by
  756. `SwiftInfoRefsContainer`. The root attribute is the Swift
  757. container that contain the Git bare repository.
  758. Args:
  759. root: The container which contains the bare repo
  760. conf: A ConfigParser object
  761. """
  762. self.root = root.lstrip("/")
  763. self.conf = conf
  764. self.scon = SwiftConnector(self.root, self.conf)
  765. objects = self.scon.get_container_objects()
  766. if not objects:
  767. raise Exception(f"There is not any GIT repo here : {self.root}")
  768. objects = [o["name"].split("/")[0] for o in objects]
  769. if OBJECTDIR not in objects:
  770. raise Exception(f"This repository ({self.root}) is not bare.")
  771. self.bare = True
  772. self._controldir = self.root
  773. object_store = SwiftObjectStore(self.scon)
  774. refs = SwiftInfoRefsContainer(self.scon, object_store)
  775. BaseRepo.__init__(self, object_store, refs)
  776. def _determine_file_mode(self) -> bool:
  777. """Probe the file-system to determine whether permissions can be trusted.
  778. Returns: True if permissions can be trusted, False otherwise.
  779. """
  780. return False
  781. def _put_named_file(self, filename, contents) -> None:
  782. """Put an object in a Swift container.
  783. Args:
  784. filename: the path to the object to put on Swift
  785. contents: the content as bytestring
  786. """
  787. with BytesIO() as f:
  788. f.write(contents)
  789. self.scon.put_object(filename, f)
  790. @classmethod
  791. def init_bare(cls, scon, conf):
  792. """Create a new bare repository.
  793. Args:
  794. scon: a `SwiftConnector` instance
  795. conf: a ConfigParser object
  796. Returns:
  797. a `SwiftRepo` instance
  798. """
  799. scon.create_root()
  800. for obj in [
  801. posixpath.join(OBJECTDIR, PACKDIR),
  802. posixpath.join(INFODIR, "refs"),
  803. ]:
  804. scon.put_object(obj, BytesIO(b""))
  805. ret = cls(scon.root, conf)
  806. ret._init_files(True)
  807. return ret
  808. class SwiftSystemBackend(Backend):
  809. def __init__(self, logger, conf) -> None:
  810. self.conf = conf
  811. self.logger = logger
  812. def open_repository(self, path):
  813. self.logger.info("opening repository at %s", path)
  814. return SwiftRepo(path, self.conf)
  815. def cmd_daemon(args) -> None:
  816. """Entry point for starting a TCP git server."""
  817. import optparse
  818. parser = optparse.OptionParser()
  819. parser.add_option(
  820. "-l",
  821. "--listen_address",
  822. dest="listen_address",
  823. default="127.0.0.1",
  824. help="Binding IP address.",
  825. )
  826. parser.add_option(
  827. "-p",
  828. "--port",
  829. dest="port",
  830. type=int,
  831. default=TCP_GIT_PORT,
  832. help="Binding TCP port.",
  833. )
  834. parser.add_option(
  835. "-c",
  836. "--swift_config",
  837. dest="swift_config",
  838. default="",
  839. help="Path to the configuration file for Swift backend.",
  840. )
  841. options, args = parser.parse_args(args)
  842. try:
  843. import gevent
  844. import geventhttpclient # noqa: F401
  845. except ImportError:
  846. print(
  847. "gevent and geventhttpclient libraries are mandatory "
  848. " for use the Swift backend."
  849. )
  850. sys.exit(1)
  851. import gevent.monkey
  852. gevent.monkey.patch_socket()
  853. from dulwich import log_utils
  854. logger = log_utils.getLogger(__name__)
  855. conf = load_conf(options.swift_config)
  856. backend = SwiftSystemBackend(logger, conf)
  857. log_utils.default_logging_config()
  858. server = TCPGitServer(backend, options.listen_address, port=options.port)
  859. server.serve_forever()
  860. def cmd_init(args) -> None:
  861. import optparse
  862. parser = optparse.OptionParser()
  863. parser.add_option(
  864. "-c",
  865. "--swift_config",
  866. dest="swift_config",
  867. default="",
  868. help="Path to the configuration file for Swift backend.",
  869. )
  870. options, args = parser.parse_args(args)
  871. conf = load_conf(options.swift_config)
  872. if args == []:
  873. parser.error("missing repository name")
  874. repo = args[0]
  875. scon = SwiftConnector(repo, conf)
  876. SwiftRepo.init_bare(scon, conf)
  877. def main(argv=sys.argv) -> None:
  878. commands = {
  879. "init": cmd_init,
  880. "daemon": cmd_daemon,
  881. }
  882. if len(sys.argv) < 2:
  883. print(
  884. "Usage: {} <{}> [OPTIONS...]".format(sys.argv[0], "|".join(commands.keys()))
  885. )
  886. sys.exit(1)
  887. cmd = sys.argv[1]
  888. if cmd not in commands:
  889. print(f"No such subcommand: {cmd}")
  890. sys.exit(1)
  891. commands[cmd](sys.argv[2:])
  892. if __name__ == "__main__":
  893. main()