This commit is contained in:
Anaïs Saraiva
2026-08-25 12:32:54 +02:00
parent b1b4f03814
commit 1b3b45b348
12 changed files with 193 additions and 95 deletions
+40 -41
View File
@@ -1,84 +1,83 @@
# File Drop # File Drop
Un hébergeur de fichiers privé, auto-hébergé et minimal. On dépose un fichier, A minimal, self-hosted private file host. Drop a file, get a secret,
on reçoit un lien secret et intraçable. Les images, vidéos et sons s'affichent unguessable link. Images, videos and audio render directly in the browser;
directement dans le navigateur ; les autres fichiers se téléchargent. everything else downloads.
- **Backend** — Rust (Axum + Tokio) - **Backend** — Rust (Axum + Tokio)
- **Frontend** — Svelte (Vite + Tailwind) - **Frontend** — Svelte (Vite + Tailwind)
- **Stockage** — système de fichiers local, sans base de données - **Storage** — local filesystem, no database
## Démarrage rapide ## Quick start
```bash ```bash
# 1. Construire le frontend (produit frontend/dist) # 1. Build the frontend (outputs frontend/dist)
cd frontend cd frontend
npm install npm install
npm run build npm run build
cd .. cd ..
# 2. Lancer le serveur (depuis la racine du projet) # 2. Run the server (from the project root)
cargo run --release --manifest-path backend/Cargo.toml cargo run --release --manifest-path backend/Cargo.toml
``` ```
Ouvrir ensuite http://localhost:3000. Then open http://localhost:3000.
> Les chemins `STORAGE_DIR` et `STATIC_DIR` sont relatifs au dossier de > `STORAGE_DIR` and `STATIC_DIR` are relative to where you run the server.
> lancement. Lance donc le serveur depuis la racine du projet (ou règle ces > Run it from the project root, or set absolute paths in `.env`.
> variables en absolu dans `.env`).
## Configuration ## Configuration
Copier `backend/.env.example` vers `.env` puis l'adapter. Copy `backend/.env.example` to `.env` and edit it.
| Variable | Défaut | Rôle | | Variable | Default | Description |
| ---------------------- | ----------------- | ----------------------------------------- | | ---------------------- | ----------------- | ---------------------------------------------- |
| `HOST` / `PORT` | `0.0.0.0` / `3000`| Adresse d'écoute. | | `HOST` / `PORT` | `0.0.0.0` / `3000`| Bind address. |
| `STORAGE_DIR` | `./storage` | Dossier des fichiers et métadonnées. | | `STORAGE_DIR` | `./storage` | Files and metadata directory. |
| `STATIC_DIR` | `./frontend/dist` | Dossier du frontend construit. | | `STATIC_DIR` | `./frontend/dist` | Built frontend directory. |
| `MAX_FILE_SIZE` | `10GB` | Taille maximale d'envoi. | | `MAX_FILE_SIZE` | `10GB` | Maximum upload size. |
| `FILE_TTL_MS` | `172800000` | Durée de vie d'un fichier (48 h max). | | `FILE_TTL_MS` | `172800000` | File lifetime (48 h max). |
| `KEY_LENGTH` | `8` | Longueur de la clé de partage (`a-z0-9`). | | `KEY_LENGTH` | `8` | Share key length (`a-z0-9`). |
| `PUBLIC_BASE_URL` | — | URL publique pour les liens absolus. | | `PUBLIC_BASE_URL` | — | Public URL for absolute links. |
| `RATE_LIMIT_ENABLED` | `1` | Limite de requêtes par IP. | | `RATE_LIMIT_ENABLED` | `1` | Per-IP rate limiting. |
| `RATE_LIMIT_MAX` | `300` | Requêtes max par fenêtre. | | `RATE_LIMIT_MAX` | `300` | Max requests per window. |
| `RATE_LIMIT_WINDOW_MS` | `60000` | Fenêtre en millisecondes. | | `RATE_LIMIT_WINDOW_MS` | `60000` | Window in milliseconds. |
| `TRUST_PROXY` | `0` | Activer derrière un proxy (`X-Forwarded-For`). | | `TRUST_PROXY` | `0` | Enable behind a proxy (`X-Forwarded-For`). |
## API ## API
| Méthode | Route | Rôle | | Method | Route | Description |
| --------- | -------------------- | ------------------------------------------- | | -------- | ------------------ | --------------------------------------------- |
| `POST` | `/api/upload` | Envoi d'un fichier (`multipart/form-data`). | | `POST` | `/api/upload` | Upload a file (`multipart/form-data`). |
| `GET` | `/:key` | Téléchargement (inline pour les médias). | | `GET` | `/:key` | Download (inline for media). |
| `POST` | `/api/auth/:key` | Mot de passe d'un fichier protégé. | | `POST` | `/api/auth/:key` | Submit the password for a protected file. |
| `DELETE` | `/api/files/:id` | Suppression (en-tête `X-Delete-Key`). | | `DELETE` | `/api/files/:id` | Delete a file (`X-Delete-Key` header). |
| `GET` | `/api/health` | État du service. | | `GET` | `/api/health` | Service status. |
Exemple : Example:
```bash ```bash
curl -F "[email protected]" http://localhost:3000/api/upload curl -F "[email protected]" http://localhost:3000/api/upload
``` ```
Le lien renvoyé est de la forme `http://localhost:3000/<clé>.png`. Ajouter The returned link looks like `http://localhost:3000/<key>.png`. Append `?dl=1`
`?dl=1` force le téléchargement au lieu de l'affichage. to force a download instead of inline display.
## Structure ## Structure
``` ```
backend/ serveur Rust (Axum) backend/ Rust server (Axum)
frontend/ interface Svelte (Vite + Tailwind) frontend/ Svelte interface (Vite + Tailwind)
scripts/ tests scripts/ tests
storage/ données (généré au premier lancement) storage/ data (created on first run)
``` ```
## Tests ## Tests
```bash ```bash
cargo build # dans backend/ cargo build # in backend/
bash scripts/e2e-test.sh bash scripts/e2e-test.sh
``` ```
Le test couvre l'envoi, le téléchargement, l'authentification par mot de passe, The test covers upload, download, password auth, invalid keys, name
les clés invalides, la sanitisation des noms et la suppression. sanitization and deletion.
+7 -7
View File
@@ -106,19 +106,19 @@ pub async fn auth(
pub fn render_password_form(key: &str, filename: &str, error: bool) -> Response { pub fn render_password_form(key: &str, filename: &str, error: bool) -> Response {
let message = if error { let message = if error {
"<p class=\"err\">Mot de passe incorrect. Réessayez.</p>" "<p class=\"err\">Incorrect password. Please try again.</p>"
} else { } else {
"" ""
}; };
let safe_filename = escape_html(filename); let safe_filename = escape_html(filename);
let html = format!( let html = format!(
r#"<!doctype html> r#"<!doctype html>
<html lang="fr"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="referrer" content="no-referrer" /> <meta name="referrer" content="no-referrer" />
<title>Fichier protégé</title> <title>Protected file</title>
<style> <style>
* {{ box-sizing: border-box; }} * {{ box-sizing: border-box; }}
body {{ body {{
@@ -153,12 +153,12 @@ pub fn render_password_form(key: &str, filename: &str, error: bool) -> Response
<img src="/logo.svg" alt="logo" /> <img src="/logo.svg" alt="logo" />
<span>File Drop</span> <span>File Drop</span>
</div> </div>
<h1>Fichier protégé</h1> <h1>Protected file</h1>
<p>Entrez le mot de passe pour télécharger <strong>{safe_filename}</strong>.</p> <p>Enter the password to download <strong>{safe_filename}</strong>.</p>
{message} {message}
<form method="post" action="/api/auth/{key}"> <form method="post" action="/api/auth/{key}">
<input type="password" name="password" placeholder="Mot de passe" autofocus autocomplete="off" /> <input type="password" name="password" placeholder="Password" autofocus autocomplete="off" />
<button type="submit">Télécharger</button> <button type="submit">Download</button>
</form> </form>
</main> </main>
</body> </body>
+10 -1
View File
@@ -4,9 +4,18 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="referrer" content="no-referrer" /> <meta name="referrer" content="no-referrer" />
<meta name="theme-color" content="#0b0d12" /> <meta name="theme-color" content="#fafafa" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>File Drop</title> <title>File Drop</title>
<script>
(function () {
var t = localStorage.getItem('filehost.theme');
if (t !== 'dark' && t !== 'light') {
t = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
document.documentElement.classList.toggle('dark', t === 'dark');
})();
</script>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+50 -11
View File
@@ -1,11 +1,14 @@
<script> <script>
import { onMount } from 'svelte';
import Dropzone from './components/Dropzone.svelte'; import Dropzone from './components/Dropzone.svelte';
import ResultPanel from './components/ResultPanel.svelte'; import ResultPanel from './components/ResultPanel.svelte';
import FileList from './components/FileList.svelte'; import FileList from './components/FileList.svelte';
import Toast from './components/Toast.svelte'; import Toast from './components/Toast.svelte';
import { uploadFile } from './lib/api.js'; import { uploadFile } from './lib/api.js';
import { addUpload } from './lib/store.js'; import { addUpload } from './lib/store.js';
import { currentTheme, toggleTheme } from './lib/theme.js';
let theme = 'light';
let currentFile = null; let currentFile = null;
let uploading = false; let uploading = false;
let progress = 0; let progress = 0;
@@ -13,6 +16,10 @@
let password = ''; let password = '';
let toast = { message: '', kind: 'info', id: 0 }; let toast = { message: '', kind: 'info', id: 0 };
onMount(() => {
theme = currentTheme();
});
function notify(message, kind = 'info') { function notify(message, kind = 'info') {
toast = { message, kind, id: Date.now() }; toast = { message, kind, id: Date.now() };
} }
@@ -21,13 +28,17 @@
const file = e.detail; const file = e.detail;
if (!file) return; if (!file) return;
if (file.size === 0) { if (file.size === 0) {
notify('Les fichiers vides ne sont pas pris en charge.', 'error'); notify('Empty files are not supported.', 'error');
return; return;
} }
currentFile = file; currentFile = file;
notify('', 'info'); notify('', 'info');
} }
function onToggleTheme() {
theme = toggleTheme();
}
async function submit() { async function submit() {
if (!currentFile || uploading) return; if (!currentFile || uploading) return;
uploading = true; uploading = true;
@@ -49,11 +60,11 @@
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
protected: !!data.protected protected: !!data.protected
}); });
notify('Envoi terminé.', 'success'); notify('Upload complete.', 'success');
currentFile = null; currentFile = null;
password = ''; password = '';
} catch (e) { } catch (e) {
notify(e.message || 'Échec de l\'envoi.', 'error'); notify(e.message || 'Upload failed.', 'error');
} finally { } finally {
uploading = false; uploading = false;
progress = 0; progress = 0;
@@ -64,31 +75,59 @@
<div class="mx-auto max-w-6xl px-4 pb-16 sm:px-6"> <div class="mx-auto max-w-6xl px-4 pb-16 sm:px-6">
<header class="flex items-center justify-between gap-4 py-6"> <header class="flex items-center justify-between gap-4 py-6">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<img src="/logo.svg" alt="File Drop" class="h-8 w-8" /> <img src="/logo.svg" alt="File Drop" class="h-8 w-8 dark:invert" />
<div> <div>
<h1 class="text-base font-semibold leading-tight text-ink">File Drop</h1> <h1 class="text-base font-semibold leading-tight text-ink">File Drop</h1>
<p class="text-xs text-muted">Hébergement de fichiers privé</p> <p class="text-xs text-muted">Private file hosting</p>
</div> </div>
</div> </div>
<a href="/" class="btn btn-ghost px-3 py-1.5 text-xs">Nouvel envoi</a>
<div class="flex items-center gap-2">
<button
type="button"
class="btn btn-ghost p-1.5"
aria-label={theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
on:click={onToggleTheme}
>
{#if theme === 'dark'}
<svg viewBox="0 0 24 24" fill="none" class="h-4 w-4" stroke="currentColor" stroke-width="1.5">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
/>
</svg>
{:else}
<svg viewBox="0 0 24 24" fill="none" class="h-4 w-4" stroke="currentColor" stroke-width="1.5">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
/>
</svg>
{/if}
</button>
<a href="/" class="btn btn-ghost px-3 py-1.5 text-xs">New upload</a>
</div>
</header> </header>
<main class="grid grid-cols-1 items-start gap-4 lg:grid-cols-[360px_1fr]"> <main class="grid grid-cols-1 items-start gap-4 lg:grid-cols-[360px_1fr]">
<aside class="card p-5 lg:sticky lg:top-6"> <aside class="card p-5 lg:sticky lg:top-6">
<h2 class="mb-4 text-sm font-semibold text-ink">Envoyer un fichier</h2> <h2 class="mb-4 text-sm font-semibold text-ink">Upload a file</h2>
<Dropzone bind:file={currentFile} disabled={uploading} on:file={onFile} /> <Dropzone bind:file={currentFile} disabled={uploading} on:file={onFile} />
<div class="mt-4"> <div class="mt-4">
<label for="pw" class="mb-1.5 block text-xs font-medium text-muted"> <label for="pw" class="mb-1.5 block text-xs font-medium text-muted">
Mot de passe <span class="font-normal opacity-70">(optionnel)</span> Password <span class="font-normal opacity-70">(optional)</span>
</label> </label>
<input <input
id="pw" id="pw"
type="password" type="password"
class="field" class="field"
bind:value={password} bind:value={password}
placeholder="Protéger le téléchargement" placeholder="Protect the download"
autocomplete="new-password" autocomplete="new-password"
on:keydown={(e) => { on:keydown={(e) => {
if (e.key === 'Enter') submit(); if (e.key === 'Enter') submit();
@@ -99,7 +138,7 @@
{#if uploading} {#if uploading}
<div class="mt-4"> <div class="mt-4">
<div class="mb-1.5 flex items-center justify-between gap-2 text-xs text-muted"> <div class="mb-1.5 flex items-center justify-between gap-2 text-xs text-muted">
<span class="truncate">{currentFile ? currentFile.name : 'Envoi…'}</span> <span class="truncate">{currentFile ? currentFile.name : 'Uploading…'}</span>
<span class="tabular-nums">{progress}%</span> <span class="tabular-nums">{progress}%</span>
</div> </div>
<div class="h-1 w-full overflow-hidden rounded-full bg-raised"> <div class="h-1 w-full overflow-hidden rounded-full bg-raised">
@@ -114,7 +153,7 @@
disabled={!currentFile || uploading} disabled={!currentFile || uploading}
on:click={submit} on:click={submit}
> >
{uploading ? 'Envoi…' : 'Envoyer'} {uploading ? 'Uploading…' : 'Upload'}
</button> </button>
<div class="mt-4"> <div class="mt-4">
+32 -2
View File
@@ -2,6 +2,32 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
:root {
--c-base: 250 250 250;
--c-surface: 255 255 255;
--c-raised: 245 245 245;
--c-edge: 229 229 229;
--c-ink: 23 23 23;
--c-muted: 115 115 115;
--c-accent: 23 23 23;
--c-accent-strong: 10 10 10;
--c-danger: 220 38 38;
--c-ok: 22 163 74;
}
.dark {
--c-base: 10 10 10;
--c-surface: 23 23 23;
--c-raised: 38 38 38;
--c-edge: 64 64 64;
--c-ink: 250 250 250;
--c-muted: 163 163 163;
--c-accent: 250 250 250;
--c-accent-strong: 229 229 229;
--c-danger: 248 113 113;
--c-ok: 74 222 128;
}
@layer base { @layer base {
html { html {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@@ -14,7 +40,11 @@
} }
::selection { ::selection {
background: rgba(23, 23, 23, 0.08); background: rgba(0, 0, 0, 0.12);
}
.dark ::selection {
background: rgba(255, 255, 255, 0.2);
} }
summary { summary {
@@ -37,7 +67,7 @@
} }
.btn-primary { .btn-primary {
@apply bg-accent text-white hover:bg-accent-strong; @apply bg-accent text-surface hover:bg-accent-strong;
} }
.btn-ghost { .btn-ghost {
+3 -3
View File
@@ -83,9 +83,9 @@
{#if file} {#if file}
<p class="max-w-full truncate text-sm font-medium text-ink">{file.name}</p> <p class="max-w-full truncate text-sm font-medium text-ink">{file.name}</p>
<p class="mt-1 text-xs text-muted">Déposez un autre fichier pour le remplacer</p> <p class="mt-1 text-xs text-muted">Drop another file to replace it</p>
{:else} {:else}
<p class="text-sm font-medium text-ink">Glissez-déposez un fichier ici</p> <p class="text-sm font-medium text-ink">Drag &amp; drop a file here</p>
<p class="mt-1 text-xs text-muted">ou cliquez pour parcourir</p> <p class="mt-1 text-xs text-muted">or click to browse</p>
{/if} {/if}
</div> </div>
+8 -8
View File
@@ -8,22 +8,22 @@
async function onDelete(item) { async function onDelete(item) {
const ok = window.confirm( const ok = window.confirm(
`Supprimer définitivement « ${item.originalName || item.downloadName} » ?` `Delete "${item.originalName || item.downloadName}" permanently? This cannot be undone.`
); );
if (!ok) return; if (!ok) return;
try { try {
await deleteFile(item); await deleteFile(item);
removeUpload(item.id); removeUpload(item.id);
dispatch('notify', { message: 'Fichier supprimé.', kind: 'success' }); dispatch('notify', { message: 'File deleted.', kind: 'success' });
} catch (e) { } catch (e) {
dispatch('notify', { message: e.message || 'Suppression impossible.', kind: 'error' }); dispatch('notify', { message: e.message || 'Deletion failed.', kind: 'error' });
} }
} }
function onClear() { function onClear() {
if ($uploads.length === 0) return; if ($uploads.length === 0) return;
const ok = window.confirm( const ok = window.confirm(
'Vider cette liste ? Seules les entrées de CE navigateur sont supprimées. Les fichiers restent sur le serveur.' 'Clear this list? This only removes the entries from THIS browser. Files on the server are not touched.'
); );
if (!ok) return; if (!ok) return;
clearUploads(); clearUploads();
@@ -33,14 +33,14 @@
<section class="card p-5"> <section class="card p-5">
<div class="mb-4 flex items-center justify-between gap-3"> <div class="mb-4 flex items-center justify-between gap-3">
<div class="flex items-baseline gap-2"> <div class="flex items-baseline gap-2">
<h2 class="text-sm font-semibold text-ink">Fichiers</h2> <h2 class="text-sm font-semibold text-ink">Files</h2>
{#if $uploads.length} {#if $uploads.length}
<span class="text-xs text-muted">{$uploads.length}</span> <span class="text-xs text-muted">{$uploads.length}</span>
{/if} {/if}
</div> </div>
{#if $uploads.length} {#if $uploads.length}
<button type="button" class="text-xs font-medium text-muted transition-colors hover:text-danger" on:click={onClear}> <button type="button" class="text-xs font-medium text-muted transition-colors hover:text-danger" on:click={onClear}>
Vider la liste Clear list
</button> </button>
{/if} {/if}
</div> </div>
@@ -62,8 +62,8 @@
/> />
</svg> </svg>
</div> </div>
<p class="text-sm text-muted">Aucun fichier pour l'instant.</p> <p class="text-sm text-muted">No files yet.</p>
<p class="mt-1 text-xs text-muted">Envoyez votre premier fichier pour le voir ici.</p> <p class="mt-1 text-xs text-muted">Upload your first file to see it here.</p>
</div> </div>
{/if} {/if}
</section> </section>
+4 -4
View File
@@ -50,18 +50,18 @@
<div class="flex flex-col gap-2 md:items-end"> <div class="flex flex-col gap-2 md:items-end">
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<a href={forcedDownloadUrl(item)} class="btn btn-primary px-3 py-1.5 text-xs">Télécharger</a> <a href={forcedDownloadUrl(item)} class="btn btn-primary px-3 py-1.5 text-xs">Download</a>
<button type="button" class="btn btn-ghost px-3 py-1.5 text-xs" on:click={copy}> <button type="button" class="btn btn-ghost px-3 py-1.5 text-xs" on:click={copy}>
{copied ? 'Copié' : 'Copier'} {copied ? 'Copied' : 'Copy'}
</button> </button>
<button type="button" class="btn btn-danger px-3 py-1.5 text-xs" on:click={() => dispatch('delete', item)}> <button type="button" class="btn btn-danger px-3 py-1.5 text-xs" on:click={() => dispatch('delete', item)}>
Supprimer Delete
</button> </button>
</div> </div>
<details> <details>
<summary class="cursor-pointer select-none text-xs font-medium text-muted transition-colors hover:text-ink"> <summary class="cursor-pointer select-none text-xs font-medium text-muted transition-colors hover:text-ink">
Liens Links
</summary> </summary>
<div class="mt-2 md:flex md:justify-end"> <div class="mt-2 md:flex md:justify-end">
<LinkFormats {item} /> <LinkFormats {item} />
+1 -1
View File
@@ -36,7 +36,7 @@
</div> </div>
<div class="mt-3"> <div class="mt-3">
<p class="mb-2 text-[11px] font-medium uppercase tracking-wide text-muted">Partager</p> <p class="mb-2 text-[11px] font-medium uppercase tracking-wide text-muted">Share links</p>
<LinkFormats item={result} size="md" /> <LinkFormats item={result} size="md" />
</div> </div>
</div> </div>
+20
View File
@@ -0,0 +1,20 @@
const KEY = 'filehost.theme';
export function currentTheme() {
return document.documentElement.classList.contains('dark') ? 'dark' : 'light';
}
export function applyTheme(theme) {
document.documentElement.classList.toggle('dark', theme === 'dark');
try {
localStorage.setItem(KEY, theme);
} catch {
/* ignore */
}
}
export function toggleTheme() {
const next = currentTheme() === 'dark' ? 'light' : 'dark';
applyTheme(next);
return next;
}
+11 -10
View File
@@ -1,19 +1,20 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
darkMode: 'class',
content: ['./index.html', './src/**/*.{svelte,js}'], content: ['./index.html', './src/**/*.{svelte,js}'],
theme: { theme: {
extend: { extend: {
colors: { colors: {
base: '#fafafa', base: 'rgb(var(--c-base) / <alpha-value>)',
surface: '#ffffff', surface: 'rgb(var(--c-surface) / <alpha-value>)',
raised: '#f5f5f5', raised: 'rgb(var(--c-raised) / <alpha-value>)',
edge: '#e5e5e5', edge: 'rgb(var(--c-edge) / <alpha-value>)',
ink: '#171717', ink: 'rgb(var(--c-ink) / <alpha-value>)',
muted: '#737373', muted: 'rgb(var(--c-muted) / <alpha-value>)',
accent: '#171717', accent: 'rgb(var(--c-accent) / <alpha-value>)',
'accent-strong': '#0a0a0a', 'accent-strong': 'rgb(var(--c-accent-strong) / <alpha-value>)',
danger: '#dc2626', danger: 'rgb(var(--c-danger) / <alpha-value>)',
ok: '#16a34a' ok: 'rgb(var(--c-ok) / <alpha-value>)'
}, },
fontFamily: { fontFamily: {
sans: ['system-ui', '-apple-system', 'Segoe UI', 'Roboto', 'sans-serif'] sans: ['system-ui', '-apple-system', 'Segoe UI', 'Roboto', 'sans-serif']
+6 -6
View File
@@ -13,13 +13,13 @@ if [[ "${1:-}" == "--debug" ]]; then
PROFILE="debug" PROFILE="debug"
fi fi
command -v cargo >/dev/null 2>&1 || { echo "cargo (Rust) est requis." >&2; exit 1; } command -v cargo >/dev/null 2>&1 || { echo "cargo (Rust) is required." >&2; exit 1; }
command -v npm >/dev/null 2>&1 || { echo "npm (Node.js) est requis." >&2; exit 1; } command -v npm >/dev/null 2>&1 || { echo "npm (Node.js) is required." >&2; exit 1; }
echo "==> Frontend (Svelte)" echo "==> Frontend (Svelte)"
cd "$ROOT/frontend" cd "$ROOT/frontend"
if [[ ! -d node_modules ]]; then if [[ ! -d node_modules ]]; then
echo " installation des dépendances..." echo " installing dependencies..."
npm install npm install
fi fi
npm run build npm run build
@@ -33,10 +33,10 @@ else
fi fi
echo echo
echo "Build terminé." echo "Build complete."
if [[ "$PROFILE" == "release" ]]; then if [[ "$PROFILE" == "release" ]]; then
echo " binaire : $ROOT/backend/target/release/filehost" echo " binary : $ROOT/backend/target/release/filehost"
else else
echo " binaire : $ROOT/backend/target/debug/filehost" echo " binary : $ROOT/backend/target/debug/filehost"
fi fi
echo " frontend: $ROOT/frontend/dist" echo " frontend: $ROOT/frontend/dist"