Client desktop officiel de Taskori permettant de naviguer les fichiers du bucket share.taskori.app et de synchroniser un dossier local en miroir. Construit avec Tauri 2 (Rust + React/TypeScript + Tailwind CSS 4).
src/ # Frontend React/TypeScript/Tailwind
├── pages/
│ ├── Files.tsx # Navigateur de fichiers + shell principal
│ ├── Sync.tsx # Vue de synchronisation (UI uniquement)
│ ├── Settings.tsx # Paramètres (compte, bande passante, startup)
│ ├── Journal.tsx # Journal d'activité
│ ├── Links.tsx # Gestion des liens de partage
│ └── Login.tsx # Écran de connexion
├── lib/
│ ├── api.ts # Pont TypeScript → commandes Tauri (invoke)
│ ├── types.ts # Types partagés (FileItem, Project, Client…)
│ ├── updater.ts # Vérification + installation de mises à jour
│ └── i18n.tsx # FR/EN/ES
└── components/
└── ShareModal.tsx # Modale de partage (PIN, expiration)
src-tauri/src/ # Backend Rust
├── main.rs # Point d'entrée (windows_subsystem=windows)
├── lib.rs # Enregistrement des commandes Tauri + setup tray
├── api.rs # Appels HTTP vers share.taskori.app/api/v1
├── auth.rs # Gestion tokens JWT (stockage SQLite + cache mémoire)
├── sync.rs # Algorithme de sync bidirectionnel
├── watcher.rs # Moteur d'auto-sync (notify + tokio timer)
├── bandwidth.rs # Limite de débit upload/download
└── db.rs # SQLite (rusqlite bundled) — settings, state, log
src-tauri/.cargo/config.toml # linker = "rust-lld" pour Windows
L'application cible bundle identifier : app.taskori.sync. Le produit s'appelle Taskori Sync. La fenêtre principale fait 1100×720 px (min 800×540).
| Couche | Technologie |
|---|---|
| Framework desktop | Tauri 2 |
| Backend | Rust (edition 2021) |
| HTTP | reqwest 0.12 (json, multipart, stream) |
| Base de données | rusqlite 0.31 (bundled SQLite) |
| Watcher FS | notify 6 |
| Async runtime | tokio 1 (full) |
| Frontend | React 19 + TypeScript 5.8 |
| Bundler frontend | Vite 7 |
| CSS | Tailwind CSS 4 |
| Build tool Tauri | @tauri-apps/cli 2 |
| Plugins Tauri | dialog, updater, process, autostart, opener |
localStorage).ShareModal : expiration 7j / 30j / jamais, code PIN optionnel.Voir la section dédiée ci-dessous.
Affiche les 200 dernières entrées de la table sync_log (SQLite), avec horodatage Unix converti en heure lisible. Rafraîchissement automatique lors des syncs via les événements Tauri sync:started / sync:finished / sync:error.
Réplique l'interface de share.taskori.app : liste des liens actifs, création avec scope, PIN et expiration, suppression.
/api/v1/me).tauri-plugin-autostart (LaunchAgent sur macOS, registre sur Windows).La fonction centrale run_full_sync() opère en trois phases puis une réconciliation :
Phase 1 — Construction de la map remote
Pour chaque projet de l'API (/clients + /projects) :
{client}/{projet}/{fichier} → {uid, size}.{client}/{projet}/{dossier}/{fichier} → {uid, size}.upload_targets (association chemin relatif → project_uid + folder_uid optionnel).project_uid_by_path et folder_uid_by_path pour les uploads dans des chemins sans fichier existant.Les noms de segments sont assainis via sanitize_segment() : remplace /\:*?"<>| et caractères de contrôle par _, supprime les points en début/fin, fallback sur l'uid si vide.
Phase 2 — Construction de la map local
Walk récursif du dossier local racine via collect_files(). Sont ignorés :
@eaDir (cache thumbnails Synology NAS) et .git..DS_Store, Thumbs.db, desktop.ini, Icon\r.. (dotfiles)..conflict.Phase 3 — Chargement du snapshot prev (table synced_files)
Sert à distinguer "nouveau" de "supprimé" pour chaque côté.
Réconciliation sur l'union des trois ensembles de clés :
| Présent dans | Action |
|---|---|
remote + local (tailles identiques) |
Rien, mise à jour de l'état |
remote + local (tailles différentes) |
Conflit : renomme local en .conflict, télécharge la version remote |
local seulement, prev présent |
Supprimé côté remote → supprime le fichier local |
local seulement, prev absent |
Nouveau fichier local → upload vers le serveur |
remote seulement, prev présent |
Supprimé côté local → supprime via DELETE /api/v1/files/{uid} |
remote seulement, prev absent |
Nouveau fichier remote → télécharge localement |
prev seulement (plus nulle part) |
Nettoyage de l'état |
Lorsqu'un fichier local de 4 segments (client/projet/dossier/fichier) est détecté comme nouveau et que le dossier n'existe pas encore côté serveur, le moteur appelle automatiquement POST /api/v1/folders avec {name, project_uid}, enregistre le nouvel uid dans folder_uid_by_path, puis procède à l'upload. C'est le fix récent qui permet de créer des dossiers locaux et les voir apparaître côté serveur.
pub struct SyncStats {
pub downloaded: usize,
pub uploaded: usize,
pub conflicts: usize,
pub deleted: usize,
pub errors: usize,
}
Affiché dans la sidebar comme ↓N ↑N avec indicateurs optionnels conflits/erreurs/suppressions.
Calculée et loguée en B/s, KB/s ou MB/s pour chaque fichier.
access_token + refresh_token) et URL du serveur stockés dans la table settings de SQLite.Mutex<Option<String>>) pour éviter les lectures SQLite répétées pendant une session.clear_tokens() écrase avec des chaînes vides (pas de DELETE, plus simple, évite un changement de schéma).https://taskori.app.api.rs construit l'URL base depuis auth::get_server() en remplaçant taskori.app par share.taskori.app. Tous les appels ciblent donc https://share.taskori.app/api/v1/….
Le token Bearer est ajouté à chaque requête. En cas de code HTTP non-2xx, le corps JSON est inclus dans le message d'erreur retourné.
Fichier : {data_dir}/taskori-sync/state.db
data_dir = %APPDATA% (Windows), ~/.local/share (Linux), ~/Library/Application Support (macOS).Tables :
| Table | Description |
|---|---|
settings |
Paires clé/valeur (tokens, local_root, sync_interval, bandwidth…) |
synced_files |
État de sync : rel TEXT UNIQUE, remote_uid, size, last_sync_at |
sync_pairs |
(Réservé pour de futures paires projet/dossier) |
sync_log |
Événements horodatés (kind, file_name, detail) |
Migration automatique : si la table synced_files n'a pas de colonne rel (ancien schéma keyed par local_path), elle est recréée vide. Un prev vide ne peut jamais déclencher une suppression — sûr.
Singleton de processus (OnceLock<Mutex<Option<Engine>>>). reconfigure(app) arrête l'engine précédent et en démarre un nouveau selon la valeur de sync_interval :
| Valeur | Comportement |
|---|---|
realtime |
Watcher FS (notify::RecommendedWatcher, poll fallback 2s) + debounce 2s + safety poll 60s + sync initial au démarrage |
5 |
Timer tokio toutes les 5 minutes (première tick = sync immédiat) |
15 |
Timer tokio toutes les 15 minutes |
manual (défaut) |
Aucun engine automatique |
SyncGuard (deux AtomicBool statiques) garantit qu'une seule sync tourne à la fois. Un trigger supplémentaire arrivant pendant une sync active le flag pending ; à la fin de la sync en cours, une seconde passe est déclenchée automatiquement.
| Événement | Payload |
|---|---|
sync:started |
() |
sync:finished |
SyncStats (downloaded, uploaded, conflicts, deleted, errors) |
sync:error |
String (message d'erreur) |
Ces événements sont capturés dans Files.tsx via listen() pour mettre à jour la barre de statut dans la sidebar, quel que soit l'onglet actif.
La fermeture de la fenêtre est interceptée (CloseRequested) et transformée en window.hide() — l'application continue en tâche de fond avec l'icône système. Le menu icône système propose "Afficher Taskori Sync" et "Quitter".
Deux clés SQLite : upload_limit_bps et download_limit_bps. Valeur 0 = pas de limite.
Avec limite active, le fichier est découpé en chunks de max(4096, limit_bps / 10) octets (~100ms par chunk). Entre chaque chunk (sauf le premier), tokio::time::sleep de chunk_size / limit_bps * 1000 ms. Le stream est passé à reqwest::Body::wrap_stream.
Avec limite active, token-bucket par octet : après chaque chunk, calcule expected_ms = total_bytes * 1000 / limit_bps et dort si expected_ms > elapsed_ms.
L'UI Settings exprime les limites en MB/s ; la conversion vers bps se fait côté React (* 1_000_000) avant l'appel invoke("set_upload_limit", { bps }).
setInterval dans Files.tsx.Fond #0f0f13, panneaux #1a1a2e, bordures #2d2d44, accent violet-600. Police titres : Syne.
tauri.conf.json → plugin updater :
{
"endpoints": ["https://dl.taskori.app/latest.json"],
"pubkey": "<minisign base64>"
}
La clé publique est la clé minisign correspondant à TAURI_SIGNING_PRIVATE_KEY (voir note reference_taskori_sync_updater_keys dans Flatnotes — ne pas perdre).
dl.taskori.app sert latest.json avec l'en-tête Cache-Control: no-store pour que Cloudflare ne mette jamais ce fichier en cache — indispensable pour que les clients voient la nouvelle version immédiatement après une release.
check() → interroge latest.json.downloadAndInstall() (téléchargement + installation silencieuse en arrière-plan là où l'OS le permet).window.confirm() → relaunch() pour appliquer.Statuts retournés : "up-to-date" | "installed:<version>" | "error".
Vérification au démarrage dans Settings.tsx (appelée automatiquement à chaque montage via useEffect).
Push d'un tag v*.*.* sur le repo, ou workflow_dispatch.
La version est injectée depuis le tag (GITHUB_REF_NAME) directement dans src-tauri/Cargo.toml et src-tauri/tauri.conf.json via sed — pas besoin de bumper les fichiers avant de tagger.
libwebkit2gtk-4.1-dev libgtk-3-dev librsvg2-dev libayatana-appindicator3-dev libssl-dev~/.cargo/registry, ~/.cargo/git, src-tauri/targetAPPIMAGE_EXTRACT_AND_RUN=1 requis pour que AppImage fonctionne dans GitHub Actionsnpx tauri build --bundles deb,appimageTaskori-Sync_{version}_amd64.AppImage, .AppImage.sig, _amd64.debx86_64-pc-windows-msvcsrc-tauri/target — trop lourd et peu de gain)npx tauri build --bundles nsisTaskori-Sync_{version}_x64-setup.exe, .exe.sigaarch64-apple-darwin + x86_64-apple-darwinsrc-tauri/targetAPPLE_ID + APPLE_PASSWORD (App-Specific Password) + APPLE_TEAM_IDnpx tauri build --target universal-apple-darwin --bundles app,dmgTaskori-Sync_{version}_universal.dmg, .app.tar.gz, .app.tar.gz.siggh release create / gh release upload --clobber.https://dl.taskori.app/hook?tag={tag} avec X-Token: {DL_WEBHOOK_SECRET} pour déclencher le pull des assets côté dl.taskori.app (non bloquant si absent).| Secret GitHub | Utilisation |
|---|---|
TAURI_SIGNING_PRIVATE_KEY |
Clé minisign pour signer les artefacts updater (.sig) — identique sur les 3 plateformes |
TAURI_SIGNING_PRIVATE_KEY_PASSWORD |
Vide "" (clé non protégée par mot de passe) |
APPLE_CERTIFICATE |
Certificat Developer ID Application encodé base64 |
APPLE_CERTIFICATE_PASSWORD |
Mot de passe du .p12 |
APPLE_SIGNING_IDENTITY |
"Developer ID Application: ..." |
APPLE_ID |
Apple ID pour la notarisation |
APPLE_PASSWORD |
App-Specific Password Apple |
APPLE_TEAM_ID |
Team ID Apple Developer |
DL_WEBHOOK_SECRET |
Token pour le webhook dl.taskori.app |
GITHUB_TOKEN |
Automatique — création de la Release |
release, le workflow appelle POST https://dl.taskori.app/hook?tag={tag} avec X-Token pour déclencher le pull automatique des assets depuis la GitHub Release.latest.json avec Cache-Control: no-store (Cloudflare ne met pas ce fichier en cache).src-tauri/.cargo/config.toml :
[target.x86_64-pc-windows-msvc]
linker = "rust-lld"
rust-lld remplace le linker MSVC par défaut — significativement plus rapide pour la phase de linkage. Gain de plusieurs minutes sur Windows.
~/.cargo/registry, ~/.cargo/git + src-tauri/target (invalidation sur Cargo.lock).target Windows est volumineux et le restore est plus long que le gain).actions/setup-node@v6 avec cache: "npm" sur les trois plateformes. Évite de re-télécharger node_modules si package-lock.json n'a pas changé.
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
LTO + codegen-units=1 maximisent la taille du binaire réduite et les performances, au prix d'un temps de compilation plus long (compensé par le cache).
| Plateforme | Durée |
|---|---|
| Linux | ~10 min |
| macOS universal | ~14 min |
| Windows NSIS | ~18 min (objectif 15 min) |
La structure attendue dans le dossier local de sync est strictement :
<root>/
<client_name>/
<project_name>/
fichier.ext
<folder_name>/
fichier.ext
Tout fichier à la racine ou à un niveau trop profond (5+ segments) est ignoré silencieusement lors de l'upload (pas de upload_target correspondant).
Le conflit est détecté par comparaison de taille uniquement (pas de hash, pas de date). Un fichier local et remote de même taille mais contenu différent ne sera pas détecté comme conflictuel.
.conflictLors d'un conflit, le fichier local est renommé {nom}.conflict avant que la version remote soit téléchargée. Les fichiers .conflict sont ensuite ignorés par collect_files() et ne sont jamais uploadés.
La suppression d'un fichier localement déclenche DELETE /api/v1/files/{uid} lors de la prochaine sync. Inversement, une suppression remote supprime le fichier local. Ce comportement nécessite que l'état prev soit correctement renseigné — un prev vide (premiere sync ou après migration) ne peut jamais déclencher de suppression.
Les dossiers @eaDir (métadonnées Synology) sont ignorés lors du walk local — pertinent si le dossier de sync est sur un NFS Synology.
La croix de fermeture ne quitte pas l'application : elle la réduit dans la barre système. Pour quitter, utiliser le menu icône système > "Quitter" ou app.exit(0).
Par défaut https://taskori.app. L'API share est systématiquement résolue en https://share.taskori.app/api/v1/… par substitution de domaine dans api.rs. Si le serveur configuré est une instance self-hosted, la substitution taskori.app → share.taskori.app peut ne pas fonctionner.
Il n'y a pas de logique de refresh automatique du JWT dans la version actuelle. En cas d'expiration du token, les appels API échoueront avec HTTP 401 et l'utilisateur devra se reconnecter manuellement.