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
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))

View file

@ -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
"""

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 };
}
},
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, '/');
}
try {
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 {
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' });
}
}
};

View file

@ -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 @@
</div>
{/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 -->
{#if activeSection === 'admin' && user.is_staff}
<div class="bg-base-100 rounded-2xl shadow-xl p-8">