11import { execFile } from 'node:child_process'
2- import { statSync } from 'node:fs'
2+ import { createReadStream , statSync } from 'node:fs'
33import path from 'node:path'
44import { promisify } from 'node:util'
5+ import { S3Client } from '@aws-sdk/client-s3'
6+ import { Upload } from '@aws-sdk/lib-storage'
57import { formatFileSize } from '../../shared/utils/formatFileSize.js'
68import { logger } from '../../shared/utils/logger.js'
79
@@ -10,43 +12,106 @@ const execFileAsync = promisify(execFile)
1012const R2_BUCKET = 'anpr-wiegand26-registry'
1113const CONCURRENCY = 6
1214
15+ function createS3Client ( ) : S3Client {
16+ const accountId = process . env . TOOLBOX_CF_ACCOUNT_ID
17+ const accessKeyId = process . env . TOOLBOX_CF_R2_ACCESS_KEY_ID
18+ const secretAccessKey = process . env . TOOLBOX_CF_R2_SECRET_ACCESS_KEY
19+
20+ if ( ! accountId || ! accessKeyId || ! secretAccessKey ) {
21+ throw new Error (
22+ 'Missing R2 credentials. Set TOOLBOX_CF_ACCOUNT_ID, TOOLBOX_CF_R2_ACCESS_KEY_ID, and TOOLBOX_CF_R2_SECRET_ACCESS_KEY environment variables.\n'
23+ + 'Create an R2 API token at: https://dash.cloudflare.com → R2 → Manage R2 API Tokens' ,
24+ )
25+ }
26+
27+ return new S3Client ( {
28+ region : 'auto' ,
29+ endpoint : `https://${ accountId } .r2.cloudflarestorage.com` ,
30+ credentials : { accessKeyId, secretAccessKey } ,
31+ } )
32+ }
33+
1334export async function uploadToR2 ( files : string [ ] , countryCode : string , local : boolean ) : Promise < void > {
14- const target = local ? 'local' : 'remote'
15- logger . info ( '=== Uploading %d file(s) to %s R2 bucket %s (%d concurrent) ===' , files . length , target , R2_BUCKET , CONCURRENCY )
35+ const target = local ? 'local (wrangler)' : 'remote (S3 API)'
36+ if ( local ) {
37+ logger . info ( '=== Uploading %d file(s) to %s R2 bucket %s (sequential) ===' , files . length , target , R2_BUCKET )
38+ } else {
39+ logger . info ( '=== Uploading %d file(s) to %s R2 bucket %s (%d concurrent) ===' , files . length , target , R2_BUCKET , CONCURRENCY )
40+ }
1641
1742 let completed = 0
1843
1944 async function uploadWithCounter ( filepath : string ) : Promise < void > {
20- await uploadFile ( filepath , countryCode , local )
45+ if ( local ) {
46+ await uploadFileLocal ( filepath , countryCode )
47+ } else {
48+ await uploadFileRemote ( filepath , countryCode )
49+ }
2150 completed ++
2251 logger . info ( ' [%d/%d] Uploaded %s (%s)' , completed , files . length , path . basename ( filepath ) , formatFileSize ( statSync ( filepath ) . size ) )
2352 }
2453
25- const queue = [ ...files ]
26- const running = new Set < Promise < void > > ( )
54+ if ( local ) {
55+ for ( const filepath of files ) {
56+ await uploadWithCounter ( filepath )
57+ }
58+ } else {
59+ const queue = [ ...files ]
60+ const running = new Set < Promise < void > > ( )
2761
28- while ( queue . length > 0 || running . size > 0 ) {
29- while ( running . size < CONCURRENCY && queue . length > 0 ) {
30- const filepath = queue . shift ( )
31- if ( filepath === undefined ) {
32- break
62+ while ( queue . length > 0 || running . size > 0 ) {
63+ while ( running . size < CONCURRENCY && queue . length > 0 ) {
64+ const filepath = queue . shift ( )
65+ if ( filepath === undefined ) {
66+ break
67+ }
68+ const promise = uploadWithCounter ( filepath ) . then ( ( ) => {
69+ running . delete ( promise )
70+ } )
71+ running . add ( promise )
72+ }
73+ if ( running . size > 0 ) {
74+ await Promise . race ( running )
3375 }
34- const promise = uploadWithCounter ( filepath ) . then ( ( ) => {
35- running . delete ( promise )
36- } )
37- running . add ( promise )
38- }
39- if ( running . size > 0 ) {
40- await Promise . race ( running )
4176 }
4277 }
4378
4479 logger . info ( '=== Upload complete ===' )
4580}
4681
47- async function uploadFile ( filepath : string , countryCode : string , local : boolean ) : Promise < void > {
82+ let s3Client : S3Client | null = null
83+
84+ async function uploadFileRemote ( filepath : string , countryCode : string ) : Promise < void > {
85+ if ( ! s3Client ) {
86+ s3Client = createS3Client ( )
87+ }
88+
89+ const key = `${ countryCode } /${ path . basename ( filepath ) } `
90+
91+ try {
92+ const upload = new Upload ( {
93+ client : s3Client ,
94+ params : {
95+ Bucket : R2_BUCKET ,
96+ Key : key ,
97+ Body : createReadStream ( filepath ) ,
98+ ContentType : 'application/octet-stream' ,
99+ } ,
100+ queueSize : 4 ,
101+ partSize : 10 * 1024 * 1024 ,
102+ } )
103+
104+ await upload . done ( )
105+ } catch ( error : unknown ) {
106+ const message = error instanceof Error ? error . message : String ( error )
107+ logger . error ( ' Failed to upload %s: %s' , key , message )
108+ throw error
109+ }
110+ }
111+
112+ async function uploadFileLocal ( filepath : string , countryCode : string ) : Promise < void > {
48113 const key = `${ countryCode } /${ path . basename ( filepath ) } `
49- const args = [ 'r2' , 'object' , 'put' , `${ R2_BUCKET } /${ key } ` , '--file' , filepath , ... ( local ? [ '--local' ] : [ '--remote' ] ) ]
114+ const args = [ 'r2' , 'object' , 'put' , `${ R2_BUCKET } /${ key } ` , '--file' , filepath , '--local' ]
50115 const wranglerBin = path . resolve ( import . meta. dirname , '../../node_modules/.bin/wrangler' )
51116
52117 try {
0 commit comments