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:
parent
15eece8882
commit
37b4ea179c
5 changed files with 236 additions and 108 deletions
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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>
|
|
@ -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' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue