forked from dlang/cdcdb
		
	init
This commit is contained in:
		
						commit
						dc0c8349c7
					
				
					 18 changed files with 666 additions and 0 deletions
				
			
		
							
								
								
									
										18
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
.dub
 | 
			
		||||
docs.json
 | 
			
		||||
__dummy.html
 | 
			
		||||
docs/
 | 
			
		||||
/cdcdb
 | 
			
		||||
cdcdb.so
 | 
			
		||||
cdcdb.dylib
 | 
			
		||||
cdcdb.dll
 | 
			
		||||
cdcdb.a
 | 
			
		||||
cdcdb.lib
 | 
			
		||||
cdcdb-test-*
 | 
			
		||||
*.exe
 | 
			
		||||
*.pdb
 | 
			
		||||
*.o
 | 
			
		||||
*.obj
 | 
			
		||||
*.lst
 | 
			
		||||
bin
 | 
			
		||||
lib
 | 
			
		||||
							
								
								
									
										37
									
								
								.vscode/launch.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								.vscode/launch.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
{
 | 
			
		||||
	// Используйте IntelliSense, чтобы узнать о возможных атрибутах.
 | 
			
		||||
	// Наведите указатель мыши, чтобы просмотреть описания существующих атрибутов.
 | 
			
		||||
	// Для получения дополнительной информации посетите: https://go.microsoft.com/fwlink/?linkid=830387
 | 
			
		||||
	"version": "0.2.0",
 | 
			
		||||
	"configurations": [
 | 
			
		||||
		{
 | 
			
		||||
			"type": "code-d",
 | 
			
		||||
			"request": "launch",
 | 
			
		||||
			"dubBuild": true,
 | 
			
		||||
			"name": "Build & Debug DUB project",
 | 
			
		||||
			"cwd": "${command:dubWorkingDirectory}",
 | 
			
		||||
			"program": "bin/${command:dubTarget}",
 | 
			
		||||
			"args": []
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"name": "Debug D Program with sudo-gdb",
 | 
			
		||||
			"type": "cppdbg",
 | 
			
		||||
			"request": "launch",
 | 
			
		||||
			"program": "${workspaceFolder}/bin/dwatch",
 | 
			
		||||
			"args": ["-d", "/tmp/scripts"], // Аргументы командной строки для программы, если нужны
 | 
			
		||||
			"stopAtEntry": false, // Остановить на входе в main
 | 
			
		||||
			"cwd": "${workspaceFolder}",
 | 
			
		||||
			"environment": [],
 | 
			
		||||
			"externalConsole": false,
 | 
			
		||||
			"MIMode": "gdb",
 | 
			
		||||
			"miDebuggerPath": "/usr/bin/sudo-gdb", // Путь к вашему скрипту
 | 
			
		||||
			"setupCommands": [
 | 
			
		||||
				{
 | 
			
		||||
					"description": "Enable pretty-printing for gdb",
 | 
			
		||||
					"text": "-enable-pretty-printing",
 | 
			
		||||
					"ignoreFailures": true
 | 
			
		||||
				}
 | 
			
		||||
			]
 | 
			
		||||
		}
 | 
			
		||||
	]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
{
 | 
			
		||||
	"editor.insertSpaces": false,
 | 
			
		||||
	"editor.tabSize": 4,
 | 
			
		||||
	"editor.detectIndentation": false
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
# cdcdb
 | 
			
		||||
 | 
			
		||||
Подход с использованием CDC (Capture Data Change) для хранения блоков данных в базе данных SQLite.
 | 
			
		||||
 | 
			
		||||
## Сборка
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Статическая библиотека
 | 
			
		||||
dub build -c static
 | 
			
		||||
# Динамическая библиотека
 | 
			
		||||
dub build -c dynamic
 | 
			
		||||
# Тест-утилита
 | 
			
		||||
dub build -c binary
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										38
									
								
								dub.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								dub.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
{
 | 
			
		||||
	"authors": [
 | 
			
		||||
		"Alexander Zhirov"
 | 
			
		||||
	],
 | 
			
		||||
	"copyright": "Copyright © 2025, Alexander Zhirov",
 | 
			
		||||
	"description": "A CDC Approach for Storing Chunks in an SQLite Database.",
 | 
			
		||||
	"license": "BSL-1.0",
 | 
			
		||||
	"name": "cdcdb",
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"arsd-official:sqlite": "~>12.0.0",
 | 
			
		||||
		"zstd": "~>0.2.1"
 | 
			
		||||
	},
 | 
			
		||||
	"stringImportPaths": [
 | 
			
		||||
		"source/cdcdb/db",
 | 
			
		||||
		"source/cdcdb/cdc"
 | 
			
		||||
	],
 | 
			
		||||
	"configurations": [
 | 
			
		||||
		{
 | 
			
		||||
			"name": "static",
 | 
			
		||||
			"targetType": "staticLibrary",
 | 
			
		||||
			"targetPath": "lib",
 | 
			
		||||
			"sourcePaths": ["source"]
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"name": "dynamic",
 | 
			
		||||
			"targetType": "dynamicLibrary",
 | 
			
		||||
			"targetPath": "lib",
 | 
			
		||||
			"sourcePaths": ["source"]
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"name": "binary",
 | 
			
		||||
			"targetType": "executable",
 | 
			
		||||
			"targetPath": "bin",
 | 
			
		||||
			"mainSourceFile": "test/app.d",
 | 
			
		||||
			"sourcePaths": ["source", "test"]
 | 
			
		||||
		}
 | 
			
		||||
	]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								dub.selections.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								dub.selections.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
{
 | 
			
		||||
	"fileVersion": 1,
 | 
			
		||||
	"versions": {
 | 
			
		||||
		"arsd-official": "12.0.0",
 | 
			
		||||
		"zstd": "0.2.1"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								source/cdcdb/cdc/cas.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								source/cdcdb/cdc/cas.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
module cdcdb.cdc.cas;
 | 
			
		||||
 | 
			
		||||
import cdcdb.db;
 | 
			
		||||
import cdcdb.cdc.core;
 | 
			
		||||
 | 
			
		||||
final class CAS
 | 
			
		||||
{
 | 
			
		||||
private:
 | 
			
		||||
	DBLite _db;
 | 
			
		||||
public:
 | 
			
		||||
	this(string database)
 | 
			
		||||
	{
 | 
			
		||||
		_db = new DBLite(database);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	size_t saveSnapshot(const(ubyte)[] data)
 | 
			
		||||
	{
 | 
			
		||||
		// Параметры для CDC вынести в отдельные настройки (продумать)
 | 
			
		||||
		auto cdc = new CDC(100, 200, 500, 0xFF, 0x0F);
 | 
			
		||||
		// Разбить на фрагменты
 | 
			
		||||
		auto chunks = cdc.split(data);
 | 
			
		||||
 | 
			
		||||
		import std.stdio : writeln;
 | 
			
		||||
 | 
			
		||||
		_db.beginImmediate();
 | 
			
		||||
		// Записать фрагменты в БД
 | 
			
		||||
		foreach (chunk; chunks)
 | 
			
		||||
		{
 | 
			
		||||
			writeln(chunk.index);
 | 
			
		||||
		}
 | 
			
		||||
		_db.commit();
 | 
			
		||||
		// Записать манифест в БД
 | 
			
		||||
		// Вернуть ID манифеста
 | 
			
		||||
		return 0;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										90
									
								
								source/cdcdb/cdc/core.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								source/cdcdb/cdc/core.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,90 @@
 | 
			
		|||
module cdcdb.cdc.core;
 | 
			
		||||
 | 
			
		||||
import cdcdb.cdc.types;
 | 
			
		||||
 | 
			
		||||
import std.digest.sha : SHA256, digest;
 | 
			
		||||
 | 
			
		||||
final class CDC
 | 
			
		||||
{
 | 
			
		||||
private:
 | 
			
		||||
	size_t _minSize, _normalSize, _maxSize;
 | 
			
		||||
	ulong _maskS, _maskL;
 | 
			
		||||
	// _gear
 | 
			
		||||
	mixin(import("gear.d"));
 | 
			
		||||
 | 
			
		||||
	size_t cut(const(ubyte)[] src) @safe nothrow
 | 
			
		||||
	{
 | 
			
		||||
		size_t size = src.length;
 | 
			
		||||
		if (size == 0)
 | 
			
		||||
			return 0;
 | 
			
		||||
		if (size <= _minSize)
 | 
			
		||||
			return size;
 | 
			
		||||
 | 
			
		||||
		if (size > _maxSize)
 | 
			
		||||
			size = _maxSize;
 | 
			
		||||
		auto normalSize = _normalSize;
 | 
			
		||||
		if (size < normalSize)
 | 
			
		||||
			normalSize = size;
 | 
			
		||||
 | 
			
		||||
		ulong fingerprint = 0;
 | 
			
		||||
		size_t index;
 | 
			
		||||
 | 
			
		||||
		// инициализация без cut-check
 | 
			
		||||
		while (index < _minSize)
 | 
			
		||||
		{
 | 
			
		||||
			fingerprint = (fingerprint << 1) + _gear[src[index]];
 | 
			
		||||
			++index;
 | 
			
		||||
		}
 | 
			
		||||
		// строгая маска
 | 
			
		||||
		while (index < normalSize)
 | 
			
		||||
		{
 | 
			
		||||
			fingerprint = (fingerprint << 1) + _gear[src[index]];
 | 
			
		||||
			if ((fingerprint & _maskS) == 0)
 | 
			
		||||
				return index;
 | 
			
		||||
			++index;
 | 
			
		||||
		}
 | 
			
		||||
		// слабая маска
 | 
			
		||||
		while (index < size)
 | 
			
		||||
		{
 | 
			
		||||
			fingerprint = (fingerprint << 1) + _gear[src[index]];
 | 
			
		||||
			if ((fingerprint & _maskL) == 0)
 | 
			
		||||
				return index;
 | 
			
		||||
			++index;
 | 
			
		||||
		}
 | 
			
		||||
		return size;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	this(size_t minSize, size_t normalSize, size_t maxSize, ulong maskS, ulong maskL) @safe @nogc nothrow
 | 
			
		||||
	{
 | 
			
		||||
		assert(minSize > 0 && minSize < normalSize && normalSize < maxSize,
 | 
			
		||||
			"Неверные размеры: требуется min < normal < max и min > 0");
 | 
			
		||||
		_minSize = minSize;
 | 
			
		||||
		_normalSize = normalSize;
 | 
			
		||||
		_maxSize = maxSize;
 | 
			
		||||
		_maskS = maskS;
 | 
			
		||||
		_maskL = maskL;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Chunk[] split(const(ubyte)[] data) @safe
 | 
			
		||||
	{
 | 
			
		||||
		Chunk[] chunks;
 | 
			
		||||
		if (data.length == 0)
 | 
			
		||||
			return chunks;
 | 
			
		||||
		chunks.reserve(data.length / _normalSize);
 | 
			
		||||
		size_t offset = 0;
 | 
			
		||||
		size_t index = 1;
 | 
			
		||||
 | 
			
		||||
		while (offset < data.length)
 | 
			
		||||
		{
 | 
			
		||||
			auto size = cut(data[offset .. $]);
 | 
			
		||||
			auto bytes = data[offset .. offset + size];
 | 
			
		||||
			ubyte[32] hash = digest!SHA256(bytes);
 | 
			
		||||
			chunks ~= Chunk(index, offset, size, hash);
 | 
			
		||||
 | 
			
		||||
			offset += size;
 | 
			
		||||
			++index;
 | 
			
		||||
		}
 | 
			
		||||
		return chunks;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								source/cdcdb/cdc/gear.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								source/cdcdb/cdc/gear.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,66 @@
 | 
			
		|||
immutable ulong[256] _gear = [
 | 
			
		||||
	0x2722039f43c57a70, 0x338c1bd5b7ac5204, 0xf9f2c73ff33c98c0, 0x7dee12e6cd31cb32,
 | 
			
		||||
	0x9688335e0f2decfd, 0x5307003c8e60b963, 0xfd2a2848eb358095, 0xc3614773074ee6b7,
 | 
			
		||||
	0x6e35235234b6ed0a, 0x9d4cfa9d8e3850cc, 0xaa1b3d8af8ad86bd, 0x79c6d2e28bfb333d,
 | 
			
		||||
	0x3df08966a00c33ec, 0xfd58bbf83f38c690, 0x5ef9ee9a4552545b, 0x099192a7e5599bdc,
 | 
			
		||||
	0xa8f2419947f21017, 0xd6a03d010f2fda7c, 0x1fe53de04074fc20, 0x75b5aff7c66605f8,
 | 
			
		||||
	0x1a94b7484bf509a9, 0xbf2e371a53466ded, 0xedcf13f8eb0f0fdf, 0xfba81285ead7dafe,
 | 
			
		||||
	0x839fb29274557fa5, 0xeefe64b15cc7f7f0, 0x7d15f8e862726515, 0x59b416e43cca2adc,
 | 
			
		||||
	0x9c2c925dcde12d4a, 0xf3df9373c3e63a07, 0x747cb5ec08ffa4ef, 0x26c93138f3f19b29,
 | 
			
		||||
	0xcdade11723bd59ed, 0xc7a6a7d0c18642cb, 0x88c2976f22f5d084, 0x7c48f54cdaf480fe,
 | 
			
		||||
	0x91ea7c7fd3d06d54, 0xed3e31236e8c9366, 0xa16da2234f420bc4, 0x5ee6136449d30974,
 | 
			
		||||
	0xe32a921921181e16, 0xa6ab2fb8c7212257, 0x754a8a581ce819ca, 0x22b2de3e7c7a2f57,
 | 
			
		||||
	0xd2773285e49b9160, 0x19b0449384554129, 0x145e7d2c46da460e, 0xdd720d0e79a3615d,
 | 
			
		||||
	0x621ae4f0ad576223, 0x4f6da235cc25d5c9, 0x6d6d39e005d67437, 0x5839c8f3d2f71122,
 | 
			
		||||
	0x691a668bc7f5c153, 0xb2eb484f8546c61d, 0x7955c346c34cbcdc, 0x413c2a0ba6fd0a88,
 | 
			
		||||
	0x29ad3d418323592b, 0xb7d04d0abf3f8d50, 0x76e742f91dfc77ea, 0x8a5c80d1a0d5ff5c,
 | 
			
		||||
	0xce1aa9b0bdd16adc, 0x74e4bd6f412c8186, 0xbf1dddc8f63dfc08, 0x11dcb84c1b5c32cb,
 | 
			
		||||
	0x3320ed259fc0d8c0, 0x13dbd4c934c58e01, 0x344b61dd3741a9f9, 0x935861bea84a6f81,
 | 
			
		||||
	0xaf70eea3844052f9, 0xc0a83f93799c2e81, 0xdd23b2a943a5af16, 0x05b4efd89b3e818b,
 | 
			
		||||
	0x75b2a3d0fe099aec, 0x5aab5599ae580c37, 0xe9b64238ed626a6b, 0xb63f47c31c31ec1d,
 | 
			
		||||
	0x0b50ee03c3425dde, 0xf287ebed924249f6, 0xe09eee62604318c4, 0x0d334cb1bd82bc13,
 | 
			
		||||
	0xc41abf3d95b18620, 0x869c3bc45e2c9edf, 0x526de53484e093c7, 0xc640fee4784fd9ce,
 | 
			
		||||
	0x761637787d81c0ea, 0x817bf175cb17e903, 0x94a4846f1158f988, 0x99c254e5f8e698e0,
 | 
			
		||||
	0xa4623d4d1b76352e, 0x326dae4493475c3a, 0xed2944be79511208, 0x163a0a9b65f40339,
 | 
			
		||||
	0x336489f8c2f6190c, 0x670d217f8e6bee33, 0x662e19c285c5a4a1, 0xcab8f4512d0b251a,
 | 
			
		||||
	0x61d4476812ed1017, 0x0ec77209307430af, 0x20a94905901093dc, 0xaa9fe2cae9ffa699,
 | 
			
		||||
	0xc75f757de6c045dc, 0x141ef38478656459, 0x9b3ce9c4e3dd7858, 0x3ab62b9aa45a3d0d,
 | 
			
		||||
	0x61a89423e18e5e68, 0x3802972ecadf592d, 0xcfc85c5724ff3af8, 0x381ee916e97a628a,
 | 
			
		||||
	0x2fa2c37a040e488a, 0x9813a505b4ca4036, 0xc4254f1aaf7b2f42, 0xe8a0720b79a1188d,
 | 
			
		||||
	0xe663a71adb5d53e3, 0x6e3b5927934102af, 0xbd8c502741b1fcb1, 0x1af6fa2fb1d2e5a6,
 | 
			
		||||
	0xc88d367a79d06f5d, 0x29fe7cdab66530d9, 0x34bef2ebe612d95f, 0x9ab6977a57db1fa2,
 | 
			
		||||
	0x73774fc29deac09a, 0x7832f37495fd28fb, 0x1559a3badfbd42a6, 0x7e6831522a50d2bc,
 | 
			
		||||
	0xddb8564f3aafe3b7, 0x86acb9eca71bc09d, 0x21b0a9469727d4fc, 0x26d3b66f525ebcab,
 | 
			
		||||
	0x77e3fd126fd97e3a, 0x5306b81a9fe2a92e, 0x7292138f116d8911, 0x285b466c9939b076,
 | 
			
		||||
	0x40527805d9a4379d, 0x8986c05119c7ca1e, 0x6a7890c402303c31, 0xb1b109dc109405bc,
 | 
			
		||||
	0x1d71f3997b288f30, 0xfa203ff4dc9ea72c, 0x8ae3eea975cc92da, 0x3468e4305eabb928,
 | 
			
		||||
	0xd79c37e720467df1, 0x011e6490c0f832d2, 0x29ce2ada8509647a, 0xb4e325b9f3ba783c,
 | 
			
		||||
	0xa812ca4fad720763, 0x0cdf098645ccb476, 0xf6b47e21637fcd76, 0x3597f48148a297de,
 | 
			
		||||
	0x5875212868ab81ec, 0x1ea36243940596bb, 0xfd93ac7c39a27586, 0xabb09b0f803a7214,
 | 
			
		||||
	0x8cc8ec1ea21a16af, 0x824a0db50ae906d1, 0x3d972fb701ca3e70, 0xda60d493e9a20bd0,
 | 
			
		||||
	0x97d282f6bda26087, 0x9bc8f7842af296d0, 0x14804a1663a0cf7e, 0x3b71cc25885e75f3,
 | 
			
		||||
	0x131adc05e336042b, 0x566aa36d26eee86c, 0x97d4c4d4fd4b0dd1, 0xd2407b1485c7bee1,
 | 
			
		||||
	0xcad613e7b92e6df1, 0xe3ceccd99d975088, 0x99e6b93ff96a2636, 0x1ad75dbed057f0d0,
 | 
			
		||||
	0x5e3ba609dd100c6e, 0x9c5efa00b33a18f3, 0xad89369e692bdb28, 0xf7a546fca26d1d7d,
 | 
			
		||||
	0x5813db1fe943575f, 0x24c3467f03a144ae, 0xc892f2ce492cb7c8, 0xc44672263508d34b,
 | 
			
		||||
	0xd400e1c0a5734a40, 0x3ca24ee74bf8e84f, 0xd83bd4e907c351a5, 0xe142297005fa9aa8,
 | 
			
		||||
	0x0f6d796cf68abda0, 0x6c8e25bc6d9ae2e8, 0xccc235f322a42cf3, 0xabaf39cea8ca450c,
 | 
			
		||||
	0x02b9cdf615a0d7b6, 0x8aaf7d8b55d4dc39, 0xbe2c2bc6ef13c6c5, 0x6ad98aa4a4bc610f,
 | 
			
		||||
	0x1051a62ac2a2b434, 0xbd167e6eba260d35, 0xb9b86ac04ac4f811, 0xabe8a6453e196739,
 | 
			
		||||
	0x439ff734b19246b4, 0xcea324040c9e8981, 0x87f55cf1035e1a22, 0xa227d679c33597f9,
 | 
			
		||||
	0xbf4d654b6cdd0015, 0xc0302ec55f87a46e, 0xed32173466c70a83, 0x8ceb757b648d2bf2,
 | 
			
		||||
	0x1873757a6d17446b, 0xeb0f366fea62e77e, 0x145aa2795d34dd93, 0x2fc378be4c471db0,
 | 
			
		||||
	0x6d1274fb8f6364a2, 0x602a56fd1cc36728, 0x5f8aa6e0c892b4b5, 0x33e2c5653d8b1ad6,
 | 
			
		||||
	0x1f6c8b2a004714f4, 0x4042b98d54acbfef, 0x4606386f11f6456f, 0xf56bd21a8a35c540,
 | 
			
		||||
	0xd2b23c57b3718e1f, 0x94726832fe96e61d, 0xa225b072752a823b, 0x0bd957cf585f8cda,
 | 
			
		||||
	0x533d819bb30b4221, 0xda0f9cff9a0115fa, 0xd14de3b8fe3354ea, 0xa96328e96d9364c0,
 | 
			
		||||
	0x9078dc0eff2676ab, 0x22585cd4521c6210, 0x5903254df4e402a5, 0x1b54b71b55ae697a,
 | 
			
		||||
	0xb899b86756b2aa39, 0x5d5d2dd5cd0bce8b, 0x7b3a78a4a0662015, 0xa9fbfc7678fc7931,
 | 
			
		||||
	0xa732d694f6ab64a0, 0x9fc960e7db3e9716, 0x76c765948f3c2ba5, 0x076a509dca2a4349,
 | 
			
		||||
	0xca5bfc5973661e59, 0x454ec4d49bddd45d, 0x56115e001997cee2, 0xd689eb8926051c7f,
 | 
			
		||||
	0xf50df8ca9c355e3f, 0x88a375a9f0492a69, 0xe059fd001d50439a, 0x765c5d6f66d5e788,
 | 
			
		||||
	0xaf57f4eea178f896, 0x06e8cca68730fbbd, 0xb7b1f6f86904ce4e, 0x3c3b10b0c08cf0bf,
 | 
			
		||||
	0x1e0e310524778bd4, 0xd65d7cd93cde7c69, 0x18543b187c77fcf3, 0x180f6cdd1af3a60a,
 | 
			
		||||
	0xe1cd4c2bc3656704, 0x218fdfc5aa282d00, 0x844eeaf2e439b242, 0x05df1a59e415b4c6,
 | 
			
		||||
	0x14abdd3ace097c2c, 0x7f3b0705b04b14d4, 0xf69c57f60180332b, 0x165fc3f0e65db80f
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										3
									
								
								source/cdcdb/cdc/package.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								source/cdcdb/cdc/package.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
module cdcdb.cdc;
 | 
			
		||||
 | 
			
		||||
public import cdcdb.cdc.cas;
 | 
			
		||||
							
								
								
									
										20
									
								
								source/cdcdb/cdc/types.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								source/cdcdb/cdc/types.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
module cdcdb.cdc.types;
 | 
			
		||||
 | 
			
		||||
/// Единица разбиения
 | 
			
		||||
struct Chunk
 | 
			
		||||
{
 | 
			
		||||
	size_t index; // 1..N
 | 
			
		||||
	size_t offset; // смещение в исходном буфере
 | 
			
		||||
	size_t size; // размер чанка
 | 
			
		||||
	ubyte[32] sha256; // hex(SHA-256) содержимого
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Метаданные снимка
 | 
			
		||||
struct SnapshotInfo
 | 
			
		||||
{
 | 
			
		||||
	size_t id;
 | 
			
		||||
	string createdUTC; // ISO-8601
 | 
			
		||||
	string label;
 | 
			
		||||
	size_t sourceLength;
 | 
			
		||||
	size_t chunks;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								source/cdcdb/db/dblite.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								source/cdcdb/db/dblite.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
module cdcdb.db.dblite;
 | 
			
		||||
 | 
			
		||||
import arsd.sqlite;
 | 
			
		||||
import std.file : exists, isFile;
 | 
			
		||||
 | 
			
		||||
final class DBLite : Sqlite
 | 
			
		||||
{
 | 
			
		||||
private:
 | 
			
		||||
	string _dbPath;
 | 
			
		||||
	// _scheme
 | 
			
		||||
	mixin(import("scheme.d"));
 | 
			
		||||
public:
 | 
			
		||||
	this(string database)
 | 
			
		||||
	{
 | 
			
		||||
		_dbPath = database;
 | 
			
		||||
		super(database);
 | 
			
		||||
 | 
			
		||||
		foreach (schemeQuery; _scheme)
 | 
			
		||||
		{
 | 
			
		||||
			sql(schemeQuery);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		query("PRAGMA journal_mode=WAL");
 | 
			
		||||
		query("PRAGMA synchronous=NORMAL");
 | 
			
		||||
		query("PRAGMA foreign_keys=ON");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	void beginImmediate()
 | 
			
		||||
	{
 | 
			
		||||
		query("BEGIN IMMEDIATE");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	void commit()
 | 
			
		||||
	{
 | 
			
		||||
		query("COMMIT");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	void rollback()
 | 
			
		||||
	{
 | 
			
		||||
		query("ROLLBACK");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	SqliteResult sql(T...)(string queryText, T args)
 | 
			
		||||
	{
 | 
			
		||||
		return cast(SqliteResult) query(queryText, args);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								source/cdcdb/db/package.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								source/cdcdb/db/package.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
module cdcdb.db;
 | 
			
		||||
 | 
			
		||||
public import cdcdb.db.dblite;
 | 
			
		||||
							
								
								
									
										75
									
								
								source/cdcdb/db/scheme.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								source/cdcdb/db/scheme.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,75 @@
 | 
			
		|||
auto _scheme = [
 | 
			
		||||
	q{
 | 
			
		||||
		-- Метаданные снапшота
 | 
			
		||||
		CREATE TABLE IF NOT EXISTS snapshots (
 | 
			
		||||
			-- Уникальный числовой идентификатор снимка. Используется во внешних ключах.
 | 
			
		||||
			id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
			-- Произвольная метка/название снимка.
 | 
			
		||||
			label TEXT,
 | 
			
		||||
			-- Время создания записи в UTC. По умолчанию - сейчас.
 | 
			
		||||
			created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
 | 
			
		||||
			-- Полная длина исходного файла в байтах для этого снимка (до разбиения на чанки).
 | 
			
		||||
			source_length INTEGER NOT NULL,
 | 
			
		||||
			-- Пороговые размеры FastCDC (минимальный/целевой/максимальный размер чанка) в байтах.
 | 
			
		||||
			-- Фиксируются здесь, чтобы позже можно было корректно пересобрать/сравнить.
 | 
			
		||||
			algo_min INTEGER NOT NULL,
 | 
			
		||||
			algo_normal INTEGER NOT NULL,
 | 
			
		||||
			algo_max INTEGER NOT NULL,
 | 
			
		||||
			-- Маски для определения границ чанков (быстрый роллинг-хэш/FastCDC).
 | 
			
		||||
			-- Обычно степени вида 2^n - 1. Хранятся для воспроизводимости.
 | 
			
		||||
			mask_s INTEGER NOT NULL,
 | 
			
		||||
			mask_l INTEGER NOT NULL,
 | 
			
		||||
			-- Состояние снимка:
 | 
			
		||||
			-- pending - метаданные созданы, состав не полностью загружен;
 | 
			
		||||
			-- ready - все чанки привязаны, снимок готов к использованию.
 | 
			
		||||
			status TEXT NOT NULL DEFAULT "pending" CHECK (status IN ("pending","ready"))
 | 
			
		||||
		)
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
		-- Уникальные куски содержимого (сам контент в БД)
 | 
			
		||||
		CREATE TABLE IF NOT EXISTS blobs (
 | 
			
		||||
			-- Хэш содержимого чанка. Ключ обеспечивает дедупликацию: одинаковый контент хранится один раз.
 | 
			
		||||
			sha256 TEXT PRIMARY KEY,
 | 
			
		||||
			-- Размер этого чанка в байтах.
 | 
			
		||||
			size INTEGER NOT NULL,
 | 
			
		||||
			-- Сырые байты чанка.
 | 
			
		||||
			content BLOB NOT NULL,
 | 
			
		||||
			-- Когда этот контент впервые появился в базе (UTC).
 | 
			
		||||
			created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP)
 | 
			
		||||
		)
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
		-- Состав снапшота (порядок чанков важен)
 | 
			
		||||
		CREATE TABLE IF NOT EXISTS snapshot_chunks (
 | 
			
		||||
			-- Ссылка на snapshots.id. Определяет, к какому снимку относится строка.
 | 
			
		||||
			snapshot_id INTEGER NOT NULL,
 | 
			
		||||
			-- Позиция чанка в снимке (индексация).
 | 
			
		||||
			-- Обеспечивает порядок сборки.
 | 
			
		||||
			chunk_index INTEGER NOT NULL,
 | 
			
		||||
			-- Смещение чанка в исходном файле в байтах.
 | 
			
		||||
			-- Можно восстановить как сумму size предыдущих чанков по chunk_index,
 | 
			
		||||
			-- но хранение ускоряет проверки/отладку.
 | 
			
		||||
			offset INTEGER,
 | 
			
		||||
			-- Размер именно этого чанка в байтах (дублирует blobs.size для быстрого доступа и валидации).
 | 
			
		||||
			size INTEGER NOT NULL,
 | 
			
		||||
			-- Ссылка на blobs.sha256. Привязывает позицию в снимке к конкретному содержимому.
 | 
			
		||||
			sha256 TEXT NOT NULL,
 | 
			
		||||
			-- Гарантирует уникальность позиции чанка в рамках снимка и задаёт естественный порядок.
 | 
			
		||||
			PRIMARY KEY (snapshot_id, chunk_index),
 | 
			
		||||
			-- При удалении снимка его строки состава удаляются автоматически.
 | 
			
		||||
			-- Обновления id каскадятся (на практике id не меняют).
 | 
			
		||||
			FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON UPDATE CASCADE ON DELETE CASCADE,
 | 
			
		||||
			-- Нельзя удалить blob, если он где-то используется (RESTRICT).
 | 
			
		||||
			-- Обновление хэша каскадится (редкий случай).
 | 
			
		||||
			FOREIGN KEY (sha256) REFERENCES blobs(sha256) ON UPDATE CASCADE ON DELETE RESTRICT
 | 
			
		||||
		)
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
		-- Быстрый выбор всех чанков конкретного снимка (частый запрос).
 | 
			
		||||
		CREATE INDEX IF NOT EXISTS idx_snapshot_chunks_snapshot ON snapshot_chunks(snapshot_id)
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
		-- Быстрый обратный поиск: где используется данный blob (для GC/аналитики).
 | 
			
		||||
		CREATE INDEX IF NOT EXISTS idx_snapshot_chunks_sha ON snapshot_chunks(sha256)
 | 
			
		||||
	}
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										151
									
								
								source/cdcdb/db/scheme.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								source/cdcdb/db/scheme.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,151 @@
 | 
			
		|||
# Схемы базы данных для хранения снимков (фрагментов)
 | 
			
		||||
 | 
			
		||||
## Структура базы данных
 | 
			
		||||
```mermaid
 | 
			
		||||
erDiagram
 | 
			
		||||
  %% Композитный PK у SNAPSHOT_CHUNKS: (snapshot_id, chunk_index)
 | 
			
		||||
 | 
			
		||||
  SNAPSHOTS {
 | 
			
		||||
    int    id PK
 | 
			
		||||
    string label
 | 
			
		||||
    string created_utc
 | 
			
		||||
    int    source_length
 | 
			
		||||
    int    algo_min
 | 
			
		||||
    int    algo_normal
 | 
			
		||||
    int    algo_max
 | 
			
		||||
    int    mask_s
 | 
			
		||||
    int    mask_l
 | 
			
		||||
    string status
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  BLOBS {
 | 
			
		||||
    string sha256 PK
 | 
			
		||||
    int    size
 | 
			
		||||
    blob   content
 | 
			
		||||
    string created_utc
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  SNAPSHOT_CHUNKS {
 | 
			
		||||
    int    snapshot_id FK
 | 
			
		||||
    int    chunk_index
 | 
			
		||||
    int    offset
 | 
			
		||||
    int    size
 | 
			
		||||
    string sha256 FK
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  %% Связи и поведение внешних ключей
 | 
			
		||||
  SNAPSHOTS ||--o{ SNAPSHOT_CHUNKS : "1:N, ON DELETE CASCADE"
 | 
			
		||||
  BLOBS     ||--o{ SNAPSHOT_CHUNKS : "1:N, ON DELETE RESTRICT"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Схема последовательности записи в базу данных
 | 
			
		||||
 | 
			
		||||
```mermaid
 | 
			
		||||
sequenceDiagram
 | 
			
		||||
    autonumber
 | 
			
		||||
    participant APP as Приложение
 | 
			
		||||
    participant CH as Разбиение на чанки (FastCDC)
 | 
			
		||||
    participant HS as Хеширование (SHA-256)
 | 
			
		||||
    participant DB as База данных (SQLite)
 | 
			
		||||
 | 
			
		||||
    Note over APP,DB: Подготовка
 | 
			
		||||
    APP->>DB: Открывает соединение, включает PRAGMA (WAL, foreign_keys=ON)
 | 
			
		||||
    APP->>DB: BEGIN IMMEDIATE (начать транзакцию с блокировкой на запись)
 | 
			
		||||
 | 
			
		||||
    Note over APP,DB: Создание метаданных снимка
 | 
			
		||||
    APP->>DB: INSERT INTO snapshots(label, source_length, algo_min, algo_normal, algo_max, mask_s, mask_l, status='pending')
 | 
			
		||||
    DB-->>APP: id снимка = last_insert_rowid()
 | 
			
		||||
 | 
			
		||||
    Note over APP,CH: Поток файла → чанки
 | 
			
		||||
    APP->>CH: Читает файл, передает параметры FastCDC (min/normal/max, mask_s/mask_l)
 | 
			
		||||
    loop Для каждого чанка в порядке следования
 | 
			
		||||
        CH-->>APP: Возвращает {chunk_index, offset, size, bytes}
 | 
			
		||||
 | 
			
		||||
        Note over APP,HS: Хеш содержимого
 | 
			
		||||
        APP->>HS: Вычисляет SHA-256(bytes)
 | 
			
		||||
        HS-->>APP: digest (sha256)
 | 
			
		||||
 | 
			
		||||
        Note over APP,DB: Дедупликация контента
 | 
			
		||||
        APP->>DB: SELECT 1 FROM blobs WHERE sha256 = ?
 | 
			
		||||
        alt Блоб отсутствует
 | 
			
		||||
            APP->>DB: INSERT INTO blobs(sha256, size, content)
 | 
			
		||||
            DB-->>APP: OK
 | 
			
		||||
        else Блоб уже есть
 | 
			
		||||
            DB-->>APP: Найден (пропускаем вставку содержимого)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        Note over APP,DB: Привязка чанка к снимку
 | 
			
		||||
        APP->>DB: INSERT INTO snapshot_chunks(snapshot_id, chunk_index, offset, size, sha256)
 | 
			
		||||
        DB-->>APP: OK (PK: (snapshot_id, chunk_index))
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    Note over APP,DB: Валидация и завершение
 | 
			
		||||
    APP->>DB: SELECT SUM(size) FROM snapshot_chunks WHERE snapshot_id = ?
 | 
			
		||||
    DB-->>APP: total_size
 | 
			
		||||
    alt total_size == snapshots.source_length
 | 
			
		||||
        APP->>DB: UPDATE snapshots SET status='ready' WHERE id = ?
 | 
			
		||||
        APP->>DB: COMMIT
 | 
			
		||||
        DB-->>APP: Транзакция зафиксирована
 | 
			
		||||
    else Несоответствие размеров или ошибка
 | 
			
		||||
        APP->>DB: ROLLBACK
 | 
			
		||||
        DB-->>APP: Откат изменений
 | 
			
		||||
        APP-->>APP: Логирует ошибку, возвращает код/исключение
 | 
			
		||||
    end
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Схема последовательности восстановления из базы данных
 | 
			
		||||
 | 
			
		||||
```mermaid
 | 
			
		||||
sequenceDiagram
 | 
			
		||||
    autonumber
 | 
			
		||||
    participant APP as Приложение
 | 
			
		||||
    participant DB as База данных (SQLite)
 | 
			
		||||
    participant FS as Целевой файл
 | 
			
		||||
    participant HS as Хеширование (опц.)
 | 
			
		||||
 | 
			
		||||
    Note over APP,DB: Подготовка к чтению
 | 
			
		||||
    APP->>DB: Открывает соединение (read), BEGIN (снимок чтения)
 | 
			
		||||
 | 
			
		||||
    Note over APP,DB: Выбор снимка
 | 
			
		||||
    APP->>DB: Находит нужный снимок по id/label, читает status и source_length
 | 
			
		||||
    DB-->>APP: id, status, source_length
 | 
			
		||||
    alt status == "ready"
 | 
			
		||||
    else снимок не готов
 | 
			
		||||
        APP-->>APP: Прерывает восстановление с ошибкой
 | 
			
		||||
        DB-->>APP: END
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    Note over APP,DB: Получение состава снимка
 | 
			
		||||
    APP->>DB: SELECT chunk_index, offset, size, sha256 FROM snapshot_chunks WHERE snapshot_id=? ORDER BY chunk_index
 | 
			
		||||
    DB-->>APP: Строки чанков в порядке chunk_index
 | 
			
		||||
 | 
			
		||||
    loop Для каждого чанка
 | 
			
		||||
        APP->>DB: SELECT content, size FROM blobs WHERE sha256=?
 | 
			
		||||
        DB-->>APP: content, blob_size
 | 
			
		||||
 | 
			
		||||
        Note over APP,HS: (опц.) контроль целостности чанка
 | 
			
		||||
        APP->>HS: Вычисляет SHA-256(content)
 | 
			
		||||
        HS-->>APP: digest
 | 
			
		||||
        APP-->>APP: Сверяет digest с sha256 и size с blob_size
 | 
			
		||||
 | 
			
		||||
        alt offset задан
 | 
			
		||||
            APP->>FS: Позиционируется на offset и пишет content (pwrite/seek+write)
 | 
			
		||||
        else offset отсутствует
 | 
			
		||||
            APP->>FS: Дописывает content в конец файла
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    Note over APP,DB: Финальная проверка
 | 
			
		||||
    APP-->>APP: Суммирует размеры записанных чанков → total_size
 | 
			
		||||
    APP->>DB: Берёт snapshots.source_length
 | 
			
		||||
    DB-->>APP: source_length
 | 
			
		||||
    alt total_size == source_length
 | 
			
		||||
        APP->>FS: fsync и close
 | 
			
		||||
        DB-->>APP: END
 | 
			
		||||
        APP-->>APP: Успешное восстановление
 | 
			
		||||
    else размеры не совпали
 | 
			
		||||
        APP->>FS: Удаляет/помечает файл как повреждённый
 | 
			
		||||
        DB-->>APP: END
 | 
			
		||||
        APP-->>APP: Фиксирует ошибку (несоответствие сумм)
 | 
			
		||||
    end
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										3
									
								
								source/cdcdb/package.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								source/cdcdb/package.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
module cdcdb;
 | 
			
		||||
 | 
			
		||||
public import cdcdb.cdc;
 | 
			
		||||
							
								
								
									
										11
									
								
								test/app.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								test/app.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
import std.stdio;
 | 
			
		||||
 | 
			
		||||
import cdcdb;
 | 
			
		||||
 | 
			
		||||
import std.file : read;
 | 
			
		||||
 | 
			
		||||
void main()
 | 
			
		||||
{
 | 
			
		||||
	auto cas = new CAS("/tmp/base.db");
 | 
			
		||||
	cas.saveSnapshot(cast(ubyte[]) read("/tmp/text"));
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								tools/gen.d
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										42
									
								
								tools/gen.d
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
#!/usr/bin/rdmd
 | 
			
		||||
 | 
			
		||||
import std.stdio : write, writef, writeln;
 | 
			
		||||
import std.random : Random, unpredictableSeed, uniform;
 | 
			
		||||
 | 
			
		||||
void main()
 | 
			
		||||
{
 | 
			
		||||
	enum N = 256;
 | 
			
		||||
	ulong[N] gear;
 | 
			
		||||
 | 
			
		||||
	auto rng = Random(unpredictableSeed);
 | 
			
		||||
 | 
			
		||||
	bool[ulong] seen;
 | 
			
		||||
	ulong[] vals;
 | 
			
		||||
	vals.reserve(N);
 | 
			
		||||
 | 
			
		||||
	while (vals.length < N)
 | 
			
		||||
	{
 | 
			
		||||
		const v = uniform!ulong(rng);
 | 
			
		||||
		if (v in seen)
 | 
			
		||||
			continue;
 | 
			
		||||
		seen[v] = true;
 | 
			
		||||
		vals ~= v;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gear[] = vals[0 .. N];
 | 
			
		||||
 | 
			
		||||
	writeln("immutable ulong[256] gear = [");
 | 
			
		||||
	foreach (i, v; gear)
 | 
			
		||||
	{
 | 
			
		||||
		if (i % 4 == 0)
 | 
			
		||||
			write("\t");
 | 
			
		||||
		writef("0x%016x", v);
 | 
			
		||||
		if (i != N - 1)
 | 
			
		||||
			write(",");
 | 
			
		||||
		if ((i + 1) % 4 == 0 || i == N - 1)
 | 
			
		||||
			writeln();
 | 
			
		||||
		else
 | 
			
		||||
			write(" ");
 | 
			
		||||
	}
 | 
			
		||||
	writeln("];");
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue