Backup Jira attachments to Google Drive


Intro video is not displayed because you have disallowed functional cookies.
Get Started

Not the template you're looking for? Browse more.

About the template


This template enables continuous backup of Jira Cloud attachments to Google Drive. Get started to learn more.

About ScriptRunner Connect


What is ScriptRunner Connect?

ScriptRunner Connect is an AI assisted code-first (JavaScript/TypeScript) integration platform (iPaaS) for building complex integrations and automations.

Can I try it out for free?

Yes. ScriptRunner Connect comes with a forever free tier.

Can I customize the integration logic?

Absolutely. The main value proposition of ScriptRunner Connect is that you'll get full access to the code that is powering the integration, which means you can make any changes to the the integration logic yourself.

Can I change the integration to communicate with additional apps?

Yes. Since ScriptRunner Connect specializes in enabling complex integrations, you can easily change the integration logic to connect to as many additional apps as you need, no limitations.

What if I don't feel comfortable making changes to the code?

First you can try out our AI assistant which can help you understand what the code does, and also help you make changes to the code. Alternatively you can hire our professionals to make the changes you need or build new integrations from scratch.

Do I have to host it myself?

No. ScriptRunner Connect is a fully managed SaaS (Software-as-a-Service) product.

What about security?

ScriptRunner Connect is ISO 27001 and SOC 2 certified. Learn more about our security.

Template Content


README

Scripts

TypeScriptOnJiraCloudIssueUpdated
Issue Updated

README


Jira Cloud Attachment Backup to Google Drive

๐Ÿ“‹ Overview

This integration automatically backs up Jira Cloud attachments to Google Drive whenever they are uploaded to Jira issues. When an attachment is added to an issue, the integration:

  • Detects attachment uploads via the Issue Updated event using the changelog
  • Gets the issue key directly from the event
  • Downloads the attachment from Jira using the attachment ID from the changelog
  • Creates a root attachments folder in Google Drive (name configured via parameter)
  • Creates an issue-specific subfolder within the root folder (e.g. JiraCloudAttachmentsBackup/PROJ-123/)
  • Uploads the attachment to the issue subfolder with the same filename and MIME type

This provides an additional layer of data protection, organizes attachments by issue, and makes it easy to access Jira attachments through Google Drive.

This template was vibe coded with AI agent.

๐Ÿ–Š๏ธ Setup

Connectors

You'll need two Connectors configured in ScriptRunner Connect:

  1. Jira Cloud Connector

    • Used for authentication to download attachments from Jira Cloud
    • Refer to the ScriptRunner Connect web UI for configuration guidance
  2. Google Drive Connector

    • Used for authentication to upload attachments to Google Drive
    • Refer to the ScriptRunner Connect web UI for configuration guidance

API Connections

Configure the following API Connections in the ScriptRunner Connect web UI:

  1. Jira Cloud API Connection

    • Link to the Jira Cloud Connector (create new or reuse existing)
    • This connection enables authenticated access to download attachments from Jira
  2. Google Drive API Connection

    • Link to the Google Drive Connector (create new or reuse existing)
    • This connection enables uploading files to Google Drive

Parameters

Configure the following Parameters in the ScriptRunner Connect web UI:

  1. ATTACHMENTS_FOLDER (Text)
    • The name of the root folder in Google Drive where attachments will be backed up
    • Default value: JiraCloudAttachmentsBackup
    • Important: Do not manually create this folder in Google Drive - the integration will create it automatically
    • Issue-specific subfolders will be created within this folder (e.g. JiraCloudAttachmentsBackup/PROJ-123/)
    • All folders are created and managed by the integration to ensure proper configuration

Event Listeners

Configure an Event Listener for Jira Cloud issue updates:

  1. Jira Cloud - Issue Updated
    • Event type: Issue Updated
    • Linked script: OnJiraCloudIssueUpdated
    • Webhook setup: Configure the webhook in your Jira Cloud instance to point to the Event Listener URL provided by ScriptRunner Connect
    • Webhook events: Select "Issue updated" event
    • The script filters for attachment uploads using the changelog automatically

Webhook Configuration in Jira Cloud:

  1. Go to Jira Settings โ†’ System โ†’ WebHooks
  2. Click "Create a WebHook"
  3. Enter a name (e.g. "ScriptRunner Connect - Attachment Backup")
  4. Enter the webhook URL provided by ScriptRunner Connect
  5. Select event: Issue โ†’ updated
  6. Click "Create"

Note: The Issue Updated event will trigger for all issue changes, but the script filters to only process attachment uploads by checking the changelog for field === 'Attachment'.

๐Ÿš€ Using the template

Manual Testing

To test the integration:

  1. Ensure the ATTACHMENTS_FOLDER parameter is configured in ScriptRunner Connect
  2. Important: Do not manually create the folder in Google Drive - let the integration create it automatically
  3. Upload an attachment to any Jira Cloud issue (e.g. PROJ-123)
  4. Check the Script Invocation Logs in ScriptRunner Connect web UI to verify successful execution
  5. Verify the root attachments folder was created in your Google Drive (as configured in ATTACHMENTS_FOLDER parameter)
  6. Verify an issue-specific subfolder was created (e.g. JiraCloudAttachmentsBackup/PROJ-123/)
  7. Confirm the attachment was copied to the issue subfolder

Automatic Operation

Once configured, the integration runs automatically whenever:

  • A user uploads an attachment to a Jira Cloud issue
  • The attachment is created via Jira Cloud's REST API
  • An app or automation creates an attachment in Jira

The script uses the changelog to detect attachment additions, ensuring it only processes relevant updates.

Monitoring

Monitor the integration through ScriptRunner Connect web UI:

  • Script Invocation Logs: View execution logs and console output
  • HTTP Logs: Inspect API calls to Jira Cloud and Google Drive
  • Event Listener Logs: Track incoming webhook events from Jira

โ—๏ธ Considerations

Storage Space

  • Google Drive has storage limits depending on your account type (15 GB for free accounts, more for paid plans)
  • Monitor your Google Drive storage usage regularly
  • Consider implementing cleanup policies for old backups if needed

File Name Conflicts

  • Attachments are organized by issue key in separate subfolders, reducing the likelihood of name conflicts
  • If an attachment with the same name already exists in an issue's subfolder, Google Drive will create a new file with the same name (Google Drive allows duplicate file names)
  • This means multiple versions of the same filename will accumulate over time within each issue folder

Large Files

  • The integration handles files of all sizes, but very large files (100+ MB) may take longer to process
  • For files larger than 100MB, special optimizations are required - see Working with very large attachments for details

Rate Limiting

  • Both Jira Cloud and Google Drive APIs have rate limits
  • For high-volume Jira instances with frequent attachment uploads, monitor for rate limiting errors
  • Managed APIs automatically retry on rate limits with built-in backoff logic

Event Volume

  • The integration listens to Issue Updated events, which occur for all issue changes (not just attachments)
  • The script filters these events to only process attachment additions using the changelog
  • For very high-volume instances, consider the number of issue updates and webhook call volume

Permissions

  • The Google Drive Connector must have permissions to create folders and upload files
  • The Jira Cloud Connector must have permissions to read issue attachments

๐Ÿ”ง Troubleshooting

Missing ATTACHMENTS_FOLDER Parameter

  • Ensure the ATTACHMENTS_FOLDER parameter is configured in the ScriptRunner Connect web UI
  • The parameter type should be "Text"
  • Default value: JiraCloudAttachmentsBackup (or use your preferred folder name)

Attachment Not Found in Google Drive

  1. Check Script Invocation Logs for any errors during upload
  2. Verify the Google Drive API Connection has proper permissions
  3. Ensure the Google Drive Connector has authorization to create folders and files
  4. Check HTTP Logs to see if the Google Drive API returned any errors

Webhook Not Triggering

  1. Verify the webhook is properly configured in Jira Cloud settings
  2. Ensure the webhook URL matches the Event Listener URL in ScriptRunner Connect
  3. Check that the "attachment created" event is selected in the webhook configuration
  4. Test the webhook from Jira Cloud's webhook configuration page

Download Errors

  1. Verify the Jira Cloud API Connection is properly configured
  2. Check that the Jira Cloud Connector has permissions to read attachments
  3. Ensure the attachment still exists in Jira (not deleted before backup completed)
  4. Check HTTP Logs for detailed error messages from Jira Cloud API

Performance Issues

  • For large files, consider monitoring memory usage in Script Invocation Logs
  • If processing large attachments frequently, consider implementing batching or queuing mechanisms
  • Review HTTP Logs to identify slow API calls that may need optimization

API Connections


TypeScriptOnJiraCloudIssueUpdated

import GoogleDrive from './api/google/drive';
import JiraCloud from './api/jira/cloud';
import { IssueUpdatedEvent } from '@sr-connect/jira-cloud/events';
import { FileFields } from '@managed-api/google-drive-v3-core/definitions/File';
import { convertBase64ToBuffer } from '@sr-connect/convert';

/**
 * Backs up Jira Cloud attachments to Google Drive when they are uploaded to issues.
 * Listens to issue update events and filters for attachment uploads using the changelog.
 * Creates a root attachments folder in Google Drive if it doesn't exist,
 * then creates an issue-specific subfolder, downloads the attachment from Jira,
 * and uploads it to the issue subfolder.
 *
 * @param event Object that holds Issue Updated event data
 * @param context Object that holds function invocation context data
 */
export default async function (event: IssueUpdatedEvent, context: Context<EV>): Promise<void> {
    const attachmentsFolderName = context.environment.vars.ATTACHMENTS_FOLDER;
    const issueKey = event.issue.key;

    // Check if this update includes attachment changes
    if (!event.changelog?.items) {
        console.log('No changelog items found, skipping');
        return;
    }

    // Filter for attachment additions
    const attachmentChanges = event.changelog.items.filter((item) => item.field === 'Attachment');

    if (attachmentChanges.length === 0) {
        console.log('No attachment changes detected, skipping');
        return;
    }

    console.log(`Processing ${attachmentChanges.length} attachment(s) for issue ${issueKey}`);

    // Find or create the root attachments folder in Google Drive
    const rootFolderId = await findOrCreateFolder(attachmentsFolderName, null);

    // Find or create the issue-specific subfolder
    const issueFolderId = await findOrCreateFolder(issueKey, rootFolderId);

    // Process each attachment change
    for (const change of attachmentChanges) {
        // The 'to' field contains the attachment ID
        if (change.to) {
            await backupAttachment(change.to, issueFolderId, issueKey, attachmentsFolderName);
        }
    }

    console.log(`Successfully processed all attachments for issue ${issueKey}`);
}

/**
 * Backs up a single attachment to Google Drive.
 *
 * @param attachmentId The ID of the attachment to backup
 * @param issueFolderId The Google Drive folder ID where the attachment should be stored
 * @param issueKey The issue key for logging purposes
 * @param attachmentsFolderName The root folder name for logging purposes
 */
async function backupAttachment(
    attachmentId: string,
    issueFolderId: string,
    issueKey: string,
    attachmentsFolderName: string,
): Promise<void> {
    console.log(`Backing up attachment ID: ${attachmentId}`);

    // Get attachment metadata to retrieve filename and MIME type
    const attachmentMetadata = await JiraCloud.Issue.Attachment.Metadata.getMetadata({
        id: attachmentId,
    });

    console.log(`Attachment filename: ${attachmentMetadata.filename}`);

    // Download the attachment content
    const attachmentContent = await JiraCloud.Issue.Attachment.getAttachmentContent({
        id: attachmentId,
    });

    // Convert to buffer if it's a string (base64)
    const contentBuffer =
        typeof attachmentContent === 'string' ? convertBase64ToBuffer(attachmentContent) : attachmentContent;

    // Upload to Google Drive
    await uploadToGoogleDrive(
        issueFolderId,
        attachmentMetadata.filename || `attachment-${attachmentId}`,
        contentBuffer,
        attachmentMetadata.mimeType || 'application/octet-stream',
    );

    console.log(
        `Successfully backed up attachment ${attachmentMetadata.filename} to Google Drive in ${attachmentsFolderName}/${issueKey}`,
    );
}

/**
 * Finds a folder in Google Drive by name, or creates it if it doesn't exist.
 * Uses Google Drive API to search for the folder by name and MIME type.
 *
 * @param folderName The name of the folder to find or create
 * @param parentFolderId The ID of the parent folder, or null for root
 * @returns The ID of the folder
 */
async function findOrCreateFolder(folderName: string, parentFolderId: string | null): Promise<string> {
    // Build search query
    let query = `name='${folderName}' and mimeType='application/vnd.google-apps.folder' and trashed=false`;
    if (parentFolderId) {
        query += ` and '${parentFolderId}' in parents`;
    }

    // Search for existing folder
    const searchResults = await GoogleDrive.File.getFiles({
        q: query,
        fields: 'files(id, name)',
    });

    // If folder exists, return its ID
    if (searchResults.files && searchResults.files.length > 0) {
        const folderId = searchResults.files[0].id!;
        console.log(`Found existing folder '${folderName}': ${folderId}`);
        return folderId;
    }

    // Create the folder if it doesn't exist
    console.log(`Folder '${folderName}' not found, creating new folder`);
    const createBody: { name: string; mimeType: string; parents?: string[] } = {
        name: folderName,
        mimeType: 'application/vnd.google-apps.folder',
    };

    if (parentFolderId) {
        createBody.parents = [parentFolderId];
    }

    const newFolder = await GoogleDrive.File.createFile({
        body: createBody,
        fields: 'id',
    });

    // Type guard to check if response is FileFields
    if ('id' in newFolder) {
        const folderId = (newFolder as FileFields).id!;
        console.log(`Created folder '${folderName}': ${folderId}`);
        return folderId;
    }

    throw new Error(`Failed to create folder '${folderName}': unexpected response type`);
}

/**
 * Uploads an attachment to Google Drive in the specified folder.
 * Uses multipart upload to send both metadata and file content in a single request.
 *
 * @param folderId The ID of the parent folder in Google Drive
 * @param filename The name to use for the uploaded file
 * @param content The file content as an ArrayBuffer or Uint8Array
 * @param mimeType The MIME type of the file
 */
async function uploadToGoogleDrive(
    folderId: string,
    filename: string,
    content: ArrayBuffer | Uint8Array,
    mimeType: string,
): Promise<void> {
    console.log(`Uploading ${filename} (${content.byteLength} bytes) to Google Drive folder ${folderId}`);

    // Google Drive API accepts string | ArrayBuffer, so we pass Uint8Array as-is (it's compatible at runtime)
    await GoogleDrive.File.createFile({
        uploadType: 'multipart',
        body: {
            name: filename,
            parents: [folderId],
            mimeType: mimeType,
            content: content as ArrayBuffer,
        },
    });

    console.log(`Upload completed for ${filename}`);
}

ยฉ 2025 ScriptRunner ยท Terms and Conditions ยท Privacy Policy ยท Legal Notice ยท Cookie Preferences