SHA256
НЕ ПРОВЕРЕНО: откат DM-вложений, оставлены ревизии и удаление
This commit is contained in:
@@ -640,36 +640,6 @@ public final class DatabaseInitializer {
|
||||
ON signed_messages_v2 (base_key, message_type);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS dm_files (
|
||||
file_hash_sha256 BLOB NOT NULL PRIMARY KEY,
|
||||
file_size INTEGER NOT NULL,
|
||||
ref_count INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS dm_message_file_links (
|
||||
message_key TEXT NOT NULL,
|
||||
login TEXT NOT NULL,
|
||||
file_hash_sha256 BLOB NOT NULL,
|
||||
PRIMARY KEY (message_key, login, file_hash_sha256),
|
||||
FOREIGN KEY (message_key) REFERENCES signed_messages_v2(message_key),
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
||||
FOREIGN KEY (file_hash_sha256) REFERENCES dm_files(file_hash_sha256)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_dm_message_file_links_login
|
||||
ON dm_message_file_links (login, file_hash_sha256);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_dm_message_file_links_message
|
||||
ON dm_message_file_links (message_key);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_signed_messages_v2_receipt_incoming
|
||||
ON signed_messages_v2 (target_login, receipt_ref_base_key)
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.sql.Statement;
|
||||
public final class SqliteDbController {
|
||||
|
||||
private static volatile SqliteDbController instance;
|
||||
private static final int LATEST_SCHEMA_VERSION = 6;
|
||||
private static final int LATEST_SCHEMA_VERSION = 7;
|
||||
|
||||
private final String jdbcUrl;
|
||||
|
||||
@@ -89,6 +89,7 @@ public final class SqliteDbController {
|
||||
case 4 -> migrateToV4();
|
||||
case 5 -> migrateToV5();
|
||||
case 6 -> migrateToV6();
|
||||
case 7 -> migrateToV7();
|
||||
default -> throw new RuntimeException("Unknown DB migration target version: " + targetVersion);
|
||||
}
|
||||
}
|
||||
@@ -216,7 +217,6 @@ public final class SqliteDbController {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
ensureSignedMessagesRevisionColumn(c, st);
|
||||
ensureDmFileTables(st);
|
||||
setSchemaVersion(c, 6);
|
||||
c.commit();
|
||||
} catch (Exception e) {
|
||||
@@ -230,6 +230,25 @@ public final class SqliteDbController {
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateToV7() {
|
||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
||||
Statement st = c.createStatement()) {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
dropDmFileTables(st);
|
||||
setSchemaVersion(c, 7);
|
||||
c.commit();
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw new RuntimeException("DB migration to v7 failed", e);
|
||||
} finally {
|
||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("DB migration to v7 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureChat200StateTables(Statement st) throws SQLException {
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS chat200_state (
|
||||
@@ -357,36 +376,11 @@ public final class SqliteDbController {
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureDmFileTables(Statement st) throws SQLException {
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS dm_files (
|
||||
file_hash_sha256 BLOB NOT NULL PRIMARY KEY,
|
||||
file_size INTEGER NOT NULL,
|
||||
ref_count INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS dm_message_file_links (
|
||||
message_key TEXT NOT NULL,
|
||||
login TEXT NOT NULL,
|
||||
file_hash_sha256 BLOB NOT NULL,
|
||||
PRIMARY KEY (message_key, login, file_hash_sha256),
|
||||
FOREIGN KEY (message_key) REFERENCES signed_messages_v2(message_key),
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
||||
FOREIGN KEY (file_hash_sha256) REFERENCES dm_files(file_hash_sha256)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_dm_message_file_links_login
|
||||
ON dm_message_file_links (login, file_hash_sha256);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_dm_message_file_links_message
|
||||
ON dm_message_file_links (message_key);
|
||||
""");
|
||||
private static void dropDmFileTables(Statement st) throws SQLException {
|
||||
st.executeUpdate("DROP INDEX IF EXISTS idx_dm_message_file_links_login");
|
||||
st.executeUpdate("DROP INDEX IF EXISTS idx_dm_message_file_links_message");
|
||||
st.executeUpdate("DROP TABLE IF EXISTS dm_message_file_links");
|
||||
st.executeUpdate("DROP TABLE IF EXISTS dm_files");
|
||||
}
|
||||
|
||||
private static boolean columnExists(Connection c, String tableName, String columnName) throws SQLException {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.entities.DmFileRef;
|
||||
import shine.db.entities.SignedMessageV2Entry;
|
||||
|
||||
import java.sql.Connection;
|
||||
@@ -10,9 +9,7 @@ import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class SignedMessagesV2DAO {
|
||||
private static volatile SignedMessagesV2DAO instance;
|
||||
@@ -45,9 +42,6 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Атомарная вставка пары блоков legacy/read-receipt: либо вставляются оба, либо не вставляется ни один.
|
||||
*/
|
||||
public boolean insertPairBothOrNothing(SignedMessageV2Entry first, SignedMessageV2Entry second) throws Exception {
|
||||
try (Connection c = db.getConnection()) {
|
||||
boolean prevAutoCommit = c.getAutoCommit();
|
||||
@@ -73,25 +67,11 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Атомарный upsert пары контентных DM с полной заменой файловых связей.
|
||||
* Возвращает true, только если ревизия применена. Более старая или идентичная ревизия игнорируется.
|
||||
*/
|
||||
public boolean upsertContentPairReplaceFiles(
|
||||
SignedMessageV2Entry incoming,
|
||||
List<DmFileRef> incomingFiles,
|
||||
SignedMessageV2Entry outgoing,
|
||||
List<DmFileRef> outgoingFiles
|
||||
) throws Exception {
|
||||
public boolean upsertContentPair(SignedMessageV2Entry incoming, SignedMessageV2Entry outgoing) throws Exception {
|
||||
try (Connection c = db.getConnection()) {
|
||||
boolean prevAutoCommit = c.getAutoCommit();
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
if (!allFilesExist(c, incomingFiles) || !allFilesExist(c, outgoingFiles)) {
|
||||
c.rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
Long currentIncomingRevision = getRevisionTimeMs(c, incoming.getMessageKey());
|
||||
Long currentOutgoingRevision = getRevisionTimeMs(c, outgoing.getMessageKey());
|
||||
long currentRevision = Math.max(
|
||||
@@ -112,12 +92,8 @@ public final class SignedMessagesV2DAO {
|
||||
return false;
|
||||
}
|
||||
|
||||
replaceFileLinks(c, incoming.getMessageKey(), incoming.getTargetLogin(), incomingFiles);
|
||||
replaceFileLinks(c, outgoing.getMessageKey(), outgoing.getTargetLogin(), outgoingFiles);
|
||||
|
||||
upsertMessage(c, incoming);
|
||||
upsertMessage(c, outgoing);
|
||||
|
||||
resetDeliveryRows(c, incoming.getMessageKey());
|
||||
resetDeliveryRows(c, outgoing.getMessageKey());
|
||||
|
||||
@@ -132,40 +108,6 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean fileExists(byte[] fileHash, long fileSize) throws Exception {
|
||||
try (Connection c = db.getConnection()) {
|
||||
return fileExists(c, fileHash, fileSize);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean fileExistsByHash(byte[] fileHash) throws Exception {
|
||||
try (Connection c = db.getConnection()) {
|
||||
String sql = "SELECT 1 FROM dm_files WHERE file_hash_sha256 = ? LIMIT 1";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setBytes(1, fileHash);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
return rs.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void registerFileIfAbsent(byte[] fileHash, long fileSize) throws Exception {
|
||||
try (Connection c = db.getConnection()) {
|
||||
String sql = """
|
||||
INSERT INTO dm_files (file_hash_sha256, file_size, ref_count)
|
||||
VALUES (?, ?, 0)
|
||||
ON CONFLICT(file_hash_sha256) DO UPDATE SET
|
||||
file_size = excluded.file_size
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setBytes(1, fileHash);
|
||||
ps.setLong(2, fileSize);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SignedMessageV2Entry getByMessageKey(String messageKey) throws Exception {
|
||||
try (Connection c = db.getConnection()) {
|
||||
String sql = """
|
||||
@@ -299,106 +241,6 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
private void replaceFileLinks(Connection c, String messageKey, String login, List<DmFileRef> nextFiles) throws SQLException {
|
||||
List<byte[]> oldHashes = listLinkedFileHashes(c, messageKey, login);
|
||||
for (byte[] oldHash : oldHashes) {
|
||||
adjustRefCount(c, oldHash, -1);
|
||||
}
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement("""
|
||||
DELETE FROM dm_message_file_links
|
||||
WHERE message_key = ? AND login = ? COLLATE NOCASE
|
||||
""")) {
|
||||
ps.setString(1, messageKey);
|
||||
ps.setString(2, login);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
|
||||
if (nextFiles == null || nextFiles.isEmpty()) return;
|
||||
|
||||
Set<String> dedup = new HashSet<>();
|
||||
for (DmFileRef ref : nextFiles) {
|
||||
if (ref == null || ref.getFileHash() == null) continue;
|
||||
String dedupKey = Arrays.toString(ref.getFileHash());
|
||||
if (!dedup.add(dedupKey)) continue;
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement("""
|
||||
INSERT OR IGNORE INTO dm_message_file_links (
|
||||
message_key, login, file_hash_sha256
|
||||
) VALUES (?, ?, ?)
|
||||
""")) {
|
||||
ps.setString(1, messageKey);
|
||||
ps.setString(2, login);
|
||||
ps.setBytes(3, ref.getFileHash());
|
||||
int inserted = ps.executeUpdate();
|
||||
if (inserted > 0) {
|
||||
adjustRefCount(c, ref.getFileHash(), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<byte[]> listLinkedFileHashes(Connection c, String messageKey, String login) throws SQLException {
|
||||
String sql = """
|
||||
SELECT file_hash_sha256
|
||||
FROM dm_message_file_links
|
||||
WHERE message_key = ? AND login = ? COLLATE NOCASE
|
||||
""";
|
||||
List<byte[]> out = new ArrayList<>();
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, messageKey);
|
||||
ps.setString(2, login);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
out.add(rs.getBytes(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private void adjustRefCount(Connection c, byte[] fileHash, int delta) throws SQLException {
|
||||
String sql = """
|
||||
UPDATE dm_files
|
||||
SET ref_count = CASE
|
||||
WHEN ref_count + ? < 0 THEN 0
|
||||
ELSE ref_count + ?
|
||||
END
|
||||
WHERE file_hash_sha256 = ?
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setInt(1, delta);
|
||||
ps.setInt(2, delta);
|
||||
ps.setBytes(3, fileHash);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean allFilesExist(Connection c, List<DmFileRef> refs) throws SQLException {
|
||||
if (refs == null) return true;
|
||||
for (DmFileRef ref : refs) {
|
||||
if (ref == null || ref.getFileHash() == null) return false;
|
||||
if (!fileExists(c, ref.getFileHash(), ref.getFileSize())) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean fileExists(Connection c, byte[] fileHash, long fileSize) throws SQLException {
|
||||
String sql = """
|
||||
SELECT 1
|
||||
FROM dm_files
|
||||
WHERE file_hash_sha256 = ? AND file_size = ?
|
||||
LIMIT 1
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setBytes(1, fileHash);
|
||||
ps.setLong(2, fileSize);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
return rs.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Long getRevisionTimeMs(Connection c, String messageKey) throws SQLException {
|
||||
String sql = "SELECT revision_time_ms FROM signed_messages_v2 WHERE message_key = ? LIMIT 1";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package shine.db.entities;
|
||||
|
||||
public class DmFileRef {
|
||||
private byte[] fileHash;
|
||||
private long fileSize;
|
||||
|
||||
public DmFileRef() {
|
||||
}
|
||||
|
||||
public DmFileRef(byte[] fileHash, long fileSize) {
|
||||
this.fileHash = fileHash;
|
||||
this.fileSize = fileSize;
|
||||
}
|
||||
|
||||
public byte[] getFileHash() { return fileHash; }
|
||||
public void setFileHash(byte[] fileHash) { this.fileHash = fileHash; }
|
||||
public long getFileSize() { return fileSize; }
|
||||
public void setFileSize(long fileSize) { this.fileSize = fileSize; }
|
||||
}
|
||||
Reference in New Issue
Block a user