200 lines
6.4 KiB
Python
200 lines
6.4 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy import event, inspect
|
|
from sqlalchemy.orm import Session as OrmSession
|
|
|
|
from govoplan_core.core.change_sequence import record_change
|
|
from govoplan_files.backend.db.models import FileAsset, FileFolder, FileShare, new_uuid
|
|
|
|
FILES_MODULE_ID = "files"
|
|
FILES_ASSETS_COLLECTION = "files.assets"
|
|
FILES_FOLDERS_COLLECTION = "files.folders"
|
|
FILES_CONNECTOR_PROFILES_COLLECTION = "files.connector_profiles"
|
|
FILES_CONNECTOR_CREDENTIALS_COLLECTION = "files.connector_credentials"
|
|
FILES_CONNECTOR_POLICIES_COLLECTION = "files.connector_policies"
|
|
FILES_CONNECTOR_SPACES_COLLECTION = "files.connector_spaces"
|
|
|
|
_REGISTERED = False
|
|
|
|
|
|
def register_files_change_tracking() -> None:
|
|
global _REGISTERED
|
|
if _REGISTERED:
|
|
return
|
|
event.listen(OrmSession, "before_flush", _record_files_changes)
|
|
_REGISTERED = True
|
|
|
|
|
|
def _record_files_changes(session: OrmSession, _flush_context: object, _instances: object) -> None:
|
|
for obj in tuple(session.new) + tuple(session.dirty):
|
|
if isinstance(obj, FileAsset):
|
|
_record_asset_change(session, obj)
|
|
elif isinstance(obj, FileFolder):
|
|
_record_folder_change(session, obj)
|
|
elif isinstance(obj, FileShare):
|
|
_record_share_visibility_change(session, obj)
|
|
|
|
|
|
def _record_asset_change(session: OrmSession, asset: FileAsset) -> None:
|
|
operation = _operation_for_soft_deletable(
|
|
asset,
|
|
changed_attrs=(
|
|
"owner_type",
|
|
"owner_user_id",
|
|
"owner_group_id",
|
|
"current_version_id",
|
|
"display_path",
|
|
"filename",
|
|
"description",
|
|
"deleted_at",
|
|
"metadata_",
|
|
),
|
|
)
|
|
if operation is None:
|
|
return
|
|
resource_id = _ensure_id(asset)
|
|
record_change(
|
|
session,
|
|
module_id=FILES_MODULE_ID,
|
|
collection=FILES_ASSETS_COLLECTION,
|
|
resource_type="file",
|
|
resource_id=resource_id,
|
|
operation=operation,
|
|
tenant_id=asset.tenant_id,
|
|
actor_type="user" if asset.created_by_user_id else None,
|
|
actor_id=asset.created_by_user_id,
|
|
payload={
|
|
"owner_type": asset.owner_type,
|
|
"owner_id": _owner_id(asset.owner_type, asset.owner_user_id, asset.owner_group_id),
|
|
"path": asset.display_path,
|
|
"previous_path": _previous_value(asset, "display_path"),
|
|
"filename": asset.filename,
|
|
"deleted_at": _isoformat(asset.deleted_at),
|
|
},
|
|
)
|
|
|
|
|
|
def _record_folder_change(session: OrmSession, folder: FileFolder) -> None:
|
|
operation = _operation_for_soft_deletable(
|
|
folder,
|
|
changed_attrs=("owner_type", "owner_user_id", "owner_group_id", "path", "deleted_at", "metadata_"),
|
|
)
|
|
if operation is None:
|
|
return
|
|
resource_id = _ensure_id(folder)
|
|
record_change(
|
|
session,
|
|
module_id=FILES_MODULE_ID,
|
|
collection=FILES_FOLDERS_COLLECTION,
|
|
resource_type="folder",
|
|
resource_id=resource_id,
|
|
operation=operation,
|
|
tenant_id=folder.tenant_id,
|
|
actor_type="user" if folder.created_by_user_id else None,
|
|
actor_id=folder.created_by_user_id,
|
|
payload={
|
|
"owner_type": folder.owner_type,
|
|
"owner_id": _owner_id(folder.owner_type, folder.owner_user_id, folder.owner_group_id),
|
|
"path": folder.path,
|
|
"previous_path": _previous_value(folder, "path"),
|
|
"deleted_at": _isoformat(folder.deleted_at),
|
|
},
|
|
)
|
|
|
|
|
|
def _record_share_visibility_change(session: OrmSession, share: FileShare) -> None:
|
|
state = inspect(share)
|
|
if not share.file_asset_id:
|
|
return
|
|
if not state.pending and not _has_attr_changes(
|
|
state,
|
|
("file_asset_id", "target_type", "target_id", "permission", "revoked_at"),
|
|
):
|
|
return
|
|
_ensure_id(share)
|
|
record_change(
|
|
session,
|
|
module_id=FILES_MODULE_ID,
|
|
collection=FILES_ASSETS_COLLECTION,
|
|
resource_type="file",
|
|
resource_id=share.file_asset_id,
|
|
operation="updated",
|
|
tenant_id=share.tenant_id,
|
|
actor_type="user" if share.created_by_user_id else None,
|
|
actor_id=share.created_by_user_id,
|
|
payload={
|
|
"share_id": share.id,
|
|
"share_target_type": share.target_type,
|
|
"share_target_id": share.target_id,
|
|
"share_permission": share.permission,
|
|
"share_revoked_at": _isoformat(share.revoked_at),
|
|
},
|
|
)
|
|
|
|
|
|
def _operation_for_soft_deletable(obj: object, *, changed_attrs: tuple[str, ...]) -> str | None:
|
|
state = inspect(obj)
|
|
if state.pending:
|
|
return "created"
|
|
if not _has_attr_changes(state, changed_attrs):
|
|
return None
|
|
if "deleted_at" in state.attrs:
|
|
history = state.attrs.deleted_at.history
|
|
if history.has_changes():
|
|
if any(value is not None for value in history.added):
|
|
return "deleted"
|
|
if any(value is not None for value in history.deleted):
|
|
return "created"
|
|
return "updated"
|
|
|
|
|
|
def _has_attr_changes(state: Any, attrs: tuple[str, ...]) -> bool:
|
|
return any(name in state.attrs and state.attrs[name].history.has_changes() for name in attrs)
|
|
|
|
|
|
def _previous_value(obj: object, attr_name: str) -> str | None:
|
|
state = inspect(obj)
|
|
if attr_name not in state.attrs:
|
|
return None
|
|
history = state.attrs[attr_name].history
|
|
if not history.has_changes() or not history.deleted:
|
|
return None
|
|
value = history.deleted[0]
|
|
return str(value) if value is not None else None
|
|
|
|
|
|
def _ensure_id(obj: object) -> str:
|
|
resource_id = getattr(obj, "id", None)
|
|
if resource_id:
|
|
return str(resource_id)
|
|
resource_id = new_uuid()
|
|
setattr(obj, "id", resource_id)
|
|
return resource_id
|
|
|
|
|
|
def _owner_id(owner_type: str, owner_user_id: str | None, owner_group_id: str | None) -> str | None:
|
|
if owner_type == "user":
|
|
return owner_user_id
|
|
if owner_type == "group":
|
|
return owner_group_id
|
|
return None
|
|
|
|
|
|
def _isoformat(value: datetime | None) -> str | None:
|
|
return value.isoformat() if value else None
|
|
|
|
|
|
__all__ = [
|
|
"FILES_ASSETS_COLLECTION",
|
|
"FILES_CONNECTOR_CREDENTIALS_COLLECTION",
|
|
"FILES_CONNECTOR_POLICIES_COLLECTION",
|
|
"FILES_CONNECTOR_PROFILES_COLLECTION",
|
|
"FILES_CONNECTOR_SPACES_COLLECTION",
|
|
"FILES_FOLDERS_COLLECTION",
|
|
"FILES_MODULE_ID",
|
|
"register_files_change_tracking",
|
|
]
|