Version: 0.13.0
Architecture documentation for the HyperFactions data persistence system.
HyperFactions uses an interface-based storage layer with:
- Storage Interfaces - Abstract contracts for data operations
- JSON Implementations - File-based storage with pretty-printed JSON
- Async Operations - All I/O returns
CompletableFuturefor non-blocking - Data Models - Java records for immutable data structures (Faction, Zone, PlayerPower, ChunkKey) and mutable PlayerData class
- Auto-Save - Periodic saves with configurable interval
- Safe-Save - Atomic writes with SHA-256 checksums, backup recovery,
.bakauto-cleanup - Per-UUID Locking -
JsonPlayerStorageuses per-UUID locks to prevent concurrent load-modify-save race conditions (e.g., simultaneous deaths losing kill/death increments) - Migration Support - Automatic config (v1→v8) and data (v0→v1, v1→v2) format upgrades
- Backup System - GFS rotation with hourly/daily/weekly/manual/migration types
- Import Directories - Data import from ElbaphFactions, HyFactions, SimpleClaims, and FactionsX
Storage Interface Implementation
│ │
FactionStorage ────────────────► JsonFactionStorage
PlayerStorage ────────────────► JsonPlayerStorage
ZoneStorage ────────────────► JsonZoneStorage
│ │
└──────── Data Models ◄────────────┘
│
Faction, PlayerPower, PlayerData,
Zone, FactionClaim, ChunkKey, etc.
Backup System
│
BackupManager ─────────────────► ZIP archives in backups/
│ (GFS rotation: hourly, daily, weekly)
│
└── BackupMetadata ──────► Filename-encoded metadata
<server>/mods/com.hyperfactions_HyperFactions/
├── config/ # Configuration files
│ ├── factions.json # Faction gameplay settings
│ ├── server.json # Server behavior settings
│ └── ... # Other module configs
├── data/ # All data files (migrated from root in v0→v1)
│ ├── factions/ # Per-faction JSON files
│ │ └── {uuid}.json
│ ├── players/ # Per-player power data
│ │ └── {uuid}.json
│ ├── chat/ # Per-faction chat history
│ │ └── {factionId}.json
│ ├── economy/ # Per-faction treasury data
│ │ └── {factionId}.json
│ ├── zones.json # All zones in one file
│ ├── invites.json # Pending faction invites
│ ├── join_requests.json # Pending join requests
│ └── .version # Data layout version marker (currently: 1)
└── backups/ # Backup archives
├── hourly_2025-01-15_12-00-00.zip
├── daily_2025-01-15_00-00-00.zip
├── weekly_2025-01-13_00-00-00.zip
├── manual_my-backup.zip
└── migration_v3-to-v4_2025-01-15_00-00-00.zip
The BackupManager implements GFS (Grandfather-Father-Son) rotation for automatic backup management.
| Type | Auto-Rotated | Default Retention |
|---|---|---|
HOURLY |
Yes | Last 24 |
DAILY |
Yes | Last 7 |
WEEKLY |
Yes | Last 4 |
MANUAL |
No | Keep all (configurable) |
MIGRATION |
No | Keep all |
Each ZIP archive contains:
data/factions/— All faction JSON filesdata/players/— All player power JSON filesdata/chat/— Per-faction chat history filesdata/economy/— Per-faction treasury data filesdata/zones.json— Zone definitionsdata/invites.json— Pending faction invitesdata/join_requests.json— Pending join requestsconfig/— Configuration files (factions.json, server.json, etc.)
| Method | Description |
|---|---|
createBackup(type) |
Create async ZIP backup |
restoreBackup(name) |
Async ZIP extraction + reload |
listBackups() |
List sorted by timestamp (newest first) |
performRotation() |
GFS cleanup of old backups |
startScheduledBackups() |
Schedule hourly backups (72,000 ticks) |
See Data Import & Migration for import directory details and config migration.
| Class | Path | Purpose |
|---|---|---|
| FactionStorage | storage/FactionStorage.java |
Faction storage interface |
| PlayerStorage | storage/PlayerStorage.java |
Player power storage interface |
| ZoneStorage | storage/ZoneStorage.java |
Zone storage interface |
| JsonFactionStorage | storage/json/JsonFactionStorage.java |
JSON faction storage |
| JsonPlayerStorage | storage/json/JsonPlayerStorage.java |
JSON player storage |
| JsonZoneStorage | storage/json/JsonZoneStorage.java |
JSON zone storage |
| ChatHistoryStorage | storage/ChatHistoryStorage.java |
Chat history storage interface |
| JsonChatHistoryStorage | storage/json/JsonChatHistoryStorage.java |
JSON chat history storage |
| JsonEconomyStorage | storage/JsonEconomyStorage.java |
JSON economy/treasury storage |
| StorageUtils | storage/StorageUtils.java |
Atomic write, checksum, backup recovery |
| StorageHealth | storage/StorageHealth.java |
Storage health monitoring |
<server>/mods/com.hyperfactions_HyperFactions/
├── config/ # Configuration files (see config.md)
│ ├── factions.json # Faction gameplay settings
│ ├── server.json # Server behavior settings
│ └── ... # Other module configs
├── data/ # All data files
│ ├── factions/ # One file per faction
│ │ └── {uuid}.json
│ ├── players/ # One file per player
│ │ └── {uuid}.json
│ ├── chat/ # Per-faction chat history
│ │ └── {factionId}.json
│ ├── economy/ # Per-faction treasury data
│ │ └── {factionId}.json
│ ├── zones.json # All zones in single file
│ ├── invites.json # Pending faction invites
│ ├── join_requests.json # Pending join requests
│ └── .version # Data layout version (1)
├── update_preferences.json # Update notification preferences
└── backups/ # Backup storage (ZIP archives)
└── backup_*.zip
public interface FactionStorage {
/**
* Initialize storage (create directories, etc.).
*/
CompletableFuture<Void> init();
/**
* Shutdown storage (flush pending writes).
*/
CompletableFuture<Void> shutdown();
/**
* Load a single faction by ID.
*/
CompletableFuture<Optional<Faction>> loadFaction(UUID factionId);
/**
* Save a faction (create or update).
*/
CompletableFuture<Void> saveFaction(Faction faction);
/**
* Delete a faction.
*/
CompletableFuture<Void> deleteFaction(UUID factionId);
/**
* Load all factions.
*/
CompletableFuture<Collection<Faction>> loadAllFactions();
}public interface PlayerStorage {
CompletableFuture<Void> init();
CompletableFuture<Void> shutdown();
// Power-only operations (delegates to PlayerData internally)
CompletableFuture<Optional<PlayerPower>> loadPlayerPower(UUID uuid);
CompletableFuture<Void> savePlayerPower(PlayerPower power);
CompletableFuture<Void> deletePlayerPower(UUID uuid);
CompletableFuture<Collection<PlayerPower>> loadAllPlayerPower();
// UUID discovery
CompletableFuture<Set<UUID>> getAllPlayerUuids();
// Full player data operations (power + history + stats + preferences)
CompletableFuture<Optional<PlayerData>> loadPlayerData(UUID uuid);
CompletableFuture<Void> savePlayerData(PlayerData data);
/**
* Atomically loads player data, applies the updater, and saves.
* Thread-safe: concurrent updates to the same player are serialized.
*
* @param updater a Consumer that modifies the player data in place
*/
CompletableFuture<Void> updatePlayerData(UUID uuid, Consumer<PlayerData> updater);
}public interface ZoneStorage {
CompletableFuture<Void> init();
CompletableFuture<Void> shutdown();
CompletableFuture<Collection<Zone>> loadAllZones();
CompletableFuture<Void> saveAllZones(Collection<Zone> zones);
}storage/json/JsonFactionStorage.java
Stores one JSON file per faction in factions/ directory:
public class JsonFactionStorage implements FactionStorage {
private final Path factionsDir;
private final Gson gson;
public JsonFactionStorage(Path dataDir) {
this.factionsDir = dataDir.resolve("factions");
this.gson = new GsonBuilder()
.setPrettyPrinting()
.serializeNulls()
.create();
}
@Override
public CompletableFuture<Void> saveFaction(Faction faction) {
return CompletableFuture.runAsync(() -> {
Path file = factionsDir.resolve(faction.id() + ".json");
try (Writer writer = Files.newBufferedWriter(file)) {
gson.toJson(factionToJson(faction), writer);
}
});
}
private Path getFactionFile(UUID factionId) {
return factionsDir.resolve(factionId.toString() + ".json");
}
}storage/json/JsonPlayerStorage.java
Stores one JSON file per player in players/ directory. Uses per-UUID locking to prevent race conditions from concurrent kill/death tracking:
public class JsonPlayerStorage implements PlayerStorage {
private final Path playersDir;
private final ConcurrentHashMap<UUID, ReentrantLock> playerLocks = new ConcurrentHashMap<>();
@Override
public CompletableFuture<Void> savePlayerPower(PlayerPower power) {
return CompletableFuture.runAsync(() -> {
Path file = playersDir.resolve(power.uuid() + ".json");
// Write JSON...
});
}
/**
* Atomically update player data under a per-UUID lock.
* Prevents lost updates from concurrent deaths/kills.
* Note: updater is a Consumer that modifies PlayerData in place (mutable class).
*/
public CompletableFuture<Void> updatePlayerData(UUID uuid, Consumer<PlayerData> updater) {
return CompletableFuture.runAsync(() -> {
ReentrantLock lock = playerLocks.computeIfAbsent(uuid, k -> new ReentrantLock());
lock.lock();
try {
PlayerData data = loadPlayerDataSync(uuid);
if (data == null) data = new PlayerData(uuid);
updater.accept(data); // Mutates in place
savePlayerDataSync(data);
} finally {
lock.unlock();
}
});
}
}The updatePlayerData method ensures that concurrent operations (e.g., two simultaneous deaths) do not lose increments through unsynchronized load-modify-save cycles.
storage/json/JsonZoneStorage.java
Stores all zones in a single zones.json file (zones are typically few in number):
public class JsonZoneStorage implements ZoneStorage {
private final Path zonesFile;
@Override
public CompletableFuture<Void> saveAllZones(Collection<Zone> zones) {
return CompletableFuture.runAsync(() -> {
// Write all zones as JSON array
});
}
}Immutable record with with*() copy methods for updates:
public record Faction(
UUID id,
String name,
@Nullable String description,
@Nullable String tag,
String color, // Hex string, e.g., "#55FFFF"
long createdAt,
@Nullable FactionHome home,
Map<UUID, FactionMember> members, // Map, not List
Set<FactionClaim> claims, // Set, not List
Map<UUID, FactionRelation> relations, // Map keyed by target faction UUID
List<FactionLog> logs,
boolean open,
@Nullable FactionPermissions permissions,
@Nullable Double hardcorePower // Hardcore mode faction power pool
) {
// Compact constructor: copies collections to immutable, auto-migrates legacy color codes
// Update methods return new Faction instances:
// withName(), withDescription(), withTag(), withColor(), withOpen()
// withMember(), withoutMember(), withClaim(), withoutClaimAt()
// withRelation(), withHome(), withLog(), withPermissions(), withHardcorePower()
}JSON Structure (factions/{uuid}.json):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Warriors",
"description": "A mighty faction",
"tag": "WAR",
"color": "#55FFFF",
"createdAt": 1706745600000,
"open": false,
"hardcorePower": null,
"home": {
"world": "world",
"x": 100.5,
"y": 64.0,
"z": 200.5,
"yaw": 90.0,
"pitch": 0.0,
"setAt": 1706745600000,
"setBy": "player-uuid"
},
"members": {
"player-uuid": {
"uuid": "player-uuid",
"username": "PlayerName",
"role": "LEADER",
"joinedAt": 1706745600000,
"lastOnline": 1706832000000
}
},
"claims": [
{
"world": "world",
"chunkX": 10,
"chunkZ": 20,
"claimedAt": 1706745600000,
"claimedBy": "player-uuid"
}
],
"relations": {
"other-faction-uuid": {
"targetFactionId": "other-faction-uuid",
"type": "ALLY",
"since": 1706745600000
}
},
"logs": [
{
"type": "MEMBER_JOIN",
"message": "PlayerName joined",
"timestamp": 1706745600000,
"actorUuid": "player-uuid"
}
],
"permissions": {
"outsiderBreak": false,
"memberBreak": true,
"pvpEnabled": true
}
}Note:
coloris stored as a hex string (e.g.,"#55FFFF"). Legacy single-char codes (e.g.,"c") are auto-migrated to hex on load.membersandrelationsare serialized as maps keyed by UUID.
public record FactionMember(
UUID uuid,
String username,
FactionRole role,
long joinedAt,
long lastOnline
) {}public enum FactionRole {
LEADER, // Full control
OFFICER, // Can manage members, claims
MEMBER // Basic permissions
}public record PlayerPower(
UUID uuid,
double power,
double maxPower,
long lastDeath,
long lastRegen,
@Nullable Double maxPowerOverride, // Per-player max power override (null = use global config)
boolean powerLossDisabled, // Absolute bypass: never loses power from any source
boolean claimDecayExempt // Treated as always online for claim decay
) {
// getEffectiveMaxPower() returns override if set, else maxPower
// withPower(), withDeathPenalty(), withRegen(), withMaxPower()
// withMaxPowerOverride(), withPowerLossDisabled(), withClaimDecayExempt()
}Note:
PlayerPoweris the immutable power-only record. Actual on-disk storage uses the mutablePlayerDataclass which wraps power fields plus kill/death stats, membership history, preferences, and admin bypass state.PlayerPoweris extracted fromPlayerDataviatoPower().
JSON Structure (players/{uuid}.json) — stored as PlayerData:
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"username": "PlayerName",
"power": 15.5,
"maxPower": 20.0,
"lastDeath": 1706745600000,
"lastRegen": 1706832000000,
"kills": 5,
"deaths": 2,
"firstJoined": 1706745600000,
"lastOnline": 1706832000000,
"maxPowerOverride": null,
"powerLossDisabled": false,
"claimDecayExempt": false,
"adminBypassEnabled": false,
"languagePreference": null,
"territoryAlertsEnabled": true,
"deathAnnouncementsEnabled": true,
"powerNotificationsEnabled": true,
"membershipHistory": []
}Mutable class combining power fields with extended player data. Stored on disk in data/players/{uuid}.json:
public class PlayerData {
private UUID uuid;
private String username;
// Power fields (same as PlayerPower record)
private double power;
private double maxPower;
private long lastDeath;
private long lastRegen;
private Double maxPowerOverride;
private boolean powerLossDisabled;
private boolean claimDecayExempt;
// Stats
private int kills;
private int deaths;
private long firstJoined;
private long lastOnline;
// History
private List<MembershipRecord> membershipHistory;
// Admin state
private boolean adminBypassEnabled;
// Player preferences (i18n + notifications)
private String languagePreference;
private boolean territoryAlertsEnabled = true;
private boolean deathAnnouncementsEnabled = true;
private boolean powerNotificationsEnabled = true;
// Conversion: toPower() -> PlayerPower, updatePower(PlayerPower) <- PlayerPower
// Membership: addRecord(), closeActiveRecord(), getActiveRecord(), updateHighestRole()
// Stats: incrementKills(), incrementDeaths()
}public record Zone(
UUID id,
String name,
ZoneType type,
String world,
Set<ChunkKey> chunks,
long createdAt,
UUID createdBy,
@Nullable Map<String, Boolean> flags, // Boolean flags (null = use zone type defaults)
@Nullable Map<String, String> settings, // String-valued settings for enum/selection options
@Nullable Boolean notifyOnEntry, // Show entry notification (null/true = show)
@Nullable String notifyTitleUpper, // Custom upper title text (null = default)
@Nullable String notifyTitleLower // Custom lower title text (null = default)
) {
// Compact constructor ensures chunks is immutable
// withChunk(), withoutChunk(), withName(), withFlag(), withoutFlag()
// withSetting(), withoutSetting(), withNotifyOnEntry(), withNotifyTitleUpper/Lower()
// getEffectiveFlag() considers parent-child flag enforcement and zone type defaults
// getEffectiveSetting() considers zone type defaults
}JSON Structure (zones.json):
[
{
"id": "zone-uuid",
"name": "Spawn",
"type": "SAFE",
"world": "world",
"chunks": [
{ "x": 0, "z": 0 },
{ "x": 0, "z": 1 }
],
"createdAt": 1706745600000,
"createdBy": "admin-uuid",
"flags": {
"pvp_enabled": false,
"build_allowed": false
},
"settings": null,
"notifyOnEntry": true,
"notifyTitleUpper": null,
"notifyTitleLower": null
}
]Immutable identifier for a chunk:
/**
* Note: Hytale uses 32-block chunks (shift by 5), not 16-block chunks.
*/
public record ChunkKey(
String world,
int chunkX, // NOT "x" — field name is "chunkX"
int chunkZ // NOT "z" — field name is "chunkZ"
) {
private static final int CHUNK_SIZE = 32;
private static final int CHUNK_SHIFT = 5;
public static ChunkKey fromWorldCoords(String world, double x, double z) {
return new ChunkKey(world, (int) Math.floor(x) >> CHUNK_SHIFT, (int) Math.floor(z) >> CHUNK_SHIFT);
}
public static ChunkKey fromBlockCoords(String world, int blockX, int blockZ) {
return new ChunkKey(world, blockX >> CHUNK_SHIFT, blockZ >> CHUNK_SHIFT);
}
// Navigation: north(), south(), east(), west()
// Utility: isAdjacentTo(), getCenterX(), getCenterZ(), getMinBlockX(), etc.
}All storage operations are async to prevent blocking the main thread:
// In manager
public void loadAll() {
factionStorage.loadAllFactions()
.thenAccept(factions -> {
for (Faction faction : factions) {
cache.put(faction.id(), faction);
}
})
.join(); // Block only during startup
}
public void saveFaction(Faction faction) {
// Fire and forget during normal operation
factionStorage.saveFaction(faction);
}During startup, .join() is used to ensure data is loaded before the plugin is ready:
// In HyperFactions.enable()
factionStorage.init().join();
playerStorage.init().join();
zoneStorage.init().join();
factionManager.loadAll().join();
powerManager.loadAll().join();
zoneManager.loadAll().join();During normal operation, saves are fire-and-forget:
// In FactionManager
public void updateFaction(Faction faction) {
cache.put(faction.id(), faction);
factionStorage.saveFaction(faction); // Async, doesn't block
}Configured in config/server.json:
{
"autoSave": {
"enabled": true,
"intervalMinutes": 5
}
}Implementation in HyperFactions.java:
private void startAutoSaveTask() {
int intervalMinutes = ConfigManager.get().getAutoSaveIntervalMinutes();
int periodTicks = intervalMinutes * 60 * 20;
autoSaveTaskId = scheduleRepeatingTask(periodTicks, periodTicks, this::saveAllData);
}
public void saveAllData() {
Logger.info("Auto-saving data...");
factionManager.saveAll().join();
powerManager.saveAll().join();
zoneManager.saveAll().join();
Logger.info("Auto-save complete");
}All 7 storage types use StorageUtils.writeAtomic() for crash-safe writes:
- Write content to a temp file (
file.{counter}.tmp) - Compute SHA-256 checksum of content
- Read back temp file and verify checksum matches
- Copy existing file to
.bakbackup - Atomic rename: temp → target
- Delete
.bakfile (cleanup after successful write)
If the process crashes during steps 1-4, the original file is untouched. If it crashes during step 5, the .bak file provides recovery. On startup, cleanupOrphanedFiles() removes any stray .tmp or orphaned .bak files.
| Storage | File Pattern | Notes |
|---|---|---|
| JsonFactionStorage | data/factions/{uuid}.json |
One file per faction |
| JsonPlayerStorage | data/players/{uuid}.json |
One file per player |
| JsonZoneStorage | data/zones.json |
Single file for all zones |
| JsonChatHistoryStorage | data/chat/{factionId}.json |
One file per faction |
| JsonEconomyStorage | data/economy/{factionId}.json |
One file per faction |
| InviteManager | data/invites.json |
Single file |
| JoinRequestManager | data/join_requests.json |
Single file |
migration/migrations/data/DataV0ToV1Migration.java
Moves data files from the plugin root into a data/ subdirectory. The migration:
- Creates
data/directory - Moves:
factions/,players/,chat/,economy/,zones.json,invites.json,join_requests.json - Also moves any
.bakfiles alongside their data files - Writes
data/.versionwith1(last step — if crash before this, migration re-runs) - MigrationRunner creates ZIP backup before execution for rollback support
Detection: Runs when data/.version doesn't exist AND at least one old-path item exists.
migration/migrations/data/DataV1ToV2Migration.java
Second data migration step. Also handled by MigrationRunner.
migration/MigrationRunner.java
Handles automatic data format upgrades:
Old single-chunk format:
{
"id": "...",
"world": "world",
"chunkX": 10,
"chunkZ": 20
}Migrates to multi-chunk format:
{
"id": "...",
"world": "world",
"chunks": [{ "x": 10, "z": 20 }]
}Migration is detected and run automatically on load.
Monitors storage system health:
public final class StorageHealth {
private static final StorageHealth INSTANCE = new StorageHealth();
/** Time window for rate calculation — 5 minutes. */
private static final long RATE_WINDOW_MS = 5 * 60 * 1000;
private final AtomicLong totalSuccesses = new AtomicLong(0);
private final AtomicLong totalFailures = new AtomicLong(0);
/** Timestamped writes for rate calculation. */
private final LinkedList<TimestampedWrite> recentWrites = new LinkedList<>();
/** Per-file success/failure counts. */
private final Map<String, AtomicLong> successCounts = new ConcurrentHashMap<>();
private final Map<String, AtomicLong> failureCounts = new ConcurrentHashMap<>();
public void recordSuccess(String filePath) { ... }
public void recordFailure(String filePath, String error) { ... }
/**
* Returns false if the recent failure rate exceeds 10%
* (rate-based, not consecutive-failure-based).
*/
public boolean isHealthy() {
return getRecentFailureRate() < 0.10;
}
public double getRecentFailureRate() {
// Calculates failures / total writes in the 5-minute window
}
}To add database support, implement the storage interfaces:
public class MySqlFactionStorage implements FactionStorage {
private final DataSource dataSource;
@Override
public CompletableFuture<Void> saveFaction(Faction faction) {
return CompletableFuture.runAsync(() -> {
try (Connection conn = dataSource.getConnection()) {
// SQL INSERT/UPDATE
}
});
}
@Override
public CompletableFuture<Optional<Faction>> loadFaction(UUID factionId) {
return CompletableFuture.supplyAsync(() -> {
try (Connection conn = dataSource.getConnection()) {
// SQL SELECT
}
});
}
}Then configure in HyperFactions:
// In HyperFactions.enable()
if (ConfigManager.get().isUsingDatabase()) {
factionStorage = new MySqlFactionStorage(dataSource);
} else {
factionStorage = new JsonFactionStorage(dataDir);
}Storage integrates with the backup system:
// In BackupManager
public void createBackup(BackupType type) {
// Save all data first
hyperFactions.saveAllData();
// Copy data directories to backup (from data/ subdirectory)
Path dataPath = dataDir.resolve("data");
copyDirectory(dataPath.resolve("factions"), backupDir);
copyDirectory(dataPath.resolve("players"), backupDir);
copyDirectory(dataPath.resolve("chat"), backupDir);
copyDirectory(dataPath.resolve("economy"), backupDir);
copyFile(dataPath.resolve("zones.json"), backupDir);
copyFile(dataPath.resolve("invites.json"), backupDir);
copyFile(dataPath.resolve("join_requests.json"), backupDir);
copyDirectory(dataDir.resolve("config"), backupDir);
}JSON files can be manually edited while the server is stopped:
- Stop the server
- Edit JSON files
- Start the server (data loads fresh)
Warning: Editing while the server is running may cause data loss due to auto-save overwriting changes.
| Class | Path |
|---|---|
| FactionStorage | storage/FactionStorage.java |
| PlayerStorage | storage/PlayerStorage.java |
| ZoneStorage | storage/ZoneStorage.java |
| JsonFactionStorage | storage/json/JsonFactionStorage.java |
| JsonPlayerStorage | storage/json/JsonPlayerStorage.java |
| JsonZoneStorage | storage/json/JsonZoneStorage.java |
| Faction | data/Faction.java |
| PlayerPower | data/PlayerPower.java |
| Zone | data/Zone.java |
| ChunkKey | data/ChunkKey.java |
| PlayerData | data/PlayerData.java |
| ChatHistoryStorage | storage/ChatHistoryStorage.java |
| JsonChatHistoryStorage | storage/json/JsonChatHistoryStorage.java |
| JsonEconomyStorage | storage/JsonEconomyStorage.java |
| StorageUtils | storage/StorageUtils.java |
| DataV0ToV1Migration | migration/migrations/data/DataV0ToV1Migration.java |
| DataV1ToV2Migration | migration/migrations/data/DataV1ToV2Migration.java |
| MigrationRunner | migration/MigrationRunner.java |
| BackupManager | backup/BackupManager.java |