ContentTextFileWriter.java
package net.sasasin.sreader.service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import net.sasasin.sreader.config.FeedReaderProperties;
import net.sasasin.sreader.domain.ContentTextFileExportTarget;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class ContentTextFileWriter {
private static final Logger logger = LoggerFactory.getLogger(ContentTextFileWriter.class);
private static final String CONTENT_HEADER_ID_PATTERN = "[0-9a-f]{32}";
private final FeedReaderProperties properties;
public ContentTextFileWriter(FeedReaderProperties properties) {
this.properties = properties;
}
public WriteResult write(ContentTextFileExportTarget target) throws IOException {
String contentHeaderId = target.contentHeaderId();
if (contentHeaderId == null || !contentHeaderId.matches(CONTENT_HEADER_ID_PATTERN)) {
throw new IllegalArgumentException("Invalid contentHeaderId: " + contentHeaderId);
}
String relativePath = contentHeaderId + ".txt";
Path outputDir = properties.textExport().outputDir().normalize();
Path outputPath = outputDir.resolve(relativePath).normalize();
if (!outputPath.startsWith(outputDir)) {
throw new IllegalArgumentException("Invalid output path: " + outputPath);
}
Files.createDirectories(outputDir);
Path temp = Files.createTempFile(outputDir, contentHeaderId + "-", ".tmp");
try {
Files.writeString(temp, content(target), StandardCharsets.UTF_8);
move(temp, outputPath);
long fileSizeBytes = Files.size(outputPath);
String sha256 = sha256(outputPath);
return new WriteResult(relativePath, fileSizeBytes, sha256);
} finally {
Files.deleteIfExists(temp);
}
}
private String content(ContentTextFileExportTarget target) {
String title = target.title() == null ? "" : target.title();
return "URL: "
+ target.url()
+ System.lineSeparator()
+ "TITLE: "
+ title
+ System.lineSeparator()
+ System.lineSeparator()
+ target.fullText()
+ System.lineSeparator();
}
private void move(Path temp, Path outputPath) throws IOException {
try {
Files.move(
temp, outputPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
logger.warn("Atomic move is not supported for content text export; falling back", e);
Files.move(temp, outputPath, StandardCopyOption.REPLACE_EXISTING);
}
}
private String sha256(Path path) throws IOException {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update(Files.readAllBytes(path));
return HexFormat.of().formatHex(digest.digest());
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 is not available", e);
}
}
public record WriteResult(String relativePath, long fileSizeBytes, String sha256) {}
}