ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: очистка хранилища и проектные квоты
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user