I'm now sharing this simple Python script, which is designed to help you efficiently download all attachments from multiple Jira Cloud issues.
This script streamlines the process of retrieving attachments, organizing them into a structured folder, and providing a comprehensive report of the download activity. This is particularly useful for archiving, migrating data, or performing bulk analysis of attached files.
(This might play together with this other script to bulk attach files to Jira Issues in Jira Cloud)
Using the Jira Cloud REST API, this script retrieves attachments from a list of Jira issues provided in a CSV or TXT file. The script downloads all attachments for the specified issues and saves them into a newly created directory. It also generates a CSV report detailing successful downloads and logs any failed attempts for easy troubleshooting.
The script accepts two input file formats:
Example CSV:
Issue Key PROJ-001 PROJ-002 PROJ-003
Example TXT:
PROJ-001 PROJ-002 PROJ-003
requests
library: pip install requests
import requests from requests.auth import HTTPBasicAuth from getpass import getpass import logging import os import csv from datetime import datetime import time # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') log_filename = f"jira_attachment_download_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log" file_handler = logging.FileHandler(log_filename) file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) logging.getLogger().addHandler(file_handler) def get_jira_auth(): jira_domain = input("Enter your Jira Cloud domain (e.g., your-domain.atlassian.net): ") email = input("Enter your Jira email: ") api_token = getpass("Enter your Jira API token: ") return jira_domain, HTTPBasicAuth(email, api_token) def read_issue_keys(file_path): issue_keys = [] if file_path.lower().endswith('.csv'): with open(file_path, 'r', newline='', encoding='utf-8-sig') as csvfile: reader = csv.DictReader(csvfile) for row in reader: issue_keys.append(row['Issue Key']) elif file_path.lower().endswith('.txt'): with open(file_path, 'r', encoding='utf-8-sig') as txtfile: for line in txtfile: issue_keys.append(line.strip()) return issue_keys def download_attachments(jira_domain, auth, file_path): base_url = f"https://{jira_domain}/rest/api/3/issue/" attachments_folder = f"Bulk Attachments {datetime.now().strftime('%Y%m%d_%H%M%S')}" os.makedirs(attachments_folder, exist_ok=True) logging.info(f"Attachments will be saved in: {os.path.abspath(attachments_folder)}") output_csv_path = os.path.join(attachments_folder, "attachments_report.csv") logging.info(f"Attachment report will be saved as: {os.path.abspath(output_csv_path)}") failed_downloads = [] total_attachments = 0 failed_attachments = 0 issue_keys = read_issue_keys(file_path) with open(output_csv_path, 'w', newline='', encoding='utf-8') as output_csvfile: writer = csv.writer(output_csvfile) writer.writerow(['Issue Key', 'Filename']) for issue_key in issue_keys: try: response = requests.get(f"{base_url}{issue_key}", auth=auth) response.raise_for_status() issue_data = response.json() attachments = issue_data.get('fields', {}).get('attachment', []) for attachment in attachments: total_attachments += 1 try: attachment_url = attachment['content'] attachment_filename = attachment['filename'] attachment_response = requests.get(attachment_url, auth=auth, stream=True) attachment_response.raise_for_status() attachment_path = os.path.join(attachments_folder, attachment_filename) with open(attachment_path, 'wb') as f: for chunk in attachment_response.iter_content(chunk_size=8192): f.write(chunk) writer.writerow([issue_key, attachment_filename]) except requests.exceptions.RequestException as e: failed_attachments += 1 failed_downloads.append((issue_key, attachment_filename, str(e))) logging.error(f"Failed to download {attachment_filename} from issue {issue_key}: {e}") if total_attachments % 5 == 0: logging.info(f"Successfully downloaded {total_attachments} attachments so far...") time.sleep(2) # Delay to handle rate limiting except requests.exceptions.RequestException as e: logging.error(f"Failed to retrieve issue {issue_key}: {e}") failed_downloads.append((issue_key, 'N/A', str(e))) logging.info(f"Download completed: {total_attachments - failed_attachments} attachments downloaded successfully.") logging.info(f"{failed_attachments} attachments failed to download from {len(failed_downloads)} issues.") if failed_downloads: with open(os.path.join(attachments_folder, 'failed_downloads.log'), 'w', encoding='utf-8') as log_file: for issue_key, filename, error in failed_downloads: log_file.write(f"Issue Key: {issue_key}, Filename: {filename}, Error: {error}\n") logging.info(f"Details of failed downloads logged in: {os.path.join(attachments_folder, 'failed_downloads.log')}") def main(): jira_domain, auth = get_jira_auth() while True: file_path = input(""" Enter the path to your CSV or TXT file. The file must contain a list of issue keys. - CSV files must have a column named "Issue Key". - TXT files must have one issue key per line. Examples: 1. CSV in the same directory as the script: issues.csv 2. TXT in a subdirectory called "data": data/issues.txt 3. Full path to the file: /path/to/your/issues.csv or /path/to/your/issues.txt File Path: """) if file_path.lower().endswith(('.csv', '.txt')): break else: print("Invalid file type. Please enter a CSV or TXT file.") download_attachments(jira_domain, auth, file_path) if __name__ == "__main__": main()
This script is provided "as is" and "as available" without warranties and is not officially supported or endorsed by Atlassian. Use at your own risk. Jira API changes may impact functionality.
Cheers!
Delfino Rosales
Senior Cloud Support Engineer
Amsterdam, NL
Online forums and learning are now in one easy-to-use experience.
By continuing, you accept the updated Community Terms of Use and acknowledge the Privacy Policy. Your public name, photo, and achievements may be publicly visible and available in search engines.
0 comments