1 Commits

Author SHA1 Message Date
6d491eb694 Merge pull request 'feat/shop' (#36) from feat/shop into dev
All checks were successful
Build and Deploy / test-backend (push) Successful in 27s
Build and Deploy / test-frontend (push) Successful in 1m3s
Build and Deploy / build-and-push (push) Successful in 50s
Build and Deploy / deploy (push) Successful in 11s
Reviewed-on: #36
2026-03-10 11:16:22 +01:00
7 changed files with 14 additions and 26 deletions

View File

@@ -32,7 +32,7 @@ Crea un database PostgreSQL chiamato `printcalc`. Lo schema viene gestito dal pr
Configura il percorso di OrcaSlicer in `backend/src/main/resources/application.properties` o tramite la variabile d'ambiente `SLICER_PATH`. Per il media service pubblico puoi configurare anche: Configura il percorso di OrcaSlicer in `backend/src/main/resources/application.properties` o tramite la variabile d'ambiente `SLICER_PATH`. Per il media service pubblico puoi configurare anche:
- `MEDIA_STORAGE_ROOT` per la root `storage_media` usata dal backend (`original/`, `public/`, `private/`) - `MEDIA_STORAGE_ROOT` per la root `storage_media` usata dal backend (`original/`, `public/`, `private/`)
- `SHOP_STORAGE_ROOT` per la root `storage_shop` usata dal backend per i modelli dei prodotti shop - `MEDIA_PUBLIC_BASE_URL` per gli URL assoluti restituiti dalle API admin, ad esempio `https://example.com/media`
- `MEDIA_FFMPEG_PATH` per il binario `ffmpeg` - `MEDIA_FFMPEG_PATH` per il binario `ffmpeg`
- `MEDIA_UPLOAD_MAX_FILE_SIZE_BYTES` per il limite per asset immagine - `MEDIA_UPLOAD_MAX_FILE_SIZE_BYTES` per il limite per asset immagine
@@ -64,13 +64,12 @@ I prezzi non sono più gestiti tramite variabili d'ambiente fisse ma tramite tab
* `/frontend`: Applicazione Angular. * `/frontend`: Applicazione Angular.
* `/backend/profiles`: Contiene i file di configurazione per OrcaSlicer. * `/backend/profiles`: Contiene i file di configurazione per OrcaSlicer.
* `/storage_media`: Originali e varianti media pubbliche/private su filesystem. * `/storage_media`: Originali e varianti media pubbliche/private su filesystem.
* `/storage_shop`: Modelli e file prodotti dello shop.
## Media pubblici ## Media pubblici
Il backend salva sempre l'originale in `storage_media/original/` e precomputa le varianti pubbliche in `storage_media/public/`. La cartella `storage_media/private/` è predisposta per asset non pubblici. Il backend salva sempre l'originale in `storage_media/original/` e precomputa le varianti pubbliche in `storage_media/public/`. La cartella `storage_media/private/` è predisposta per asset non pubblici.
Nel deploy Docker i volumi attesi sono `/mnt/cache/appdata/print-calculator/${ENV}/storage_media:/app/storage_media` e `/mnt/cache/appdata/print-calculator/${ENV}/storage_shop:/app/storage_shop`. Nel deploy Docker il volume media atteso è `/mnt/cache/appdata/print-calculator/${ENV}/storage_media:/app/storage_media`.
Nginx non deve passare dal backend per i file pubblici. Configurazione attesa: Nginx non deve passare dal backend per i file pubblici. Configurazione attesa:
@@ -107,7 +106,7 @@ Operativamente:
Assicurati che `slicer.path` punti al binario corretto. Su macOS è solitamente `/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer`. Su Linux è il percorso all'AppImage (estratta o meno). Assicurati che `slicer.path` punti al binario corretto. Su macOS è solitamente `/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer`. Su Linux è il percorso all'AppImage (estratta o meno).
### FFmpeg e media pubblici ### FFmpeg e media pubblici
Verifica che `MEDIA_FFMPEG_PATH` punti a un `ffmpeg` con supporto JPEG, WebP e AVIF. Se gli URL media restituiti dalle API admin non sono raggiungibili, controlla che `APP_FRONTEND_BASE_URL` punti al dominio corretto, che `location /media/` sia esposto da Nginx e che il volume `storage_media` sia montato correttamente. Verifica che `MEDIA_FFMPEG_PATH` punti a un `ffmpeg` con supporto JPEG, WebP e AVIF. Se gli URL media restituiti dalle API admin non sono raggiungibili, controlla che `MEDIA_PUBLIC_BASE_URL` corrisponda al `location /media/` esposto da Nginx e che il volume `storage_media` sia montato correttamente.
### Database connection ### Database connection
Verifica le credenziali in `application.properties`. Se usi Docker, puoi passare `DB_URL`, `DB_USERNAME` e `DB_PASSWORD` come variabili d'ambiente. Verifica le credenziali in `application.properties`. Se usi Docker, puoi passare `DB_URL`, `DB_USERNAME` e `DB_PASSWORD` come variabili d'ambiente.

View File

@@ -18,15 +18,15 @@ public class MediaStorageService {
private final Path originalRootLocation; private final Path originalRootLocation;
private final Path publicRootLocation; private final Path publicRootLocation;
private final Path privateRootLocation; private final Path privateRootLocation;
private final String frontendBaseUrl; private final String publicBaseUrl;
public MediaStorageService(@Value("${media.storage.root:storage_media}") String storageRoot, public MediaStorageService(@Value("${media.storage.root:storage_media}") String storageRoot,
@Value("${app.frontend.base-url:${APP_FRONTEND_BASE_URL:http://localhost:8080}}") String frontendBaseUrl) { @Value("${media.public.base-url:http://localhost:8080/media}") String publicBaseUrl) {
this.normalizedRootLocation = Paths.get(storageRoot).toAbsolutePath().normalize(); this.normalizedRootLocation = Paths.get(storageRoot).toAbsolutePath().normalize();
this.originalRootLocation = normalizedRootLocation.resolve("original").normalize(); this.originalRootLocation = normalizedRootLocation.resolve("original").normalize();
this.publicRootLocation = normalizedRootLocation.resolve("public").normalize(); this.publicRootLocation = normalizedRootLocation.resolve("public").normalize();
this.privateRootLocation = normalizedRootLocation.resolve("private").normalize(); this.privateRootLocation = normalizedRootLocation.resolve("private").normalize();
this.frontendBaseUrl = frontendBaseUrl; this.publicBaseUrl = publicBaseUrl;
init(); init();
} }
@@ -73,12 +73,11 @@ public class MediaStorageService {
if (storageKey == null || storageKey.isBlank()) { if (storageKey == null || storageKey.isBlank()) {
return null; return null;
} }
String mediaBaseUrl = buildMediaBaseUrl();
String normalizedKey = storageKey.startsWith("/") ? storageKey.substring(1) : storageKey; String normalizedKey = storageKey.startsWith("/") ? storageKey.substring(1) : storageKey;
if (mediaBaseUrl.endsWith("/")) { if (publicBaseUrl.endsWith("/")) {
return mediaBaseUrl + normalizedKey; return publicBaseUrl + normalizedKey;
} }
return mediaBaseUrl + "/" + normalizedKey; return publicBaseUrl + "/" + normalizedKey;
} }
private void copy(Path source, Path destination) throws IOException { private void copy(Path source, Path destination) throws IOException {
@@ -128,15 +127,4 @@ public class MediaStorageService {
} }
return visibility.trim().toUpperCase(Locale.ROOT); return visibility.trim().toUpperCase(Locale.ROOT);
} }
private String buildMediaBaseUrl() {
String normalized = frontendBaseUrl != null ? frontendBaseUrl.trim() : "";
if (normalized.isBlank()) {
normalized = "http://localhost:4200";
}
if (normalized.endsWith("/")) {
normalized = normalized.substring(0, normalized.length() - 1);
}
return normalized + "/media";
}
} }

View File

@@ -9,4 +9,5 @@ admin.session.ttl-minutes=480
# Local media storage served by a local static server on port 8081. # Local media storage served by a local static server on port 8081.
media.storage.root=/Users/joe/IdeaProjects/print-calculator/storage_media media.storage.root=/Users/joe/IdeaProjects/print-calculator/storage_media
media.public.base-url=http://localhost:8081
media.ffmpeg.path=ffmpeg media.ffmpeg.path=ffmpeg

View File

@@ -28,6 +28,7 @@ clamav.enabled=${CLAMAV_ENABLED:false}
# Media configuration # Media configuration
media.storage.root=${MEDIA_STORAGE_ROOT:storage_media} media.storage.root=${MEDIA_STORAGE_ROOT:storage_media}
media.public.base-url=${MEDIA_PUBLIC_BASE_URL:http://localhost:8080/media}
media.ffmpeg.path=${MEDIA_FFMPEG_PATH:ffmpeg} media.ffmpeg.path=${MEDIA_FFMPEG_PATH:ffmpeg}
media.upload.max-file-size-bytes=${MEDIA_UPLOAD_MAX_FILE_SIZE_BYTES:26214400} media.upload.max-file-size-bytes=${MEDIA_UPLOAD_MAX_FILE_SIZE_BYTES:26214400}
shop.model.max-file-size-bytes=${SHOP_MODEL_MAX_FILE_SIZE_BYTES:104857600} shop.model.max-file-size-bytes=${SHOP_MODEL_MAX_FILE_SIZE_BYTES:104857600}

View File

@@ -87,7 +87,7 @@ class AdminMediaControllerServiceTest {
storageRoot = tempDir.resolve("storage_media"); storageRoot = tempDir.resolve("storage_media");
MediaStorageService mediaStorageService = new MediaStorageService( MediaStorageService mediaStorageService = new MediaStorageService(
storageRoot.toString(), storageRoot.toString(),
"https://cdn.example" "https://cdn.example/media"
); );
service = new AdminMediaControllerService( service = new AdminMediaControllerService(

View File

@@ -41,7 +41,7 @@ class PublicMediaQueryServiceTest {
void setUp() { void setUp() {
MediaStorageService mediaStorageService = new MediaStorageService( MediaStorageService mediaStorageService = new MediaStorageService(
tempDir.resolve("storage_media").toString(), tempDir.resolve("storage_media").toString(),
"https://cdn.example" "https://cdn.example/media"
); );
service = new PublicMediaQueryService(mediaUsageRepository, mediaVariantRepository, mediaStorageService); service = new PublicMediaQueryService(mediaUsageRepository, mediaVariantRepository, mediaStorageService);
} }

View File

@@ -32,7 +32,7 @@ services:
- TEMP_DIR=/app/temp - TEMP_DIR=/app/temp
- PROFILES_DIR=/app/profiles - PROFILES_DIR=/app/profiles
- MEDIA_STORAGE_ROOT=${MEDIA_STORAGE_ROOT:-/app/storage_media} - MEDIA_STORAGE_ROOT=${MEDIA_STORAGE_ROOT:-/app/storage_media}
- SHOP_STORAGE_ROOT=${SHOP_STORAGE_ROOT:-/app/storage_shop} - MEDIA_PUBLIC_BASE_URL=${MEDIA_PUBLIC_BASE_URL:-http://localhost:8080/media}
- MEDIA_FFMPEG_PATH=${MEDIA_FFMPEG_PATH:-ffmpeg} - MEDIA_FFMPEG_PATH=${MEDIA_FFMPEG_PATH:-ffmpeg}
- MEDIA_UPLOAD_MAX_FILE_SIZE_BYTES=${MEDIA_UPLOAD_MAX_FILE_SIZE_BYTES:-26214400} - MEDIA_UPLOAD_MAX_FILE_SIZE_BYTES=${MEDIA_UPLOAD_MAX_FILE_SIZE_BYTES:-26214400}
restart: always restart: always
@@ -47,7 +47,6 @@ services:
- /mnt/cache/appdata/print-calculator/${ENV}/storage_orders:/app/storage_orders - /mnt/cache/appdata/print-calculator/${ENV}/storage_orders:/app/storage_orders
- /mnt/cache/appdata/print-calculator/${ENV}/storage_requests:/app/storage_requests - /mnt/cache/appdata/print-calculator/${ENV}/storage_requests:/app/storage_requests
- /mnt/cache/appdata/print-calculator/${ENV}/storage_media:/app/storage_media - /mnt/cache/appdata/print-calculator/${ENV}/storage_media:/app/storage_media
- /mnt/cache/appdata/print-calculator/${ENV}/storage_shop:/app/storage_shop
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"