mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-09 15:05:26 +03:00
refactor delete and case endpoints
allow bulk deleting and reassigning
This commit is contained in:
parent
2d8962c8a9
commit
0f2c461cd2
@ -62,7 +62,4 @@ class BatchExportBody(BaseModel):
|
|||||||
if item.end_time <= item.start_time:
|
if item.end_time <= item.start_time:
|
||||||
raise ValueError("end_time must be after start_time")
|
raise ValueError("end_time must be after start_time")
|
||||||
|
|
||||||
if self.export_case_id is None and self.new_case_name is None:
|
|
||||||
raise ValueError("Either export_case_id or new_case_name must be provided")
|
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|||||||
24
frigate/api/defs/request/export_bulk_body.py
Normal file
24
frigate/api/defs/request/export_bulk_body.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"""Request bodies for bulk export operations."""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, conlist, constr
|
||||||
|
|
||||||
|
|
||||||
|
class ExportBulkDeleteBody(BaseModel):
|
||||||
|
"""Request body for bulk deleting exports."""
|
||||||
|
|
||||||
|
# List of export IDs with at least one element and each element with at least one char
|
||||||
|
ids: conlist(constr(min_length=1), min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportBulkReassignBody(BaseModel):
|
||||||
|
"""Request body for bulk reassigning exports to a case."""
|
||||||
|
|
||||||
|
# List of export IDs with at least one element and each element with at least one char
|
||||||
|
ids: conlist(constr(min_length=1), min_length=1)
|
||||||
|
export_case_id: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
max_length=30,
|
||||||
|
description="Case ID to assign to, or null to unassign from current case",
|
||||||
|
)
|
||||||
@ -23,13 +23,3 @@ class ExportCaseUpdateBody(BaseModel):
|
|||||||
description: Optional[str] = Field(
|
description: Optional[str] = Field(
|
||||||
default=None, description="Updated description of the export case"
|
default=None, description="Updated description of the export case"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExportCaseAssignBody(BaseModel):
|
|
||||||
"""Request body for assigning or unassigning an export to a case."""
|
|
||||||
|
|
||||||
export_case_id: Optional[str] = Field(
|
|
||||||
default=None,
|
|
||||||
max_length=30,
|
|
||||||
description="Case ID to assign to the export, or null to unassign",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -26,8 +26,11 @@ from frigate.api.defs.request.batch_export_body import (
|
|||||||
BatchExportBody,
|
BatchExportBody,
|
||||||
BatchExportItem,
|
BatchExportItem,
|
||||||
)
|
)
|
||||||
|
from frigate.api.defs.request.export_bulk_body import (
|
||||||
|
ExportBulkDeleteBody,
|
||||||
|
ExportBulkReassignBody,
|
||||||
|
)
|
||||||
from frigate.api.defs.request.export_case_body import (
|
from frigate.api.defs.request.export_case_body import (
|
||||||
ExportCaseAssignBody,
|
|
||||||
ExportCaseCreateBody,
|
ExportCaseCreateBody,
|
||||||
ExportCaseUpdateBody,
|
ExportCaseUpdateBody,
|
||||||
)
|
)
|
||||||
@ -424,48 +427,6 @@ def delete_export_case(case_id: str, request: Request, delete_exports: bool = Fa
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.patch(
|
|
||||||
"/export/{export_id}/case",
|
|
||||||
response_model=GenericResponse,
|
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
|
||||||
summary="Assign export to case",
|
|
||||||
description=(
|
|
||||||
"Assigns an export to a case, or unassigns it if export_case_id is null."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
async def assign_export_case(
|
|
||||||
export_id: str,
|
|
||||||
body: ExportCaseAssignBody,
|
|
||||||
request: Request,
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
export: Export = Export.get(Export.id == export_id)
|
|
||||||
await require_camera_access(export.camera, request=request)
|
|
||||||
except DoesNotExist:
|
|
||||||
return JSONResponse(
|
|
||||||
content={"success": False, "message": "Export not found."},
|
|
||||||
status_code=404,
|
|
||||||
)
|
|
||||||
|
|
||||||
if body.export_case_id is not None:
|
|
||||||
try:
|
|
||||||
ExportCase.get(ExportCase.id == body.export_case_id)
|
|
||||||
except DoesNotExist:
|
|
||||||
return JSONResponse(
|
|
||||||
content={"success": False, "message": "Export case not found."},
|
|
||||||
status_code=404,
|
|
||||||
)
|
|
||||||
export.export_case = body.export_case_id
|
|
||||||
else:
|
|
||||||
export.export_case = None
|
|
||||||
|
|
||||||
export.save()
|
|
||||||
|
|
||||||
return JSONResponse(
|
|
||||||
content={"success": True, "message": "Successfully updated export case."}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/jobs/export",
|
"/jobs/export",
|
||||||
response_model=ExportJobsResponse,
|
response_model=ExportJobsResponse,
|
||||||
@ -600,9 +561,9 @@ def export_recordings_batch(
|
|||||||
|
|
||||||
export_case = None
|
export_case = None
|
||||||
export_case_id = body.export_case_id
|
export_case_id = body.export_case_id
|
||||||
if export_case_id is None:
|
if export_case_id is None and body.new_case_name:
|
||||||
export_case = _create_export_case_record(
|
export_case = _create_export_case_record(
|
||||||
body.new_case_name or "New Case",
|
body.new_case_name,
|
||||||
body.new_case_description,
|
body.new_case_description,
|
||||||
)
|
)
|
||||||
export_case_id = export_case.id
|
export_case_id = export_case.id
|
||||||
@ -809,65 +770,6 @@ async def export_rename(event_id: str, body: ExportRenameBody, request: Request)
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
|
||||||
"/export/{event_id}",
|
|
||||||
response_model=GenericResponse,
|
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
|
||||||
summary="Delete export",
|
|
||||||
)
|
|
||||||
async def export_delete(event_id: str, request: Request):
|
|
||||||
try:
|
|
||||||
export: Export = Export.get(Export.id == event_id)
|
|
||||||
await require_camera_access(export.camera, request=request)
|
|
||||||
except DoesNotExist:
|
|
||||||
return JSONResponse(
|
|
||||||
content=(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"message": "Export not found.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
status_code=404,
|
|
||||||
)
|
|
||||||
|
|
||||||
files_in_use = []
|
|
||||||
for process in psutil.process_iter():
|
|
||||||
try:
|
|
||||||
if process.name() != "ffmpeg":
|
|
||||||
continue
|
|
||||||
file_list = process.open_files()
|
|
||||||
if file_list:
|
|
||||||
for nt in file_list:
|
|
||||||
if nt.path.startswith(EXPORT_DIR):
|
|
||||||
files_in_use.append(nt.path.split("/")[-1])
|
|
||||||
except psutil.Error:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if export.video_path.split("/")[-1] in files_in_use:
|
|
||||||
return JSONResponse(
|
|
||||||
content=(
|
|
||||||
{"success": False, "message": "Can not delete in progress export."}
|
|
||||||
),
|
|
||||||
status_code=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
Path(export.video_path).unlink(missing_ok=True)
|
|
||||||
|
|
||||||
if export.thumb_path:
|
|
||||||
Path(export.thumb_path).unlink(missing_ok=True)
|
|
||||||
|
|
||||||
export.delete_instance()
|
|
||||||
return JSONResponse(
|
|
||||||
content=(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"message": "Successfully deleted export.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
status_code=200,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/export/custom/{camera_name}/start/{start_time}/end/{end_time}",
|
"/export/custom/{camera_name}/start/{start_time}/end/{end_time}",
|
||||||
response_model=StartExportResponse,
|
response_model=StartExportResponse,
|
||||||
@ -1000,3 +902,102 @@ async def get_export(export_id: str, request: Request):
|
|||||||
content={"success": False, "message": "Export not found"},
|
content={"success": False, "message": "Export not found"},
|
||||||
status_code=404,
|
status_code=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_files_in_use() -> set[str]:
|
||||||
|
"""Get set of export filenames currently in use by ffmpeg."""
|
||||||
|
files_in_use: set[str] = set()
|
||||||
|
for process in psutil.process_iter():
|
||||||
|
try:
|
||||||
|
if process.name() != "ffmpeg":
|
||||||
|
continue
|
||||||
|
file_list = process.open_files()
|
||||||
|
if file_list:
|
||||||
|
for nt in file_list:
|
||||||
|
if nt.path.startswith(EXPORT_DIR):
|
||||||
|
files_in_use.add(nt.path.split("/")[-1])
|
||||||
|
except psutil.Error:
|
||||||
|
continue
|
||||||
|
return files_in_use
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/exports/delete",
|
||||||
|
response_model=GenericResponse,
|
||||||
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
|
summary="Bulk delete exports",
|
||||||
|
description="Deletes one or more exports by ID. All IDs must exist and none can be in-progress.",
|
||||||
|
)
|
||||||
|
def bulk_delete_exports(body: ExportBulkDeleteBody):
|
||||||
|
exports = list(Export.select().where(Export.id << body.ids))
|
||||||
|
|
||||||
|
if len(exports) != len(body.ids):
|
||||||
|
return JSONResponse(
|
||||||
|
content={"success": False, "message": "One or more exports not found."},
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
files_in_use = _get_files_in_use()
|
||||||
|
|
||||||
|
for export in exports:
|
||||||
|
if export.video_path.split("/")[-1] in files_in_use:
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"success": False,
|
||||||
|
"message": "Can not delete in-progress export.",
|
||||||
|
},
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
for export in exports:
|
||||||
|
Path(export.video_path).unlink(missing_ok=True)
|
||||||
|
if export.thumb_path:
|
||||||
|
Path(export.thumb_path).unlink(missing_ok=True)
|
||||||
|
|
||||||
|
Export.delete().where(Export.id << body.ids).execute()
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"success": True,
|
||||||
|
"message": f"Successfully deleted {len(exports)} export(s).",
|
||||||
|
},
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/exports/reassign",
|
||||||
|
response_model=GenericResponse,
|
||||||
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
|
summary="Bulk reassign exports to a case",
|
||||||
|
description="Assigns or unassigns one or more exports to/from a case. All IDs must exist.",
|
||||||
|
)
|
||||||
|
def bulk_reassign_exports(body: ExportBulkReassignBody):
|
||||||
|
exports = list(Export.select().where(Export.id << body.ids))
|
||||||
|
|
||||||
|
if len(exports) != len(body.ids):
|
||||||
|
return JSONResponse(
|
||||||
|
content={"success": False, "message": "One or more exports not found."},
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
if body.export_case_id is not None:
|
||||||
|
try:
|
||||||
|
ExportCase.get(ExportCase.id == body.export_case_id)
|
||||||
|
except DoesNotExist:
|
||||||
|
return JSONResponse(
|
||||||
|
content={"success": False, "message": "Export case not found."},
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
Export.update(export_case=body.export_case_id).where(
|
||||||
|
Export.id << body.ids
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"success": True,
|
||||||
|
"message": f"Successfully updated {len(exports)} export(s).",
|
||||||
|
},
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user