Add an ephemeral similarity check page

- /similar (nav: Similar): drop a file to get the exact MD5 match, the
  perceptual matches against the library, and e621 IQDB candidates
  (auto-run for images when credentials are configured). Read-only —
  nothing enters the library.
- SimilarityCheck model + /api/similarity/ (create/list/retrieve/delete)
  with signed preview URLs and an expires_at timestamp.
- Temp files are wiped on startup (AppConfig.ready, file-only so no
  database access during initialization), lazily past
  SIMILARITY_TTL_MINUTES (default 30, env-overridable), on delete, and
  by manage.py cleanup_similarity.
- uploadFile() takes a target path; .env.example documents the TTL.
This commit is contained in:
2026-09-17 18:22:11 -05:00
parent cea34422ec
commit 1c2cb8d468
15 changed files with 875 additions and 5 deletions
+52 -1
View File
@@ -1,10 +1,19 @@
import os
from datetime import timedelta
from pathlib import Path
from django.conf import settings
from django.core import signing
from rest_framework import serializers
from .models import MediaItem, MediaLocation, TempUpload, DownloadTask, MatchTask
from .models import (
DownloadTask,
MatchTask,
MediaItem,
MediaLocation,
SimilarityCheck,
TempUpload,
)
from .services import (
MEDIA_FILE_SALT,
UPLOAD_FILE_SALT,
@@ -246,3 +255,45 @@ class MatchTaskSerializer(serializers.ModelSerializer):
"updated_at",
]
read_only_fields = fields
class SimilarityCheckSerializer(serializers.ModelSerializer):
check_id = serializers.UUIDField(source="id", read_only=True)
file_url = serializers.SerializerMethodField()
expires_at = serializers.SerializerMethodField()
class Meta:
model = SimilarityCheck
fields = [
"check_id",
"original_filename",
"md5",
"size",
"file_url",
"results",
"expires_at",
"created_at",
]
read_only_fields = fields
def _request_user(self):
request = self.context.get("request")
user = getattr(request, "user", None)
if user is None or not getattr(user, "is_authenticated", False):
return None
return user
def get_file_url(self, obj):
"""Signed URL so <img> tags can preview the temporary upload."""
user = self._request_user()
if user is None or not obj.file:
return None
signature = signing.dumps(
{"check": str(obj.id), "user": user.id},
salt=UPLOAD_FILE_SALT,
)
return f"/api/similarity/{obj.id}/file/?sig={signature}"
def get_expires_at(self, obj):
ttl = int(getattr(settings, "SIMILARITY_TTL_MINUTES", 30))
return obj.created_at + timedelta(minutes=ttl)