Initial commit
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
"use server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs/promises";
|
||||
import { rawDb } from "@/lib/db/client";
|
||||
import { getAppSetting } from "@/lib/db/appSettings";
|
||||
import { safeJoin } from "@/lib/safePath";
|
||||
|
||||
const LIBRARY_ROOT = path.join(process.cwd(), "library");
|
||||
const THUMB_ROOT = path.join(process.cwd(), "data", "thumbs");
|
||||
|
||||
interface DeleteResult {
|
||||
trashed: number;
|
||||
purged: number;
|
||||
}
|
||||
|
||||
export async function bulkSetWatched(ids: number[], watched: boolean): Promise<{ updated: number }> {
|
||||
if (ids.length === 0) return { updated: 0 };
|
||||
const placeholders = ids.map(() => "?").join(",");
|
||||
const result = rawDb
|
||||
.prepare(`UPDATE images SET watched = ? WHERE id IN (${placeholders})`)
|
||||
.run(watched ? 1 : 0, ...ids);
|
||||
revalidatePath("/");
|
||||
for (const id of ids) revalidatePath(`/image/${id}`);
|
||||
return { updated: Number(result.changes) };
|
||||
}
|
||||
|
||||
export async function bulkSetOwned(ids: number[], owned: boolean): Promise<{ updated: number }> {
|
||||
if (ids.length === 0) return { updated: 0 };
|
||||
const placeholders = ids.map(() => "?").join(",");
|
||||
const result = rawDb
|
||||
.prepare(`UPDATE images SET is_owned = ? WHERE id IN (${placeholders})`)
|
||||
.run(owned ? 1 : 0, ...ids);
|
||||
revalidatePath("/");
|
||||
for (const id of ids) revalidatePath(`/image/${id}`);
|
||||
return { updated: Number(result.changes) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk mark covers as VIP / Favorite / Unmarked. VIP and Favorite are mutually
|
||||
* exclusive, so setting one clears the other; "unmark" clears both.
|
||||
*/
|
||||
export async function bulkSetMark(ids: number[], mark: "vip" | "favorite" | "unmarked"): Promise<{ updated: number }> {
|
||||
if (ids.length === 0) return { updated: 0 };
|
||||
const placeholders = ids.map(() => "?").join(",");
|
||||
const sql =
|
||||
mark === "vip" ? `UPDATE images SET is_vip = 1, is_favorite = 0 WHERE id IN (${placeholders})`
|
||||
: mark === "favorite" ? `UPDATE images SET is_favorite = 1, is_vip = 0 WHERE id IN (${placeholders})`
|
||||
: `UPDATE images SET is_vip = 0, is_favorite = 0 WHERE id IN (${placeholders})`;
|
||||
const result = rawDb.prepare(sql).run(...ids);
|
||||
revalidatePath("/");
|
||||
for (const id of ids) revalidatePath(`/image/${id}`);
|
||||
return { updated: Number(result.changes) };
|
||||
}
|
||||
|
||||
export async function deleteImages(
|
||||
ids: number[],
|
||||
options?: { permanent?: boolean },
|
||||
): Promise<DeleteResult> {
|
||||
if (ids.length === 0) return { trashed: 0, purged: 0 };
|
||||
|
||||
const useBin = getAppSetting("useRecycleBin");
|
||||
const goPermanent = options?.permanent ?? !useBin;
|
||||
|
||||
if (!goPermanent) {
|
||||
const placeholders = ids.map(() => "?").join(",");
|
||||
const now = Date.now();
|
||||
const info = rawDb.prepare(
|
||||
`UPDATE images SET deleted_at = ? WHERE id IN (${placeholders}) AND deleted_at IS NULL`,
|
||||
).run(now, ...ids);
|
||||
revalidate();
|
||||
return { trashed: Number(info.changes), purged: 0 };
|
||||
}
|
||||
|
||||
const purged = await hardDelete(ids);
|
||||
revalidate();
|
||||
// Report parents purged, not parents+children — `purged` already
|
||||
// collapses to the top-level id set the caller asked us to delete.
|
||||
return { trashed: 0, purged };
|
||||
}
|
||||
|
||||
/** Convenience for routes that delete a single image. */
|
||||
export async function deleteImage(id: number, options?: { permanent?: boolean }): Promise<DeleteResult> {
|
||||
return deleteImages([id], options);
|
||||
}
|
||||
|
||||
async function hardDelete(ids: number[]): Promise<number> {
|
||||
const placeholders = ids.map(() => "?").join(",");
|
||||
const rows = rawDb.prepare(
|
||||
`
|
||||
SELECT id, rel_path, thumb_path FROM images
|
||||
WHERE id IN (${placeholders}) OR parent_image_id IN (${placeholders})
|
||||
`,
|
||||
).all(...ids, ...ids) as Array<{ id: number; rel_path: string; thumb_path: string }>;
|
||||
|
||||
// DB delete first: if the process crashes mid-purge, files on disk become
|
||||
// orphans (cleanable via the maintenance scanner) rather than DB rows
|
||||
// pointing at vanished files. Children cascade via parent_image_id FK.
|
||||
const info = rawDb.prepare(`DELETE FROM images WHERE id IN (${placeholders})`).run(...ids);
|
||||
|
||||
if (getAppSetting("purgeFilesOnDelete")) {
|
||||
await Promise.all(rows.flatMap((r) => {
|
||||
const fileAbs = safeJoin(LIBRARY_ROOT, r.rel_path);
|
||||
const thumbAbs = safeJoin(THUMB_ROOT, r.thumb_path);
|
||||
const tasks: Array<Promise<unknown>> = [];
|
||||
if (fileAbs) tasks.push(fs.rm(fileAbs, { force: true }));
|
||||
if (thumbAbs) tasks.push(fs.rm(thumbAbs, { force: true }));
|
||||
return tasks;
|
||||
}));
|
||||
}
|
||||
return Number(info.changes);
|
||||
}
|
||||
|
||||
function revalidate() {
|
||||
revalidatePath("/");
|
||||
revalidatePath("/collection");
|
||||
revalidatePath("/tag");
|
||||
revalidatePath("/actress");
|
||||
revalidatePath("/studios");
|
||||
revalidatePath("/series");
|
||||
revalidatePath("/genres");
|
||||
}
|
||||
Reference in New Issue
Block a user