diff --git a/src/containers/AdminCampaignStats.jsx b/src/containers/AdminCampaignStats.jsx index 7db3bc25a..3d206b58a 100644 --- a/src/containers/AdminCampaignStats.jsx +++ b/src/containers/AdminCampaignStats.jsx @@ -346,8 +346,8 @@ class AdminCampaignStats extends React.Component { {campaign.exportResults.error && (
Export failed: {campaign.exportResults.error}
)} - {campaign.exportResults.campaignExportUrl && - campaign.exportResults.campaignExportUrl.startsWith("http") ? ( + {campaign.exportResults.campaignExportUrl && ( + (campaign.exportResults.campaignExportUrl.startsWith("http")) ? (
Most recent export: @@ -360,15 +360,16 @@ class AdminCampaignStats extends React.Component { Messages Export CSV
- ) : ( -
- Local export was successful, saved on the server at: -
- {campaign.exportResults.campaignExportUrl} -
- {campaign.exportResults.campaignMessagesExportUrl} -
- )} + ) : (campaign.exportResults.campaignExportUrl.startsWith("file://") && ( +
+ Local export was successful, saved on the server at: +
+ {campaign.exportResults.campaignExportUrl} +
+ {campaign.exportResults.campaignMessagesExportUrl} +
+ ) + ))} )} {campaign.joinToken && campaign.useDynamicAssignment && ( @@ -424,21 +425,21 @@ class AdminCampaignStats extends React.Component { message={ Export started - - {this.props.organizationData && - this.props.organizationData.emailEnabled && - " we'll e-mail you when it's done. "} - {campaign.cacheable && ( + {(this.props.organizationData && + this.props.organizationData.organization.emailEnabled) ? + " we'll e-mail you when it's done. " : + (campaign.cacheable && ( { this.props.data.refetch(); }} > - Reload the page + {" Reload the page"} {/*Hacky way to add a space at the beginning */} {" "} to see a download link when its ready. - )} + ))} } autoHideDuration={campaign.cacheable ? null : 5000} diff --git a/src/server/models/cacheable_queries/campaign.js b/src/server/models/cacheable_queries/campaign.js index f7bab4e09..48d04c2bf 100644 --- a/src/server/models/cacheable_queries/campaign.js +++ b/src/server/models/cacheable_queries/campaign.js @@ -254,7 +254,7 @@ const campaignCache = { await r.redis .MULTI() .SET(exportCacheKey, JSON.stringify(data)) - .EXPIRE(exportCacheKey, 43200) + .EXPIRE(exportCacheKey, 86400) .exec(); } }, diff --git a/src/workers/jobs.js b/src/workers/jobs.js index cf2453fb4..9dcb55ab3 100644 --- a/src/workers/jobs.js +++ b/src/workers/jobs.js @@ -28,7 +28,14 @@ import { rawIngestMethod } from "../extensions/contact-loaders"; import { Lambda } from "@aws-sdk/client-lambda"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { GetObjectCommand, S3 } from "@aws-sdk/client-s3"; +import { + CreateBucketCommand, + HeadBucketCommand, + GetObjectCommand, + waitUntilBucketExists, + S3Client, + PutObjectCommand +} from "@aws-sdk/client-s3"; import { SQS } from "@aws-sdk/client-sqs"; import Papa from "papaparse"; import moment from "moment"; @@ -861,12 +868,51 @@ export async function exportCampaign(job) { (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) ) { try { - const s3bucket = new S3({ - // The transformation for params is not implemented. - // Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed. - // Please create/upvote feature request on aws-sdk-js-codemod for params. - params: { Bucket: process.env.AWS_S3_BUCKET_NAME } + const client = new S3Client({ + region: process.env.AWS_REGION }); + const bucketName = process.env.AWS_S3_BUCKET_NAME; + + try { + // Check if the S3 bucket already exists + const verifyBucketCommand = new HeadBucketCommand({ + Bucket: bucketName + }); + await client.send(verifyBucketCommand); + + console.log(`S3 bucket "${bucketName}" already exists.`); + } catch (error) { + if (error.name === "NotFound") { + console.log( + `S3 bucket "${bucketName}" not found. Creating a new bucket.` + ); + + try { + // Create the S3 bucket + const createBucketCommand = new CreateBucketCommand({ + Bucket: bucketName + }); + await client.send(createBucketCommand); + + console.log(`S3 bucket "${bucketName}" created successfully.`); + } catch (createError) { + console.error( + `Error creating bucket "${bucketName}":`, + createError + ); + } + } else { + console.error("Error checking bucket existence:", error); + } + } + + // verifies that the bucket exists before moving forward + // if for some reason this fails, Spoke defensively deletes the job + await waitUntilBucketExists( + { client, maxWaitTime: 15 }, + { Bucket: bucketName } + ); + const campaignTitle = campaign.title .replace(/ /g, "_") .replace(/\//g, "_"); @@ -874,33 +920,54 @@ export async function exportCampaign(job) { "YYYY-MM-DD-HH-mm-ss" )}.csv`; const messageKey = `${key}-messages.csv`; - let params = { Key: key, Body: campaignCsv }; - await s3bucket.putObject(params); - params = { Key: key, Expires: 86400 }; - const campaignExportUrl = await await getSignedUrl(s3bucket, new GetObjectCommand(params), { - expiresIn: "/* add value from 'Expires' from v2 call if present, else remove */" - }); - params = { Key: messageKey, Body: messageCsv }; - await s3bucket.putObject(params); - params = { Key: messageKey, Expires: 86400 }; - const campaignMessagesExportUrl = await await getSignedUrl(s3bucket, new GetObjectCommand(params), { - expiresIn: "/* add value from 'Expires' from v2 call if present, else remove */" - }); + let params = { Key: key, + Body: campaignCsv, + Bucket: bucketName }; + await client.send(new PutObjectCommand(params)); + params = { Key: key, + Expires: 86400, + Bucket: bucketName }; + const campaignExportUrl = await getSignedUrl(client, new GetObjectCommand(params)); + params = { Key: messageKey, + Body: messageCsv, + Bucket: bucketName }; + await client.send(new PutObjectCommand(params)); + params = { Key: messageKey, + Expires: 86400, + Bucket: bucketName }; + const campaignMessagesExportUrl = await getSignedUrl(client, new GetObjectCommand(params)); exportResults.campaignExportUrl = campaignExportUrl; exportResults.campaignMessagesExportUrl = campaignMessagesExportUrl; - await sendEmail({ - to: user.email, - subject: `Export ready for ${campaign.title}`, - text: `Your Spoke exports are ready! These URLs will be valid for 24 hours. - Campaign export: ${campaignExportUrl} - Message export: ${campaignMessagesExportUrl}` - }).catch(err => { - log.error(err); - log.info(`Campaign Export URL - ${campaignExportUrl}`); - log.info(`Campaign Messages Export URL - ${campaignMessagesExportUrl}`); - }); - log.info(`Successfully exported ${id}`); + // extreme check on email set-up + if (( + process.env.EMAIL_FROM && + process.env.EMAIL_HOST && + process.env.EMAIL_HOST_PASSWORD && + process.env.EMAIL_HOST_PORT && + process.env.EMAIL_HOST_USER) || + ( + process.env.MAILGUN_DOMAIN && + process.env.MAILGUN_SMTP_LOGIN && + process.env.MAILGUN_SMTP_PASSWORD && + process.env.MAILGUN_SMTP_PORT && + process.env.MAILGUN_SMTP_SERVER && + process.env.MAILGUN_PUBLIC_KEY + ) + ) { + await sendEmail({ + to: user.email, + subject: `Export ready for ${campaign.title}`, + text: `Your Spoke exports are ready! These URLs will be valid for 24 hours. + Campaign export: ${campaignExportUrl} + Message export: ${campaignMessagesExportUrl}` + }).catch(err => { + log.error(err); + log.info(`Campaign Export URL - ${campaignExportUrl}`); + log.info(`Campaign Messages Export URL - ${campaignMessagesExportUrl}`); + }); + log.info(`Successfully exported ${id}`); + } } catch (err) { log.error(err); exportResults.error = err.message; @@ -927,7 +994,7 @@ export async function exportCampaign(job) { log.debug(campaignCsv); log.debug(messageCsv); } - if (exportResults.campaignExportUrl) { + if (exportResults.campaignExportUrl || exportResults.error) { exportResults.createdAt = String(new Date()); await cacheableData.campaign.saveExportData(campaign.id, exportResults); }