|
| 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 | +}); |
0 commit comments