summaryrefslogtreecommitdiff
path: root/src/main/java/com/keuin/kbackupfabric/backup
diff options
context:
space:
mode:
authorKeuin <[email protected]>2021-01-23 14:10:32 +0800
committerkeuin <[email protected]>2021-01-23 14:10:32 +0800
commit4a1d885afa7217b47d6183488c3dc6537cef05b6 (patch)
tree1b499db6b834cb0709029e30c0d52c0ddf200ffa /src/main/java/com/keuin/kbackupfabric/backup
parent4ac575330130ac4e1b4b35386ffc0aacd431a5a4 (diff)
Version 1.4.6 (preview): added metadata for incremental backup (need integrated test and display implementation)
Diffstat (limited to 'src/main/java/com/keuin/kbackupfabric/backup')
-rw-r--r--src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollection2.java (renamed from src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollection.java)19
-rw-r--r--src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionConverter.java30
-rw-r--r--src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionFactory.java8
-rw-r--r--src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionSerializer.java17
-rw-r--r--src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectElementConverter.java18
-rw-r--r--src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/Sha256IdentifierConverter.java14
-rw-r--r--src/main/java/com/keuin/kbackupfabric/backup/incremental/manager/IncCopyResult.java105
-rw-r--r--src/main/java/com/keuin/kbackupfabric/backup/incremental/manager/IncrementalBackupStorageManager.java38
-rw-r--r--src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/IncBackupInfoSerializer.java65
-rw-r--r--src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/SavedIncBackupV0.java64
-rw-r--r--src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/SavedIncBackupV1.java100
-rw-r--r--src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/SavedIncrementalBackup.java62
12 files changed, 511 insertions, 29 deletions
diff --git a/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollection.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollection2.java
index 2d07fb4..8d8eb14 100644
--- a/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollection.java
+++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollection2.java
@@ -3,12 +3,17 @@ package com.keuin.kbackupfabric.backup.incremental;
import java.io.Serializable;
import java.util.*;
-public class ObjectCollection implements Serializable {
+/**
+ * This class must be in package `com.keuin.kbackupfabric.util.backup.incremental.ObjectCollection`,
+ * or it will not be compatible with old backups.
+ */
+public class ObjectCollection2 implements Serializable {
+
private final String name;
private final Map<String, ObjectElement> elements;
- private final Map<String, ObjectCollection> subCollections;
+ private final Map<String, ObjectCollection2> subCollections;
- ObjectCollection(String name, Set<ObjectElement> elements, Map<String, ObjectCollection> subCollections) {
+ ObjectCollection2(String name, Set<ObjectElement> elements, Map<String, ObjectCollection2> subCollections) {
this.name = Objects.requireNonNull(name);
this.elements = new HashMap<>();
for (ObjectElement e : elements) {
@@ -36,15 +41,15 @@ public class ObjectCollection implements Serializable {
return elements.get(name);
}
- public Set<ObjectCollection> getSubCollectionSet() {
+ public Set<ObjectCollection2> getSubCollectionSet() {
return new HashSet<>(subCollections.values());
}
- public Map<String, ObjectCollection> getSubCollectionMap() {
+ public Map<String, ObjectCollection2> getSubCollectionMap() {
return Collections.unmodifiableMap(subCollections);
}
- public ObjectCollection getSubCollection(String name) {
+ public ObjectCollection2 getSubCollection(String name) {
return subCollections.get(name);
}
@@ -52,7 +57,7 @@ public class ObjectCollection implements Serializable {
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
- ObjectCollection that = (ObjectCollection) o;
+ ObjectCollection2 that = (ObjectCollection2) o;
return name.equals(that.name) &&
elements.equals(that.elements) &&
subCollections.equals(that.subCollections);
diff --git a/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionConverter.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionConverter.java
new file mode 100644
index 0000000..4e8a379
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionConverter.java
@@ -0,0 +1,30 @@
+package com.keuin.kbackupfabric.backup.incremental;
+
+import com.keuin.kbackupfabric.util.backup.incremental.ObjectCollection;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Convert legacy `ObjectCollection` (keep for backward-compatibility after refactoring the code)
+ * to new `ObjectCollection2`.
+ */
+public class ObjectCollectionConverter {
+ /**
+ * Convert legacy `ObjectCollection` (keep for backward-compatibility after refactoring the code)
+ * to new `ObjectCollection2`.
+ *
+ * @param objectCollection old instance.
+ * @return new instance.
+ */
+ public static ObjectCollection2 convert(ObjectCollection objectCollection) {
+ Map<String, ObjectCollection> oldSubCollectionMap = objectCollection.getSubCollectionMap();
+ Map<String, ObjectCollection2> convertedSubCollectionMap = new HashMap<>(oldSubCollectionMap.size());
+ oldSubCollectionMap.forEach((s, c) -> convertedSubCollectionMap.put(s, convert(c)));
+ Set<ObjectElement> convertedElementSet = new HashSet<>();
+ objectCollection.getElementSet().forEach(oldElement -> convertedElementSet.add(ObjectElementConverter.convert(oldElement)));
+ return new ObjectCollection2(objectCollection.getName(), convertedElementSet, convertedSubCollectionMap);
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionFactory.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionFactory.java
index 17eddaf..9b1a226 100644
--- a/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionFactory.java
+++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionFactory.java
@@ -37,9 +37,9 @@ public class ObjectCollectionFactory<T extends ObjectIdentifier> {
throw new IllegalArgumentException("minParallelProcessFileCountThreshold must not be negative.");
}
- public ObjectCollection fromDirectory(File directory, Set<String> ignoredFiles) throws IOException {
+ public ObjectCollection2 fromDirectory(File directory, Set<String> ignoredFiles) throws IOException {
- final Map<String, ObjectCollection> subCollections = new HashMap<>();
+ final Map<String, ObjectCollection2> subCollections = new HashMap<>();
if (!Objects.requireNonNull(directory).isDirectory())
throw new IllegalArgumentException("given file is not a directory");
@@ -92,10 +92,10 @@ public class ObjectCollectionFactory<T extends ObjectIdentifier> {
}
}
- return new ObjectCollection(directory.getName(), subFiles, subCollections);
+ return new ObjectCollection2(directory.getName(), subFiles, subCollections);
}
- public ObjectCollection fromDirectory(File directory) throws IOException {
+ public ObjectCollection2 fromDirectory(File directory) throws IOException {
return fromDirectory(directory, Collections.emptySet());
}
diff --git a/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionSerializer.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionSerializer.java
index f45d4d0..f663f20 100644
--- a/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionSerializer.java
+++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectCollectionSerializer.java
@@ -5,14 +5,20 @@ import java.util.Objects;
/**
* Serialize and deserialize ObjectCollection from/to the disk file.
+ * Now we want to save additional metadata in incremental backups. So the serializer on pure ObjectCollection is depreciated.
*/
public class ObjectCollectionSerializer {
- public static ObjectCollection fromFile(File file) throws IOException {
+
+ /**
+ * This doesn't work with the latest format. Use IncBackupInfoSerializer instead.
+ */
+ @Deprecated
+ public static ObjectCollection2 fromFile(File file) throws IOException {
Objects.requireNonNull(file);
- ObjectCollection collection;
+ ObjectCollection2 collection;
try (FileInputStream fileInputStream = new FileInputStream(file)) {
try (ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
- collection = (ObjectCollection) objectInputStream.readObject();
+ collection = (ObjectCollection2) objectInputStream.readObject();
} catch (ClassNotFoundException ignored) {
// this should not happen
return null;
@@ -21,7 +27,10 @@ public class ObjectCollectionSerializer {
return collection;
}
- public static void toFile(ObjectCollection collection, File file) throws IOException {
+ /**
+ * Only used for testing backward-compatibility with legacy backups.
+ */
+ public static void toFile(ObjectCollection2 collection, File file) throws IOException {
Objects.requireNonNull(collection);
Objects.requireNonNull(file);
try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
diff --git a/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectElementConverter.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectElementConverter.java
new file mode 100644
index 0000000..512c2d2
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/ObjectElementConverter.java
@@ -0,0 +1,18 @@
+package com.keuin.kbackupfabric.backup.incremental;
+
+import com.keuin.kbackupfabric.backup.incremental.identifier.Sha256IdentifierConverter;
+import com.keuin.kbackupfabric.util.backup.incremental.identifier.Sha256Identifier;
+
+public class ObjectElementConverter {
+ public static ObjectElement convert(com.keuin.kbackupfabric.util.backup.incremental.ObjectElement oldObjectElement) {
+ try {
+ return new ObjectElement(
+ oldObjectElement.getName(),
+ // in real world case, Sha256Identifier is the only used identifier in KBackup. So the cast is surely safe
+ Sha256IdentifierConverter.convert((Sha256Identifier) oldObjectElement.getIdentifier())
+ );
+ } catch (IllegalAccessException | NoSuchFieldException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/Sha256IdentifierConverter.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/Sha256IdentifierConverter.java
new file mode 100644
index 0000000..a8ec77c
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/identifier/Sha256IdentifierConverter.java
@@ -0,0 +1,14 @@
+package com.keuin.kbackupfabric.backup.incremental.identifier;
+
+import com.keuin.kbackupfabric.util.backup.incremental.identifier.SingleHashIdentifier;
+
+import java.lang.reflect.Field;
+
+public class Sha256IdentifierConverter {
+ public static Sha256Identifier convert(com.keuin.kbackupfabric.util.backup.incremental.identifier.Sha256Identifier old) throws NoSuchFieldException, IllegalAccessException {
+ Field field = ((SingleHashIdentifier) old).getClass().getSuperclass().getDeclaredField("hash");
+ field.setAccessible(true);
+ byte[] hash = (byte[]) field.get(old);
+ return new Sha256Identifier(hash);
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/backup/incremental/manager/IncCopyResult.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/manager/IncCopyResult.java
new file mode 100644
index 0000000..6011ea5
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/manager/IncCopyResult.java
@@ -0,0 +1,105 @@
+package com.keuin.kbackupfabric.backup.incremental.manager;
+
+import com.keuin.kbackupfabric.backup.BackupFilesystemUtil;
+
+import java.util.Objects;
+
+/**
+ * Returned by `addObjectCollection` in IncrementalBackupStorageManager.
+ * Immutable.
+ */
+public class IncCopyResult {
+
+ private final int totalFiles;
+ private final int filesCopied;
+ private final long bytesCopied;
+ private final long bytesTotal;
+
+ public static final IncCopyResult ZERO = new IncCopyResult(0, 0, 0, 0);
+
+ public IncCopyResult(int totalFiles, int filesCopied, long bytesCopied, long bytesTotal) {
+ this.totalFiles = totalFiles;
+ this.filesCopied = filesCopied;
+ this.bytesCopied = bytesCopied;
+ this.bytesTotal = bytesTotal;
+ }
+
+ /**
+ * Get total files in the collection, containing reused files.
+ *
+ * @return file count.
+ */
+ public int getTotalFiles() {
+ return totalFiles;
+ }
+
+ /**
+ * Get new files added to the base.
+ *
+ * @return file count.
+ */
+ public int getFilesCopied() {
+ return filesCopied;
+ }
+
+ /**
+ * Get total bytes of new files added to the base.
+ *
+ * @return bytes.
+ */
+ public long getBytesCopied() {
+ return bytesCopied;
+ }
+
+ /**
+ * Get total bytes of all files in the collection. This equals to copied_files_bytes + reused_files_bytes.
+ *
+ * @return bytes.
+ */
+ public long getBytesTotal() {
+ return bytesTotal;
+ }
+
+ /**
+ * Add with another AddResult.
+ *
+ * @param a object.
+ * @return the add result.
+ */
+ public IncCopyResult addWith(IncCopyResult a) {
+ Objects.requireNonNull(a);
+ return new IncCopyResult(
+ totalFiles + a.totalFiles,
+ filesCopied + a.filesCopied,
+ bytesCopied + a.bytesCopied,
+ bytesTotal + a.bytesTotal
+ );
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ IncCopyResult that = (IncCopyResult) o;
+ return totalFiles == that.totalFiles &&
+ filesCopied == that.filesCopied &&
+ bytesCopied == that.bytesCopied &&
+ bytesTotal == that.bytesTotal;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(totalFiles, filesCopied, bytesCopied, bytesTotal);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "Files copied: %d (%s in size, totally %d files). Total file tree size: %s.",
+ filesCopied,
+ BackupFilesystemUtil.getFriendlyFileSizeString(bytesCopied),
+ totalFiles,
+ BackupFilesystemUtil.getFriendlyFileSizeString(bytesTotal)
+ );
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/backup/incremental/manager/IncrementalBackupStorageManager.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/manager/IncrementalBackupStorageManager.java
index 945fcc4..78c6943 100644
--- a/src/main/java/com/keuin/kbackupfabric/backup/incremental/manager/IncrementalBackupStorageManager.java
+++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/manager/IncrementalBackupStorageManager.java
@@ -1,17 +1,18 @@
package com.keuin.kbackupfabric.backup.incremental.manager;
-import com.keuin.kbackupfabric.backup.incremental.ObjectCollection;
+import com.keuin.kbackupfabric.backup.incremental.ObjectCollection2;
import com.keuin.kbackupfabric.backup.incremental.ObjectElement;
import com.keuin.kbackupfabric.backup.incremental.identifier.ObjectIdentifier;
import com.keuin.kbackupfabric.backup.incremental.identifier.StorageObjectLoader;
+import com.keuin.kbackupfabric.util.FilesystemUtil;
import com.keuin.kbackupfabric.util.PrintUtil;
+import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.*;
import static org.apache.commons.io.FileUtils.forceDelete;
@@ -28,11 +29,14 @@ public class IncrementalBackupStorageManager {
/**
* Add a object collection to storage base.
+ *
* @param collection the collection.
* @return objects copied to the base.
* @throws IOException I/O Error.
*/
- public int addObjectCollection(ObjectCollection collection, File collectionBasePath) throws IOException {
+ public @Nullable
+ IncCopyResult addObjectCollection(ObjectCollection2 collection, File collectionBasePath) throws IOException {
+ // TODO: add failure detection
if (!backupStorageBase.toFile().isDirectory()) {
if (!backupStorageBase.toFile().mkdirs())
throw new IOException("Backup storage base directory does not exist, and failed to create it.");
@@ -40,22 +44,28 @@ public class IncrementalBackupStorageManager {
Objects.requireNonNull(collection);
Objects.requireNonNull(collectionBasePath);
- int copyCount = 0;
+ IncCopyResult copyCount = IncCopyResult.ZERO;
// copy sub files
for (Map.Entry<String, ObjectElement> entry : collection.getElementMap().entrySet()) {
File copyDestination = new File(backupStorageBase.toFile(), entry.getValue().getIdentifier().getIdentification());
+ File copySourceFile = new File(collectionBasePath.getAbsolutePath(), entry.getKey());
+ final long fileBytes = FilesystemUtil.getFileSizeBytes(copySourceFile.getAbsolutePath());
if (!baseContainsObject(entry.getValue())) {
// element does not exist. copy.
- Files.copy(Paths.get(collectionBasePath.getAbsolutePath(), entry.getKey()), copyDestination.toPath());
- ++copyCount;
+ Files.copy(copySourceFile.toPath(), copyDestination.toPath());
+ copyCount = copyCount.addWith(new IncCopyResult(1, 1, fileBytes, fileBytes));
+ }
+ {
+ // element exists (file reused). Just update the stat info
+ copyCount = copyCount.addWith(new IncCopyResult(1, 1, 0, fileBytes));
}
}
//copy sub dirs recursively
- for (Map.Entry<String, ObjectCollection> entry : collection.getSubCollectionMap().entrySet()) {
+ for (Map.Entry<String, ObjectCollection2> entry : collection.getSubCollectionMap().entrySet()) {
File newBase = new File(collectionBasePath, entry.getKey());
- copyCount += addObjectCollection(entry.getValue(), newBase);
+ copyCount = copyCount.addWith(addObjectCollection(entry.getValue(), newBase));
}
return copyCount;
@@ -63,12 +73,13 @@ public class IncrementalBackupStorageManager {
/**
* Restore an object collection from the storage base. i.e., restore the save from backup storage.
- * @param collection the collection to be restored.
+ *
+ * @param collection the collection to be restored.
* @param collectionBasePath save path of the collection.
* @return objects restored from the base.
* @throws IOException I/O Error.
*/
- public int restoreObjectCollection(ObjectCollection collection, File collectionBasePath) throws IOException {
+ public int restoreObjectCollection(ObjectCollection2 collection, File collectionBasePath) throws IOException {
Objects.requireNonNull(collection);
Objects.requireNonNull(collectionBasePath);
@@ -122,7 +133,7 @@ public class IncrementalBackupStorageManager {
}
//copy sub dirs recursively
- for (Map.Entry<String, ObjectCollection> entry : collection.getSubCollectionMap().entrySet()) {
+ for (Map.Entry<String, ObjectCollection2> entry : collection.getSubCollectionMap().entrySet()) {
File newBase = new File(collectionBasePath, entry.getKey());
copyCount += restoreObjectCollection(entry.getValue(), newBase);
}
@@ -130,12 +141,12 @@ public class IncrementalBackupStorageManager {
return copyCount;
}
- public int cleanUnusedObjects(Iterable<ObjectCollection> collectionIterable) {
+ public int cleanUnusedObjects(Iterable<ObjectCollection2> collectionIterable) {
// construct object list in memory
Set<String> objects = new HashSet<>();
// backupStorageBase
- for (ObjectCollection collection : collectionIterable) {
+ for (ObjectCollection2 collection : collectionIterable) {
for (ObjectElement ele : collection.getElementMap().values()) {
}
@@ -185,5 +196,4 @@ public class IncrementalBackupStorageManager {
map.put(identifier, file);
});
}
-
}
diff --git a/src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/IncBackupInfoSerializer.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/IncBackupInfoSerializer.java
new file mode 100644
index 0000000..45590ba
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/IncBackupInfoSerializer.java
@@ -0,0 +1,65 @@
+package com.keuin.kbackupfabric.backup.incremental.serializer;
+
+import com.keuin.kbackupfabric.backup.incremental.ObjectCollection2;
+import com.keuin.kbackupfabric.backup.incremental.ObjectCollectionConverter;
+import com.keuin.kbackupfabric.backup.name.BackupFileNameEncoder;
+import com.keuin.kbackupfabric.backup.name.IncrementalBackupFileNameEncoder;
+import com.keuin.kbackupfabric.util.backup.incremental.ObjectCollection;
+
+import java.io.*;
+import java.util.Objects;
+
+public class IncBackupInfoSerializer {
+ /**
+ * Load incremental backup index file into object, no matter what version it is.
+ *
+ * @param file a valid incremental backup file. (with a valid file name)
+ * @return the object. Not null.
+ * @throws IOException when failed due to an I/O error.
+ */
+ public static SavedIncrementalBackup fromFile(File file) throws IOException {
+ Objects.requireNonNull(file);
+ try (FileInputStream fileInputStream = new FileInputStream(file)) {
+ try (ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
+ Object o = objectInputStream.readObject();
+ if (o instanceof SavedIncrementalBackup) {
+ return (SavedIncrementalBackup) o;
+ } else if (o instanceof ObjectCollection) {
+ // backward compatibility with old-style (v0) incremental backup
+ BackupFileNameEncoder.BackupBasicInformation info = new IncrementalBackupFileNameEncoder().decode(file.getName());
+ if (info == null)
+ throw new IOException("Invalid backup file name.");
+ return new SavedIncBackupV0(ObjectCollectionConverter.convert((ObjectCollection) o), info);
+ } else if (o instanceof ObjectCollection2) {
+ // compatible with 1.4.6 implementation
+ BackupFileNameEncoder.BackupBasicInformation info = new IncrementalBackupFileNameEncoder().decode(file.getName());
+ if (info == null)
+ throw new IOException("Invalid backup file name.");
+ return new SavedIncBackupV0((ObjectCollection2) o, info);
+ } else {
+ throw new RuntimeException("Unrecognized backup file format: unknown class " + o.getClass().getCanonicalName());
+ }
+ } catch (ClassNotFoundException e) {
+ // this should not happen
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /**
+ * Save incremental backup index and metadata into file.
+ *
+ * @param file the file.
+ * @param backup the backup.
+ * @throws IOException when failed due to an I/O error.
+ */
+ public static void toFile(File file, SavedIncrementalBackup backup) throws IOException {
+ Objects.requireNonNull(file);
+ Objects.requireNonNull(backup);
+ try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
+ try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {
+ objectOutputStream.writeObject(backup);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/SavedIncBackupV0.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/SavedIncBackupV0.java
new file mode 100644
index 0000000..8b4a4a1
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/SavedIncBackupV0.java
@@ -0,0 +1,64 @@
+package com.keuin.kbackupfabric.backup.incremental.serializer;
+
+import com.keuin.kbackupfabric.backup.incremental.ObjectCollection2;
+import com.keuin.kbackupfabric.backup.name.BackupFileNameEncoder;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Objects;
+
+/**
+ * The old-style incremental backup. Just to keep backward compatibility with old backups.
+ */
+public class SavedIncBackupV0 implements SavedIncrementalBackup {
+
+ private final ObjectCollection2 objectCollection2;
+ private final String backupName;
+ private final LocalDateTime namedBackupTime;
+
+ public SavedIncBackupV0(ObjectCollection2 objectCollection2, BackupFileNameEncoder.BackupBasicInformation backupBasicInformation) {
+ Objects.requireNonNull(objectCollection2);
+ Objects.requireNonNull(backupBasicInformation);
+
+ this.objectCollection2 = objectCollection2;
+ this.backupName = backupBasicInformation.customName;
+ this.namedBackupTime = backupBasicInformation.time;
+ }
+
+
+ @Override
+ public ObjectCollection2 getObjectCollection() {
+ return objectCollection2;
+ }
+
+ @Override
+ public String getBackupName() {
+ return backupName;
+ }
+
+ @Override
+ public ZonedDateTime getBackupTime() {
+ return namedBackupTime.atZone(ZoneId.systemDefault());
+ }
+
+ @Override
+ public int getFilesAdded() {
+ return -1; // missing info
+ }
+
+ @Override
+ public long getTotalSizeBytes() {
+ return -1; // missing info
+ }
+
+ @Override
+ public long getIncreasedSizeBytes() {
+ return -1; // missing info
+ }
+
+ @Override
+ public String toString() {
+ return String.format("(Legacy Backup) %s, created at %s", backupName, namedBackupTime);
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/SavedIncBackupV1.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/SavedIncBackupV1.java
new file mode 100644
index 0000000..0ebe06a
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/SavedIncBackupV1.java
@@ -0,0 +1,100 @@
+package com.keuin.kbackupfabric.backup.incremental.serializer;
+
+import com.keuin.kbackupfabric.backup.BackupFilesystemUtil;
+import com.keuin.kbackupfabric.backup.incremental.ObjectCollection2;
+
+import java.io.Serializable;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Objects;
+
+public class SavedIncBackupV1 implements SavedIncrementalBackup, Serializable {
+
+ private final ObjectCollection2 objectCollection2;
+ private final String backupName;
+ private final ZonedDateTime backupTime;
+ private final long totalSizeBytes;
+ private final long increasedSizeBytes;
+ private final int filesAdded;
+ private final int totalFiles;
+ private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
+
+ public SavedIncBackupV1(ObjectCollection2 objectCollection2, String backupName, ZonedDateTime backupTime, long totalSizeBytes, long increasedSizeBytes, int filesAdded, int totalFiles) {
+ this.totalFiles = totalFiles;
+ Objects.requireNonNull(objectCollection2);
+ Objects.requireNonNull(backupName);
+ Objects.requireNonNull(backupTime);
+ this.objectCollection2 = objectCollection2;
+ this.backupName = backupName;
+ this.backupTime = backupTime;
+ this.totalSizeBytes = totalSizeBytes;
+ this.increasedSizeBytes = increasedSizeBytes;
+ this.filesAdded = filesAdded;
+ }
+
+ @Override
+ public ObjectCollection2 getObjectCollection() {
+ return objectCollection2;
+ }
+
+ @Override
+ public String getBackupName() {
+ return backupName;
+ }
+
+ @Override
+ public ZonedDateTime getBackupTime() {
+ return backupTime;
+ }
+
+ @Override
+ public int getFilesAdded() {
+ return filesAdded;
+ }
+
+ @Override
+ public long getTotalSizeBytes() {
+ return totalSizeBytes;
+ }
+
+ @Override
+ public long getIncreasedSizeBytes() {
+ return increasedSizeBytes;
+ }
+
+ public int getTotalFiles() {
+ return totalFiles;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SavedIncBackupV1 that = (SavedIncBackupV1) o;
+ return totalSizeBytes == that.totalSizeBytes &&
+ increasedSizeBytes == that.increasedSizeBytes &&
+ filesAdded == that.filesAdded &&
+ totalFiles == that.totalFiles &&
+ objectCollection2.equals(that.objectCollection2) &&
+ backupName.equals(that.backupName) &&
+ backupTime.equals(that.backupTime);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(objectCollection2, backupName, backupTime, totalSizeBytes, increasedSizeBytes, filesAdded, totalFiles);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "%s, created at %s, files: %d (total size: %s), copied size: %s, files added: %d",
+ backupName,
+ backupTime.format(formatter),
+ totalFiles,
+ BackupFilesystemUtil.getFriendlyFileSizeString(totalSizeBytes),
+ BackupFilesystemUtil.getFriendlyFileSizeString(increasedSizeBytes),
+ filesAdded
+ );
+ }
+}
diff --git a/src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/SavedIncrementalBackup.java b/src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/SavedIncrementalBackup.java
new file mode 100644
index 0000000..e2e50b6
--- /dev/null
+++ b/src/main/java/com/keuin/kbackupfabric/backup/incremental/serializer/SavedIncrementalBackup.java
@@ -0,0 +1,62 @@
+package com.keuin.kbackupfabric.backup.incremental.serializer;
+
+
+import com.keuin.kbackupfabric.backup.incremental.ObjectCollection2;
+
+import java.io.Serializable;
+import java.time.ZonedDateTime;
+
+/**
+ * The abstraction of an object saved in the disk, containing all information (except binary data of files) about an incremental backup.
+ */
+public interface SavedIncrementalBackup extends Serializable {
+
+ /**
+ * Get an instance with the latest version.
+ */
+ static SavedIncrementalBackup newLatest(ObjectCollection2 objectCollection2, String backupName, ZonedDateTime backupTime, long totalSizeBytes, long increasedSizeBytes, int filesAdded, int totalFiles) {
+ return new SavedIncBackupV1(objectCollection2, backupName, backupTime, totalSizeBytes, increasedSizeBytes, filesAdded, totalFiles);
+ }
+
+ /**
+ * Get the object collection of the level directory.
+ *
+ * @return the object collection.
+ */
+ ObjectCollection2 getObjectCollection();
+
+ /**
+ * Get the custom backup name.
+ *
+ * @return the backup name.
+ */
+ String getBackupName();
+
+ /**
+ * Get the time when this backup was made.
+ *
+ * @return the time.
+ */
+ ZonedDateTime getBackupTime();
+
+ /**
+ * Get new files added to the base.
+ *
+ * @return file count.
+ */
+ int getFilesAdded();
+
+ /**
+ * Get the total size of the saved world.
+ *
+ * @return the size in bytes.
+ */
+ long getTotalSizeBytes();
+
+ /**
+ * Get the size we cost to add this backup into the base.
+ *
+ * @return the increased size in bytes.
+ */
+ long getIncreasedSizeBytes();
+}