Add admin/image domain separation with host isolation middleware, optional hide-folder public URLs, GitHub release update check, and Lucky/Nginx deployment docs. Switch license to GPL-3.0-only.
673 lines
19 KiB
TypeScript
673 lines
19 KiB
TypeScript
import { DatabaseSync } from 'node:sqlite'
|
|
import { mkdirSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { getDataDir } from './data-dir'
|
|
import { logException } from './logger'
|
|
|
|
export type LogAction = 'upload' | 'delete'
|
|
export type LogSource = 'web' | 'api' | 'twikoo'
|
|
|
|
export interface ActivityLogRow {
|
|
id: number
|
|
action: LogAction
|
|
key: string
|
|
original_name: string
|
|
size: number
|
|
content_type: string
|
|
source: LogSource
|
|
user_id: number | null
|
|
username: string | null
|
|
backend_id: string | null
|
|
backend_name: string | null
|
|
backend_type: string | null
|
|
created_at: string
|
|
}
|
|
|
|
export interface ActivityLogInput {
|
|
action: LogAction
|
|
key: string
|
|
originalName: string
|
|
size: number
|
|
contentType: string
|
|
source: LogSource
|
|
userId?: number | null
|
|
backendId?: string | null
|
|
createdAt?: string
|
|
}
|
|
|
|
let db: DatabaseSync | null = null
|
|
|
|
export function getDb(): DatabaseSync {
|
|
if (db) return db
|
|
|
|
const dataDir = getDataDir()
|
|
mkdirSync(dataDir, { recursive: true })
|
|
const path = join(dataDir, 'pichost.db')
|
|
db = new DatabaseSync(path)
|
|
db.exec(`
|
|
PRAGMA journal_mode = WAL;
|
|
CREATE TABLE IF NOT EXISTS activity_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
action TEXT NOT NULL CHECK(action IN ('upload', 'delete')),
|
|
key TEXT NOT NULL,
|
|
original_name TEXT NOT NULL,
|
|
size INTEGER NOT NULL DEFAULT 0,
|
|
content_type TEXT NOT NULL DEFAULT '',
|
|
source TEXT NOT NULL CHECK(source IN ('web', 'api', 'twikoo')),
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at
|
|
ON activity_logs(created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_activity_logs_action_created
|
|
ON activity_logs(action, created_at DESC);
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
role TEXT NOT NULL CHECK(role IN ('admin', 'user')),
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id TEXT PRIMARY KEY,
|
|
user_id INTEGER NOT NULL,
|
|
token_hash TEXT NOT NULL,
|
|
expires_at TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
|
|
`)
|
|
migrateSchema(db)
|
|
return db
|
|
}
|
|
|
|
function migrateSchema(database: DatabaseSync): void {
|
|
const columns = database.prepare('PRAGMA table_info(users)').all() as Array<{
|
|
name: string
|
|
}>
|
|
if (!columns.some(column => column.name === 'api_token')) {
|
|
database.exec('ALTER TABLE users ADD COLUMN api_token TEXT')
|
|
database.exec(
|
|
'CREATE UNIQUE INDEX IF NOT EXISTS idx_users_api_token ON users(api_token) WHERE api_token IS NOT NULL'
|
|
)
|
|
}
|
|
if (!columns.some(column => column.name === 'auto_delete_days')) {
|
|
database.exec('ALTER TABLE users ADD COLUMN auto_delete_days INTEGER NOT NULL DEFAULT 0')
|
|
}
|
|
const userColumns = database.prepare('PRAGMA table_info(users)').all() as Array<{
|
|
name: string
|
|
}>
|
|
if (!userColumns.some(column => column.name === 'auto_delete_enabled_at')) {
|
|
database.exec('ALTER TABLE users ADD COLUMN auto_delete_enabled_at TEXT')
|
|
database.exec(`
|
|
UPDATE users
|
|
SET auto_delete_enabled_at = datetime('now')
|
|
WHERE auto_delete_days > 0 AND auto_delete_enabled_at IS NULL
|
|
`)
|
|
}
|
|
|
|
const daysRow = database.prepare(`
|
|
SELECT value FROM settings WHERE key = 'auto_delete_days'
|
|
`).get() as { value: string } | undefined
|
|
const enabledAtRow = database.prepare(`
|
|
SELECT value FROM settings WHERE key = 'auto_delete_enabled_at'
|
|
`).get() as { value: string } | undefined
|
|
const globalDays = daysRow ? Number(daysRow.value) : 0
|
|
if (globalDays > 0 && !enabledAtRow) {
|
|
const now = new Date().toISOString()
|
|
database.prepare(`
|
|
INSERT INTO settings (key, value, updated_at)
|
|
VALUES ('auto_delete_enabled_at', ?, ?)
|
|
`).run(now, now)
|
|
}
|
|
|
|
const logColumns = database.prepare('PRAGMA table_info(activity_logs)').all() as Array<{
|
|
name: string
|
|
}>
|
|
if (!logColumns.some(column => column.name === 'user_id')) {
|
|
database.exec('ALTER TABLE activity_logs ADD COLUMN user_id INTEGER')
|
|
database.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_activity_logs_user_created
|
|
ON activity_logs(user_id, created_at DESC)
|
|
`)
|
|
}
|
|
const activityLogColumns = database.prepare('PRAGMA table_info(activity_logs)').all() as Array<{
|
|
name: string
|
|
}>
|
|
if (!activityLogColumns.some(column => column.name === 'backend_id')) {
|
|
database.exec('ALTER TABLE activity_logs ADD COLUMN backend_id TEXT')
|
|
}
|
|
}
|
|
|
|
const USER_COLUMNS = 'id, username, password_hash, role, created_at, api_token, auto_delete_days, auto_delete_enabled_at'
|
|
|
|
export type UserRole = 'admin' | 'user'
|
|
|
|
export interface UserRow {
|
|
id: number
|
|
username: string
|
|
password_hash: string
|
|
role: UserRole
|
|
created_at: string
|
|
api_token: string | null
|
|
auto_delete_days: number
|
|
auto_delete_enabled_at: string | null
|
|
}
|
|
|
|
export interface AutoDeletePolicy {
|
|
days: number
|
|
enabledAt: string | null
|
|
}
|
|
|
|
export interface SessionRow {
|
|
id: string
|
|
user_id: number
|
|
token_hash: string
|
|
expires_at: string
|
|
created_at: string
|
|
}
|
|
|
|
export function countUsers(): number {
|
|
const row = getDb().prepare('SELECT COUNT(*) AS count FROM users').get() as {
|
|
count: number
|
|
}
|
|
return row.count
|
|
}
|
|
|
|
export function listUserIdUsernameMap(): Map<number, string> {
|
|
const rows = getDb().prepare(`
|
|
SELECT id, username
|
|
FROM users
|
|
`).all() as Array<{ id: number, username: string }>
|
|
|
|
return new Map(rows.map(row => [row.id, row.username]))
|
|
}
|
|
|
|
export function findUserById(id: number): UserRow | null {
|
|
const row = getDb().prepare(`
|
|
SELECT ${USER_COLUMNS}
|
|
FROM users
|
|
WHERE id = ?
|
|
`).get(id) as UserRow | undefined
|
|
return row ?? null
|
|
}
|
|
|
|
export function findUserByUsername(username: string): UserRow | null {
|
|
const row = getDb().prepare(`
|
|
SELECT ${USER_COLUMNS}
|
|
FROM users
|
|
WHERE username = ?
|
|
`).get(username) as UserRow | undefined
|
|
return row ?? null
|
|
}
|
|
|
|
export function findUserByApiToken(token: string): UserRow | null {
|
|
if (!token) return null
|
|
const row = getDb().prepare(`
|
|
SELECT ${USER_COLUMNS}
|
|
FROM users
|
|
WHERE api_token = ?
|
|
`).get(token) as UserRow | undefined
|
|
return row ?? null
|
|
}
|
|
|
|
export function getUserApiToken(userId: number): string | null {
|
|
const row = getDb().prepare(`
|
|
SELECT api_token
|
|
FROM users
|
|
WHERE id = ?
|
|
`).get(userId) as { api_token: string | null } | undefined
|
|
return row?.api_token ?? null
|
|
}
|
|
|
|
export function setUserApiToken(userId: number, token: string): void {
|
|
getDb().prepare(`
|
|
UPDATE users
|
|
SET api_token = ?
|
|
WHERE id = ?
|
|
`).run(token, userId)
|
|
}
|
|
|
|
export function getUserAutoDeletePolicy(userId: number): AutoDeletePolicy {
|
|
const row = getDb().prepare(`
|
|
SELECT auto_delete_days, auto_delete_enabled_at
|
|
FROM users
|
|
WHERE id = ?
|
|
`).get(userId) as { auto_delete_days: number, auto_delete_enabled_at: string | null } | undefined
|
|
|
|
return {
|
|
days: row?.auto_delete_days ?? 0,
|
|
enabledAt: row?.auto_delete_enabled_at ?? null
|
|
}
|
|
}
|
|
|
|
export function getUserAutoDeleteDays(userId: number): number {
|
|
return getUserAutoDeletePolicy(userId).days
|
|
}
|
|
|
|
export function setUserAutoDeletePolicy(userId: number, days: number): void {
|
|
const current = getUserAutoDeletePolicy(userId)
|
|
const now = new Date().toISOString()
|
|
let enabledAt = current.enabledAt
|
|
|
|
if (days > 0 && current.days === 0) {
|
|
enabledAt = now
|
|
} else if (days === 0) {
|
|
enabledAt = null
|
|
}
|
|
|
|
getDb().prepare(`
|
|
UPDATE users
|
|
SET auto_delete_days = ?, auto_delete_enabled_at = ?
|
|
WHERE id = ?
|
|
`).run(days, enabledAt, userId)
|
|
}
|
|
|
|
export function listUserAutoDeletePolicies(): Map<number, AutoDeletePolicy> {
|
|
const rows = getDb().prepare(`
|
|
SELECT id, auto_delete_days, auto_delete_enabled_at
|
|
FROM users
|
|
WHERE auto_delete_days > 0
|
|
`).all() as Array<{
|
|
id: number
|
|
auto_delete_days: number
|
|
auto_delete_enabled_at: string | null
|
|
}>
|
|
|
|
return new Map(rows.map(row => [
|
|
row.id,
|
|
{ days: row.auto_delete_days, enabledAt: row.auto_delete_enabled_at }
|
|
]))
|
|
}
|
|
|
|
export function listAdminUserIds(): Set<number> {
|
|
const rows = getDb().prepare(`
|
|
SELECT id
|
|
FROM users
|
|
WHERE role = 'admin'
|
|
`).all() as Array<{ id: number }>
|
|
|
|
return new Set(rows.map(row => row.id))
|
|
}
|
|
|
|
export function setUserAutoDeleteDays(userId: number, days: number): void {
|
|
setUserAutoDeletePolicy(userId, days)
|
|
}
|
|
|
|
export function listUserAutoDeleteDays(): Map<number, number> {
|
|
const policies = listUserAutoDeletePolicies()
|
|
return new Map([...policies.entries()].map(([id, policy]) => [id, policy.days]))
|
|
}
|
|
|
|
export function updateUserPassword(userId: number, passwordHash: string): void {
|
|
getDb().prepare(`
|
|
UPDATE users
|
|
SET password_hash = ?
|
|
WHERE id = ?
|
|
`).run(passwordHash, userId)
|
|
}
|
|
|
|
export function ensureUserApiToken(userId: number): string {
|
|
const existing = getUserApiToken(userId)
|
|
if (existing) return existing
|
|
|
|
const token = generateApiUploadToken()
|
|
setUserApiToken(userId, token)
|
|
return token
|
|
}
|
|
|
|
export function insertUser(input: {
|
|
username: string
|
|
passwordHash: string
|
|
role: UserRole
|
|
createdAt: string
|
|
}): number {
|
|
const result = getDb().prepare(`
|
|
INSERT INTO users (username, password_hash, role, created_at)
|
|
VALUES (?, ?, ?, ?)
|
|
`).run(input.username, input.passwordHash, input.role, input.createdAt)
|
|
return Number(result.lastInsertRowid)
|
|
}
|
|
|
|
export function createSessionRow(input: {
|
|
id: string
|
|
userId: number
|
|
tokenHash: string
|
|
expiresAt: string
|
|
createdAt: string
|
|
}): void {
|
|
getDb().prepare(`
|
|
INSERT INTO sessions (id, user_id, token_hash, expires_at, created_at)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`).run(
|
|
input.id,
|
|
input.userId,
|
|
input.tokenHash,
|
|
input.expiresAt,
|
|
input.createdAt
|
|
)
|
|
}
|
|
|
|
export function findSessionById(id: string): SessionRow | null {
|
|
const row = getDb().prepare(`
|
|
SELECT id, user_id, token_hash, expires_at, created_at
|
|
FROM sessions
|
|
WHERE id = ?
|
|
`).get(id) as SessionRow | undefined
|
|
return row ?? null
|
|
}
|
|
|
|
export function deleteSession(id: string): void {
|
|
getDb().prepare('DELETE FROM sessions WHERE id = ?').run(id)
|
|
}
|
|
|
|
export function deleteSessionsForUser(userId: number): void {
|
|
getDb().prepare('DELETE FROM sessions WHERE user_id = ?').run(userId)
|
|
}
|
|
|
|
export const SETTINGS_API_UPLOAD_TOKEN = 'api_upload_token'
|
|
export const SETTINGS_WEBP_QUALITY = 'webp_quality'
|
|
export const SETTINGS_ALLOWED_REFERER_HOSTS = 'allowed_referer_hosts'
|
|
export const SETTINGS_HIDE_FOLDER_IN_URL = 'hide_folder_in_url'
|
|
export const SETTINGS_SITE_BASE_URL = 'site_base_url'
|
|
export const SETTINGS_IMAGE_BASE_URL = 'image_base_url'
|
|
export const SETTINGS_AUTO_DELETE_DAYS = 'auto_delete_days'
|
|
export const SETTINGS_AUTO_DELETE_ENABLED_AT = 'auto_delete_enabled_at'
|
|
export const SETTINGS_ALLOW_REGISTRATION = 'allow_registration'
|
|
|
|
export function isAllowRegistration(): boolean {
|
|
return getSetting(SETTINGS_ALLOW_REGISTRATION) === 'true'
|
|
}
|
|
|
|
export function setAllowRegistration(allow: boolean): void {
|
|
setSetting(SETTINGS_ALLOW_REGISTRATION, allow ? 'true' : 'false')
|
|
}
|
|
|
|
export function getSetting(key: string): string | null {
|
|
const row = getDb().prepare(`
|
|
SELECT value
|
|
FROM settings
|
|
WHERE key = ?
|
|
`).get(key) as { value: string } | undefined
|
|
return row?.value ?? null
|
|
}
|
|
|
|
export function setSetting(key: string, value: string): void {
|
|
const updatedAt = new Date().toISOString()
|
|
getDb().prepare(`
|
|
INSERT INTO settings (key, value, updated_at)
|
|
VALUES (?, ?, ?)
|
|
ON CONFLICT(key) DO UPDATE SET
|
|
value = excluded.value,
|
|
updated_at = excluded.updated_at
|
|
`).run(key, value, updatedAt)
|
|
}
|
|
|
|
export function generateApiUploadToken(): string {
|
|
const bytes = new Uint8Array(32)
|
|
crypto.getRandomValues(bytes)
|
|
return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('')
|
|
}
|
|
|
|
export function insertActivityLog(input: ActivityLogInput): void {
|
|
try {
|
|
const createdAt = input.createdAt ?? new Date().toISOString()
|
|
getDb().prepare(`
|
|
INSERT INTO activity_logs
|
|
(action, key, original_name, size, content_type, source, user_id, backend_id, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run(
|
|
input.action,
|
|
input.key,
|
|
input.originalName,
|
|
input.size,
|
|
input.contentType,
|
|
input.source,
|
|
input.userId ?? null,
|
|
input.backendId ?? null,
|
|
createdAt
|
|
)
|
|
} catch (error) {
|
|
logException('activity-log insert failed', error, {
|
|
action: input.action,
|
|
key: input.key
|
|
})
|
|
}
|
|
}
|
|
|
|
export function listActivityLogs(options: {
|
|
limit: number
|
|
page?: number
|
|
action?: LogAction
|
|
source?: LogSource
|
|
folder?: string
|
|
userId?: number
|
|
search?: string
|
|
}): {
|
|
items: ActivityLogRow[]
|
|
total: number
|
|
page: number
|
|
pageSize: number
|
|
totalPages: number
|
|
} {
|
|
const database = getDb()
|
|
const pageSize = options.limit
|
|
const { whereSql, params } = buildActivityLogWhere(options)
|
|
|
|
const countRow = database.prepare(`
|
|
SELECT COUNT(*) AS count
|
|
FROM activity_logs al
|
|
${whereSql}
|
|
`).get(...params) as { count: number }
|
|
|
|
const total = countRow.count
|
|
const totalPages = Math.max(1, Math.ceil(total / pageSize) || 1)
|
|
const requestedPage = Math.max(1, options.page ?? 1)
|
|
const safePage = Math.min(requestedPage, totalPages)
|
|
const offset = (safePage - 1) * pageSize
|
|
|
|
const rows = database.prepare(`
|
|
SELECT
|
|
al.id,
|
|
al.action,
|
|
al.key,
|
|
al.original_name,
|
|
al.size,
|
|
al.content_type,
|
|
al.source,
|
|
al.user_id,
|
|
u.username,
|
|
al.backend_id,
|
|
sb.name AS backend_name,
|
|
sb.type AS backend_type,
|
|
al.created_at
|
|
FROM activity_logs al
|
|
LEFT JOIN users u ON u.id = al.user_id
|
|
LEFT JOIN storage_backends sb ON sb.id = al.backend_id
|
|
${whereSql}
|
|
ORDER BY al.id DESC
|
|
LIMIT ? OFFSET ?
|
|
`).all(...params, pageSize, offset) as unknown as ActivityLogRow[]
|
|
|
|
return {
|
|
items: rows,
|
|
total,
|
|
page: safePage,
|
|
pageSize,
|
|
totalPages
|
|
}
|
|
}
|
|
|
|
export function summarizeActivityLogs(options: {
|
|
source?: LogSource
|
|
folder?: string
|
|
userId?: number
|
|
search?: string
|
|
}): {
|
|
total: number
|
|
upload: number
|
|
delete: number
|
|
} {
|
|
const database = getDb()
|
|
const { whereSql, params } = buildActivityLogWhere(options, { includeAction: false })
|
|
const row = database.prepare(`
|
|
SELECT
|
|
COUNT(*) AS total,
|
|
COALESCE(SUM(CASE WHEN al.action = 'upload' THEN 1 ELSE 0 END), 0) AS upload_count,
|
|
COALESCE(SUM(CASE WHEN al.action = 'delete' THEN 1 ELSE 0 END), 0) AS delete_count
|
|
FROM activity_logs al
|
|
${whereSql}
|
|
`).get(...params) as { total: number, upload_count: number, delete_count: number } | undefined
|
|
|
|
return {
|
|
total: row?.total ?? 0,
|
|
upload: row?.upload_count ?? 0,
|
|
delete: row?.delete_count ?? 0
|
|
}
|
|
}
|
|
|
|
function buildActivityLogWhere(
|
|
options: {
|
|
action?: LogAction
|
|
source?: LogSource
|
|
folder?: string
|
|
userId?: number
|
|
search?: string
|
|
},
|
|
{ includeAction = true }: { includeAction?: boolean } = {}
|
|
): { whereSql: string, params: Array<string | number> } {
|
|
const params: Array<string | number> = []
|
|
const where: string[] = []
|
|
|
|
if (includeAction && options.action) {
|
|
where.push('al.action = ?')
|
|
params.push(options.action)
|
|
}
|
|
if (options.source) {
|
|
where.push('al.source = ?')
|
|
params.push(options.source)
|
|
}
|
|
if (options.folder) {
|
|
where.push('al.key LIKE ?')
|
|
params.push(`${options.folder}/%`)
|
|
}
|
|
if (options.userId !== undefined) {
|
|
where.push('al.user_id = ?')
|
|
params.push(options.userId)
|
|
}
|
|
if (options.search) {
|
|
where.push('al.original_name LIKE ?')
|
|
params.push(`%${options.search}%`)
|
|
}
|
|
|
|
return {
|
|
whereSql: where.length ? `WHERE ${where.join(' AND ')}` : '',
|
|
params
|
|
}
|
|
}
|
|
|
|
function countSince(action: LogAction, sinceIso: string): number {
|
|
const row = getDb().prepare(`
|
|
SELECT COUNT(*) AS total
|
|
FROM activity_logs
|
|
WHERE action = ? AND created_at >= ?
|
|
`).get(action, sinceIso) as unknown as { total: number } | undefined
|
|
return row?.total ?? 0
|
|
}
|
|
|
|
function sumUploadBytes(): number {
|
|
const row = getDb().prepare(`
|
|
SELECT COALESCE(SUM(size), 0) AS total
|
|
FROM activity_logs
|
|
WHERE action = 'upload'
|
|
`).get() as unknown as { total: number } | undefined
|
|
return row?.total ?? 0
|
|
}
|
|
|
|
function countByUploadFolder(): Array<{ folder: string, count: number }> {
|
|
const rows = getDb().prepare(`
|
|
SELECT
|
|
CASE
|
|
WHEN instr(key, '/') > 0 THEN substr(key, 1, instr(key, '/') - 1)
|
|
ELSE '(root)'
|
|
END AS folder,
|
|
COUNT(*) AS total
|
|
FROM activity_logs
|
|
WHERE action = 'upload'
|
|
GROUP BY folder
|
|
ORDER BY total DESC, folder ASC
|
|
`).all() as unknown as Array<{ folder: string, total: number }>
|
|
|
|
return rows.map(row => ({ folder: row.folder, count: row.total }))
|
|
}
|
|
|
|
function countBySource(): Record<LogSource, number> {
|
|
const rows = getDb().prepare(`
|
|
SELECT source, COUNT(*) AS total
|
|
FROM activity_logs
|
|
WHERE action = 'upload'
|
|
GROUP BY source
|
|
`).all() as unknown as Array<{ source: LogSource, total: number }>
|
|
|
|
const result: Record<LogSource, number> = { web: 0, api: 0, twikoo: 0 }
|
|
for (const row of rows) {
|
|
result[row.source] = row.total
|
|
}
|
|
return result
|
|
}
|
|
|
|
function startOfDayIsoInShanghai(now = new Date()): string {
|
|
// Asia/Shanghai = UTC+8,没有夏令时
|
|
const shifted = new Date(now.getTime() + 8 * 60 * 60 * 1000)
|
|
const y = shifted.getUTCFullYear()
|
|
const m = shifted.getUTCMonth()
|
|
const d = shifted.getUTCDate()
|
|
return new Date(Date.UTC(y, m, d) - 8 * 60 * 60 * 1000).toISOString()
|
|
}
|
|
|
|
function startOfMonthIsoInShanghai(now = new Date()): string {
|
|
const shifted = new Date(now.getTime() + 8 * 60 * 60 * 1000)
|
|
const y = shifted.getUTCFullYear()
|
|
const m = shifted.getUTCMonth()
|
|
return new Date(Date.UTC(y, m, 1) - 8 * 60 * 60 * 1000).toISOString()
|
|
}
|
|
|
|
export function getActivityStats(): {
|
|
uploadToday: number
|
|
uploadMonth: number
|
|
deleteToday: number
|
|
deleteMonth: number
|
|
uploadTotal: number
|
|
deleteTotal: number
|
|
uploadBytesTotal: number
|
|
bySource: Record<LogSource, number>
|
|
byFolderUploads: Array<{ folder: string, count: number }>
|
|
} {
|
|
const startOfDay = startOfDayIsoInShanghai()
|
|
const startOfMonth = startOfMonthIsoInShanghai()
|
|
|
|
const uploadTotalRow = getDb().prepare(`
|
|
SELECT COUNT(*) AS total FROM activity_logs WHERE action = 'upload'
|
|
`).get() as unknown as { total: number }
|
|
const deleteTotalRow = getDb().prepare(`
|
|
SELECT COUNT(*) AS total FROM activity_logs WHERE action = 'delete'
|
|
`).get() as unknown as { total: number }
|
|
|
|
return {
|
|
uploadToday: countSince('upload', startOfDay),
|
|
uploadMonth: countSince('upload', startOfMonth),
|
|
deleteToday: countSince('delete', startOfDay),
|
|
deleteMonth: countSince('delete', startOfMonth),
|
|
uploadTotal: uploadTotalRow.total,
|
|
deleteTotal: deleteTotalRow.total,
|
|
uploadBytesTotal: sumUploadBytes(),
|
|
bySource: countBySource(),
|
|
byFolderUploads: countByUploadFolder()
|
|
}
|
|
}
|