1- import { execFile } from 'node:child_process'
1+ import { exec , execFile } from 'node:child_process'
22import { createReadStream , statSync } from 'node:fs'
33import path from 'node:path'
44import { promisify } from 'node:util'
5- import { S3Client } from '@aws-sdk/client-s3'
5+ import { HeadObjectCommand , S3Client } from '@aws-sdk/client-s3'
66import { Upload } from '@aws-sdk/lib-storage'
77import { formatFileSize } from '../../shared/utils/formatFileSize.js'
88import { logger } from '../../shared/utils/logger.js'
99
10+ const execAsync = promisify ( exec )
1011const execFileAsync = promisify ( execFile )
1112
1213const R2_BUCKET = 'anpr-wiegand26-registry'
@@ -31,7 +32,7 @@ function createS3Client(): S3Client {
3132 } )
3233}
3334
34- export async function uploadToR2 ( files : string [ ] , countryCode : string , local : boolean ) : Promise < void > {
35+ export async function uploadToR2 ( files : string [ ] , countryCode : string , local : boolean , force : boolean ) : Promise < void > {
3536 const target = local ? 'local (wrangler)' : 'remote (S3 API)'
3637 if ( local ) {
3738 logger . info ( '=== Uploading %d file(s) to %s R2 bucket %s (sequential) ===' , files . length , target , R2_BUCKET )
@@ -40,15 +41,36 @@ export async function uploadToR2(files: string[], countryCode: string, local: bo
4041 }
4142
4243 let completed = 0
44+ let skipped = 0
4345
4446 async function uploadWithCounter ( filepath : string ) : Promise < void > {
47+ const key = `${ countryCode } /${ path . basename ( filepath ) } `
48+
49+ if ( ! force && ! local ) {
50+ const exists = await objectExistsRemote ( key )
51+ if ( exists ) {
52+ skipped ++
53+ logger . info ( ' [skip] %s already exists on R2' , key )
54+ return
55+ }
56+ }
57+
58+ if ( ! force && local ) {
59+ const exists = await objectExistsLocal ( key )
60+ if ( exists ) {
61+ skipped ++
62+ logger . info ( ' [skip] %s already exists on local R2' , key )
63+ return
64+ }
65+ }
66+
4567 if ( local ) {
4668 await uploadFileLocal ( filepath , countryCode )
4769 } else {
4870 await uploadFileRemote ( filepath , countryCode )
4971 }
5072 completed ++
51- logger . info ( ' [%d/%d] Uploaded %s (%s)' , completed , files . length , path . basename ( filepath ) , formatFileSize ( statSync ( filepath ) . size ) )
73+ logger . info ( ' [%d/%d] Uploaded %s (%s)' , completed , files . length - skipped , path . basename ( filepath ) , formatFileSize ( statSync ( filepath ) . size ) )
5274 }
5375
5476 if ( local ) {
@@ -76,21 +98,51 @@ export async function uploadToR2(files: string[], countryCode: string, local: bo
7698 }
7799 }
78100
79- logger . info ( '=== Upload complete ===' )
101+ if ( skipped > 0 ) {
102+ logger . info ( '=== Upload complete (%d uploaded, %d skipped) ===' , completed , skipped )
103+ } else {
104+ logger . info ( '=== Upload complete ===' )
105+ }
80106}
81107
82108let s3Client : S3Client | null = null
83109
84- async function uploadFileRemote ( filepath : string , countryCode : string ) : Promise < void > {
110+ function getS3Client ( ) : S3Client {
85111 if ( ! s3Client ) {
86112 s3Client = createS3Client ( )
87113 }
114+ return s3Client
115+ }
116+
117+ async function objectExistsRemote ( key : string ) : Promise < boolean > {
118+ const client = getS3Client ( )
119+ try {
120+ await client . send ( new HeadObjectCommand ( { Bucket : R2_BUCKET , Key : key } ) )
121+ return true
122+ } catch {
123+ return false
124+ }
125+ }
88126
127+ async function objectExistsLocal ( key : string ) : Promise < boolean > {
128+ const wranglerBin = path . resolve ( import . meta. dirname , '../../node_modules/.bin/wrangler' )
129+ try {
130+ await execAsync ( `"${ wranglerBin } " r2 object get "${ R2_BUCKET } /${ key } " --local --pipe > /dev/null` , {
131+ cwd : path . resolve ( import . meta. dirname , '../..' ) ,
132+ } )
133+ return true
134+ } catch {
135+ return false
136+ }
137+ }
138+
139+ async function uploadFileRemote ( filepath : string , countryCode : string ) : Promise < void > {
140+ const client = getS3Client ( )
89141 const key = `${ countryCode } /${ path . basename ( filepath ) } `
90142
91143 try {
92144 const upload = new Upload ( {
93- client : s3Client ,
145+ client,
94146 params : {
95147 Bucket : R2_BUCKET ,
96148 Key : key ,
0 commit comments