Add storage

This commit is contained in:
2026-05-15 19:01:10 +08:00
parent 8a16b01e9e
commit 8aa7387e4d
49 changed files with 2112 additions and 223 deletions
@@ -1,59 +1,174 @@
package com.cyynote.dso;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
import javax.sql.DataSource;
public class DsHelper {
private static final Logger log = Logger.getLogger(DsHelper.class.getName());
private static boolean inited = false;
public static void initData(DataSource ds) {
if (inited)
return;
if (inited) return;
inited = true;
Connection conn = null;
try {
conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT name FROM sqlite_master WHERE type='table' AND name='appx';");
ResultSet res = ps.executeQuery();
if (!res.next()) {
String[] sqls = getSqlFromFile();
for (String sql : sqls)
runSql(conn, sql);
try (Connection conn = ds.getConnection()) {
// 1. Initial schema: create all base tables if not present
if (!tableExists(conn, "appx")) {
log.info("[DB] Running initial schema...");
for (String sql : loadSqlFile("/db/schema.sql")) {
runSqlIgnoreError(conn, sql, null);
}
}
ps.close();
} catch (SQLException sqlException) {
throw new RuntimeException(sqlException);
} finally {
try {
if (conn != null)
conn.close();
} catch (SQLException sQLException) {}
// 2. Versioned migrations
runMigrations(conn);
} catch (SQLException e) {
throw new RuntimeException("DB init failed", e);
}
}
private static String[] getSqlFromFile() {
private static void runMigrations(Connection conn) throws SQLException {
runSql(conn, "CREATE TABLE IF NOT EXISTS db_version (" +
"version INTEGER PRIMARY KEY, applied_at TEXT)");
List<String> migrationFiles = listMigrationFiles();
Collections.sort(migrationFiles);
for (String fileName : migrationFiles) {
int version = parseMigrationVersion(fileName);
if (version < 0) continue;
if (isMigrationApplied(conn, version)) continue;
log.info("[DB] Running migration: " + fileName);
String[] statements = loadSqlFile("/db/migrations/" + fileName);
for (String sql : statements) {
runSqlIgnoreError(conn, sql, "duplicate column");
}
PreparedStatement ps = conn.prepareStatement(
"INSERT OR IGNORE INTO db_version(version, applied_at) VALUES(?, datetime('now'))");
ps.setInt(1, version);
ps.executeUpdate();
ps.close();
log.info("[DB] Migration V" + version + " applied.");
}
}
private static boolean isMigrationApplied(Connection conn, int version) throws SQLException {
PreparedStatement ps = conn.prepareStatement(
"SELECT 1 FROM db_version WHERE version = ?");
ps.setInt(1, version);
ResultSet rs = ps.executeQuery();
boolean exists = rs.next();
ps.close();
return exists;
}
private static List<String> listMigrationFiles() {
List<String> files = new ArrayList<>();
try {
InputStream ins = com.cyynote.dso.DsHelper.class.getResourceAsStream("/db/schema.sql");
int len = ins.available();
byte[] bs = new byte[len];
ins.read(bs);
String str = new String(bs, "UTF-8");
String[] sql = str.split(";");
return sql;
URL dirUrl = DsHelper.class.getResource("/db/migrations/");
if (dirUrl == null) return files;
String protocol = dirUrl.getProtocol();
if ("file".equals(protocol)) {
java.io.File dir = new java.io.File(dirUrl.toURI());
if (dir.isDirectory()) {
for (java.io.File f : dir.listFiles()) {
if (f.getName().startsWith("V") && f.getName().endsWith(".sql")) {
files.add(f.getName());
}
}
}
} else if ("jar".equals(protocol)) {
String jarPath = dirUrl.getPath();
String jarFile = jarPath.substring(5, jarPath.indexOf("!"));
String prefix = jarPath.substring(jarPath.indexOf("!") + 2);
try (java.util.jar.JarFile jar = new java.util.jar.JarFile(jarFile)) {
java.util.Enumeration<java.util.jar.JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
String name = entries.nextElement().getName();
if (name.startsWith(prefix) && name.endsWith(".sql")) {
String fileName = name.substring(name.lastIndexOf('/') + 1);
if (fileName.startsWith("V")) {
files.add(fileName);
}
}
}
}
}
} catch (Exception e) {
log.warning("[DB] Could not list migration files: " + e.getMessage());
}
return files;
}
private static int parseMigrationVersion(String fileName) {
try {
String num = fileName.substring(1, fileName.indexOf("__"));
return Integer.parseInt(num);
} catch (Exception e) {
return -1;
}
}
private static boolean tableExists(Connection conn, String tableName) throws SQLException {
PreparedStatement ps = conn.prepareStatement(
"SELECT name FROM sqlite_master WHERE type='table' AND name=?");
ps.setString(1, tableName);
ResultSet rs = ps.executeQuery();
boolean exists = rs.next();
ps.close();
return exists;
}
private static String[] loadSqlFile(String path) {
try {
InputStream ins = DsHelper.class.getResourceAsStream(path);
if (ins == null) return new String[0];
byte[] bs = ins.readAllBytes();
String str = new String(bs, StandardCharsets.UTF_8);
String[] parts = str.split(";");
List<String> result = new ArrayList<>();
for (String p : parts) {
String trimmed = p.trim();
if (!trimmed.isEmpty()) result.add(trimmed);
}
return result.toArray(new String[0]);
} catch (Exception ex) {
throw new RuntimeException(ex);
throw new RuntimeException("Failed to load SQL file: " + path, ex);
}
}
private static void runSql(Connection conn, String sql) throws SQLException {
System.out.println(sql);
PreparedStatement ps = conn.prepareStatement(sql);
ps.executeUpdate();
ps.close();
Statement st = conn.createStatement();
st.execute(sql);
st.close();
}
private static void runSqlIgnoreError(Connection conn, String sql, String ignoreContaining) {
try {
String trimmed = sql.trim();
if (trimmed.isEmpty()) return;
log.info("[DB SQL] " + trimmed.substring(0, Math.min(80, trimmed.length())));
Statement st = conn.createStatement();
st.execute(trimmed);
st.close();
} catch (SQLException e) {
if (ignoreContaining != null && e.getMessage() != null &&
e.getMessage().toLowerCase().contains(ignoreContaining.toLowerCase())) {
log.info("[DB] Ignored: " + e.getMessage());
} else {
log.warning("[DB] SQL error (non-fatal): " + e.getMessage() + " | SQL: " + sql.trim().substring(0, Math.min(80, sql.trim().length())));
}
}
}
}