Compare commits

..

14 Commits

Author SHA1 Message Date
github-actions[bot] f149363b12 chore: release 1.11.1 2026-05-17 16:51:27 +00:00
Charles GTE aa3ff39bb3 fix: Dockerfile (#58)
* fix: s3 port type mismatch, add string_or_number_to_string deserializer

* chore: docker-compose.databases.yml

* fix: tests and refactoring, add justfile instead of makefile

* fix: databases.json

* fix: add gbak in production Dockerfile

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-05-17 18:49:18 +02:00
github-actions[bot] db26910679 chore: release 1.11.0 2026-05-11 20:07:01 +00:00
Charles GTE eac2853b83 feat: add-mssql-support (#56)
* fix: tests and refactoring, add justfile instead of makefile

* chore: add tiberius dependency for MSSQL support

* feat: add Mssql variant to DbType enum and config validation

* feat: add MSSQL domain adapter with tiberius ping and sqlpackage backup/restore

* feat: install sqlpackage in Docker base and prod stages

* feat: add MSSQL service to docker-compose and config examples

* fix: log backup errors instead of silently discarding them

* fix: use dotnet-install.sh instead of Microsoft apt feed (SHA1 key rejected by Debian Trixie)

* fix: add /TrustServerCertificate:true to sqlpackage commands for self-signed certs

* fix: use connection string with TrustServerCertificate for sqlpackage

* fix: use user database in MSSQL config example (system databases cannot be exported)

* feat: add seed-mssql task and seed script

* fix: docker-compose.yml
2026-05-11 22:04:36 +02:00
github-actions[bot] a4bbb37a4e chore: release 1.10.1 2026-04-29 20:42:31 +00:00
Charles GTE 14068846b9 fix: migration-process (#55)
* fix: s3 port type mismatch, add string_or_number_to_string deserializer

* chore: docker-compose.databases.yml

* fix: tests and refactoring, add justfile instead of makefile

* fix: restoration process for postgres/mysql and mariadb

* fix: restoration process for postgres/mysql and mariadb

* fix: postgres restore

* fix: mariadb/mysql restore, added backticks to drop command

* fix: mongodb and migration test for firebird

* fix: docker-compose.yml

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-04-29 22:40:02 +02:00
github-actions[bot] 0a422448b4 chore: release 1.10.0 2026-04-16 09:22:34 +00:00
Charles GTE 14a6c9ab0a feat: add chunk size in .env variables (#54)
Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-04-16 11:20:14 +02:00
Charles GTE 86027fd6bd fix: docker compose files [skip-release] (#53)
* fix: s3 port type mismatch, add string_or_number_to_string deserializer

* chore: docker-compose.databases.yml

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-04-02 19:30:07 +02:00
Charles GTE 8eab97a3d6 chore: README.md (#52) [skip-release]
Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-04-01 17:24:48 +02:00
github-actions[bot] 25c95108f0 chore: release 1.9.0 2026-03-31 13:12:45 +00:00
Charles GTE f37e4d2de9 feat: add firebird (#51)
* feat: add firebird

* feat: add firebird tests

* fix: tests and refactoring, add justfile instead of makefile
2026-03-31 15:10:05 +02:00
github-actions[bot] a1aa66ff3f chore: release 1.8.0 2026-03-29 07:30:57 +00:00
Charles GTE d4abcbcebc feat: separate mysql and mariadb (#49)
* feat: separate mysql and mariadb

* feat: separate mysql and mariadb
2026-03-29 09:28:30 +02:00
51 changed files with 1382 additions and 299 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
contact@soluce-technologies.com.
contact@portabase.io.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
+1 -1
View File
@@ -18,7 +18,7 @@ We take security seriously and aim to support the following versions of the proj
If you discover a security vulnerability in this project, we appreciate your help in disclosing it responsibly.
1. **Contact Us**
Please report the vulnerability by emailing **[contact@soluce-technologies.com](mailto:contact@soluce-technologies.com)**. Include the following details:
Please report the vulnerability by emailing **[contact@portabase.io](mailto:contact@portabase.io)**. Include the following details:
- A detailed description of the issue.
- Steps to reproduce the vulnerability (if applicable).
- Any potential impacts or risks.
+4 -1
View File
@@ -3,4 +3,7 @@
/src/data/
.DS_Store
.env
.env
.claude
/docs
+1 -1
View File
@@ -27,5 +27,5 @@ keywords:
- self-hosted
- portabase
license: Apache-2.0
version: 1.7.0
version: 1.11.1
date-released: '2026-02-24'
Generated
+123 -8
View File
@@ -169,6 +169,19 @@ dependencies = [
"syn",
]
[[package]]
name = "asynchronous-codec"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568"
dependencies = [
"bytes",
"futures-sink",
"futures-util",
"memchr",
"pin-project-lite",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@@ -496,7 +509,7 @@ dependencies = [
"pin-project-lite",
"rustls 0.21.12",
"rustls 0.23.37",
"rustls-native-certs",
"rustls-native-certs 0.8.3",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.4",
@@ -770,7 +783,7 @@ dependencies = [
"pin-project-lite",
"rand 0.9.2",
"rustls 0.23.37",
"rustls-native-certs",
"rustls-native-certs 0.8.3",
"rustls-pki-types",
"serde",
"serde_derive",
@@ -968,6 +981,12 @@ version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
[[package]]
name = "connection-string"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "510ca239cf13b7f8d16a2b48f263de7b4f8c566f0af58d901031473c76afb1e3"
[[package]]
name = "const-oid"
version = "0.9.6"
@@ -1436,6 +1455,26 @@ dependencies = [
"syn",
]
[[package]]
name = "enumflags2"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
dependencies = [
"enumflags2_derive",
]
[[package]]
name = "enumflags2_derive"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -2050,7 +2089,7 @@ dependencies = [
"hyper 1.8.1",
"hyper-util",
"rustls 0.23.37",
"rustls-native-certs",
"rustls-native-certs 0.8.3",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.4",
@@ -2854,6 +2893,12 @@ dependencies = [
"syn",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-probe"
version = "0.2.1"
@@ -3038,7 +3083,7 @@ dependencies = [
[[package]]
name = "portabase-agent"
version = "1.7.0"
version = "1.11.1"
dependencies = [
"aes",
"aes-gcm",
@@ -3076,6 +3121,7 @@ dependencies = [
"testcontainers",
"testcontainers-modules",
"thiserror 2.0.18",
"tiberius",
"time",
"tokio",
"tokio-postgres",
@@ -3191,6 +3237,12 @@ dependencies = [
"termtree",
]
[[package]]
name = "pretty-hex"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5"
[[package]]
name = "prettyplease"
version = "0.2.37"
@@ -3677,16 +3729,37 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [
"openssl-probe 0.1.6",
"rustls-pemfile",
"schannel",
"security-framework 2.11.1",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
dependencies = [
"openssl-probe",
"openssl-probe 0.2.1",
"rustls-pki-types",
"schannel",
"security-framework",
"security-framework 3.7.0",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64 0.21.7",
]
[[package]]
@@ -3711,10 +3784,10 @@ dependencies = [
"log",
"once_cell",
"rustls 0.23.37",
"rustls-native-certs",
"rustls-native-certs 0.8.3",
"rustls-platform-verifier-android",
"rustls-webpki 0.103.9",
"security-framework",
"security-framework 3.7.0",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
@@ -3832,6 +3905,19 @@ dependencies = [
"zeroize",
]
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.11.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework"
version = "3.7.0"
@@ -4353,6 +4439,34 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "tiberius"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1446cb4198848d1562301a3340424b4f425ef79f35ef9ee034769a9dd92c10d"
dependencies = [
"async-trait",
"asynchronous-codec",
"byteorder",
"bytes",
"chrono",
"connection-string",
"encoding_rs",
"enumflags2",
"futures-util",
"num-traits",
"once_cell",
"pin-project-lite",
"pretty-hex",
"rustls-native-certs 0.6.3",
"rustls-pemfile",
"thiserror 1.0.69",
"tokio-rustls 0.24.1",
"tokio-util",
"tracing",
"uuid",
]
[[package]]
name = "time"
version = "0.3.47"
@@ -4668,6 +4782,7 @@ version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
+3 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "portabase-agent"
version = "1.7.0"
version = "1.11.1"
edition = "2024"
[dependencies]
@@ -35,7 +35,8 @@ rand = "0.9.2"
bytes = "1.11.0"
async-stream = "0.3.6"
uuid = { version = "1.20.0", features = ["v4"] }
tokio-util = "0.7.18"
tokio-util = { version = "0.7.18", features = ["compat"] }
tiberius = { version = "0.12", default-features = false, features = ["rustls", "chrono"] }
aws-config = "1.8.13"
aws-sdk-s3 = { version = "1.122.0", features = ["behavior-version-latest"] }
async-compression = { version = "0.4.37", features = ["tokio", "gzip"] }
-54
View File
@@ -1,54 +0,0 @@
include .env
export $(shell sed 's/=.*//' .env)
CLUSTER_SCRIPT=docker/entrypoints/app-dev-entrypoint.sh
.PHONY: seed-mongo seed-mysql seed-postgres
up:
@bash $(CLUSTER_SCRIPT)
seed-mongo:
@echo "Seeding MongoDB..."
bash ./scripts/mongo/seed-mongo.sh
seed-mongo-auth:
@echo "Seeding MongoDB with auth..."
bash ./scripts/mongo/seed-mongo.sh auth
seed-mysql:
@echo "Seeding MySQL..."
mysql -h 127.0.0.1 -P "$$MYSQL_PORT" -u "$$MYSQL_USER" -p"$$MYSQL_PASSWORD" "$$MYSQL_DB" < ./scripts/mysql/seed-mysql.sql
seed-mysql-1gb:
@echo "Seeding MySQL..."
mysql -h 127.0.0.1 -P "$$MYSQL_PORT" -u "$$MYSQL_USER" -p"$$MYSQL_PASSWORD" "$$MYSQL_DB" < ./scripts/mysql/seed-1gb.sql
seed-postgres:
@echo "Seeding Postgres..."
docker exec -i -e PGPASSWORD=$$PG_PASSWORD $$PG_CONTAINER \
psql -U $$PG_USER -d $$PG_DB < ./scripts/postgres/seed.sql
seed-postgres-1gb:
@echo "Seeding Postgres..."
docker exec -i -e PGPASSWORD=$$PG_PASSWORD $$PG_CONTAINER \
psql -U $$PG_USER -d $$PG_DB < ./scripts/postgres/seed-1gb.sql
SQLITE_SEED_FILE := $(if $(filter big,$(SEED)),./scripts/sqlite/seed-big.sql,./scripts/sqlite/seed.sql)
seed-sqlite:
@echo "Seeding Sqlite..."
@echo "Run as root to fix permissions inside the volume"
docker exec -u 0 -it db-sqlite sh -c "chmod -R 777 /workspace/data"
@echo "Create the database file (if it doesnt exist)"
docker exec -u 0 -it db-sqlite sh -c "touch /workspace/data/app.db"
@echo "Seed the database"
docker exec -i db-sqlite sh -c "sqlite3 /workspace/data/app.db" < $(SQLITE_SEED_FILE)
@echo "Verify"
docker exec -it db-sqlite sqlite3 /workspace/data/app.db "SELECT name FROM sqlite_master WHERE type='table';"
@echo "Done"
seed-all: seed-mongo seed-mysql seed-postgres seed-postgres-1gb
+6 -2
View File
@@ -13,6 +13,7 @@
[![License: Apache](https://img.shields.io/badge/License-apache-yellow.svg)](LICENSE)
[![Docker Pulls](https://img.shields.io/docker/pulls/portabase/agent?color=brightgreen)](https://hub.docker.com/r/portabase/agent)
[![Helm Chart](https://img.shields.io/badge/Helm-Kubernetes-326ce5?logo=helm&logoColor=white)](https://github.com/Portabase/agent/pkgs/container/charts%2Fportabase-agent)
[![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey)](https://github.com/Portabase/portabase)
[![Support Portabase](https://img.shields.io/badge/Support-Portabase-orange)](https://www.buymeacoffee.com/portabase)
@@ -20,9 +21,12 @@
[![MySQL](https://img.shields.io/badge/MySQL-4479A1?logo=mysql&logoColor=white)](https://www.mysql.com/)
[![SQLite](https://img.shields.io/badge/-SQLite-blue?logo=sqlite&logoColor=white)](https://sqlite.org/)
[![Redis](https://img.shields.io/badge/Redis-DC382D?style=flat&logo=Redis&logoColor=white)](https://redis.io/)
[![Valkey](https://img.shields.io/badge/Valkey-6284fc?style=flat&logo=Valkey&logoColor=white)](https://valkey.io/)
[![MariaDB](https://img.shields.io/badge/MariaDB-003545?logo=mariadb&logoColor=white)](https://mariadb.org/)
[![MongoDB](https://img.shields.io/badge/-MongoDB-13aa52?logo=mongodb&logoColor=white)](https://www.mongodb.com/)
[![Valkey](https://img.shields.io/badge/Valkey-6284fc?style=flat&logo=Valkey&logoColor=white)](https://valkey.io/)
[![Firebird](https://img.shields.io/badge/Firebird-f55b14?style=flat&logo=Firebird&logoColor=white)](https://firebirdsql.org/)
[![Self Hosted](https://img.shields.io/badge/self--hosted-yes-brightgreen)](https://github.com/Portabase/portabase)
[![Open Source](https://img.shields.io/badge/open%20source-❤️-red)](https://github.com/Portabase/portabase)
@@ -50,7 +54,7 @@ You have 4 ways to install Portabase Agent:
- Automated CLI (recommended) - [details](https://portabase.io/docs/agent/setup#cli)
- Docker Compose setup - [details](https://portabase.io/docs/agent/setup#docker)
- Kubernetes with Helm (soon)
- Kubernetes with Helm [details](https://portabase.io/docs/agent/setup#helm)
- Development setup - [details](https://portabase.io/docs/agent/setup#development)
**Ensure Docker is installed on your machine before getting started.**
+29 -9
View File
@@ -21,7 +21,7 @@
"generated_id": "16678124-ff7e-4c97-8c83-0adeff214681"
},
{
"name": "Test database 4 - MongoDB Auth",
"name": "Test database 3 - MongoDB Auth",
"database": "testdbauth",
"type": "mongodb",
"username": "root",
@@ -31,7 +31,7 @@
"generated_id": "16678145-ff7e-4c97-8c83-0adeff214681"
},
{
"name": "Test database 5 - MongoDB",
"name": "Test database 4 - MongoDB",
"database": "testdb",
"type": "mongodb",
"port": 27017,
@@ -39,26 +39,26 @@
"generated_id": "16678147-ff7e-4c97-8c83-0adeff214681"
},
{
"name": "Test database 6 - SQLite DB",
"name": "Test database 5 - SQLite DB",
"type": "sqlite",
"path": "/sqlite-data/workspace/data/app.db",
"generated_id": "16678178-ff7e-4c97-8c83-0adeff214681"
},
{
"name": "Test database 7 - SQLite DB",
"name": "Test database 6 - SQLite DB",
"type": "sqlite",
"path": "/sqlite-data-2/workspace/data/app.db",
"generated_id": "16678179-ff7e-4c97-8c83-0adeff214681"
},
{
"name": "Test database 8 - Redis",
"name": "Test database 7 - Redis",
"type": "redis",
"port": 6379,
"host": "db-redis",
"generated_id": "16678166-ff7e-4c97-8c83-0adeff214681"
},
{
"name": "Test database 9 - Redis Auth",
"name": "Test database 8 - Redis Auth",
"type": "redis",
"password": "supersecurepassword",
"port": 6379,
@@ -67,14 +67,14 @@
"generated_id": "16678160-ff7e-4c97-8c83-0adeff214681"
},
{
"name": "Test database 10 - Valkey",
"name": "Test database 9 - Valkey",
"type": "valkey",
"port": 6379,
"host": "db-valkey",
"generated_id": "16678560-ff7e-4c97-8c83-0adeff214681"
},
{
"name": "Test database 11 - Valkey Auth",
"name": "Test database 10 - Valkey Auth",
"type": "valkey",
"password": "supersecurepassword",
"port": 6379,
@@ -83,7 +83,7 @@
"generated_id": "16678561-ff7e-4c97-8c83-0adeff214681"
},
{
"name": "Test database 12 - Mysql",
"name": "Test database 11 - Mysql",
"database": "mysqldb",
"type": "mysql",
"username": "mysqldb",
@@ -91,6 +91,26 @@
"port": 3306,
"host": "db-mysql",
"generated_id": "16656124-ff7e-4c97-8c83-0adeff214681"
},
{
"name": "Test database 12 - Firebird",
"database": "/var/lib/firebird/data/mirror.fdb",
"type": "firebird",
"username": "alice",
"password": "fake_password",
"port": 3050,
"host": "db-firebird",
"generated_id": "16706124-ff7e-4c97-8c83-0adeff214681"
},
{
"name": "Test database 13 - MsSQL",
"database": "myappdb",
"type": "mssql",
"username": "sa",
"password": "Portabase!Strong1",
"port": 1433,
"host": "db-mssql",
"generated_id": "16706125-ff7e-4c97-8c83-0adeff214682"
}
]
}
+77 -3
View File
@@ -29,11 +29,85 @@ host = "db-mongodb-auth"
generated_id = "16678145-ff7e-4c97-8c83-0adeff214681"
[[databases]]
name = "Test database 3 - MongoDB"
name = "Test database 4 - MongoDB"
database = "testdb"
type = "mongodb"
username = ""
password = ""
port = 27017
host = "db-mongodb"
generated_id = "16678147-ff7e-4c97-8c83-0adeff214681"
[[databases]]
name = "Test database 5 - SQLite DB"
type = "sqlite"
path = "/sqlite-data/workspace/data/app.db"
generated_id = "16678178-ff7e-4c97-8c83-0adeff214681"
[[databases]]
name = "Test database 6 - SQLite DB 2"
type = "sqlite"
path = "/sqlite-data-2/workspace/data/app.db"
generated_id = "16678179-ff7e-4c97-8c83-0adeff214681"
[[databases]]
name = "Test database 7 - Redis"
type = "redis"
port = 6379
host = "db-redis"
generated_id = "16678166-ff7e-4c97-8c83-0adeff214681"
[[databases]]
name = "Test database 8 - Redis Auth"
type = "redis"
password = "supersecurepassword"
username = "default"
port = 6379
host = "db-redis-auth"
generated_id = "16678160-ff7e-4c97-8c83-0adeff214681"
[[databases]]
name = "Test database 9 - Valkey"
type = "valkey"
port = 6379
host = "db-valkey"
generated_id = "16678560-ff7e-4c97-8c83-0adeff214681"
[[databases]]
name = "Test database 10 - Valkey Auth"
type = "valkey"
password = "supersecurepassword"
username = "default"
port = 6379
host = "db-valkey-auth"
generated_id = "16678561-ff7e-4c97-8c83-0adeff214681"
[[databases]]
name = "Test database 11 - Mysql"
database = "mysqldb"
type = "mysql"
password = "changeme"
username = "mysqldb"
port = 3306
host = "db-mysql"
generated_id = "16656124-ff7e-4c97-8c83-0adeff214681"
[[databases]]
name = "Test database 12 - Firebird"
database = "/var/lib/firebird/data/mirror.fdb"
type = "firebird"
password = "fake_password"
username = "alice"
port = 3050
host = "db-firebird"
generated_id = "16706124-ff7e-4c97-8c83-0adeff214681"
[[databases]]
name = "Test database 13 - MSSQL"
database = "myappdb"
type = "mssql"
username = "sa"
password = "Portabase!Strong1"
port = 1433
host = "db-mssql"
generated_id = "16706125-ff7e-4c97-8c83-0adeff214682"
+198
View File
@@ -0,0 +1,198 @@
name: databases
services:
db-postgres:
container_name: db-postgres
image: postgres:17-alpine
ports:
- "5436:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=devdb
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme
networks:
- portabase
db-mariadb:
container_name: db-mariadb
image: mariadb:latest
ports:
- "3311:3306"
environment:
- MYSQL_DATABASE=mariadb
- MYSQL_USER=mariadb
- MYSQL_PASSWORD=changeme
- MYSQL_RANDOM_ROOT_PASSWORD=yes
volumes:
- mariadb-data:/var/lib/mysql
networks:
- portabase
db-mysql:
container_name: db-mysql
image: mysql:9.5
ports:
- "3312:3306"
environment:
- MYSQL_DATABASE=mysqldb
- MYSQL_USER=mysqldb
- MYSQL_PASSWORD=changeme
- MYSQL_RANDOM_ROOT_PASSWORD=yes
volumes:
- mysql-data:/var/lib/mysql
networks:
- portabase
db-mongodb-auth:
container_name: db-mongodb-auth
image: mongo:latest
ports:
- "27082:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: rootpassword
MONGO_INITDB_DATABASE: testdbauth
command: mongod --auth
networks:
- portabase
volumes:
- mongodb-data-auth:/data/db
healthcheck:
test: [ "CMD", "mongo", "--eval", "db.adminCommand('ping')" ]
interval: 5s
timeout: 5s
retries: 10
db-mongodb:
container_name: db-mongodb
image: mongo:latest
ports:
- "27083:27017"
volumes:
- mongodb-data:/data/db
healthcheck:
test: [ "CMD", "mongosh", "--eval", "db.adminCommand('ping')" ]
interval: 5s
timeout: 5s
retries: 10
environment:
MONGO_INITDB_DATABASE: testdb
networks:
- portabase
sqlite:
container_name: db-sqlite
image: keinos/sqlite3
volumes:
- sqlite-data:/workspace/data
working_dir: /workspace
command: tail -f /dev/null
stdin_open: true
tty: true
db-redis:
image: redis:latest
container_name: db-redis
ports:
- "6378:6379"
volumes:
- redis-data:/data
command: [ "redis-server", "--appendonly", "yes" ]
networks:
- portabase
db-redis-auth:
image: redis:latest
container_name: db-redis-auth
ports:
- "6380:6379"
volumes:
- redis-data-auth:/data
environment:
- REDIS_PASSWORD=supersecurepassword
command: [ "redis-server", "--requirepass", "supersecurepassword", "--appendonly", "yes" ]
networks:
- portabase
db-valkey:
image: valkey/valkey
container_name: db-valkey
environment:
- ALLOW_EMPTY_PASSWORD=yes
ports:
- '6381:6379'
volumes:
- valkey-data:/data
networks:
- portabase
db-valkey-auth:
image: valkey/valkey
container_name: db-valkey-auth
command: >
--requirepass "supersecurepassword"
ports:
- '6382:6379'
volumes:
- valkey-data-auth:/data
networks:
- portabase
db-firebird:
image: firebirdsql/firebird
container_name: db-firebird
restart: always
environment:
- FIREBIRD_ROOT_PASSWORD=fake_root_password
- FIREBIRD_USER=alice
- FIREBIRD_PASSWORD=fake_password
- FIREBIRD_DATABASE=mirror.fdb
- FIREBIRD_DATABASE_DEFAULT_CHARSET=UTF8
volumes:
- firebird-data:/var/lib/firebird/data
ports:
- "3060:3050"
networks:
- portabase
db-mssql:
container_name: db-mssql
image: mcr.microsoft.com/azure-sql-edge:latest
ports:
- "1433:1433"
environment:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: "Portabase!Strong1"
volumes:
- mssql-data:/var/opt/mssql
networks:
- portabase
healthcheck:
test: ["CMD-SHELL", "cat /proc/net/tcp6 | grep -q '059901' || exit 1"]
interval: 10s
timeout: 5s
retries: 20
volumes:
postgres-data:
mariadb-data:
mysql-data:
mongodb-data:
mongodb-data-auth:
sqlite-data:
redis-data:
redis-data-auth:
valkey-data:
valkey-data-auth:
firebird-data:
mssql-data:
networks:
portabase:
name: portabase_network
external: true
+11 -158
View File
@@ -7,19 +7,20 @@ services:
container_name: rust-dev
volumes:
- .:/app
- ./databases.json:/config/config.json
# - ./databases.toml:/config/config.toml
- cargo-registry:/usr/local/cargo/registry
- cargo-git:/usr/local/cargo/git
# - /var/run/docker.sock:/var/run/docker.sock
# - cargo-target:/app/target
# - sqlite-data:/sqlite-data/workspace/data
# - ./scripts/sqlite/test-db:/sqlite-data-2/workspace/data
- ./databases.json:/config/config.json
#- ./databases.toml:/config/config.toml
#- /var/run/docker.sock:/var/run/docker.sock
#- cargo-target:/app/target
- databases_sqlite-data:/sqlite-data/workspace/data
- ./scripts/sqlite/test-db:/sqlite-data-2/workspace/data
environment:
APP_ENV: development
LOG: debug
TZ: "Europe/Paris"
EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiNGI1OTM2MGItNTNkMi00ZTZmLWE1ODctODcyMmQ1NDc1MTNmIiwibWFzdGVyS2V5QjY0IjoiQlhWM1hvbEM2NTZTVjdkTmdjV1BHUWxrKytycExJNmxHRGk3Q1BCNWllbz0ifQ=="
EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiNzNlZmJhNjMtNTkzMy00Mzk3LWI0ZmMtMjlmNTViNmI5YzA4IiwibWFzdGVyS2V5QjY0IjoiQlhWM1hvbEM2NTZTVjdkTmdjV1BHUWxrKytycExJNmxHRGk3Q1BCNWllbz0ifQ=="
#CHUNK_SIZE_MB: "1"
#POOLING: 1
#DATABASES_CONFIG_FILE: "config.toml"
extra_hosts:
@@ -27,160 +28,12 @@ services:
networks:
- portabase
# db-postgres:
# container_name: db-postgres
# image: postgres:17-alpine
# ports:
# - "5436:5432"
# volumes:
# - postgres-data:/var/lib/postgresql/data
# environment:
# - POSTGRES_DB=devdb
# - POSTGRES_USER=devuser
# - POSTGRES_PASSWORD=changeme
# networks:
# - portabase
db-mariadb:
container_name: db-mariadb
image: mariadb:latest
ports:
- "3311:3306"
environment:
- MYSQL_DATABASE=mariadb
- MYSQL_USER=mariadb
- MYSQL_PASSWORD=changeme
- MYSQL_RANDOM_ROOT_PASSWORD=yes
volumes:
- mariadb-data:/var/lib/mysql
networks:
- portabase
db-mysql:
container_name: db-mysql
image: mysql:9.5
ports:
- "3312:3306"
environment:
- MYSQL_DATABASE=mysqldb
- MYSQL_USER=mysqldb
- MYSQL_PASSWORD=changeme
- MYSQL_RANDOM_ROOT_PASSWORD=yes
volumes:
- mysql-data:/var/lib/mysql
networks:
- portabase
#
#
# db-mongodb-auth:
# container_name: db-mongodb-auth
# image: mongo:latest
# ports:
# - "27082:27017"
# environment:
# MONGO_INITDB_ROOT_USERNAME: root
# MONGO_INITDB_ROOT_PASSWORD: rootpassword
# MONGO_INITDB_DATABASE: testdbauth
# command: mongod --auth
# networks:
# - portabase
# volumes:
# - mongodb-data-auth:/data/db
# healthcheck:
# test: [ "CMD", "mongo", "--eval", "db.adminCommand('ping')" ]
# interval: 5s
# timeout: 5s
# retries: 10
#
# db-mongodb:
# container_name: db-mongodb
# image: mongo:latest
# ports:
# - "27083:27017"
# volumes:
# - mongodb-data:/data/db
# healthcheck:
# test: [ "CMD", "mongosh", "--eval", "db.adminCommand('ping')" ]
# interval: 5s
# timeout: 5s
# retries: 10
# environment:
# MONGO_INITDB_DATABASE: testdb
# networks:
# - portabase
# sqlite:
# container_name: db-sqlite
# image: keinos/sqlite3
# volumes:
# - sqlite-data:/workspace/data
# working_dir: /workspace
# command: tail -f /dev/null
# stdin_open: true
# tty: true
# db-redis:
# image: redis:latest
# container_name: db-redis
# ports:
# - "6379:6379"
# volumes:
# - redis-data:/data
# command: [ "redis-server", "--appendonly", "yes" ]
# networks:
# - portabase
#
# db-redis-auth:
# image: redis:latest
# container_name: db-redis-auth
# ports:
# - "6380:6379"
# volumes:
# - redis-data-auth:/data
# environment:
# - REDIS_PASSWORD=supersecurepassword
# command: [ "redis-server", "--requirepass", "supersecurepassword", "--appendonly", "yes" ]
# networks:
# - portabase
#
# db-valkey:
# image: valkey/valkey
# container_name: db-valkey
# environment:
# - ALLOW_EMPTY_PASSWORD=yes
# ports:
# - '6381:6379'
# volumes:
# - valkey-data:/data
# networks:
# - portabase
#
# db-valkey-auth:
# image: valkey/valkey
# container_name: db-valkey-auth
# command: >
# --requirepass "supersecurepassword"
# ports:
# - '6382:6379'
# volumes:
# - valkey-data-auth:/data
# networks:
# - portabase
volumes:
cargo-registry:
cargo-git:
# cargo-target:
# postgres-data:
mariadb-data:
mysql-data:
# mongodb-data:
# mongodb-data-auth:
# sqlite-data:
# redis-data:
# redis-data-auth:
# valkey-data:
# valkey-data-auth:
#cargo-target:
databases_sqlite-data:
external: true
networks:
portabase:
+24 -1
View File
@@ -18,9 +18,24 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
sqlite3 \
redis-tools \
valkey \
firebird3.0-utils \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
ENV DOTNET_ROOT=/usr/local/dotnet
RUN curl -sSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh \
&& chmod +x /tmp/dotnet-install.sh \
&& /tmp/dotnet-install.sh --channel 8.0 --install-dir /usr/local/dotnet \
&& rm /tmp/dotnet-install.sh \
&& /usr/local/dotnet/dotnet tool install --global microsoft.sqlpackage
ENV PATH="$PATH:/usr/local/dotnet:/root/.dotnet/tools"
RUN ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') \
&& curl -sSL "https://github.com/microsoft/go-sqlcmd/releases/latest/download/sqlcmd-linux-${ARCH}.tar.bz2" \
| tar -xjf - -C /usr/local/bin sqlcmd \
&& chmod +x /usr/local/bin/sqlcmd
ARG TARGETARCH
# =========================
@@ -96,12 +111,19 @@ RUN apt-get update && apt-get install -y \
libreadline8 \
libncurses6 \
zlib1g \
curl \
mariadb-client \
sqlite3 \
redis-tools \
valkey \
firebird3.0-utils \
&& rm -rf /var/lib/apt/lists/*
ENV DOTNET_ROOT=/usr/local/dotnet
RUN curl -sSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh \
&& chmod +x /tmp/dotnet-install.sh \
&& /tmp/dotnet-install.sh --channel 8.0 --runtime dotnet --install-dir /usr/local/dotnet \
&& rm /tmp/dotnet-install.sh
WORKDIR /app
@@ -112,8 +134,9 @@ RUN chmod +x /entrypoint.sh
COPY --from=base /usr/lib/postgresql/ /usr/lib/postgresql/
COPY --from=base /usr/local/mongodb/bin/ /usr/local/mongodb/bin/
COPY --from=base /root/.dotnet/tools/ /root/.dotnet/tools/
ENV PATH="$PATH:/usr/local/dotnet:/root/.dotnet/tools"
ENV APP_ENV=production
CMD ["/entrypoint.sh"]
+8 -3
View File
@@ -34,6 +34,11 @@ check_network() {
check_docker
check_network
echo "Starting docker-compose..."
docker compose -f ./docker-compose.yml up
echo "Docker-compose started successfully."
echo "Stopping old database containers..."
docker compose -f ./docker-compose.databases.yml down
echo "Starting database containers..."
docker compose -f ./docker-compose.databases.yml up -d
echo "Starting main services..."
docker compose -f ./docker-compose.yml up
+61
View File
@@ -0,0 +1,61 @@
set dotenv-load := true
set shell := ["bash", "-cu"]
CLUSTER_SCRIPT := "docker/entrypoints/app-dev-entrypoint.sh"
up:
bash {{CLUSTER_SCRIPT}}
seed-mongo:
echo "Seeding MongoDB..."
bash ./scripts/mongo/seed-mongo.sh
seed-mongo-auth:
echo "Seeding MongoDB with auth..."
bash ./scripts/mongo/seed-mongo.sh auth
seed-mysql:
echo "Seeding MySQL..."
mysql -h 127.0.0.1 -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DB" < ./scripts/mysql/seed-mysql.sql
seed-mysql-1gb:
echo "Seeding MySQL (1GB)..."
mysql -h 127.0.0.1 -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DB" < ./scripts/mysql/seed-1gb.sql
seed-postgres:
echo "Seeding Postgres..."
docker exec -i -e PGPASSWORD="$PG_PASSWORD" "$PG_CONTAINER" \
psql -U "$PG_USER" -d "$PG_DB" < ./scripts/postgres/seed.sql
seed-postgres-1gb:
echo "Seeding Postgres (1GB)..."
docker exec -i -e PGPASSWORD="$PG_PASSWORD" "$PG_CONTAINER" \
psql -U "$PG_USER" -d "$PG_DB" < ./scripts/postgres/seed-1gb.sql
seed-sqlite:
echo "Seeding SQLite..."
bash ./scripts/sqlite/seed.sh
echo "Done"
seed-firebird:
echo "Seeding Firebird..."
docker exec -i db-firebird isql -user alice -password fake_password /var/lib/firebird/data/mirror.fdb < ./scripts/firebird/seed.sql
echo "Verifying Firebird tables..."
echo "SELECT RDB\$RELATION_NAME FROM RDB\$RELATIONS WHERE RDB\$SYSTEM_FLAG = 0 AND RDB\$VIEW_BLR IS NULL;" \
| docker exec -i db-firebird isql -user alice -password fake_password /var/lib/firebird/data/mirror.fdb
seed-mssql:
echo "Seeding MSSQL..."
docker exec -i rust-dev sqlcmd -S "db-mssql,1433" -U sa -P "$MSSQL_SA_PASSWORD" -N disable -i /app/scripts/mssql/seed.sql
echo "Done"
seed-all:
just seed-mongo
just seed-mysql
just seed-postgres
just seed-postgres-1gb
just seed-sqlite
just seed-mongo
just seed-firebird
just seed-mssql
+11
View File
@@ -0,0 +1,11 @@
CREATE TABLE users (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (email, name) VALUES ('alice@example.com', 'Alice');
INSERT INTO users (email, name) VALUES ('bob@example.com', 'Bob');
COMMIT;
+20
View File
@@ -0,0 +1,20 @@
IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = N'myappdb')
CREATE DATABASE [myappdb];
GO
USE [myappdb];
GO
IF OBJECT_ID('users', 'U') IS NULL
CREATE TABLE users (
id INT IDENTITY(1,1) PRIMARY KEY,
email NVARCHAR(255) NOT NULL UNIQUE,
name NVARCHAR(255),
created_at DATETIME DEFAULT GETDATE()
);
GO
INSERT INTO users (email, name) VALUES ('alice@example.com', 'Alice');
INSERT INTO users (email, name) VALUES ('bob@example.com', 'Bob');
INSERT INTO users (email, name) VALUES ('charlie@example.com', 'Charlie');
GO
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
SEED_VALUE="${SEED:-}"
if [ "$SEED_VALUE" = "big" ]; then
SQLITE_SEED_FILE="./scripts/sqlite/seed-big.sql"
else
SQLITE_SEED_FILE="./scripts/sqlite/seed.sql"
fi
docker exec -u 0 db-sqlite sh -c "chmod -R 777 /workspace/data"
docker exec -u 0 db-sqlite sh -c "touch /workspace/data/app.db"
docker exec -i db-sqlite sh -c "sqlite3 /workspace/data/app.db" < "$SQLITE_SEED_FILE"
+6
View File
@@ -9,7 +9,9 @@ use crate::services::config::{DatabaseConfig, DbType};
use anyhow::Result;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::domain::firebird::database::FirebirdDatabase;
use crate::domain::mariadb::database::MariaDBDatabase;
use crate::domain::mssql::database::MssqlDatabase;
#[async_trait::async_trait]
pub trait Database: Send + Sync {
@@ -34,6 +36,8 @@ impl DatabaseFactory {
DbType::Sqlite => Arc::new(SqliteDatabase::new(cfg)),
DbType::Redis => Arc::new(RedisDatabase::new(cfg)),
DbType::Valkey => Arc::new(ValkeyDatabase::new(cfg)),
DbType::Firebird => Arc::new(FirebirdDatabase::new(cfg)),
DbType::Mssql => Arc::new(MssqlDatabase::new(cfg)),
}
}
@@ -49,6 +53,8 @@ impl DatabaseFactory {
DbType::Sqlite => Arc::new(SqliteDatabase::new(cfg)),
DbType::Redis => Arc::new(RedisDatabase::new(cfg)),
DbType::Valkey => Arc::new(ValkeyDatabase::new(cfg)),
DbType::Firebird => Arc::new(FirebirdDatabase::new(cfg)),
DbType::Mssql => Arc::new(MssqlDatabase::new(cfg)),
}
}
}
+48
View File
@@ -0,0 +1,48 @@
use crate::services::config::DatabaseConfig;
use anyhow::{Context, Result};
use std::path::PathBuf;
use std::process::Command;
use tracing::{debug, error, info};
pub async fn run(
cfg: DatabaseConfig,
backup_dir: PathBuf,
file_extension: &'static str,
) -> Result<PathBuf> {
tokio::task::spawn_blocking(move || -> Result<PathBuf> {
debug!("Starting backup for database {}", cfg.name);
let file_path = backup_dir.join(format!("{}{}", cfg.generated_id, file_extension));
let db_path = format!(
"{}/{}:{}",
cfg.host,
cfg.port,
cfg.database
);
info!("Firebird database target: {}", db_path);
info!("Backup file: {}", file_path.display());
let output = Command::new("gbak")
.arg("-b")
.arg("-v")
.arg("-user").arg(&cfg.username)
.arg("-password").arg(&cfg.password)
.arg(db_path)
.arg(&file_path)
.output()
.with_context(|| format!("Failed to run gbak for {}", cfg.name))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
error!("Firebird backup failed: {}", stderr);
anyhow::bail!("Firebird backup failed for {}: {}", cfg.name, stderr);
}
info!("Firebird backup completed: {}", file_path.display());
Ok(file_path)
})
.await?
}
+48
View File
@@ -0,0 +1,48 @@
use super::{backup, ping, restore};
use crate::domain::factory::Database;
use crate::services::config::DatabaseConfig;
use crate::utils::locks::{DbOpLock, FileLock};
use anyhow::Result;
use async_trait::async_trait;
use std::path::{Path, PathBuf};
pub struct FirebirdDatabase {
cfg: DatabaseConfig,
}
impl FirebirdDatabase {
pub fn new(cfg: DatabaseConfig) -> Self {
Self { cfg }
}
}
#[async_trait]
impl Database for FirebirdDatabase {
fn file_extension(&self) -> &'static str {
".fbk"
}
async fn ping(&self) -> Result<bool> {
ping::run(self.cfg.clone()).await
}
async fn backup(&self, dir: &Path) -> Result<PathBuf> {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
let res = backup::run(
self.cfg.clone(),
dir.to_path_buf(),
self.file_extension(),
)
.await;
FileLock::release(&self.cfg.generated_id).await?;
res
}
async fn restore(&self, file: &Path) -> Result<()> {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Restore.as_str()).await?;
let res = restore::run(self.cfg.clone(), file.to_path_buf()).await;
FileLock::release(&self.cfg.generated_id).await?;
res
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod database;
mod ping;
mod backup;
mod restore;
+57
View File
@@ -0,0 +1,57 @@
use std::process::Stdio;
use tracing::{error, info};
use tokio::io::AsyncWriteExt;
use crate::services::config::DatabaseConfig;
use tokio::process::Command;
use tokio::time::{Duration, timeout};
pub async fn run(cfg: DatabaseConfig) -> anyhow::Result<bool> {
let db_path = format!("{}/{}:{}", cfg.host, cfg.port, cfg.database);
info!("Running Ping database from {}", db_path);
let mut child = Command::new("isql-fb")
.arg("-q")
.arg("-user")
.arg(&cfg.username)
.arg("-password")
.arg(&cfg.password)
.arg(&db_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let query = b"SELECT 1 FROM RDB$DATABASE;\nQUIT;\n";
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(query).await?;
}
let output = match timeout(Duration::from_secs(5), child.wait_with_output()).await {
Ok(res) => res?,
Err(_) => return Ok(false),
};
if !output.status.success() {
error!("Error output for firebird: {:?}", output);
return Ok(false);
}
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.to_lowercase().contains("error") {
error!("Error output for firebird: {:?}", output);
return Ok(false);
}
if stdout.contains("1") {
return Ok(true);
}
info!("stdout {}", stdout);
error!("stderr {}", stderr);
Ok(false)
}
+40
View File
@@ -0,0 +1,40 @@
use crate::services::config::DatabaseConfig;
use anyhow::{Context, Result};
use std::path::PathBuf;
use std::process::Command;
use tracing::{debug, error, info};
pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf) -> Result<()> {
tokio::task::spawn_blocking(move || -> Result<()> {
debug!("Starting Firebird restore for database {}", cfg.name);
let db_path = format!("{}/{}:{}", cfg.host, cfg.port, cfg.database);
info!("Restore source: {}", restore_file.display());
info!("Restore target: {}", db_path);
let output = Command::new("gbak")
.arg("-c")
.arg("-v")
.arg("-replace_database")
.arg("-user")
.arg(&cfg.username)
.arg("-password")
.arg(&cfg.password)
.arg(&restore_file)
.arg(&db_path)
.output()
.with_context(|| format!("Failed to run gbak restore for {}", cfg.name))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
error!("Firebird restore failed for {}: {}", cfg.name, stderr);
anyhow::bail!("Firebird restore failed for {}: {}", cfg.name, stderr);
}
info!("Firebird restore completed for {}", cfg.name);
Ok(())
})
.await?
}
+4 -3
View File
@@ -33,6 +33,7 @@ pub async fn run(
let mariadb_dump = select_mariadb_path(&version).join("mariadb-dump");
info!("Mariadb dump found: {}", mariadb_dump.display());
let output = Command::new("mariadb-dump")
.arg("--host").arg(&cfg.host)
.arg("--port").arg(cfg.port.to_string())
@@ -43,18 +44,18 @@ pub async fn run(
.arg("--single-transaction")
.arg("--quick")
.arg("--skip-lock-tables")
.arg("--add-drop-database")
.arg("--databases").arg(&cfg.database)
.arg("--no-create-db")
.arg("--skip-add-drop-table")
.arg("--compress")
.arg("--max-allowed-packet=512M")
.arg("--net-buffer-length=16K")
.arg("--default-character-set=utf8mb4")
.arg(&cfg.database)
.arg("-r").arg(&file_path)
.envs(env)
.output()
.with_context(|| format!("Failed to run mariadb-dump for {}", cfg.name))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Mariadb backup failed for {}: {}", cfg.name, stderr);
+4 -3
View File
@@ -15,9 +15,9 @@ pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf) -> Result<()> {
.with_context(|| format!("Failed to open restore file {}", restore_file.display()))?;
file.read_to_string(&mut sql_content)
.with_context(|| format!("Failed to read restore file {}", restore_file.display()))?;
let drop_create_cmd = format!(
"DROP DATABASE IF EXISTS {0}; CREATE DATABASE {0};",
"DROP DATABASE IF EXISTS `{0}`; CREATE DATABASE `{0}`;",
cfg.database
);
@@ -47,6 +47,7 @@ pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf) -> Result<()> {
.arg(cfg.port.to_string())
.arg("--user")
.arg(&cfg.username)
.arg("--database")
.arg(&cfg.database)
.env("MYSQL_PWD", &cfg.password)
.stdin(std::process::Stdio::piped())
@@ -77,4 +78,4 @@ pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf) -> Result<()> {
handle.await??;
Ok(())
}
}
+2
View File
@@ -6,3 +6,5 @@ mod redis;
mod sqlite;
mod valkey;
mod mariadb;
mod firebird;
pub mod mssql;
-1
View File
@@ -15,7 +15,6 @@ pub async fn run(
let file_path = backup_dir.join(format!("{}{}", cfg.generated_id, file_extension));
let mongodump = select_mongo_path().join("mongodump");
info!("{:?}", mongodump);
let uri = get_mongo_uri(cfg.clone())?;
let output = Command::new(mongodump)
+15
View File
@@ -28,3 +28,18 @@ pub fn get_mongo_uri(cfg: DatabaseConfig) -> Result<String> {
))
}
}
pub fn extract_db_name(dry_output: &str) -> Option<String> {
let mut dbs = std::collections::HashSet::new();
for line in dry_output.lines() {
if let Some(pos) = line.find("archive prelude ") {
let rest = &line[pos + "archive prelude ".len()..];
if let Some(dot) = rest.find('.') {
let db = &rest[..dot];
dbs.insert(db.to_string());
}
}
}
dbs.into_iter().next()
}
+26 -3
View File
@@ -1,4 +1,4 @@
use crate::domain::mongodb::connection::{get_mongo_uri, select_mongo_path};
use crate::domain::mongodb::connection::{extract_db_name, get_mongo_uri, select_mongo_path};
use crate::services::config::DatabaseConfig;
use anyhow::{Context, Result};
use std::path::PathBuf;
@@ -12,18 +12,41 @@ pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf) -> Result<()> {
let mongorestore = select_mongo_path().join("mongorestore");
let uri = get_mongo_uri(cfg.clone())?;
let output = Command::new(mongorestore)
let dry_run = Command::new(&mongorestore)
.arg(format!(
"--uri={}",
format!(
"mongodb://{}:{}@{}:{}/?authSource=admin",
cfg.username, cfg.password, cfg.host, cfg.port
)
))
.arg(format!("--archive={}", restore_file.display()))
.arg("--gzip")
.arg("--dryRun")
.arg("--verbose")
.output()?;
let dry_output = String::from_utf8_lossy(&dry_run.stderr);
let source_db = extract_db_name(&dry_output)
.context("Could not detect source database name from archive")?;
info!("Detected source database in archive: {}", source_db);
let output = Command::new(&mongorestore)
.arg(format!("--uri={}", uri))
.arg(format!("--archive={}", restore_file.display()))
.arg("--gzip")
.arg("--drop")
.arg(format!("--nsInclude={}.*", source_db))
.arg(format!("--nsFrom={}.*", source_db))
.arg(format!("--nsTo={}.*", cfg.database))
.output()
.with_context(|| format!("Failed to run mongorestore for {}", cfg.name))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
error!("MongoDB restore failed for {}: {}", cfg.name, stderr);
anyhow::bail!("MongoDB restore failed for : {}", cfg.name);
anyhow::bail!("MongoDB restore failed for: {}", cfg.name);
}
info!("MongoDB restore completed for {}", cfg.name);
+47
View File
@@ -0,0 +1,47 @@
use crate::services::config::DatabaseConfig;
use anyhow::{Context, Result};
use std::path::PathBuf;
use std::process::Command;
use tracing::{debug, error, info};
pub async fn run(
cfg: DatabaseConfig,
backup_dir: PathBuf,
file_extension: &'static str,
) -> Result<PathBuf> {
tokio::task::spawn_blocking(move || -> Result<PathBuf> {
debug!("Starting MSSQL backup for database {}", cfg.name);
let file_path = backup_dir.join(format!("{}{}", cfg.generated_id, file_extension));
let connection_string = format!(
"Server=tcp:{},{};Database={};User Id={};Password={};TrustServerCertificate=True;Encrypt=False",
cfg.host, cfg.port, cfg.database, cfg.username, cfg.password
);
info!(
"MSSQL backup: {}:{}/{} → {}",
cfg.host,
cfg.port,
cfg.database,
file_path.display()
);
let output = Command::new("sqlpackage")
.arg("/a:Export")
.arg(format!("/scs:{}", connection_string))
.arg(format!("/tf:{}", file_path.display()))
.output()
.with_context(|| format!("Failed to run sqlpackage for {}", cfg.name))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
error!("MSSQL backup failed — stderr: {} stdout: {}", stderr, stdout);
anyhow::bail!("MSSQL backup failed for {}: {}", cfg.name, stderr);
}
info!("MSSQL backup completed: {}", file_path.display());
Ok(file_path)
})
.await?
}
+18
View File
@@ -0,0 +1,18 @@
use crate::services::config::DatabaseConfig;
use anyhow::Result;
use tiberius::{AuthMethod, Client, Config};
use tokio::net::TcpStream;
use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt};
pub async fn build_client(cfg: &DatabaseConfig) -> Result<Client<Compat<TcpStream>>> {
let mut config = Config::new();
config.host(&cfg.host);
config.port(cfg.port);
config.authentication(AuthMethod::sql_server(&cfg.username, &cfg.password));
config.trust_cert();
let tcp = TcpStream::connect(config.get_addr()).await?;
tcp.set_nodelay(true)?;
let client = Client::connect(config, tcp.compat_write()).await?;
Ok(client)
}
+47
View File
@@ -0,0 +1,47 @@
use super::{backup, ping, restore};
use crate::domain::factory::Database;
use crate::services::config::DatabaseConfig;
use crate::utils::locks::{DbOpLock, FileLock};
use anyhow::Result;
use async_trait::async_trait;
use std::path::{Path, PathBuf};
pub struct MssqlDatabase {
cfg: DatabaseConfig,
}
impl MssqlDatabase {
pub fn new(cfg: DatabaseConfig) -> Self {
Self { cfg }
}
}
#[async_trait]
impl Database for MssqlDatabase {
fn file_extension(&self) -> &'static str {
".bacpac"
}
async fn ping(&self) -> Result<bool> {
ping::run(self.cfg.clone()).await
}
async fn backup(&self, dir: &Path) -> Result<PathBuf> {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
let res = backup::run(
self.cfg.clone(),
dir.to_path_buf(),
self.file_extension(),
)
.await;
FileLock::release(&self.cfg.generated_id).await?;
res
}
async fn restore(&self, file: &Path) -> Result<()> {
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Restore.as_str()).await?;
let res = restore::run(self.cfg.clone(), file.to_path_buf()).await;
FileLock::release(&self.cfg.generated_id).await?;
res
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod database;
mod connection;
mod ping;
mod backup;
mod restore;
+24
View File
@@ -0,0 +1,24 @@
use crate::services::config::DatabaseConfig;
use anyhow::Result;
use tracing::{error, info};
pub async fn run(cfg: DatabaseConfig) -> Result<bool> {
info!("Running ping for MSSQL database {}", cfg.name);
match super::connection::build_client(&cfg).await {
Ok(mut client) => match client.simple_query("SELECT 1").await {
Ok(_) => {
info!("MSSQL ping succeeded for {}", cfg.name);
Ok(true)
}
Err(e) => {
error!("MSSQL ping query failed for {}: {:?}", cfg.name, e);
Ok(false)
}
},
Err(e) => {
error!("MSSQL connection failed for {}: {:?}", cfg.name, e);
Ok(false)
}
}
}
+45
View File
@@ -0,0 +1,45 @@
use crate::services::config::DatabaseConfig;
use anyhow::{Context, Result};
use std::path::PathBuf;
use std::process::Command;
use tracing::{debug, error, info};
pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf) -> Result<()> {
tokio::task::spawn_blocking(move || -> Result<()> {
debug!("Starting MSSQL restore for database {}", cfg.name);
let connection_string = format!(
"Server=tcp:{},{};Database={};User Id={};Password={};TrustServerCertificate=True;Encrypt=False",
cfg.host, cfg.port, cfg.database, cfg.username, cfg.password
);
info!(
"MSSQL restore: {} → {}:{}/{}",
restore_file.display(),
cfg.host,
cfg.port,
cfg.database
);
let output = Command::new("sqlpackage")
.arg("/a:Import")
.arg(format!("/tcs:{}", connection_string))
.arg(format!("/sf:{}", restore_file.display()))
.output()
.with_context(|| format!("Failed to run sqlpackage restore for {}", cfg.name))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
error!(
"MSSQL restore failed for {} — stderr: {} stdout: {}",
cfg.name, stderr, stdout
);
anyhow::bail!("MSSQL restore failed for {}: {}", cfg.name, stderr);
}
info!("MSSQL restore completed for {}", cfg.name);
Ok(())
})
.await?
}
+7 -9
View File
@@ -30,25 +30,23 @@ pub async fn run(
let file_path = backup_dir.join(format!("{}{}", cfg.generated_id, file_extension));
// let mysql_dump = select_mysql_path(&version).join("mysqldump");
// info!("MySQL dump found: {}", mysql_dump.display());
let output = Command::new("mysqldump")
.arg("--host")
.arg(cfg.host)
.arg(&cfg.host)
.arg("--port")
.arg(cfg.port.to_string())
.arg("--user")
.arg(cfg.username)
.arg(&cfg.username)
.arg("--routines")
.arg("--events")
.arg("--triggers")
.arg("--verbose")
.arg("--single-transaction")
.arg("--quick")
.arg("--add-drop-database")
.arg("--databases")
.arg(cfg.database)
.arg("--skip-lock-tables")
.arg("--skip-add-drop-table")
.arg("--no-create-db") // IMPORTANT
.arg("--default-character-set=utf8mb4")
.arg(&cfg.database) // IMPORTANT: NOT --databases
.arg("-r")
.arg(&file_path)
.envs(env)
+2 -1
View File
@@ -17,7 +17,7 @@ pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf) -> Result<()> {
.with_context(|| format!("Failed to read restore file {}", restore_file.display()))?;
let drop_create_cmd = format!(
"DROP DATABASE IF EXISTS {0}; CREATE DATABASE {0};",
"DROP DATABASE IF EXISTS `{0}`; CREATE DATABASE `{0}`;",
cfg.database
);
@@ -47,6 +47,7 @@ pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf) -> Result<()> {
.arg(cfg.port.to_string())
.arg("--user")
.arg(&cfg.username)
.arg("--database")
.arg(&cfg.database)
.env("MYSQL_PWD", &cfg.password)
.stdin(std::process::Stdio::piped())
+4 -4
View File
@@ -37,8 +37,8 @@ pub async fn run(
info!("Connections terminated for database {}", cfg.name);
let url = format!(
"postgresql://{}:{}@{}:{}/postgres",
cfg.username, cfg.password, cfg.host, cfg.port
"postgresql://{}:{}@{}:{}/{}",
cfg.username, cfg.password, cfg.host, cfg.port, cfg.database
);
debug!("Restore URL: {}", url);
@@ -51,7 +51,7 @@ pub async fn run(
.arg("--no-privileges")
.arg("--clean")
.arg("--if-exists")
.arg("--create")
// .arg("--create")
.arg("--dbname")
.arg(&url)
.arg("-v")
@@ -139,7 +139,7 @@ pub async fn run(
.arg("--no-privileges")
.arg("--clean")
.arg("--if-exists")
.arg("--create")
// .arg("--create")
.arg("--dbname")
.arg(&url)
.arg("-v")
+1 -4
View File
@@ -30,10 +30,7 @@ pub async fn run(cfg: DatabaseConfig) -> Result<bool> {
let output = output.context("Failed to execute redis-cli")?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if !stdout.is_empty() {
error!("Redis stderr: {}", stderr);
}
if stderr.contains("NOAUTH") {
error!("Redis authentication failed (NOAUTH required)");
+2 -1
View File
@@ -19,6 +19,7 @@ pub async fn run(
};
let db_path = PathBuf::from(db_path_str);
info!("database path: {}", db_path.display());
if !db_path.exists() {
anyhow::bail!("SQLite database file not found: {}", db_path.display());
@@ -31,7 +32,7 @@ pub async fn run(
.arg(format!(".backup '{}'", file_path.display()))
.output()
.context("SQLite backup command failed to start")?;
info!("Backup successful: {:?}", output);
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
error!("SQLite backup failed for {}: {}", cfg.name, stderr);
+1 -5
View File
@@ -30,11 +30,7 @@ pub async fn run(cfg: DatabaseConfig) -> Result<bool> {
let output = output.context("Failed to execute valkey-cli")?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if !stdout.is_empty() {
error!("Valkey stderr: {}", stderr);
}
if stderr.contains("NOAUTH") {
error!("Valkey authentication failed (NOAUTH required)");
return Ok(false);
+10 -7
View File
@@ -52,13 +52,16 @@ impl BackupService {
code: Some("backup_already_in_progress".into()),
}),
Err(_) => Ok(BackupResult {
generated_id,
db_type,
status: "failed".into(),
backup_file: None,
code: None,
}),
Err(e) => {
error!("Backup failed for {}: {:?}", generated_id, e);
Ok(BackupResult {
generated_id,
db_type,
status: "failed".into(),
backup_file: None,
code: None,
})
}
}
}
}
+12 -4
View File
@@ -21,6 +21,8 @@ pub enum DbType {
Sqlite,
Redis,
Valkey,
Firebird,
Mssql,
}
impl DbType {
@@ -33,6 +35,8 @@ impl DbType {
DbType::Sqlite => "sqlite",
DbType::Redis => "redis",
DbType::Valkey => "valkey",
DbType::Firebird => "firebird",
DbType::Mssql => "mssql",
}
}
}
@@ -163,14 +167,14 @@ impl ConfigService {
}
let username = match db.db_type {
DbType::Postgresql | DbType::Mysql | DbType::Mariadb => {
DbType::Postgresql | DbType::Mysql | DbType::Mariadb | DbType::Mssql => {
required(&db.username, &db.name, "username")?
}
_ => optional(&db.username),
};
let password = match db.db_type {
DbType::Postgresql | DbType::Mysql | DbType::Mariadb => {
DbType::Postgresql | DbType::Mysql | DbType::Mariadb | DbType::Mssql => {
required(&db.password, &db.name, "password")?
}
_ => optional(&db.password),
@@ -182,7 +186,9 @@ impl ConfigService {
| DbType::Mariadb
| DbType::MongoDB
| DbType::Redis
| DbType::Valkey => required(&db.host, &db.name, "host")?,
| DbType::Firebird
| DbType::Valkey
| DbType::Mssql => required(&db.host, &db.name, "host")?,
DbType::Sqlite => optional(&db.host),
};
@@ -192,7 +198,9 @@ impl ConfigService {
| DbType::Mariadb
| DbType::MongoDB
| DbType::Redis
| DbType::Valkey => required(&db.port, &db.name, "port")?,
| DbType::Firebird
| DbType::Valkey
| DbType::Mssql => required(&db.port, &db.name, "port")?,
DbType::Sqlite => db.port.unwrap_or(0),
};
+2
View File
@@ -9,6 +9,7 @@ use reqwest::Client;
use std::error::Error;
use std::sync::Arc;
use futures_util::future::try_join_all;
use tracing::info;
use crate::domain::factory::DatabaseFactory;
pub struct StatusService {
@@ -32,6 +33,7 @@ impl StatusService {
let db_engine = DatabaseFactory::create_for_backup(db.clone()).await;
let reachable = db_engine.ping().await?;
info!("Ping {} => {:?}",db.name, reachable);
Ok::<DatabasePayload, anyhow::Error>(DatabasePayload {
name: &db.name,
+14
View File
@@ -14,6 +14,7 @@ pub struct Settings {
pub pooling: usize,
pub timezone: String,
pub log: String,
pub chunk_size: usize, // bytes
}
impl Settings {
@@ -35,6 +36,18 @@ impl Settings {
pooling_seconds
);
}
let chunk_size_mb = env::var("CHUNK_SIZE_MB")
.unwrap_or_else(|_| "1".to_string())
.parse::<usize>()
.expect("CHUNK_SIZE_MB must be a valid positive integer");
if chunk_size_mb == 0 || chunk_size_mb > 10 {
panic!("CHUNK_SIZE_MB must be between 1 and 10 MB");
}
let chunk_size = chunk_size_mb * 1024 * 1024;
let tz = env::var("TZ").unwrap_or_else(|_| "UTC".to_string());
Self {
@@ -49,6 +62,7 @@ impl Settings {
pooling: pooling_seconds,
timezone: tz,
log: env::var("LOG").unwrap_or_else(|_| "info".into()),
chunk_size
}
}
}
+114
View File
@@ -0,0 +1,114 @@
use crate::domain::factory::DatabaseFactory;
use crate::services::config::{DatabaseConfig, DbType};
use crate::tests::init_tracing_for_test;
use crate::utils::compress::{compress_to_tar_gz_large, decompress_large_tar_gz};
use std::path::PathBuf;
use std::time::Duration;
use tempfile::TempDir;
use testcontainers::runners::AsyncRunner;
use testcontainers::{ContainerAsync, GenericImage, ImageExt};
use testcontainers::core::{AccessMode, IntoContainerPort};
use tracing::{error, info};
use testcontainers::core::{Mount};
async fn create_config() -> (ContainerAsync<GenericImage>, DatabaseConfig) {
let mount = Mount::volume_mount("firebird-test-data", "/var/lib/firebird/data")
.with_access_mode(AccessMode::ReadWrite);
let container = GenericImage::new("firebirdsql/firebird", "latest")
.with_exposed_port(3050.tcp())
.with_mount(mount)
.with_env_var("FIREBIRD_ROOT_PASSWORD", "fake_root_password")
.with_env_var("FIREBIRD_USER", "alice")
.with_env_var("FIREBIRD_PASSWORD", "fake_password")
.with_env_var("FIREBIRD_DATABASE", "mirror.fdb")
.with_env_var("FIREBIRD_DATABASE_DEFAULT_CHARSET", "UTF8")
.start()
.await
.expect("Firebird started");
tokio::time::sleep(Duration::from_secs(15)).await;
let host = container.get_host().await.unwrap().to_string();
let port = container.get_host_port_ipv4(3050).await.unwrap();
let database = "/var/lib/firebird/data/mirror.fdb";
let config = DatabaseConfig {
name: "Test Firebird".to_string(),
database: database.to_string(),
db_type: DbType::Firebird,
username: "alice".to_string(),
password: "fake_password".to_string(),
port,
host,
generated_id: "3c445eb4-c2c6-4bde-a423-ee1385dcf6d2".to_string(),
path: "".to_string(),
};
(container, config)
}
#[tokio::test]
async fn firebird_ping_test() {
init_tracing_for_test();
let (_container, config) = create_config().await;
let db = DatabaseFactory::create_for_backup(config.clone()).await;
let reachable = db.ping().await.unwrap_or(false);
assert!(reachable);
}
#[tokio::test]
async fn firebird_backup_restore_test() {
init_tracing_for_test();
let (_container, config) = create_config().await;
let temp_dir = TempDir::new().unwrap();
let backup_path = temp_dir.path();
let db = DatabaseFactory::create_for_backup(config.clone()).await;
let file_path = db.backup(backup_path).await.unwrap();
tokio::time::sleep(Duration::from_secs(10)).await;
assert!(file_path.is_file());
let compression = compress_to_tar_gz_large(&file_path).await.unwrap();
assert!(compression.compressed_path.is_file());
let files = decompress_large_tar_gz(
compression.compressed_path.as_path(),
temp_dir.path(),
)
.await
.unwrap();
let backup_file: PathBuf = if files.len() == 1 {
files[0].clone()
} else {
panic!("Unexpected number of files after decompression");
};
let db = DatabaseFactory::create_for_restore(config.clone(), &backup_file).await;
let reachable = db.ping().await.unwrap_or(false);
info!("Reachable: {}", reachable);
assert!(reachable);
match db.restore(&backup_file).await {
Ok(_) => {
info!("Restore succeeded for {}", config.generated_id);
assert!(true)
}
Err(e) => {
error!("Restore failed for {}: {:?}", config.generated_id, e);
assert!(false)
}
}
}
+2
View File
@@ -4,3 +4,5 @@ mod mysql;
mod postgres;
mod redis;
mod valkey;
mod firebird;
mod mssql;
+151
View File
@@ -0,0 +1,151 @@
use crate::domain::factory::DatabaseFactory;
use crate::services::config::{DatabaseConfig, DbType};
use crate::tests::init_tracing_for_test;
use crate::utils::compress::{compress_to_tar_gz_large, decompress_large_tar_gz};
use std::path::PathBuf;
use std::time::Duration;
use tempfile::TempDir;
use tiberius::{AuthMethod, Client, Config};
use testcontainers::runners::AsyncRunner;
use testcontainers::{ContainerAsync, GenericImage, ImageExt};
use testcontainers::core::IntoContainerPort;
use tokio::net::TcpStream;
use tokio_util::compat::TokioAsyncWriteCompatExt;
use tracing::{error, info};
const SA_PASSWORD: &str = "Test!Str0ng1";
async fn start_container() -> ContainerAsync<GenericImage> {
GenericImage::new("mcr.microsoft.com/azure-sql-edge", "latest")
.with_exposed_port(1433.tcp())
.with_env_var("ACCEPT_EULA", "Y")
.with_env_var("MSSQL_SA_PASSWORD", SA_PASSWORD)
.start()
.await
.expect("azure-sql-edge container started")
}
async fn create_user_database(host: &str, port: u16, db_name: &str) {
let mut config = Config::new();
config.host(host);
config.port(port);
config.authentication(AuthMethod::sql_server("sa", SA_PASSWORD));
config.trust_cert();
let tcp = TcpStream::connect(config.get_addr()).await.unwrap();
tcp.set_nodelay(true).unwrap();
let mut client = Client::connect(config, tcp.compat_write()).await.unwrap();
let sql = format!(
"IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = N'{}') CREATE DATABASE [{}]",
db_name, db_name
);
client.simple_query(sql.as_str()).await.unwrap();
}
fn make_config(host: String, port: u16, database: &str) -> DatabaseConfig {
DatabaseConfig {
name: "Test MSSQL".to_string(),
database: database.to_string(),
db_type: DbType::Mssql,
username: "sa".to_string(),
password: SA_PASSWORD.to_string(),
port,
host,
generated_id: "5a445eb4-c2c6-4bde-a423-ee1385dcf6d3".to_string(),
path: "".to_string(),
}
}
#[tokio::test]
async fn mssql_ping_test() {
init_tracing_for_test();
let container = start_container().await;
tokio::time::sleep(Duration::from_secs(30)).await;
let host = container.get_host().await.unwrap().to_string();
let port = container.get_host_port_ipv4(1433).await.unwrap();
let config = make_config(host, port, "master");
let db = DatabaseFactory::create_for_backup(config).await;
let reachable = db.ping().await.unwrap_or(false);
assert!(reachable, "MSSQL ping should succeed");
}
#[tokio::test]
async fn mssql_backup_test() {
init_tracing_for_test();
let container = start_container().await;
tokio::time::sleep(Duration::from_secs(30)).await;
let host = container.get_host().await.unwrap().to_string();
let port = container.get_host_port_ipv4(1433).await.unwrap();
create_user_database(&host, port, "backupdb").await;
let config = make_config(host, port, "backupdb");
let temp_dir = TempDir::new().unwrap();
let db = DatabaseFactory::create_for_backup(config).await;
let file_path = db.backup(temp_dir.path()).await.unwrap();
assert!(file_path.is_file(), "backup file should exist");
assert!(
file_path.metadata().unwrap().len() > 0,
"backup file should be non-empty"
);
assert!(
file_path.extension().and_then(|e| e.to_str()) == Some("bacpac"),
"backup file should have .bacpac extension"
);
}
#[tokio::test]
async fn mssql_backup_restore_test() {
init_tracing_for_test();
let container = start_container().await;
tokio::time::sleep(Duration::from_secs(30)).await;
let host = container.get_host().await.unwrap().to_string();
let port = container.get_host_port_ipv4(1433).await.unwrap();
create_user_database(&host, port, "sourcedb").await;
let backup_config = make_config(host.clone(), port, "sourcedb");
let temp_dir = TempDir::new().unwrap();
let db = DatabaseFactory::create_for_backup(backup_config).await;
let file_path = db.backup(temp_dir.path()).await.unwrap();
assert!(file_path.is_file());
let compression = compress_to_tar_gz_large(&file_path).await.unwrap();
assert!(compression.compressed_path.is_file());
let files = decompress_large_tar_gz(
compression.compressed_path.as_path(),
temp_dir.path(),
)
.await
.unwrap();
let backup_file: PathBuf = if files.len() == 1 {
files[0].clone()
} else {
panic!("Unexpected number of files after decompression: {}", files.len());
};
let restore_config = make_config(host, port, "restoreddb");
let db_restore = DatabaseFactory::create_for_restore(restore_config, &backup_file).await;
match db_restore.restore(&backup_file).await {
Ok(_) => info!("MSSQL restore succeeded"),
Err(e) => {
error!("MSSQL restore failed: {:?}", e);
panic!("Restore failed: {:?}", e);
}
}
}
+24 -7
View File
@@ -3,12 +3,15 @@ use anyhow::Result;
use bytes::Bytes;
use futures::{Stream, StreamExt};
use std::pin::Pin;
use tokio_util::io::ReaderStream;
use tokio::io::{AsyncReadExt};
use crate::settings::CONFIG;
pub struct UploadStream {
pub stream: Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
}
pub async fn build_stream(
file_path: &std::path::Path,
encrypt: bool,
@@ -25,13 +28,27 @@ pub async fn build_stream(
Ok(UploadStream { stream })
} else {
let file = tokio::fs::File::open(file_path).await?;
let reader = ReaderStream::new(file);
let mut file = tokio::fs::File::open(file_path).await?;
let stream = Box::pin(
reader.map(|r| r.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))),
);
let stream = async_stream::stream! {
let mut buffer = vec![0u8; CONFIG.chunk_size];
Ok(UploadStream { stream })
loop {
let n = match file.read(&mut buffer).await {
Ok(0) => break,
Ok(n) => n,
Err(e) => {
yield Err(e);
break;
}
};
yield Ok(Bytes::copy_from_slice(&buffer[..n]));
}
};
Ok(UploadStream {
stream: Box::pin(stream),
})
}
}
+4 -3
View File
@@ -3,8 +3,7 @@ use bytes::Bytes;
use futures::{Stream, StreamExt};
use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
use tracing::{error, info};
const PATCH_CHUNK_SIZE: usize = 1 * 1024 * 1024;
use crate::settings::CONFIG;
pub async fn upload_to_tus_stream_with_headers<S>(
encrypted_stream: S,
@@ -66,7 +65,9 @@ where
while let Some(chunk) = stream.next().await {
let chunk = chunk.context("Stream produced IO error")?;
for sub_chunk in chunk.chunks(PATCH_CHUNK_SIZE) {
info!("Chunk: {:?}", CONFIG.chunk_size);
for sub_chunk in chunk.chunks(CONFIG.chunk_size) {
let mut patch_headers = extra_headers.clone();
patch_headers.insert("Tus-Resumable", HeaderValue::from_static("1.0.0"));
patch_headers.insert(