Skip to content

Commit 2f135db

Browse files
author
waveringana
committed
migrate to hono, redis job queue
1 parent cf31d0d commit 2f135db

14 files changed

Lines changed: 1877 additions & 29 deletions

File tree

app/app-hono.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { Hono } from 'hono';
2+
import type { Env, Variables } from './types/hono';
3+
import { serve } from '@hono/node-server';
4+
import { serveStatic } from '@hono/node-server/serve-static';
5+
import { logger } from 'hono/logger';
6+
import { cors } from 'hono/cors';
7+
import { csrf } from 'hono/csrf';
8+
import session from 'express-session';
9+
import SQLiteStore from 'connect-sqlite3';
10+
import { cfg } from './config';
11+
import * as db from './lib/db-new';
12+
13+
// Import routes
14+
import { authRoutes } from './routes-hono/auth';
15+
import { apiRoutes } from './routes-hono/api';
16+
import { mediaRoutes } from './routes-hono/media';
17+
import { sseRoutes } from './routes-hono/sse';
18+
19+
// Import job queue (starts workers)
20+
import './services/JobQueue';
21+
22+
const app = new Hono<{ Bindings: Env; Variables: Variables }>();
23+
24+
const port = parseInt(process.env.EBPORT || '3000');
25+
26+
// Session middleware
27+
const sessionStore = SQLiteStore(session);
28+
const sessionMiddleware = session({
29+
secret: process.env.EBSECRET || 'pleasechangeme',
30+
resave: false,
31+
saveUninitialized: false,
32+
store: new sessionStore({
33+
db: 'sessions.db',
34+
dir: './var/db',
35+
}) as any,
36+
cookie: {
37+
secure: process.env.NODE_ENV === 'production',
38+
httpOnly: true,
39+
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
40+
},
41+
});
42+
43+
// Global middleware
44+
app.use('*', logger());
45+
46+
// Wrap express-session for Hono
47+
app.use('*', async (c, next) => {
48+
await new Promise<void>((resolve, reject) => {
49+
sessionMiddleware(c.env.incoming as any, c.env.outgoing as any, (err?: any) => {
50+
if (err) reject(err);
51+
else resolve();
52+
});
53+
});
54+
await next();
55+
});
56+
57+
// Static files
58+
app.use('/uploads/*', serveStatic({ root: './' }));
59+
app.use('/css/*', serveStatic({ root: './dist/public' }));
60+
app.use('/js/*', serveStatic({ root: './dist/public' }));
61+
app.use('/assets/*', serveStatic({ root: './dist/client' }));
62+
app.use('/favicon.ico', serveStatic({ path: './dist/public/favicon.ico' }));
63+
app.use('/site.webmanifest', serveStatic({ path: './dist/public/site.webmanifest' }));
64+
app.use('/apple-touch-icon.png', serveStatic({ path: './dist/public/apple-touch-icon.png' }));
65+
app.use('/android-chrome-192x192.png', serveStatic({ path: './dist/public/android-chrome-192x192.png' }));
66+
app.use('/android-chrome-512x512.png', serveStatic({ path: './dist/public/android-chrome-512x512.png' }));
67+
app.use('/favicon-16x16.png', serveStatic({ path: './dist/public/favicon-16x16.png' }));
68+
app.use('/favicon-32x32.png', serveStatic({ path: './dist/public/favicon-32x32.png' }));
69+
70+
// Login redirect
71+
app.get('/login', (c) => c.redirect('/auth/login'));
72+
73+
// Mount routes
74+
app.route('/api', apiRoutes);
75+
app.route('/auth', authRoutes);
76+
app.route('/', mediaRoutes);
77+
app.route('/', sseRoutes);
78+
79+
// Health check
80+
app.get('/health', (c) => {
81+
return c.json({
82+
status: 'ok',
83+
timestamp: Date.now(),
84+
processVideo: cfg.processVideo,
85+
});
86+
});
87+
88+
// Error handler
89+
app.onError((err, c) => {
90+
console.error('Error:', err);
91+
return c.json(
92+
{
93+
error: err.message || 'Internal Server Error',
94+
},
95+
500
96+
);
97+
});
98+
99+
// 404 handler
100+
app.notFound((c) => {
101+
return c.json({ error: 'Not Found' }, 404);
102+
});
103+
104+
// Graceful shutdown
105+
process.on('SIGTERM', () => {
106+
console.log('SIGTERM received, shutting down gracefully...');
107+
process.exit(0);
108+
});
109+
110+
process.on('SIGINT', () => {
111+
console.log('SIGINT received, shutting down gracefully...');
112+
process.exit(0);
113+
});
114+
115+
// Start server
116+
console.log('Process video:', cfg.processVideo);
117+
console.log(`Starting Hono server on port ${port}...`);
118+
119+
serve({
120+
fetch: app.fetch,
121+
port,
122+
});
123+
124+
console.log(`🚀 Hono server running on http://localhost:${port}`);

app/lib/db-new.ts

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import Database from 'better-sqlite3';
2+
import mkdirp from 'mkdirp';
3+
import crypto from 'crypto';
4+
5+
mkdirp.sync('./uploads');
6+
mkdirp.sync('./var/db');
7+
8+
// Create database connection (synchronous and faster)
9+
export const db = new Database('./var/db/media.db');
10+
11+
// Enable WAL mode for better concurrency
12+
db.pragma('journal_mode = WAL');
13+
14+
/**
15+
* Database Interfaces
16+
*/
17+
export interface MediaRow {
18+
id: number;
19+
path: string;
20+
expire: number | null;
21+
username: string;
22+
}
23+
24+
export interface UserRow {
25+
id: number;
26+
username: string;
27+
hashed_password: Buffer;
28+
salt: Buffer;
29+
expire: number | null;
30+
}
31+
32+
export interface SettingsRow {
33+
id: number;
34+
downscaling: boolean;
35+
namerandomization: boolean;
36+
}
37+
38+
/**
39+
* Prepared Statements (reusable and faster)
40+
*/
41+
const statements = {
42+
// Media
43+
insertMedia: db.prepare('INSERT INTO media (path, expire, username) VALUES (?, ?, ?)'),
44+
getMediaById: db.prepare('SELECT * FROM media WHERE id = ?'),
45+
getAllMedia: db.prepare('SELECT * FROM media'),
46+
getMediaByUsername: db.prepare('SELECT * FROM media WHERE username = ?'),
47+
getMediaPath: db.prepare('SELECT path FROM media WHERE id = ?'),
48+
deleteMedia: db.prepare('DELETE FROM media WHERE id = ?'),
49+
getExpiredMedia: db.prepare('SELECT * FROM media WHERE expire < ?'),
50+
51+
// Users
52+
insertUser: db.prepare('INSERT OR IGNORE INTO users (username, hashed_password, salt) VALUES (?, ?, ?)'),
53+
getUserByUsername: db.prepare('SELECT * FROM users WHERE username = ?'),
54+
getAllUsers: db.prepare('SELECT * FROM users'),
55+
deleteUser: db.prepare('DELETE FROM users WHERE id = ?'),
56+
};
57+
58+
/**
59+
* Create database schema
60+
*/
61+
export function createDatabase(version: number): void {
62+
console.log('Creating database schema');
63+
64+
db.exec(`
65+
CREATE TABLE IF NOT EXISTS users (
66+
id INTEGER PRIMARY KEY,
67+
username TEXT UNIQUE NOT NULL,
68+
hashed_password BLOB NOT NULL,
69+
expire INTEGER,
70+
salt BLOB NOT NULL
71+
)
72+
`);
73+
74+
db.exec(`
75+
CREATE TABLE IF NOT EXISTS media (
76+
id INTEGER PRIMARY KEY,
77+
path TEXT NOT NULL,
78+
expire INTEGER,
79+
username TEXT NOT NULL
80+
)
81+
`);
82+
83+
db.exec(`
84+
CREATE TABLE IF NOT EXISTS settings (
85+
id INTEGER PRIMARY KEY,
86+
downscaling BOOLEAN,
87+
namerandomization BOOLEAN
88+
)
89+
`);
90+
91+
db.pragma(`user_version = ${version}`);
92+
93+
// Create default admin user if not exists
94+
createUser('admin', process.env.EBPASS || 'changeme');
95+
}
96+
97+
/**
98+
* Update database schema
99+
*/
100+
export function updateDatabase(oldVersion: number, newVersion: number): void {
101+
console.log(`Updating database from ${oldVersion} to ${newVersion}`);
102+
103+
if (oldVersion === 1) {
104+
db.exec('ALTER TABLE media ADD COLUMN username TEXT');
105+
db.exec('ALTER TABLE users ADD COLUMN expire TEXT');
106+
db.exec(`
107+
CREATE TABLE IF NOT EXISTS settings (
108+
id INTEGER PRIMARY KEY,
109+
downscaling BOOLEAN,
110+
namerandomization BOOLEAN
111+
)
112+
`);
113+
}
114+
115+
if (oldVersion === 2) {
116+
db.exec(`
117+
CREATE TABLE IF NOT EXISTS settings (
118+
id INTEGER PRIMARY KEY,
119+
downscaling BOOLEAN,
120+
namerandomization BOOLEAN
121+
)
122+
`);
123+
}
124+
125+
db.pragma(`user_version = ${newVersion}`);
126+
}
127+
128+
/**
129+
* Check database version
130+
*/
131+
export function checkVersion(): void {
132+
const result = db.pragma('user_version', { simple: true }) as number;
133+
134+
if (result === 0) {
135+
// New database
136+
createDatabase(3);
137+
} else if (result !== 3) {
138+
// Needs update
139+
updateDatabase(result, 3);
140+
}
141+
}
142+
143+
/**
144+
* Media Operations
145+
*/
146+
export function insertMedia(filename: string, expireDate: number | null, username: string): number {
147+
try {
148+
const result = statements.insertMedia.run(filename, expireDate, username);
149+
console.log(`Uploaded ${filename} to database (ID: ${result.lastInsertRowid})`);
150+
if (expireDate === null) {
151+
console.log('It will not expire');
152+
} else {
153+
console.log(`It will expire on ${new Date(expireDate)}`);
154+
}
155+
return result.lastInsertRowid as number;
156+
} catch (error) {
157+
console.error('Error inserting media:', error);
158+
throw error;
159+
}
160+
}
161+
162+
export function getMediaById(id: number): MediaRow | undefined {
163+
return statements.getMediaById.get(id) as MediaRow | undefined;
164+
}
165+
166+
export function getAllMedia(): MediaRow[] {
167+
return statements.getAllMedia.all() as MediaRow[];
168+
}
169+
170+
export function getMediaByUsername(username: string): MediaRow[] {
171+
return statements.getMediaByUsername.all(username) as MediaRow[];
172+
}
173+
174+
export function getMediaPath(id: number): { path: string } | undefined {
175+
return statements.getMediaPath.get(id) as { path: string } | undefined;
176+
}
177+
178+
export function deleteMedia(id: number): void {
179+
statements.deleteMedia.run(id);
180+
}
181+
182+
export function getExpiredMedia(timestamp: number): MediaRow[] {
183+
return statements.getExpiredMedia.all(timestamp) as MediaRow[];
184+
}
185+
186+
/**
187+
* User Operations
188+
*/
189+
export function createUser(username: string, password: string): void {
190+
console.log(`Creating user ${username}`);
191+
const salt = crypto.randomBytes(16);
192+
const hashedPassword = crypto.pbkdf2Sync(password, salt, 310000, 32, 'sha256');
193+
194+
try {
195+
statements.insertUser.run(username, hashedPassword, salt);
196+
} catch (error) {
197+
// User already exists or other error
198+
console.log(`User ${username} already exists or error occurred`);
199+
}
200+
}
201+
202+
export function getUserByUsername(username: string): UserRow | undefined {
203+
return statements.getUserByUsername.get(username) as UserRow | undefined;
204+
}
205+
206+
export function getAllUsers(): UserRow[] {
207+
return statements.getAllUsers.all() as UserRow[];
208+
}
209+
210+
export function deleteUser(id: number): void {
211+
statements.deleteUser.run(id);
212+
}
213+
214+
/**
215+
* Generic delete (for backwards compatibility)
216+
*/
217+
export function deleteId(table: string, id: number): void {
218+
const stmt = db.prepare(`DELETE FROM ${table} WHERE id = ?`);
219+
stmt.run(id);
220+
}
221+
222+
/**
223+
* Expire old entries
224+
*/
225+
export function expireOldEntries(timestamp: number): void {
226+
const expiredMedia = getExpiredMedia(timestamp);
227+
expiredMedia.forEach((row) => {
228+
deleteMedia(row.id);
229+
});
230+
}
231+
232+
/**
233+
* Initialize database on import
234+
*/
235+
checkVersion();
236+
237+
// Graceful shutdown
238+
process.on('exit', () => {
239+
db.close();
240+
});
241+
242+
process.on('SIGINT', () => {
243+
db.close();
244+
process.exit(0);
245+
});

app/lib/ffmpeg.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export const ffmpegDownscale = (
117117
path: string,
118118
filename: string,
119119
extension: string,
120+
onProgress?: (progress: number) => void,
120121
): Promise<void> => {
121122
const startTime = Date.now();
122123
const outputOptions = [
@@ -138,8 +139,15 @@ export const ffmpegDownscale = (
138139
.outputOptions(outputOptions)
139140
.output(`uploads/720p-${filename}${extension}`)
140141
.on("progress", function (progress) {
141-
// fire-and-forget async write to avoid blocking
142-
fsp.writeFile(progressFile, JSON.stringify({ progress: (progress.percent ?? 0) / 100 }))
142+
const progressValue = (progress.percent ?? 0) / 100;
143+
144+
// Call progress callback if provided
145+
if (onProgress) {
146+
onProgress(progressValue);
147+
}
148+
149+
// Also write to file for backward compatibility
150+
fsp.writeFile(progressFile, JSON.stringify({ progress: progressValue }))
143151
.catch(() => {/* ignore */});
144152
})
145153
.on("end", () => {

0 commit comments

Comments
 (0)