Files
votianng/backend/src/main/java/de/stadtbote/votian/LegacyCronFiles.java
T
Sven 9de2358480 Portiere PHP-Cronjobs auf Spring-Tasks und ergänze Legacy-Tests
Migriert die weiterhin benötigten Zeitaufgaben einschließlich SOAP-Importen, Betriebsstatus und Wiederanlaufkontrolle. Aktiviert 115 native Spring-Tasks im Docker-Image ohne Cron-Daemon oder PHP und dokumentiert die entfallenen HHA-Aufrufe. Ergänzt PHP-/UI-Testwerkzeuge und aktualisiert den Projektstand auf Version 0.2.0.
2026-10-27 09:53:23 +01:00

73 lines
5.7 KiB
Java

package de.stadtbote.votian;
import org.springframework.stereotype.Component;
import org.springframework.core.env.Environment;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.channels.FileChannel;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static de.stadtbote.votian.AdminSupport.bad;
/** Native do_bzip2.php. Only explicitly mounted legacy paths are eligible, never NG document stores. */
@Component
class LegacyCronFiles {
final Environment env;
LegacyCronFiles(Environment env){this.env=env;}
static List<Path> paths(String value) throws IOException {
var result=new ArrayList<Path>();
for(String raw:value.split(",")){if(raw.isBlank())continue;Path p=Path.of(raw.strip()).toAbsolutePath().normalize();
if(p.getNameCount()<3||!Files.isDirectory(p,NOFOLLOW_LINKS)||!p.equals(p.toRealPath()))throw bad("Cron-Dateipfad muss ein eigenes vorhandenes Verzeichnis ohne symbolische Links sein.");
result.add(p);
}return result;
}
static boolean same(BasicFileAttributes a,BasicFileAttributes b){return a.size()==b.size()&&a.lastModifiedTime().equals(b.lastModifiedTime())&&Objects.equals(a.fileKey(),b.fileKey());}
static String digest(InputStream in) throws IOException {try(in){var md=java.security.MessageDigest.getInstance("SHA-256");byte[] b=new byte[65536];int n;while((n=in.read(b))!=-1)md.update(b,0,n);return HexFormat.of().formatHex(md.digest());}catch(java.security.NoSuchAlgorithmException e){throw new IllegalStateException(e);}}
static void compress(Path source) throws IOException {
var before=Files.readAttributes(source,BasicFileAttributes.class,NOFOLLOW_LINKS);if(!before.isRegularFile())return;
Path target=source.resolveSibling(source.getFileName()+".bz2");
if(Files.exists(target,NOFOLLOW_LINKS)){
if(!Files.isRegularFile(target,NOFOLLOW_LINKS))throw bad("Cron-Archivziel ist keine reguläre Datei.");
String original=digest(Files.newInputStream(source,NOFOLLOW_LINKS));
String archived=digest(new BZip2CompressorInputStream(Files.newInputStream(target,NOFOLLOW_LINKS)));
if(!original.equals(archived))throw bad("Vorhandenes Cron-Archiv passt nicht zur Quelldatei; kein Überschreiben.");
}else{
Path temp=Files.createTempFile(source.getParent(),".ng-cron-",".part");
try{
try(var input=Files.newInputStream(source,NOFOLLOW_LINKS);var output=new BZip2CompressorOutputStream(Files.newOutputStream(temp),9)){input.transferTo(output);}
try(var channel=FileChannel.open(temp,StandardOpenOption.WRITE)){channel.force(true);}
if(!same(before,Files.readAttributes(source,BasicFileAttributes.class,NOFOLLOW_LINKS)))throw bad("Quelldatei wurde während der Kompression geändert.");
// Atomic publication without overwrite; the temporary file is on the same filesystem.
Files.createLink(target,temp);
}finally{Files.deleteIfExists(temp);}
}
if(!same(before,Files.readAttributes(source,BasicFileAttributes.class,NOFOLLOW_LINKS)))throw bad("Quelldatei wurde während der Kompression geändert.");
Files.delete(source);
}
LegacyCronRunner.Result run(LocalDate today){
try{
var bin=paths(env.getProperty("app.legacy-cron.files.binlog-directories",""));
var logs=paths(env.getProperty("app.legacy-cron.files.log-directories",""));
var purge=paths(env.getProperty("app.legacy-cron.files.temporary-directories",""));
if(bin.isEmpty()&&logs.isEmpty()&&purge.isEmpty())throw bad("Keine Altdatei-Verzeichnisse für die Cron-Wartung konfiguriert.");
var all=new ArrayList<Path>();all.addAll(bin);all.addAll(logs);all.addAll(purge);
for(int i=0;i<all.size();i++)for(int j=i+1;j<all.size();j++)if(all.get(i).startsWith(all.get(j))||all.get(j).startsWith(all.get(i)))throw bad("Cron-Dateipfade dürfen sich nicht überlappen.");
int count=0;
for(Path directory:bin){List<Path> candidates;try(var stream=Files.list(directory)){candidates=stream.filter(p->Files.isRegularFile(p,NOFOLLOW_LINKS)&&p.getFileName().toString().matches("mps2-bin\\.0[0-9]+" )).toList();}
long newest=Long.MIN_VALUE;for(var p:candidates)newest=Math.max(newest,Files.getLastModifiedTime(p,NOFOLLOW_LINKS).toMillis());
for(var p:candidates)if(Files.getLastModifiedTime(p,NOFOLLOW_LINKS).toMillis()<newest){compress(p);count++;}
}
String month="_"+today.minusMonths(1).format(DateTimeFormatter.ofPattern("yyyyMM"));
for(Path directory:logs){List<Path> candidates;try(var stream=Files.list(directory)){candidates=stream.filter(p->Files.isRegularFile(p,NOFOLLOW_LINKS)&&p.getFileName().toString().contains(month)&&p.getFileName().toString().endsWith(".log")).toList();}for(var p:candidates){compress(p);count++;}}
// PHP deletes all regular files recursively in these three temporary trees; links stay untouched.
for(Path directory:purge)try(var stream=Files.walk(directory)){for(var p:stream.filter(p->Files.isRegularFile(p,NOFOLLOW_LINKS)).toList()){Files.delete(p);count++;}}
return LegacyCronRunner.Result.ok(count);
}catch(IOException e){return new LegacyCronRunner.Result("UNCERTAIN","Dateiwartung unterbrochen; Quelldateien und Archive vor Freigabe prüfen.",0);}
}
}