Skip to content

Commit f7b059d

Browse files
committed
feat(setup): add cloud function for file transfer and unit tests
1 parent d032f37 commit f7b059d

File tree

11 files changed

+173
-9
lines changed

11 files changed

+173
-9
lines changed

packages/actions/src/helpers/constants.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ export const commonTerms = {
306306
checkAndPrepareCoordinatorForFinalization: "checkAndPrepareCoordinatorForFinalization",
307307
finalizeCircuit: "finalizeCircuit",
308308
finalizeCeremony: "finalizeCeremony",
309-
downloadCircuitArtifacts: "downloadCircuitArtifacts"
309+
downloadCircuitArtifacts: "downloadCircuitArtifacts",
310+
transferObject: "transferObject",
310311
}
311312
}

packages/actions/src/helpers/functions.ts

+28
Original file line numberDiff line numberDiff line change
@@ -436,3 +436,31 @@ export const finalizeCeremony = async (functions: Functions, ceremonyId: string)
436436
ceremonyId
437437
})
438438
}
439+
440+
/**
441+
* Transfer an object between two buckets
442+
* @param functions <Functions> - the Firebase cloud functions object instance.
443+
* @param originBucketName <string> - the name of the origin bucket.
444+
* @param originObjectKey <string> - the key of the origin object.
445+
* @param destinationBucketName <string> - the name of the destination bucket.
446+
* @param destinationObjectKey <string> - the key of the destination object.
447+
* @returns <Promise<boolean>> - true when the transfer is completed; otherwise false.
448+
*/
449+
export const transferObject = async (
450+
functions: Functions,
451+
originBucketName: string,
452+
originObjectKey: string,
453+
destinationBucketName: string,
454+
destinationObjectKey: string
455+
): Promise<boolean> => {
456+
const cf = httpsCallable(functions, commonTerms.cloudFunctionsNames.transferObject)
457+
458+
const { data: result }: any= await cf({
459+
originBucketName,
460+
originObjectKey,
461+
destinationBucketName,
462+
destinationObjectKey
463+
})
464+
465+
return result
466+
}

packages/actions/src/helpers/utils.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ export const parseCeremonyFile = async (path: string, cleanup: boolean = false):
9393
const localR1csPath = `./${circuitData.name}.r1cs`
9494

9595
// check that the artifacts exist in S3
96-
const s3 = new S3Client({region: 'us-east-1'})
96+
// we don't need any privileges to download this
97+
// just the correct region
98+
const s3 = new S3Client({region: artifacts.region})
9799

98100
try {
99101
await s3.send(new HeadObjectCommand({

packages/actions/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ export {
8787
verifyContribution,
8888
checkAndPrepareCoordinatorForFinalization,
8989
finalizeCircuit,
90-
finalizeCeremony
90+
finalizeCeremony,
91+
transferObject
9192
} from "./helpers/functions"
9293
export { toHex, blake512FromPath, computeSHA256ToHex, compareHashes } from "./helpers/crypto"
9394
export {

packages/actions/src/types/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,7 @@ export type SetupCeremonyData = {
624624
export type CeremonySetupTemplateCircuitArtifacts = {
625625
artifacts: {
626626
bucket: string
627+
region: string
627628
r1csStoragePath: string
628629
wasmStoragePath: string
629630
}

packages/actions/test/data/artifacts/ceremonySetup.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"title": "Test dev ceremony",
2+
"title": "Test dev 2 ceremony",
33
"description": "This is an example ceremony",
44
"startDate": "2023-08-07T00:00:00",
55
"endDate": "2023-09-10T00:00:00",

packages/actions/test/unit/storage.test.ts

+58-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import {
1717
cleanUpMockUsers,
1818
sleep,
1919
cleanUpRecursively,
20-
mockCeremoniesCleanup
20+
mockCeremoniesCleanup,
21+
generatePseudoRandomStringOfNumbers,
22+
uploadFileToS3
2123
} from "../utils/index"
2224
import { fakeCeremoniesData, fakeCircuitsData, fakeUsersData } from "../data/samples"
2325
import {
@@ -39,7 +41,7 @@ import {
3941
import { TestingEnvironment } from "../../src/types/enums"
4042
import { ChunkWithUrl, ETagWithPartNumber } from "../../src/types/index"
4143
import { getChunksAndPreSignedUrls, getWasmStorageFilePath, uploadParts } from "../../src/helpers/storage"
42-
import { completeMultiPartUpload, openMultiPartUpload } from "../../src/helpers/functions"
44+
import { completeMultiPartUpload, openMultiPartUpload, transferObject } from "../../src/helpers/functions"
4345

4446
chai.use(chaiAsPromised)
4547

@@ -684,6 +686,60 @@ describe("Storage", () => {
684686
})
685687
})
686688

689+
describe("transferObject", () => {
690+
// we need two buckets - source and destination
691+
const sourceBucketName = generatePseudoRandomStringOfNumbers(10)
692+
const destinationBucketName = generatePseudoRandomStringOfNumbers(10)
693+
const objectKey = "test.txt"
694+
fs.writeFileSync(objectKey, "test")
695+
696+
beforeAll(async () => {
697+
// login as coordinator
698+
await signInWithEmailAndPassword(userAuth, users[1].data.email, passwords[1])
699+
// create the buckets and upload the file
700+
await createS3Bucket(userFunctions, sourceBucketName)
701+
await createS3Bucket(userFunctions, destinationBucketName)
702+
await uploadFileToS3(
703+
sourceBucketName,
704+
objectKey,
705+
objectKey
706+
)
707+
})
708+
709+
it("should successfully transfer an object between buckets", async () => {
710+
const result = await transferObject(
711+
userFunctions,
712+
sourceBucketName,
713+
objectKey,
714+
destinationBucketName,
715+
objectKey
716+
)
717+
718+
expect(result).to.be.true
719+
})
720+
721+
it("should transfer an object between buckets in different regions", async () => {})
722+
it("should throw when trying to transfer an object that does not exist", async () => {
723+
await expect(transferObject(
724+
userFunctions,
725+
sourceBucketName,
726+
"i-dont-exist.txt",
727+
destinationBucketName,
728+
objectKey
729+
)).to.be.rejected
730+
})
731+
732+
afterAll(async () => {
733+
// delete the buckets
734+
await deleteObjectFromS3(sourceBucketName, objectKey)
735+
await deleteObjectFromS3(destinationBucketName, objectKey)
736+
await deleteBucket(sourceBucketName)
737+
await deleteBucket(destinationBucketName)
738+
739+
fs.unlinkSync(objectKey)
740+
})
741+
})
742+
687743
// @todo this is not used in the cli yet
688744
describe("uploadFileToStorage", () => {
689745
it("should successfully upload a file to storage", async () => {})

packages/actions/test/utils/storage.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ export const uploadFileToS3 = async (bucketName: string, objectKey: string, path
106106
const params = {
107107
Bucket: bucketName,
108108
Key: objectKey,
109-
Body: fs.createReadStream(path)
109+
Body: fs.createReadStream(path),
110+
ACL: "public-read"
110111
}
111112

112113
const command = new PutObjectCommand(params)

packages/backend/src/functions/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export {
2929
generateGetObjectPreSignedUrl,
3030
startMultiPartUpload,
3131
generatePreSignedUrlsParts,
32-
completeMultiPartUpload
32+
completeMultiPartUpload,
33+
transferObject
3334
} from "./storage"
3435
export { checkAndRemoveBlockingContributor, resumeContributionAfterTimeoutExpiration } from "./timeout"
3536

packages/backend/src/functions/storage.ts

+57-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as functions from "firebase-functions"
22
import admin from "firebase-admin"
33
import {
4+
S3Client,
5+
CopyObjectCommand,
46
GetObjectCommand,
57
CreateMultipartUploadCommand,
68
UploadPartCommand,
@@ -30,7 +32,8 @@ import {
3032
CompleteMultiPartUploadData,
3133
CreateBucketData,
3234
GeneratePreSignedUrlsPartsData,
33-
StartMultiPartUploadData
35+
StartMultiPartUploadData,
36+
TransferObjectData
3437
} from "../types/index"
3538

3639
dotenv.config()
@@ -228,6 +231,59 @@ export const createBucket = functions
228231
}
229232
})
230233

234+
/**
235+
* Transfer a public object from one bucket to another.
236+
* @returns <Promise<boolean>> - true if the operation was successful; otherwise false.
237+
*/
238+
export const transferObject = functions
239+
.runWith({
240+
memory: "512MB"
241+
})
242+
.https.onCall(async (data: TransferObjectData, context: functions.https.CallableContext): Promise<boolean> => {
243+
// Check if the user has the coordinator claim.
244+
if (!context.auth || !context.auth.token.coordinator) logAndThrowError(COMMON_ERRORS.CM_NOT_COORDINATOR_ROLE)
245+
246+
if (
247+
!data.sourceBucketName ||
248+
!data.sourceObjectKey ||
249+
!data.destinationBucketName ||
250+
!data.destinationObjectKey ||
251+
!data.sourceRegion
252+
) logAndThrowError(COMMON_ERRORS.CM_MISSING_OR_WRONG_INPUT_DATA)
253+
254+
// Connect to S3 client.
255+
const S3 = await getS3Client()
256+
257+
const copyParams = {
258+
Bucket: data.destinationBucketName,
259+
CopySource: `${data.sourceBucketName}/${encodeURIComponent(data.sourceObjectKey)}`,
260+
Key: data.destinationObjectKey,
261+
}
262+
263+
const command = new CopyObjectCommand(copyParams)
264+
265+
try {
266+
// Execute S3 command.
267+
await S3.send(command)
268+
269+
printLog(
270+
`The object was copied from ${data.sourceBucketName} to ${data.destinationBucketName}`,
271+
LogLevel.LOG
272+
)
273+
274+
return true
275+
} catch (error: any) {
276+
// eslint-disable-next-line @typescript-eslint/no-shadow
277+
if (error.$metadata.httpStatusCode === 403) logAndThrowError(SPECIFIC_ERRORS.SE_STORAGE_MISSING_PERMISSIONS)
278+
279+
// @todo handle more specific errors here.
280+
281+
// nb. do not handle common errors! This method must return false if not found!
282+
}
283+
284+
return false
285+
})
286+
231287
/**
232288
* Check if a specified object exist in a given AWS S3 bucket.
233289
* @returns <Promise<boolean>> - true if the object exist in the given bucket; otherwise false.

packages/backend/src/types/index.ts

+17
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@ export type BucketAndObjectKeyData = {
3333
objectKey: string
3434
}
3535

36+
/**
37+
* Group all the necessary data needed for running the `transferObject` cloud function.
38+
* @typedef {Object} TransferObjectData
39+
* @property {string} sourceRegion - the region of the source bucket.
40+
* @property {string} sourceBucketName - the name of the source bucket.
41+
* @property {string} sourceObjectKey - the unique key to identify the object inside the given AWS S3 source bucket.
42+
* @property {string} destinationBucketName - the name of the destination bucket.
43+
* @property {string} destinationObjectKey - the unique key to identify the object inside the given AWS S3 destination bucket.
44+
*/
45+
export type TransferObjectData = {
46+
sourceRegion: string
47+
sourceBucketName: string
48+
sourceObjectKey: string
49+
destinationBucketName: string
50+
destinationObjectKey: string
51+
}
52+
3653
/**
3754
* Group all the necessary data needed for running the `startMultiPartUpload` cloud function.
3855
* @typedef {Object} StartMultiPartUploadData

0 commit comments

Comments
 (0)