ContentTextFileStore.java

package net.sasasin.sreader.service.text;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import net.sasasin.sreader.config.FeedReaderProperties;
import org.springframework.stereotype.Component;

/** Deletes export files only after the corresponding database transaction has committed. */
@Component
public class ContentTextFileStore {

  private static final String CONTENT_HEADER_ID_PATTERN = "[0-9a-f]{32}";

  private final FeedReaderProperties properties;

  public ContentTextFileStore(FeedReaderProperties properties) {
    this.properties = properties;
  }

  public DeleteResult deleteForHeaderId(String contentHeaderId) {
    if (contentHeaderId == null || !contentHeaderId.matches(CONTENT_HEADER_ID_PATTERN)) {
      return DeleteResult.failed("Invalid contentHeaderId: " + contentHeaderId);
    }
    Path outputDir = properties.textExport().outputDir().normalize();
    Path outputPath = outputDir.resolve(contentHeaderId + ".txt").normalize();
    if (!outputPath.startsWith(outputDir)) {
      return DeleteResult.failed("Invalid output path: " + outputPath);
    }
    try {
      return Files.deleteIfExists(outputPath) ? DeleteResult.deleted() : DeleteResult.missing();
    } catch (IOException e) {
      return DeleteResult.failed(e.getMessage());
    }
  }

  public record DeleteResult(Status status, String error) {
    public static DeleteResult deleted() {
      return new DeleteResult(Status.DELETED, null);
    }

    public static DeleteResult missing() {
      return new DeleteResult(Status.MISSING, null);
    }

    public static DeleteResult failed(String error) {
      return new DeleteResult(Status.FAILED, error);
    }
  }

  public enum Status {
    DELETED,
    MISSING,
    FAILED
  }
}