gengxin
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user