✨(backend) implement recording expiration mechanism
Add expiration system for recordings. Include option for users to set recordings as permanent (no expiration) which is the default behavior. System only calculates expiration dates and tracks status - actual deletion is handled by Minio bucket lifecycle policies, not by application code.
This commit is contained in:
committed by
aleb_the_flash
parent
af21478143
commit
1a0051a90b
@@ -40,6 +40,7 @@ def get_frontend_configuration(request):
|
||||
"recording": {
|
||||
"is_enabled": settings.RECORDING_ENABLE,
|
||||
"available_modes": settings.RECORDING_WORKER_CLASSES.keys(),
|
||||
"expiration_days": settings.RECORDING_EXPIRATION_DAYS,
|
||||
},
|
||||
}
|
||||
frontend_configuration.update(settings.FRONTEND_CONFIGURATION)
|
||||
|
||||
@@ -159,7 +159,17 @@ class RecordingSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.Recording
|
||||
fields = ["id", "room", "created_at", "updated_at", "status", "mode", "key"]
|
||||
fields = [
|
||||
"id",
|
||||
"room",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"status",
|
||||
"mode",
|
||||
"key",
|
||||
"is_expired",
|
||||
"expired_at",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ Declare and configure the models for the Meet core application
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from logging import getLogger
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import models as auth_models
|
||||
@@ -12,6 +13,7 @@ from django.contrib.auth.base_user import AbstractBaseUser
|
||||
from django.core import mail, validators
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.text import capfirst, slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -601,6 +603,34 @@ class Recording(BaseModel):
|
||||
|
||||
return f"{settings.RECORDING_OUTPUT_FOLDER}/{self.id}.{self.extension}"
|
||||
|
||||
@property
|
||||
def expired_at(self) -> Optional[datetime]:
|
||||
"""
|
||||
Calculate the expiration date based on created_at and RECORDING_EXPIRATION_DAYS.
|
||||
Returns None if no expiration is configured.
|
||||
|
||||
Note: This is a naive and imperfect implementation since recordings are actually
|
||||
saved to the bucket after created_at timestamp is set. The actual expiration
|
||||
will be determined by the bucket lifecycle policy which operates on the object's
|
||||
timestamp in the storage system, not this value.
|
||||
"""
|
||||
|
||||
if not settings.RECORDING_EXPIRATION_DAYS:
|
||||
return None
|
||||
|
||||
return self.created_at + timedelta(days=settings.RECORDING_EXPIRATION_DAYS)
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
"""
|
||||
Determine if the recording has expired by comparing expired_at with current UTC time.
|
||||
Returns False if no expiration is configured or if expiration date is in the future.
|
||||
"""
|
||||
if not self.expired_at:
|
||||
return False
|
||||
|
||||
return self.expired_at < timezone.now()
|
||||
|
||||
|
||||
class RecordingAccess(BaseAccess):
|
||||
"""Relation model to give access to a recording for a user or a team with a role."""
|
||||
|
||||
@@ -47,11 +47,12 @@ def test_api_recordings_list_authenticated_no_recording():
|
||||
"role",
|
||||
["administrator", "member", "owner"],
|
||||
)
|
||||
def test_api_recordings_list_authenticated_direct(role):
|
||||
def test_api_recordings_list_authenticated_direct(role, settings):
|
||||
"""
|
||||
Authenticated users listing recordings, should only see the recordings
|
||||
to which they are related.
|
||||
"""
|
||||
settings.RECORDING_EXPIRATIONS_DAYS = None
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
@@ -89,6 +90,8 @@ def test_api_recordings_list_authenticated_direct(role):
|
||||
},
|
||||
"status": "initiated",
|
||||
"updated_at": recording.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"expired_at": None,
|
||||
"is_expired": False,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
Test recordings API endpoints in the Meet core app: retrieve.
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
import pytest
|
||||
from freezegun import freeze_time
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from ...factories import RecordingFactory, UserFactory, UserRecordingAccessFactory
|
||||
@@ -61,8 +64,9 @@ def test_api_recording_retrieve_members():
|
||||
}
|
||||
|
||||
|
||||
def test_api_recording_retrieve_administrators():
|
||||
def test_api_recording_retrieve_administrators(settings):
|
||||
"""A user who is an administrator of a recording should be able to retrieve it."""
|
||||
settings.RECORDING_EXPIRATION_DAYS = None
|
||||
|
||||
user = UserFactory()
|
||||
recording = RecordingFactory()
|
||||
@@ -91,11 +95,14 @@ def test_api_recording_retrieve_administrators():
|
||||
"updated_at": recording.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"status": str(recording.status),
|
||||
"mode": str(recording.mode),
|
||||
"expired_at": None,
|
||||
"is_expired": False,
|
||||
}
|
||||
|
||||
|
||||
def test_api_recording_retrieve_owners():
|
||||
def test_api_recording_retrieve_owners(settings):
|
||||
"""A user who is an owner of a recording should be able to retrieve it."""
|
||||
settings.RECORDING_EXPIRATION_DAYS = None
|
||||
user = UserFactory()
|
||||
recording = RecordingFactory()
|
||||
|
||||
@@ -123,6 +130,87 @@ def test_api_recording_retrieve_owners():
|
||||
"updated_at": recording.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"status": str(recording.status),
|
||||
"mode": str(recording.mode),
|
||||
"expired_at": None,
|
||||
"is_expired": False,
|
||||
}
|
||||
|
||||
|
||||
@freeze_time("2023-01-15 12:00:00")
|
||||
def test_api_recording_retrieve_compute_expiration_date_correctly(settings):
|
||||
"""Test that the API returns the correct expiration date for a non-expired recording."""
|
||||
settings.RECORDING_EXPIRATION_DAYS = 1
|
||||
|
||||
user = UserFactory()
|
||||
recording = RecordingFactory()
|
||||
|
||||
UserRecordingAccessFactory(
|
||||
recording=recording, user=user, role=random.choice(["administrator", "owner"])
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"/api/v1.0/recordings/{recording.id!s}/")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
room = recording.room
|
||||
|
||||
assert content == {
|
||||
"id": str(recording.id),
|
||||
"key": recording.key,
|
||||
"room": {
|
||||
"access_level": str(room.access_level),
|
||||
"id": str(room.id),
|
||||
"name": room.name,
|
||||
"slug": room.slug,
|
||||
},
|
||||
"created_at": "2023-01-15T12:00:00Z",
|
||||
"updated_at": "2023-01-15T12:00:00Z",
|
||||
"status": str(recording.status),
|
||||
"mode": str(recording.mode),
|
||||
"expired_at": "2023-01-16T12:00:00Z",
|
||||
"is_expired": False, # Ensure the recording is still valid and hasn't expired
|
||||
}
|
||||
|
||||
|
||||
def test_api_recording_retrieve_expired(settings):
|
||||
"""Test that the API returns the correct expiration date and flag for an expired recording."""
|
||||
settings.RECORDING_EXPIRATION_DAYS = 2
|
||||
|
||||
user = UserFactory()
|
||||
|
||||
with freeze_time("2023-01-15 12:00:00"):
|
||||
recording = RecordingFactory()
|
||||
|
||||
UserRecordingAccessFactory(
|
||||
recording=recording, user=user, role=random.choice(["administrator", "owner"])
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"/api/v1.0/recordings/{recording.id!s}/")
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.json()
|
||||
room = recording.room
|
||||
|
||||
assert content == {
|
||||
"id": str(recording.id),
|
||||
"key": recording.key,
|
||||
"room": {
|
||||
"access_level": str(room.access_level),
|
||||
"id": str(room.id),
|
||||
"name": room.name,
|
||||
"slug": room.slug,
|
||||
},
|
||||
"created_at": "2023-01-15T12:00:00Z",
|
||||
"updated_at": "2023-01-15T12:00:00Z",
|
||||
"status": str(recording.status),
|
||||
"mode": str(recording.mode),
|
||||
"expired_at": "2023-01-17T12:00:00Z",
|
||||
"is_expired": True, # Ensure the recording has expired
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user