Coverage for website/photos/tasks.py: 35.19%
48 statements
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
1import logging
2import os
4from django.db import transaction
5from django.dispatch import Signal
6from django.utils import timezone
8from celery import shared_task
9from django_drf_filepond.models import TemporaryUpload, TemporaryUploadChunked
10from django_drf_filepond.models import storage as filepond_storage
12from mailinglists.services import get_member_email_addresses
13from members.models.member import Member
14from photos.models import Album
15from utils.snippets import send_email
17from .services import extract_archive
19logger = logging.getLogger(__name__)
20album_uploaded = Signal()
23@shared_task
24def process_album_upload(
25 archive_upload_id: str, album_id: int, uploader_id: int | None = None
26):
27 upload = TemporaryUpload.objects.get(upload_id=archive_upload_id)
29 try:
30 album = Album.objects.get(id=album_id)
31 except Album.DoesNotExist:
32 logger.exception("Album %s does not exist", album_id)
33 upload.file.delete()
34 upload.delete()
36 uploader = (
37 Member.objects.filter(id=uploader_id).first()
38 if uploader_id is not None
39 else None
40 )
42 try:
43 with transaction.atomic():
44 # We make the upload atomic separately, so we can keep using the db if it fails.
45 # See https://docs.djangoproject.com/en/4.2/topics/db/transactions/#handling-exceptions-within-postgresql-transactions.
46 warnings, count = extract_archive(album, upload.file)
47 album.is_processing = False
48 album.save()
50 # Send signal to notify that an album has been uploaded. This is used
51 # by facedetection, and possibly in the future to notify the uploader.
52 album_uploaded.send(sender=None, album=album)
54 if uploader is not None:
55 # Notify uploader of the upload result.
56 send_email(
57 to=get_member_email_addresses(uploader),
58 subject=("Album upload processed completed."),
59 txt_template="photos/email/upload-processed.txt",
60 context={
61 "name": uploader.first_name,
62 "album": album,
63 "upload_name": upload.upload_name,
64 "warnings": warnings,
65 "num_processed": count,
66 },
67 )
68 except Exception as e:
69 logger.exception(f"Failed to process album upload: {e}", exc_info=e)
71 finally:
72 upload.file.delete()
73 upload.delete()
76@shared_task
77def clean_broken_uploads():
78 # Cancel and remove completed uploads that are older than 12 hours.
79 for upload in TemporaryUpload.objects.filter(
80 uploaded__lte=timezone.now() - timezone.timedelta(hours=12)
81 ):
82 logger.info(f"Removing old upload {upload.upload_id}")
83 upload.file.delete()
84 upload.delete()
86 # Cancel and remove uploads that have not received new chunks in the last hour.
87 for tuc in TemporaryUploadChunked.objects.filter(
88 last_upload_time__lt=timezone.now() - timezone.timedelta(hours=1),
89 ).exclude(upload_id__in=TemporaryUpload.objects.values("upload_id")):
90 logger.info(f"Removing incomplete chunked upload {tuc.upload_id}")
91 # Store the chunk and check if we've now completed the upload.
92 upload_file = os.path.join(
93 tuc.upload_dir, f"{tuc.file_id}_{tuc.last_chunk + 1}"
94 )
95 filepond_storage.delete(upload_file)
96 tuc.delete()