| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687 |
- """Verify a completion delivery and reconstruct fresh batches from source archives.
- Runs offline. Never installs tools, writes a library registry or publishes data.
- """
- import argparse
- import hashlib
- import json
- from pathlib import Path, PurePosixPath
- import sys
- import zipfile
- HERE = Path(__file__).resolve().parent
- sys.path.insert(0, str(HERE / 'voc-library'))
- if not (HERE / 'voc-library').is_dir():
- sys.path.insert(0, str(HERE.parent / 'server/voc-library'))
- import library_channel
- import structured_batch
- def sha(path):
- h = hashlib.sha256()
- with Path(path).open('rb') as stream:
- for block in iter(lambda: stream.read(1024 * 1024), b''):
- h.update(block)
- return h.hexdigest()
- def prepare(package, output):
- package, output = Path(package).resolve(), Path(output).absolute()
- expected_files = []
- for line in (package / 'FILES.sha256').read_text('utf-8').splitlines():
- fingerprint, rel = line.split(' ', 1)
- assert not PurePosixPath(rel).is_absolute() and '..' not in PurePosixPath(rel).parts and '\\' not in rel
- p = package / rel
- assert p.resolve().is_relative_to(package) and not p.is_symlink() and sha(p) == fingerprint, 'PACKAGE_HASH_MISMATCH:' + rel
- expected_files.append(rel)
- actual = {p.relative_to(package).as_posix() for p in package.rglob('*') if p.is_file()}
- assert actual == set(expected_files) | {'FILES.sha256'}, 'PACKAGE_FILE_SET_MISMATCH'
- release = json.loads((package / 'release.json').read_bytes())
- assert not output.exists() and not any(p.is_symlink() for p in (output, *output.parents)), 'NEW_OUTPUT_DIRECTORY_REQUIRED'
- output.mkdir(parents=True, mode=0o700)
- reports = []
- for batch in release['batches']:
- name = batch['batch']
- archive = package / 'batches' / name / 'staging.source.zip'
- assert sha(archive) == batch['sourceArchiveSha256']
- directory = output / name
- directory.mkdir(mode=0o700)
- with zipfile.ZipFile(archive) as z:
- assert len(z.namelist()) == len(set(z.namelist())), 'DUPLICATE_ARCHIVE_MEMBER'
- assert sum(i.file_size for i in z.infolist()) <= library_channel.MAX_EXPANDED
- manifest_raw = z.read('agent-batch/batch.json')
- assert hashlib.sha256(manifest_raw).hexdigest() == batch['batchManifestSha256']
- manifest = json.loads(manifest_raw)
- expected = {s['path']: s['sha256'] for s in manifest['sources']}
- expected.update({a['path']: a['sha256'] for a in manifest.get('attachments', [])})
- (directory / 'batch.json').write_bytes(manifest_raw)
- for rel, hashed in expected.items():
- assert rel.startswith('raw/') and ':' not in rel and '\\' not in rel and '..' not in PurePosixPath(rel).parts
- raw = z.read('agent-batch/02_采集档案/' + rel)
- assert hashlib.sha256(raw).hexdigest() == hashed
- path = directory / rel
- path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
- path.write_bytes(raw)
- _, _, _, rows, assets = structured_batch.load(directory / 'batch.json')
- packed, fingerprint = library_channel.pack(directory / 'batch.json')
- assert fingerprint == batch['channelArchiveSha256'], 'CHANNEL_RECONSTRUCTION_MISMATCH'
- report = {'batch': name, 'ok': True, 'sourcesVerified': len(assets),
- 'counts': {k: len(v) for k, v in rows.items()}, 'channelArchiveSha256': fingerprint,
- 'channelArchiveBytes': len(packed), 'batchPath': str(directory / 'batch.json')}
- reports.append(report)
- result = {'ok': True, 'packageFilesVerified': len(expected_files), 'batches': reports,
- 'networkRequests': 0, 'productionWrites': 0, 'paidCalls': 0}
- (output / 'prepared.json').write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
- return result
- if __name__ == '__main__':
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--package', required=True)
- parser.add_argument('--output', required=True)
- args = parser.parse_args()
- try:
- print(json.dumps(prepare(args.package, args.output), ensure_ascii=False, indent=2))
- except Exception as error:
- print(json.dumps({'ok': False, 'error': str(error), 'productionWrites': 0}, ensure_ascii=False), file=sys.stderr)
- sys.exit(1)
|