Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/.*
/target
.DS_Store
172 changes: 172 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Änderungsprotokoll – seit letztem Commit (697896b)

## Übersicht

Alle Änderungen betreffen das Scheduling, den Medien-Download, die Sicherheit und die UI-Darstellung von Medien-Zuständen. Neue Datei: `MediaDownloadService.java` (asynchroner Bild-Download mit Retry-Logik). Verschoben: `test-import.tsv` nach `src/test/resources/`.

---

## 1. Docker / Infrastruktur

### compose.yml
- **Mastodon-Image korrigiert**: `bitnami/mastodon:4.2.9` → `bitnamilegacy/mastodon:4.2.9` (altes Image war nicht mehr verfügbar)

### .gitignore
- `.DS_Store` in allen Verzeichnissen ignoriert (ohne führenden `/`)

---

## 2. Neue Datei: MediaDownloadService.java

Komplett neuer Service für den asynchronen Download von Bildern aus URLs (statt synchron beim Import).

**Kernfunktionen:**
- `downloadAllPending(List<UUID>)` – Queued Downloads mit zeitlichem Abstand, priorisiert nach Status-Datum
- `downloadOne(UUID)` – Einzelner Download mit Status-Tracking (PENDING → LOADING → READY/ERROR)
- `fetchAndResize(UUID)` – HTTP-Download mit automatischem Resize auf konfigurierte Maximalgröße
- `publishIfOverdue(UUID)` – Veröffentlicht Medien sofort, falls der geplante Zeitpunkt bereits überschritten ist
- `openConnection(String)` – HTTP-Verbindung mit manuellem Redirect-Handling (max. 5 Redirects)

**Sicherheit:**
- `validateUrl(String)` – SSRF-Schutz: nur HTTPS erlaubt, private/interne IPs geblockt (Loopback, Link-Local, Site-Local, Any-Local)
- `BoundedInputStream` (innere Klasse) – begrenzt Downloads auf 10 MB, schützt gegen Decompression Bombs

**Fehlerbehandlung:**
- Download-Retries: max. 3 Versuche mit steigenden Wartezeiten (10s, 30s, 90s)
- Publish-Retries: gleiche Strategie bei fehlgeschlagener Mastodon-Veröffentlichung
- Sauberes Shutdown via `@PreDestroy` mit 10s Timeout

**Ressourcen-Management:**
- InputStream in try-with-resources
- HttpURLConnection in try/finally mit `disconnect()`
- Graphics2D in try/finally mit `dispose()`

---

## 3. Medien-Entity: LoadStatus

### MediaEntity.java
- **Neues Enum `LoadStatus`**: `PENDING`, `LOADING`, `READY`, `ERROR`
- **Neue Felder**: `sourceUrl` (Quell-URL), `loadStatus` (Download-Status, als String persistiert)

---

## 4. Import-Refactoring

### ImportService.java
- **Synchroner Bild-Download entfernt**: `importMedia()` lädt Bilder nicht mehr selbst herunter
- Stattdessen: setzt nur `sourceUrl` und `loadStatus = PENDING`
- Entfernte Imports: `BufferedImage`, `ImageIO`, `URL`, `ByteArrayOutputStream`, Bild-Konfig-Konstanten
- Entfernte Logik: Skalierung, Content-Type-Prüfung, "Scaled down"-Beschreibung

### ImportController.java
- **Async-Download-Trigger**: Nach dem Speichern der Gruppe werden alle PENDING-Medien gesammelt und an `MediaDownloadService.downloadAllPending()` übergeben

---

## 5. TimerService – Überarbeitung

### Thread-Sicherheit
- `HashMap` → `ConcurrentHashMap` für `scheduledMedia` und `scheduledStatus`
- `static` entfernt – Maps und Scheduler sind jetzt Instanz-Felder
- `computeIfAbsent` statt manueller Contains-Prüfung (verhindert Race Conditions bei doppeltem Scheduling)

### Logging
- SLF4J-Logger hinzugefügt
- Logging bei: Scheduling, erfolgreicher Veröffentlichung, Fehlern, Retry-Versuchen, Init, Shutdown

### Fehlerbehandlung
- Status-Publish: try/catch mit `TransactionTemplate` in der Lambda (Exceptions werden nicht mehr vom Scheduler verschluckt)
- Media-Publish: Neue Methode `scheduleMediaPublish()` mit Retry-Logik (10s Intervall, max. 30 Versuche) – wartet auf Download-Abschluss

### Scheduling-Guard korrigiert
- **Vorher**: `delay > AUTODONE_SCHEDULING` – Tasks innerhalb der letzten 30s vor Fälligkeit wurden nach einem Gruppen-Save nicht erneut gescheduled
- **Nachher**: `delay > 0` – alle noch nicht fälligen Tasks werden gescheduled

### Cancel-Verhalten
- `future.cancel(true)` → `future.cancel(false)` (kein Interrupt laufender Tasks)

### Sauberes Shutdown
- `@PreDestroy` mit `scheduler.shutdownNow()` und `awaitTermination(10s)`

---

## 6. ServerService – Domain-Fix

### ServerService.java
- **Domain-Überschreibung verhindert**: Nach `copyFields(server, metadata(domain))` wird `server.domain = domain` explizit gesetzt
- **Problem**: Mastodon meldet in `/api/v2/instance` seine eigene Domain (z.B. `localhost:3000`), die per `copyFields` die vom Nutzer eingegebene Domain überschrieb

---

## 7. StatusService – Null-sichere Media-IDs

### StatusService.java
- `media_ids` werden jetzt gefiltert: nur Medien mit vorhandener `id` (= bereits bei Mastodon hochgeladen) werden übergeben
- Leere `media_ids`-Liste wird nicht mehr gesendet

---

## 8. MediaService – Download-Guard

### MediaService.java
- `publish()` prüft jetzt: `file == null` oder `loadStatus == PENDING/LOADING` → überspringt Veröffentlichung (statt Fehler)

---

## 9. SecurityConfig – H2-Console im Testprofil

### SecurityConfig.java
- `/h2-console/**` wird im `test`-Profil als öffentlich zugänglich konfiguriert

---

## 10. UI-Änderungen

### forms/group.html – Enable-Button mit Download-Sperre
- Enable-Checkbox wird deaktiviert, solange Medien noch heruntergeladen werden (`PENDING`/`LOADING`) und die Gruppe nicht bereits aktiviert ist
- Warntext in Orange: "(media still downloading...)"
- Normaler Text wenn keine Downloads laufen: "(activates scheduled submissions)"

### entity/group.html – Auto-Refresh bei laufenden Downloads
- `<meta http-equiv="refresh" content="5">` wird bedingt gerendert wenn `mediaLoading` true ist
- Seite lädt sich alle 5s automatisch neu bis alle Downloads abgeschlossen sind
- Kein JavaScript nötig (reine HTML-Lösung)

### forms/media.html – Status-abhängige Vorschau
- **READY/null**: Bild-Vorschau wie bisher
- **PENDING**: Uhr-Icon mit "Waiting for download..." und Quell-URL
- **LOADING**: Spinner mit "Downloading..."
- **ERROR**: Warndreiecks-Icon mit "Download failed" und Quell-URL

### tables/media.html – Status-abhängige Kartenansicht
- Gleiche Logik wie forms/media.html, angepasst für Card-Layout
- Zusätzlich: Quell-URL als Truncated-Text unter der Karte (nur wenn nicht READY)

---

## 11. GroupController – mediaLoading-Attribut

### GroupController.java
- Neues Model-Attribut `mediaLoading`: prüft ob irgendein Medium der Gruppe den Status `PENDING` oder `LOADING` hat
- Wird in `forms/group.html` und `entity/group.html` verwendet

---

## 12. Verschobene/neue Dateien

- `test-import.tsv` → `src/test/resources/test-import.tsv` (Maven-Testressourcen)
- `MediaDownloadService.java` (neu, untracked)

---

## Zusammenfassung der Kategorien

| Kategorie | Dateien |
|---|---|
| Bugfix | ServerService, TimerService (Scheduling-Guard), StatusService |
| Feature | MediaDownloadService (neu), MediaEntity (LoadStatus), ImportService/Controller (async Download) |
| Sicherheit | MediaDownloadService (SSRF-Schutz, BoundedInputStream) |
| Robustheit | TimerService (ConcurrentHashMap, Logging, Retry, Shutdown), MediaDownloadService (Retry, Ressourcen) |
| UI | forms/group.html, entity/group.html, forms/media.html, tables/media.html |
| Infrastruktur | compose.yml, .gitignore, SecurityConfig |
2 changes: 1 addition & 1 deletion compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ services:

mastodon:
<<: *service
image: docker.io/bitnami/mastodon:4.2.9
image: docker.io/bitnamilegacy/mastodon:4.2.9
ports: [3000:3000]

postgres:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ public SecurityFilterChain securityFilterChain(ClientRepository repository, Http
authorizeHttpRequests.requestMatchers("/webjars/**").permitAll();
authorizeHttpRequests.requestMatchers(DEFAULT_LOGIN_PAGE_URL).permitAll();
paths.forEach((path) -> authorizeHttpRequests.requestMatchers(path).permitAll());

if (getEnvironment().matchesProfiles("test")) {
authorizeHttpRequests.requestMatchers("/h2-console/**").permitAll();
}

authorizeHttpRequests.anyRequest().authenticated();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import static de.uoc.dh.idh.autodone.entities.MediaEntity.LoadStatus.LOADING;
import static de.uoc.dh.idh.autodone.entities.MediaEntity.LoadStatus.PENDING;

import de.uoc.dh.idh.autodone.entities.GroupEntity;
import de.uoc.dh.idh.autodone.entities.StatusEntity;
import de.uoc.dh.idh.autodone.services.GroupService;
Expand Down Expand Up @@ -64,9 +67,15 @@ public String get(Model model, @RequestParam() Map<String, String> params) {
var status = mapFields(of("group", group), new StatusEntity());
var page = statusService.getPage(params.get("page"), params.get("sort"), group);

var mediaLoading = group.status != null && group.status.stream()
.filter(s -> s.media != null)
.flatMap(s -> s.media.stream())
.anyMatch(m -> m.loadStatus == PENDING || m.loadStatus == LOADING);

model.addAttribute("group", group);
model.addAttribute("status", status);
model.addAttribute("page", page);
model.addAttribute("mediaLoading", mediaLoading);
return "entity/group";
} else {
var group = new GroupEntity();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
import static de.uoc.dh.idh.autodone.utils.ObjectUtils.mapFields;
import static de.uoc.dh.idh.autodone.utils.WebUtils.href;

import static de.uoc.dh.idh.autodone.entities.MediaEntity.LoadStatus.PENDING;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Map;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
Expand All @@ -21,6 +25,7 @@
import de.uoc.dh.idh.autodone.entities.GroupEntity;
import de.uoc.dh.idh.autodone.services.GroupService;
import de.uoc.dh.idh.autodone.services.ImportService;
import de.uoc.dh.idh.autodone.services.MediaDownloadService;
import de.uoc.dh.idh.autodone.services.StatusService;
import jakarta.servlet.http.HttpSession;

Expand All @@ -37,6 +42,9 @@ public class ImportController {
@Autowired()
private ImportService importService;

@Autowired()
private MediaDownloadService mediaDownloadService;

@Autowired()
private StatusService statusService;

Expand Down Expand Up @@ -83,6 +91,26 @@ public String post(@RequestBody() MultipartFile file, @RequestParam() Map<String
httpSession.removeAttribute("import");

var save = groupService.save(mapFields(form, group, FORCE));

// Collect media UUIDs with PENDING status and trigger async download
var pendingMedia = new ArrayList<UUID>();

if (save.status != null) {
for (var status : save.status) {
if (status.media != null) {
for (var media : status.media) {
if (media.loadStatus == PENDING) {
pendingMedia.add(media.uuid);
}
}
}
}
}

if (!pendingMedia.isEmpty()) {
mediaDownloadService.downloadAllPending(pendingMedia);
}

return "redirect:/group?uuid=" + save.uuid;
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/de/uoc/dh/idh/autodone/entities/MediaEntity.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.uoc.dh.idh.autodone.entities;

import static jakarta.persistence.EnumType.STRING;
import static jakarta.persistence.GenerationType.UUID;
import static java.util.Base64.getEncoder;

Expand All @@ -10,6 +11,7 @@

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
Expand All @@ -20,6 +22,12 @@
@JsonNaming(SnakeCaseStrategy.class)
public class MediaEntity {

public enum LoadStatus {
PENDING, LOADING, READY, ERROR
}

//

@Id()
@GeneratedValue(strategy = UUID)
public UUID uuid;
Expand All @@ -43,6 +51,13 @@ public class MediaEntity {
@Column()
public String id;

@Column()
public String sourceUrl;

@Enumerated(STRING)
@Column()
public LoadStatus loadStatus;

@Column()
public String url;

Expand Down
Loading