Skip to content

Commit bd33d59

Browse files
committed
[Wiegand] --force argument in script to upload parquet files to R2
1 parent 93898b2 commit bd33d59

3 files changed

Lines changed: 67 additions & 11 deletions

File tree

scripts/generate-wiegand-parquet/cli.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface CliConfig {
77
country: Country
88
upload: boolean
99
local: boolean
10+
force: boolean
1011
}
1112

1213
export function parseCli(): CliConfig {
@@ -15,6 +16,7 @@ export function parseCli(): CliConfig {
1516
country: { type: 'string', short: 'c' },
1617
upload: { type: 'boolean', short: 'u', default: false },
1718
local: { type: 'boolean', short: 'l', default: false },
19+
force: { type: 'boolean', short: 'f', default: false },
1820
help: { type: 'boolean', short: 'h', default: false },
1921
},
2022
strict: true,
@@ -34,6 +36,7 @@ export function parseCli(): CliConfig {
3436
country: countriesByCode[countryCode],
3537
upload: values.upload ?? false,
3638
local: values.local ?? false,
39+
force: values.force ?? false,
3740
}
3841
}
3942

@@ -46,13 +49,14 @@ Wiegand26 -> License Plate parquet generator
4649
Generates parquet files mapping Wiegand26 decimal values to license plate texts.
4750
4851
Usage:
49-
pnpm run script:generate-wiegand-parquet -- --country <code> [--upload] [--local]
52+
pnpm run script:generate-wiegand-parquet -- --country <code> [--upload] [--local] [--force]
5053
pnpm run script:generate-wiegand-parquet -- --help
5154
5255
Options:
5356
-c, --country <code> Country code to process (${AVAILABLE_CODES})
5457
-u, --upload Upload generated files to Cloudflare R2
5558
-l, --local Upload to local Miniflare R2 (via wrangler) instead of remote (via S3 API)
59+
-f, --force Re-upload files even if they already exist on R2
5660
-h, --help Show this help message
5761
5862
Environment variables (required for --upload without --local):

scripts/generate-wiegand-parquet/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import { uploadToR2 } from './upload.js'
1010
const OUTPUT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), 'output')
1111
const TEMP_DIR = path.join(OUTPUT_DIR, 'tmp')
1212

13-
const { country, upload, local } = parseCli()
13+
const { country, upload, local, force } = parseCli()
1414

1515
async function main(): Promise<void> {
1616
logger.info('=== Wiegand26 -> License Plate parquet generator ===')
1717
logger.info('Output directory:', OUTPUT_DIR)
1818
logger.info('Country:', country.code)
19-
logger.info('Upload to R2:', upload, local ? '(local)' : '(remote)')
19+
logger.info('Upload to R2:', upload, local ? '(local)' : '(remote)', force ? '(force)' : '')
2020
logger.info('Ranges:', computeRanges().length)
2121

2222
mkdirSync(OUTPUT_DIR, { recursive: true })
@@ -26,7 +26,7 @@ async function main(): Promise<void> {
2626
rmSync(TEMP_DIR, { recursive: true })
2727

2828
if (upload) {
29-
await uploadToR2(files, country.code, local)
29+
await uploadToR2(files, country.code, local, force)
3030
} else {
3131
logger.info('Skipping R2 upload (use --upload to enable)')
3232
}

scripts/generate-wiegand-parquet/upload.ts

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { execFile } from 'node:child_process'
1+
import { exec, execFile } from 'node:child_process'
22
import { createReadStream, statSync } from 'node:fs'
33
import path from 'node:path'
44
import { promisify } from 'node:util'
5-
import { S3Client } from '@aws-sdk/client-s3'
5+
import { HeadObjectCommand, S3Client } from '@aws-sdk/client-s3'
66
import { Upload } from '@aws-sdk/lib-storage'
77
import { formatFileSize } from '../../shared/utils/formatFileSize.js'
88
import { logger } from '../../shared/utils/logger.js'
99

10+
const execAsync = promisify(exec)
1011
const execFileAsync = promisify(execFile)
1112

1213
const 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

82108
let 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

Comments
 (0)