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 paths(String value) throws IOException { var result=new ArrayList(); 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();all.addAll(bin);all.addAll(logs);all.addAll(purge); for(int i=0;i 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() 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);} } }