自部署多端同步:服务端、实时协作、口令登录与油画加载动画
- 新增 Node 服务端(Hono + Hocuspocus + node:sqlite),数据保存在服务器 - 基于 Yjs 的多端实时同步,本机 IndexedDB 离线缓存,撤销只针对本机操作 - 空间(标签)有独立地址,增删改通过接口同步到所有设备 - 口令登录(签名 Cookie 会话、登录限流、同源校验) - 图片上传到服务器,过大照片上传前自动压缩 - 历史版本(自动/手动保存、一键恢复)、跨空间搜索、在线设备 - 随机油画加载动画(Painting Loaders,本地收录 p5 / p5.brush) - 选中样式与拖拽性能优化:拖动结束后才同步位置 - Docker / docker-compose 部署,README 与 CLAUDE.md 更新
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
export interface ServerConfig {
|
||||
port: number
|
||||
host: string
|
||||
dataDir: string
|
||||
imagesDir: string
|
||||
databaseFile: string
|
||||
/** 构建后的前端目录;为空表示不托管静态文件(开发时由 Vite 负责) */
|
||||
staticDir: string | null
|
||||
password: string
|
||||
secret: string
|
||||
sessionDays: number
|
||||
maxImageBytes: number
|
||||
production: boolean
|
||||
}
|
||||
|
||||
export function loadConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig {
|
||||
const production = env.NODE_ENV === 'production'
|
||||
const dataDir = path.resolve(env.DATA_DIR || './data')
|
||||
const imagesDir = path.join(dataDir, 'images')
|
||||
mkdirSync(imagesDir, { recursive: true })
|
||||
|
||||
let password = env.BOUNDLESS_PASSWORD ?? ''
|
||||
if (!password) {
|
||||
if (production) {
|
||||
throw new Error('请通过环境变量 BOUNDLESS_PASSWORD 设置登录口令')
|
||||
}
|
||||
password = 'boundless'
|
||||
console.warn('[boundless] 未设置 BOUNDLESS_PASSWORD,开发模式下使用默认口令 "boundless"')
|
||||
}
|
||||
|
||||
// 默认托管构建好的 dist/;开发时页面由 Vite 提供,这里有没有都不影响
|
||||
const staticDir = path.resolve(env.STATIC_DIR || 'dist')
|
||||
|
||||
return {
|
||||
port: Number(env.PORT) || 3000,
|
||||
host: env.HOST || '0.0.0.0',
|
||||
dataDir,
|
||||
imagesDir,
|
||||
databaseFile: path.join(dataDir, 'boundless.sqlite'),
|
||||
staticDir: staticDir && existsSync(staticDir) ? staticDir : null,
|
||||
password,
|
||||
secret: env.BOUNDLESS_SECRET || loadOrCreateSecret(dataDir),
|
||||
sessionDays: Number(env.SESSION_DAYS) || 90,
|
||||
maxImageBytes: (Number(env.MAX_IMAGE_MB) || 20) * 1024 * 1024,
|
||||
production,
|
||||
}
|
||||
}
|
||||
|
||||
/** 未显式提供密钥时,生成一份并保存在数据目录,重启后登录状态仍然有效。 */
|
||||
function loadOrCreateSecret(dataDir: string) {
|
||||
const file = path.join(dataDir, 'session.secret')
|
||||
if (existsSync(file)) {
|
||||
const saved = readFileSync(file, 'utf8').trim()
|
||||
if (saved) return saved
|
||||
}
|
||||
const secret = randomBytes(32).toString('base64url')
|
||||
writeFileSync(file, secret, { mode: 0o600 })
|
||||
return secret
|
||||
}
|
||||
Reference in New Issue
Block a user