Skip to content

Commit 91ddbe2

Browse files
Bitbucket: add support for downloading repository archives (#791)
1 parent a42aeff commit 91ddbe2

File tree

5 files changed

+110
-0
lines changed

5 files changed

+110
-0
lines changed

atlassian/bitbucket/__init__.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2368,6 +2368,51 @@ def delete_repo_condition(self, project_key, repo_key, id_condition):
23682368
url = self._url_repo_condition(project_key, repo_key, id_condition)
23692369
return self.delete(url) or {}
23702370

2371+
def download_repo_archive(
2372+
self,
2373+
project_key,
2374+
repository_slug,
2375+
dest_fd,
2376+
at=None,
2377+
filename=None,
2378+
format=None,
2379+
path=None,
2380+
prefix=None,
2381+
chunk_size=128,
2382+
):
2383+
"""
2384+
Downloads a repository archive.
2385+
Note that the data is written to the specified file-like object,
2386+
rather than simply being returned.
2387+
For further information visit:
2388+
https://docs.atlassian.com/bitbucket-server/rest/7.13.0/bitbucket-rest.html#idp199
2389+
:param project_key:
2390+
:param repository_slug:
2391+
:param dest_fd: a file-like object to which the archive will be written
2392+
:param at: string: Optional, the commit to download an archive of; if not supplied, an archive of the default branch is downloaded
2393+
:param filename: string: Optional, a filename to include the "Content-Disposition" header
2394+
:param format: string: Optional, the format to stream the archive in; must be one of: zip, tar, tar.gz or tgz. If not specified, then the archive will be in zip format.
2395+
:param paths: string: Optional, path to include in the streamed archive
2396+
:param prefix: string: Optional, a prefix to apply to all entries in the streamed archive; if the supplied prefix does not end with a trailing /, one will be added automatically
2397+
:param chunk_size: int: Optional, download chunk size. Defeault is 128
2398+
"""
2399+
url = f"{self._url_repo(project_key, repository_slug)}/archive"
2400+
params = {}
2401+
if at is not None:
2402+
params["at"] = at
2403+
if filename is not None:
2404+
params["filename"] = filename
2405+
if format is not None:
2406+
params["format"] = format
2407+
if path is not None:
2408+
params["path"] = path
2409+
if prefix is not None:
2410+
params["prefix"] = prefix
2411+
headers = {"Accept": "*/*"}
2412+
response = self.get(url, params=params, headers=headers, advanced_mode=True)
2413+
for chunk in response.iter_content(chunk_size=chunk_size):
2414+
dest_fd.write(chunk)
2415+
23712416
@deprecated(
23722417
version="2.0.2",
23732418
reason="Use atlassian.bitbucket.cloud instead of atlassian.bitbucket",

atlassian/bitbucket/server/projects/repos/__init__.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,3 +275,34 @@ def users(self):
275275
API docs: https://docs.atlassian.com/bitbucket-server/rest/7.8.0/bitbucket-rest.html#idp285
276276
"""
277277
return self.__users
278+
279+
def download_archive(self, dest_fd, at=None, filename=None, format=None, path=None, prefix=None, chunk_size=128):
280+
"""
281+
Downloads a repository archive.
282+
Note that the data is written to the specified file-like object,
283+
rather than simply being returned.
284+
For further information visit:
285+
https://docs.atlassian.com/bitbucket-server/rest/7.13.0/bitbucket-rest.html#idp199
286+
:param dest_fd: a file-like object to which the archive will be written
287+
:param at: string: Optional, the commit to download an archive of; if not supplied, an archive of the default branch is downloaded
288+
:param filename: string: Optional, a filename to include the "Content-Disposition" header
289+
:param format: string: Optional, the format to stream the archive in; must be one of: zip, tar, tar.gz or tgz. If not specified, then the archive will be in zip format.
290+
:param paths: string: Optional, path to include in the streamed archive
291+
:param prefix: string: Optional, a prefix to apply to all entries in the streamed archive; if the supplied prefix does not end with a trailing /, one will be added automatically
292+
:param chunk_size: int: Optional, download chunk size. Defeault is 128
293+
"""
294+
params = {}
295+
if at is not None:
296+
params["at"] = at
297+
if filename is not None:
298+
params["filename"] = filename
299+
if format is not None:
300+
params["format"] = format
301+
if path is not None:
302+
params["path"] = path
303+
if prefix is not None:
304+
params["prefix"] = prefix
305+
headers = {"Accept": "*/*"}
306+
response = self.get("archive", params=params, headers=headers, advanced_mode=True)
307+
for chunk in response.iter_content(chunk_size=chunk_size):
308+
dest_fd.write(chunk)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# coding=utf-8
2+
from atlassian import Bitbucket
3+
4+
bitbucket = Bitbucket(url="http://localhost:7990", username="admin", password="admin")
5+
6+
with open("archive.tgz", mode="wb") as dest_fd:
7+
bitbucket.download_repo_archive(
8+
project_key="DEMO",
9+
repository_slug="example-repository",
10+
at="master",
11+
format="tar.gz",
12+
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
responses = {
2+
None: b"\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x14\x75\xc4\x52\xb4\x05\x4d\x93\x0e\x00\x00\x00\x0e\x00\x00\x00\x09\x00\x00\x00\x72\x65\x61\x64\x6d\x65\x2e\x6d\x64\x54\x65\x73\x74\x20\x72\x65\x61\x64\x6d\x65\x2e\x6d\x64\x50\x4b\x01\x02\x3f\x00\x0a\x00\x00\x00\x00\x00\x14\x75\xc4\x52\xb4\x05\x4d\x93\x0e\x00\x00\x00\x0e\x00\x00\x00\x09\x00\x24\x00\x00\x00\x00\x00\x00\x00\x20\x00\x00\x00\x00\x00\x00\x00\x72\x65\x61\x64\x6d\x65\x2e\x6d\x64\x0a\x00\x20\x00\x00\x00\x00\x00\x01\x00\x18\x00\x8a\x6d\xf9\xc4\xfb\x58\xd7\x01\x8a\x6d\xf9\xc4\xfb\x58\xd7\x01\x93\xfa\xc8\xc4\xfb\x58\xd7\x01\x50\x4b\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00\x5b\x00\x00\x00\x35\x00\x00\x00\x00\x00",
3+
"at=CommitId": b"\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\xaa\x75\xc4\x52\x6e\xb0\x98\x60\x1a\x00\x00\x00\x1a\x00\x00\x00\x09\x00\x00\x00\x72\x65\x61\x64\x6d\x65\x2e\x6d\x64\x54\x65\x73\x74\x20\x72\x65\x61\x64\x6d\x65\x2e\x6d\x64\x20\x61\x74\x20\x43\x6f\x6d\x6d\x69\x74\x49\x64\x50\x4b\x01\x02\x3f\x00\x0a\x00\x00\x00\x00\x00\xaa\x75\xc4\x52\x6e\xb0\x98\x60\x1a\x00\x00\x00\x1a\x00\x00\x00\x09\x00\x24\x00\x00\x00\x00\x00\x00\x00\x20\x00\x00\x00\x00\x00\x00\x00\x72\x65\x61\x64\x6d\x65\x2e\x6d\x64\x0a\x00\x20\x00\x00\x00\x00\x00\x01\x00\x18\x00\x78\xa8\x10\x6c\xfc\x58\xd7\x01\x78\xa8\x10\x6c\xfc\x58\xd7\x01\x93\xfa\xc8\xc4\xfb\x58\xd7\x01\x50\x4b\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00\x5b\x00\x00\x00\x41\x00\x00\x00\x00\x00",
4+
}

tests/test_bitbucket_server.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# coding: utf8
2+
import io
23
import pytest
34
import sys
5+
import zipfile
46

57
from atlassian.bitbucket.server import Server
68

@@ -272,3 +274,19 @@ def test_repository_permissions(self):
272274
user = repo.users.get("jcitizen1")
273275
assert user.name == "jcitizen1", "Get a user"
274276
assert user.delete() == {}, "Delete a user"
277+
278+
def test_download_repo_archive(self):
279+
repo = BITBUCKET.projects.get("PRJ").repos.get("my-repo1-slug")
280+
with io.BytesIO() as buf:
281+
repo.download_archive(buf)
282+
with zipfile.ZipFile(buf) as archive:
283+
assert archive.namelist() == ["readme.md"]
284+
with archive.open("readme.md") as file_in_archive:
285+
assert file_in_archive.read() == b"Test readme.md"
286+
287+
with io.BytesIO() as buf:
288+
repo.download_archive(buf, at="CommitId")
289+
with zipfile.ZipFile(buf) as archive:
290+
assert archive.namelist() == ["readme.md"]
291+
with archive.open("readme.md") as file_in_archive:
292+
assert file_in_archive.read() == b"Test readme.md at CommitId"

0 commit comments

Comments
 (0)