iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
TypeScript · Avancé

OpenFiler

Construire une plateforme de gestion de fichiers complète — upload, authentification, analytics, sécurité fail2ban et déploiement Docker.

Next.js 15TypeScriptSQLiteBetter AuthTailwind CSS v4
GitHub
01Setup et configuration02Authentification avec Better Auth03Base de données SQLite04Système d'upload de fichiers05Dashboard et interface06Corbeille et liens de partage07Déploiement Docker
Chapitre 6·25 min

Corbeille et liens de partage

Deux fonctionnalités qui semblent simples mais demandent de la rigueur : la corbeille (suppression en deux étapes) et les liens de partage (accès public temporaire avec tracking).

Corbeille — logique de déplacement

La suppression dans OpenFiler ne supprime jamais directement. Le fichier passe d'abord en corbeille — déplacé sur le disque et en base.

lib/trash.ts
import { rename } from 'fs/promises'
import { join } from 'path'
import db from '@/lib/db'
 
export async function moveToTrash(fileId: number, userId: string) {
  const file = db.prepare('SELECT * FROM file_metadata WHERE id = ?').get(fileId) as FileMetadata
  if (!file) throw new Error('Fichier introuvable')
 
  const src = join(process.cwd(), 'upload', file.folder, file.filename)
  const dest = join(process.cwd(), 'upload', 'trash', file.filename)
 
  await rename(src, dest)
 
  const transfer = db.transaction(() => {
    db.prepare(`
      INSERT INTO trash (filename, original_name, folder, size, deleted_by)
      VALUES (?, ?, ?, ?, ?)
    `).run(file.filename, file.original_name, file.folder, file.size, userId)
 
    db.prepare('DELETE FROM file_metadata WHERE id = ?').run(fileId)
  })
  transfer()
}
 
export async function restoreFromTrash(trashId: number) {
  const item = db.prepare('SELECT * FROM trash WHERE id = ?').get(trashId) as TrashItem
  if (!item) throw new Error('Introuvable en corbeille')
 
  const src = join(process.cwd(), 'upload', 'trash', item.filename)
  const dest = join(process.cwd(), 'upload', item.folder, item.filename)
 
  await rename(src, dest)
 
  const transfer = db.transaction(() => {
    db.prepare(`
      INSERT INTO file_metadata (filename, original_name, folder, size, mime_type)
      VALUES (?, ?, ?, ?, '')
    `).run(item.filename, item.original_name, item.folder, item.size)
 
    db.prepare('DELETE FROM trash WHERE id = ?').run(trashId)
  })
  transfer()
}

La transaction garantit que si le déplacement disque réussit mais l'écriture SQL échoue, on ne perd pas de cohérence — ou plutôt, que les deux opérations réussissent ensemble.

Liens de partage — génération et accès

lib/share.ts
import { randomBytes } from 'crypto'
import db from '@/lib/db'
 
export function createShareLink(
  filename: string,
  folder: string,
  durationHours: number
): string {
  const token = randomBytes(32).toString('hex')
  const expiresAt = new Date(Date.now() + durationHours * 3600 * 1000).toISOString()
 
  db.prepare(`
    INSERT INTO share_link (token, filename, folder, expires_at)
    VALUES (?, ?, ?, ?)
  `).run(token, filename, folder, expiresAt)
 
  return token
}
 
export function resolveShareLink(token: string) {
  const link = db.prepare(`
    SELECT * FROM share_link
    WHERE token = ? AND expires_at > datetime('now')
  `).get(token) as ShareLink | undefined
 
  return link ?? null
}

Route publique de partage

app/share/[token]/page.tsx
import { resolveShareLink } from '@/lib/share'
import { notFound } from 'next/navigation'
 
export default async function SharePage({ params }: { params: { token: string } }) {
  const link = resolveShareLink(params.token)
  if (!link) notFound()
 
  // Logger l'accès dans file_view
  // Afficher le fichier ou proposer le téléchargement
 
  return (
    <main>
      <h1>{link.filename}</h1>
      <a href={`/api/download/${link.folder}/${link.filename}?token=${params.token}`}>
        Télécharger
      </a>
    </main>
  )
}

La route est publique — pas de session requise. L'expiration est vérifiée en base avec expires_at > datetime('now'). Un lien expiré retourne une page 404.

Durées disponibles

const DURATIONS = [
  { label: '1 heure',   hours: 1 },
  { label: '24 heures', hours: 24 },
  { label: '7 jours',   hours: 168 },
  { label: '30 jours',  hours: 720 },
]

Présenté comme un sélecteur dans la modal de partage — l'utilisateur choisit la durée, le token est généré immédiatement et affiché une seule fois.

Précédent
Dashboard et interface
Suivant
Déploiement Docker

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main