← 메인 화면프로젝트 자료·코드
← 자료 목록

cc/tools/publish_materials.py

프로그램·코드 · 3.9 KB원본 다운로드

원본 내용을 읽기 전용으로 표시합니다.

  1. """E:/proj의 공개용 코드·자료만 별도 묶음으로 복사. 원본 실행/수정은 하지 않는다."""
  2. import argparse
  3. import csv
  4. import hashlib
  5. import json
  6. import re
  7. from collections import Counter
  8. from datetime import datetime, timezone
  9. from pathlib import Path
  10. DENIED = {'.git', '.venv', '__pycache__', 'cache', '.pytest_cache', 'artifacts', 'vendor'}
  11. SOURCE_EXTENSIONS = {'.py', '.html', '.css', '.js'}
  12. SECRET_PATTERNS = [
  13. re.compile(r'-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----'),
  14. re.compile(r'\b(?:sk-|gw_)[A-Za-z0-9_\-]{25,}'),
  15. re.compile(r'''(?i)(?:api[_-]?key|password|passwd|secret[_-]?key|access[_-]?token)\s*[=:]\s*["']([^"'\n]{8,})["']'''),
  16. ]
  17. def candidates(root):
  18. chosen = [(p, 'code') for p in root.glob('*.py')]
  19. chosen += [(p, 'map' if p.suffix == '.html' else 'image' if p.suffix == '.png' else 'csv' if p.suffix == '.csv' else 'text')
  20. for p in (root / 'output').iterdir() if p.is_file() and p.suffix in {'.html', '.png', '.csv', '.txt'}]
  21. chosen += [(p, 'csv' if p.suffix == '.csv' else 'text') for p in (root / 'data').iterdir() if p.is_file() and p.suffix in {'.csv', '.geojson'}]
  22. for folder in ('startup_app', 'tests', 'tools'):
  23. chosen += [(p, 'code') for p in (root / 'cc' / folder).rglob('*') if p.is_file() and p.suffix in SOURCE_EXTENSIONS and not DENIED.intersection(p.relative_to(root).parts)]
  24. chosen += [(root / 'cc' / name, 'text') for name in ('requirements.txt', 'requirements-dev.txt')]
  25. def order(item):
  26. parts = item[0].relative_to(root).parts
  27. group = 0 if len(parts) == 1 else {'output': 1, 'data': 2, 'cc': 3}.get(parts[0], 4)
  28. return group, item[0].as_posix()
  29. return sorted(chosen, key=order)
  30. def publish(root, output):
  31. root, output = root.resolve(), output.resolve()
  32. selected = candidates(root)
  33. entries, contents = [], []
  34. for source, kind in selected:
  35. if source.is_symlink() or not source.resolve().is_relative_to(root):
  36. raise ValueError(f'허용되지 않은 경로: {source.name}')
  37. raw = source.read_bytes()
  38. if kind != 'image':
  39. text = raw.decode('utf-8-sig')
  40. if any(pattern.search(text) for pattern in SECRET_PATTERNS):
  41. # 값은 출력하지 않는다. 검토 후 별도로 공개해야 한다.
  42. raise ValueError(f'자격증명 가능성이 있어 공개 중단: {source.relative_to(root)}')
  43. relative = source.relative_to(root).as_posix()
  44. digest = hashlib.sha256(raw).hexdigest()
  45. entry = {'id': hashlib.sha256((relative + '\0' + digest).encode()).hexdigest()[:20], 'path': relative, 'kind': kind, 'size': len(raw), 'sha256': digest}
  46. if kind == 'csv':
  47. with source.open(encoding='utf-8-sig', newline='') as stream:
  48. reader = csv.reader(stream)
  49. entry['columns'] = len(next(reader, []))
  50. entry['rows'] = sum(1 for _ in reader)
  51. entries.append(entry)
  52. contents.append(raw)
  53. for entry, raw in zip(entries, contents):
  54. destination = output / 'files' / entry['path']
  55. destination.parent.mkdir(parents=True, exist_ok=True)
  56. destination.write_bytes(raw)
  57. manifest = {'created': datetime.now(timezone.utc).isoformat(), 'files': entries}
  58. output.mkdir(parents=True, exist_ok=True)
  59. temporary = output / 'manifest.next.json'
  60. temporary.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding='utf-8')
  61. temporary.replace(output / 'manifest.json')
  62. return {'count': len(entries), 'types': dict(Counter(e['kind'] for e in entries)), 'bytes': sum(e['size'] for e in entries)}
  63. if __name__ == '__main__':
  64. parser = argparse.ArgumentParser()
  65. parser.add_argument('--root', type=Path, default=Path('E:/proj'))
  66. parser.add_argument('--output', type=Path, default=Path(__file__).resolve().parents[1] / 'public_materials')
  67. args = parser.parse_args()
  68. print(json.dumps(publish(args.root, args.output), ensure_ascii=False))
파일 확인 정보

SHA-256: 0262b9c5feabe5b97c7e3febbb502a6ca7c3e49274008832dabac66a77088c25