import fs from 'node:fs' import path from 'node:path' import { inventory, hash, verifyPublicObject, PROJECT } from './upload.mjs' import { validateCosMediaManifest, loadCosMediaManifest } from '../../build/tang-detective-cos-media.mjs' const json = value => JSON.stringify(value, null, 2) + '\n' const write = (file, value) => { const temp = `${file}.${process.pid}.tmp` fs.writeFileSync(temp, json(value), { flag: 'wx' }) fs.renameSync(temp, file) } const destination = { bucket: 'gz-1349751149', region: 'ap-guangzhou', baseUrl: 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com' } const mode = process.argv[2] if (!['activate', 'audit'].includes(mode)) throw new Error('USE_ACTIVATE_OR_AUDIT') const sourceDirectory = path.join(PROJECT, 'native/tang-detective') const manifestPath = path.join(PROJECT, 'build/tang-detective-cos-manifest.json') let manifest if (mode === 'activate') { if (fs.existsSync(manifestPath)) throw new Error('ACTIVATION_MANIFEST_EXISTS_USE_AUDIT') const input = inventory(PROJECT) // No missing source or changed original can activate a new mapping. const plan = JSON.parse(fs.readFileSync(path.join(PROJECT, 'build/tang-detective-admin-upload-plan.json'))) const observed = JSON.parse(fs.readFileSync(path.join(PROJECT, 'build/tang-detective-admin-upload-observations.json'))) if (plan.uploadRunId !== observed.uploadRunId || plan.sourceManifestSha256 !== input.sourceManifestSha256 || observed.baseUrl !== destination.baseUrl || observed.images.length !== 135 || observed.audio.length !== 44 || observed.imageDirectory !== 'uploads/images/20260908/' || observed.audioDirectory !== 'uploads/voice/20260908/') { throw new Error('OBSERVATION_PLAN_BINDING_FAILED') } const byHash = new Map() for (const entry of plan.entries) { let filename, directory if (!entry.stagedName) { filename = observed.firstImage directory = observed.imageDirectory if (entry.observedUrl !== `${observed.baseUrl}/${directory}${filename}`) throw new Error('FIRST_SAMPLE_CHANGED') } else { const match = /^tang-20260908-1714-(image|audio)-(\d{3})\.(jpg|mp3)$/.exec(entry.stagedName) if (!match || match[1] !== entry.kind) throw new Error('INVALID_STAGED_NAME') const i = Number(match[2]) - 1 filename = (entry.kind === 'image' ? observed.images : observed.audio)[i] directory = entry.kind === 'image' ? observed.imageDirectory : observed.audioDirectory } if (!/^[a-z0-9.-]+$/.test(filename || '')) throw new Error('UNSAFE_OBSERVED_FILENAME') byHash.set(entry.sha256, { ...entry, objectKey: directory + filename, url: `${observed.baseUrl}/${directory}${filename}` }) } if (byHash.size !== 180) throw new Error('INCOMPLETE_PLAN') manifest = { schemaVersion: 2, uploadRunId: plan.uploadRunId, sourceManifestSha256: input.sourceManifestSha256, destination, entries: input.entries.map(entry => { const mapping = byHash.get(entry.sha256) if (!mapping || mapping.bytes !== entry.bytes || mapping.contentType !== entry.contentType) throw new Error('PLAN_BYTES_CHANGED') return { ...entry, objectKey: mapping.objectKey, url: mapping.url } }) } } else { loadCosMediaManifest({ sourceDirectory }) // Validate source, receipt and complete existing binding first. manifest = JSON.parse(fs.readFileSync(manifestPath)) } const unique = [...new Map(manifest.entries.map(entry => [entry.url, entry])).values()] const receipt = { schemaVersion: 1, runId: manifest.uploadRunId, mode, startedAt: new Date().toISOString(), phase: 'public-readback', complete: false, uploadTransport: 'authenticated-existing-admin-ui', destination, sourceManifestSha256: manifest.sourceManifestSha256, bucketPermissionsChanged: false, originalFilesChanged: false, objects: [] } const receiptPath = path.join(PROJECT, `build/tang-detective-admin-${mode === 'activate' ? 'upload' : 'audit'}-receipt.json`) write(receiptPath, receipt) try { // Each unsigned GET is independent. Four workers, no access token, database connection or upload here. let cursor = 0 const outcomes = await Promise.allSettled(Array.from({ length: 4 }, async () => { for (;;) { const i = cursor++ if (i >= unique.length) return const entry = unique[i] const url = new URL(entry.url) if (url.origin !== destination.baseUrl || url.search || url.hash || url.username || url.password || !/^\/uploads\/(images|voice)\/20260908\/[a-z0-9.-]+$/.test(url.pathname)) throw new Error('UNSAFE_READBACK_URL') const verified = await verifyPublicObject(entry) receipt.objects.push({ ...entry, ...verified, uploaded: true, publicReadVerified: true }) write(receiptPath, receipt) if (receipt.objects.length % 20 === 0) console.log(JSON.stringify({ verified: receipt.objects.length, total: unique.length })) } })) const failure = outcomes.find(outcome => outcome.status === 'rejected') if (failure) throw failure.reason // All task-owned requests have settled before recording failure. const verified = new Map(receipt.objects.map(entry => [entry.url, entry])) const completeManifest = { ...manifest, entries: manifest.entries.map(entry => { const proof = verified.get(entry.url) return { ...entry, uploaded: true, publicReadVerified: true, remoteVerifiedSha256: proof.remoteVerifiedSha256, rangeVerified: proof.rangeVerified, verifiedAt: proof.verifiedAt } }) } // Canonical receipt binds the original activation manifest. Audit never changes it or prune hashes. if (mode === 'activate') { validateCosMediaManifest(completeManifest, { sourceDirectory }) inventory(PROJECT) fs.writeFileSync(manifestPath, json(completeManifest), { flag: 'wx' }) } receipt.complete = true receipt.phase = 'complete' receipt.mediaManifestSha256 = hash(fs.readFileSync(manifestPath)) receipt.completedAt = new Date().toISOString() write(receiptPath, receipt) console.log(JSON.stringify({ complete: true, uniqueObjects: unique.length, sourceMediaPaths: manifest.entries.length, allBytesHashesTypesVerified: true, audioRangeVerified: unique.filter(e => e.kind === 'audio').length, mode, manifestPath })) } catch (error) { receipt.phase = 'failed' receipt.failedAt = new Date().toISOString() receipt.errorCode = 'PUBLIC_READBACK_OR_MANIFEST_VALIDATION_FAILED' write(receiptPath, receipt) throw error }