- Local export was successful, saved on the server at:
-
- {campaign.exportResults.campaignExportUrl}
-
- {campaign.exportResults.campaignMessagesExportUrl}
-
+ 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);
}