ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: очистка хранилища и проектные квоты

This commit is contained in:
DCCONSTRUCTIONS
2026-04-28 13:25:37 +03:00
parent 02d79da6f9
commit 46e27a326c
15 changed files with 870 additions and 66 deletions
@@ -6,10 +6,28 @@ from urllib.parse import quote
from botocore.exceptions import ClientError
from django.http import HttpResponse, StreamingHttpResponse
from django.utils import timezone
from plane.settings.storage import S3Storage
def _mark_preview_state(asset, status, error=None):
attributes = dict(asset.attributes or {})
if status == "failed":
attributes["preview_status"] = "failed"
attributes["preview_failed_at"] = timezone.now().isoformat()
if error:
attributes["preview_error"] = str(error)
else:
attributes.pop("preview_failed_at", None)
attributes.pop("preview_error", None)
attributes["preview_status"] = "ok"
asset.attributes = attributes
asset.save(update_fields=["attributes", "updated_at"])
def attachment_object_exists(asset):
storage = S3Storage(request=None)
try:
@@ -34,7 +52,11 @@ def get_attachment_preview_response(request, asset, disposition="inline"):
try:
storage_response = storage.s3_client.get_object(**request_kwargs)
except ClientError:
if disposition == "inline" and asset.attributes.get("preview_status") == "failed":
_mark_preview_state(asset, "ok")
except ClientError as exc:
if disposition == "inline":
_mark_preview_state(asset, "failed", error=exc.response.get("Error", {}).get("Code", exc))
return HttpResponse("Attachment object not found.", status=404)
content_type = (
@@ -277,6 +277,46 @@ def release_file_asset_blob(
return FileBlobReleaseResult(had_blob=had_blob, deleted_object=deleted_object, object_key=delete_object_key)
def _delete_legacy_asset_object(asset: FileAsset, storage: S3Storage) -> bool:
object_key = _object_key(asset)
if not object_key:
return False
has_other_reference = FileAsset.all_objects.filter(asset=object_key).exclude(pk=asset.pk).exists()
if has_other_reference:
return False
return storage.delete_files(object_names=[object_key])
def hard_delete_file_asset(asset: FileAsset, request=None, storage: S3Storage | None = None) -> bool:
"""
Permanently remove a FileAsset row and release its storage reference.
Blob-backed assets release the StoredBlob ref-count. Legacy non-blob assets
are physically deleted only when no other FileAsset row still points at the
same object key.
"""
storage = storage or _server_storage(request=request)
blob_id = asset.blob_id
deleted_object = False
if blob_id:
release_result = release_file_asset_blob(asset, request=request)
deleted_object = release_result.deleted_object
StoredBlob.all_objects.filter(
pk=blob_id,
ref_count=0,
status=StoredBlob.Status.ORPHANED,
).delete()
else:
deleted_object = _delete_legacy_asset_object(asset, storage)
asset.delete(soft=False)
return deleted_object
def attach_existing_blob_to_file_asset(source: FileAsset, target: FileAsset) -> bool:
if not source.blob_id:
source.refresh_from_db(fields=["blob"])
@@ -1,4 +1,7 @@
from django.conf import settings
from django.db.models import Sum
from rest_framework import status
from rest_framework.response import Response
def get_workspace_file_size_limit(workspace):
@@ -22,3 +25,71 @@ def resolve_workspace_upload_size_limit(workspace, requested_size):
return requested_size
return min(requested_size, workspace_limit)
def get_project_storage_quota(project):
if not getattr(project, "storage_quota_enabled", False):
return None
quota = getattr(project, "storage_quota", 0) or 0
return max(int(quota), 0) or None
def get_project_storage_usage(project):
from plane.db.models import FileAsset
total = (
FileAsset.objects.filter(
workspace_id=project.workspace_id,
project_id=project.id,
is_uploaded=True,
)
.aggregate(total=Sum("size"))
.get("total")
)
return int(total or 0)
def get_project_storage_quota_state(project, requested_size=0):
quota = get_project_storage_quota(project)
used = get_project_storage_usage(project)
requested = max(int(requested_size or 0), 0)
if quota is None:
return {
"enabled": False,
"quota": 0,
"used": used,
"remaining": None,
"allowed": True,
"exceeded": False,
}
remaining = max(quota - used, 0)
return {
"enabled": True,
"quota": quota,
"used": used,
"remaining": remaining,
"allowed": used + requested <= quota,
"exceeded": used > quota,
}
def get_project_storage_quota_response(project, requested_size=0):
quota_state = get_project_storage_quota_state(project, requested_size=requested_size)
if quota_state["allowed"]:
return None
return Response(
{
"error": "Project storage quota exceeded.",
"status": False,
"code": "project_storage_quota_exceeded",
"project_id": str(project.id),
"quota": quota_state["quota"],
"used": quota_state["used"],
"remaining": quota_state["remaining"],
},
status=status.HTTP_400_BAD_REQUEST,
)