Add per-user activity logs with storage backend tracking, gallery storage filter, and IP/token upload rate limiting. Fix storage delete modal flash, pin local backend in filter order, and link settings version to GitHub releases.
111 lines
3.0 KiB
TypeScript
111 lines
3.0 KiB
TypeScript
import { insertActivityLog, listAdminUserIds, listUserAutoDeletePolicies, type AutoDeletePolicy } from './db'
|
|
import { getGlobalAutoDeletePolicy } from './env'
|
|
import { deleteImage, type StoredImage } from './storage'
|
|
import { listImageKeysForAutoDelete } from './image-index'
|
|
import { logInfo, logWarn, serializeError } from './logger'
|
|
|
|
export interface AutoDeleteResult {
|
|
deleted: number
|
|
failed: number
|
|
skipped: boolean
|
|
days: number
|
|
}
|
|
|
|
const MS_PER_DAY = 24 * 60 * 60 * 1000
|
|
|
|
function imageAgeDays(uploadedMs: number, nowMs: number): number {
|
|
return (nowMs - uploadedMs) / MS_PER_DAY
|
|
}
|
|
|
|
function policyMatches(
|
|
uploadedMs: number,
|
|
nowMs: number,
|
|
policy: AutoDeletePolicy
|
|
): boolean {
|
|
if (policy.days <= 0) return false
|
|
|
|
if (policy.enabledAt) {
|
|
const enabledMs = new Date(policy.enabledAt).getTime()
|
|
if (!Number.isFinite(enabledMs) || uploadedMs < enabledMs) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return imageAgeDays(uploadedMs, nowMs) >= policy.days
|
|
}
|
|
|
|
export function shouldAutoDeleteImage(
|
|
image: StoredImage,
|
|
globalPolicy: AutoDeletePolicy,
|
|
adminUserIds: Set<number>,
|
|
userPolicies: Map<number, AutoDeletePolicy>,
|
|
nowMs = Date.now()
|
|
): boolean {
|
|
const uploadedMs = new Date(image.uploadedAt).getTime()
|
|
if (!Number.isFinite(uploadedMs) || uploadedMs > nowMs) {
|
|
return false
|
|
}
|
|
|
|
const ownerId = image.userId ?? null
|
|
|
|
if (ownerId != null) {
|
|
const userPolicy = userPolicies.get(ownerId)
|
|
if (userPolicy && policyMatches(uploadedMs, nowMs, userPolicy)) {
|
|
return true
|
|
}
|
|
|
|
if (adminUserIds.has(ownerId) && policyMatches(uploadedMs, nowMs, globalPolicy)) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
return policyMatches(uploadedMs, nowMs, globalPolicy)
|
|
}
|
|
|
|
export async function runAutoDeleteCleanup(): Promise<AutoDeleteResult> {
|
|
const globalPolicy = getGlobalAutoDeletePolicy()
|
|
const userPolicies = listUserAutoDeletePolicies()
|
|
const adminUserIds = listAdminUserIds()
|
|
|
|
if (globalPolicy.days <= 0 && userPolicies.size === 0) {
|
|
return { deleted: 0, failed: 0, skipped: true, days: 0 }
|
|
}
|
|
|
|
const nowMs = Date.now()
|
|
const images = await listImageKeysForAutoDelete()
|
|
let deleted = 0
|
|
let failed = 0
|
|
|
|
for (const image of images) {
|
|
if (!shouldAutoDeleteImage(image, globalPolicy, adminUserIds, userPolicies, nowMs)) {
|
|
continue
|
|
}
|
|
|
|
try {
|
|
await deleteImage(image.key)
|
|
insertActivityLog({
|
|
action: 'delete',
|
|
key: image.key,
|
|
originalName: image.originalName,
|
|
size: image.size,
|
|
contentType: image.contentType,
|
|
source: 'web',
|
|
userId: image.userId ?? null,
|
|
backendId: image.backendId ?? null
|
|
})
|
|
deleted++
|
|
} catch (error) {
|
|
logWarn('auto-delete failed', { key: image.key, error: serializeError(error) })
|
|
failed++
|
|
}
|
|
}
|
|
|
|
if (deleted > 0 || failed > 0) {
|
|
logInfo('auto-delete cleanup finished', { globalDays: globalPolicy.days, deleted, failed })
|
|
}
|
|
|
|
return { deleted, failed, skipped: false, days: globalPolicy.days }
|
|
}
|