Files
FileHost/server/api/admin/export/csv.get.ts
T
2026-10-18 19:51:59 +08:00

82 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { promises as fs } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import * as tar from 'tar'
import { requireAdminAuth } from '../../../utils/access'
import { getDb } from '../../../utils/db'
import { logInfo } from '../../../utils/logger'
/** CSV 字段转义 */
function escapeCsv(value: unknown): string {
if (value === null || value === undefined) return ''
const text = String(value)
return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text
}
/** 敏感字段脱敏:密码哈希、会话令牌、API Token、存储密钥 */
function maskValue(column: string, value: unknown): unknown {
if (value === null || value === undefined) return value
const text = String(value)
if (!text) return value
if (column === 'password_hash' || column === 'token_hash' || column === 'api_token') {
return `${text.slice(0, 6)}…(${text.length})`
}
if (column === 'secret_json') return '{"hidden":true}'
return value
}
export default defineEventHandler(async (event) => {
await requireAdminAuth(event)
const db = getDb()
const workDir = join(tmpdir(), `pichost-csv-${Date.now()}`)
const outDir = join(workDir, 'csv')
const archiveName = `pichost-db-${new Date().toISOString().slice(0, 10)}.tar.gz`
await fs.mkdir(outDir, { recursive: true })
try {
const tables = db.prepare(`
SELECT name FROM sqlite_master
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
ORDER BY name
`).all() as Array<{ name: string }>
for (const table of tables) {
const rows = db.prepare(`SELECT * FROM "${table.name}"`).all() as Array<Record<string, unknown>>
const columns = rows.length
? Object.keys(rows[0]!)
: (db.prepare(`PRAGMA table_info("${table.name}")`).all() as Array<{ name: string }>)
.map(column => column.name)
const lines = [
columns.map(column => escapeCsv(column)).join(','),
...rows.map(row => columns
.map(column => escapeCsv(maskValue(column, row[column])))
.join(','))
]
// 加 BOM,Excel 打开中文不乱码
await fs.writeFile(
join(outDir, `${table.name}.csv`),
`\uFEFF${lines.join('\r\n')}`,
'utf8'
)
}
await tar.c(
{ gzip: true, file: join(workDir, archiveName), cwd: outDir, portable: true },
['.']
)
const buffer = await fs.readFile(join(workDir, archiveName))
logInfo('database csv exported', { tables: tables.length, bytes: buffer.byteLength })
setHeader(event, 'Content-Type', 'application/gzip')
setHeader(event, 'Content-Disposition', `attachment; filename="${archiveName}"`)
return buffer
} finally {
await fs.rm(workDir, { recursive: true, force: true }).catch(() => {})
}
})