Skip to content

Commit 1e1e24a

Browse files
authored
[Jira]: Improve attachment download methods. (#1622)
* Update download_issue_attachments method to utilize stream option * Fix get_attachment_content to use URL from attachment metadata * add comment for get_attachment_content
1 parent 0a09708 commit 1e1e24a

1 file changed

Lines changed: 67 additions & 5 deletions

File tree

atlassian/jira.py

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -255,14 +255,68 @@ def get_attachment(self, attachment_id: T_id) -> T_resp_json:
255255
url = f"{base_url}/{attachment_id}"
256256
return self.get(url)
257257

258-
def download_issue_attachments(self, issue: T_id, path: Optional[str] = None) -> Optional[str]:
258+
def download_issue_attachments(
259+
self,
260+
issue: str,
261+
path: Optional[str] = None,
262+
overwrite: bool = False,
263+
stream: bool = False,
264+
block_size: Optional[int] = 16384,
265+
timeout: Optional[int] = None,
266+
) -> Optional[str]:
259267
"""
260268
Downloads all attachments from a Jira issue.
261269
:param issue: The issue-key of the Jira issue
262270
:param path: Path to directory where attachments will be saved. If None, current working directory will be used.
271+
:param overwrite: If True, always download and create new zip file.
272+
If False (default), download will be skipped when zip file already exists in path.
273+
:param stream: If True, request stream mode will be used to download and write files.
274+
If False (default), whole attachment content will be downloaded into memory first, then will be written to disk afterwards.
275+
:param block_size: Block size of each stream content chunks. This option is only applicable when stream=True is set.
276+
Default size of 16 KiB is used to balance speed and memory usage.
277+
Smaller value will decrease memory usage, but may also decrease download speed if too small.
278+
:param timeout: Request timeout parameter in seconds. None (default) will never cause timeout.
263279
:return: A message indicating the result of the download operation.
264280
"""
265-
return self.download_attachments_from_issue(issue=issue, path=path, cloud=self.cloud)
281+
try:
282+
if path is None:
283+
path = os.getcwd()
284+
issue_id = self.issue(issue, fields="id")["id"]
285+
attachment_name = f"{issue_id}_attachments.zip"
286+
file_path = os.path.join(path, attachment_name)
287+
if not overwrite and os.path.isfile(file_path):
288+
return "File already exists"
289+
290+
if self.cloud:
291+
url = self.url + f"/secure/issueAttachments/{issue_id}.zip"
292+
else:
293+
url = self.url + f"/secure/attachmentzip/{issue_id}.zip"
294+
response = self._session.get(url, stream=stream, timeout=timeout)
295+
response.raise_for_status()
296+
297+
# if Jira issue doesn't have any attachments _session.get
298+
# request response will return 22 bytes of PKzip format
299+
file_size = int(response.headers.get("Content-Length", 0))
300+
if file_size == 22:
301+
return "No attachments found on the Jira issue"
302+
303+
with open(file_path, "wb") as file:
304+
if not stream:
305+
file.write(response.content)
306+
else:
307+
for data in response.iter_content(block_size):
308+
file.write(data)
309+
310+
return "Attachments downloaded successfully"
311+
312+
except FileNotFoundError:
313+
raise FileNotFoundError("Verify if directory path is correct and/or if directory exists")
314+
except PermissionError:
315+
raise PermissionError(
316+
"Directory found, but there is a problem with saving file to this directory. Check directory permissions"
317+
)
318+
except Exception as e:
319+
raise e
266320

267321
@deprecated(version="3.41.20", reason="Use download_issue_attachments instead")
268322
def download_attachments_from_issue(
@@ -312,9 +366,17 @@ def get_attachment_content(self, attachment_id: T_id) -> bytes:
312366
:param attachment_id: int
313367
:return: content as bytes
314368
"""
315-
base_url = self.resource_url("attachment")
316-
url = f"{base_url}/content/{attachment_id}"
317-
return self.get(url, not_json_response=True)
369+
attachment_info = self.get_attachment(attachment_id)
370+
# Type check for mypy. If attachment is not found, or unavailable, it would raise HTTPError anyways.
371+
if attachment_info is None:
372+
return b""
373+
url = attachment_info["content"]
374+
return self.get(
375+
url,
376+
not_json_response=True,
377+
absolute=True,
378+
headers={"Accept": "*/*"},
379+
)
318380

319381
def remove_attachment(self, attachment_id: T_id) -> T_resp_json:
320382
"""

0 commit comments

Comments
 (0)