Construire une plateforme de gestion de fichiers complète — upload, authentification, analytics, sécurité fail2ban et déploiement Docker.
OpenFiler utilise SQLite avec better-sqlite3 — synchrone, sans serveur, parfait pour une app mono-utilisateur. Pas de promesses, pas d'ORM, juste du SQL direct.
Pour une app de gestion de fichiers personnelle, SQLite est le bon choix : zéro configuration, fichier unique, performances excellentes en lecture. Si le projet évolue vers du multi-utilisateur, migrer vers PostgreSQL se fait en changeant l'adaptateur Better Auth et le driver dans seed.ts.
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { seed } = await import('./lib/seed')
seed()
}
}Next.js exécute instrumentation.ts une seule fois au démarrage du serveur. La condition NEXT_RUNTIME === 'nodejs' évite l'exécution dans l'edge runtime.
import Database from 'better-sqlite3'
const db = new Database(process.env.DATABASE_URL!)
export function seed() {
db.exec(`
CREATE TABLE IF NOT EXISTS file_metadata (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
original_name TEXT NOT NULL,
folder TEXT NOT NULL,
size INTEGER NOT NULL,
mime_type TEXT NOT NULL,
is_public INTEGER DEFAULT 0,
uploaded_at TEXT DEFAULT (datetime('now')),
uploaded_by TEXT
);
CREATE TABLE IF NOT EXISTS trash (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
original_name TEXT NOT NULL,
folder TEXT NOT NULL,
size INTEGER NOT NULL,
deleted_at TEXT DEFAULT (datetime('now')),
deleted_by TEXT
);
CREATE TABLE IF NOT EXISTS share_link (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT UNIQUE NOT NULL,
filename TEXT NOT NULL,
folder TEXT NOT NULL,
expires_at TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS file_view (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
folder TEXT NOT NULL,
ip TEXT,
user_agent TEXT,
referer TEXT,
viewed_at TEXT DEFAULT (datetime('now')),
type TEXT DEFAULT 'view'
);
CREATE TABLE IF NOT EXISTS login_attempt (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip TEXT NOT NULL,
email TEXT,
success INTEGER DEFAULT 0,
attempted_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS banned_ip (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip TEXT UNIQUE NOT NULL,
reason TEXT,
banned_at TEXT DEFAULT (datetime('now')),
expires_at TEXT
);
`)
// Créer l'admin par défaut si il n'existe pas
const existing = db.prepare('SELECT id FROM user WHERE email = ?').get('admin@openfiler.local')
if (!existing) {
// Better Auth gère le hash — on passe par son API
createDefaultAdmin()
}
}CREATE TABLE IF NOT EXISTS garantit l'idempotence : on peut relancer le seed sans erreur.
better-sqlite3 est synchrone par design — pas de await, pas de callbacks.
// Lecture
const files = db.prepare('SELECT * FROM file_metadata WHERE folder = ?').all(folder)
// Écriture
const result = db.prepare(
'INSERT INTO file_metadata (filename, original_name, folder, size, mime_type) VALUES (?, ?, ?, ?, ?)'
).run(filename, originalName, folder, size, mimeType)
// Transaction
const transfer = db.transaction((fileId: number) => {
const file = db.prepare('SELECT * FROM file_metadata WHERE id = ?').get(fileId)
db.prepare('INSERT INTO trash ...').run(...)
db.prepare('DELETE FROM file_metadata WHERE id = ?').run(fileId)
})
transfer(42)Les transactions sont atomiques — si une étape échoue, tout est annulé.