feat(settings): add compact database card to General section
Adds get_database_info and vacuum_database Tauri commands. The General section now shows current DB size and reclaimable space on load, with a Compact now button that runs PRAGMA wal_checkpoint(FULL) + VACUUM and reports how many MB were freed. Button disables automatically when the database is already compact (< 0.5 MB reclaimable).
This commit is contained in:
@@ -1696,3 +1696,54 @@ pub async fn open_app_data_folder(app: AppHandle) -> Result<(), String> {
|
|||||||
.open_path(app_dir.to_string_lossy().as_ref(), None::<&str>)
|
.open_path(app_dir.to_string_lossy().as_ref(), None::<&str>)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Database maintenance
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct DatabaseInfo {
|
||||||
|
pub size_mb: f64,
|
||||||
|
pub reclaimable_mb: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct VacuumResult {
|
||||||
|
pub before_mb: f64,
|
||||||
|
pub after_mb: f64,
|
||||||
|
pub freed_mb: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_database_info(app: AppHandle, db: State<'_, DbState>) -> Result<DatabaseInfo, String> {
|
||||||
|
let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||||
|
let db_path = app_dir.join("gallery.db");
|
||||||
|
let size_bytes = std::fs::metadata(&db_path).map(|m| m.len()).unwrap_or(0);
|
||||||
|
|
||||||
|
let conn = db.get().map_err(|e| e.to_string())?;
|
||||||
|
let page_size: i64 = conn.query_row("PRAGMA page_size", [], |r| r.get(0)).map_err(|e| e.to_string())?;
|
||||||
|
let freelist_count: i64 = conn.query_row("PRAGMA freelist_count", [], |r| r.get(0)).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(DatabaseInfo {
|
||||||
|
size_mb: size_bytes as f64 / 1_048_576.0,
|
||||||
|
reclaimable_mb: (freelist_count * page_size) as f64 / 1_048_576.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn vacuum_database(app: AppHandle, db: State<'_, DbState>) -> Result<VacuumResult, String> {
|
||||||
|
let app_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||||
|
let db_path = app_dir.join("gallery.db");
|
||||||
|
let before_bytes = std::fs::metadata(&db_path).map(|m| m.len()).unwrap_or(0);
|
||||||
|
|
||||||
|
let conn = db.get().map_err(|e| e.to_string())?;
|
||||||
|
conn.execute_batch("PRAGMA wal_checkpoint(FULL); VACUUM;").map_err(|e| e.to_string())?;
|
||||||
|
drop(conn);
|
||||||
|
|
||||||
|
let after_bytes = std::fs::metadata(&db_path).map(|m| m.len()).unwrap_or(0);
|
||||||
|
Ok(VacuumResult {
|
||||||
|
before_mb: before_bytes as f64 / 1_048_576.0,
|
||||||
|
after_mb: after_bytes as f64 / 1_048_576.0,
|
||||||
|
freed_mb: before_bytes.saturating_sub(after_bytes) as f64 / 1_048_576.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ pub fn run() {
|
|||||||
commands::get_tagging_queue_folder_ids,
|
commands::get_tagging_queue_folder_ids,
|
||||||
commands::set_tagging_queue_folder_ids,
|
commands::set_tagging_queue_folder_ids,
|
||||||
commands::open_app_data_folder,
|
commands::open_app_data_folder,
|
||||||
|
commands::get_database_info,
|
||||||
|
commands::vacuum_database,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { TaggerAcceleration, TaggingQueueScope, useGalleryStore } from "../store";
|
import { DatabaseInfo, TaggerAcceleration, TaggingQueueScope, VacuumResult, useGalleryStore } from "../store";
|
||||||
|
|
||||||
type SettingsSection = "workspace" | "general";
|
type SettingsSection = "workspace" | "general";
|
||||||
|
|
||||||
@@ -117,6 +117,9 @@ export function SettingsModal() {
|
|||||||
const [taggerBatchSizeSaving, setTaggerBatchSizeSaving] = useState(false);
|
const [taggerBatchSizeSaving, setTaggerBatchSizeSaving] = useState(false);
|
||||||
const [taggerBatchSizeError, setTaggerBatchSizeError] = useState<string | null>(null);
|
const [taggerBatchSizeError, setTaggerBatchSizeError] = useState<string | null>(null);
|
||||||
const [openingDataFolder, setOpeningDataFolder] = useState(false);
|
const [openingDataFolder, setOpeningDataFolder] = useState(false);
|
||||||
|
const [dbInfo, setDbInfo] = useState<DatabaseInfo | null>(null);
|
||||||
|
const [vacuuming, setVacuuming] = useState(false);
|
||||||
|
const [vacuumResult, setVacuumResult] = useState<VacuumResult | null>(null);
|
||||||
|
|
||||||
const thresholdErrorTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const thresholdErrorTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const batchSizeErrorTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const batchSizeErrorTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
@@ -156,6 +159,8 @@ export function SettingsModal() {
|
|||||||
const clearTaggingJobs = useGalleryStore((state) => state.clearTaggingJobs);
|
const clearTaggingJobs = useGalleryStore((state) => state.clearTaggingJobs);
|
||||||
const clearTaggingJobsForFolders = useGalleryStore((state) => state.clearTaggingJobsForFolders);
|
const clearTaggingJobsForFolders = useGalleryStore((state) => state.clearTaggingJobsForFolders);
|
||||||
const openAppDataFolder = useGalleryStore((state) => state.openAppDataFolder);
|
const openAppDataFolder = useGalleryStore((state) => state.openAppDataFolder);
|
||||||
|
const getDatabaseInfo = useGalleryStore((state) => state.getDatabaseInfo);
|
||||||
|
const vacuumDatabase = useGalleryStore((state) => state.vacuumDatabase);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!settingsOpen) return;
|
if (!settingsOpen) return;
|
||||||
@@ -173,6 +178,12 @@ export function SettingsModal() {
|
|||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [settingsOpen, loadTaggerModelStatus, loadTaggerAcceleration, loadTaggerThreshold, loadTaggerBatchSize, loadTaggingQueueScope, loadTaggingQueueFolderIds, setSettingsOpen]);
|
}, [settingsOpen, loadTaggerModelStatus, loadTaggerAcceleration, loadTaggerThreshold, loadTaggerBatchSize, loadTaggingQueueScope, loadTaggingQueueFolderIds, setSettingsOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!settingsOpen || activeSection !== "general") return;
|
||||||
|
setVacuumResult(null);
|
||||||
|
void getDatabaseInfo().then(setDbInfo).catch(() => {});
|
||||||
|
}, [settingsOpen, activeSection, getDatabaseInfo]);
|
||||||
|
|
||||||
// Clean up error timers on unmount
|
// Clean up error timers on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -582,6 +593,62 @@ export function SettingsModal() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|
||||||
|
<SettingsCard
|
||||||
|
title="Compact database"
|
||||||
|
description="Reclaims wasted space left behind when images or tags are deleted. Safe to run at any time."
|
||||||
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="rounded-xl border border-white/[0.07] bg-black/20 p-4">
|
||||||
|
<p className="text-[11px] uppercase tracking-[0.14em] text-gray-600">Database size</p>
|
||||||
|
<p className="mt-2 text-2xl font-semibold text-white">
|
||||||
|
{vacuumResult
|
||||||
|
? `${vacuumResult.after_mb.toFixed(1)} MB`
|
||||||
|
: dbInfo
|
||||||
|
? `${dbInfo.size_mb.toFixed(1)} MB`
|
||||||
|
: "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-white/[0.07] bg-black/20 p-4">
|
||||||
|
<p className="text-[11px] uppercase tracking-[0.14em] text-gray-600">Reclaimable</p>
|
||||||
|
<p className={`mt-2 text-2xl font-semibold ${vacuumResult ? "text-emerald-300" : "text-white"}`}>
|
||||||
|
{vacuumResult
|
||||||
|
? `−${vacuumResult.freed_mb.toFixed(1)} MB freed`
|
||||||
|
: dbInfo
|
||||||
|
? `${dbInfo.reclaimable_mb.toFixed(1)} MB`
|
||||||
|
: "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-4 rounded-xl border border-white/[0.07] bg-black/20 p-4">
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
{vacuumResult
|
||||||
|
? `Compacted from ${vacuumResult.before_mb.toFixed(1)} MB to ${vacuumResult.after_mb.toFixed(1)} MB.`
|
||||||
|
: dbInfo && dbInfo.reclaimable_mb < 0.5
|
||||||
|
? "Database is already compact."
|
||||||
|
: "Run this after removing folders or bulk-deleting images."}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="shrink-0 rounded-md border border-white/10 bg-white/[0.055] px-3 py-1.5 text-xs text-gray-300 transition-colors hover:bg-white/10 hover:text-white disabled:cursor-not-allowed disabled:opacity-45"
|
||||||
|
onClick={() => {
|
||||||
|
setVacuuming(true);
|
||||||
|
setVacuumResult(null);
|
||||||
|
void vacuumDatabase()
|
||||||
|
.then((result) => {
|
||||||
|
setVacuumResult(result);
|
||||||
|
setDbInfo({ size_mb: result.after_mb, reclaimable_mb: 0 });
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => setVacuuming(false));
|
||||||
|
}}
|
||||||
|
disabled={vacuuming || (dbInfo !== null && dbInfo.reclaimable_mb < 0.5 && vacuumResult === null)}
|
||||||
|
>
|
||||||
|
{vacuuming ? "Compacting..." : "Compact now"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsCard>
|
||||||
</SectionShell>
|
</SectionShell>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -71,6 +71,17 @@ export interface ImageTag {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DatabaseInfo {
|
||||||
|
size_mb: number;
|
||||||
|
reclaimable_mb: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VacuumResult {
|
||||||
|
before_mb: number;
|
||||||
|
after_mb: number;
|
||||||
|
freed_mb: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TaggerModelStatus {
|
export interface TaggerModelStatus {
|
||||||
model_id: string;
|
model_id: string;
|
||||||
model_name: string;
|
model_name: string;
|
||||||
@@ -340,6 +351,8 @@ interface GalleryState {
|
|||||||
toggleTaggingQueueFolder: (folderId: number) => void;
|
toggleTaggingQueueFolder: (folderId: number) => void;
|
||||||
setTaggingQueueFolderIds: (folderIds: number[]) => void;
|
setTaggingQueueFolderIds: (folderIds: number[]) => void;
|
||||||
openAppDataFolder: () => Promise<void>;
|
openAppDataFolder: () => Promise<void>;
|
||||||
|
getDatabaseInfo: () => Promise<DatabaseInfo>;
|
||||||
|
vacuumDatabase: () => Promise<VacuumResult>;
|
||||||
retryFailedEmbeddings: (folderId: number) => Promise<void>;
|
retryFailedEmbeddings: (folderId: number) => Promise<void>;
|
||||||
updateImageDetails: (imageId: number, updates: { favorite?: boolean; rating?: number }) => Promise<void>;
|
updateImageDetails: (imageId: number, updates: { favorite?: boolean; rating?: number }) => Promise<void>;
|
||||||
setCacheDir: (dir: string) => void;
|
setCacheDir: (dir: string) => void;
|
||||||
@@ -1350,6 +1363,10 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
await invoke("open_app_data_folder");
|
await invoke("open_app_data_folder");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getDatabaseInfo: () => invoke<DatabaseInfo>("get_database_info"),
|
||||||
|
|
||||||
|
vacuumDatabase: () => invoke<VacuumResult>("vacuum_database"),
|
||||||
|
|
||||||
loadTaggerModelStatus: async () => {
|
loadTaggerModelStatus: async () => {
|
||||||
try {
|
try {
|
||||||
const taggerModelStatus = await invoke<TaggerModelStatus>("get_tagger_model_status");
|
const taggerModelStatus = await invoke<TaggerModelStatus>("get_tagger_model_status");
|
||||||
|
|||||||
Reference in New Issue
Block a user