feat(files): expose exact versions for records filing

This commit is contained in:
2026-08-06 01:43:27 +02:00
parent 3a3360650f
commit 60c3d14f08
5 changed files with 286 additions and 1 deletions
+58 -1
View File
@@ -13,6 +13,7 @@ from govoplan_core.core.files import (
)
from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard
from govoplan_core.core.modules import (
CapabilityDocumentation,
DocumentationCondition,
DocumentationLink,
DocumentationTopic,
@@ -46,6 +47,10 @@ from govoplan_files.backend.provider_state import (
remote_storage_provider_states,
)
from govoplan_files.backend.search_source import create_files_search_source
from govoplan_files.backend.record_source import (
CAPABILITY_RECORD_SOURCE_FILES,
create_files_record_source,
)
register_files_change_tracking()
@@ -281,11 +286,12 @@ manifest = ModuleManifest(
name="Files",
version="0.1.18",
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
optional_dependencies=("campaigns", "encryption", "search"),
optional_dependencies=("campaigns", "encryption", "records", "search"),
provides_interfaces=(
ModuleInterfaceProvider(name="files.access", version="0.1.6"),
ModuleInterfaceProvider(name="files.campaign_attachments", version="0.1.6"),
ModuleInterfaceProvider(name=CAPABILITY_FILES_ARTIFACT_STORE, version="0.1.14"),
ModuleInterfaceProvider(name=CAPABILITY_RECORD_SOURCE_FILES, version="1.0.0"),
),
requires_interfaces=(
ModuleInterfaceRequirement(
@@ -343,6 +349,49 @@ manifest = ModuleManifest(
),
),
documentation=(
DocumentationTopic(
id="files.records.exact-version-source",
title="File exact versions into an eAkte",
summary="Let Records preserve an immutable Files version reference after current access and integrity checks.",
body=(
"When Records is enabled, Files exposes exact managed FileVersion identities through the "
"provider-neutral record-source contract. Filing verifies the active tenant, current Files "
"permission and owner/share access, the immutable version identity, and the managed blob "
"integrity gate. Records receives the filename, path, version, digest, media type, size, "
"protection state, and filing launch link; Files continues to own the bytes."
),
layer="configured",
documentation_types=("admin", "user"),
audience=("file_user", "file_manager", "records_manager", "auditor"),
related_modules=("records",),
order=40,
conditions=(
DocumentationCondition(
required_modules=("files", "records"),
required_scopes=("files:file:read",),
),
),
links=(
DocumentationLink(label="Files", href="/files", kind="runtime"),
DocumentationLink(label="Records", href="/records", kind="runtime"),
DocumentationLink(label="Files handbook", href="govoplan-files/docs/FILES_HANDBOOK.md", kind="repository"),
),
metadata={
"kind": "workflow",
"help_contexts": ["files.list", "records.action.file"],
"prerequisites": [
"The exact file version exists and passes the current Files integrity gate.",
"You currently have Files read access and Records filing authority.",
],
"steps": [
"Select the exact managed file version that belongs to the institutional record.",
"Choose the destination record and state the access purpose and filing reason.",
"Confirm filing; Files rechecks current access and resolves the exact version.",
"Open the record chronology and verify the version identity and SHA-256 digest.",
],
"outcome": "Records preserves an exact governed reference while Files remains byte authority.",
},
),
DocumentationTopic(
id="files.search.managed-content",
title="Search managed files and folders",
@@ -903,6 +952,14 @@ manifest = ModuleManifest(
CAPABILITY_FILES_ACCESS: lambda context: __import__("govoplan_files.backend.capabilities", fromlist=["access_capability"]).access_capability(context),
CAPABILITY_FILES_ARTIFACT_STORE: lambda context: __import__("govoplan_files.backend.capabilities", fromlist=["artifact_store_capability"]).artifact_store_capability(context),
"files.campaign_attachments": lambda context: __import__("govoplan_files.backend.capabilities", fromlist=["campaign_capability"]).campaign_capability(context),
CAPABILITY_RECORD_SOURCE_FILES: create_files_record_source,
},
capability_documentation={
CAPABILITY_RECORD_SOURCE_FILES: CapabilityDocumentation(
label="Files record source",
summary="Resolves currently authorized immutable managed file versions for Records filing.",
contract_version="1.0.0",
),
},
operational_check_providers=(
OperationalCheckProviderRegistration(
+113
View File
@@ -0,0 +1,113 @@
from __future__ import annotations
from collections.abc import Sequence
from urllib.parse import quote
from sqlalchemy.orm import Session
from govoplan_core.core.records import (
RecordContractError,
RecordSourceLocator,
RecordSourceReference,
)
from govoplan_files.backend.db.models import FileBlob, FileVersion
from govoplan_files.backend.storage.common import FileStorageError
from govoplan_files.backend.storage.files import get_asset_for_user
CAPABILITY_RECORD_SOURCE_FILES = "records.source.files"
class FilesRecordSource:
provider_id = "files"
def resource_types(self) -> Sequence[str]:
return ("file_version",)
def resolve(
self,
session: object,
principal: object,
*,
locator: RecordSourceLocator,
purpose: str,
) -> RecordSourceReference:
if not isinstance(session, Session):
raise RecordContractError(
"Files record references require a database session."
)
tenant_id = str(getattr(principal, "tenant_id", "") or "").strip()
if not tenant_id or locator.tenant_id != tenant_id:
raise RecordContractError("Files record references cannot cross tenants.")
if locator.source_module != "files" or locator.resource_type != "file_version":
raise RecordContractError("Unsupported Files record source type.")
if not str(purpose or "").strip():
raise RecordContractError("Files record references require a purpose.")
if not hasattr(principal, "has") or not (
principal.has("files:file:read") or principal.has("files:file:admin")
):
raise RecordContractError("Current Files read permission is required.")
user = getattr(principal, "user", None)
user_id = str(
getattr(user, "id", "") or getattr(principal, "membership_id", "") or ""
).strip()
if not user_id:
raise RecordContractError(
"Files record references require a tenant user principal."
)
try:
asset = get_asset_for_user(
session,
tenant_id=tenant_id,
user_id=user_id,
asset_id=locator.resource_id,
is_admin=principal.has("files:file:admin"),
)
except FileStorageError as exc:
raise RecordContractError(str(exc)) from exc
version_query = session.query(FileVersion).filter(
FileVersion.tenant_id == tenant_id,
FileVersion.file_asset_id == asset.id,
)
revision = locator.source_revision.strip()
version = version_query.filter(FileVersion.id == revision).one_or_none()
if version is None:
raise RecordContractError("The exact file version does not exist.")
blob = session.get(FileBlob, version.blob_id)
if blob is None or blob.tenant_id != tenant_id:
raise RecordContractError(
"The exact file version has no managed content object."
)
if blob.quarantined_at is not None or blob.integrity_status == "failed":
raise RecordContractError(
"The exact file version failed the current integrity gate."
)
return RecordSourceReference(
locator=locator,
label=version.filename_at_upload,
authority_mode="external_authoritative",
content_sha256=version.checksum_sha256,
content_type=version.content_type,
size_bytes=version.size_bytes,
recorded_at=version.created_at,
launch_url=(
f"/files?fileId={quote(asset.id, safe='')}&versionId={quote(version.id, safe='')}"
),
metadata={
"display_path": version.display_path_at_upload,
"version_number": version.version_number,
"integrity_status": blob.integrity_status,
"protection": blob.protection_discriminator,
},
)
def create_files_record_source(_context: object) -> FilesRecordSource:
return FilesRecordSource()
__all__ = [
"CAPABILITY_RECORD_SOURCE_FILES",
"FilesRecordSource",
"create_files_record_source",
]