@@ -30,14 +30,14 @@ import {
3030} from " cloudflare:workers" ;
3131
3232
33- // We are using Email Routing to send emails out and D1 for our cart database
33+ // We are using R2 to store the D1 backup
3434type Env = {
3535 BACKUP_WORKFLOW: Workflow ;
3636 D1_REST_API_TOKEN: string ;
3737 BACKUP_BUCKET: R2Bucket ;
3838};
3939
40- // Workflow parameters: we expect a cartId
40+ // Workflow parameters: we expect accountId and databaseId
4141type Params = {
4242 accountId: string ;
4343 databaseId: string ;
@@ -46,85 +46,40 @@ type Params = {
4646// Workflow logic
4747export class backupWorkflow extends WorkflowEntrypoint <Env , Params > {
4848 async run(event : WorkflowEvent <Params >, step : WorkflowStep ) {
49-
50-
51- // Retrieve the cart from the D1 database
52- // if the cart hasn't been checked out yet retry every 2 minutes, 10 times, otherwise give up
53- const cart = await step .do (
54- " retrieve cart" ,
55- {
56- retries: {
57- limit: 10 ,
58- delay: 2000 * 60 ,
59- backoff: " constant" ,
60- },
61- timeout: " 30 seconds" ,
62- },
63- async () => {
64- const { results } = await this .env .DB .prepare (
65- ` SELECT * FROM cart WHERE id = ? ` ,
66- )
67- .bind (event .payload .cartId )
68- .all ();
69- // should return { checkedOut: true, amount: 250 , account: { email: "[email protected] " }}; 70- if (results [0 ].checkedOut === false ) {
71- throw new Error (" cart hasn't been checked out yet" );
72- }
73- return results [0 ];
74- },
75- );
76-
77- // Proceed to payment, retry 10 times every minute or give up
78- const payment = await step .do (
79- " payment" ,
80- {
81- retries: {
82- limit: 10 ,
83- delay: 1000 * 60 ,
84- backoff: " constant" ,
85- },
86- timeout: " 30 seconds" ,
87- },
88- async () => {
89- let resp = await fetch (" https://payment-processor.example.com/" , {
90- method: " POST" ,
91- headers: {
92- " Content-Type" : " application/json; charset=utf-8" ,
93- },
94- body: JSON .stringify ({ amount: cart .amount }),
95- });
96-
97- if (! resp .ok ) {
98- throw new Error (" payment has failed" );
99- }
100-
101- return { success: true , amount: cart .amount };
102- },
103- );
104-
105- // Send invoice to the customer, retry 10 times every 5 minutes or give up
106- // Requires that cart.account.email has previously been validated in Email Routing,
107- // See https://developers.cloudflare.com/email-routing/email-workers/
108- await step .do (
109- " send invoice" ,
110- {
111- retries: {
112- limit: 10 ,
113- delay: 5000 * 60 ,
114- backoff: " constant" ,
115- },
116- timeout: " 30 seconds" ,
117- },
118- async () => {
119- const message = genEmail (cart .account .email , payment .amount );
120- try {
121- await this .env .SEND_EMAIL .send (message );
122- } catch (e ) {
123- throw new Error (" failed to send invoice" );
124- }
125- },
126- );
49+ const { accountId, databaseId } = event .payload ;
12750
51+ const url = ` https://api.cloudflare.com/client/v4/accounts/${accountId }/d1/database/${databaseId }/export ` ;
52+ const method = " POST" ;
53+ const headers = new Headers ();
54+ headers .append (" Content-Type" , " application/json" );
55+ headers .append (" Authorization" , ` Bearer ${this .env .D1_REST_API_TOKEN } ` );
56+
57+ const bookmark = step .do (` Starting backup for ${databaseId } ` , async () => {
58+ const payload = { output_format: " polling" };
59+
60+ const res = await fetch (url , { method , headers , body: JSON .stringify (payload ) });
61+ const { result } = (await res .json ()) as any ;
62+
63+ // If we don't get `at_bookmark` we throw to retry the step
64+ if (! result ?.at_bookmark ) throw new Error (" Missing `at_bookmark`" );
65+
66+ return result .at_bookmark ;
67+ });
68+
69+ step .do (" Check backup status and store it on R2" , async () => {
70+ const payload = { current_bookmark: bookmark };
71+
72+ const res = await fetch (url , { method , headers , body: JSON .stringify (payload ) });
73+ const { result } = (await res .json ()) as any ;
74+
75+ // The endpoint sends `signed_url` when the backup is ready.
76+ // If we don't get `signed_url` we throw to retry the step.
77+ if (! result ?.signed_url ) throw new Error (" Missing `signed_url`" );
78+
79+ const dumpResponse = await fetch (result .signed_url );
80+ // We stream the file directly to R2
81+ await this .env .BACKUP_BUCKET .put (result .filename , dumpResponse .body );
82+ });
12883 }
12984}
13085
@@ -134,8 +89,8 @@ export default {
13489 },
13590 async scheduled(controller : ScheduledController , env : Env , ctx : ExecutionContext ) {
13691 const params: Params = {
137- accountId: " {account_id }" ,
138- databaseId: " {database_id },
92+ accountId: " {accountId }" ,
93+ databaseId: " {databaseId },
13994 };
14095 const instance = await env .BACKUP_WORKFLOW .create ({ params });
14196 console .log (` Started workflow: ${instance .id } ` );
@@ -171,6 +126,10 @@ name = "backup-workflow"
171126binding = " BACKUP_WORKFLOW"
172127class_name = " backupWorkflow"
173128
129+ [[r2_buckets ]]
130+ binding = " BACKUP_BUCKET"
131+ bucket_name = " d1-backups"
132+
174133[triggers ]
175134crons = [ " 0 0 * * *" ]
176135
0 commit comments