feat: expand platform APIs, sources, and test coverage
Some checks failed
CI/CD Pipeline / Run Tests (pull_request) Successful in 1m53s
CI/CD Pipeline / Telegram Notify Success (push) Has been cancelled
CI/CD Pipeline / Run Tests (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 2m54s
CI/CD Pipeline / Telegram Notify Success (pull_request) Has been skipped
Some checks failed
CI/CD Pipeline / Run Tests (pull_request) Successful in 1m53s
CI/CD Pipeline / Telegram Notify Success (push) Has been cancelled
CI/CD Pipeline / Run Tests (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 2m54s
CI/CD Pipeline / Telegram Notify Success (pull_request) Has been skipped
This commit is contained in:
@@ -1,2 +1 @@
|
||||
"""Приложение экспорта защищённых резервных архивов."""
|
||||
|
||||
|
||||
@@ -36,4 +36,3 @@ class BackupExportJobAdmin(admin.ModelAdmin):
|
||||
"updated_at",
|
||||
]
|
||||
ordering = ["-actual_date", "-created_at"]
|
||||
|
||||
|
||||
@@ -10,4 +10,3 @@ class BackupsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.backups"
|
||||
verbose_name = _("Резервные копии")
|
||||
|
||||
|
||||
@@ -92,4 +92,3 @@ class BackupExportJob(TimestampMixin, models.Model):
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Backup {self.actual_date} [{self.status}]"
|
||||
|
||||
|
||||
@@ -13,4 +13,3 @@ class BackupExportRequestSerializer(serializers.Serializer):
|
||||
"Если не передана, используется текущая дата."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import hashlib
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import uuid
|
||||
import zlib
|
||||
from collections.abc import Iterable
|
||||
from contextlib import suppress
|
||||
@@ -144,7 +145,9 @@ class BackupExportService:
|
||||
started_at__lte=actual_date,
|
||||
).filter(Q(ended_at__isnull=True) | Q(ended_at__gt=actual_date))
|
||||
|
||||
register_ids = list(active_periods.values_list("registry_id", flat=True).distinct())
|
||||
register_ids = list(
|
||||
active_periods.values_list("registry_id", flat=True).distinct()
|
||||
)
|
||||
upload_ids = list(
|
||||
active_periods.values_list("started_by_upload_id", flat=True).distinct()
|
||||
)
|
||||
@@ -155,9 +158,13 @@ class BackupExportService:
|
||||
report_ids = list(reports_qs.values_list("id", flat=True))
|
||||
|
||||
export_map: dict[type[Model], Iterable] = {
|
||||
Organization: Organization.objects.filter(id__in=active_org_ids).order_by("id"),
|
||||
Organization: Organization.objects.filter(id__in=active_org_ids).order_by(
|
||||
"id"
|
||||
),
|
||||
Register: Register.objects.filter(id__in=register_ids).order_by("name"),
|
||||
RegisterUpload: RegisterUpload.objects.filter(id__in=upload_ids).order_by("id"),
|
||||
RegisterUpload: RegisterUpload.objects.filter(id__in=upload_ids).order_by(
|
||||
"id"
|
||||
),
|
||||
RegistryMembershipPeriod: active_periods.order_by(
|
||||
"registry_id",
|
||||
"organization_id",
|
||||
@@ -216,7 +223,9 @@ class BackupExportService:
|
||||
}
|
||||
|
||||
if field.is_relation and field.related_model is not None:
|
||||
on_delete_handler = getattr(field.remote_field.on_delete, "__name__", "unknown")
|
||||
on_delete_handler = getattr(
|
||||
field.remote_field.on_delete, "__name__", "unknown"
|
||||
)
|
||||
field_meta["related_model"] = field.related_model._meta.label
|
||||
field_meta["on_delete"] = on_delete_handler
|
||||
|
||||
@@ -249,15 +258,14 @@ class BackupExportService:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _serialize_queryset(cls, *, model: type[Model], queryset: Iterable) -> list[dict]:
|
||||
def _serialize_queryset(
|
||||
cls, *, model: type[Model], queryset: Iterable
|
||||
) -> list[dict]:
|
||||
field_names = [field.attname for field in model._meta.local_fields]
|
||||
serialized = []
|
||||
for row in queryset.values(*field_names).iterator(chunk_size=1000):
|
||||
serialized.append(
|
||||
{
|
||||
key: cls._normalize_value(value)
|
||||
for key, value in row.items()
|
||||
}
|
||||
{key: cls._normalize_value(value) for key, value in row.items()}
|
||||
)
|
||||
return serialized
|
||||
|
||||
@@ -320,7 +328,9 @@ class BackupExportService:
|
||||
return decoded_key
|
||||
|
||||
@classmethod
|
||||
def _build_bin_container(cls, *, encrypted_payload: bytes, header_payload: dict) -> bytes:
|
||||
def _build_bin_container(
|
||||
cls, *, encrypted_payload: bytes, header_payload: dict
|
||||
) -> bytes:
|
||||
header = {
|
||||
"format": "mostovik-backup-bin",
|
||||
"version": cls.BIN_FORMAT_VERSION,
|
||||
@@ -375,7 +385,9 @@ class BackupExportJobService:
|
||||
requested_by_id: int | None,
|
||||
) -> BackupRequestResult:
|
||||
job = cls._get_job_for_update(actual_date)
|
||||
existing_job_result = cls._result_for_existing_job(actual_date=actual_date, job=job)
|
||||
existing_job_result = cls._result_for_existing_job(
|
||||
actual_date=actual_date, job=job
|
||||
)
|
||||
if existing_job_result is not None:
|
||||
return existing_job_result
|
||||
|
||||
@@ -408,17 +420,27 @@ class BackupExportJobService:
|
||||
status=BackupExportJob.Status.PENDING,
|
||||
)
|
||||
|
||||
from apps.backups.tasks import generate_backup_for_date
|
||||
|
||||
task = generate_backup_for_date.delay(job_id=new_job.id)
|
||||
new_job.task_id = task.id or ""
|
||||
task_id = str(uuid.uuid4())
|
||||
new_job.task_id = task_id
|
||||
new_job.save(update_fields=["task_id", "updated_at"])
|
||||
transaction.on_commit(
|
||||
lambda: cls._enqueue_backup_task(job_id=new_job.id, task_id=task_id)
|
||||
)
|
||||
|
||||
return BackupRequestResult(
|
||||
action="started",
|
||||
message="Формирование бэкапа запущено.",
|
||||
actual_date=actual_date,
|
||||
task_id=new_job.task_id,
|
||||
task_id=task_id,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _enqueue_backup_task(*, job_id: int, task_id: str) -> None:
|
||||
from apps.backups.tasks import generate_backup_for_date
|
||||
|
||||
generate_backup_for_date.apply_async(
|
||||
kwargs={"job_id": job_id},
|
||||
task_id=task_id,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -431,7 +453,10 @@ class BackupExportJobService:
|
||||
if job is None:
|
||||
return None
|
||||
|
||||
if job.status in (BackupExportJob.Status.PENDING, BackupExportJob.Status.STARTED):
|
||||
if job.status in (
|
||||
BackupExportJob.Status.PENDING,
|
||||
BackupExportJob.Status.STARTED,
|
||||
):
|
||||
return BackupRequestResult(
|
||||
action="wait",
|
||||
message="Бэкап формируется, пожалуйста подождите.",
|
||||
@@ -461,7 +486,9 @@ class BackupExportJobService:
|
||||
|
||||
if not cls._archive_exists(job):
|
||||
job.delete()
|
||||
raise BackupExportError("Файл бэкапа отсутствует, запустите формирование снова")
|
||||
raise BackupExportError(
|
||||
"Файл бэкапа отсутствует, запустите формирование снова"
|
||||
)
|
||||
|
||||
archive_path = Path(job.archive_path)
|
||||
archive_bytes = archive_path.read_bytes()
|
||||
@@ -475,7 +502,8 @@ class BackupExportJobService:
|
||||
archive_filename=archive_filename,
|
||||
bin_filename="",
|
||||
checksum_filename=job.checksum_filename,
|
||||
checksum_sha256=job.checksum_sha256 or hashlib.sha256(archive_bytes).hexdigest(),
|
||||
checksum_sha256=job.checksum_sha256
|
||||
or hashlib.sha256(archive_bytes).hexdigest(),
|
||||
organizations_count=job.organizations_count,
|
||||
actual_date=job.actual_date,
|
||||
)
|
||||
|
||||
@@ -10,4 +10,3 @@ backups_urlpatterns = [
|
||||
]
|
||||
|
||||
urlpatterns = []
|
||||
|
||||
|
||||
@@ -64,12 +64,16 @@ class BackupExportView(APIView):
|
||||
def post(self, request):
|
||||
serializer = BackupExportRequestSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
actual_date = serializer.validated_data.get("actual_date") or timezone.localdate()
|
||||
actual_date = (
|
||||
serializer.validated_data.get("actual_date") or timezone.localdate()
|
||||
)
|
||||
|
||||
try:
|
||||
result = BackupExportJobService.check_or_start_job(
|
||||
actual_date=actual_date,
|
||||
requested_by_id=request.user.id if request.user.is_authenticated else None,
|
||||
requested_by_id=request.user.id
|
||||
if request.user.is_authenticated
|
||||
else None,
|
||||
)
|
||||
except BackupExportError as exc:
|
||||
raise ValidationError({"backup": str(exc)}) from exc
|
||||
@@ -94,9 +98,9 @@ class BackupExportView(APIView):
|
||||
|
||||
response = HttpResponse(artifact.archive_bytes, content_type="application/zip")
|
||||
response.status_code = status.HTTP_200_OK
|
||||
response["Content-Disposition"] = (
|
||||
f'attachment; filename="{artifact.archive_filename}"'
|
||||
)
|
||||
response[
|
||||
"Content-Disposition"
|
||||
] = f'attachment; filename="{artifact.archive_filename}"'
|
||||
response["X-Backup-SHA256"] = artifact.checksum_sha256
|
||||
response["X-Backup-Checksum-File"] = artifact.checksum_filename
|
||||
response["X-Backup-Organizations"] = str(artifact.organizations_count)
|
||||
|
||||
Reference in New Issue
Block a user