Footer: - Left is now 'Backend Storage:' with a capacity bar (blue, peach at 80%, red at 95% per DESIGN.md) and a used/total/free tooltip; the watched folder path is no longer printed. /api/status/ returns a compact storage summary instead of the path (the full storage page still shows paths to authenticated users). - Centre shows the backend API origin (empty = same origin). Staff get a link to /setup to point the browser elsewhere; everyone else sees it as plain text. The Account 'Backend connection' card is gone — this is installation plumbing, not a per-user setting. - Design spec updated to match. Staff role: - The custom role did nothing on several endpoints that only accepted Django's is_staff/is_superuser. One canonical check now exists: User.is_app_staff (superuser, Django staff, or the staff role), used by the stats/users APIs, item object permissions, can_delete, upload/ similarity/download/match querysets, and the management commands (which also pick staff-role accounts for e621 sync/match and file ownership). Verified with a role-only staff account (is_staff/is_superuser false): stats/users 200, all 32 downloads + 2 scans visible, others' items editable; the same account as role=user gets 403 for all of those.
76 lines
2.8 KiB
Python
76 lines
2.8 KiB
Python
from django.contrib.auth import get_user_model
|
|
from django.core.management.base import BaseCommand, CommandError
|
|
from django.db.models import Q
|
|
|
|
from apps.library import e621, matching
|
|
from apps.library.models import MatchTask, MediaItem
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = (
|
|
"Match library items against e621 by MD5 and refresh matched metadata "
|
|
"(also detects deleted posts). Uses a user's stored e621 credentials."
|
|
)
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument(
|
|
"--user",
|
|
help="Username whose e621 credentials to use "
|
|
"(defaults to the first staff account with credentials).",
|
|
)
|
|
parser.add_argument(
|
|
"--scope",
|
|
choices=[MatchTask.SCOPE_MISSING, MatchTask.SCOPE_ALL],
|
|
default=MatchTask.SCOPE_MISSING,
|
|
help="'missing' checks unchecked items only; 'all' also refreshes "
|
|
"already matched items (default: missing).",
|
|
)
|
|
|
|
def handle(self, *args, **options):
|
|
User = get_user_model()
|
|
username = options.get("user")
|
|
if username:
|
|
user = User.objects.filter(username=username).first()
|
|
if user is None:
|
|
raise CommandError(f"No such user: {username}")
|
|
else:
|
|
user = (
|
|
User.objects.filter(
|
|
Q(is_superuser=True)
|
|
| Q(is_staff=True)
|
|
| Q(role=User.ROLE_STAFF),
|
|
e621_api_key__gt="",
|
|
e621_username__gt="",
|
|
)
|
|
.order_by("id")
|
|
.first()
|
|
)
|
|
if not e621.credentials_configured(user):
|
|
raise CommandError(
|
|
"No e621 credentials found. Pass --user or configure them in Account."
|
|
)
|
|
|
|
queryset = matching.scope_queryset(options["scope"])
|
|
total = queryset.count()
|
|
counters = {"matched": 0, "not_found": 0, "deleted": 0}
|
|
for index, item_id in enumerate(queryset.values_list("id", flat=True), start=1):
|
|
item = MediaItem.objects.filter(id=item_id).first()
|
|
if item is None:
|
|
continue
|
|
try:
|
|
status = matching.check_item(item, user)
|
|
except e621.E621Error as exc:
|
|
raise CommandError(f"Aborted after {index - 1}/{total}: {exc}")
|
|
counters[status] = counters.get(status, 0) + 1
|
|
self.stdout.write(
|
|
f"[{index}/{total}] J-{item_id}: {status}", ending="\r"
|
|
)
|
|
|
|
self.stdout.write(
|
|
self.style.SUCCESS(
|
|
"\nDone: "
|
|
f"{counters['matched']} matched, {counters['not_found']} not found, "
|
|
f"{counters['deleted']} deleted (scope: {options['scope']})."
|
|
)
|
|
)
|