SHA256
16 12 25
Промежуточная версия и ТУДУ на чём остановился
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package utils.blockchain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* BchInfoEntry — данные об одной цепочке блокчейна.
|
||||
* Используется менеджером BchInfoManager.
|
||||
*/
|
||||
public final class BchInfoEntry {
|
||||
|
||||
@JsonProperty("blockchainId")
|
||||
public final long blockchainId;
|
||||
|
||||
@JsonProperty("userLogin")
|
||||
public final String userLogin;
|
||||
|
||||
@JsonProperty("publicKeyBase64")
|
||||
public final String publicKeyBase64;
|
||||
|
||||
@JsonProperty("blockchainSizeLimit")
|
||||
public final int blockchainSizeLimit;
|
||||
|
||||
@JsonProperty("blockchainSize")
|
||||
public final int blockchainSize;
|
||||
|
||||
@JsonProperty("lastBlockNumber")
|
||||
public final int lastBlockNumber;
|
||||
|
||||
@JsonProperty("lastBlockHash")
|
||||
public final String lastBlockHash;
|
||||
|
||||
@JsonCreator
|
||||
public BchInfoEntry(
|
||||
@JsonProperty("blockchainId") long blockchainId,
|
||||
@JsonProperty("userLogin") String userLogin,
|
||||
@JsonProperty("publicKeyBase64") String publicKeyBase64,
|
||||
@JsonProperty("blockchainSizeLimit") int blockchainSizeLimit,
|
||||
@JsonProperty("blockchainSize") int blockchainSize,
|
||||
@JsonProperty("lastBlockNumber") int lastBlockNumber,
|
||||
@JsonProperty("lastBlockHash") String lastBlockHash
|
||||
) {
|
||||
this.blockchainId = blockchainId;
|
||||
this.userLogin = userLogin == null ? "" : userLogin;
|
||||
this.publicKeyBase64 = publicKeyBase64;
|
||||
this.blockchainSizeLimit = blockchainSizeLimit;
|
||||
this.blockchainSize = blockchainSize;
|
||||
this.lastBlockNumber = lastBlockNumber;
|
||||
this.lastBlockHash = lastBlockHash == null ? "" : lastBlockHash;
|
||||
}
|
||||
|
||||
/** Публичный ключ в бинарном виде (32 байта) или null, если битый. */
|
||||
public byte[] getPublicKey32() {
|
||||
try {
|
||||
byte[] raw = Base64.getDecoder().decode(publicKeyBase64);
|
||||
return (raw != null && raw.length == 32) ? raw : null;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package utils.blockchain;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* BchInfoManager — Singleton.
|
||||
*.
|
||||
* Держит в памяти информацию обо всех блокчейнах.
|
||||
* Сейчас читает и пишет JSON на диск (data/blockchain_info.json).
|
||||
* В будущем можно заменить на SQL без изменений в остальном коде.
|
||||
*/
|
||||
public final class BchInfoManager {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BchInfoManager.class);
|
||||
|
||||
private static final String FILE_NAME = "blockchain_info.json";
|
||||
private static final Path DATA_DIR = Paths.get("data");
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
private static BchInfoManager instance;
|
||||
|
||||
/** blockchainId → запись о цепочке */
|
||||
private final Map<Long, BchInfoEntry> records = new LinkedHashMap<>();
|
||||
private final Path path = DATA_DIR.resolve(FILE_NAME);
|
||||
|
||||
private BchInfoManager() {
|
||||
ensureDataDir();
|
||||
load();
|
||||
}
|
||||
|
||||
public static synchronized BchInfoManager getInstance() {
|
||||
if (instance == null) instance = new BchInfoManager();
|
||||
return instance;
|
||||
}
|
||||
|
||||
// ========== API ==========
|
||||
|
||||
/** Создать новую цепочку (после первого HEADER-блока). */
|
||||
public synchronized void addBlockchain(long blockchainId,
|
||||
String userLogin,
|
||||
byte[] publicKey32,
|
||||
int blockchainSizeLimit) {
|
||||
if (publicKey32 == null || publicKey32.length != 32)
|
||||
throw new IllegalArgumentException("publicKey32 must be 32 bytes");
|
||||
if (records.containsKey(blockchainId))
|
||||
throw new IllegalArgumentException("blockchain already exists: " + blockchainId);
|
||||
|
||||
BchInfoEntry entry = new BchInfoEntry(
|
||||
blockchainId,
|
||||
userLogin,
|
||||
Base64.getEncoder().encodeToString(publicKey32),
|
||||
blockchainSizeLimit,
|
||||
0, 0, ""
|
||||
);
|
||||
|
||||
records.put(blockchainId, entry);
|
||||
log.info("Добавлен блокчейн id={} login='{}' (лимит {})", blockchainId, userLogin, blockchainSizeLimit);
|
||||
save();
|
||||
}
|
||||
|
||||
/** Обновить состояние после добавления нового блока. */
|
||||
public synchronized void updateBlockchainState(long blockchainId,
|
||||
int lastBlockNumber,
|
||||
String lastBlockHash,
|
||||
int blockchainSize) {
|
||||
BchInfoEntry prev = getEntryOrThrow(blockchainId);
|
||||
|
||||
BchInfoEntry updated = new BchInfoEntry(
|
||||
prev.blockchainId,
|
||||
prev.userLogin,
|
||||
prev.publicKeyBase64,
|
||||
prev.blockchainSizeLimit,
|
||||
blockchainSize,
|
||||
lastBlockNumber,
|
||||
lastBlockHash
|
||||
);
|
||||
|
||||
records.put(blockchainId, updated);
|
||||
log.info("Обновлено состояние id={} lastNum={} hash={} size={}",
|
||||
blockchainId, lastBlockNumber, lastBlockHash, blockchainSize);
|
||||
save();
|
||||
}
|
||||
|
||||
/** Получить полную информацию по blockchainId. */
|
||||
public synchronized BchInfoEntry getBchInfo(long blockchainId) {
|
||||
return records.get(blockchainId);
|
||||
}
|
||||
|
||||
/** Быстро проверить существование цепочки. */
|
||||
public synchronized boolean exists(long blockchainId) {
|
||||
return records.containsKey(blockchainId);
|
||||
}
|
||||
|
||||
/** id → userLogin (для поиска пользователей). */
|
||||
public synchronized Map<Long, String> getAllLoginsSnapshot() {
|
||||
Map<Long, String> copy = new LinkedHashMap<>(records.size());
|
||||
for (var e : records.entrySet()) {
|
||||
copy.put(e.getKey(), e.getValue().userLogin);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
// ========== private ==========
|
||||
|
||||
private BchInfoEntry getEntryOrThrow(long blockchainId) {
|
||||
BchInfoEntry e = records.get(blockchainId);
|
||||
if (e == null) throw new IllegalStateException("Блокчейн с id=" + blockchainId + " не найден.");
|
||||
return e;
|
||||
}
|
||||
|
||||
private void ensureDataDir() {
|
||||
try {
|
||||
if (!Files.exists(DATA_DIR)) {
|
||||
Files.createDirectories(DATA_DIR);
|
||||
log.info("Создана директория данных: {}", DATA_DIR);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Не удалось создать директорию хранения: " + DATA_DIR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void load() {
|
||||
if (!Files.exists(path)) {
|
||||
save();
|
||||
log.info("Создан JSON-хранилище: {}", path);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
byte[] json = Files.readAllBytes(path);
|
||||
if (json.length == 0) return;
|
||||
|
||||
Map<String, BchInfoEntry> map = MAPPER.readValue(
|
||||
json,
|
||||
MAPPER.getTypeFactory().constructMapType(Map.class, String.class, BchInfoEntry.class)
|
||||
);
|
||||
|
||||
records.clear();
|
||||
for (var e : map.entrySet()) {
|
||||
try {
|
||||
long id = Long.parseLong(e.getKey());
|
||||
records.put(id, e.getValue());
|
||||
} catch (NumberFormatException nfe) {
|
||||
log.warn("Пропущен некорректный ключ '{}' в JSON", e.getKey());
|
||||
}
|
||||
}
|
||||
log.info("Загружено {} записей из {}", records.size(), path);
|
||||
} catch (IOException e) {
|
||||
log.error("Ошибка загрузки {}", path, e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Атомарная запись JSON через .tmp + ATOMIC_MOVE */
|
||||
private synchronized void save() {
|
||||
try {
|
||||
Map<String, BchInfoEntry> map = new LinkedHashMap<>();
|
||||
for (var e : records.entrySet())
|
||||
map.put(String.valueOf(e.getKey()), e.getValue());
|
||||
|
||||
byte[] json = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsBytes(map);
|
||||
|
||||
Path tmp = path.resolveSibling(FILE_NAME + ".tmp");
|
||||
Files.write(tmp, json, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
Files.move(tmp, path, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
|
||||
log.debug("Сохранено {} записей в {}", records.size(), path);
|
||||
} catch (IOException e) {
|
||||
log.error("Ошибка сохранения {}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# utils.blockchain
|
||||
|
||||
Хранит состояние всех цепочек и их владельцев (логин, ключ, хэш, размер).
|
||||
|
||||
---
|
||||
|
||||
## Классы
|
||||
|
||||
### `BchInfoManager`
|
||||
Singleton-менеджер всех известных цепочек.
|
||||
Хранит карту `blockchainId → BchInfoEntry` в файле `data/blockchain_info.json`.
|
||||
Обновляется после каждого добавления блока.
|
||||
|
||||
Главные методы:
|
||||
- `getInstance()` — получить менеджер.
|
||||
- `addBlockchain(id, login, key32, limit)` — создать новую цепочку (первый блок).
|
||||
- `updateBlockchainState(id, num, hash, size)` — обновить состояние после блока.
|
||||
- `getBchInfo(id)` — получить всю запись (`BchInfoEntry`) по цепочке.
|
||||
- `getAllLoginsSnapshot()` — карта `id → login` для поиска.
|
||||
|
||||
---
|
||||
|
||||
### `BchInfoEntry`
|
||||
Сущность одной цепочки:
|
||||
- `blockchainId`
|
||||
- `userLogin`
|
||||
- `publicKeyBase64`
|
||||
- `blockchainSizeLimit`
|
||||
- `blockchainSize`
|
||||
- `lastBlockNumber`
|
||||
- `lastBlockHash`
|
||||
|
||||
Метод:
|
||||
- `getPublicKey32()` — вернуть ключ в бинарном виде.
|
||||
|
||||
---
|
||||
|
||||
## Итого
|
||||
- Менеджер хранит метаданные всех цепочек.
|
||||
- Сущность описывает одну цепочку.
|
||||
- Сейчас всё лежит в JSON, позже можно заменить на SQL без изменений кода.
|
||||
@@ -0,0 +1,24 @@
|
||||
# TODO
|
||||
|
||||
1. Проверка перед созданием нового блокчейна
|
||||
|
||||
Сейчас:
|
||||
- Метод `BlockchainIdInfo.addBlockchain(...)` просто добавляет новую цепочку (blockchainId + userLogin + publicKey32) в локальное хранилище.
|
||||
- Любой первый блок (HEADER, recordNumber = 0) с валидной подписью автоматически создаёт новую запись.
|
||||
|
||||
Что нужно сделать в будущем:
|
||||
- Перед созданием новой цепочки проверять, что этот пользователь реально зарегистрирован в системе и имеет право открыть блокчейн.
|
||||
- Проверять, что `userLogin` и `publicKey32` совпадают с тем, что у нас уже привязано к этому пользователю.
|
||||
- Если пользователь не найден или ключ не совпадает — отказ, цепочку не создавать.
|
||||
|
||||
Идея: `handleAddBlock(...)` должен вызывать будущий Auth/Users сервис до `addBlockchain(...)`.
|
||||
|
||||
|
||||
2. Перенос хранения из файлов в базу SQL
|
||||
|
||||
Сейчас:
|
||||
- Блоки пишутся в файл `data/<blockchainId>.bch` через `FileStoreUtil`.
|
||||
- Метаданные по цепочке (логин, публичный ключ, последний номер блока, последний hash, размер и т.д.) хранятся в `data/blockchain_id_info.json` через `BlockchainIdInfo`.
|
||||
|
||||
Что нужно сделать:
|
||||
- Убрать файловое хранение и перейти на SQL.
|
||||
@@ -0,0 +1,185 @@
|
||||
package utils.files;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* ===============================================================
|
||||
* FileStoreUtil — синглтон-утилита для записи/дозаписи/чтения файлов.
|
||||
* ---------------------------------------------------------------
|
||||
* Где хранит:
|
||||
* • Все файлы размещаются в внешней папке DATA_DIR = "data" (в корне запуска).
|
||||
* Папка создаётся автоматически при первом обращении.
|
||||
*.
|
||||
* Что умеет:
|
||||
* • newFile(String fileName, byte[] data)
|
||||
* - создаёт/переписывает файл с именем fileName и записывает data.
|
||||
* • addDataToFile(String fileName, byte[] data)
|
||||
* - дописывает 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 static final String DATA_DIR_NAME = "data";
|
||||
/** Расширение файлов «блокчейнов». */
|
||||
public static final String BLOCKCHAIN_FILE_EXTENSION = ".bch";
|
||||
|
||||
private static final FileStoreUtil INSTANCE = new FileStoreUtil();
|
||||
|
||||
private final Path dataDirPath;
|
||||
|
||||
private FileStoreUtil() {
|
||||
this.dataDirPath = Paths.get(DATA_DIR_NAME);
|
||||
ensureDataDirExists();
|
||||
}
|
||||
|
||||
/** Получить единственный экземпляр утилиты. */
|
||||
public static FileStoreUtil getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// ОБЩИЕ МЕТОДЫ РАБОТЫ С ФАЙЛОМ
|
||||
// ===============================================================
|
||||
|
||||
/**
|
||||
* Создать/переписать файл и записать в него массив байт.
|
||||
* @param fileName имя файла (без каталогов)
|
||||
* @param data содержимое
|
||||
* @throws IllegalArgumentException при неверном имени или null-данных
|
||||
* @throws IllegalStateException при ошибках ввода/вывода
|
||||
*/
|
||||
public void newFile(String fileName, byte[] data) {
|
||||
Objects.requireNonNull(data, "Данные не должны быть null");
|
||||
Path target = resolveSafe(fileName);
|
||||
try {
|
||||
Files.write(target, data, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Не удалось записать файл: " + target, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Дозаписать массив байт в конец файла (создаст файл, если отсутствует).
|
||||
* @param fileName имя файла (без каталогов)
|
||||
* @param data добавляемые данные
|
||||
* @throws IllegalArgumentException при неверном имени или null-данных
|
||||
* @throws IllegalStateException при ошибках ввода/вывода
|
||||
*/
|
||||
public void addDataToFile(String fileName, byte[] data) {
|
||||
Objects.requireNonNull(data, "Данные не должны быть null");
|
||||
Path target = resolveSafe(fileName);
|
||||
try {
|
||||
Files.write(target, data,
|
||||
StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Не удалось дописать файл: " + target, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Прочитать весь файл в память и вернуть как byte[].
|
||||
* @param fileName имя файла (без каталогов)
|
||||
* @return содержимое файла
|
||||
* @throws IllegalStateException если файл не существует или ошибка ввода/вывода
|
||||
*/
|
||||
public byte[] readAllDataFromFile(String fileName) {
|
||||
Path target = resolveSafe(fileName);
|
||||
if (!Files.exists(target)) {
|
||||
throw new IllegalStateException("Файл не найден: " + target);
|
||||
}
|
||||
try {
|
||||
return Files.readAllBytes(target);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Не удалось прочитать файл: " + target, e);
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// ОБЁРТКИ ДЛЯ «БЛОКЧЕЙН-ФАЙЛОВ»
|
||||
// ===============================================================
|
||||
|
||||
/**
|
||||
* Обёртка над newFile: имя формируется из blockchainId + ".bch".
|
||||
*/
|
||||
public void newBlockchain(long blockchainId, byte[] data) {
|
||||
String fileName = buildBlockchainFileName(blockchainId);
|
||||
newFile(fileName, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обёртка над addDataToFile: имя формируется из blockchainId + ".bch".
|
||||
*/
|
||||
public void addDataToBlockchain(long blockchainId, byte[] data) {
|
||||
String fileName = buildBlockchainFileName(blockchainId);
|
||||
addDataToFile(fileName, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обёртка над readAllDataFromFile: имя формируется из blockchainId + ".bch".
|
||||
*/
|
||||
public byte[] readAllDataFromBlockchain(long blockchainId) {
|
||||
String fileName = buildBlockchainFileName(blockchainId);
|
||||
return readAllDataFromFile(fileName);
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// ВСПОМОГАТЕЛЬНЫЕ
|
||||
// ===============================================================
|
||||
|
||||
private void ensureDataDirExists() {
|
||||
try {
|
||||
if (!Files.exists(dataDirPath)) {
|
||||
Files.createDirectories(dataDirPath);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Не удалось создать директорию хранения: " + dataDirPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Безопасно собрать путь внутри каталога data, запретив подстановку каталогов в имени файла.
|
||||
*/
|
||||
private Path resolveSafe(String fileName) {
|
||||
validateSimpleFileName(fileName);
|
||||
return dataDirPath.resolve(fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Простейшая валидация имени файла:
|
||||
* • запретить разделители путей и возврат на родительский каталог.
|
||||
*/
|
||||
private void validateSimpleFileName(String fileName) {
|
||||
Objects.requireNonNull(fileName, "Имя файла не должно быть null");
|
||||
if (fileName.isBlank()) {
|
||||
throw new IllegalArgumentException("Имя файла не должно быть пустым");
|
||||
}
|
||||
if (fileName.contains("/") || fileName.contains("\\") || fileName.contains("..")) {
|
||||
throw new IllegalArgumentException("Недопустимое имя файла: " + fileName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Построить имя «блокчейн-файла» из идентификатора и расширения .bch.
|
||||
* Пример: 12345 → "12345.bch"
|
||||
*/
|
||||
private String buildBlockchainFileName(long blockchainId) {
|
||||
return Long.toString(blockchainId) + BLOCKCHAIN_FILE_EXTENSION;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package utils.files;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* ===============================================================
|
||||
* FileStoreUtilSelfTest — запускаемый тест утилиты FileStoreUtil.
|
||||
* ---------------------------------------------------------------
|
||||
* Сценарий:
|
||||
* 1) Создаём «блокчейн-файл» для id=20251021 с начальными данными.
|
||||
* 2) Дозаписываем ещё порцию данных.
|
||||
* 3) Читаем целиком и печатаем длину + превью.
|
||||
*.
|
||||
* Ожидаемый итог:
|
||||
* • В папке "data" появится файл "20251021.bch"
|
||||
* • В консоли будет длина содержимого и небольшой превью-дамп.
|
||||
* ===============================================================
|
||||
*/
|
||||
public class FileStoreUtilSelfTest {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== FileStoreUtil self-test ===");
|
||||
|
||||
FileStoreUtil fs = FileStoreUtil.getInstance();
|
||||
|
||||
long blockchainId = 20251021L;
|
||||
|
||||
byte[] part1 = "Hello ".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] part2 = "Blockchain!".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// 1) создаём новый файл для «блокчейна»
|
||||
fs.newBlockchain(blockchainId, part1);
|
||||
|
||||
// 2) дозаписываем данные
|
||||
fs.addDataToBlockchain(blockchainId, part2);
|
||||
|
||||
// 3) читаем всё содержимое и показываем превью
|
||||
byte[] all = fs.readAllDataFromBlockchain(blockchainId);
|
||||
System.out.println("Total bytes read: " + all.length);
|
||||
System.out.println("Preview (UTF-8): " + new String(all, StandardCharsets.UTF_8));
|
||||
|
||||
// небольшой hex-дамп первых 32 байт (для наглядности)
|
||||
System.out.println("Preview (HEX 32B): " + toHexPreview(all, 32));
|
||||
|
||||
System.out.println("✅ Self-test passed (файл: data/" + blockchainId + FileStoreUtil.BLOCKCHAIN_FILE_EXTENSION + ")");
|
||||
}
|
||||
|
||||
private static String toHexPreview(byte[] data, int max) {
|
||||
int n = Math.min(data.length, max);
|
||||
StringBuilder sb = new StringBuilder(n * 2);
|
||||
for (int i = 0; i < n; i++) {
|
||||
sb.append(String.format("%02X", data[i]));
|
||||
if (i + 1 < n) sb.append(' ');
|
||||
}
|
||||
if (data.length > n) sb.append(" ...");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# utils.files
|
||||
|
||||
Хранение блокчейнов в виде файлов в папке `data/`.
|
||||
|
||||
---
|
||||
|
||||
## FileStoreUtil
|
||||
Singleton для чтения и записи `.bch` файлов.
|
||||
|
||||
- `newBlockchain(id, data)` — создать новый файл `data/<id>.bch`
|
||||
- `addDataToBlockchain(id, data)` — добавить байты в конец файла
|
||||
- `readAllDataFromBlockchain(id)` — прочитать весь файл как массив байт
|
||||
|
||||
Каждый файл содержит последовательность полных блоков `[RAW][signature64][hash32]...`
|
||||
|
||||
---
|
||||
|
||||
## FileStoreUtilSelfTest
|
||||
Тест: создаёт, дописывает и читает файл, чтобы проверить корректность.
|
||||
|
||||
---
|
||||
|
||||
Пока используется как временное файловое хранилище.
|
||||
В будущем всё это уйдёт в SQL.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
В будущем всё это уйдёт в SQL.
|
||||
|
||||
|
||||
|
||||
TODO И проработать вот эту проблему
|
||||
|
||||
есть вариант тто при врнезапном жёстком завершении приложения, может дописаться в конец файла только половина записи и это будет жёсткой ошибкой
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# utils.search
|
||||
|
||||
Поиск пользователей по логину среди зарегистрированных блокчейнов.
|
||||
|
||||
---
|
||||
|
||||
## UserSearchService
|
||||
Singleton-сервис для поиска первых 5 логинов, содержащих подстроку (без учёта регистра).
|
||||
|
||||
Основные методы:
|
||||
- `searchFirst5(String query)` — вернуть список `Pair(id, login)`
|
||||
- `packPair(Pair p)` — упаковать пару в бинарный ответ `[8]id + [1]len + [len]login`
|
||||
|
||||
Используется сервером в операции `SEARCH_USERS (op=30)` для ответа клиенту.
|
||||
@@ -0,0 +1,70 @@
|
||||
package utils.search;
|
||||
|
||||
|
||||
import utils.blockchain.BchInfoManager;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* UserSearchService — поиск первых 5 пользователей по подстроке логина (без учёта регистра).
|
||||
*/
|
||||
public final class UserSearchService {
|
||||
|
||||
private static final UserSearchService INSTANCE = new UserSearchService();
|
||||
private UserSearchService() {}
|
||||
public static UserSearchService getInstance() { return INSTANCE; }
|
||||
|
||||
/** Результат одной пары: id + исходный login (с родным регистром). */
|
||||
public static final class Pair {
|
||||
public final long blockchainId;
|
||||
public final String userLogin;
|
||||
public Pair(long blockchainId, String userLogin) {
|
||||
this.blockchainId = blockchainId;
|
||||
this.userLogin = userLogin;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Найти первые до 5 логинов, содержащих подстроку (case-insensitive).
|
||||
*/
|
||||
public List<Pair> searchFirst5(String query) {
|
||||
String q = (query == null ? "" : query).toLowerCase(Locale.ROOT).trim();
|
||||
List<Pair> out = new ArrayList<>(5);
|
||||
if (q.isEmpty()) return out;
|
||||
|
||||
// берём снапшот id→login
|
||||
Map<Long, String> all = BchInfoManager.getInstance().getAllLoginsSnapshot();
|
||||
|
||||
for (var e : all.entrySet()) {
|
||||
if (out.size() >= 5) break;
|
||||
String login = e.getValue() == null ? "" : e.getValue();
|
||||
if (login.toLowerCase(Locale.ROOT).contains(q)) {
|
||||
out.add(new Pair(e.getKey(), login));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Упаковка пары в байтовый формат ответа: [8] id + [1] L + [L] login UTF-8 (L<=255)
|
||||
public static byte[] packPair(Pair p) {
|
||||
byte[] loginUtf8 = (p.userLogin == null ? "" : p.userLogin).getBytes(StandardCharsets.UTF_8);
|
||||
int L = Math.min(loginUtf8.length, 255);
|
||||
byte[] b = new byte[8 + 1 + L];
|
||||
// beLong
|
||||
b[0]=(byte)((p.blockchainId>>>56)&0xFF);
|
||||
b[1]=(byte)((p.blockchainId>>>48)&0xFF);
|
||||
b[2]=(byte)((p.blockchainId>>>40)&0xFF);
|
||||
b[3]=(byte)((p.blockchainId>>>32)&0xFF);
|
||||
b[4]=(byte)((p.blockchainId>>>24)&0xFF);
|
||||
b[5]=(byte)((p.blockchainId>>>16)&0xFF);
|
||||
b[6]=(byte)((p.blockchainId>>>8 )&0xFF);
|
||||
b[7]=(byte)((p.blockchainId )&0xFF);
|
||||
b[8]=(byte)L;
|
||||
System.arraycopy(loginUtf8, 0, b, 9, L);
|
||||
return b;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user