25 12 25
Дорабатываю добавление блоков. Промежуточный комит2.Омталось созранение стате в бд поправить
This commit is contained in:
parent
e1b2c62231
commit
d460ea2952
@ -6,40 +6,29 @@ import java.util.Objects;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* ===============================================================
|
* ===============================================================
|
||||||
* FileStoreUtil — синглтон-утилита для записи/дозаписи/чтения файлов.
|
* FileStoreUtil — утилита работы с файлами в папке data/.
|
||||||
* ---------------------------------------------------------------
|
*
|
||||||
* Где хранит:
|
* Теперь поддерживает:
|
||||||
* • Все файлы размещаются в внешней папке DATA_DIR = "data" (в корне запуска).
|
* - основной файл блокчейна: <blockchainName>.bch
|
||||||
* Папка создаётся автоматически при первом обращении.
|
* - временный файл блокчейна: <blockchainName>.tmp_bch
|
||||||
*.
|
*
|
||||||
* Что умеет:
|
* Важное:
|
||||||
* • newFile(String fileName, byte[] data)
|
* - validateSimpleFileName() запрещает path traversal.
|
||||||
* - создаёт/переписывает файл с именем fileName и записывает data.
|
* - atomicReplaceBlockchainFile(): пытается сделать ATOMIC_MOVE (если ФС поддерживает),
|
||||||
* • addDataToFile(String fileName, byte[] data)
|
* иначе делает обычный REPLACE_EXISTING move.
|
||||||
* - дописывает data в конец файла (создаст файл, если его ещё нет).
|
|
||||||
* • readAllDataFromFile(String fileName)
|
|
||||||
* - читает весь файл целиком и возвращает содержимое в виде byte[].
|
|
||||||
*.
|
|
||||||
* Обёртки под «блокчейны»:
|
|
||||||
* • newBlockchain(long blockchainId, byte[] data)
|
|
||||||
* • addDataToBlockchain(long blockchainId, byte[] data)
|
|
||||||
* • readAllDataFromBlockchain(long blockchainId)
|
|
||||||
* - те же операции, но имя файла формируется из blockchainId и расширения ".bch".
|
|
||||||
*.
|
|
||||||
* Безопасность имён:
|
|
||||||
* • Внутри утилиты есть простая валидация имени файла: запрещены разделители путей,
|
|
||||||
* чтобы исключить выход из каталога data (path traversal).
|
|
||||||
*.
|
|
||||||
* Совместимость: Java 17.
|
|
||||||
* ===============================================================
|
* ===============================================================
|
||||||
*/
|
*/
|
||||||
public final class FileStoreUtil {
|
public final class FileStoreUtil {
|
||||||
|
|
||||||
/** Базовая папка для хранения всех файлов (создаётся автоматически). */
|
/** Базовая папка для хранения всех файлов (создаётся автоматически). */
|
||||||
public static final String DATA_DIR_NAME = "data";
|
public static final String DATA_DIR_NAME = "data";
|
||||||
/** Расширение файлов «блокчейнов». */
|
|
||||||
|
/** Расширение основного файла блокчейна. */
|
||||||
public static final String BLOCKCHAIN_FILE_EXTENSION = ".bch";
|
public static final String BLOCKCHAIN_FILE_EXTENSION = ".bch";
|
||||||
|
|
||||||
|
/** Расширение временного файла (старое+новое). */
|
||||||
|
public static final String BLOCKCHAIN_TMP_EXTENSION = ".tmp_bch";
|
||||||
|
|
||||||
private static final FileStoreUtil INSTANCE = new FileStoreUtil();
|
private static final FileStoreUtil INSTANCE = new FileStoreUtil();
|
||||||
|
|
||||||
private final Path dataDirPath;
|
private final Path dataDirPath;
|
||||||
@ -49,56 +38,40 @@ public final class FileStoreUtil {
|
|||||||
ensureDataDirExists();
|
ensureDataDirExists();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Получить единственный экземпляр утилиты. */
|
|
||||||
public static FileStoreUtil getInstance() {
|
public static FileStoreUtil getInstance() {
|
||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================================
|
/* ===================================================================== */
|
||||||
// ОБЩИЕ МЕТОДЫ РАБОТЫ С ФАЙЛОМ
|
/* ======================== Базовые операции =========================== */
|
||||||
// ===============================================================
|
/* ===================================================================== */
|
||||||
|
|
||||||
/**
|
|
||||||
* Создать/переписать файл и записать в него массив байт.
|
|
||||||
* @param fileName имя файла (без каталогов)
|
|
||||||
* @param data содержимое
|
|
||||||
* @throws IllegalArgumentException при неверном имени или null-данных
|
|
||||||
* @throws IllegalStateException при ошибках ввода/вывода
|
|
||||||
*/
|
|
||||||
public void newFile(String fileName, byte[] data) {
|
public void newFile(String fileName, byte[] data) {
|
||||||
Objects.requireNonNull(data, "Данные не должны быть null");
|
Objects.requireNonNull(data, "data == null");
|
||||||
Path target = resolveSafe(fileName);
|
Path target = resolveSafe(fileName);
|
||||||
try {
|
try {
|
||||||
Files.write(target, data, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
|
Files.write(target, data,
|
||||||
|
StandardOpenOption.CREATE,
|
||||||
|
StandardOpenOption.TRUNCATE_EXISTING,
|
||||||
|
StandardOpenOption.WRITE);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new IllegalStateException("Не удалось записать файл: " + target, e);
|
throw new IllegalStateException("Не удалось записать файл: " + target, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Дозаписать массив байт в конец файла (создаст файл, если отсутствует).
|
|
||||||
* @param fileName имя файла (без каталогов)
|
|
||||||
* @param data добавляемые данные
|
|
||||||
* @throws IllegalArgumentException при неверном имени или null-данных
|
|
||||||
* @throws IllegalStateException при ошибках ввода/вывода
|
|
||||||
*/
|
|
||||||
public void addDataToFile(String fileName, byte[] data) {
|
public void addDataToFile(String fileName, byte[] data) {
|
||||||
Objects.requireNonNull(data, "Данные не должны быть null");
|
Objects.requireNonNull(data, "data == null");
|
||||||
Path target = resolveSafe(fileName);
|
Path target = resolveSafe(fileName);
|
||||||
try {
|
try {
|
||||||
Files.write(target, data,
|
Files.write(target, data,
|
||||||
StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
|
StandardOpenOption.CREATE,
|
||||||
|
StandardOpenOption.WRITE,
|
||||||
|
StandardOpenOption.APPEND);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new IllegalStateException("Не удалось дописать файл: " + target, e);
|
throw new IllegalStateException("Не удалось дописать файл: " + target, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Прочитать весь файл в память и вернуть как byte[].
|
|
||||||
* @param fileName имя файла (без каталогов)
|
|
||||||
* @return содержимое файла
|
|
||||||
* @throws IllegalStateException если файл не существует или ошибка ввода/вывода
|
|
||||||
*/
|
|
||||||
public byte[] readAllDataFromFile(String fileName) {
|
public byte[] readAllDataFromFile(String fileName) {
|
||||||
Path target = resolveSafe(fileName);
|
Path target = resolveSafe(fileName);
|
||||||
if (!Files.exists(target)) {
|
if (!Files.exists(target)) {
|
||||||
@ -111,37 +84,91 @@ public final class FileStoreUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================================
|
public boolean exists(String fileName) {
|
||||||
// ОБЁРТКИ ДЛЯ «БЛОКЧЕЙН-ФАЙЛОВ»
|
Path target = resolveSafe(fileName);
|
||||||
// ===============================================================
|
return Files.exists(target);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
public long size(String fileName) {
|
||||||
* Обёртка над newFile: имя формируется из blockchainId + ".bch".
|
Path target = resolveSafe(fileName);
|
||||||
*/
|
try {
|
||||||
public void newBlockchain(long blockchainId, byte[] data) {
|
return Files.size(target);
|
||||||
String fileName = buildBlockchainFileName(blockchainId);
|
} catch (IOException e) {
|
||||||
newFile(fileName, data);
|
throw new IllegalStateException("Не удалось получить размер файла: " + target, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================================================== */
|
||||||
|
/* ===================== Блокчейн-файлы по имени ======================= */
|
||||||
|
/* ===================================================================== */
|
||||||
|
|
||||||
|
/** <blockchainName>.bch */
|
||||||
|
public String buildBlockchainFileName(String blockchainName) {
|
||||||
|
validateSimpleFileName(blockchainName);
|
||||||
|
return blockchainName + BLOCKCHAIN_FILE_EXTENSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** <blockchainName>.tmp_bch */
|
||||||
|
public String buildBlockchainTmpFileName(String blockchainName) {
|
||||||
|
validateSimpleFileName(blockchainName);
|
||||||
|
return blockchainName + BLOCKCHAIN_TMP_EXTENSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path resolveBlockchainPath(String blockchainName) {
|
||||||
|
return resolveSafe(buildBlockchainFileName(blockchainName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path resolveBlockchainTmpPath(String blockchainName) {
|
||||||
|
return resolveSafe(buildBlockchainTmpFileName(blockchainName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] readBlockchain(String blockchainName) {
|
||||||
|
return readAllDataFromFile(buildBlockchainFileName(blockchainName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeBlockchainTmp(String blockchainName, byte[] data) {
|
||||||
|
newFile(buildBlockchainTmpFileName(blockchainName), data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обёртка над addDataToFile: имя формируется из blockchainId + ".bch".
|
* Атомарно заменить основной файл блокчейна временным:
|
||||||
|
* <name>.tmp_bch -> <name>.bch
|
||||||
|
*
|
||||||
|
* Стратегия:
|
||||||
|
* 1) Пытаемся Files.move(..., ATOMIC_MOVE, REPLACE_EXISTING)
|
||||||
|
* 2) Если ATOMIC_MOVE не поддерживается — делаем move с REPLACE_EXISTING без атомарности
|
||||||
|
*
|
||||||
|
* Важный нюанс:
|
||||||
|
* - атомарность гарантируется только в пределах одной файловой системы.
|
||||||
*/
|
*/
|
||||||
public void addDataToBlockchain(long blockchainId, byte[] data) {
|
public void atomicReplaceBlockchainFile(String blockchainName) {
|
||||||
String fileName = buildBlockchainFileName(blockchainId);
|
Path tmp = resolveBlockchainTmpPath(blockchainName);
|
||||||
addDataToFile(fileName, data);
|
Path main = resolveBlockchainPath(blockchainName);
|
||||||
|
|
||||||
|
if (!Files.exists(tmp)) {
|
||||||
|
throw new IllegalStateException("TMP-файл не найден: " + tmp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
try {
|
||||||
* Обёртка над readAllDataFromFile: имя формируется из blockchainId + ".bch".
|
// 1) Пытаемся атомарный move
|
||||||
*/
|
Files.move(tmp, main,
|
||||||
public byte[] readAllDataFromBlockchain(long blockchainId) {
|
StandardCopyOption.REPLACE_EXISTING,
|
||||||
String fileName = buildBlockchainFileName(blockchainId);
|
StandardCopyOption.ATOMIC_MOVE);
|
||||||
return readAllDataFromFile(fileName);
|
} catch (AtomicMoveNotSupportedException e) {
|
||||||
|
// 2) Если ФС не поддерживает атомарный move — делаем обычный replace
|
||||||
|
try {
|
||||||
|
Files.move(tmp, main, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new IllegalStateException("Не удалось заменить файл блокчейна (non-atomic): " + main, ex);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("Не удалось заменить файл блокчейна (atomic): " + main, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================================
|
/* ===================================================================== */
|
||||||
// ВСПОМОГАТЕЛЬНЫЕ
|
/* ============================ Helpers ================================= */
|
||||||
// ===============================================================
|
/* ===================================================================== */
|
||||||
|
|
||||||
private void ensureDataDirExists() {
|
private void ensureDataDirExists() {
|
||||||
try {
|
try {
|
||||||
@ -153,20 +180,21 @@ public final class FileStoreUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Безопасно собрать путь внутри каталога data, запретив подстановку каталогов в имени файла.
|
|
||||||
*/
|
|
||||||
private Path resolveSafe(String fileName) {
|
private Path resolveSafe(String fileName) {
|
||||||
validateSimpleFileName(fileName);
|
validateSimpleFileName(fileName);
|
||||||
return dataDirPath.resolve(fileName);
|
return dataDirPath.resolve(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Простейшая валидация имени файла:
|
* Валидация "простого имени":
|
||||||
* • запретить разделители путей и возврат на родительский каталог.
|
* - запрещаем слэши, обратные слэши, ".."
|
||||||
|
* - запрещаем пустоту
|
||||||
|
*
|
||||||
|
* Важно: сюда у нас попадает и blockchainName (как часть имени файла),
|
||||||
|
* поэтому blockchainName должен быть "простым": без путей.
|
||||||
*/
|
*/
|
||||||
private void validateSimpleFileName(String fileName) {
|
private void validateSimpleFileName(String fileName) {
|
||||||
Objects.requireNonNull(fileName, "Имя файла не должно быть null");
|
Objects.requireNonNull(fileName, "fileName == null");
|
||||||
if (fileName.isBlank()) {
|
if (fileName.isBlank()) {
|
||||||
throw new IllegalArgumentException("Имя файла не должно быть пустым");
|
throw new IllegalArgumentException("Имя файла не должно быть пустым");
|
||||||
}
|
}
|
||||||
@ -174,12 +202,4 @@ public final class FileStoreUtil {
|
|||||||
throw new IllegalArgumentException("Недопустимое имя файла: " + fileName);
|
throw new IllegalArgumentException("Недопустимое имя файла: " + fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Построить имя «блокчейн-файла» из идентификатора и расширения .bch.
|
|
||||||
* Пример: 12345 → "12345.bch"
|
|
||||||
*/
|
|
||||||
private String buildBlockchainFileName(long blockchainId) {
|
|
||||||
return Long.toString(blockchainId) + BLOCKCHAIN_FILE_EXTENSION;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,59 +1,59 @@
|
|||||||
package utils.files;
|
//package utils.files;
|
||||||
|
//
|
||||||
import java.nio.charset.StandardCharsets;
|
//import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
//import java.util.Arrays;
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* ===============================================================
|
// * ===============================================================
|
||||||
* FileStoreUtilSelfTest — запускаемый тест утилиты FileStoreUtil.
|
// * FileStoreUtilSelfTest — запускаемый тест утилиты FileStoreUtil.
|
||||||
* ---------------------------------------------------------------
|
// * ---------------------------------------------------------------
|
||||||
* Сценарий:
|
// * Сценарий:
|
||||||
* 1) Создаём «блокчейн-файл» для id=20251021 с начальными данными.
|
// * 1) Создаём «блокчейн-файл» для id=20251021 с начальными данными.
|
||||||
* 2) Дозаписываем ещё порцию данных.
|
// * 2) Дозаписываем ещё порцию данных.
|
||||||
* 3) Читаем целиком и печатаем длину + превью.
|
// * 3) Читаем целиком и печатаем длину + превью.
|
||||||
*.
|
// *.
|
||||||
* Ожидаемый итог:
|
// * Ожидаемый итог:
|
||||||
* • В папке "data" появится файл "20251021.bch"
|
// * • В папке "data" появится файл "20251021.bch"
|
||||||
* • В консоли будет длина содержимого и небольшой превью-дамп.
|
// * • В консоли будет длина содержимого и небольшой превью-дамп.
|
||||||
* ===============================================================
|
// * ===============================================================
|
||||||
*/
|
// */
|
||||||
public class FileStoreUtilSelfTest {
|
//public class FileStoreUtilSelfTest {
|
||||||
|
//
|
||||||
public static void main(String[] args) {
|
// public static void main(String[] args) {
|
||||||
System.out.println("=== FileStoreUtil self-test ===");
|
// System.out.println("=== FileStoreUtil self-test ===");
|
||||||
|
//
|
||||||
FileStoreUtil fs = FileStoreUtil.getInstance();
|
// FileStoreUtil fs = FileStoreUtil.getInstance();
|
||||||
|
//
|
||||||
long blockchainId = 20251021L;
|
// long blockchainId = 20251021L;
|
||||||
|
//
|
||||||
byte[] part1 = "Hello ".getBytes(StandardCharsets.UTF_8);
|
// byte[] part1 = "Hello ".getBytes(StandardCharsets.UTF_8);
|
||||||
byte[] part2 = "Blockchain!".getBytes(StandardCharsets.UTF_8);
|
// byte[] part2 = "Blockchain!".getBytes(StandardCharsets.UTF_8);
|
||||||
|
//
|
||||||
// 1) создаём новый файл для «блокчейна»
|
// // 1) создаём новый файл для «блокчейна»
|
||||||
fs.newBlockchain(blockchainId, part1);
|
// fs.newBlockchain(blockchainId, part1);
|
||||||
|
//
|
||||||
// 2) дозаписываем данные
|
// // 2) дозаписываем данные
|
||||||
fs.addDataToBlockchain(blockchainId, part2);
|
// fs.addDataToBlockchain(blockchainId, part2);
|
||||||
|
//
|
||||||
// 3) читаем всё содержимое и показываем превью
|
// // 3) читаем всё содержимое и показываем превью
|
||||||
byte[] all = fs.readAllDataFromBlockchain(blockchainId);
|
// byte[] all = fs.readAllDataFromBlockchain(blockchainId);
|
||||||
System.out.println("Total bytes read: " + all.length);
|
// System.out.println("Total bytes read: " + all.length);
|
||||||
System.out.println("Preview (UTF-8): " + new String(all, StandardCharsets.UTF_8));
|
// System.out.println("Preview (UTF-8): " + new String(all, StandardCharsets.UTF_8));
|
||||||
|
//
|
||||||
// небольшой hex-дамп первых 32 байт (для наглядности)
|
// // небольшой hex-дамп первых 32 байт (для наглядности)
|
||||||
System.out.println("Preview (HEX 32B): " + toHexPreview(all, 32));
|
// System.out.println("Preview (HEX 32B): " + toHexPreview(all, 32));
|
||||||
|
//
|
||||||
System.out.println("✅ Self-test passed (файл: data/" + blockchainId + FileStoreUtil.BLOCKCHAIN_FILE_EXTENSION + ")");
|
// System.out.println("✅ Self-test passed (файл: data/" + blockchainId + FileStoreUtil.BLOCKCHAIN_FILE_EXTENSION + ")");
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static String toHexPreview(byte[] data, int max) {
|
// private static String toHexPreview(byte[] data, int max) {
|
||||||
int n = Math.min(data.length, max);
|
// int n = Math.min(data.length, max);
|
||||||
StringBuilder sb = new StringBuilder(n * 2);
|
// StringBuilder sb = new StringBuilder(n * 2);
|
||||||
for (int i = 0; i < n; i++) {
|
// for (int i = 0; i < n; i++) {
|
||||||
sb.append(String.format("%02X", data[i]));
|
// sb.append(String.format("%02X", data[i]));
|
||||||
if (i + 1 < n) sb.append(' ');
|
// if (i + 1 < n) sb.append(' ');
|
||||||
}
|
// }
|
||||||
if (data.length > n) sb.append(" ...");
|
// if (data.length > n) sb.append(" ...");
|
||||||
return sb.toString();
|
// return sb.toString();
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|||||||
@ -6,29 +6,46 @@ import shine.db.dao.BlockchainStateDAO;
|
|||||||
import shine.db.dao.BlocksDAO;
|
import shine.db.dao.BlocksDAO;
|
||||||
import shine.db.entities.BlockEntry;
|
import shine.db.entities.BlockEntry;
|
||||||
import shine.db.entities.BlockchainStateEntry;
|
import shine.db.entities.BlockchainStateEntry;
|
||||||
|
import utils.files.FileStoreUtil;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BlockchainDbWriter — единая точка записи блока + состояния в БД.
|
* BlockchainWriter — единая точка записи:
|
||||||
|
* 1) создаём новый файл <name>.tmp_bch = oldFileBytes + newBlockBytes
|
||||||
|
* 2) атомарно фиксируем в БД:
|
||||||
|
* - blocks (строка блока)
|
||||||
|
* - blockchain_state (включая новый fileSizeBytes)
|
||||||
|
* 3) атомарно заменяем файл:
|
||||||
|
* - удаляем/замещаем старый <name>.bch
|
||||||
|
* - переименовываем <name>.tmp_bch -> <name>.bch
|
||||||
*
|
*
|
||||||
* Важно:
|
* Важно:
|
||||||
* - Здесь обеспечивается атомарность записи: либо вставился блок и обновилось состояние, либо не вставилось ничего.
|
* - Шаг (2) — строго атомарный (SQL tx).
|
||||||
* - Соединение открывается/закрывается внутри (удобно для хэндлера).
|
* - Шаг (3) — атомарный на уровне ФС, если поддерживается ATOMIC_MOVE.
|
||||||
|
* - Если сервер упадёт между (2) и (3), останется tmp — твой recovery при старте починит.
|
||||||
*/
|
*/
|
||||||
public final class BlockchainWriter {
|
public final class BlockchainWriter {
|
||||||
|
|
||||||
private final SqliteDbController db;
|
private final SqliteDbController db;
|
||||||
private final BlocksDAO blocksDAO;
|
private final BlocksDAO blocksDAO;
|
||||||
private final BlockchainStateDAO stateDAO;
|
private final BlockchainStateDAO stateDAO;
|
||||||
|
private final FileStoreUtil fs;
|
||||||
|
|
||||||
public BlockchainWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO) {
|
public BlockchainWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO) {
|
||||||
this.db = SqliteDbController.getInstance();
|
this.db = SqliteDbController.getInstance();
|
||||||
this.blocksDAO = blocksDAO;
|
this.blocksDAO = blocksDAO;
|
||||||
this.stateDAO = stateDAO;
|
this.stateDAO = stateDAO;
|
||||||
|
this.fs = FileStoreUtil.getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Главный метод:
|
||||||
|
* - создаёт tmp-файл (старое+новое),
|
||||||
|
* - атомарно коммитит БД (block+state),
|
||||||
|
* - атомарно заменяет основной файл.
|
||||||
|
*/
|
||||||
public void appendBlockAndState(
|
public void appendBlockAndState(
|
||||||
String login,
|
String login,
|
||||||
String blockchainName,
|
String blockchainName,
|
||||||
@ -38,43 +55,126 @@ public final class BlockchainWriter {
|
|||||||
String newHashHex
|
String newHashHex
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// ШАГ 1. Готовим bytes нового блока (включая signature+hash)
|
||||||
|
// =====================================================================
|
||||||
|
final byte[] newBlockFullBytes = block.toBytes(); // ✅ включает хвост signature+hash
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// ШАГ 2. Считаем новый fileSizeBytes
|
||||||
|
// - если genesis (state == null): старый размер = 0
|
||||||
|
// - иначе берём st.fileSizeBytes
|
||||||
|
// =====================================================================
|
||||||
|
final long oldFileSize = (stOrNull == null) ? 0L : stOrNull.getFileSizeBytes();
|
||||||
|
final long newFileSize = safeAdd(oldFileSize, newBlockFullBytes.length);
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// ШАГ 3. Создаём новый tmp-файл:
|
||||||
|
// tmp = (old file bytes) + (new block bytes)
|
||||||
|
//
|
||||||
|
// Важно:
|
||||||
|
// - читаем старый файл ТОЛЬКО если state не null и size > 0
|
||||||
|
// - если genesis: старого файла нет => tmp = newBlock
|
||||||
|
// =====================================================================
|
||||||
|
final byte[] tmpBytes;
|
||||||
|
if (stOrNull == null || oldFileSize == 0) {
|
||||||
|
// genesis: tmp = только новый блок
|
||||||
|
tmpBytes = newBlockFullBytes;
|
||||||
|
} else {
|
||||||
|
// не genesis: tmp = старый файл + новый блок
|
||||||
|
byte[] oldBytes;
|
||||||
|
try {
|
||||||
|
oldBytes = fs.readBlockchain(blockchainName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Здесь лучше падать: state говорит, что файл есть, а прочитать нельзя.
|
||||||
|
throw new SQLException("Cannot read old blockchain file for: " + blockchainName, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (на будущее) можно проверять согласованность: oldBytes.length == oldFileSize
|
||||||
|
// но ты всё равно будешь делать recovery при старте — оставим как подсказку.
|
||||||
|
|
||||||
|
tmpBytes = concat(oldBytes, newBlockFullBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пишем tmp на диск ДО транзакции БД:
|
||||||
|
// - если сервер упадёт позже — tmp останется, но БД может не успеть обновиться (это ок для recovery)
|
||||||
|
try {
|
||||||
|
fs.writeBlockchainTmp(blockchainName, tmpBytes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new SQLException("Cannot write tmp blockchain file for: " + blockchainName, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// ШАГ 4. АТОМАРНО фиксируем БД:
|
||||||
|
// - UPSERT blocks
|
||||||
|
// - UPSERT blockchain_state (включая fileSizeBytes = newFileSize)
|
||||||
|
// =====================================================================
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
|
|
||||||
boolean oldAutoCommit = c.getAutoCommit();
|
boolean oldAutoCommit = c.getAutoCommit();
|
||||||
c.setAutoCommit(false);
|
c.setAutoCommit(false);
|
||||||
|
|
||||||
|
boolean committed = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) блок
|
// 4.1) вставляем/апдейтим запись блока
|
||||||
insertBlockRow(c, login, blockchainName, prevGlobalHashHex, block);
|
insertBlockRow(c, login, blockchainName, prevGlobalHashHex, block);
|
||||||
|
|
||||||
// 2) state
|
// 4.2) апдейтим состояние (включая fileSizeBytes)
|
||||||
appendState(c, blockchainName, block.recordNumber, stOrNull, newHashHex);
|
appendState(c, blockchainName, block.recordNumber, stOrNull, newHashHex, newFileSize);
|
||||||
|
|
||||||
// 3) commit
|
// 4.3) commit
|
||||||
c.commit();
|
c.commit();
|
||||||
|
committed = true;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
try { c.rollback(); } catch (SQLException ignore) {}
|
try { c.rollback(); } catch (SQLException ignore) {}
|
||||||
|
|
||||||
if (e instanceof SQLException se) throw se;
|
if (e instanceof SQLException se) throw se;
|
||||||
throw new SQLException("appendBlockAndState failed", e);
|
throw new SQLException("appendBlockAndState failed (db tx)", e);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
try { c.setAutoCommit(oldAutoCommit); } catch (SQLException ignore) {}
|
try { c.setAutoCommit(oldAutoCommit); } catch (SQLException ignore) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// ШАГ 5. После успешного коммита БД — атомарно заменяем файл:
|
||||||
|
// <name>.tmp_bch -> <name>.bch
|
||||||
|
//
|
||||||
|
// Если тут упадём:
|
||||||
|
// - БД уже обновлена
|
||||||
|
// - tmp остаётся
|
||||||
|
// - recovery при старте восстановит консистентность
|
||||||
|
// =================================================================
|
||||||
|
if (committed) {
|
||||||
|
try {
|
||||||
|
fs.atomicReplaceBlockchainFile(blockchainName);
|
||||||
|
} catch (Exception moveError) {
|
||||||
|
// Здесь ВАЖНО: мы уже не можем откатить БД.
|
||||||
|
// Оставляем tmp и даём наверх ошибку — клиент увидит internal_error,
|
||||||
|
// а ты при старте починишь файловую часть.
|
||||||
|
throw new SQLException(
|
||||||
|
"DB committed but file replace failed; tmp kept for recovery. blockchainName=" + blockchainName,
|
||||||
|
moveError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обновление состояния blockchain_state (создаём если отсутствует).
|
* Обновление состояния blockchain_state (создаём если отсутствует).
|
||||||
* Пока линии не используются: lineIndex=0 и lineHash = globalHash.
|
* Пока линии не используются: lineIndex=0 и lineHash = globalHash.
|
||||||
|
*
|
||||||
|
* + обновляем fileSizeBytes
|
||||||
*/
|
*/
|
||||||
private void appendState(
|
private void appendState(
|
||||||
Connection c,
|
Connection c,
|
||||||
String blockchainName,
|
String blockchainName,
|
||||||
int globalNumber,
|
int globalNumber,
|
||||||
BlockchainStateEntry stOrNull,
|
BlockchainStateEntry stOrNull,
|
||||||
String newHashHex
|
String newHashHex,
|
||||||
|
long newFileSizeBytes
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
|
|
||||||
BlockchainStateEntry st = stOrNull;
|
BlockchainStateEntry st = stOrNull;
|
||||||
@ -87,14 +187,17 @@ public final class BlockchainWriter {
|
|||||||
st.setLastGlobalNumber(globalNumber);
|
st.setLastGlobalNumber(globalNumber);
|
||||||
st.setLastGlobalHash(newHashHex);
|
st.setLastGlobalHash(newHashHex);
|
||||||
|
|
||||||
// Линии пока не используются: lineIndex=0 и lineHash=globalHash
|
// Линии пока не используются
|
||||||
st.setLastLineNumber(0, globalNumber);
|
st.setLastLineNumber(0, globalNumber);
|
||||||
st.setLastLineHash(0, newHashHex);
|
st.setLastLineHash(0, newHashHex);
|
||||||
|
|
||||||
|
// ✅ ВАЖНО: сохраняем ожидаемый размер файла
|
||||||
|
st.setFileSizeBytes(newFileSizeBytes);
|
||||||
|
|
||||||
// Метка времени обновления
|
// Метка времени обновления
|
||||||
st.setUpdatedAtMs(System.currentTimeMillis());
|
st.setUpdatedAtMs(System.currentTimeMillis());
|
||||||
|
|
||||||
// UPSERT состояния
|
// UPSERT
|
||||||
stateDAO.upsert(c, st);
|
stateDAO.upsert(c, st);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,33 +214,48 @@ public final class BlockchainWriter {
|
|||||||
|
|
||||||
BlockEntry e = new BlockEntry();
|
BlockEntry e = new BlockEntry();
|
||||||
|
|
||||||
// Кому принадлежит блок (логин владельца цепочки)
|
|
||||||
e.setLogin(login);
|
e.setLogin(login);
|
||||||
e.setBchName(blockchainName);
|
e.setBchName(blockchainName);
|
||||||
|
|
||||||
// Глобальная нумерация
|
|
||||||
e.setBlockGlobalNumber(block.recordNumber);
|
e.setBlockGlobalNumber(block.recordNumber);
|
||||||
e.setBlockGlobalPreHashe(prevGlobalHashHex);
|
e.setBlockGlobalPreHashe(prevGlobalHashHex);
|
||||||
|
|
||||||
// Линии пока не используются: lineIndex=0, lineNumber=globalNumber
|
// линии пока не используем
|
||||||
e.setBlockLineIndex(0);
|
e.setBlockLineIndex(0);
|
||||||
e.setBlockLineNumber(block.recordNumber);
|
e.setBlockLineNumber(block.recordNumber);
|
||||||
e.setBlockLinePreHashe(prevGlobalHashHex);
|
e.setBlockLinePreHashe(prevGlobalHashHex);
|
||||||
|
|
||||||
// msgType у тебя пока 0 (при желании позже можно ставить по Body/type)
|
// тип сообщения — по body.type()
|
||||||
// ✅ Теперь сохраняем тип блока
|
|
||||||
e.setMsgType(block.body.type());
|
e.setMsgType(block.body.type());
|
||||||
|
|
||||||
// Сырые байты полного блока
|
// полный блок (RAW + signature + hash)
|
||||||
e.setBlockByte(block.toBytes());
|
e.setBlockByte(block.toBytes());
|
||||||
|
|
||||||
// Поля "кому" (для сообщений/трансферов) пока пустые
|
|
||||||
e.setToLogin(null);
|
e.setToLogin(null);
|
||||||
e.setToBchName(null);
|
e.setToBchName(null);
|
||||||
e.setToBlockGlobalNumber(null);
|
e.setToBlockGlobalNumber(null);
|
||||||
e.setToBlockHashe(null);
|
e.setToBlockHashe(null);
|
||||||
|
|
||||||
// UPSERT блока
|
|
||||||
blocksDAO.upsert(c, e);
|
blocksDAO.upsert(c, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===================================================================== */
|
||||||
|
/* =============================== Utils ================================ */
|
||||||
|
/* ===================================================================== */
|
||||||
|
|
||||||
|
private static byte[] concat(byte[] a, byte[] b) {
|
||||||
|
byte[] out = new byte[a.length + b.length];
|
||||||
|
System.arraycopy(a, 0, out, 0, a.length);
|
||||||
|
System.arraycopy(b, 0, out, a.length, b.length);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long safeAdd(long x, long y) {
|
||||||
|
// защита от переполнения long (маловероятно, но пусть будет)
|
||||||
|
long r = x + y;
|
||||||
|
if (((x ^ r) & (y ^ r)) < 0) {
|
||||||
|
throw new IllegalArgumentException("fileSizeBytes overflow: " + x + " + " + y);
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -18,9 +18,9 @@ public final class InboundMessageProcessor {
|
|||||||
private static final Logger log = LoggerFactory.getLogger(InboundMessageProcessor.class);
|
private static final Logger log = LoggerFactory.getLogger(InboundMessageProcessor.class);
|
||||||
|
|
||||||
private static final Map<Integer, MessageHandler> HANDLERS = Map.of(
|
private static final Map<Integer, MessageHandler> HANDLERS = Map.of(
|
||||||
WireCodes.Op.PING, new PingHandler(),
|
WireCodes.Op.PING, new PingHandler()
|
||||||
// WireCodes.Op.ADD_BLOCK, new AddBlockHandler(),
|
// WireCodes.Op.ADD_BLOCK, new AddBlockHandler(),
|
||||||
WireCodes.Op.GET_BLOCKCHAIN,new GetBlockchainHandler()
|
// WireCodes.Op.GET_BLOCKCHAIN,new GetBlockchainHandler()
|
||||||
// WireCodes.Op.SEARCH_USERS, new SearchUsersHandler(),
|
// WireCodes.Op.SEARCH_USERS, new SearchUsersHandler(),
|
||||||
// WireCodes.Op.GET_LAST_BLOCK_INFO,new GetLastBlockInfoHandler()
|
// WireCodes.Op.GET_LAST_BLOCK_INFO,new GetLastBlockInfoHandler()
|
||||||
|
|
||||||
|
|||||||
@ -1,53 +1,53 @@
|
|||||||
package server.logic.ws_protocol.binary.handlers;
|
//package server.logic.ws_protocol.binary.handlers;
|
||||||
|
//
|
||||||
import org.slf4j.Logger;
|
//import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
//import org.slf4j.LoggerFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
//import server.logic.ws_protocol.WireCodes;
|
||||||
import utils.files.FileStoreUtil;
|
//import utils.files.FileStoreUtil;
|
||||||
|
//
|
||||||
import java.nio.ByteBuffer;
|
//import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
//import java.nio.ByteOrder;
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* Возврат полного содержимого блокчейна (GET_BLOCKCHAIN).
|
// * Возврат полного содержимого блокчейна (GET_BLOCKCHAIN).
|
||||||
*/
|
// */
|
||||||
public class GetBlockchainHandler implements MessageHandler {
|
//public class GetBlockchainHandler implements MessageHandler {
|
||||||
private static final Logger log = LoggerFactory.getLogger(GetBlockchainHandler.class);
|
// private static final Logger log = LoggerFactory.getLogger(GetBlockchainHandler.class);
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public byte[] handle(byte[] msg) {
|
// public byte[] handle(byte[] msg) {
|
||||||
try {
|
// try {
|
||||||
if (msg.length < 12)
|
// if (msg.length < 12)
|
||||||
return intTo4Bytes(WireCodes.Status.BAD_REQUEST);
|
// return intTo4Bytes(WireCodes.Status.BAD_REQUEST);
|
||||||
|
//
|
||||||
long id = ByteBuffer.wrap(msg, 4, 8)
|
// long id = ByteBuffer.wrap(msg, 4, 8)
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
// .order(ByteOrder.BIG_ENDIAN)
|
||||||
.getLong();
|
// .getLong();
|
||||||
|
//
|
||||||
FileStoreUtil fs = FileStoreUtil.getInstance();
|
// FileStoreUtil fs = FileStoreUtil.getInstance();
|
||||||
byte[] data = fs.readAllDataFromBlockchain(id);
|
// byte[] data = fs.readAllDataFromBlockchain(id);
|
||||||
|
//
|
||||||
return packOk(data);
|
// return packOk(data);
|
||||||
|
//
|
||||||
} catch (IllegalStateException e) {
|
// } catch (IllegalStateException e) {
|
||||||
log.warn("GET_BLOCKCHAIN: файл не найден ({})", e.getMessage());
|
// log.warn("GET_BLOCKCHAIN: файл не найден ({})", e.getMessage());
|
||||||
return intTo4Bytes(WireCodes.Status.CHAIN_NOT_FOUND);
|
// return intTo4Bytes(WireCodes.Status.CHAIN_NOT_FOUND);
|
||||||
} catch (Exception e) {
|
// } catch (Exception e) {
|
||||||
log.error("GET_BLOCKCHAIN: ошибка", e);
|
// log.error("GET_BLOCKCHAIN: ошибка", e);
|
||||||
return intTo4Bytes(WireCodes.Status.INTERNAL_ERROR);
|
// return intTo4Bytes(WireCodes.Status.INTERNAL_ERROR);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static byte[] packOk(byte[] data) {
|
// private static byte[] packOk(byte[] data) {
|
||||||
if (data == null) data = new byte[0];
|
// if (data == null) data = new byte[0];
|
||||||
ByteBuffer out = ByteBuffer.allocate(8 + data.length).order(ByteOrder.BIG_ENDIAN);
|
// ByteBuffer out = ByteBuffer.allocate(8 + data.length).order(ByteOrder.BIG_ENDIAN);
|
||||||
out.putInt(WireCodes.Status.OK);
|
// out.putInt(WireCodes.Status.OK);
|
||||||
out.putInt(data.length);
|
// out.putInt(data.length);
|
||||||
out.put(data);
|
// out.put(data);
|
||||||
return out.array();
|
// return out.array();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static byte[] intTo4Bytes(int code) {
|
// private static byte[] intTo4Bytes(int code) {
|
||||||
return ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(code).array();
|
// return ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(code).array();
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user