This is an automated email from the ASF dual-hosted git repository.

szetszwo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 3a18da72b56 HDDS-15935. Extract ExportFileManager and document 
container export directory layout (#10866)
3a18da72b56 is described below

commit 3a18da72b56d3fb10f05502f91af7cea035c5862
Author: Sarveksha Yeshavantha Raju 
<[email protected]>
AuthorDate: Sun Aug 2 05:04:09 2026 +0530

    HDDS-15935. Extract ExportFileManager and document container export 
directory layout (#10866)
---
 .../org/apache/hadoop/ozone/util/UUIDUtil.java     |   9 +
 .../scm/container/export/ExportFileManager.java    | 234 +++++++++++++++++++++
 .../hdds/scm/container/export/ExportJob.java       |  74 +++++++
 .../hdds/scm/container/export/ExportScope.java     |  68 ++++++
 .../hdds/scm/container/export/package-info.java    |  21 ++
 .../container/export/TestExportFileManager.java    | 156 ++++++++++++++
 6 files changed, 562 insertions(+)

diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/util/UUIDUtil.java 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/util/UUIDUtil.java
index 8f4da0cfc46..ef1de0d6aa9 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/util/UUIDUtil.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/util/UUIDUtil.java
@@ -19,6 +19,7 @@
 
 import java.security.SecureRandom;
 import java.util.Random;
+import java.util.UUID;
 import java.util.function.Consumer;
 
 /**
@@ -47,6 +48,14 @@ private static byte[] getUUIDBytes(Consumer<byte[]> 
generator) {
     return bytes;
   }
 
+  public static boolean isValidUuidString(String value) {
+    try {
+      return value.equals(UUID.fromString(value).toString());
+    } catch (IllegalArgumentException e) {
+      return false;
+    }
+  }
+
   private UUIDUtil() {
   }
 }
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/export/ExportFileManager.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/export/ExportFileManager.java
new file mode 100644
index 00000000000..e0bca340a24
--- /dev/null
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/export/ExportFileManager.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.hdds.scm.container.export;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import org.apache.commons.io.FileUtils;
+import org.apache.hadoop.ozone.util.UUIDUtil;
+import org.apache.ratis.util.AtomicFileOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages on-disk paths and artifacts for container ID export jobs.
+ *
+ * <p>The export directory ({@code exportDirectory}, typically {@code 
{scm.db.dirs}/exports})
+ * uses the layout below. The manager gzip-compresses the archive ({@code 
.tar.gz}) so operators
+ * can stream entries with {@code zcat}.
+ *
+ * <p>While a job runs, shard text files are written under {@code 
export_{jobId}/}. The archive is
+ * created only after all shards are written. The export manager writes
+ * {@code container-ids_{scope}_{timestamp}_job{jobId}.tar.gz.tmp} and 
atomically renames it to
+ * {@code .tar.gz} on close ({@link AtomicFileOutputStream}), so a partial 
{@code .tar.gz} is
+ * never visible. {@link #lock()} uses {@code in_use.lock} to exclude 
concurrent writers.
+ *
+ * <pre>
+ * {exportDirectory}/
+ * ├── in_use.lock
+ * ├── container-ids_{scope}_{timestamp}_job{jobId}.tar.gz
+ * ├── container-ids_{scope}_{timestamp}_job{jobId}.tar.gz.tmp
+ * └── export_{jobId}/
+ *     ├── container-ids_{scope}_{metadataTimestamp}_part001.txt
+ *     └── ...
+ * </pre>
+ *
+ * <p><b>Incomplete work</b> ({@code export_{jobId}/} and {@code .tar.gz.tmp}) 
is removed by
+ * {@link #cleanupFailedJob(Path, File)} on failure or cancel, and by {@link 
#start()} for every
+ * leftover directory and temp file after SCM restart. Completed {@code 
.tar.gz} files are kept.
+ *
+ * <p><b>Completed {@code .tar.gz}</b> remains on disk until the export 
manager evicts it
+ * ({@code maxTerminalJobs} in {@code ContainerExportManager}) via {@link 
#deleteExportTar(String)}.
+ *
+ * <p><b>SCM restart:</b> in-memory job status is lost. {@link #start()} 
clears incomplete work;
+ * {@link #listCompletedArchivePaths()} returns existing {@code tarPath} 
values (oldest first);
+ * {@link #jobIdFromArchiveFileName(String)} parses {@code jobId} for 
terminal-job rebuild in
+ * {@code ContainerExportManager}.
+ */
+final class ExportFileManager {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(ExportFileManager.class);
+
+  static final String EXPORT_JOB_DIR_PREFIX = "export_";
+  static final String EXPORT_ARCHIVE_JOB_INFIX = "_job";
+  static final String EXPORT_ARCHIVE_SUFFIX = ".tar.gz";
+  static final String EXPORT_ARCHIVE_TMP_SUFFIX = EXPORT_ARCHIVE_SUFFIX + 
AtomicFileOutputStream.TMP_EXTENSION;
+  static final String EXPORT_LOCK_NAME = "in_use.lock";
+  private static final int ARCHIVE_TIMESTAMP_LENGTH = 16;
+
+  private final String exportDirectory;
+  private FileLock exportDirectoryLock;
+
+  ExportFileManager(String exportDirectory) {
+    this.exportDirectory = Objects.requireNonNull(exportDirectory, 
"exportDirectory == null");
+  }
+
+  String getExportDirectory() {
+    return exportDirectory;
+  }
+
+  void start() throws IOException {
+    Files.createDirectories(Paths.get(exportDirectory));
+    removeIncompleteWorkOnStartup();
+  }
+
+  void lock() throws IOException {
+    if (exportDirectoryLock != null) {
+      return;
+    }
+    File lockFile = new File(exportDirectory, EXPORT_LOCK_NAME);
+    RandomAccessFile lockAccessFile = new RandomAccessFile(lockFile, "rws");
+    try {
+      FileLock lock = lockAccessFile.getChannel().tryLock();
+      if (lock == null) {
+        lockAccessFile.close();
+        throw new OverlappingFileLockException();
+      }
+      exportDirectoryLock = lock;
+      LOG.debug("Acquired container export directory lock {}", 
lockFile.getAbsolutePath());
+    } catch (OverlappingFileLockException | IOException e) {
+      lockAccessFile.close();
+      throw new IOException("Failed to lock container export directory " + 
exportDirectory, e);
+    }
+  }
+
+  void unlock() throws IOException {
+    if (exportDirectoryLock == null) {
+      return;
+    }
+    exportDirectoryLock.release();
+    exportDirectoryLock.channel().close();
+    exportDirectoryLock = null;
+  }
+
+  File resolveArchiveFile(ExportScope scope, String archiveTimestamp, 
ExportJob.Id jobId) {
+    return new File(exportDirectory, String.format("container-ids_%s_%s%s%s%s",
+        scope.getValue(), archiveTimestamp, EXPORT_ARCHIVE_JOB_INFIX, 
jobId.getValue(), EXPORT_ARCHIVE_SUFFIX));
+  }
+
+  File resolveArchiveTempFile(ExportScope scope, String archiveTimestamp, 
ExportJob.Id jobId) {
+    return AtomicFileOutputStream.getTemporaryFile(resolveArchiveFile(scope, 
archiveTimestamp, jobId));
+  }
+
+  /**
+   * Returns completed archive paths ({@code tarPath} in {@code 
ExportJob.Status}), oldest first.
+   */
+  List<String> listCompletedArchivePaths() {
+    File exportDir = new File(exportDirectory);
+    File[] matches = exportDir.listFiles((dir, fileName) -> 
fileName.endsWith(EXPORT_ARCHIVE_SUFFIX)
+        && !fileName.endsWith(EXPORT_ARCHIVE_TMP_SUFFIX));
+    if (matches == null || matches.length == 0) {
+      return Collections.emptyList();
+    }
+    Arrays.sort(matches, Comparator.comparing(
+        file -> archiveTimestampFromArchiveFileName(file.getName())));
+    List<String> archivePaths = new ArrayList<>(matches.length);
+    for (File archive : matches) {
+      archivePaths.add(archive.getAbsolutePath());
+    }
+    return archivePaths;
+  }
+
+  static String archiveTimestampFromArchiveFileName(String fileName) {
+    int jobIndex = fileName.lastIndexOf(EXPORT_ARCHIVE_JOB_INFIX);
+    if (jobIndex < ARCHIVE_TIMESTAMP_LENGTH + 1
+            || !fileName.endsWith(EXPORT_ARCHIVE_SUFFIX)
+            || fileName.endsWith(EXPORT_ARCHIVE_TMP_SUFFIX)) {
+      return null;
+    }
+    return fileName.substring(jobIndex - ARCHIVE_TIMESTAMP_LENGTH, jobIndex);
+  }
+
+  static ExportJob.Id jobIdFromArchiveFileName(String fileName) {
+    if (!fileName.endsWith(EXPORT_ARCHIVE_SUFFIX)) {
+      return null;
+    }
+    String nameWithoutSuffix = fileName.substring(0, fileName.length() - 
EXPORT_ARCHIVE_SUFFIX.length());
+    int jobIndex = nameWithoutSuffix.lastIndexOf(EXPORT_ARCHIVE_JOB_INFIX);
+    if (jobIndex < 0) {
+      return null;
+    }
+    String jobId = nameWithoutSuffix.substring(jobIndex + 
EXPORT_ARCHIVE_JOB_INFIX.length());
+    return UUIDUtil.isValidUuidString(jobId) ? ExportJob.Id.of(jobId) : null;
+  }
+
+  void deleteExportTar(String tarPath) {
+    if (tarPath == null) {
+      return;
+    }
+    File archive = new File(tarPath);
+    if (archive.isFile() && FileUtils.deleteQuietly(archive)) {
+      LOG.debug("Removed container export archive: {}", archive.getName());
+    }
+    FileUtils.deleteQuietly(AtomicFileOutputStream.getTemporaryFile(archive));
+  }
+
+  void cleanupFailedJob(Path jobDir, File archiveFile) {
+    if (jobDir != null) {
+      FileUtils.deleteQuietly(jobDir.toFile());
+    }
+    if (archiveFile != null) {
+      
FileUtils.deleteQuietly(AtomicFileOutputStream.getTemporaryFile(archiveFile));
+    }
+  }
+
+  private void removeIncompleteWorkOnStartup() {
+    File exportDir = new File(exportDirectory);
+    File[] children = exportDir.listFiles();
+    if (children != null) {
+      for (File child : children) {
+        if (child.isDirectory() && jobIdFromExportDirName(child.getName()) != 
null) {
+          FileUtils.deleteQuietly(child);
+          LOG.debug("Removed incomplete container export job directory: {}", 
child.getAbsolutePath());
+        }
+      }
+    }
+    File[] tempFiles = exportDir.listFiles((dir, fileName) -> 
fileName.endsWith(EXPORT_ARCHIVE_TMP_SUFFIX));
+    if (tempFiles != null) {
+      for (File tempFile : tempFiles) {
+        if (FileUtils.deleteQuietly(tempFile)) {
+          LOG.debug("Removed incomplete container export archive temp file: 
{}", tempFile.getName());
+        }
+      }
+    }
+  }
+
+  static String exportJobDirName(ExportJob.Id jobId) {
+    return EXPORT_JOB_DIR_PREFIX + jobId.getValue();
+  }
+
+  private static ExportJob.Id jobIdFromExportDirName(String dirName) {
+    if (!dirName.startsWith(EXPORT_JOB_DIR_PREFIX)) {
+      return null;
+    }
+    String jobId = dirName.substring(EXPORT_JOB_DIR_PREFIX.length());
+    return UUIDUtil.isValidUuidString(jobId) ? ExportJob.Id.of(jobId) : null;
+  }
+}
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/export/ExportJob.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/export/ExportJob.java
new file mode 100644
index 00000000000..f9dee07eaea
--- /dev/null
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/export/ExportJob.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.hdds.scm.container.export;
+
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Container ID export job identifier.
+ */
+public final class ExportJob {
+
+  /**
+   * Unique job identifier.
+   */
+  public static final class Id {
+    private final String value;
+
+    private Id(String value) {
+      this.value = Objects.requireNonNull(value, "value == null");
+    }
+
+    public static Id newId() {
+      return new Id(UUID.randomUUID().toString());
+    }
+
+    public static Id of(String value) {
+      return new Id(value);
+    }
+
+    public String getValue() {
+      return value;
+    }
+
+    @Override
+    public String toString() {
+      return value;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (!(obj instanceof Id)) {
+        return false;
+      }
+      return value.equals(((Id) obj).value);
+    }
+
+    @Override
+    public int hashCode() {
+      return value.hashCode();
+    }
+  }
+
+  private ExportJob() {
+  }
+}
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/export/ExportScope.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/export/ExportScope.java
new file mode 100644
index 00000000000..921fdf7f588
--- /dev/null
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/export/ExportScope.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.hdds.scm.container.export;
+
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos.LifeCycleState;
+import org.apache.hadoop.hdds.scm.container.ContainerHealthState;
+
+/**
+ * Container listing filters for an export job.
+ * An export job filters containers by {@link ContainerHealthState}, {@link 
LifeCycleState} or both.
+ * Example archive name:
+ * {@code 
container-ids_health-MISSING_lifecycle-OPEN_20260101T120000Z_job{jobId}.tar.gz}
+ */
+public final class ExportScope {
+
+  private static final String ANY = "ANY";
+  private final LifeCycleState lifeCycleState;
+  private final ContainerHealthState healthState;
+  private final String value;
+
+  private ExportScope(LifeCycleState lifeCycleState, ContainerHealthState 
healthState, String value) {
+    this.lifeCycleState = lifeCycleState;
+    this.healthState = healthState;
+    this.value = value;
+  }
+
+  public static ExportScope of(LifeCycleState lifeCycleState, 
ContainerHealthState healthState) {
+    String health = healthState != null ? healthState.name() : ANY;
+    String lifecycle = lifeCycleState != null ? lifeCycleState.name() : ANY;
+    String value = "health-" + health + "_lifecycle-" + lifecycle;
+    return new ExportScope(lifeCycleState, healthState, value);
+  }
+
+  public LifeCycleState getLifeCycleState() {
+    return lifeCycleState;
+  }
+
+  public ContainerHealthState getHealthState() {
+    return healthState;
+  }
+
+  /**
+   * Stable filter name segment used in export TAR and shard file names.
+   */
+  public String getValue() {
+    return value;
+  }
+
+  @Override
+  public String toString() {
+    return value;
+  }
+}
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/export/package-info.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/export/package-info.java
new file mode 100644
index 00000000000..103c9519fca
--- /dev/null
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/export/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This package contains classes related to container export.
+ */
+package org.apache.hadoop.hdds.scm.container.export;
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/export/TestExportFileManager.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/export/TestExportFileManager.java
new file mode 100644
index 00000000000..51bb88a56f5
--- /dev/null
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/export/TestExportFileManager.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.hdds.scm.container.export;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.UUID;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos.LifeCycleState;
+import org.apache.hadoop.hdds.scm.container.ContainerHealthState;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Tests for {@link ExportFileManager}.
+ */
+public class TestExportFileManager {
+
+  @TempDir
+  private File tempDir;
+
+  private ExportFileManager fileManager;
+
+  @BeforeEach
+  public void setup() throws Exception {
+    fileManager = new ExportFileManager(tempDir.getAbsolutePath());
+    fileManager.start();
+  }
+
+  @Test
+  public void testExportScopeUsesAnyForNullFilters() {
+    assertEquals("health-MISSING_lifecycle-ANY",
+        ExportScope.of(null, ContainerHealthState.MISSING).getValue());
+    assertEquals("health-ANY_lifecycle-OPEN",
+        ExportScope.of(LifeCycleState.OPEN, null).getValue());
+  }
+
+  @Test
+  public void testResolveArchiveFile() {
+    ExportJob.Id jobId = ExportJob.Id.newId();
+    ExportScope scope = ExportScope.of(null, ContainerHealthState.MISSING);
+    File archive = fileManager.resolveArchiveFile(scope, "20260101T120000Z", 
jobId);
+    
assertTrue(archive.getName().contains("health-MISSING_lifecycle-ANY_20260101T120000Z"));
+    
assertTrue(archive.getName().endsWith(ExportFileManager.EXPORT_ARCHIVE_JOB_INFIX
 + jobId.getValue()
+        + ExportFileManager.EXPORT_ARCHIVE_SUFFIX));
+  }
+
+  @Test
+  public void testResolveArchiveTempFile() {
+    ExportJob.Id jobId = ExportJob.Id.newId();
+    ExportScope scope = ExportScope.of(null, ContainerHealthState.MISSING);
+    File tempFile = fileManager.resolveArchiveTempFile(scope, 
"20260101T120000Z", jobId);
+    
assertTrue(tempFile.getName().endsWith(ExportFileManager.EXPORT_ARCHIVE_TMP_SUFFIX));
+  }
+
+  @Test
+  public void testJobIdFromArchiveFileName() {
+    String jobId = UUID.randomUUID().toString();
+    String fileName = 
"container-ids_health-MISSING_lifecycle-ANY_20260101T120000Z"
+        + ExportFileManager.EXPORT_ARCHIVE_JOB_INFIX + jobId + 
ExportFileManager.EXPORT_ARCHIVE_SUFFIX;
+    assertEquals(ExportJob.Id.of(jobId), 
ExportFileManager.jobIdFromArchiveFileName(fileName));
+    
assertNull(ExportFileManager.jobIdFromArchiveFileName("container-ids_health-MISSING_lifecycle-ANY_20260101T120000Z"
+        + ExportFileManager.EXPORT_ARCHIVE_SUFFIX));
+  }
+
+  @Test
+  public void testArchiveTimestampFromArchiveFileName() {
+    String fileName = 
"container-ids_health-MISSING_lifecycle-ANY_20260101T120000Z"
+        + ExportFileManager.EXPORT_ARCHIVE_JOB_INFIX + UUID.randomUUID() + 
ExportFileManager.EXPORT_ARCHIVE_SUFFIX;
+    assertEquals("20260101T120000Z", 
ExportFileManager.archiveTimestampFromArchiveFileName(fileName));
+  }
+
+  @Test
+  public void testListCompletedArchivePaths() throws Exception {
+    ExportScope scope = ExportScope.of(null, ContainerHealthState.MISSING);
+    ExportJob.Id olderJobId = ExportJob.Id.newId();
+    File olderArchive = fileManager.resolveArchiveFile(scope, 
"20260101T120000Z", olderJobId);
+    assertTrue(olderArchive.createNewFile());
+    assertTrue(olderArchive.setLastModified(2_000L));
+    ExportJob.Id newerJobId = ExportJob.Id.newId();
+    File newerArchive = fileManager.resolveArchiveFile(scope, 
"20260101T120001Z", newerJobId);
+    assertTrue(newerArchive.createNewFile());
+    assertTrue(newerArchive.setLastModified(1_000L));
+    ExportJob.Id tempJobId = ExportJob.Id.newId();
+    File tempArchive = fileManager.resolveArchiveTempFile(scope, 
"20260101T120002Z", tempJobId);
+    assertTrue(tempArchive.createNewFile());
+
+    List<String> completedPaths = fileManager.listCompletedArchivePaths();
+    assertEquals(2, completedPaths.size());
+    assertEquals(olderArchive.getAbsolutePath(), completedPaths.get(0));
+    assertEquals(newerArchive.getAbsolutePath(), completedPaths.get(1));
+  }
+
+  @Test
+  public void testOrphanJobDirRemovedOnStartup() throws Exception {
+    ExportJob.Id jobId = ExportJob.Id.newId();
+    Path orphanJobDir = 
tempDir.toPath().resolve(ExportFileManager.exportJobDirName(jobId));
+    Files.createDirectories(orphanJobDir);
+
+    fileManager.start();
+
+    assertFalse(Files.exists(orphanJobDir));
+  }
+
+  @Test
+  public void testIncompleteExportArtifactsRemovedOnStartup() throws Exception 
{
+    ExportJob.Id jobId = ExportJob.Id.newId();
+    Path jobDir = 
tempDir.toPath().resolve(ExportFileManager.exportJobDirName(jobId));
+    Files.createDirectories(jobDir);
+    ExportScope scope = ExportScope.of(null, ContainerHealthState.MISSING);
+    File partialArchiveTemp = fileManager.resolveArchiveTempFile(scope, 
"20260101T000000Z", jobId);
+    assertTrue(partialArchiveTemp.createNewFile());
+
+    fileManager.start();
+
+    assertFalse(Files.exists(jobDir));
+    assertFalse(partialArchiveTemp.exists());
+  }
+
+  @Test
+  public void testOrphanJobDirDoesNotDeleteCompletedTar() throws Exception {
+    ExportJob.Id jobId = ExportJob.Id.newId();
+    ExportScope scope = ExportScope.of(null, ContainerHealthState.MISSING);
+    File completedArchive = fileManager.resolveArchiveFile(scope, 
"20260101T000000Z", jobId);
+    assertTrue(completedArchive.createNewFile());
+    Path orphanJobDir = 
tempDir.toPath().resolve(ExportFileManager.exportJobDirName(jobId));
+    Files.createDirectories(orphanJobDir);
+
+    fileManager.start();
+
+    assertTrue(completedArchive.exists());
+    assertFalse(Files.exists(orphanJobDir));
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to