prepare-product-completion.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. """Verify a completion delivery and reconstruct fresh batches from source archives.
  2. Runs offline. Never installs tools, writes a library registry or publishes data.
  3. """
  4. import argparse
  5. import hashlib
  6. import json
  7. from pathlib import Path, PurePosixPath
  8. import sys
  9. import zipfile
  10. HERE = Path(__file__).resolve().parent
  11. sys.path.insert(0, str(HERE / 'voc-library'))
  12. if not (HERE / 'voc-library').is_dir():
  13. sys.path.insert(0, str(HERE.parent / 'server/voc-library'))
  14. import library_channel
  15. import structured_batch
  16. def sha(path):
  17. h = hashlib.sha256()
  18. with Path(path).open('rb') as stream:
  19. for block in iter(lambda: stream.read(1024 * 1024), b''):
  20. h.update(block)
  21. return h.hexdigest()
  22. def prepare(package, output):
  23. package, output = Path(package).resolve(), Path(output).absolute()
  24. expected_files = []
  25. for line in (package / 'FILES.sha256').read_text('utf-8').splitlines():
  26. fingerprint, rel = line.split(' ', 1)
  27. assert not PurePosixPath(rel).is_absolute() and '..' not in PurePosixPath(rel).parts and '\\' not in rel
  28. p = package / rel
  29. assert p.resolve().is_relative_to(package) and not p.is_symlink() and sha(p) == fingerprint, 'PACKAGE_HASH_MISMATCH:' + rel
  30. expected_files.append(rel)
  31. actual = {p.relative_to(package).as_posix() for p in package.rglob('*') if p.is_file()}
  32. assert actual == set(expected_files) | {'FILES.sha256'}, 'PACKAGE_FILE_SET_MISMATCH'
  33. release = json.loads((package / 'release.json').read_bytes())
  34. assert not output.exists() and not any(p.is_symlink() for p in (output, *output.parents)), 'NEW_OUTPUT_DIRECTORY_REQUIRED'
  35. output.mkdir(parents=True, mode=0o700)
  36. reports = []
  37. for batch in release['batches']:
  38. name = batch['batch']
  39. archive = package / 'batches' / name / 'staging.source.zip'
  40. assert sha(archive) == batch['sourceArchiveSha256']
  41. directory = output / name
  42. directory.mkdir(mode=0o700)
  43. with zipfile.ZipFile(archive) as z:
  44. assert len(z.namelist()) == len(set(z.namelist())), 'DUPLICATE_ARCHIVE_MEMBER'
  45. assert sum(i.file_size for i in z.infolist()) <= library_channel.MAX_EXPANDED
  46. manifest_raw = z.read('agent-batch/batch.json')
  47. assert hashlib.sha256(manifest_raw).hexdigest() == batch['batchManifestSha256']
  48. manifest = json.loads(manifest_raw)
  49. expected = {s['path']: s['sha256'] for s in manifest['sources']}
  50. expected.update({a['path']: a['sha256'] for a in manifest.get('attachments', [])})
  51. (directory / 'batch.json').write_bytes(manifest_raw)
  52. for rel, hashed in expected.items():
  53. assert rel.startswith('raw/') and ':' not in rel and '\\' not in rel and '..' not in PurePosixPath(rel).parts
  54. raw = z.read('agent-batch/02_采集档案/' + rel)
  55. assert hashlib.sha256(raw).hexdigest() == hashed
  56. path = directory / rel
  57. path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
  58. path.write_bytes(raw)
  59. _, _, _, rows, assets = structured_batch.load(directory / 'batch.json')
  60. packed, fingerprint = library_channel.pack(directory / 'batch.json')
  61. assert fingerprint == batch['channelArchiveSha256'], 'CHANNEL_RECONSTRUCTION_MISMATCH'
  62. report = {'batch': name, 'ok': True, 'sourcesVerified': len(assets),
  63. 'counts': {k: len(v) for k, v in rows.items()}, 'channelArchiveSha256': fingerprint,
  64. 'channelArchiveBytes': len(packed), 'batchPath': str(directory / 'batch.json')}
  65. reports.append(report)
  66. result = {'ok': True, 'packageFilesVerified': len(expected_files), 'batches': reports,
  67. 'networkRequests': 0, 'productionWrites': 0, 'paidCalls': 0}
  68. (output / 'prepared.json').write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  69. return result
  70. if __name__ == '__main__':
  71. parser = argparse.ArgumentParser(description=__doc__)
  72. parser.add_argument('--package', required=True)
  73. parser.add_argument('--output', required=True)
  74. args = parser.parse_args()
  75. try:
  76. print(json.dumps(prepare(args.package, args.output), ensure_ascii=False, indent=2))
  77. except Exception as error:
  78. print(json.dumps({'ok': False, 'error': str(error), 'productionWrites': 0}, ensure_ascii=False), file=sys.stderr)
  79. sys.exit(1)