feat: add public h5 dashboard and publish traceability
This commit is contained in:
@@ -237,6 +237,95 @@ class AtomicReportPublisher:
|
||||
"an artifact path escaped the controlled project root",
|
||||
) from None
|
||||
|
||||
@staticmethod
|
||||
def _is_regular_file(path: Path) -> bool:
|
||||
return path.is_file() and not path.is_symlink()
|
||||
|
||||
@staticmethod
|
||||
def _is_sha256(value: Any) -> bool:
|
||||
return (
|
||||
isinstance(value, str)
|
||||
and len(value) == 64
|
||||
and all(character in "0123456789abcdef" for character in value)
|
||||
)
|
||||
|
||||
def _existing_publication_metadata(
|
||||
self,
|
||||
report: CompanyReport,
|
||||
reservation: ReservedReport,
|
||||
built: BuiltWorkbook,
|
||||
archive_path: Path,
|
||||
result_path: Path,
|
||||
) -> Optional[Tuple[FileMetadata, FileMetadata]]:
|
||||
archive_present = archive_path.exists() or archive_path.is_symlink()
|
||||
result_present = result_path.exists() or result_path.is_symlink()
|
||||
if not archive_present and not result_present:
|
||||
return None
|
||||
if not self._is_regular_file(archive_path) or not self._is_regular_file(
|
||||
result_path
|
||||
):
|
||||
raise PublicationError(
|
||||
ErrorCode.PUBLISH_FAILED,
|
||||
"an existing company report publication is incomplete",
|
||||
)
|
||||
|
||||
semantic_sha256 = built.summary.get("semantic_sha256")
|
||||
if not self._is_sha256(semantic_sha256):
|
||||
raise PublicationError(
|
||||
ErrorCode.PUBLISH_FAILED,
|
||||
"the generated company report semantic identity is invalid",
|
||||
)
|
||||
|
||||
try:
|
||||
result_payload = json.loads(result_path.read_text(encoding="utf-8"))
|
||||
if not isinstance(result_payload, dict):
|
||||
raise ValueError("result JSON must be an object")
|
||||
artifact_payload = result_payload.get("artifact")
|
||||
if not isinstance(artifact_payload, dict):
|
||||
raise ValueError("result JSON artifact identity is missing")
|
||||
existing_sha256 = artifact_payload.get("sha256")
|
||||
if not self._is_sha256(existing_sha256):
|
||||
raise ValueError("result JSON artifact hash is invalid")
|
||||
archive_sha256 = sha256_file(archive_path)
|
||||
if archive_sha256 != existing_sha256:
|
||||
raise ValueError("archive and result JSON hashes differ")
|
||||
|
||||
artifact_storage_key = self._storage_key(archive_path)
|
||||
expected_payload = self._success_result(
|
||||
report,
|
||||
reservation,
|
||||
artifact_storage_key,
|
||||
existing_sha256,
|
||||
semantic_sha256,
|
||||
)
|
||||
if result_payload != expected_payload:
|
||||
raise ValueError("result JSON identity differs")
|
||||
|
||||
artifact = FileMetadata(
|
||||
file_kind="company_ten_day_xlsx",
|
||||
original_filename=report.filename,
|
||||
storage_key=artifact_storage_key,
|
||||
sha256=existing_sha256,
|
||||
byte_size=archive_path.stat().st_size,
|
||||
mime_type=XLSX_MIME,
|
||||
)
|
||||
result_json = FileMetadata(
|
||||
file_kind="result_json",
|
||||
original_filename=result_path.name,
|
||||
storage_key=self._storage_key(result_path),
|
||||
sha256=sha256_file(result_path),
|
||||
byte_size=result_path.stat().st_size,
|
||||
mime_type="application/json",
|
||||
)
|
||||
artifact.validate()
|
||||
result_json.validate()
|
||||
return artifact, result_json
|
||||
except (OSError, UnicodeError, TypeError, ValueError, RepositoryError):
|
||||
raise PublicationError(
|
||||
ErrorCode.PUBLISH_FAILED,
|
||||
"an existing company report publication identity is invalid",
|
||||
) from None
|
||||
|
||||
def _archive_untracked_current(self, current_path: Path, archive_root: Path) -> None:
|
||||
if not current_path.is_file():
|
||||
return
|
||||
@@ -311,6 +400,28 @@ class AtomicReportPublisher:
|
||||
|
||||
try:
|
||||
month_root.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
|
||||
existing = self._existing_publication_metadata(
|
||||
report,
|
||||
reservation,
|
||||
built,
|
||||
archive_path,
|
||||
result_path,
|
||||
)
|
||||
if existing is not None:
|
||||
artifact, result_json = existing
|
||||
if not current_existed:
|
||||
self._atomic_copy(archive_path, current_path)
|
||||
current_replaced = True
|
||||
repository.activate_report(reservation, artifact, result_json)
|
||||
return PublicationOutcome(
|
||||
current_path=current_path,
|
||||
archive_path=archive_path,
|
||||
result_path=result_path,
|
||||
artifact=artifact,
|
||||
result_json=result_json,
|
||||
)
|
||||
|
||||
self._archive_untracked_current(current_path, archive_root)
|
||||
if current_existed:
|
||||
shutil.copyfile(current_path, backup_path)
|
||||
|
||||
Reference in New Issue
Block a user