Hi Atlassian Community!
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)
The Solution
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.
Input File Formats
The script accepts two input file formats:
- CSV (.csv): Must contain a column named "Issue Key".
- Text (.txt): Must contain one issue key per line.
Example CSV:
Issue Key
PROJ-001
PROJ-002
PROJ-003
Example TXT:
PROJ-001
PROJ-002
PROJ-003
Key Features
- Bulk download
Download all attachments from multiple Jira issues
- Organized storage
Saves attachments into a timestamped folder
- CSV and TXT sSupport
Flexible input options for issue keys
- Comprehensive reporting
Generates a CSV report of downloaded attachments and a log file for failed downloads
- Error handling
Handles issues with invalid file paths, incorrect issue keys, and API request errors
- Rate Limiting handling
Includes delays to respect Jira API rate limits
- Logging
Provides detailed logging to track progress and identify any issues
Preparation
- Ensure the user running the script has "Browse Projects" and "View Issues" permissions in the relevant Jira projects
- Install the
requests library: pip install requests
- Prepare your Jira Cloud domain (your-domain.atlassian.net), email address, and API token.
- Create your CSV or TXT file containing the list of Jira issue keys, you can use the examples shared above
The Script
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()
Disclaimer
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!