Files
LingChair/server/data/FileManager.ts
2025-10-06 15:36:12 +08:00

145 lines
4.5 KiB
TypeScript

import { DatabaseSync, SQLInputValue } from "node:sqlite"
import { Buffer } from "node:buffer"
import path from 'node:path'
import crypto from 'node:crypto'
import fs from 'node:fs/promises'
import fs_sync from 'node:fs'
import chalk from "chalk"
import { fileTypeFromBuffer } from 'file-type'
import config from "../config.ts"
class FileBean {
declare count: number
declare name: string
declare hash: string
declare mime: string
declare chatid?: string
declare upload_time: number
declare last_used_time: number
}
type FileBeanKey = keyof FileBean
class File {
declare bean: FileBean
constructor(bean: FileBean) {
this.bean = bean
}
private setAttr(key: FileBeanKey, value: SQLInputValue) {
FileManager.database.prepare(`UPDATE ${FileManager.table_name} SET ${key} = ? WHERE count = ?`).run(value, this.bean.count)
this.bean[key] = value as never
}
getMime() {
return this.bean.mime
}
getName() {
return this.bean.name
}
getFilePath() {
const hash = this.bean.hash
return path.join(
config.data_path,
"uploaded_files",
hash.substring(0, 1),
hash.substring(2, 3),
hash.substring(3, 4),
this.bean.hash
)
}
getChatId() {
return this.bean.chatid
}
getUploadTime() {
return this.bean.upload_time
}
getLastUsedTime() {
return this.bean.last_used_time
}
getHash() {
return this.bean.hash
}
readSync() {
this.setAttr("last_used_time", Date.now())
return fs_sync.readFileSync(this.getFilePath())
}
}
export default class FileManager {
static FileBean = FileBean
static File = File
static table_name: string = "FileReferences"
static database: DatabaseSync = FileManager.init()
private static init(): DatabaseSync {
const db: DatabaseSync = new DatabaseSync(path.join(config.data_path, FileManager.table_name + '.db'))
db.exec(`
CREATE TABLE IF NOT EXISTS ${FileManager.table_name} (
/* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT,
/* 文件名称 */ name TEXT NOT NULL,
/* 文件哈希 */ hash TEXT NOT NULL,
/* MIME 类型 */ mime TEXT NOT NULL,
/* 来源对话 */ chatid TEXT,
/* 上传时间 */ upload_time INT8 NOT NULL,
/* 最后使用时间 */ last_used_time INT8 NOT NULL
);
`)
return db
}
static async uploadFile(fileName: string, data: Buffer, chatId?: string) {
const hash = crypto.createHash('sha256').update(data).digest('hex')
const file = FileManager.findByHash(hash)
if (file) return file
const mime = (await fileTypeFromBuffer(data))?.mime || 'application/octet-stream'
const folder = path.join(
config.data_path,
"uploaded_files",
hash.substring(0, 1),
hash.substring(2, 3),
hash.substring(3, 4)
)
await fs.mkdir(folder, { recursive: true })
await fs.writeFile(
path.join(
folder,
hash
),
data
)
return new FileManager.File(
FileManager.findAllBeansByCondition(
'count = ?',
FileManager.database.prepare(`INSERT INTO ${FileManager.table_name} (
name,
hash,
mime,
chatid,
upload_time,
last_used_time
) VALUES (?, ?, ?, ?, ?, ?);`).run(
fileName,
hash,
mime,
chatId || null,
Date.now(),
-1
).lastInsertRowid
)[0]
)
}
private static findAllBeansByCondition(condition: string, ...args: SQLInputValue[]): FileBean[] {
return FileManager.database.prepare(`SELECT * FROM ${FileManager.table_name} WHERE ${condition};`).all(...args) as unknown as FileBean[]
}
static findByHash(hash: string): File | null {
const beans = FileManager.findAllBeansByCondition('hash = ?', hash)
if (beans.length == 0)
return null
else if (beans.length > 1)
console.error(chalk.red(`警告: 查询 hash = ${hash} 时, 查询到多个相同 Hash 的文件`))
return new FileManager.File(beans[0])
}
}