1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-05 05:05:17 +02:00

Refactor image cleanup command to use LocationImage model and update import/export view to include backup and restore functionality

This commit is contained in:
Sean Morley 2025-06-25 12:55:00 -04:00
parent 15eece8882
commit 37b4ea179c
5 changed files with 236 additions and 108 deletions

View file

@ -1,7 +1,7 @@
import os import os
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.conf import settings from django.conf import settings
from adventures.models import AdventureImage, Attachment from adventures.models import LocationImage, Attachment
from users.models import CustomUser from users.models import CustomUser
@ -21,8 +21,8 @@ class Command(BaseCommand):
# Get all image and attachment file paths from database # Get all image and attachment file paths from database
used_files = set() used_files = set()
# Get AdventureImage file paths # Get LocationImage file paths
for img in AdventureImage.objects.all(): for img in LocationImage.objects.all():
if img.image and img.image.name: if img.image and img.image.name:
used_files.add(os.path.join(settings.MEDIA_ROOT, img.image.name)) used_files.add(os.path.join(settings.MEDIA_ROOT, img.image.name))

View file

@ -13,6 +13,7 @@ from rest_framework import viewsets, status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.parsers import MultiPartParser from rest_framework.parsers import MultiPartParser
from rest_framework.permissions import IsAuthenticated
from adventures.models import ( from adventures.models import (
Location, Collection, Transportation, Note, Checklist, ChecklistItem, Location, Collection, Transportation, Note, Checklist, ChecklistItem,
@ -23,6 +24,7 @@ from worldtravel.models import VisitedCity, VisitedRegion, City, Region, Country
User = get_user_model() User = get_user_model()
class BackupViewSet(viewsets.ViewSet): class BackupViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
""" """
Simple ViewSet for handling backup and import operations Simple ViewSet for handling backup and import operations
""" """

View file

@ -1,91 +0,0 @@
<script lang="ts">
import { addToast } from '$lib/toasts';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
let modal: HTMLDialogElement;
let url: string = '';
export let name: string | null = null;
let error = '';
onMount(() => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) {
modal.showModal();
}
});
async function fetchImage() {
let res = await fetch(url);
let data = await res.blob();
if (!data) {
error = 'No image found at that URL.';
return;
}
let file = new File([data], 'image.jpg', { type: 'image/jpeg' });
close();
dispatch('image', { file });
}
async function fetchWikiImage() {
let res = await fetch(`/api/generate/img/?name=${name}`);
let data = await res.json();
if (data.source) {
let imageUrl = data.source;
let res = await fetch(imageUrl);
let blob = await res.blob();
let file = new File([blob], `${name}.jpg`, { type: 'image/jpeg' });
close();
dispatch('image', { file });
} else {
error = 'No image found for that Wikipedia article.';
}
}
function close() {
dispatch('close');
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
dispatch('close');
}
}
</script>
<dialog id="my_modal_1" class="modal">
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-lg">Image Fetcher with URL</h3>
<form>
<input
type="text"
class="input input-bordered w-full max-w-xs"
bind:value={url}
placeholder="Enter a URL"
/>
<button class="btn btn-primary" on:click={fetchImage}>Submit</button>
</form>
<h3 class="font-bold text-lg">Image Fetcher from Wikipedia</h3>
<form>
<input
type="text"
class="input input-bordered w-full max-w-xs"
bind:value={name}
placeholder="Enter a Wikipedia Article Name"
/>
<button class="btn btn-primary" on:click={fetchWikiImage}>Submit</button>
</form>
{#if error}
<p class="text-red-500">{error}</p>
{/if}
<button class="btn btn-primary" on:click={close}>Close</button>
</div>
</dialog>

View file

@ -262,7 +262,7 @@ export const actions: Actions = {
return { success: true }; return { success: true };
} }
}, },
changeEmail: async (event) => { restoreData: async (event) => {
if (!event.locals.user) { if (!event.locals.user) {
return redirect(302, '/'); return redirect(302, '/');
} }
@ -270,28 +270,51 @@ export const actions: Actions = {
if (!sessionId) { if (!sessionId) {
return redirect(302, '/'); return redirect(302, '/');
} }
try {
const formData = await event.request.formData(); const formData = await event.request.formData();
const new_email = formData.get('new_email') as string | null | undefined; const file = formData.get('file') as File | null | undefined;
if (!new_email) { const confirm = formData.get('confirm') as string | null | undefined;
return fail(400, { message: 'auth.email_required' });
} else { 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 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', method: 'POST',
headers: { headers: {
Referer: event.url.origin, // Include Referer header Referer: event.url.origin,
Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken 'X-CSRFToken': csrfToken
}, },
body: JSON.stringify({ body: apiFormData
new_email
})
}); });
if (!res.ok) { 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 }; return { success: true };
} catch (error) {
console.error('Restore error:', error);
return fail(500, { message: 'settings.generic_error' });
} }
} }
}; };

View file

@ -20,12 +20,13 @@
emails = data.props.emails; emails = data.props.emails;
} }
let new_password_disable_setting: boolean = false;
let new_email: string = ''; let new_email: string = '';
let public_url: string = data.props.publicUrl; let public_url: string = data.props.publicUrl;
let immichIntegration = data.props.immichIntegration; let immichIntegration = data.props.immichIntegration;
let googleMapsEnabled = data.props.googleMapsEnabled; let googleMapsEnabled = data.props.googleMapsEnabled;
let activeSection: string = 'profile'; let activeSection: string = 'profile';
let acknowledgeRestoreOverride: boolean = false;
let fileInput: HTMLInputElement;
let newImmichIntegration: ImmichIntegration = { let newImmichIntegration: ImmichIntegration = {
server_url: '', server_url: '',
@ -41,6 +42,7 @@
{ id: 'security', icon: '🔒', label: () => $t('settings.security') }, { id: 'security', icon: '🔒', label: () => $t('settings.security') },
{ id: 'emails', icon: '📧', label: () => $t('settings.emails') }, { id: 'emails', icon: '📧', label: () => $t('settings.emails') },
{ id: 'integrations', icon: '🔗', label: () => $t('settings.integrations') }, { id: 'integrations', icon: '🔗', label: () => $t('settings.integrations') },
{ id: 'import_export', icon: '📦', label: () => $t('settings.backup_restore') },
{ id: 'admin', icon: '⚙️', label: () => $t('settings.admin') }, { id: 'admin', icon: '⚙️', label: () => $t('settings.admin') },
{ id: 'advanced', icon: '🛠️', label: () => $t('settings.advanced') } { id: 'advanced', icon: '🛠️', label: () => $t('settings.advanced') }
]; ];
@ -924,6 +926,198 @@
</div> </div>
{/if} {/if}
<!-- import export -->
{#if activeSection === 'import_export'}
<div class="bg-base-100 rounded-2xl shadow-xl p-8">
<div class="flex items-center gap-4 mb-6">
<div class="p-3 bg-accent/10 rounded-xl">
<span class="text-2xl">📦</span>
</div>
<div>
<div>
<h2 class="text-2xl font-bold">Backup & Restore</h2>
<p class="text-base-content/70">
Save your data or restore it from a previous backup file.
</p>
</div>
</div>
</div>
<!-- Backup Coverage -->
<div class="bg-base-200 rounded-xl p-4 mb-6">
<h4 class="text-sm font-semibold mb-3 text-base-content/70">What's Included</h4>
<div class="grid grid-cols-2 gap-4 text-sm">
<!-- Backed Up -->
<div class="space-y-2">
<div class="flex items-center justify-between">
<span>📍 Locations</span>
<span></span>
</div>
<div class="flex items-center justify-between">
<span>🚶 Visits</span>
<span></span>
</div>
<div class="flex items-center justify-between">
<span>📚 Collections</span>
<span></span>
</div>
<div class="flex items-center justify-between">
<span>🖼️ Media</span>
<span></span>
</div>
<div class="flex items-center justify-between">
<span>🌍 World Travel Visits</span>
<span></span>
</div>
</div>
<!-- Not Backed Up -->
<div class="space-y-2">
<div class="flex items-center justify-between">
<span>⚙️ Settings</span>
<span></span>
</div>
<div class="flex items-center justify-between">
<span>👤 Profile</span>
<span></span>
</div>
<div class="flex items-center justify-between">
<span>🔗 Integrations</span>
<span></span>
</div>
<div class="flex items-center justify-between opacity-30">
<span></span>
<span></span>
</div>
</div>
</div>
</div>
<div class="space-y-6">
<!-- Backup Data -->
<div class="p-6 bg-base-200 rounded-xl">
<h3 class="text-lg font-semibold mb-4">📤 Backup Your Data</h3>
<p class="text-base-content/70 mb-4">
Download a complete backup of your account data including locations,
collections, media, and visits.
</p>
<div class="flex gap-4">
<a class="btn btn-primary" href="/api/backup/export"> 💾 Download Backup </a>
</div>
</div>
<!-- Restore Data -->
<div class="p-6 bg-base-200 rounded-xl">
<h3 class="text-lg font-semibold mb-4">📥 Restore Data</h3>
<p class="text-base-content/70 mb-4">
Upload a backup file to restore your data.
</p>
<!-- Warning Alert -->
<div class="alert alert-warning mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
<div>
<h4 class="font-bold">⚠️ Data Override Warning</h4>
<p class="text-sm">
Restoring data will completely replace all existing data in your account.
This action cannot be undone.
</p>
</div>
</div>
<!-- File Upload Form -->
<form
method="post"
action="?/restoreData"
use:enhance
enctype="multipart/form-data"
class="space-y-4"
>
<div class="form-control">
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="label">
<span class="label-text font-medium">Select backup file</span>
</label>
<input
type="file"
name="file"
class="file-input file-input-bordered file-input-primary w-full"
accept=".zip"
required
/>
<div class="label">
<span class="label-text-alt">Supported formats: .zip</span>
</div>
</div>
<!-- Acknowledgment Checkbox -->
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
name="confirm"
value="yes"
class="checkbox checkbox-warning"
required
bind:checked={acknowledgeRestoreOverride}
/>
<div>
<span class="label-text font-medium text-warning"
>I acknowledge that this will override all my existing data</span
>
<p class="text-xs text-base-content/60 mt-1">
This action is irreversible and will replace all locations, collections,
and visits in your account.
</p>
</div>
</label>
</div>
{#if $page.form?.message && $page.form?.message.includes('restore')}
<div class="alert alert-error">
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{$t($page.form?.message)}</span>
</div>
{/if}
<div class="flex gap-4">
<button
type="submit"
class="btn btn-warning"
disabled={!acknowledgeRestoreOverride}
>
🚀 Restore Data
</button>
</div>
</form>
</div>
</div>
</div>
{/if}
<!-- Admin Section --> <!-- Admin Section -->
{#if activeSection === 'admin' && user.is_staff} {#if activeSection === 'admin' && user.is_staff}
<div class="bg-base-100 rounded-2xl shadow-xl p-8"> <div class="bg-base-100 rounded-2xl shadow-xl p-8">