From 37b4ea179cd3f81e5ae504d4c0f715ab3822a8d4 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 25 Jun 2025 12:55:00 -0400 Subject: [PATCH] Refactor image cleanup command to use LocationImage model and update import/export view to include backup and restore functionality --- .../management/commands/image_cleanup.py | 6 +- .../adventures/views/import_export_view.py | 2 + .../src/lib/components/ImageFetcher.svelte | 91 -------- frontend/src/routes/settings/+page.server.ts | 49 +++-- frontend/src/routes/settings/+page.svelte | 196 +++++++++++++++++- 5 files changed, 236 insertions(+), 108 deletions(-) delete mode 100644 frontend/src/lib/components/ImageFetcher.svelte diff --git a/backend/server/adventures/management/commands/image_cleanup.py b/backend/server/adventures/management/commands/image_cleanup.py index 0f5e1ad..c6c7f26 100644 --- a/backend/server/adventures/management/commands/image_cleanup.py +++ b/backend/server/adventures/management/commands/image_cleanup.py @@ -1,7 +1,7 @@ import os from django.core.management.base import BaseCommand from django.conf import settings -from adventures.models import AdventureImage, Attachment +from adventures.models import LocationImage, Attachment from users.models import CustomUser @@ -21,8 +21,8 @@ class Command(BaseCommand): # Get all image and attachment file paths from database used_files = set() - # Get AdventureImage file paths - for img in AdventureImage.objects.all(): + # Get LocationImage file paths + for img in LocationImage.objects.all(): if img.image and img.image.name: used_files.add(os.path.join(settings.MEDIA_ROOT, img.image.name)) diff --git a/backend/server/adventures/views/import_export_view.py b/backend/server/adventures/views/import_export_view.py index c0e3719..db4cd36 100644 --- a/backend/server/adventures/views/import_export_view.py +++ b/backend/server/adventures/views/import_export_view.py @@ -13,6 +13,7 @@ from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.parsers import MultiPartParser +from rest_framework.permissions import IsAuthenticated from adventures.models import ( Location, Collection, Transportation, Note, Checklist, ChecklistItem, @@ -23,6 +24,7 @@ from worldtravel.models import VisitedCity, VisitedRegion, City, Region, Country User = get_user_model() class BackupViewSet(viewsets.ViewSet): + permission_classes = [IsAuthenticated] """ Simple ViewSet for handling backup and import operations """ diff --git a/frontend/src/lib/components/ImageFetcher.svelte b/frontend/src/lib/components/ImageFetcher.svelte deleted file mode 100644 index 8d4e52f..0000000 --- a/frontend/src/lib/components/ImageFetcher.svelte +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - diff --git a/frontend/src/routes/settings/+page.server.ts b/frontend/src/routes/settings/+page.server.ts index 1ef6b13..32a5aa3 100644 --- a/frontend/src/routes/settings/+page.server.ts +++ b/frontend/src/routes/settings/+page.server.ts @@ -262,7 +262,7 @@ export const actions: Actions = { return { success: true }; } }, - changeEmail: async (event) => { + restoreData: async (event) => { if (!event.locals.user) { return redirect(302, '/'); } @@ -270,28 +270,51 @@ export const actions: Actions = { if (!sessionId) { return redirect(302, '/'); } - const formData = await event.request.formData(); - const new_email = formData.get('new_email') as string | null | undefined; - if (!new_email) { - return fail(400, { message: 'auth.email_required' }); - } else { + + try { + const formData = await event.request.formData(); + const file = formData.get('file') as File | null | undefined; + const confirm = formData.get('confirm') as string | null | undefined; + + if (!file || file.size === 0) { + return fail(400, { message: 'settings.no_file_selected' }); + } + + if (confirm !== 'yes') { + return fail(400, { message: 'settings.confirmation_required' }); + } + let csrfToken = await fetchCSRFToken(); - let res = await fetch(`${endpoint}/auth/change-email/`, { + + // Create FormData for the API request + const apiFormData = new FormData(); + apiFormData.append('file', file); + apiFormData.append('confirm', 'yes'); + + let res = await fetch(`${endpoint}/api/backup/import/`, { method: 'POST', headers: { - Referer: event.url.origin, // Include Referer header + Referer: event.url.origin, Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, - 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }, - body: JSON.stringify({ - new_email - }) + body: apiFormData }); + if (!res.ok) { - return fail(res.status, await res.json()); + const errorData = await res.json(); + return fail(res.status, { + message: errorData.code + ? `settings.restore_error_${errorData.code}` + : 'settings.generic_error', + details: errorData + }); } + return { success: true }; + } catch (error) { + console.error('Restore error:', error); + return fail(500, { message: 'settings.generic_error' }); } } }; diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 3300b4f..0f27986 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -20,12 +20,13 @@ emails = data.props.emails; } - let new_password_disable_setting: boolean = false; let new_email: string = ''; let public_url: string = data.props.publicUrl; let immichIntegration = data.props.immichIntegration; let googleMapsEnabled = data.props.googleMapsEnabled; let activeSection: string = 'profile'; + let acknowledgeRestoreOverride: boolean = false; + let fileInput: HTMLInputElement; let newImmichIntegration: ImmichIntegration = { server_url: '', @@ -41,6 +42,7 @@ { id: 'security', icon: '🔒', label: () => $t('settings.security') }, { id: 'emails', icon: '📧', label: () => $t('settings.emails') }, { id: 'integrations', icon: '🔗', label: () => $t('settings.integrations') }, + { id: 'import_export', icon: '📦', label: () => $t('settings.backup_restore') }, { id: 'admin', icon: '⚙️', label: () => $t('settings.admin') }, { id: 'advanced', icon: '🛠️', label: () => $t('settings.advanced') } ]; @@ -924,6 +926,198 @@ {/if} + + {#if activeSection === 'import_export'} +
+
+
+ 📦 +
+
+
+

Backup & Restore

+

+ Save your data or restore it from a previous backup file. +

+
+
+
+ + +
+

What's Included

+
+ +
+
+ 📍 Locations + +
+
+ 🚶 Visits + +
+
+ 📚 Collections + +
+
+ 🖼️ Media + +
+
+ 🌍 World Travel Visits + +
+
+ +
+
+ ⚙️ Settings + +
+
+ 👤 Profile + +
+
+ 🔗 Integrations + +
+
+ + +
+
+
+
+ +
+ +
+

📤 Backup Your Data

+

+ Download a complete backup of your account data including locations, + collections, media, and visits. +

+ +
+ + +
+

📥 Restore Data

+

+ Upload a backup file to restore your data. +

+ + +
+ + + +
+

⚠️ Data Override Warning

+

+ Restoring data will completely replace all existing data in your account. + This action cannot be undone. +

+
+
+ + +
+
+ + + +
+ Supported formats: .zip +
+
+ + +
+ +
+ + {#if $page.form?.message && $page.form?.message.includes('restore')} +
+ + + + {$t($page.form?.message)} +
+ {/if} + +
+ +
+
+
+
+
+ {/if} + {#if activeSection === 'admin' && user.is_staff}