Update Rex-EMoolator version

This commit is contained in:
Patryk Gensch
2026-08-20 21:40:10 +02:00
parent fd21d8c9a6
commit aaf2073ff7
6 changed files with 75 additions and 29 deletions
@@ -101,7 +101,7 @@ public final class Main {
try (Database db = Database.open(dataDir);
Statement st = db.connection().createStatement();
ResultSet rs = st.executeQuery("""
SELECT c.display_name, c.container, c.status, c.file_count,
SELECT c.display_name, c.container, c.fs_type, c.status, c.file_count,
g.root_path,
COALESCE(g.engine_override, g.engine_detected) AS engine,
MAX(CASE WHEN d.field = 'edition' THEN d.value END) AS edition,
@@ -114,12 +114,13 @@ public final class Main {
ORDER BY family, edition, c.display_name, g.root_path
""")) {
System.out.printf("%-40s %-10s %-10s %-13s %-22s %s%n",
"KOPIA", "STATUS", "KORZEŃ", "SILNIK", "RODZINA", "WYDANIE");
System.out.printf("%-40s %-8s %-10s %-10s %-13s %-22s %s%n",
"KOPIA", "FS", "STATUS", "KORZEŃ", "SILNIK", "RODZINA", "WYDANIE");
while (rs.next()) {
String root = rs.getString("root_path");
System.out.printf("%-40s %-10s %-10s %-13s %-22s %s%n",
System.out.printf("%-40s %-8s %-10s %-10s %-13s %-22s %s%n",
truncate(rs.getString("display_name"), 40),
nvl(rs.getString("fs_type")),
rs.getString("status"),
root == null ? "" : (root.isEmpty() ? "/" : root + "/"),
nvl(rs.getString("engine")),
@@ -46,6 +46,29 @@ public final class Database implements AutoCloseable {
st.executeUpdate(ddl);
}
}
migrate();
}
/**
* Dokłada kolumny, których {@code CREATE TABLE IF NOT EXISTS} nie doda do już
* istniejącej tabeli. Tylko {@code ADD COLUMN} — bez przebudowy tabel, bo na
* {@code copy} wiszą klucze obce z {@code ON DELETE CASCADE}.
*/
private void migrate() throws SQLException {
addColumnIfMissing("copy", "fs_type", "TEXT");
}
private void addColumnIfMissing(String table, String column, String type) throws SQLException {
try (Statement st = connection.createStatement()) {
try (java.sql.ResultSet rs = st.executeQuery("PRAGMA table_info(" + table + ")")) {
while (rs.next()) {
if (column.equalsIgnoreCase(rs.getString("name"))) {
return;
}
}
}
st.executeUpdate("ALTER TABLE " + table + " ADD COLUMN " + column + " " + type);
}
}
@Override
@@ -98,6 +121,9 @@ public final class Database implements AutoCloseable {
// Kopia to nośnik, nie gra: jedna płyta bywa dwupackiem z dwoma drzewami
// gier, więc przypisanie do wydania wisi przy game_root, nie tutaj.
// status mówi, czy zawartość w ogóle dało się odczytać.
// container to rodzaj artefaktu (plik .iso / .zip / katalog na dysku),
// fs_type to system plików wykryty przez :core po zawartości — to nie to samo:
// Wojna Trojańska.iso jest plikiem .iso, ale w środku ma UDF, nie ISO9660.
"""
CREATE TABLE IF NOT EXISTS copy (
id INTEGER PRIMARY KEY,
@@ -105,6 +131,7 @@ public final class Database implements AutoCloseable {
status TEXT NOT NULL DEFAULT 'ok'
CHECK (status IN ('ok', 'installer', 'unreadable')),
status_note TEXT,
fs_type TEXT,
path TEXT NOT NULL UNIQUE,
display_name TEXT,
size INTEGER,
@@ -1,6 +1,8 @@
package pl.genschu.rexcatalog.ingest;
import pl.genschu.bloomooemulator.engine.filesystem.AssetSourceDispatcher;
import pl.genschu.bloomooemulator.engine.filesystem.FileSystemDetector;
import pl.genschu.bloomooemulator.engine.filesystem.FileSystemType;
import pl.genschu.bloomooemulator.engine.filesystem.IFileSystem;
import pl.genschu.rexcatalog.db.Database;
@@ -25,7 +27,8 @@ import java.util.Map;
* Indeksuje pojedynczą kopię (ISO / ZIP / rozpakowany katalog) do bazy katalogu.
*
* <p>Nic nie rozpakowuje na dysk — czyta przez {@link IFileSystem} z {@code :core},
* czyli tę samą implementację ISO9660/Joliet, której używa emulator przy uruchamianiu gry.
* czyli te same implementacje ISO9660/Joliet i UDF, których używa emulator
* przy uruchamianiu gry.
* Kopia, której nie da się odczytać, i tak trafia do bazy — ze statusem i powodem,
* bo „mam ten obraz, ale go nie otwieram" to też fakt wart skatalogowania.
*/
@@ -54,14 +57,16 @@ public final class Ingestor {
Map<String, Long> sizeByPath = new LinkedHashMap<>();
Map<String, FormatSniffer.Result> formatByPath = new LinkedHashMap<>();
long totalBytes;
String fsType = null;
try {
// detekcja po zawartości, nie po rozszerzeniu — .iso bywa obrazem UDF
fsType = fsTypeName(FileSystemDetector.detectFileSystemType(source));
IFileSystem fs = AssetSourceDispatcher.openAssets(source);
totalBytes = walk(fs, "", sha1ByPath, sizeByPath, formatByPath);
} catch (Exception e) {
// np. Wojna Trojańska.iso — obraz UDF, którego IsoFileSystem nie parsuje
String note = e.getMessage() == null ? e.toString() : e.getMessage();
persistUnreadable(source, container, containerSha256, note);
persistUnreadable(source, container, fsType, containerSha256, note);
return new Result(source.getPath(), container, "unreadable", note, 0, 0, List.of());
}
@@ -74,7 +79,7 @@ public final class Ingestor {
}
long containerSize = source.isDirectory() ? totalBytes : source.length();
persist(source, container, status, statusNote, containerSize, containerSha256,
persist(source, container, fsType, status, statusNote, containerSize, containerSha256,
sha1ByPath, sizeByPath, formatByPath, roots);
List<RootSummary> summaries = new ArrayList<>();
@@ -93,6 +98,16 @@ public final class Ingestor {
.orElse(null);
}
private static String fsTypeName(FileSystemType type) {
return switch (type) {
case DIRECTORY -> "dir";
case ZIP -> "zip";
case ISO9660 -> "iso9660";
case UDF -> "udf";
case UNKNOWN -> null;
};
}
private static String containerType(File source) throws IOException {
if (source.isDirectory()) {
return "dir";
@@ -154,13 +169,13 @@ public final class Ingestor {
return bytes;
}
private void persistUnreadable(File source, String container, String sha256, String note)
throws Exception {
private void persistUnreadable(File source, String container, String fsType,
String sha256, String note) throws Exception {
Connection conn = db.connection();
boolean auto = conn.getAutoCommit();
conn.setAutoCommit(false);
try {
upsertCopy(conn, source, container, "unreadable", note,
upsertCopy(conn, source, container, fsType, "unreadable", note,
source.length(), sha256, 0);
conn.commit();
} catch (Exception e) {
@@ -171,7 +186,8 @@ public final class Ingestor {
}
}
private void persist(File source, String container, String status, String statusNote,
private void persist(File source, String container, String fsType,
String status, String statusNote,
long size, String sha256,
Map<String, String> sha1ByPath,
Map<String, Long> sizeByPath,
@@ -181,7 +197,7 @@ public final class Ingestor {
boolean previousAutoCommit = conn.getAutoCommit();
conn.setAutoCommit(false);
try {
long copyId = upsertCopy(conn, source, container, status, statusNote,
long copyId = upsertCopy(conn, source, container, fsType, status, statusNote,
size, sha256, sha1ByPath.size());
try (Statement st = conn.createStatement()) {
@@ -261,30 +277,32 @@ public final class Ingestor {
}
}
private long upsertCopy(Connection conn, File source, String container,
private long upsertCopy(Connection conn, File source, String container, String fsType,
String status, String statusNote,
long size, String sha256, int fileCount) throws Exception {
String path = source.getAbsolutePath();
try (PreparedStatement st = conn.prepareStatement("""
INSERT INTO copy (container, status, status_note, path, display_name,
INSERT INTO copy (container, fs_type, status, status_note, path, display_name,
size, sha256, file_count, ingested_at, core_version)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(path) DO UPDATE SET
container = excluded.container, status = excluded.status,
container = excluded.container, fs_type = excluded.fs_type,
status = excluded.status,
status_note = excluded.status_note, size = excluded.size,
sha256 = excluded.sha256, file_count = excluded.file_count,
ingested_at = excluded.ingested_at, core_version = excluded.core_version
""")) {
st.setString(1, container);
st.setString(2, status);
st.setString(3, statusNote);
st.setString(4, path);
st.setString(5, source.getName());
st.setLong(6, size);
st.setString(7, sha256);
st.setInt(8, fileCount);
st.setString(9, Instant.now().toString());
st.setString(10, coreVersion);
st.setString(2, fsType);
st.setString(3, status);
st.setString(4, statusNote);
st.setString(5, path);
st.setString(6, source.getName());
st.setLong(7, size);
st.setString(8, sha256);
st.setInt(9, fileCount);
st.setString(10, Instant.now().toString());
st.setString(11, coreVersion);
st.executeUpdate();
}
try (PreparedStatement st = conn.prepareStatement("SELECT id FROM copy WHERE path = ?")) {