696 lines
25 KiB
Markdown
696 lines
25 KiB
Markdown
Certo Fabio — ora che tutto il backend e il frontend sono finalmente coerenti, ti preparo un README tecnico completo, scritto come si deve, chiaro, professionale e utile sia per te che per chiunque metterà mano al progetto in futuro.
|
||
|
||
È pensato per essere messo direttamente in README.md alla radice del progetto.
|
||
|
||
---
|
||
|
||
📸 PhotoGallery — README Tecnico Completo
|
||
|
||
Una piattaforma completa per la gestione di foto multi‑utente, con:
|
||
|
||
- autenticazione JWT
|
||
- scansione automatica delle cartelle
|
||
- thumbnails generate lato server
|
||
- sincronizzazione incrementale
|
||
- WebSocket real‑time
|
||
- frontend modulare e reattivo
|
||
- mappa globale stile Google Photos
|
||
- pannello info EXIF + geolocalizzazione
|
||
- bottom sheet multi‑foto
|
||
- modal fullscreen con navigazione
|
||
|
||
---
|
||
|
||
🚀 1. Architettura generale
|
||
|
||
`
|
||
backend/
|
||
server.js
|
||
api_v1/
|
||
scanner/
|
||
config.js
|
||
...
|
||
db/
|
||
knex.js
|
||
routes/
|
||
photos.js
|
||
|
||
public/
|
||
index.html
|
||
login.html
|
||
css/
|
||
js/
|
||
auth.js
|
||
login.js
|
||
logout.js
|
||
config.js
|
||
api.js
|
||
state.js
|
||
sync.js
|
||
gallery.js
|
||
modal.js
|
||
infoPanel.js
|
||
mapGlobal.js
|
||
bottomSheet.js
|
||
optionsSheet.js
|
||
`
|
||
|
||
---
|
||
|
||
🔐 2. Autenticazione
|
||
|
||
Login
|
||
POST /auth/login
|
||
|
||
Richiede:
|
||
|
||
`json
|
||
{ "email": "...", "password": "..." }
|
||
`
|
||
|
||
Risponde:
|
||
|
||
`json
|
||
{ "token": "JWT...", "name": "Fabio" }
|
||
`
|
||
|
||
Il token contiene:
|
||
|
||
`json
|
||
{
|
||
"id": 1,
|
||
"email": "fabio@example.com",
|
||
"name": "Fabio",
|
||
"exp": 1234567890
|
||
}
|
||
`
|
||
|
||
Logout
|
||
POST /auth/logout
|
||
|
||
Il token viene inserito in denylist.
|
||
|
||
Middleware
|
||
Tutte le rotte (eccetto /auth/*) richiedono:
|
||
|
||
`
|
||
Authorization: Bearer <token>
|
||
`
|
||
|
||
Il middleware aggiunge automaticamente:
|
||
|
||
`
|
||
req.query.user = [req.user.name, "Common"]
|
||
`
|
||
|
||
Quindi il frontend non deve mai passare user=.
|
||
|
||
---
|
||
|
||
🗂️ 3. Configurazione
|
||
|
||
GET /config restituisce:
|
||
|
||
`json
|
||
{
|
||
"baseUrl": "https://…",
|
||
"pathFull": false,
|
||
"galleryRefreshSeconds": 30
|
||
}
|
||
`
|
||
|
||
Il frontend usa:
|
||
|
||
- baseUrl per costruire URL assoluti
|
||
- pathFull per capire se i path sono già completi
|
||
- galleryRefreshSeconds per il polling
|
||
|
||
---
|
||
|
||
🖼️ 4. Foto: API principali
|
||
|
||
Tutte le foto
|
||
GET /photos?user[]=Fabio&user[]=Common
|
||
(aggiunto automaticamente dal middleware)
|
||
|
||
Foto per ID
|
||
GET /photos/byIds?id=123
|
||
|
||
Cambiamenti incrementali
|
||
GET /photos/changes?since=<ISO>
|
||
|
||
Restituisce:
|
||
|
||
`json
|
||
{
|
||
"changes": [
|
||
{ "photoid": 123, "changetype": "added", "timestamp": "..." },
|
||
{ "photoid": 456, "changetype": "removed", "timestamp": "..." }
|
||
]
|
||
}
|
||
`
|
||
|
||
---
|
||
|
||
🔄 5. Sincronizzazione
|
||
|
||
Il frontend implementa:
|
||
|
||
Full load
|
||
- chiamato al primo avvio
|
||
- scarica tutte le foto
|
||
- salva in localStorage
|
||
- aggiorna lastSync
|
||
|
||
Incremental sync
|
||
- usa /photos/changes
|
||
- applica solo differenze
|
||
- aggiorna la gallery
|
||
- aggiorna lastSync
|
||
|
||
WebSocket real‑time
|
||
Il server invia:
|
||
|
||
`
|
||
{ type: "added", id }
|
||
{ type: "removed", id }
|
||
{ type: "add_dir", folder }
|
||
{ type: "del_dir", folder }
|
||
`
|
||
|
||
Il frontend aggiorna:
|
||
|
||
- stato locale
|
||
- gallery
|
||
- mappa
|
||
|
||
---
|
||
|
||
🗺️ 6. Mappa globale
|
||
|
||
Basata su Leaflet + MarkerCluster.
|
||
|
||
- clustering automatico
|
||
- collage thumbnails nei cluster
|
||
- click cluster → bottom sheet
|
||
- click foto → modal
|
||
- refresh automatico quando cambia la gallery
|
||
|
||
---
|
||
|
||
🪟 7. Modal foto/video
|
||
|
||
Funzionalità:
|
||
|
||
- navigazione ← →
|
||
- click ai bordi
|
||
- tastiera
|
||
- preload ±3 foto
|
||
- supporto video (mp4/webm/mov)
|
||
- pannello info integrato
|
||
|
||
---
|
||
|
||
ℹ️ 8. Info Panel
|
||
|
||
Mostra:
|
||
|
||
- nome
|
||
- data
|
||
- dimensioni
|
||
- peso
|
||
- MIME
|
||
- cartella
|
||
- EXIF GPS
|
||
- reverse geocoding (se presente)
|
||
- mappa dedicata
|
||
|
||
Auto‑refresh quando cambi foto nel modal.
|
||
|
||
---
|
||
|
||
📚 9. Bottom Sheet
|
||
|
||
Usato per:
|
||
|
||
- mostrare gruppi di foto (cluster mappa)
|
||
- navigazione rapida
|
||
- apertura modal
|
||
|
||
---
|
||
|
||
⚙️ 10. Options Sheet
|
||
|
||
Gestisce:
|
||
|
||
- ordinamento (asc/desc)
|
||
- raggruppamento (auto/day/month/year)
|
||
- filtri (folder/location/type)
|
||
|
||
Aggiorna la gallery in tempo reale.
|
||
|
||
---
|
||
|
||
🧠 11. Stato locale
|
||
|
||
Gestito in state.js:
|
||
|
||
- localPhotos[]
|
||
- lastSync
|
||
- cache in localStorage
|
||
- funzioni: add/remove/update
|
||
|
||
---
|
||
|
||
🧩 12. Costruzione URL media
|
||
|
||
Tutto passa da:
|
||
|
||
`js
|
||
mediaUrlsFromPhoto(photo)
|
||
`
|
||
|
||
Restituisce:
|
||
|
||
`js
|
||
{ original, preview }
|
||
`
|
||
|
||
Supporta:
|
||
|
||
- path relativi
|
||
- path assoluti
|
||
- baseUrl dinamico
|
||
- cartella utente
|
||
|
||
---
|
||
|
||
🧪 13. Test end‑to‑end consigliato
|
||
|
||
1. Vai su /login
|
||
2. Inserisci email/password
|
||
3. Redirect a /
|
||
4. Full load → gallery appare
|
||
5. Apri una foto → modal
|
||
6. Apri info → mappa + EXIF
|
||
7. Apri mappa globale → cluster
|
||
8. Click cluster → bottom sheet
|
||
9. Click foto → modal
|
||
10. Cancella una foto dal filesystem → WS → frontend aggiorna
|
||
11. Logout → redirect a login
|
||
|
||
---
|
||
|
||
🛠️ 14. Requisiti
|
||
|
||
- Node.js 18+
|
||
- SQLite3
|
||
- ffmpeg (per thumbnails video)
|
||
- ImageMagick (per thumbnails foto)
|
||
|
||
---
|
||
|
||
▶️ 15. Avvio
|
||
|
||
`bash
|
||
npm install
|
||
node server.js
|
||
`
|
||
|
||
Server su:
|
||
|
||
`
|
||
http://localhost:4000
|
||
`
|
||
|
||
---
|
||
|
||
🎯 16. TODO / miglioramenti futuri
|
||
|
||
- virtualizzazione gallery (migliaia di foto)
|
||
- ricerca per testo
|
||
- timeline orizzontale
|
||
- upload drag&drop
|
||
- editing EXIF
|
||
- preferiti / album
|
||
|
||
---
|
||
|
||
💬 17. Contatti
|
||
|
||
Progetto sviluppato da Fabio
|
||
Backend + frontend completamente integrati.
|
||
|
||
---
|
||
|
||
Perfetto Fabio — ti preparo il diagramma architetturale completo, chiaro, leggibile e fedele al tuo backend e al tuo frontend attuale.
|
||
Niente fronzoli, niente astrazioni inutili: solo ciò che serve davvero per capire come gira tutto.
|
||
|
||
---
|
||
|
||
🧩 DIAGRAMMA ARCHITETTURALE — PhotoGallery System
|
||
|
||
1) Vista generale (High‑Level)
|
||
|
||
`
|
||
┌──────────────────────────┐
|
||
│ FRONTEND │
|
||
│ (public/, JS modules) │
|
||
└─────────────┬────────────┘
|
||
│ HTTP + WS
|
||
┌─────────────▼────────────┐
|
||
│ BACKEND │
|
||
│ (Node + Express) │
|
||
└─────────────┬────────────┘
|
||
│ DB queries
|
||
┌─────────────▼────────────┐
|
||
│ SQLite DB │
|
||
│ (tabella photos) │
|
||
└───────────────────────────┘
|
||
`
|
||
|
||
---
|
||
|
||
2) Dettaglio Frontend (moduli JS)
|
||
|
||
`
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ FRONTEND │
|
||
│ │
|
||
│ index.html / login.html │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ auth.js ←→ login.js ←→ logout.js │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ config.js → api.js → state.js → sync.js │
|
||
│ │ │ │ │
|
||
│ │ │ └── WebSocket │
|
||
│ ▼ ▼ │
|
||
│ gallery.js ←→ modal.js ←→ infoPanel.js │
|
||
│ │ │ │
|
||
│ ▼ ▼ │
|
||
│ mapGlobal.js ←→ bottomSheet.js ←→ optionsSheet.js │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
`
|
||
|
||
Flusso principale:
|
||
1. auth.js controlla token → se valido, mostra app
|
||
2. config.js carica /config
|
||
3. sync.js fa full load → incremental sync → WebSocket
|
||
4. state.js mantiene foto locali
|
||
5. gallery.js renderizza
|
||
6. modal.js apre foto/video
|
||
7. infoPanel.js mostra EXIF + mappa
|
||
8. mapGlobal.js mostra mappa globale
|
||
9. bottomSheet.js mostra strip foto
|
||
10. optionsSheet.js gestisce filtri/ordinamento
|
||
|
||
---
|
||
|
||
3) Dettaglio Backend (server.js)
|
||
|
||
`
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ BACKEND │
|
||
│ │
|
||
│ server.js │
|
||
│ │ │
|
||
│ ├── STATIC: serve public/ │
|
||
│ ├── /config │
|
||
│ ├── /auth/login │
|
||
│ ├── /auth/logout │
|
||
│ │ │
|
||
│ ├── JWT middleware │
|
||
│ │ ├── verify token │
|
||
│ │ ├── denylist │
|
||
│ │ └── req.user = { id, email, name } │
|
||
│ │ │
|
||
│ ├── GET middleware (user filtering) │
|
||
│ │ └── req.query.user = [req.user.name, "Common"] │
|
||
│ │ │
|
||
│ ├── /scan (Admin → tutti, User → solo se stesso) │
|
||
│ ├── /apiv1/autoscan (ADD, DEL, ADDDIR, DELDIR) │
|
||
│ │ └── WebSocket broadcast │
|
||
│ │ │
|
||
│ ├── /photos (router SQLite) │
|
||
│ │ ├── /photos │
|
||
│ │ ├── /photos/byIds │
|
||
│ │ └── /photos/changes │
|
||
│ │ │
|
||
│ ├── /files (serve file statici sicuri) │
|
||
│ ├── /initDB /initDBuser │
|
||
│ │ │
|
||
│ └── ws-server.js (WebSocket) │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
`
|
||
|
||
---
|
||
|
||
4) Flusso completo di una foto (end‑to‑end)
|
||
|
||
`
|
||
[1] File aggiunto nel filesystem
|
||
│
|
||
▼
|
||
[2] scan_auto → type="ADD"
|
||
│
|
||
├── scanFile()
|
||
├── scanPhotoSingle()
|
||
├── INSERT in DB
|
||
├── genera thumbnails
|
||
└── WS: { type:"added", id }
|
||
│
|
||
▼
|
||
[3] Frontend riceve WS
|
||
│
|
||
├── getPhotoById(id)
|
||
├── addPhotoLocal()
|
||
├── refreshGallery()
|
||
└── redrawPhotoMarkers()
|
||
`
|
||
|
||
---
|
||
|
||
5) Flusso login → gallery
|
||
|
||
`
|
||
login.html
|
||
│
|
||
▼
|
||
login.js → AppAuth.login(email, password)
|
||
│
|
||
▼
|
||
POST /auth/login
|
||
│
|
||
▼
|
||
token JWT salvato
|
||
│
|
||
▼
|
||
redirect → /
|
||
│
|
||
▼
|
||
index.html
|
||
│
|
||
▼
|
||
auth.js.isLoggedIn() → OK
|
||
│
|
||
▼
|
||
config.js → GET /config
|
||
│
|
||
▼
|
||
sync.js.fullLoad()
|
||
│
|
||
▼
|
||
GET /photos
|
||
│
|
||
▼
|
||
state.js.setLocalPhotos()
|
||
│
|
||
▼
|
||
gallery.js.renderGallery()
|
||
│
|
||
▼
|
||
mapGlobal.js.redrawPhotoMarkers()
|
||
`
|
||
|
||
---
|
||
|
||
6) Diagramma WebSocket
|
||
|
||
`
|
||
┌──────────────┐ WS ┌──────────────┐
|
||
│ Backend │ ───────────→ │ Frontend │
|
||
└──────────────┘ └──────────────┘
|
||
│ │
|
||
│ added → { id } │
|
||
│────────────────────────────────▶│ addPhotoLocal()
|
||
│ │ refreshGallery()
|
||
│ │ redrawPhotoMarkers()
|
||
│ │
|
||
│ removed → { id } │
|
||
│────────────────────────────────▶│ removePhotoLocal()
|
||
│ │ refreshGallery()
|
||
│ │ redrawPhotoMarkers()
|
||
│ │
|
||
│ adddir / deldir │
|
||
│────────────────────────────────▶│ incrementalSync()
|
||
`
|
||
|
||
---
|
||
|
||
7) Diagramma Database
|
||
|
||
`
|
||
┌──────────────────────────────┐
|
||
│ photos │
|
||
├──────────────────────────────┤
|
||
│ id (PK) │
|
||
│ user │
|
||
│ name │
|
||
│ path │
|
||
│ cartella │
|
||
│ mime_type │
|
||
│ width │
|
||
│ height │
|
||
│ size_bytes │
|
||
│ taken_at │
|
||
│ gps_lat │
|
||
│ gps_lng │
|
||
│ gps_alt │
|
||
│ location_json │
|
||
│ created_at │
|
||
└──────────────────────────────┘
|
||
`
|
||
|
||
---
|
||
|
||
8) Diagramma URL media
|
||
|
||
`
|
||
BASE_URL/photos/<user>/<type>/<cartella>/<file>
|
||
|
||
Esempi:
|
||
|
||
original:
|
||
https://server/photos/Fabio/original/2024/IMG_001.jpg
|
||
|
||
thumbs:
|
||
https://server/photos/Fabio/thumbs/2024/IMG001thub2.jpg
|
||
`
|
||
|
||
---
|
||
|
||
9) Diagramma dei moduli JS (dipendenze)
|
||
|
||
`
|
||
auth.js
|
||
↑
|
||
login.js → logout.js
|
||
↑
|
||
index.html
|
||
↓
|
||
config.js → api.js → state.js → sync.js → WebSocket
|
||
↓ ↓ ↓
|
||
gallery.js ← modal.js ← infoPanel.js
|
||
↓
|
||
mapGlobal.js ← bottomSheet.js ← optionsSheet.js
|
||
`
|
||
|
||
---
|
||
|
||
Utente Browser/Frontend Backend (Express) DB (SQLite)
|
||
│ │ │ │
|
||
│ 1. Apre /login │ │ │
|
||
├──────────────────────────▶│ │ │
|
||
│ │ │ │
|
||
│ │ 2. Inserisce email/password │ │
|
||
│ ├────────────────────────────────▶│ POST /auth/login │
|
||
│ │ │ │
|
||
│ │ │ 3. Verifica utente │
|
||
│ │ │ bcrypt.compare() │
|
||
│ │ ├────────────────────────▶│
|
||
│ │ │ │
|
||
│ │ │ 4. Genera JWT │
|
||
│ │ │ createToken() │
|
||
│ │ │ │
|
||
│ │ 5. Riceve token │ │
|
||
│ ◀─────────────────────────────────┤ │
|
||
│ │ │ │
|
||
│ │ 6. Salva token (localStorage) │ │
|
||
│ │ 7. Redirect → / │ │
|
||
├──────────────────────────▶│ │ │
|
||
│ │ │ │
|
||
│ │ 8. GET /config │ │
|
||
│ ├────────────────────────────────▶│ │
|
||
│ │ │ │
|
||
│ │ 9. Riceve config │ │
|
||
│ ◀─────────────────────────────────┤ │
|
||
│ │ │ │
|
||
│ │ 10. Full Sync │ │
|
||
│ │ GET /photos │ │
|
||
│ ├────────────────────────────────▶│ │
|
||
│ │ │ 11. Middleware JWT │
|
||
│ │ │ req.user = {name,...} │
|
||
│ │ │ │
|
||
│ │ │ 12. Filtra per utente │
|
||
│ │ │ req.query.user=[Fabio,Common]
|
||
│ │ │ │
|
||
│ │ ├────────────────────────▶│ SELECT * FROM photos WHERE user IN (...)
|
||
│ │ │ │
|
||
│ │ 13. Riceve lista foto │ │
|
||
│ ◀─────────────────────────────────┤ │
|
||
│ │ │ │
|
||
│ │ 14. state.setLocalPhotos() │ │
|
||
│ │ 15. gallery.render() │ │
|
||
│ │ 16. mapGlobal.redrawMarkers() │ │
|
||
│ │ │ │
|
||
│ │ 17. Apre WebSocket │ │
|
||
│ ├────────────────────────────────▶│ ws-server │
|
||
│ │ │ │
|
||
│ │ │ │
|
||
│ │ 18. Incremental Sync (polling) │ │
|
||
│ │ GET /photos/changes?since=... │ │
|
||
│ ├────────────────────────────────▶│ │
|
||
│ │ │ │
|
||
│ │ 19. Riceve changes │ │
|
||
│ ◀─────────────────────────────────┤ │
|
||
│ │ │ │
|
||
│ │ 20. Applica differenze │ │
|
||
│ │ state.add/remove/update │ │
|
||
│ │ gallery.refresh() │ │
|
||
│ │ mapGlobal.redrawMarkers() │ │
|
||
│ │ │ │
|
||
│ │ │ │
|
||
│ │ 21. Evento reale: file aggiunto │ │
|
||
│ │ │ scan_auto → ADD │
|
||
│ │ │ INSERT in DB │
|
||
│ │ ├────────────────────────▶│
|
||
│ │ │ │
|
||
│ │ │ 22. WS broadcast │
|
||
│ ◀─────────────────────────────────┤ {type:"added", id} │
|
||
│ │ │ │
|
||
│ │ 23. getPhotoById(id) │ │
|
||
│ ├────────────────────────────────▶│ /photos/byIds │
|
||
│ │ │ │
|
||
│ │ 24. Riceve foto │ │
|
||
│ ◀─────────────────────────────────┤ │
|
||
│ │ │ │
|
||
│ │ 25. state.addPhotoLocal() │ │
|
||
│ │ 26. gallery.refresh() │ │
|
||
│ │ 27. mapGlobal.redrawMarkers() │ │
|
||
│ │ │ │
|
||
│ │ │ │
|
||
│ 28. Clic su foto │ │ │
|
||
├──────────────────────────▶│ modal.open(photo) │ │
|
||
│ │ infoPanel.render(photo) │ │
|
||
│ │ │ │
|
||
│ │ │ │
|
||
│ 29. Logout │ │ │
|
||
├──────────────────────────▶│ AppAuth.logout() │ │
|
||
│ ├────────────────────────────────▶│ POST /auth/logout │
|
||
│ │ │ addToDenylist(token) │
|
||
│ │ │ │
|
||
│ │ 30. clearTokens() │ │
|
||
│ │ 31. redirect → /login │ │
|
||
└───────────────────────────┴─────────────────────────────────┴─────────────────────────┘
|