diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1a304c0..c0b9f1f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,11 +1,11 @@ -# This file designates default owners for different parts of the codebase. -# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners - -* @YourGitHubUsername - -/apps/android/ @android-team -/apps/web-dashboard/ @web-team -/services/ @backend-team -/module/ @kernel-team -/ml/ @ml-team +# This file designates default owners for different parts of the codebase. +# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +* @YourGitHubUsername + +/apps/android/ @android-team +/apps/web-dashboard/ @web-team +/services/ @backend-team +/module/ @kernel-team +/ml/ @ml-team /infrastructure/ @devops-team \ No newline at end of file diff --git a/.github/workflows/android-app.yml b/.github/workflows/android-app.yml index f4ab4a2..ab6fff9 100644 --- a/.github/workflows/android-app.yml +++ b/.github/workflows/android-app.yml @@ -1,18 +1,132 @@ -name: Android App CI - -on: - push: - branches: [ main, develop ] - paths: - - 'apps/android/**' - pull_request: - branches: [ main ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Build Placeholder - run: echo "Building Android app..." \ No newline at end of file +name: Android App CI + +on: + push: + branches: [ main, develop ] + paths: + - 'apps/android/**' + - 'shared/**' + pull_request: + branches: [ main ] + paths: + - 'apps/android/**' + - 'shared/**' + +env: + GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx2048m -Dorg.gradle.daemon=false" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run tests + run: ./gradlew :apps:android:shared:testDebugUnitTest + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: android-test-results + path: | + apps/android/shared/build/reports/tests/ + apps/android/shared/build/test-results/ + + build: + needs: test + runs-on: ubuntu-latest + strategy: + matrix: + build-type: [debug, release] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build Android App (${{ matrix.build-type }}) + run: | + if [ "${{ matrix.build-type }}" = "release" ]; then + ./gradlew :apps:android:androidApp:assembleRelease + else + ./gradlew :apps:android:androidApp:assembleDebug + fi + + - name: Upload APK artifacts + uses: actions/upload-artifact@v4 + with: + name: android-apk-${{ matrix.build-type }} + path: apps/android/androidApp/build/outputs/apk/${{ matrix.build-type }}/*.apk + + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run Android Lint + run: ./gradlew :apps:android:androidApp:lintDebug + + - name: Upload lint results + uses: actions/upload-artifact@v4 + if: always() + with: + name: android-lint-results + path: apps/android/androidApp/build/reports/lint-results-debug.html \ No newline at end of file diff --git a/.github/workflows/microservices.yml b/.github/workflows/microservices.yml index 89f876a..53bb28b 100644 --- a/.github/workflows/microservices.yml +++ b/.github/workflows/microservices.yml @@ -1,18 +1,216 @@ -name: Microservices CI - -on: - push: - branches: [ main, develop ] - paths: - - 'services/**' - pull_request: - branches: [ main ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Build Placeholder - run: echo "Building microservices..." \ No newline at end of file +name: Microservices CI + +on: + push: + branches: [ main, develop ] + paths: + - 'services/**' + - 'shared/**' + pull_request: + branches: [ main ] + paths: + - 'services/**' + - 'shared/**' + +env: + GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx2048m -Dorg.gradle.daemon=false" + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + test-kotlin-services: + runs-on: ubuntu-latest + strategy: + matrix: + service: [backup-engine] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Test ${{ matrix.service }} + run: ./gradlew :services:${{ matrix.service }}:test + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-${{ matrix.service }} + path: | + services/${{ matrix.service }}/build/reports/tests/ + services/${{ matrix.service }}/build/test-results/ + + test-rust-services: + runs-on: ubuntu-latest + strategy: + matrix: + service: [storage-hal] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Cache cargo dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + services/${{ matrix.service }}/target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('services/${{ matrix.service }}/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Run tests for ${{ matrix.service }} + run: | + cd services/${{ matrix.service }} + cargo test + + - name: Run clippy for ${{ matrix.service }} + run: | + cd services/${{ matrix.service }} + cargo clippy -- -D warnings + + - name: Check formatting for ${{ matrix.service }} + run: | + cd services/${{ matrix.service }} + cargo fmt --check + + test-python-services: + runs-on: ubuntu-latest + strategy: + matrix: + service: [ml-optimizer] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('services/${{ matrix.service }}/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies for ${{ matrix.service }} + run: | + cd services/${{ matrix.service }} + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-cov black flake8 + + - name: Run tests for ${{ matrix.service }} + run: | + cd services/${{ matrix.service }} + pytest --cov=. --cov-report=xml + + - name: Check code formatting for ${{ matrix.service }} + run: | + cd services/${{ matrix.service }} + black --check . + + - name: Run linting for ${{ matrix.service }} + run: | + cd services/${{ matrix.service }} + flake8 . + + test-nodejs-services: + runs-on: ubuntu-latest + strategy: + matrix: + service: [sync-coordinator] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js 18 + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: 'services/${{ matrix.service }}/package-lock.json' + + - name: Install dependencies for ${{ matrix.service }} + run: | + cd services/${{ matrix.service }} + npm ci + + - name: Run tests for ${{ matrix.service }} + run: | + cd services/${{ matrix.service }} + npm test + + - name: Run linting for ${{ matrix.service }} + run: | + cd services/${{ matrix.service }} + npm run lint + + - name: Type check for ${{ matrix.service }} + run: | + cd services/${{ matrix.service }} + npm run type-check + + build-docker-images: + needs: [test-kotlin-services, test-rust-services, test-python-services, test-nodejs-services] + runs-on: ubuntu-latest + strategy: + matrix: + service: [backup-engine, storage-hal, ml-optimizer, sync-coordinator] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.service }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./services/${{ matrix.service }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/ml-training.yml b/.github/workflows/ml-training.yml index b84ff75..03206ad 100644 --- a/.github/workflows/ml-training.yml +++ b/.github/workflows/ml-training.yml @@ -1,18 +1,213 @@ -name: ML Training CI - -on: - push: - branches: [ main, develop ] - paths: - - 'ml/**' - pull_request: - branches: [ main ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Build Placeholder - run: echo "Running ML training..." \ No newline at end of file +name: ML Training CI + +on: + push: + branches: [ main, develop ] + paths: + - 'ml/**' + - 'services/ml-optimizer/**' + pull_request: + branches: [ main ] + paths: + - 'ml/**' + - 'services/ml-optimizer/**' + schedule: + # Run weekly training on Sundays at 2 AM UTC + - cron: '0 2 * * 0' + workflow_dispatch: + inputs: + model_type: + description: 'Type of model to train' + required: true + default: 'all' + type: choice + options: + - all + - anomaly_detection + - backup_prediction + - optimization + +jobs: + validate-data: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-ml-${{ hashFiles('ml/**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip-ml- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pandas numpy scikit-learn pytest + + - name: Validate training datasets + run: | + python -c " + import os + import pandas as pd + import numpy as np + + datasets_dir = 'ml/datasets' + if os.path.exists(datasets_dir): + for file in os.listdir(datasets_dir): + if file.endswith('.csv'): + df = pd.read_csv(os.path.join(datasets_dir, file)) + print(f'Dataset {file}: {df.shape[0]} rows, {df.shape[1]} columns') + print(f'Missing values: {df.isnull().sum().sum()}') + else: + print('No datasets directory found, creating placeholder') + os.makedirs(datasets_dir, exist_ok=True) + " + + train-anomaly-detection: + needs: validate-data + runs-on: ubuntu-latest + if: ${{ github.event.inputs.model_type == 'anomaly_detection' || github.event.inputs.model_type == 'all' || github.event.inputs.model_type == '' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-anomaly-${{ hashFiles('ml/models/anomaly_detection/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip-anomaly- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + cd ml/models/anomaly_detection + pip install scikit-learn pandas numpy joblib matplotlib seaborn pytest + + - name: Train anomaly detection model + run: | + cd ml/models/anomaly_detection + python anomaly_detector.py + + - name: Test model + run: | + cd ml/models/anomaly_detection + python -m pytest test_*.py -v || echo "No tests found" + + - name: Upload model artifacts + uses: actions/upload-artifact@v4 + with: + name: anomaly-detection-model + path: | + ml/models/anomaly_detection/*.pkl + ml/models/anomaly_detection/*.joblib + ml/models/anomaly_detection/metrics.json + + train-backup-prediction: + needs: validate-data + runs-on: ubuntu-latest + if: ${{ github.event.inputs.model_type == 'backup_prediction' || github.event.inputs.model_type == 'all' || github.event.inputs.model_type == '' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-backup-${{ hashFiles('ml/models/backup_prediction/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip-backup- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + cd ml/models/backup_prediction + pip install scikit-learn pandas numpy joblib matplotlib seaborn pytest + + - name: Train backup prediction model + run: | + cd ml/models/backup_prediction + python backup_predictor.py + + - name: Test model + run: | + cd ml/models/backup_prediction + python -m pytest test_*.py -v || echo "No tests found" + + - name: Upload model artifacts + uses: actions/upload-artifact@v4 + with: + name: backup-prediction-model + path: | + ml/models/backup_prediction/*.pkl + ml/models/backup_prediction/*.joblib + ml/models/backup_prediction/metrics.json + + model-validation: + needs: [train-anomaly-detection, train-backup-prediction] + runs-on: ubuntu-latest + if: always() + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download all model artifacts + uses: actions/download-artifact@v4 + with: + path: trained-models + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install validation dependencies + run: | + python -m pip install --upgrade pip + pip install scikit-learn pandas numpy joblib + + - name: Validate trained models + run: | + python -c " + import os + import joblib + import pickle + + models_dir = 'trained-models' + if os.path.exists(models_dir): + for root, dirs, files in os.walk(models_dir): + for file in files: + if file.endswith(('.pkl', '.joblib')): + try: + model_path = os.path.join(root, file) + if file.endswith('.pkl'): + model = pickle.load(open(model_path, 'rb')) + else: + model = joblib.load(model_path) + print(f'Successfully loaded model: {model_path}') + print(f'Model type: {type(model)}') + except Exception as e: + print(f'Failed to load {model_path}: {e}') + else: + print('No trained models found') + " \ No newline at end of file diff --git a/.github/workflows/module-build.yml b/.github/workflows/module-build.yml index b18a9d6..6fdb3cd 100644 --- a/.github/workflows/module-build.yml +++ b/.github/workflows/module-build.yml @@ -1,18 +1,156 @@ -name: Module Build CI - -on: - push: - branches: [ main, develop ] - paths: - - 'module/**' - pull_request: - branches: [ main ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Build Placeholder - run: echo "Building module..." \ No newline at end of file +name: Module Build CI + +on: + push: + branches: [ main, develop ] + paths: + - 'module/**' + pull_request: + branches: [ main ] + paths: + - 'module/**' + +jobs: + build-native-module: + runs-on: ubuntu-latest + strategy: + matrix: + arch: [x86_64, aarch64] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + cmake \ + linux-headers-generic \ + gcc-aarch64-linux-gnu \ + g++-aarch64-linux-gnu + + - name: Cache CMake build + uses: actions/cache@v4 + with: + path: | + module/native/build + key: ${{ runner.os }}-cmake-${{ matrix.arch }}-${{ hashFiles('module/native/CMakeLists.txt') }} + restore-keys: | + ${{ runner.os }}-cmake-${{ matrix.arch }}- + + - name: Configure CMake build + run: | + cd module/native + mkdir -p build + cd build + if [ "${{ matrix.arch }}" = "aarch64" ]; then + cmake .. -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc -DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ + else + cmake .. + fi + + - name: Build native components + run: | + cd module/native/build + make -j$(nproc) + + - name: Run component tests + if: matrix.arch == 'x86_64' + run: | + cd module/native/build + # Run tests if available + if [ -f "test_runner" ]; then + ./test_runner + else + echo "No test runner found, skipping tests" + fi + + - name: Package build artifacts + run: | + cd module/native/build + tar -czf ../../../module-${{ matrix.arch }}.tar.gz . + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: module-${{ matrix.arch }} + path: module-${{ matrix.arch }}.tar.gz + + validate-module-properties: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate module.prop + run: | + if [ -f "module/module.prop" ]; then + echo "Validating module.prop..." + # Check required fields + grep -q "^id=" module/module.prop || (echo "Missing id field" && exit 1) + grep -q "^name=" module/module.prop || (echo "Missing name field" && exit 1) + grep -q "^version=" module/module.prop || (echo "Missing version field" && exit 1) + grep -q "^versionCode=" module/module.prop || (echo "Missing versionCode field" && exit 1) + grep -q "^author=" module/module.prop || (echo "Missing author field" && exit 1) + grep -q "^description=" module/module.prop || (echo "Missing description field" && exit 1) + echo "module.prop validation passed" + else + echo "module.prop not found" + exit 1 + fi + + check-kernel-compatibility: + runs-on: ubuntu-latest + strategy: + matrix: + kernel_version: ['5.15', '6.1', '6.6'] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install kernel headers for ${{ matrix.kernel_version }} + run: | + sudo apt-get update + # This is a simulation - in real scenarios you'd need actual kernel headers + echo "Checking compatibility with kernel ${{ matrix.kernel_version }}" + + - name: Check source compatibility + run: | + echo "Checking C++ source compatibility with kernel ${{ matrix.kernel_version }}" + # Check for deprecated kernel APIs + if grep -r "deprecated_function" module/native/ 2>/dev/null; then + echo "Warning: Found deprecated kernel functions" + fi + + # Check for kernel version-specific code + if grep -r "LINUX_VERSION_CODE" module/native/ 2>/dev/null; then + echo "Found kernel version checks in code" + fi + + echo "Compatibility check completed for kernel ${{ matrix.kernel_version }}" + + security-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run Semgrep security scan + uses: returntocorp/semgrep-action@v1 + with: + config: > + p/security-audit + p/cpp + scanDirPath: module/ + continue-on-error: true + + - name: Check for hardcoded secrets + run: | + echo "Scanning for hardcoded secrets in module..." + # Check for common secret patterns + if grep -r -i "password\|secret\|key\|token" module/ --include="*.cpp" --include="*.h" --include="*.c"; then + echo "Warning: Found potential hardcoded secrets" + else + echo "No hardcoded secrets detected" + fi \ No newline at end of file diff --git a/.github/workflows/performance-test.yml b/.github/workflows/performance-test.yml index cf035b7..a43543e 100644 --- a/.github/workflows/performance-test.yml +++ b/.github/workflows/performance-test.yml @@ -1,16 +1,331 @@ -name: Performance Test - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Build Placeholder - run: echo "Running performance test..." \ No newline at end of file +name: Performance Test + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + schedule: + # Run performance tests weekly on Saturdays at 3 AM UTC + - cron: '0 3 * * 6' + workflow_dispatch: + inputs: + test_type: + description: 'Type of performance test to run' + required: true + default: 'all' + type: choice + options: + - all + - backup + - restore + - deduplication + - compression + - ml_inference + +env: + PERFORMANCE_DATA_SIZE: 1GB + TEST_DURATION: 300 # 5 minutes + +jobs: + setup-test-environment: + runs-on: ubuntu-latest + outputs: + test-data-key: ${{ steps.generate-key.outputs.key }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Generate test data cache key + id: generate-key + run: echo "key=test-data-${{ github.run_id }}" >> $GITHUB_OUTPUT + + - name: Generate test data + run: | + mkdir -p test-data + # Generate various file types for testing + echo "Generating test data..." + + # Create text files + for i in {1..100}; do + head -c 10M test-data/text_file_$i.txt + done + + # Create binary files + for i in {1..50}; do + head -c 20M test-data/binary_file_$i.bin + done + + # Create duplicate files for deduplication testing + cp test-data/text_file_1.txt test-data/duplicate_1.txt + cp test-data/text_file_2.txt test-data/duplicate_2.txt + + echo "Test data generated: $(du -sh test-data)" + + - name: Cache test data + uses: actions/cache@v4 + with: + path: test-data + key: ${{ steps.generate-key.outputs.key }} + + backup-performance: + needs: setup-test-environment + runs-on: ubuntu-latest + if: ${{ github.event.inputs.test_type == 'backup' || github.event.inputs.test_type == 'all' || github.event.inputs.test_type == '' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Restore test data + uses: actions/cache@v4 + with: + path: test-data + key: ${{ needs.setup-test-environment.outputs.test-data-key }} + + - name: Set up monitoring + run: | + # Install system monitoring tools + sudo apt-get update + sudo apt-get install -y htop iotop sysstat + + # Start system monitoring in background + iostat -x 1 > iostat.log & + IOSTAT_PID=$! + echo $IOSTAT_PID > iostat.pid + + - name: Build backup service + run: | + cd services/backup-engine + if [ -f "build.gradle.kts" ]; then + ../../gradlew build + else + echo "No build file found, creating mock backup service" + mkdir -p build + echo '#!/bin/bash' > build/backup_perf_test + echo 'echo "Mock backup performance test"' >> build/backup_perf_test + echo 'time tar -czf /tmp/backup.tar.gz "$@"' >> build/backup_perf_test + chmod +x build/backup_perf_test + fi + + - name: Run backup performance test + run: | + cd services/backup-engine + echo "Starting backup performance test..." + start_time=$(date +%s.%N) + + # Run backup with timing + if [ -f "build/backup_perf_test" ]; then + time ./build/backup_perf_test ../../test-data + else + time tar -czf /tmp/backup.tar.gz test-data + fi + + end_time=$(date +%s.%N) + duration=$(echo "$end_time - $start_time" | bc -l) + + echo "Backup completed in $duration seconds" + echo "BACKUP_DURATION=$duration" >> $GITHUB_ENV + + # Calculate throughput + data_size=$(du -sb test-data | cut -f1) + throughput=$(echo "scale=2; $data_size / $duration / 1024 / 1024" | bc -l) + echo "Backup throughput: $throughput MB/s" + echo "BACKUP_THROUGHPUT=$throughput" >> $GITHUB_ENV + + - name: Stop monitoring and collect metrics + run: | + # Stop iostat + if [ -f iostat.pid ]; then + kill $(cat iostat.pid) || true + fi + + # Collect system metrics + echo "=== System Metrics ===" > performance_metrics.txt + echo "Backup Duration: $BACKUP_DURATION seconds" >> performance_metrics.txt + echo "Backup Throughput: $BACKUP_THROUGHPUT MB/s" >> performance_metrics.txt + echo "" >> performance_metrics.txt + echo "=== CPU and Memory Usage ===" >> performance_metrics.txt + cat iostat.log | tail -20 >> performance_metrics.txt + + - name: Upload performance metrics + uses: actions/upload-artifact@v4 + with: + name: backup-performance-metrics + path: | + performance_metrics.txt + iostat.log + + restore-performance: + needs: [setup-test-environment, backup-performance] + runs-on: ubuntu-latest + if: ${{ github.event.inputs.test_type == 'restore' || github.event.inputs.test_type == 'all' || github.event.inputs.test_type == '' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download backup from previous job + run: | + # In real scenario, we'd download the backup created in backup-performance job + # For now, create a mock backup + if [ ! -f "/tmp/backup.tar.gz" ]; then + echo "Creating mock backup for restore test" + mkdir -p mock-data + head -c 100M mock-data/large_file.bin + tar -czf /tmp/backup.tar.gz mock-data + fi + + - name: Run restore performance test + run: | + echo "Starting restore performance test..." + start_time=$(date +%s.%N) + + # Run restore with timing + mkdir -p restored-data + time tar -xzf /tmp/backup.tar.gz -C restored-data + + end_time=$(date +%s.%N) + duration=$(echo "$end_time - $start_time" | bc -l) + + echo "Restore completed in $duration seconds" + + # Calculate throughput + data_size=$(du -sb restored-data | cut -f1) + throughput=$(echo "scale=2; $data_size / $duration / 1024 / 1024" | bc -l) + echo "Restore throughput: $throughput MB/s" + + echo "=== Restore Performance ===" > restore_metrics.txt + echo "Duration: $duration seconds" >> restore_metrics.txt + echo "Throughput: $throughput MB/s" >> restore_metrics.txt + + - name: Upload restore metrics + uses: actions/upload-artifact@v4 + with: + name: restore-performance-metrics + path: restore_metrics.txt + + ml-inference-performance: + runs-on: ubuntu-latest + if: ${{ github.event.inputs.test_type == 'ml_inference' || github.event.inputs.test_type == 'all' || github.event.inputs.test_type == '' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install ML dependencies + run: | + python -m pip install --upgrade pip + pip install scikit-learn pandas numpy time + + - name: Run ML inference performance test + run: | + cd services/ml-optimizer + python -c " + import time + import numpy as np + from sklearn.ensemble import RandomForestClassifier + from sklearn.datasets import make_classification + + print('Generating test data...') + X, y = make_classification(n_samples=10000, n_features=20, n_classes=2, random_state=42) + + print('Training model...') + model = RandomForestClassifier(n_estimators=100, random_state=42) + start_time = time.time() + model.fit(X, y) + training_time = time.time() - start_time + + print('Running inference performance test...') + test_X, _ = make_classification(n_samples=1000, n_features=20, n_classes=2, random_state=123) + + # Measure inference time + start_time = time.time() + predictions = model.predict(test_X) + inference_time = time.time() - start_time + + throughput = len(test_X) / inference_time + + print(f'Training time: {training_time:.2f} seconds') + print(f'Inference time: {inference_time:.4f} seconds') + print(f'Inference throughput: {throughput:.2f} predictions/second') + + # Save metrics + with open('ml_performance_metrics.txt', 'w') as f: + f.write(f'Training time: {training_time:.2f} seconds\n') + f.write(f'Inference time: {inference_time:.4f} seconds\n') + f.write(f'Inference throughput: {throughput:.2f} predictions/second\n') + " + + - name: Upload ML performance metrics + uses: actions/upload-artifact@v4 + with: + name: ml-inference-performance-metrics + path: services/ml-optimizer/ml_performance_metrics.txt + + performance-report: + needs: [backup-performance, restore-performance, ml-inference-performance] + runs-on: ubuntu-latest + if: always() + steps: + - name: Download all performance metrics + uses: actions/download-artifact@v4 + with: + path: metrics + + - name: Generate performance report + run: | + echo "# Performance Test Report" > performance_report.md + echo "" >> performance_report.md + echo "Generated on: $(date)" >> performance_report.md + echo "" >> performance_report.md + + if [ -d "metrics/backup-performance-metrics" ]; then + echo "## Backup Performance" >> performance_report.md + echo '```' >> performance_report.md + cat metrics/backup-performance-metrics/performance_metrics.txt >> performance_report.md + echo '```' >> performance_report.md + echo "" >> performance_report.md + fi + + if [ -d "metrics/restore-performance-metrics" ]; then + echo "## Restore Performance" >> performance_report.md + echo '```' >> performance_report.md + cat metrics/restore-performance-metrics/restore_metrics.txt >> performance_report.md + echo '```' >> performance_report.md + echo "" >> performance_report.md + fi + + if [ -d "metrics/ml-inference-performance-metrics" ]; then + echo "## ML Inference Performance" >> performance_report.md + echo '```' >> performance_report.md + cat metrics/ml-inference-performance-metrics/ml_performance_metrics.txt >> performance_report.md + echo '```' >> performance_report.md + fi + + - name: Upload consolidated report + uses: actions/upload-artifact@v4 + with: + name: performance-test-report + path: performance_report.md + + - name: Comment performance results on PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const reportPath = 'performance_report.md'; + + if (fs.existsSync(reportPath)) { + const report = fs.readFileSync(reportPath, 'utf8'); + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: report + }); + } \ No newline at end of file diff --git a/.github/workflows/release-orchestration.yml b/.github/workflows/release-orchestration.yml index eabf494..a439658 100644 --- a/.github/workflows/release-orchestration.yml +++ b/.github/workflows/release-orchestration.yml @@ -1,133 +1,133 @@ -name: CoreState v2.0 Release Orchestration - -on: - push: - tags: - - 'v2.*' - workflow_dispatch: - inputs: - release_type: - description: 'Release type' - required: true - default: 'stable' - type: choice - options: - - stable - - beta - - canary - -env: - DOCKER_REGISTRY: ghcr.io - KUBERNETES_CLUSTER: corestate-prod - ML_TRAINING_CLUSTER: ml-cluster-prod - -jobs: - security-scan: - runs-on: ubuntu-latest - strategy: - matrix: - component: [ 'apps/android', 'services', 'module', 'apps/web-dashboard', 'apps/daemon' ] - steps: - - uses: actions/checkout@v4 - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master - with: - scan-type: 'fs' - scan-ref: '${{ matrix.component }}' - severity: 'CRITICAL,HIGH' - exit-code: '1' - - build-android: - needs: security-scan - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up JDK - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - name: Build Android App - run: | - chmod +x gradlew - ./gradlew :apps:android:androidApp:assembleRelease :apps:android:androidApp:bundleRelease - - name: Upload Android Artifacts - uses: actions/upload-artifact@v4 - with: - name: android-app-${{ github.sha }} - path: apps/android/androidApp/build/outputs/ - - build-daemon: - needs: security-scan - runs-on: ubuntu-latest - steps: - - name: Install AArch64 Linker - run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu - - uses: actions/checkout@v4 - - name: Install Rust MUSL target - run: rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl - - name: Build Daemon - run: | - cd apps/daemon - cargo build --release --target x86_64-unknown-linux-musl - cargo build --release --target aarch64-unknown-linux-musl - - name: Upload Daemon Artifacts - uses: actions/upload-artifact@v4 - with: - name: daemon-${{ github.sha }} - path: apps/daemon/target/ - - build-web-dashboard: - needs: security-scan - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Build Web Dashboard - run: | - cd apps/web-dashboard - npm install - npm run build - - name: Upload Web Dashboard Artifacts - uses: actions/upload-artifact@v4 - with: - name: web-dashboard-${{ github.sha }} - path: apps/web-dashboard/build/ - - build-microservices: - needs: security-scan - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up JDK - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - name: Build Microservices - run: | - chmod +x gradlew - ./gradlew build - # Docker build would happen here, requires docker login etc. - echo "Docker build placeholder for ${{ env.DOCKER_REGISTRY }}/corestate/services:${{ github.ref_name }}" - - create-release: - # This job now only depends on the build jobs that produce release artifacts - needs: [build-android, build-daemon, build-web-dashboard, build-microservices] - runs-on: ubuntu-latest - steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - name: List downloaded artifacts - run: ls -R artifacts - - name: Create GitHub Release - uses: softprops/action-gh-release@v1 - with: - files: | - artifacts/android-app-${{ github.sha }}/**/*.apk - artifacts/android-app-${{ github.sha }}/**/*.aab - artifacts/daemon-${{ github.sha }}/**/*.tar.gz - body: | - # CoreState ${{ github.ref_name }} Release +name: CoreState v2.0 Release Orchestration + +on: + push: + tags: + - 'v2.*' + workflow_dispatch: + inputs: + release_type: + description: 'Release type' + required: true + default: 'stable' + type: choice + options: + - stable + - beta + - canary + +env: + DOCKER_REGISTRY: ghcr.io + KUBERNETES_CLUSTER: corestate-prod + ML_TRAINING_CLUSTER: ml-cluster-prod + +jobs: + security-scan: + runs-on: ubuntu-latest + strategy: + matrix: + component: [ 'apps/android', 'services', 'module', 'apps/web-dashboard', 'apps/daemon' ] + steps: + - uses: actions/checkout@v4 + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '${{ matrix.component }}' + severity: 'CRITICAL,HIGH' + exit-code: '1' + + build-android: + needs: security-scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + - name: Build Android App + run: | + chmod +x gradlew + ./gradlew :apps:android:androidApp:assembleRelease :apps:android:androidApp:bundleRelease + - name: Upload Android Artifacts + uses: actions/upload-artifact@v4 + with: + name: android-app-${{ github.sha }} + path: apps/android/androidApp/build/outputs/ + + build-daemon: + needs: security-scan + runs-on: ubuntu-latest + steps: + - name: Install AArch64 Linker + run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu + - uses: actions/checkout@v4 + - name: Install Rust MUSL target + run: rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl + - name: Build Daemon + run: | + cd apps/daemon + cargo build --release --target x86_64-unknown-linux-musl + cargo build --release --target aarch64-unknown-linux-musl + - name: Upload Daemon Artifacts + uses: actions/upload-artifact@v4 + with: + name: daemon-${{ github.sha }} + path: apps/daemon/target/ + + build-web-dashboard: + needs: security-scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build Web Dashboard + run: | + cd apps/web-dashboard + npm install + npm run build + - name: Upload Web Dashboard Artifacts + uses: actions/upload-artifact@v4 + with: + name: web-dashboard-${{ github.sha }} + path: apps/web-dashboard/build/ + + build-microservices: + needs: security-scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + - name: Build Microservices + run: | + chmod +x gradlew + ./gradlew build + # Docker build would happen here, requires docker login etc. + echo "Docker build placeholder for ${{ env.DOCKER_REGISTRY }}/corestate/services:${{ github.ref_name }}" + + create-release: + # This job now only depends on the build jobs that produce release artifacts + needs: [build-android, build-daemon, build-web-dashboard, build-microservices] + runs-on: ubuntu-latest + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + - name: List downloaded artifacts + run: ls -R artifacts + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + files: | + artifacts/android-app-${{ github.sha }}/**/*.apk + artifacts/android-app-${{ github.sha }}/**/*.aab + artifacts/daemon-${{ github.sha }}/**/*.tar.gz + body: | + # CoreState ${{ github.ref_name }} Release This is an automated release. See the attached artifacts for downloads. \ No newline at end of file diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 9ee9354..6e1103b 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -1,16 +1,193 @@ -name: Security Scan - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Build Placeholder - run: echo "Running security scan..." \ No newline at end of file +name: Security Scan + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + schedule: + # Run daily at 2 AM UTC + - cron: '0 2 * * *' + workflow_dispatch: + +jobs: + dependency-scan: + runs-on: ubuntu-latest + strategy: + matrix: + component: + - path: 'apps/web-dashboard' + type: 'npm' + - path: 'services/sync-coordinator' + type: 'npm' + - path: 'apps/daemon' + type: 'cargo' + - path: 'services/storage-hal' + type: 'cargo' + - path: 'services/ml-optimizer' + type: 'pip' + - path: '.' + type: 'gradle' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '${{ matrix.component.path }}' + format: 'sarif' + output: 'trivy-results-${{ matrix.component.type }}.sarif' + severity: 'CRITICAL,HIGH,MEDIUM' + + - name: Upload Trivy scan results + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-results-${{ matrix.component.type }}.sarif' + category: 'trivy-${{ matrix.component.type }}' + + secret-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run GitLeaks secret scanner + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} + + code-security-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: java, javascript, python, cpp + queries: security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:multi" + + semgrep-scan: + runs-on: ubuntu-latest + name: Semgrep Security Scan + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run Semgrep + uses: returntocorp/semgrep-action@v1 + with: + config: > + p/security-audit + p/owasp-top-10 + p/kotlin + p/java + p/typescript + p/python + p/rust + p/cpp + generateSarif: "1" + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: semgrep.sarif + + license-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: FOSSA Scan + uses: fossas/fossa-action@main + with: + api-key: ${{ secrets.FOSSA_API_KEY }} + run-tests: true + continue-on-error: true + + container-scan: + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + strategy: + matrix: + service: [backup-engine, storage-hal, ml-optimizer, sync-coordinator] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build Docker image for scanning + run: | + cd services/${{ matrix.service }} + if [ -f "Dockerfile" ]; then + docker build -t scan-image:${{ matrix.service }} . + else + echo "No Dockerfile found for ${{ matrix.service }}, skipping" + exit 0 + fi + + - name: Run Trivy container scan + uses: aquasecurity/trivy-action@master + with: + image-ref: 'scan-image:${{ matrix.service }}' + format: 'sarif' + output: 'container-scan-${{ matrix.service }}.sarif' + severity: 'CRITICAL,HIGH' + + - name: Upload container scan results + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'container-scan-${{ matrix.service }}.sarif' + category: 'container-${{ matrix.service }}' + + infrastructure-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run Checkov IaC scan + uses: bridgecrewio/checkov-action@master + with: + directory: infrastructure/ + framework: terraform,kubernetes,dockerfile + output_format: sarif + output_file_path: checkov-results.sarif + + - name: Upload Checkov scan results + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: checkov-results.sarif + category: 'infrastructure' + + security-report: + needs: [dependency-scan, secret-scan, code-security-scan, semgrep-scan, license-scan, container-scan, infrastructure-scan] + runs-on: ubuntu-latest + if: always() + steps: + - name: Security Scan Summary + run: | + echo "## Security Scan Results" >> $GITHUB_STEP_SUMMARY + echo "- Dependency Scan: ${{ needs.dependency-scan.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Secret Scan: ${{ needs.secret-scan.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Code Security Scan: ${{ needs.code-security-scan.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Semgrep Scan: ${{ needs.semgrep-scan.result }}" >> $GITHUB_STEP_SUMMARY + echo "- License Scan: ${{ needs.license-scan.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Container Scan: ${{ needs.container-scan.result }}" >> $GITHUB_STEP_SUMMARY + echo "- Infrastructure Scan: ${{ needs.infrastructure-scan.result }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties index bfa2a23..d7fcba4 100644 --- a/.gradle/buildOutputCleanup/cache.properties +++ b/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Wed Jul 23 01:24:21 CEST 2025 -gradle.version=8.14.2 +#Wed Jul 23 01:24:21 CEST 2025 +gradle.version=8.14.2 diff --git a/README.md b/README.md index 2b06f0b..f028601 100644 --- a/README.md +++ b/README.md @@ -1,169 +1,169 @@ -# CoreState v2.0 - Next-Generation Advanced Backup System - -## 1. Executive Summary - -CoreState v2.0 is a high-performance, distributed backup system designed for reliability, scalability, and advanced feature support. It leverages a microservices architecture to provide a robust platform for backing up and restoring data across various environments. CoreState v2.0 introduces a sophisticated backup engine, advanced ML-based optimizations, and a modular design to support future enhancements and integrations. - -The system is built with a polyglot technology stack, including Rust for the high-performance daemon, Kotlin/Java for backend services, Python for machine learning, and a web-based dashboard for user interaction. It is designed to be cloud-native, with support for Kubernetes deployment and various storage backends. - -## 2. Architecture Overview - -CoreState v2.0 is composed of several key components that work together to provide a comprehensive backup solution. - -![Architecture Diagram](docs/architecture/overview.md) - -### Core Components: - -* **Web Dashboard:** A React-based web interface for users to manage backups, monitor system status, and configure settings. -* **Daemon:** A lightweight, high-performance agent written in Rust that runs on client machines to perform backup and restore operations. -* **Backup Engine:** The core service, written in Kotlin, responsible for orchestrating the backup and restore workflows, including scheduling, data processing, and storage management. -* **ML Optimizer:** A Python-based service that uses machine learning models to optimize backup schedules, detect anomalies, and predict storage needs. -* **Sync Coordinator:** Manages data synchronization and consistency across distributed components. -* **Storage HAL (Hardware Abstraction Layer):** Provides a unified interface for interacting with different storage backends (e.g., S3, Azure Blob, GCP Cloud Storage, local filesystems). - -### Supporting Services: - -* **Analytics Engine:** Collects and processes system metrics for monitoring and reporting. -* **Compression Engine:** Provides data compression services to reduce storage footprint. -* **Deduplication Service:** Identifies and eliminates redundant data blocks to optimize storage. -* **Encryption Service:** Manages data encryption and key management to ensure data security. -* **Index Service:** Maintains an index of backed-up data for fast searching and retrieval. - -## 3. Project Structure - -The project is organized into the following directories: - -``` -CoreState-v2/ -├── apps/ # Client applications (Web Dashboard, Daemon) -│ ├── android/ -│ ├── daemon/ -│ └── web-dashboard/ -├── docs/ # Project documentation -│ ├── api/ -│ └── architecture/ -├── infrastructure/ # Infrastructure as Code (Kubernetes, Terraform) -│ ├── docker/ -│ ├── kubernetes/ -│ └── terraform/ -├── ml/ # Machine Learning models and datasets -│ ├── datasets/ -│ └── models/ -├── module/ # Kernel module for advanced features -│ ├── kernel_patches/ -│ └── native/ -├── services/ # Backend microservices -│ ├── analytics-engine/ -│ ├── backup-engine/ -│ ├── compression-engine/ -│ ├── deduplication-service/ -│ ├── encryption-service/ -│ ├── index-service/ -│ ├── ml-optimizer/ -│ ├── storage-hal/ -│ └── sync-coordinator/ -├── shared/ # Shared libraries, contracts, and protobuf definitions -│ ├── contracts/ -│ ├── libs/ -│ └── proto/ -├── tests/ # E2E, integration, performance, and unit tests -│ ├── e2e/ -│ ├── integration/ -│ ├── performance/ -│ └── unit/ -└── tools/ # Developer and operational tools - ├── benchmarking/ - ├── cli/ - └── migration/ -``` - -## 4. Feature Implementations - -### 4.1. High-Performance Daemon - -The CoreState Daemon is a native application written in Rust for maximum performance and minimal resource footprint on client systems. It is responsible for: - -* File system monitoring for changes. -* Executing backup and restore tasks as directed by the Backup Engine. -* Client-side encryption and compression. - -### 4.2. ML-Powered Optimization - -The ML Optimizer service provides intelligent features: - -* **Predictive Backups:** Analyzes data change patterns to predict optimal backup times. -* **Anomaly Detection:** Identifies unusual activity that might indicate a ransomware attack or data corruption. -* **Storage Optimization:** Recommends storage tiering strategies based on data access patterns. - -### 4.3. Advanced Kernel-Level Features - -For supported platforms, CoreState v2.0 can utilize a kernel module for advanced capabilities: - -* **CoW Snapshots:** Near-instantaneous, low-overhead snapshots using Copy-on-Write. -* **Block-Level Tracking:** Efficiently tracks changed data blocks for incremental backups. -* **Hardware Acceleration:** Integrates with hardware security modules (HSMs) for enhanced encryption performance. - -### 4.4. Cloud-Native and Distributed - -The system is designed for the cloud: - -* **Kubernetes-Native:** All services are containerized and can be deployed and managed with Kubernetes. -* **Scalable:** Services can be scaled independently to meet demand. -* **Resilient:** The distributed nature of the system ensures high availability. - -## 5. Getting Started - -### Prerequisites - -* Docker -* Kubernetes (e.g., Minikube, Kind, or a cloud provider's EKS/AKS/GKE) -* `kubectl` -* `gradle` (for Backup Engine) -* `rustc` and `cargo` (for Daemon) -* `python` and `pip` (for ML Optimizer) -* `npm` (for Web Dashboard) - -### Building and Running - -1. **Build Services:** Each service in the `/services` directory contains instructions for building its Docker image. For example, for the Backup Engine: - ```bash - cd services/backup-engine - ./gradlew build - docker build -t corestate-backup-engine . - ``` - -2. **Deploy to Kubernetes:** - ```bash - kubectl apply -f infrastructure/kubernetes/ - ``` - -3. **Build and Run Web Dashboard:** - ```bash - cd apps/web-dashboard - npm install - npm start - ``` - -4. **Build and Run Daemon:** - ```bash - cd apps/daemon - cargo build --release - ``` - -## 6. API and Communication - -Services communicate via gRPC. Protocol definitions are located in the `shared/proto` directory. - -* [`backup.proto`](shared/proto/backup.proto): Defines messages and services for backup and restore operations. -* [`sync.proto`](shared/proto/sync.proto): Defines messages and services for data synchronization. -* [`analytics.proto`](shared/proto/analytics.proto): Defines messages and services for analytics and monitoring. - -API documentation can be found in [`docs/api/grpc.md`](docs/api/grpc.md). - -## 7. Contributing - -Contributions are welcome! Please refer to the project's contribution guidelines and code of conduct. - -## 8. License - +# CoreState v2.0 - Next-Generation Advanced Backup System + +## 1. Executive Summary + +CoreState v2.0 is a high-performance, distributed backup system designed for reliability, scalability, and advanced feature support. It leverages a microservices architecture to provide a robust platform for backing up and restoring data across various environments. CoreState v2.0 introduces a sophisticated backup engine, advanced ML-based optimizations, and a modular design to support future enhancements and integrations. + +The system is built with a polyglot technology stack, including Rust for the high-performance daemon, Kotlin/Java for backend services, Python for machine learning, and a web-based dashboard for user interaction. It is designed to be cloud-native, with support for Kubernetes deployment and various storage backends. + +## 2. Architecture Overview + +CoreState v2.0 is composed of several key components that work together to provide a comprehensive backup solution. + +![Architecture Diagram](docs/architecture/overview.md) + +### Core Components: + +* **Web Dashboard:** A React-based web interface for users to manage backups, monitor system status, and configure settings. +* **Daemon:** A lightweight, high-performance agent written in Rust that runs on client machines to perform backup and restore operations. +* **Backup Engine:** The core service, written in Kotlin, responsible for orchestrating the backup and restore workflows, including scheduling, data processing, and storage management. +* **ML Optimizer:** A Python-based service that uses machine learning models to optimize backup schedules, detect anomalies, and predict storage needs. +* **Sync Coordinator:** Manages data synchronization and consistency across distributed components. +* **Storage HAL (Hardware Abstraction Layer):** Provides a unified interface for interacting with different storage backends (e.g., S3, Azure Blob, GCP Cloud Storage, local filesystems). + +### Supporting Services: + +* **Analytics Engine:** Collects and processes system metrics for monitoring and reporting. +* **Compression Engine:** Provides data compression services to reduce storage footprint. +* **Deduplication Service:** Identifies and eliminates redundant data blocks to optimize storage. +* **Encryption Service:** Manages data encryption and key management to ensure data security. +* **Index Service:** Maintains an index of backed-up data for fast searching and retrieval. + +## 3. Project Structure + +The project is organized into the following directories: + +``` +CoreState-v2/ +├── apps/ # Client applications (Web Dashboard, Daemon) +│ ├── android/ +│ ├── daemon/ +│ └── web-dashboard/ +├── docs/ # Project documentation +│ ├── api/ +│ └── architecture/ +├── infrastructure/ # Infrastructure as Code (Kubernetes, Terraform) +│ ├── docker/ +│ ├── kubernetes/ +│ └── terraform/ +├── ml/ # Machine Learning models and datasets +│ ├── datasets/ +│ └── models/ +├── module/ # Kernel module for advanced features +│ ├── kernel_patches/ +│ └── native/ +├── services/ # Backend microservices +│ ├── analytics-engine/ +│ ├── backup-engine/ +│ ├── compression-engine/ +│ ├── deduplication-service/ +│ ├── encryption-service/ +│ ├── index-service/ +│ ├── ml-optimizer/ +│ ├── storage-hal/ +│ └── sync-coordinator/ +├── shared/ # Shared libraries, contracts, and protobuf definitions +│ ├── contracts/ +│ ├── libs/ +│ └── proto/ +├── tests/ # E2E, integration, performance, and unit tests +│ ├── e2e/ +│ ├── integration/ +│ ├── performance/ +│ └── unit/ +└── tools/ # Developer and operational tools + ├── benchmarking/ + ├── cli/ + └── migration/ +``` + +## 4. Feature Implementations + +### 4.1. High-Performance Daemon + +The CoreState Daemon is a native application written in Rust for maximum performance and minimal resource footprint on client systems. It is responsible for: + +* File system monitoring for changes. +* Executing backup and restore tasks as directed by the Backup Engine. +* Client-side encryption and compression. + +### 4.2. ML-Powered Optimization + +The ML Optimizer service provides intelligent features: + +* **Predictive Backups:** Analyzes data change patterns to predict optimal backup times. +* **Anomaly Detection:** Identifies unusual activity that might indicate a ransomware attack or data corruption. +* **Storage Optimization:** Recommends storage tiering strategies based on data access patterns. + +### 4.3. Advanced Kernel-Level Features + +For supported platforms, CoreState v2.0 can utilize a kernel module for advanced capabilities: + +* **CoW Snapshots:** Near-instantaneous, low-overhead snapshots using Copy-on-Write. +* **Block-Level Tracking:** Efficiently tracks changed data blocks for incremental backups. +* **Hardware Acceleration:** Integrates with hardware security modules (HSMs) for enhanced encryption performance. + +### 4.4. Cloud-Native and Distributed + +The system is designed for the cloud: + +* **Kubernetes-Native:** All services are containerized and can be deployed and managed with Kubernetes. +* **Scalable:** Services can be scaled independently to meet demand. +* **Resilient:** The distributed nature of the system ensures high availability. + +## 5. Getting Started + +### Prerequisites + +* Docker +* Kubernetes (e.g., Minikube, Kind, or a cloud provider's EKS/AKS/GKE) +* `kubectl` +* `gradle` (for Backup Engine) +* `rustc` and `cargo` (for Daemon) +* `python` and `pip` (for ML Optimizer) +* `npm` (for Web Dashboard) + +### Building and Running + +1. **Build Services:** Each service in the `/services` directory contains instructions for building its Docker image. For example, for the Backup Engine: + ```bash + cd services/backup-engine + ./gradlew build + docker build -t corestate-backup-engine . + ``` + +2. **Deploy to Kubernetes:** + ```bash + kubectl apply -f infrastructure/kubernetes/ + ``` + +3. **Build and Run Web Dashboard:** + ```bash + cd apps/web-dashboard + npm install + npm start + ``` + +4. **Build and Run Daemon:** + ```bash + cd apps/daemon + cargo build --release + ``` + +## 6. API and Communication + +Services communicate via gRPC. Protocol definitions are located in the `shared/proto` directory. + +* [`backup.proto`](shared/proto/backup.proto): Defines messages and services for backup and restore operations. +* [`sync.proto`](shared/proto/sync.proto): Defines messages and services for data synchronization. +* [`analytics.proto`](shared/proto/analytics.proto): Defines messages and services for analytics and monitoring. + +API documentation can be found in [`docs/api/grpc.md`](docs/api/grpc.md). + +## 7. Contributing + +Contributions are welcome! Please refer to the project's contribution guidelines and code of conduct. + +## 8. License + This project is licensed under the [MIT License](LICENSE). \ No newline at end of file diff --git a/apps/android/androidApp/build.gradle.kts b/apps/android/androidApp/build.gradle.kts index 64e299a..e6ce4b7 100644 --- a/apps/android/androidApp/build.gradle.kts +++ b/apps/android/androidApp/build.gradle.kts @@ -1,25 +1,164 @@ -plugins { - id("com.android.application") - kotlin("android") -} - -android { - namespace = "com.corestate.androidApp" - compileSdk = 34 - defaultConfig { - applicationId = "com.corestate.androidApp" - minSdk = 26 - targetSdk = 34 - versionCode = 1 - versionName = "1.0" - } - buildTypes { - getByName("release") { - isMinifyEnabled = false - } - } -} - -dependencies { - implementation(project(":apps:android:shared")) +plugins { + id("com.android.application") + kotlin("android") + kotlin("kapt") + id("dagger.hilt.android.plugin") + id("kotlin-parcelize") +} + +android { + namespace = "com.corestate.androidApp" + compileSdk = 34 + + defaultConfig { + applicationId = "com.corestate.androidApp" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "2.0.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + getByName("release") { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + getByName("debug") { + isDebuggable = true + applicationIdSuffix = ".debug" + } + } + + buildFeatures { + compose = true + dataBinding = true + viewBinding = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.4" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs = listOf( + "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", + "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi" + ) + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation(project(":apps:android:shared")) + + // Android Core + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("androidx.activity:activity-compose:1.8.1") + implementation("androidx.fragment:fragment-ktx:1.6.2") + + // Compose BOM + implementation(platform("androidx.compose:compose-bom:2023.10.01")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material-icons-extended") + + // Navigation + implementation("androidx.navigation:navigation-compose:2.7.5") + implementation("androidx.hilt:hilt-navigation-compose:1.1.0") + + // Lifecycle + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") + implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0") + + // Dependency Injection + implementation("com.google.dagger:hilt-android:2.48") + kapt("com.google.dagger:hilt-compiler:2.48") + + // Networking + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + + // gRPC + implementation("io.grpc:grpc-okhttp:1.58.0") + implementation("io.grpc:grpc-protobuf-lite:1.58.0") + implementation("io.grpc:grpc-stub:1.58.0") + + // Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3") + + // Local Storage + implementation("androidx.room:room-runtime:2.6.0") + implementation("androidx.room:room-ktx:2.6.0") + kapt("androidx.room:room-compiler:2.6.0") + + // DataStore + implementation("androidx.datastore:datastore-preferences:1.0.0") + + // WorkManager + implementation("androidx.work:work-runtime-ktx:2.8.1") + implementation("androidx.hilt:hilt-work:1.1.0") + kapt("androidx.hilt:hilt-compiler:1.1.0") + + // Biometric Authentication + implementation("androidx.biometric:biometric:1.1.0") + + // File Management + implementation("com.github.bumptech.glide:glide:4.16.0") + implementation("androidx.documentfile:documentfile:1.0.1") + + // Charts and UI + implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") + implementation("com.airbnb.android:lottie-compose:6.1.0") + + // Security + implementation("androidx.security:security-crypto:1.1.0-alpha06") + + // WebRTC (for P2P sync) + implementation("org.webrtc:google-webrtc:1.0.32006") + + // Permissions + implementation("com.google.accompanist:accompanist-permissions:0.32.0") + + // System UI Controller + implementation("com.google.accompanist:accompanist-systemuicontroller:0.32.0") + + // Testing + testImplementation("junit:junit:4.13.2") + testImplementation("org.mockito:mockito-core:5.6.0") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + testImplementation("androidx.arch.core:core-testing:2.2.0") + + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01")) + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") } \ No newline at end of file diff --git a/apps/android/androidApp/src/main/AndroidManifest.xml b/apps/android/androidApp/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bc8e70c --- /dev/null +++ b/apps/android/androidApp/src/main/AndroidManifest.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/CoreStateApplication.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/CoreStateApplication.kt new file mode 100644 index 0000000..cc97a12 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/CoreStateApplication.kt @@ -0,0 +1,7 @@ +package com.corestate.androidApp + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class CoreStateApplication : Application() \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/MainActivity.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/MainActivity.kt new file mode 100644 index 0000000..56e4ae4 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/MainActivity.kt @@ -0,0 +1,34 @@ +package com.corestate.androidApp + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.ui.Modifier +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import com.corestate.androidApp.ui.CoreStateApp +import com.corestate.androidApp.ui.theme.CoreStateTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + // Install splash screen before super.onCreate() + installSplashScreen() + + super.onCreate(savedInstanceState) + + setContent { + CoreStateTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + CoreStateApp() + } + } + } + } +} \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/CoreStateApp.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/CoreStateApp.kt new file mode 100644 index 0000000..e492f14 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/CoreStateApp.kt @@ -0,0 +1,99 @@ +package com.corestate.androidApp.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import androidx.hilt.navigation.compose.hiltViewModel +import com.corestate.androidApp.ui.screens.backup.BackupScreen +import com.corestate.androidApp.ui.screens.dashboard.DashboardScreen +import com.corestate.androidApp.ui.screens.files.FilesScreen +import com.corestate.androidApp.ui.screens.settings.SettingsScreen +import com.corestate.androidApp.ui.navigation.CoreStateNavigation +import com.corestate.androidApp.ui.navigation.NavigationDestination + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CoreStateApp() { + val navController = rememberNavController() + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { + NavigationBar { + CoreStateNavigation.destinations.forEach { destination -> + NavigationBarItem( + icon = { + Icon( + imageVector = destination.icon, + contentDescription = destination.title + ) + }, + label = { Text(destination.title) }, + selected = currentDestination?.hierarchy?.any { it.route == destination.route } == true, + onClick = { + navController.navigate(destination.route) { + // Pop up to the start destination of the graph to + // avoid building up a large stack of destinations + // on the back stack as users select items + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + // Avoid multiple copies of the same destination when + // reselecting the same item + launchSingleTop = true + // Restore state when reselecting a previously selected item + restoreState = true + } + } + ) + } + } + } + ) { innerPadding -> + NavHost( + navController = navController, + startDestination = NavigationDestination.Dashboard.route, + modifier = Modifier.padding(innerPadding) + ) { + composable(NavigationDestination.Dashboard.route) { + DashboardScreen( + viewModel = hiltViewModel(), + onNavigateToBackup = { + navController.navigate(NavigationDestination.Backup.route) + }, + onNavigateToFiles = { + navController.navigate(NavigationDestination.Files.route) + } + ) + } + + composable(NavigationDestination.Backup.route) { + BackupScreen( + viewModel = hiltViewModel() + ) + } + + composable(NavigationDestination.Files.route) { + FilesScreen( + viewModel = hiltViewModel() + ) + } + + composable(NavigationDestination.Settings.route) { + SettingsScreen( + viewModel = hiltViewModel() + ) + } + } + } +} \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/components/BackupProgressCard.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/components/BackupProgressCard.kt new file mode 100644 index 0000000..6c7ffeb --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/components/BackupProgressCard.kt @@ -0,0 +1,129 @@ +package com.corestate.androidApp.ui.components + +import androidx.compose.animation.core.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun BackupProgressCard( + isBackupRunning: Boolean, + progress: Float, + onStartBackup: () -> Unit, + onStopBackup: () -> Unit, + currentFile: String? = null, + estimatedTimeRemaining: String? = null +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = if (isBackupRunning) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surface + } + ) + ) { + Column( + modifier = Modifier.padding(20.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = if (isBackupRunning) "Backup in Progress" else "Ready to Backup", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + + if (isBackupRunning) { + val infiniteTransition = rememberInfiniteTransition(label = "rotation") + val rotation by infiniteTransition.animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable( + animation = tween(2000, easing = LinearEasing) + ), + label = "rotation" + ) + + Icon( + imageVector = Icons.Default.Sync, + contentDescription = "Syncing", + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + } + + if (isBackupRunning) { + Spacer(modifier = Modifier.height(16.dp)) + + LinearProgressIndicator( + progress = progress, + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "${(progress * 100).toInt()}% Complete", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + currentFile?.let { file -> + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Current: $file", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + estimatedTimeRemaining?.let { time -> + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Estimated time remaining: $time", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + if (isBackupRunning) { + Button( + onClick = onStopBackup, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error + ) + ) { + Icon(Icons.Default.Stop, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text("Stop Backup") + } + } else { + Button( + onClick = onStartBackup, + modifier = Modifier.fillMaxWidth() + ) { + Icon(Icons.Default.PlayArrow, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text("Start Backup") + } + } + } + } +} \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/components/FileSelectionCard.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/components/FileSelectionCard.kt new file mode 100644 index 0000000..30c1ee9 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/components/FileSelectionCard.kt @@ -0,0 +1,57 @@ +package com.corestate.androidApp.ui.components + +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.corestate.androidApp.ui.screens.backup.FolderModel + +@Composable +fun FileSelectionCard( + folder: FolderModel, + onRemove: () -> Unit +) { + Card( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Folder, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = folder.name, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + Text( + text = "${folder.size} • ${folder.filesCount} files", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + IconButton(onClick = onRemove) { + Icon( + imageVector = Icons.Default.Remove, + contentDescription = "Remove folder" + ) + } + } + } +} \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/components/QuickActionCard.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/components/QuickActionCard.kt new file mode 100644 index 0000000..b34efd4 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/components/QuickActionCard.kt @@ -0,0 +1,52 @@ +package com.corestate.androidApp.ui.components + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun QuickActionCard( + title: String, + description: String, + icon: ImageVector, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Card( + onClick = onClick, + modifier = modifier + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(40.dp) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/components/StatCard.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/components/StatCard.kt new file mode 100644 index 0000000..7405505 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/components/StatCard.kt @@ -0,0 +1,48 @@ +package com.corestate.androidApp.ui.components + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun StatCard( + title: String, + value: String, + icon: ImageVector, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(32.dp) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = value, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/navigation/CoreStateNavigation.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/navigation/CoreStateNavigation.kt new file mode 100644 index 0000000..a253a71 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/navigation/CoreStateNavigation.kt @@ -0,0 +1,44 @@ +package com.corestate.androidApp.ui.navigation + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.ui.graphics.vector.ImageVector + +sealed class NavigationDestination( + val route: String, + val title: String, + val icon: ImageVector +) { + object Dashboard : NavigationDestination( + route = "dashboard", + title = "Dashboard", + icon = Icons.Default.Dashboard + ) + + object Backup : NavigationDestination( + route = "backup", + title = "Backup", + icon = Icons.Default.Backup + ) + + object Files : NavigationDestination( + route = "files", + title = "Files", + icon = Icons.Default.Folder + ) + + object Settings : NavigationDestination( + route = "settings", + title = "Settings", + icon = Icons.Default.Settings + ) +} + +object CoreStateNavigation { + val destinations = listOf( + NavigationDestination.Dashboard, + NavigationDestination.Backup, + NavigationDestination.Files, + NavigationDestination.Settings + ) +} \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/backup/BackupScreen.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/backup/BackupScreen.kt new file mode 100644 index 0000000..3151307 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/backup/BackupScreen.kt @@ -0,0 +1,207 @@ +package com.corestate.androidApp.ui.screens.backup + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.corestate.androidApp.ui.components.BackupProgressCard +import com.corestate.androidApp.ui.components.FileSelectionCard + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BackupScreen( + viewModel: BackupViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { + Text( + text = "Backup", + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold + ) + } + + item { + BackupProgressCard( + isBackupRunning = uiState.isBackupRunning, + progress = uiState.backupProgress, + onStartBackup = viewModel::startBackup, + onStopBackup = viewModel::stopBackup, + currentFile = uiState.currentFile, + estimatedTimeRemaining = uiState.estimatedTimeRemaining + ) + } + + item { + Card( + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = "Backup Settings", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text("Auto Backup") + Switch( + checked = uiState.autoBackupEnabled, + onCheckedChange = viewModel::setAutoBackupEnabled + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text("Include System Files") + Switch( + checked = uiState.includeSystemFiles, + onCheckedChange = viewModel::setIncludeSystemFiles + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text("Encrypt Backups") + Switch( + checked = uiState.encryptBackups, + onCheckedChange = viewModel::setEncryptBackups + ) + } + } + } + } + + item { + Text( + text = "Selected Folders", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + } + + items(uiState.selectedFolders) { folder -> + FileSelectionCard( + folder = folder, + onRemove = { viewModel.removeFolder(folder.path) } + ) + } + + item { + OutlinedButton( + onClick = viewModel::selectFolders, + modifier = Modifier.fillMaxWidth() + ) { + Icon(Icons.Default.Add, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text("Add Folders") + } + } + + item { + Text( + text = "Backup History", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + } + + items(uiState.backupHistory) { backup -> + BackupHistoryItem( + backup = backup, + onRestore = { viewModel.restoreBackup(backup.id) }, + onDelete = { viewModel.deleteBackup(backup.id) } + ) + } + } + + // Show error snackbar if needed + uiState.error?.let { error -> + LaunchedEffect(error) { + // Show snackbar here + viewModel.dismissError() + } + } +} + +@Composable +private fun BackupHistoryItem( + backup: BackupHistoryModel, + onRestore: () -> Unit, + onDelete: () -> Unit +) { + Card( + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = backup.name, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + Text( + text = "${backup.size} • ${backup.filesCount} files", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = backup.timestamp, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Row { + IconButton(onClick = onRestore) { + Icon(Icons.Default.Restore, contentDescription = "Restore") + } + IconButton(onClick = onDelete) { + Icon(Icons.Default.Delete, contentDescription = "Delete") + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/backup/BackupViewModel.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/backup/BackupViewModel.kt new file mode 100644 index 0000000..c0d7ef7 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/backup/BackupViewModel.kt @@ -0,0 +1,232 @@ +package com.corestate.androidApp.ui.screens.backup + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class BackupViewModel @Inject constructor( + private val backupRepository: BackupRepository, + private val settingsRepository: SettingsRepository, + private val fileRepository: FileRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(BackupUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadBackupData() + observeBackupStatus() + observeSettings() + } + + private fun loadBackupData() { + viewModelScope.launch { + try { + val folders = fileRepository.getSelectedFolders() + val history = backupRepository.getBackupHistory() + + _uiState.update { currentState -> + currentState.copy( + selectedFolders = folders, + backupHistory = history, + isLoading = false + ) + } + } catch (e: Exception) { + _uiState.update { it.copy(isLoading = false, error = e.message) } + } + } + } + + private fun observeBackupStatus() { + viewModelScope.launch { + backupRepository.backupStatus.collect { status -> + _uiState.update { currentState -> + currentState.copy( + isBackupRunning = status.isRunning, + backupProgress = status.progress, + currentFile = status.currentFile, + estimatedTimeRemaining = status.estimatedTimeRemaining + ) + } + } + } + } + + private fun observeSettings() { + viewModelScope.launch { + settingsRepository.backupSettings.collect { settings -> + _uiState.update { currentState -> + currentState.copy( + autoBackupEnabled = settings.autoBackupEnabled, + includeSystemFiles = settings.includeSystemFiles, + encryptBackups = settings.encryptBackups + ) + } + } + } + } + + fun startBackup() { + viewModelScope.launch { + try { + backupRepository.startBackup() + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + fun stopBackup() { + viewModelScope.launch { + try { + backupRepository.stopBackup() + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + fun setAutoBackupEnabled(enabled: Boolean) { + viewModelScope.launch { + settingsRepository.setAutoBackupEnabled(enabled) + } + } + + fun setIncludeSystemFiles(include: Boolean) { + viewModelScope.launch { + settingsRepository.setIncludeSystemFiles(include) + } + } + + fun setEncryptBackups(encrypt: Boolean) { + viewModelScope.launch { + settingsRepository.setEncryptBackups(encrypt) + } + } + + fun selectFolders() { + viewModelScope.launch { + try { + fileRepository.selectFolders() + loadBackupData() // Reload to get updated folders + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + fun removeFolder(path: String) { + viewModelScope.launch { + try { + fileRepository.removeFolder(path) + loadBackupData() // Reload to get updated folders + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + fun restoreBackup(backupId: String) { + viewModelScope.launch { + try { + backupRepository.restoreBackup(backupId) + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + fun deleteBackup(backupId: String) { + viewModelScope.launch { + try { + backupRepository.deleteBackup(backupId) + loadBackupData() // Reload to get updated history + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + fun dismissError() { + _uiState.update { it.copy(error = null) } + } +} + +data class BackupUiState( + val isLoading: Boolean = true, + val isBackupRunning: Boolean = false, + val backupProgress: Float = 0f, + val currentFile: String? = null, + val estimatedTimeRemaining: String? = null, + val autoBackupEnabled: Boolean = false, + val includeSystemFiles: Boolean = false, + val encryptBackups: Boolean = true, + val selectedFolders: List = emptyList(), + val backupHistory: List = emptyList(), + val error: String? = null +) + +data class FolderModel( + val path: String, + val name: String, + val size: String, + val filesCount: Int +) + +data class BackupHistoryModel( + val id: String, + val name: String, + val timestamp: String, + val size: String, + val filesCount: Int, + val status: BackupStatus +) + +enum class BackupStatus { + COMPLETED, + FAILED, + IN_PROGRESS +} + +// Enhanced BackupStatus for detailed progress +data class DetailedBackupStatus( + val isRunning: Boolean, + val progress: Float, + val currentFile: String? = null, + val estimatedTimeRemaining: String? = null +) + +// Additional repository interfaces +interface SettingsRepository { + val backupSettings: Flow + suspend fun setAutoBackupEnabled(enabled: Boolean) + suspend fun setIncludeSystemFiles(include: Boolean) + suspend fun setEncryptBackups(encrypt: Boolean) +} + +interface FileRepository { + suspend fun getSelectedFolders(): List + suspend fun selectFolders() + suspend fun removeFolder(path: String) +} + +data class BackupSettings( + val autoBackupEnabled: Boolean, + val includeSystemFiles: Boolean, + val encryptBackups: Boolean +) + +// Enhanced BackupRepository interface +interface BackupRepository { + val backupStatus: Flow + suspend fun startBackup() + suspend fun stopBackup() + suspend fun getBackupHistory(): List + suspend fun restoreBackup(backupId: String) + suspend fun deleteBackup(backupId: String) +} \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/dashboard/DashboardScreen.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/dashboard/DashboardScreen.kt new file mode 100644 index 0000000..c976c0a --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/dashboard/DashboardScreen.kt @@ -0,0 +1,188 @@ +package com.corestate.androidApp.ui.screens.dashboard + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.corestate.androidApp.R +import com.corestate.androidApp.ui.components.BackupProgressCard +import com.corestate.androidApp.ui.components.QuickActionCard +import com.corestate.androidApp.ui.components.StatCard + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DashboardScreen( + viewModel: DashboardViewModel = hiltViewModel(), + onNavigateToBackup: () -> Unit, + onNavigateToFiles: () -> Unit +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { + Text( + text = "Dashboard", + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold + ) + } + + item { + BackupProgressCard( + isBackupRunning = uiState.isBackupRunning, + progress = uiState.backupProgress, + onStartBackup = viewModel::startBackup, + onStopBackup = viewModel::stopBackup + ) + } + + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + StatCard( + modifier = Modifier.weight(1f), + title = "Total Backups", + value = uiState.totalBackups.toString(), + icon = Icons.Default.Backup + ) + StatCard( + modifier = Modifier.weight(1f), + title = "Storage Used", + value = uiState.storageUsed, + icon = Icons.Default.Storage + ) + } + } + + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + StatCard( + modifier = Modifier.weight(1f), + title = "Files Protected", + value = uiState.filesProtected.toString(), + icon = Icons.Default.Shield + ) + StatCard( + modifier = Modifier.weight(1f), + title = "Last Backup", + value = uiState.lastBackupTime, + icon = Icons.Default.Schedule + ) + } + } + + item { + Text( + text = "Quick Actions", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + } + + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + QuickActionCard( + modifier = Modifier.weight(1f), + title = "Start Backup", + description = "Begin backup process", + icon = Icons.Default.PlayArrow, + onClick = onNavigateToBackup + ) + QuickActionCard( + modifier = Modifier.weight(1f), + title = "Browse Files", + description = "View backed up files", + icon = Icons.Default.Folder, + onClick = onNavigateToFiles + ) + } + } + + item { + Text( + text = "Recent Activity", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + } + + items(uiState.recentActivities) { activity -> + ActivityItem(activity = activity) + } + } +} + +@Composable +private fun ActivityItem( + activity: ActivityModel +) { + Card( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = when (activity.type) { + ActivityType.BACKUP_COMPLETED -> Icons.Default.CheckCircle + ActivityType.BACKUP_FAILED -> Icons.Default.Error + ActivityType.FILE_RESTORED -> Icons.Default.Restore + ActivityType.SYNC_COMPLETED -> Icons.Default.Sync + }, + contentDescription = null, + tint = when (activity.type) { + ActivityType.BACKUP_COMPLETED, ActivityType.FILE_RESTORED, ActivityType.SYNC_COMPLETED -> + MaterialTheme.colorScheme.primary + ActivityType.BACKUP_FAILED -> MaterialTheme.colorScheme.error + } + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = activity.title, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + Text( + text = activity.description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Text( + text = activity.timestamp, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/dashboard/DashboardViewModel.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/dashboard/DashboardViewModel.kt new file mode 100644 index 0000000..344be12 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/dashboard/DashboardViewModel.kt @@ -0,0 +1,163 @@ +package com.corestate.androidApp.ui.screens.dashboard + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DashboardViewModel @Inject constructor( + private val backupRepository: BackupRepository, + private val statisticsRepository: StatisticsRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(DashboardUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadDashboardData() + observeBackupStatus() + } + + private fun loadDashboardData() { + viewModelScope.launch { + try { + val stats = statisticsRepository.getBackupStatistics() + val activities = statisticsRepository.getRecentActivities() + + _uiState.update { currentState -> + currentState.copy( + totalBackups = stats.totalBackups, + storageUsed = formatStorageSize(stats.storageUsedBytes), + filesProtected = stats.filesProtected, + lastBackupTime = formatLastBackupTime(stats.lastBackupTimestamp), + recentActivities = activities, + isLoading = false + ) + } + } catch (e: Exception) { + _uiState.update { it.copy(isLoading = false, error = e.message) } + } + } + } + + private fun observeBackupStatus() { + viewModelScope.launch { + backupRepository.backupStatus.collect { status -> + _uiState.update { currentState -> + currentState.copy( + isBackupRunning = status.isRunning, + backupProgress = status.progress + ) + } + } + } + } + + fun startBackup() { + viewModelScope.launch { + try { + backupRepository.startBackup() + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + fun stopBackup() { + viewModelScope.launch { + try { + backupRepository.stopBackup() + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + fun dismissError() { + _uiState.update { it.copy(error = null) } + } + + private fun formatStorageSize(bytes: Long): String { + val units = arrayOf("B", "KB", "MB", "GB", "TB") + var size = bytes.toDouble() + var unitIndex = 0 + + while (size >= 1024 && unitIndex < units.size - 1) { + size /= 1024 + unitIndex++ + } + + return "%.1f %s".format(size, units[unitIndex]) + } + + private fun formatLastBackupTime(timestamp: Long): String { + if (timestamp == 0L) return "Never" + + val now = System.currentTimeMillis() + val diff = now - timestamp + val minutes = diff / (1000 * 60) + val hours = minutes / 60 + val days = hours / 24 + + return when { + minutes < 60 -> "${minutes}m ago" + hours < 24 -> "${hours}h ago" + days < 7 -> "${days}d ago" + else -> "${days / 7}w ago" + } + } +} + +data class DashboardUiState( + val isLoading: Boolean = true, + val isBackupRunning: Boolean = false, + val backupProgress: Float = 0f, + val totalBackups: Int = 0, + val storageUsed: String = "0 B", + val filesProtected: Int = 0, + val lastBackupTime: String = "Never", + val recentActivities: List = emptyList(), + val error: String? = null +) + +data class ActivityModel( + val id: String, + val type: ActivityType, + val title: String, + val description: String, + val timestamp: String +) + +enum class ActivityType { + BACKUP_COMPLETED, + BACKUP_FAILED, + FILE_RESTORED, + SYNC_COMPLETED +} + +// Mock repository interfaces - these would be implemented with real data sources +interface BackupRepository { + val backupStatus: Flow + suspend fun startBackup() + suspend fun stopBackup() +} + +interface StatisticsRepository { + suspend fun getBackupStatistics(): BackupStatistics + suspend fun getRecentActivities(): List +} + +data class BackupStatus( + val isRunning: Boolean, + val progress: Float +) + +data class BackupStatistics( + val totalBackups: Int, + val storageUsedBytes: Long, + val filesProtected: Int, + val lastBackupTimestamp: Long +) \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/files/FilesScreen.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/files/FilesScreen.kt new file mode 100644 index 0000000..718fc54 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/files/FilesScreen.kt @@ -0,0 +1,230 @@ +package com.corestate.androidApp.ui.screens.files + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FilesScreen( + viewModel: FilesViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Files", + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold + ) + + Row { + IconButton(onClick = viewModel::toggleViewMode) { + Icon( + imageVector = if (uiState.isGridView) Icons.Default.ViewList else Icons.Default.GridView, + contentDescription = "Toggle view" + ) + } + IconButton(onClick = viewModel::refreshFiles) { + Icon(Icons.Default.Refresh, contentDescription = "Refresh") + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Search bar + OutlinedTextField( + value = uiState.searchQuery, + onValueChange = viewModel::updateSearchQuery, + placeholder = { Text("Search files...") }, + leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Filter chips + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(FileType.values()) { type -> + FilterChip( + selected = uiState.selectedFileTypes.contains(type), + onClick = { viewModel.toggleFileTypeFilter(type) }, + label = { Text(type.displayName) } + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Navigation breadcrumb + if (uiState.currentPath.isNotEmpty()) { + Card( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = viewModel::navigateUp, + enabled = uiState.canNavigateUp + ) { + Icon(Icons.Default.ArrowBack, contentDescription = "Back") + } + Text( + text = uiState.currentPath, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.weight(1f) + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + } + + // File list + if (uiState.isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(uiState.filteredFiles) { file -> + FileItem( + file = file, + onFileClick = { viewModel.navigateToFile(file) }, + onRestoreClick = { viewModel.restoreFile(file) }, + onDownloadClick = { viewModel.downloadFile(file) }, + onDeleteClick = { viewModel.deleteFile(file) } + ) + } + } + } + } +} + +@Composable +private fun FileItem( + file: FileModel, + onFileClick: () -> Unit, + onRestoreClick: () -> Unit, + onDownloadClick: () -> Unit, + onDeleteClick: () -> Unit +) { + Card( + modifier = Modifier.fillMaxWidth(), + onClick = onFileClick + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = when (file.type) { + FileType.FOLDER -> Icons.Default.Folder + FileType.IMAGE -> Icons.Default.Image + FileType.VIDEO -> Icons.Default.VideoFile + FileType.AUDIO -> Icons.Default.AudioFile + FileType.DOCUMENT -> Icons.Default.Description + FileType.OTHER -> Icons.Default.InsertDriveFile + }, + contentDescription = null, + tint = when (file.type) { + FileType.FOLDER -> MaterialTheme.colorScheme.primary + FileType.IMAGE -> MaterialTheme.colorScheme.secondary + FileType.VIDEO -> MaterialTheme.colorScheme.tertiary + else -> MaterialTheme.colorScheme.onSurfaceVariant + } + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = file.name, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + Row { + Text( + text = file.size, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + if (file.lastModified.isNotEmpty()) { + Text( + text = " • ${file.lastModified}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + if (file.isBackedUp) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.CloudDone, + contentDescription = "Backed up", + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = "Backed up", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary + ) + } + } + } + + if (file.type != FileType.FOLDER) { + Row { + if (file.isBackedUp) { + IconButton(onClick = onRestoreClick) { + Icon(Icons.Default.Restore, contentDescription = "Restore") + } + } + IconButton(onClick = onDownloadClick) { + Icon(Icons.Default.Download, contentDescription = "Download") + } + IconButton(onClick = onDeleteClick) { + Icon(Icons.Default.Delete, contentDescription = "Delete") + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/files/FilesViewModel.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/files/FilesViewModel.kt new file mode 100644 index 0000000..8dcdc0f --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/files/FilesViewModel.kt @@ -0,0 +1,214 @@ +package com.corestate.androidApp.ui.screens.files + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class FilesViewModel @Inject constructor( + private val fileRepository: FileRepository, + private val backupRepository: BackupRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(FilesUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadFiles() + } + + private fun loadFiles() { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + + try { + val files = fileRepository.getFiles(uiState.value.currentPath) + _uiState.update { currentState -> + currentState.copy( + files = files, + isLoading = false + ) + } + applyFilters() + } catch (e: Exception) { + _uiState.update { + it.copy( + isLoading = false, + error = e.message + ) + } + } + } + } + + fun updateSearchQuery(query: String) { + _uiState.update { it.copy(searchQuery = query) } + applyFilters() + } + + fun toggleFileTypeFilter(type: FileType) { + _uiState.update { currentState -> + val updatedTypes = if (currentState.selectedFileTypes.contains(type)) { + currentState.selectedFileTypes - type + } else { + currentState.selectedFileTypes + type + } + currentState.copy(selectedFileTypes = updatedTypes) + } + applyFilters() + } + + fun toggleViewMode() { + _uiState.update { it.copy(isGridView = !it.isGridView) } + } + + fun refreshFiles() { + loadFiles() + } + + fun navigateToFile(file: FileModel) { + if (file.type == FileType.FOLDER) { + val newPath = if (uiState.value.currentPath.isEmpty()) { + file.name + } else { + "${uiState.value.currentPath}/${file.name}" + } + + _uiState.update { currentState -> + currentState.copy( + currentPath = newPath, + pathHistory = currentState.pathHistory + currentState.currentPath + ) + } + loadFiles() + } + } + + fun navigateUp() { + val currentState = uiState.value + if (currentState.canNavigateUp) { + val parentPath = if (currentState.pathHistory.isNotEmpty()) { + currentState.pathHistory.last() + } else { + "" + } + + _uiState.update { + it.copy( + currentPath = parentPath, + pathHistory = if (it.pathHistory.isNotEmpty()) { + it.pathHistory.dropLast(1) + } else { + emptyList() + } + ) + } + loadFiles() + } + } + + fun restoreFile(file: FileModel) { + viewModelScope.launch { + try { + backupRepository.restoreFile(file.path) + // Optionally show success message + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + fun downloadFile(file: FileModel) { + viewModelScope.launch { + try { + fileRepository.downloadFile(file.path) + // Optionally show success message + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + fun deleteFile(file: FileModel) { + viewModelScope.launch { + try { + fileRepository.deleteFile(file.path) + loadFiles() // Refresh the list + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + private fun applyFilters() { + val currentState = uiState.value + var filteredFiles = currentState.files + + // Apply search filter + if (currentState.searchQuery.isNotEmpty()) { + filteredFiles = filteredFiles.filter { file -> + file.name.contains(currentState.searchQuery, ignoreCase = true) + } + } + + // Apply file type filter + if (currentState.selectedFileTypes.isNotEmpty()) { + filteredFiles = filteredFiles.filter { file -> + currentState.selectedFileTypes.contains(file.type) + } + } + + _uiState.update { it.copy(filteredFiles = filteredFiles) } + } + + fun dismissError() { + _uiState.update { it.copy(error = null) } + } +} + +data class FilesUiState( + val isLoading: Boolean = true, + val files: List = emptyList(), + val filteredFiles: List = emptyList(), + val currentPath: String = "", + val pathHistory: List = emptyList(), + val searchQuery: String = "", + val selectedFileTypes: Set = emptySet(), + val isGridView: Boolean = false, + val error: String? = null +) { + val canNavigateUp: Boolean + get() = currentPath.isNotEmpty() +} + +data class FileModel( + val path: String, + val name: String, + val size: String, + val lastModified: String, + val type: FileType, + val isBackedUp: Boolean = false +) + +enum class FileType(val displayName: String) { + FOLDER("Folders"), + IMAGE("Images"), + VIDEO("Videos"), + AUDIO("Audio"), + DOCUMENT("Documents"), + OTHER("Other") +} + +// Enhanced repository interfaces +interface FileRepository { + suspend fun getFiles(path: String): List + suspend fun downloadFile(path: String) + suspend fun deleteFile(path: String) +} + +interface BackupRepository { + suspend fun restoreFile(path: String) +} \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/settings/SettingsScreen.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/settings/SettingsScreen.kt new file mode 100644 index 0000000..f377686 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/settings/SettingsScreen.kt @@ -0,0 +1,334 @@ +package com.corestate.androidApp.ui.screens.settings + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreen( + viewModel: SettingsViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { + Text( + text = "Settings", + style = MaterialTheme.typography.headlineLarge, + fontWeight = FontWeight.Bold + ) + } + + // Account Section + item { + SettingsSection(title = "Account") { + SettingsItem( + title = "Account Info", + subtitle = uiState.userEmail, + icon = Icons.Default.Person, + onClick = { viewModel.openAccountInfo() } + ) + SettingsItem( + title = "Storage", + subtitle = "${uiState.storageUsed} of ${uiState.storageLimit} used", + icon = Icons.Default.Storage, + onClick = { viewModel.openStorageInfo() } + ) + SettingsItem( + title = "Subscription", + subtitle = uiState.subscriptionType, + icon = Icons.Default.Star, + onClick = { viewModel.openSubscription() } + ) + } + } + + // Backup Settings Section + item { + SettingsSection(title = "Backup Settings") { + SettingsSwitchItem( + title = "Auto Backup", + subtitle = "Automatically backup when charging", + icon = Icons.Default.Backup, + checked = uiState.autoBackupEnabled, + onCheckedChange = viewModel::setAutoBackupEnabled + ) + SettingsSwitchItem( + title = "WiFi Only", + subtitle = "Only backup over WiFi", + icon = Icons.Default.Wifi, + checked = uiState.wifiOnlyBackup, + onCheckedChange = viewModel::setWifiOnlyBackup + ) + SettingsSwitchItem( + title = "Encrypt Backups", + subtitle = "End-to-end encryption", + icon = Icons.Default.Security, + checked = uiState.encryptBackups, + onCheckedChange = viewModel::setEncryptBackups + ) + SettingsItem( + title = "Backup Frequency", + subtitle = uiState.backupFrequency, + icon = Icons.Default.Schedule, + onClick = { viewModel.openBackupFrequency() } + ) + } + } + + // Security Section + item { + SettingsSection(title = "Security") { + SettingsSwitchItem( + title = "Biometric Lock", + subtitle = "Use fingerprint/face unlock", + icon = Icons.Default.Fingerprint, + checked = uiState.biometricEnabled, + onCheckedChange = viewModel::setBiometricEnabled + ) + SettingsItem( + title = "Change PIN", + subtitle = "Update your backup PIN", + icon = Icons.Default.Lock, + onClick = { viewModel.changePIN() } + ) + SettingsItem( + title = "Two-Factor Authentication", + subtitle = if (uiState.twoFactorEnabled) "Enabled" else "Disabled", + icon = Icons.Default.Security, + onClick = { viewModel.openTwoFactor() } + ) + } + } + + // Sync Settings + item { + SettingsSection(title = "Sync") { + SettingsSwitchItem( + title = "P2P Sync", + subtitle = "Sync with other devices", + icon = Icons.Default.Sync, + checked = uiState.p2pSyncEnabled, + onCheckedChange = viewModel::setP2PSyncEnabled + ) + SettingsItem( + title = "Connected Devices", + subtitle = "${uiState.connectedDevices} devices", + icon = Icons.Default.Devices, + onClick = { viewModel.openConnectedDevices() } + ) + } + } + + // Notifications Section + item { + SettingsSection(title = "Notifications") { + SettingsSwitchItem( + title = "Backup Notifications", + subtitle = "Get notified about backup status", + icon = Icons.Default.Notifications, + checked = uiState.backupNotifications, + onCheckedChange = viewModel::setBackupNotifications + ) + SettingsSwitchItem( + title = "Security Alerts", + subtitle = "Get notified about security events", + icon = Icons.Default.Warning, + checked = uiState.securityAlerts, + onCheckedChange = viewModel::setSecurityAlerts + ) + } + } + + // Advanced Section + item { + SettingsSection(title = "Advanced") { + SettingsItem( + title = "Advanced Settings", + subtitle = "Developer and power user options", + icon = Icons.Default.Settings, + onClick = { viewModel.openAdvancedSettings() } + ) + SettingsItem( + title = "Export Data", + subtitle = "Export your backup data", + icon = Icons.Default.Download, + onClick = { viewModel.exportData() } + ) + SettingsItem( + title = "Import Data", + subtitle = "Import from another backup", + icon = Icons.Default.Upload, + onClick = { viewModel.importData() } + ) + } + } + + // Support Section + item { + SettingsSection(title = "Support") { + SettingsItem( + title = "Help & FAQ", + subtitle = "Get help and find answers", + icon = Icons.Default.Help, + onClick = { viewModel.openHelp() } + ) + SettingsItem( + title = "Contact Support", + subtitle = "Get in touch with our team", + icon = Icons.Default.ContactSupport, + onClick = { viewModel.contactSupport() } + ) + SettingsItem( + title = "App Version", + subtitle = uiState.appVersion, + icon = Icons.Default.Info, + onClick = { viewModel.showAppInfo() } + ) + } + } + + // Danger Zone + item { + SettingsSection(title = "Danger Zone") { + SettingsItem( + title = "Sign Out", + subtitle = "Sign out of your account", + icon = Icons.Default.Logout, + onClick = { viewModel.signOut() }, + isDestructive = true + ) + SettingsItem( + title = "Delete Account", + subtitle = "Permanently delete your account", + icon = Icons.Default.Delete, + onClick = { viewModel.deleteAccount() }, + isDestructive = true + ) + } + } + } +} + +@Composable +private fun SettingsSection( + title: String, + content: @Composable ColumnScope.() -> Unit +) { + Card( + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(12.dp)) + content() + } + } +} + +@Composable +private fun SettingsItem( + title: String, + subtitle: String, + icon: androidx.compose.ui.graphics.vector.ImageVector, + onClick: () -> Unit, + isDestructive: Boolean = false +) { + Surface( + onClick = onClick, + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = if (isDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + color = if (isDestructive) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@Composable +private fun SettingsSwitchItem( + title: String, + subtitle: String, + icon: androidx.compose.ui.graphics.vector.ImageVector, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Switch( + checked = checked, + onCheckedChange = onCheckedChange + ) + } +} \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/settings/SettingsViewModel.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/settings/SettingsViewModel.kt new file mode 100644 index 0000000..e436e95 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/screens/settings/SettingsViewModel.kt @@ -0,0 +1,277 @@ +package com.corestate.androidApp.ui.screens.settings + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val settingsRepository: SettingsRepository, + private val userRepository: UserRepository, + private val securityRepository: SecurityRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(SettingsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadSettings() + } + + private fun loadSettings() { + viewModelScope.launch { + try { + combine( + settingsRepository.getSettings(), + userRepository.getUserInfo(), + securityRepository.getSecuritySettings() + ) { settings, userInfo, securitySettings -> + Triple(settings, userInfo, securitySettings) + }.collect { (settings, userInfo, securitySettings) -> + _uiState.update { currentState -> + currentState.copy( + userEmail = userInfo.email, + storageUsed = formatStorageSize(userInfo.storageUsedBytes), + storageLimit = formatStorageSize(userInfo.storageLimitBytes), + subscriptionType = userInfo.subscriptionType, + autoBackupEnabled = settings.autoBackupEnabled, + wifiOnlyBackup = settings.wifiOnlyBackup, + encryptBackups = settings.encryptBackups, + backupFrequency = settings.backupFrequency, + biometricEnabled = securitySettings.biometricEnabled, + twoFactorEnabled = securitySettings.twoFactorEnabled, + p2pSyncEnabled = settings.p2pSyncEnabled, + connectedDevices = settings.connectedDevicesCount, + backupNotifications = settings.backupNotifications, + securityAlerts = settings.securityAlerts, + appVersion = "2.0.0" + ) + } + } + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + // Account actions + fun openAccountInfo() { + // Navigate to account info screen + } + + fun openStorageInfo() { + // Navigate to storage info screen + } + + fun openSubscription() { + // Navigate to subscription screen + } + + // Backup settings + fun setAutoBackupEnabled(enabled: Boolean) { + viewModelScope.launch { + settingsRepository.setAutoBackupEnabled(enabled) + } + } + + fun setWifiOnlyBackup(enabled: Boolean) { + viewModelScope.launch { + settingsRepository.setWifiOnlyBackup(enabled) + } + } + + fun setEncryptBackups(enabled: Boolean) { + viewModelScope.launch { + settingsRepository.setEncryptBackups(enabled) + } + } + + fun openBackupFrequency() { + // Open backup frequency selection dialog + } + + // Security settings + fun setBiometricEnabled(enabled: Boolean) { + viewModelScope.launch { + securityRepository.setBiometricEnabled(enabled) + } + } + + fun changePIN() { + // Navigate to PIN change screen + } + + fun openTwoFactor() { + // Navigate to 2FA settings + } + + // Sync settings + fun setP2PSyncEnabled(enabled: Boolean) { + viewModelScope.launch { + settingsRepository.setP2PSyncEnabled(enabled) + } + } + + fun openConnectedDevices() { + // Navigate to connected devices screen + } + + // Notification settings + fun setBackupNotifications(enabled: Boolean) { + viewModelScope.launch { + settingsRepository.setBackupNotifications(enabled) + } + } + + fun setSecurityAlerts(enabled: Boolean) { + viewModelScope.launch { + settingsRepository.setSecurityAlerts(enabled) + } + } + + // Advanced actions + fun openAdvancedSettings() { + // Navigate to advanced settings + } + + fun exportData() { + viewModelScope.launch { + try { + // Implement data export + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + fun importData() { + viewModelScope.launch { + try { + // Implement data import + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + // Support actions + fun openHelp() { + // Open help screen or external link + } + + fun contactSupport() { + // Open support contact options + } + + fun showAppInfo() { + // Show app information dialog + } + + // Danger zone actions + fun signOut() { + viewModelScope.launch { + try { + userRepository.signOut() + // Navigate to login screen + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + fun deleteAccount() { + viewModelScope.launch { + try { + // Show confirmation dialog first + userRepository.deleteAccount() + // Navigate to login screen + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } + } + + fun dismissError() { + _uiState.update { it.copy(error = null) } + } + + private fun formatStorageSize(bytes: Long): String { + val units = arrayOf("B", "KB", "MB", "GB", "TB") + var size = bytes.toDouble() + var unitIndex = 0 + + while (size >= 1024 && unitIndex < units.size - 1) { + size /= 1024 + unitIndex++ + } + + return "%.1f %s".format(size, units[unitIndex]) + } +} + +data class SettingsUiState( + val userEmail: String = "", + val storageUsed: String = "0 B", + val storageLimit: String = "0 B", + val subscriptionType: String = "Free", + val autoBackupEnabled: Boolean = false, + val wifiOnlyBackup: Boolean = true, + val encryptBackups: Boolean = true, + val backupFrequency: String = "Daily", + val biometricEnabled: Boolean = false, + val twoFactorEnabled: Boolean = false, + val p2pSyncEnabled: Boolean = false, + val connectedDevices: Int = 0, + val backupNotifications: Boolean = true, + val securityAlerts: Boolean = true, + val appVersion: String = "2.0.0", + val error: String? = null +) + +// Repository interfaces +interface SettingsRepository { + suspend fun getSettings(): Flow + suspend fun setAutoBackupEnabled(enabled: Boolean) + suspend fun setWifiOnlyBackup(enabled: Boolean) + suspend fun setEncryptBackups(enabled: Boolean) + suspend fun setP2PSyncEnabled(enabled: Boolean) + suspend fun setBackupNotifications(enabled: Boolean) + suspend fun setSecurityAlerts(enabled: Boolean) +} + +interface UserRepository { + suspend fun getUserInfo(): UserInfo + suspend fun signOut() + suspend fun deleteAccount() +} + +interface SecurityRepository { + suspend fun getSecuritySettings(): SecuritySettings + suspend fun setBiometricEnabled(enabled: Boolean) +} + +data class AppSettings( + val autoBackupEnabled: Boolean, + val wifiOnlyBackup: Boolean, + val encryptBackups: Boolean, + val backupFrequency: String, + val p2pSyncEnabled: Boolean, + val connectedDevicesCount: Int, + val backupNotifications: Boolean, + val securityAlerts: Boolean +) + +data class UserInfo( + val email: String, + val storageUsedBytes: Long, + val storageLimitBytes: Long, + val subscriptionType: String +) + +data class SecuritySettings( + val biometricEnabled: Boolean, + val twoFactorEnabled: Boolean +) \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/theme/Color.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/theme/Color.kt new file mode 100644 index 0000000..a0547c2 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/theme/Color.kt @@ -0,0 +1,30 @@ +package com.corestate.androidApp.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) + +// CoreState brand colors +val CoreStatePrimary = Color(0xFF1976D2) +val CoreStateSecondary = Color(0xFF388E3C) +val CoreStateAccent = Color(0xFFFF5722) + +// Additional colors +val Grey50 = Color(0xFFFAFAFA) +val Grey100 = Color(0xFFF5F5F5) +val Grey800 = Color(0xFF424242) +val Grey900 = Color(0xFF212121) +val White = Color(0xFFFFFFFF) +val Black = Color(0xFF000000) + +// Status colors +val Success = Color(0xFF4CAF50) +val Warning = Color(0xFFFF9800) +val Error = Color(0xFFF44336) +val Info = Color(0xFF2196F3) \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/theme/Theme.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/theme/Theme.kt new file mode 100644 index 0000000..cbf2405 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/theme/Theme.kt @@ -0,0 +1,70 @@ +package com.corestate.androidApp.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80, + background = Grey900, + surface = Grey800, + onPrimary = Grey900, + onSecondary = Grey900, + onTertiary = Grey900, + onBackground = Grey100, + onSurface = Grey100, +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40, + background = Grey50, + surface = Grey100, + onPrimary = White, + onSecondary = White, + onTertiary = White, + onBackground = Grey900, + onSurface = Grey900, +) + +@Composable +fun CoreStateTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/theme/Type.kt b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/theme/Type.kt new file mode 100644 index 0000000..aeca257 --- /dev/null +++ b/apps/android/androidApp/src/main/java/com/corestate/androidApp/ui/theme/Type.kt @@ -0,0 +1,54 @@ +package com.corestate.androidApp.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ), + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ), + // Additional styles for the app + headlineLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp + ), + headlineMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp + ), + headlineSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp + ) +) \ No newline at end of file diff --git a/apps/android/androidApp/src/main/res/values/strings.xml b/apps/android/androidApp/src/main/res/values/strings.xml new file mode 100644 index 0000000..6780650 --- /dev/null +++ b/apps/android/androidApp/src/main/res/values/strings.xml @@ -0,0 +1,113 @@ + + + CoreState + + + Dashboard + Backup + Files + Settings + + + Dashboard + Backup in Progress + Ready to Backup + Start Backup + Stop Backup + Total Backups + Storage Used + Files Protected + Last Backup + Quick Actions + Recent Activity + + + Backup + Backup Settings + Auto Backup + Include System Files + Encrypt Backups + Selected Folders + Add Folders + Backup History + + + Files + Search files... + Toggle view + Refresh + Restore + Download + Delete + Backed up + + + Settings + Account + Account Info + Storage + Subscription + Backup Settings + WiFi Only + Backup Frequency + Security + Biometric Lock + Change PIN + Two-Factor Authentication + Sync + P2P Sync + Connected Devices + Notifications + Backup Notifications + Security Alerts + Advanced + Advanced Settings + Export Data + Import Data + Support + Help & FAQ + Contact Support + App Version + Danger Zone + Sign Out + Delete Account + + + OK + Cancel + Save + Back + Next + Done + Error + Loading… + Retry + + + Folders + Images + Videos + Audio + Documents + Other + + + Backup Completed + Backup Failed + File Restored + Sync Completed + + + CoreState Backup + Backup in progress… + Backup completed successfully + Backup failed. Tap to retry. + + + Storage Permission Required + CoreState needs access to your storage to backup your files. + Camera Permission Required + CoreState needs camera access to scan QR codes for device pairing. + Biometric Permission Required + CoreState needs biometric access to secure your backups. + \ No newline at end of file diff --git a/apps/android/shared/build.gradle.kts b/apps/android/shared/build.gradle.kts index 4c09f92..6ff8a8c 100644 --- a/apps/android/shared/build.gradle.kts +++ b/apps/android/shared/build.gradle.kts @@ -1,19 +1,137 @@ -plugins { - kotlin("multiplatform") - id("com.android.library") -} - -kotlin { - androidTarget() - sourceSets { - val commonMain by getting - } -} - -android { - namespace = "com.corestate.shared" - compileSdk = 34 - defaultConfig { - minSdk = 26 - } +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") + id("com.android.library") + id("com.google.protobuf") version "0.9.4" +} + +kotlin { + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "17" + } + } + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + val commonMain by getting { + dependencies { + // Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + + // Serialization + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.6.0") + + // DateTime + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1") + + // Networking + implementation("io.ktor:ktor-client-core:2.3.5") + implementation("io.ktor:ktor-client-content-negotiation:2.3.5") + implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.5") + implementation("io.ktor:ktor-client-logging:2.3.5") + + // UUID + implementation("com.benasher44:uuid:0.8.2") + + // Logging + implementation("co.touchlab:kermit:2.0.2") + + // Settings/Preferences + implementation("com.russhwolf:multiplatform-settings:1.1.1") + + // SQL Database + implementation("app.cash.sqldelight:runtime:2.0.0") + implementation("app.cash.sqldelight:coroutines-extensions:2.0.0") + } + } + + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + } + } + + val androidMain by getting { + dependencies { + // Android-specific networking + implementation("io.ktor:ktor-client-okhttp:2.3.5") + + // Android SQLite + implementation("app.cash.sqldelight:android-driver:2.0.0") + + // Android-specific crypto + implementation("androidx.security:security-crypto:1.1.0-alpha06") + + // gRPC for Android + implementation("io.grpc:grpc-okhttp:1.58.0") + implementation("io.grpc:grpc-protobuf-lite:1.58.0") + implementation("io.grpc:grpc-stub:1.58.0") + + // WebRTC + implementation("org.webrtc:google-webrtc:1.0.32006") + } + } + + val iosMain by getting { + dependencies { + // iOS-specific networking + implementation("io.ktor:ktor-client-darwin:2.3.5") + + // iOS SQLite + implementation("app.cash.sqldelight:native-driver:2.0.0") + } + } + } +} + +android { + namespace = "com.corestate.shared" + compileSdk = 34 + + defaultConfig { + minSdk = 26 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildFeatures { + buildConfig = true + } +} + +// Protocol Buffers configuration +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.24.4" + } + plugins { + id("grpc") { + artifact = "io.grpc:protoc-gen-grpc-java:1.58.0" + } + id("grpckt") { + artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.0:jdk8@jar" + } + } + generateProtoTasks { + all().forEach { + it.plugins { + id("grpc") + id("grpckt") + } + it.builtins { + id("kotlin") + } + } + } } \ No newline at end of file diff --git a/apps/android/shared/src/commonMain/kotlin/sync/P2PSyncManager.kt b/apps/android/shared/src/commonMain/kotlin/sync/P2PSyncManager.kt index 4b67310..35bb98b 100644 --- a/apps/android/shared/src/commonMain/kotlin/sync/P2PSyncManager.kt +++ b/apps/android/shared/src/commonMain/kotlin/sync/P2PSyncManager.kt @@ -1,133 +1,133 @@ -package sync - -// --- Placeholder WebRTC and Signaling Classes --- -// These would be expect/actual implementations in a real KMP project. - -class RTCPeerConnection(config: RTCConfiguration) { - var onIceCandidate: ((candidate: String) -> Unit)? = null - var onDataChannel: ((channel: RTCDataChannel) -> Unit)? = null - suspend fun setRemoteDescription(offer: SessionDescription) {} - suspend fun createAnswer(): SessionDescription = SessionDescription("answer") - suspend fun setLocalDescription(answer: SessionDescription) {} - fun createDataChannel(label: String, init: RTCDataChannelInit): RTCDataChannel = RTCDataChannel(label) -} - -class RTCDataChannel(val label: String) { - var onOpen: (() -> Unit)? = null - var onMessage: ((message: ByteArray) -> Unit)? = null - var onError: ((error: String) -> Unit)? = null - fun sendMessage(data: ByteArray) {} -} - -class RTCConfiguration { - var iceServers: List = emptyList() -} - -data class RTCIceServer(val urls: List, val username: String? = null, val credential: String? = null) { - constructor(url: String) : this(listOf(url)) -} - -data class SessionDescription(val sdp: String) -data class RTCDataChannelInit(var ordered: Boolean = true, var maxRetransmits: Int = 0, var maxPacketLifeTime: Int = 0) - -interface SignalingServer { - suspend fun sendCandidate(peerId: String, candidate: String) - suspend fun sendAnswer(peerId: String, answer: SessionDescription) -} - -// --- Placeholder Data and Logic --- - -fun getLocalBackupState(): BackupState { return BackupState("localNode", emptyMap()) } -fun getCredential(): String = "secret" -val localNodeId = "localNode123" - -data class BackupState(val nodeId: String, val state: Map) { - fun toProto(): ByteArray = this.toString().toByteArray() -} - -// --- Main P2PSyncManager Class --- - -class P2PSyncManager(private val signalingServer: SignalingServer) { - private val peerConnections = mutableMapOf() - private val dataChannels = mutableMapOf() - // private val chunkTransferManager = ChunkTransferManager() // Assuming this is another component - // private val logger = ... // Placeholder for a logger - - suspend fun initiatePeerSync(peerId: String, offer: SessionDescription) { - val connection = RTCPeerConnection( - RTCConfiguration().apply { - iceServers = listOf( - RTCIceServer("stun:stun.corestate.io:3478"), - RTCIceServer( - urls = listOf("turn:turn.corestate.io:3478"), - username = "corestate", - credential = getCredential() - ) - ) - } - ) - - connection.onIceCandidate = { candidate -> - // In a real app, you'd launch a coroutine to send this - // signalingServer.sendCandidate(peerId, candidate) - println("Sending ICE candidate to $peerId") - } - - connection.onDataChannel = { channel -> - setupDataChannel(peerId, channel) - } - - peerConnections[peerId] = connection - - val syncChannel = connection.createDataChannel( - "backup-sync", - RTCDataChannelInit().apply { - ordered = true - maxRetransmits = 3 - maxPacketLifeTime = 30000 // 30 seconds - } - ) - - setupDataChannel(peerId, syncChannel) - - connection.setRemoteDescription(offer) - val answer = connection.createAnswer() - connection.setLocalDescription(answer) - - signalingServer.sendAnswer(peerId, answer) - } - - private fun setupDataChannel(peerId: String, channel: RTCDataChannel) { - channel.onOpen = { - println("Data channel opened with peer: $peerId") - startSyncProtocol(peerId) - } - - channel.onMessage = { message -> - handleSyncMessage(peerId, message) - } - - channel.onError = { error -> - println("Data channel error with peer $peerId: $error") - reconnectToPeer(peerId) - } - - dataChannels[peerId] = channel - } - - private fun startSyncProtocol(peerId: String) { - val localState = getLocalBackupState() - // val syncRequest = SyncProtocol.SyncRequest.newBuilder() ... // Protobuf integration - val syncRequest = localState.toProto() - - dataChannels[peerId]?.sendMessage(syncRequest) - } - - private fun handleSyncMessage(peerId: String, message: ByteArray) { - println("Received sync message from $peerId: ${message.decodeToString()}") - } - - private fun reconnectToPeer(peerId: String) { - println("Attempting to reconnect to peer $peerId...") - } +package sync + +// --- Placeholder WebRTC and Signaling Classes --- +// These would be expect/actual implementations in a real KMP project. + +class RTCPeerConnection(config: RTCConfiguration) { + var onIceCandidate: ((candidate: String) -> Unit)? = null + var onDataChannel: ((channel: RTCDataChannel) -> Unit)? = null + suspend fun setRemoteDescription(offer: SessionDescription) {} + suspend fun createAnswer(): SessionDescription = SessionDescription("answer") + suspend fun setLocalDescription(answer: SessionDescription) {} + fun createDataChannel(label: String, init: RTCDataChannelInit): RTCDataChannel = RTCDataChannel(label) +} + +class RTCDataChannel(val label: String) { + var onOpen: (() -> Unit)? = null + var onMessage: ((message: ByteArray) -> Unit)? = null + var onError: ((error: String) -> Unit)? = null + fun sendMessage(data: ByteArray) {} +} + +class RTCConfiguration { + var iceServers: List = emptyList() +} + +data class RTCIceServer(val urls: List, val username: String? = null, val credential: String? = null) { + constructor(url: String) : this(listOf(url)) +} + +data class SessionDescription(val sdp: String) +data class RTCDataChannelInit(var ordered: Boolean = true, var maxRetransmits: Int = 0, var maxPacketLifeTime: Int = 0) + +interface SignalingServer { + suspend fun sendCandidate(peerId: String, candidate: String) + suspend fun sendAnswer(peerId: String, answer: SessionDescription) +} + +// --- Placeholder Data and Logic --- + +fun getLocalBackupState(): BackupState { return BackupState("localNode", emptyMap()) } +fun getCredential(): String = "secret" +val localNodeId = "localNode123" + +data class BackupState(val nodeId: String, val state: Map) { + fun toProto(): ByteArray = this.toString().toByteArray() +} + +// --- Main P2PSyncManager Class --- + +class P2PSyncManager(private val signalingServer: SignalingServer) { + private val peerConnections = mutableMapOf() + private val dataChannels = mutableMapOf() + // private val chunkTransferManager = ChunkTransferManager() // Assuming this is another component + // private val logger = ... // Placeholder for a logger + + suspend fun initiatePeerSync(peerId: String, offer: SessionDescription) { + val connection = RTCPeerConnection( + RTCConfiguration().apply { + iceServers = listOf( + RTCIceServer("stun:stun.corestate.io:3478"), + RTCIceServer( + urls = listOf("turn:turn.corestate.io:3478"), + username = "corestate", + credential = getCredential() + ) + ) + } + ) + + connection.onIceCandidate = { candidate -> + // In a real app, you'd launch a coroutine to send this + // signalingServer.sendCandidate(peerId, candidate) + println("Sending ICE candidate to $peerId") + } + + connection.onDataChannel = { channel -> + setupDataChannel(peerId, channel) + } + + peerConnections[peerId] = connection + + val syncChannel = connection.createDataChannel( + "backup-sync", + RTCDataChannelInit().apply { + ordered = true + maxRetransmits = 3 + maxPacketLifeTime = 30000 // 30 seconds + } + ) + + setupDataChannel(peerId, syncChannel) + + connection.setRemoteDescription(offer) + val answer = connection.createAnswer() + connection.setLocalDescription(answer) + + signalingServer.sendAnswer(peerId, answer) + } + + private fun setupDataChannel(peerId: String, channel: RTCDataChannel) { + channel.onOpen = { + println("Data channel opened with peer: $peerId") + startSyncProtocol(peerId) + } + + channel.onMessage = { message -> + handleSyncMessage(peerId, message) + } + + channel.onError = { error -> + println("Data channel error with peer $peerId: $error") + reconnectToPeer(peerId) + } + + dataChannels[peerId] = channel + } + + private fun startSyncProtocol(peerId: String) { + val localState = getLocalBackupState() + // val syncRequest = SyncProtocol.SyncRequest.newBuilder() ... // Protobuf integration + val syncRequest = localState.toProto() + + dataChannels[peerId]?.sendMessage(syncRequest) + } + + private fun handleSyncMessage(peerId: String, message: ByteArray) { + println("Received sync message from $peerId: ${message.decodeToString()}") + } + + private fun reconnectToPeer(peerId: String) { + println("Attempting to reconnect to peer $peerId...") + } } \ No newline at end of file diff --git a/apps/daemon/Cargo.toml b/apps/daemon/Cargo.toml index d397d72..0729cac 100644 --- a/apps/daemon/Cargo.toml +++ b/apps/daemon/Cargo.toml @@ -1,12 +1,12 @@ -[package] -name = "corestate-daemon" -version = "2.0.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -# Dependencies will be added later -[[bin]] -name = "corestate-daemon" +[package] +name = "corestate-daemon" +version = "2.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# Dependencies will be added later +[[bin]] +name = "corestate-daemon" path = "src/main.rs" \ No newline at end of file diff --git a/apps/daemon/src/main.rs b/apps/daemon/src/main.rs index b169b15..d0cdaa4 100644 --- a/apps/daemon/src/main.rs +++ b/apps/daemon/src/main.rs @@ -1,5 +1,5 @@ -// CoreState Daemon Entry Point -fn main() { - println!("CoreState Daemon v2.0 starting..."); - // Initialization logic will go here +// CoreState Daemon Entry Point +fn main() { + println!("CoreState Daemon v2.0 starting..."); + // Initialization logic will go here } \ No newline at end of file diff --git a/apps/web-dashboard/package.json b/apps/web-dashboard/package.json deleted file mode 100644 index e75af16..0000000 --- a/apps/web-dashboard/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "corestate-web-dashboard", - "version": "2.0.0", - "private": true, - "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-scripts": "5.0.1" - }, - "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - } -} \ No newline at end of file diff --git a/apps/web-dashboard/public/index.html b/apps/web-dashboard/public/index.html deleted file mode 100644 index 455b925..0000000 --- a/apps/web-dashboard/public/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - CoreState Dashboard - - - -
- - \ No newline at end of file diff --git a/apps/web-dashboard/src/index.js b/apps/web-dashboard/src/index.js deleted file mode 100644 index f634378..0000000 --- a/apps/web-dashboard/src/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; - -const root = ReactDOM.createRoot(document.getElementById('root')); -root.render( - -

CoreState Web Dashboard v2.0

-
-); \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 3c35983..4a5cad8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ -// CoreState-v2/build.gradle.kts -plugins { - id("com.android.application") version "8.2.0" apply false - id("org.jetbrains.kotlin.android") version "1.9.0" apply false - id("org.springframework.boot") version "3.1.5" apply false - id("io.spring.dependency-management") version "1.1.3" apply false +// CoreState-v2/build.gradle.kts +plugins { + id("com.android.application") version "8.2.0" apply false + id("org.jetbrains.kotlin.android") version "1.9.0" apply false + id("org.springframework.boot") version "3.1.5" apply false + id("io.spring.dependency-management") version "1.1.3" apply false } \ No newline at end of file diff --git a/gradlew.bat b/gradlew.bat index db3a6ac..5eed7ee 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,94 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH= - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/infrastructure/kubernetes/deployments/backup-engine-deployment.yaml b/infrastructure/kubernetes/deployments/backup-engine-deployment.yaml index 8e5737b..b64c4e6 100644 --- a/infrastructure/kubernetes/deployments/backup-engine-deployment.yaml +++ b/infrastructure/kubernetes/deployments/backup-engine-deployment.yaml @@ -1,21 +1,134 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: backup-engine - labels: - app: backup-engine -spec: - replicas: 3 - selector: - matchLabels: - app: backup-engine - template: - metadata: - labels: - app: backup-engine - spec: - containers: - - name: backup-engine - image: ghcr.io/corestate/backup-engine:latest # Image will be updated by CI/CD - ports: - - containerPort: 8080 \ No newline at end of file +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backup-engine + namespace: corestate + labels: + app: backup-engine + component: core + version: v2.0.0 +spec: + replicas: 3 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + selector: + matchLabels: + app: backup-engine + template: + metadata: + labels: + app: backup-engine + component: core + version: v2.0.0 + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "8080" + prometheus.io/path: "/actuator/prometheus" + spec: + serviceAccountName: backup-engine + securityContext: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 2000 + containers: + - name: backup-engine + image: ghcr.io/corestate/backup-engine:latest + imagePullPolicy: Always + ports: + - name: http + containerPort: 8080 + protocol: TCP + - name: grpc + containerPort: 9090 + protocol: TCP + env: + - name: SPRING_PROFILES_ACTIVE + value: "kubernetes" + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: backup-engine-secrets + key: database-url + - name: REDIS_URL + valueFrom: + secretKeyRef: + name: backup-engine-secrets + key: redis-url + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: backup-engine-secrets + key: aws-access-key-id + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: backup-engine-secrets + key: aws-secret-access-key + - name: JVM_OPTS + value: "-Xmx2g -Xms1g -XX:+UseG1GC" + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: http + initialDelaySeconds: 120 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "2Gi" + cpu: "1000m" + volumeMounts: + - name: config + mountPath: /app/config + readOnly: true + - name: temp-storage + mountPath: /tmp + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumes: + - name: config + configMap: + name: backup-engine-config + - name: temp-storage + emptyDir: + sizeLimit: 1Gi + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - backup-engine + topologyKey: kubernetes.io/hostname + tolerations: + - key: "node.kubernetes.io/not-ready" + operator: "Exists" + effect: "NoExecute" + tolerationSeconds: 300 + - key: "node.kubernetes.io/unreachable" + operator: "Exists" + effect: "NoExecute" + tolerationSeconds: 300 \ No newline at end of file diff --git a/infrastructure/kubernetes/deployments/ml-optimizer-deployment.yaml b/infrastructure/kubernetes/deployments/ml-optimizer-deployment.yaml new file mode 100644 index 0000000..67898f9 --- /dev/null +++ b/infrastructure/kubernetes/deployments/ml-optimizer-deployment.yaml @@ -0,0 +1,206 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ml-optimizer + namespace: corestate + labels: + app: ml-optimizer + component: ml + version: v2.0.0 +spec: + replicas: 1 + strategy: + type: Recreate # ML models might need to be loaded sequentially + selector: + matchLabels: + app: ml-optimizer + template: + metadata: + labels: + app: ml-optimizer + component: ml + version: v2.0.0 + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "8082" + prometheus.io/path: "/metrics" + spec: + serviceAccountName: ml-optimizer + securityContext: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 2000 + containers: + - name: ml-optimizer + image: ghcr.io/corestate/ml-optimizer:latest + imagePullPolicy: Always + ports: + - name: http + containerPort: 8082 + protocol: TCP + env: + - name: PYTHONPATH + value: "/app" + - name: MODEL_PATH + value: "/app/models" + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: ml-optimizer-secrets + key: database-url + - name: REDIS_URL + valueFrom: + secretKeyRef: + name: ml-optimizer-secrets + key: redis-url + - name: MLFLOW_TRACKING_URI + valueFrom: + configMapKeyRef: + name: ml-optimizer-config + key: mlflow-uri + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /ready + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + requests: + memory: "2Gi" + cpu: "1000m" + limits: + memory: "4Gi" + cpu: "2000m" + volumeMounts: + - name: models + mountPath: /app/models + - name: config + mountPath: /app/config + readOnly: true + - name: temp-storage + mountPath: /tmp + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumes: + - name: models + persistentVolumeClaim: + claimName: ml-models-pvc + - name: config + configMap: + name: ml-optimizer-config + - name: temp-storage + emptyDir: + sizeLimit: 2Gi + nodeSelector: + node-type: ml-optimized # Schedule on ML-optimized nodes if available +--- +apiVersion: v1 +kind: Service +metadata: + name: ml-optimizer-svc + namespace: corestate + labels: + app: ml-optimizer + component: ml +spec: + selector: + app: ml-optimizer + ports: + - name: http + protocol: TCP + port: 8082 + targetPort: 8082 + type: ClusterIP +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ml-optimizer + namespace: corestate + labels: + app: ml-optimizer + component: ml +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: ml-optimizer-config + namespace: corestate + labels: + app: ml-optimizer + component: ml +data: + config.yaml: | + server: + host: "0.0.0.0" + port: 8082 + workers: 4 + + models: + anomaly_detection: + enabled: true + retrain_interval: "24h" + threshold: 0.8 + + backup_prediction: + enabled: true + retrain_interval: "168h" # Weekly + prediction_horizon: "24h" + + optimization: + enabled: true + update_interval: "1h" + + training: + batch_size: 1000 + max_epochs: 100 + early_stopping_patience: 10 + + logging: + level: INFO + format: json + + mlflow-uri: "http://mlflow:5000" +--- +apiVersion: v1 +kind: Secret +metadata: + name: ml-optimizer-secrets + namespace: corestate + labels: + app: ml-optimizer + component: ml +type: Opaque +stringData: + database-url: "postgresql://ml_user:password@postgres:5432/ml_db" + redis-url: "redis://redis:6379/1" +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: ml-models-pvc + namespace: corestate + labels: + app: ml-optimizer + component: ml +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: gp3 \ No newline at end of file diff --git a/infrastructure/kubernetes/deployments/storage-hal-deployment.yaml b/infrastructure/kubernetes/deployments/storage-hal-deployment.yaml new file mode 100644 index 0000000..e9a6575 --- /dev/null +++ b/infrastructure/kubernetes/deployments/storage-hal-deployment.yaml @@ -0,0 +1,217 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: storage-hal + namespace: corestate + labels: + app: storage-hal + component: storage + version: v2.0.0 +spec: + replicas: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app: storage-hal + template: + metadata: + labels: + app: storage-hal + component: storage + version: v2.0.0 + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "8081" + prometheus.io/path: "/metrics" + spec: + serviceAccountName: storage-hal + securityContext: + runAsNonRoot: true + runAsUser: 1000 + fsGroup: 2000 + containers: + - name: storage-hal + image: ghcr.io/corestate/storage-hal:latest + imagePullPolicy: Always + ports: + - name: grpc + containerPort: 8081 + protocol: TCP + - name: metrics + containerPort: 9091 + protocol: TCP + env: + - name: RUST_LOG + value: "storage_hal=info,tower=warn" + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: storage-hal-secrets + key: aws-access-key-id + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: storage-hal-secrets + key: aws-secret-access-key + - name: AZURE_STORAGE_ACCOUNT + valueFrom: + secretKeyRef: + name: storage-hal-secrets + key: azure-storage-account + - name: AZURE_STORAGE_KEY + valueFrom: + secretKeyRef: + name: storage-hal-secrets + key: azure-storage-key + - name: GCP_CREDENTIALS + valueFrom: + secretKeyRef: + name: storage-hal-secrets + key: gcp-credentials + livenessProbe: + exec: + command: + - /app/storage-hal + - --health-check + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + exec: + command: + - /app/storage-hal + - --health-check + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "500m" + volumeMounts: + - name: config + mountPath: /app/config + readOnly: true + - name: temp-storage + mountPath: /tmp + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumes: + - name: config + configMap: + name: storage-hal-config + - name: temp-storage + emptyDir: + sizeLimit: 500Mi + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - storage-hal + topologyKey: kubernetes.io/hostname +--- +apiVersion: v1 +kind: Service +metadata: + name: storage-hal-svc + namespace: corestate + labels: + app: storage-hal + component: storage +spec: + selector: + app: storage-hal + ports: + - name: grpc + protocol: TCP + port: 8081 + targetPort: 8081 + - name: metrics + protocol: TCP + port: 9091 + targetPort: 9091 + type: ClusterIP +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: storage-hal + namespace: corestate + labels: + app: storage-hal + component: storage + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT_ID:role/storage-hal-role +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: storage-hal-config + namespace: corestate + labels: + app: storage-hal + component: storage +data: + config.toml: | + [server] + host = "0.0.0.0" + port = 8081 + + [storage] + default_backend = "aws" + erasure_coding_enabled = true + encryption_enabled = true + + [aws] + region = "us-east-1" + bucket_name = "corestate-backups" + + [azure] + container_name = "corestate-backups" + + [gcp] + bucket_name = "corestate-backups" + project_id = "corestate-project" + + [metrics] + enabled = true + port = 9091 +--- +apiVersion: v1 +kind: Secret +metadata: + name: storage-hal-secrets + namespace: corestate + labels: + app: storage-hal + component: storage +type: Opaque +stringData: + aws-access-key-id: "PLACEHOLDER_ACCESS_KEY" + aws-secret-access-key: "PLACEHOLDER_SECRET_KEY" + azure-storage-account: "PLACEHOLDER_ACCOUNT" + azure-storage-key: "PLACEHOLDER_KEY" + gcp-credentials: | + { + "type": "service_account", + "project_id": "corestate-project" + } \ No newline at end of file diff --git a/infrastructure/kubernetes/ingress/main-ingress.yaml b/infrastructure/kubernetes/ingress/main-ingress.yaml index a35bde6..7fe7de5 100644 --- a/infrastructure/kubernetes/ingress/main-ingress.yaml +++ b/infrastructure/kubernetes/ingress/main-ingress.yaml @@ -1,17 +1,17 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: corestate-ingress - annotations: - nginx.ingress.kubernetes.io/rewrite-target: / -spec: - rules: - - http: - paths: - - path: /backup - pathType: Prefix - backend: - service: - name: backup-engine-svc - port: +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: corestate-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + rules: + - http: + paths: + - path: /backup + pathType: Prefix + backend: + service: + name: backup-engine-svc + port: number: 80 \ No newline at end of file diff --git a/infrastructure/kubernetes/services/backup-engine-service.yaml b/infrastructure/kubernetes/services/backup-engine-service.yaml index 9b35405..00b068f 100644 --- a/infrastructure/kubernetes/services/backup-engine-service.yaml +++ b/infrastructure/kubernetes/services/backup-engine-service.yaml @@ -1,12 +1,101 @@ -apiVersion: v1 -kind: Service -metadata: - name: backup-engine-svc -spec: - selector: - app: backup-engine - ports: - - protocol: TCP - port: 80 - targetPort: 8080 - type: ClusterIP \ No newline at end of file +apiVersion: v1 +kind: Service +metadata: + name: backup-engine-svc + namespace: corestate + labels: + app: backup-engine + component: core + annotations: + service.beta.kubernetes.io/aws-load-balancer-type: nlb + service.beta.kubernetes.io/aws-load-balancer-internal: "true" +spec: + selector: + app: backup-engine + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 8080 + - name: grpc + protocol: TCP + port: 9090 + targetPort: 9090 + type: ClusterIP +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: backup-engine + namespace: corestate + labels: + app: backup-engine + component: core + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT_ID:role/backup-engine-role +automountServiceAccountToken: true +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: backup-engine-config + namespace: corestate + labels: + app: backup-engine + component: core +data: + application.yml: | + server: + port: 8080 + management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus + endpoint: + health: + show-details: always + metrics: + export: + prometheus: + enabled: true + logging: + level: + com.corestate: INFO + org.springframework: WARN + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" + spring: + application: + name: backup-engine + jpa: + hibernate: + ddl-auto: validate + show-sql: false + datasource: + hikari: + maximum-pool-size: 20 + minimum-idle: 5 + connection-timeout: 30000 + redis: + timeout: 5000 + lettuce: + pool: + max-active: 20 + max-idle: 8 + min-idle: 2 +--- +apiVersion: v1 +kind: Secret +metadata: + name: backup-engine-secrets + namespace: corestate + labels: + app: backup-engine + component: core +type: Opaque +stringData: + database-url: "jdbc:postgresql://postgres:5432/corestate" + redis-url: "redis://redis:6379" + aws-access-key-id: "PLACEHOLDER_ACCESS_KEY" + aws-secret-access-key: "PLACEHOLDER_SECRET_KEY" \ No newline at end of file diff --git a/infrastructure/terraform/main.tf b/infrastructure/terraform/main.tf index 3190063..2690e98 100644 --- a/infrastructure/terraform/main.tf +++ b/infrastructure/terraform/main.tf @@ -1,63 +1,419 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 5.0" - } - } -} - -provider "aws" { - region = "us-east-1" -} - -resource "aws_vpc" "main" { - cidr_block = "10.0.0.0/16" - - tags = { - Name = "corestate-vpc" - } -} - -resource "aws_eks_cluster" "main" { - name = "corestate-eks-cluster" - role_arn = aws_iam_role.eks_cluster.arn - - vpc_config { - subnet_ids = aws_subnet.private[*].id - } - - depends_on = [ - aws_iam_role_policy_attachment.eks_cluster_policy, - ] -} - -# NOTE: This is a simplified placeholder. -# A real configuration would require definitions for IAM roles, -# subnets, node groups, etc. -# The following are placeholders for required resources. - -resource "aws_iam_role" "eks_cluster" { - name = "eks-cluster-role" - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "eks.amazonaws.com" - } - }] - }) -} - -resource "aws_iam_role_policy_attachment" "eks_cluster_policy" { - policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" - role = aws_iam_role.eks_cluster.name -} - -resource "aws_subnet" "private" { - count = 2 - vpc_id = aws_vpc.main.id - cidr_block = "10.0.${count.index}.0/24" +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.20" + } + helm = { + source = "hashicorp/helm" + version = "~> 2.10" + } + } + + backend "s3" { + bucket = "corestate-terraform-state" + key = "corestate/terraform.tfstate" + region = "us-east-1" + + dynamodb_table = "corestate-terraform-locks" + encrypt = true + } +} + +data "aws_availability_zones" "available" { + state = "available" +} + +data "aws_caller_identity" "current" {} + +locals { + cluster_name = "corestate-eks-cluster" + region = "us-east-1" + + tags = { + Project = "CoreState" + Environment = var.environment + ManagedBy = "Terraform" + } +} + +variable "environment" { + description = "Environment name" + type = string + default = "dev" +} + +variable "cluster_version" { + description = "Kubernetes cluster version" + type = string + default = "1.28" +} + +Provider "aws" { + region = local.region + + default_tags { + tags = local.tags + } +} + +# VPC Configuration +resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = merge(local.tags, { + Name = "${local.cluster_name}-vpc" + "kubernetes.io/cluster/${local.cluster_name}" = "shared" + }) +} + +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.main.id + + tags = merge(local.tags, { + Name = "${local.cluster_name}-igw" + }) +} + +resource "aws_subnet" "private" { + count = 3 + + vpc_id = aws_vpc.main.id + cidr_block = "10.0.${count.index + 1}.0/24" + availability_zone = data.aws_availability_zones.available.names[count.index] + + tags = merge(local.tags, { + Name = "${local.cluster_name}-private-${count.index + 1}" + "kubernetes.io/cluster/${local.cluster_name}" = "owned" + "kubernetes.io/role/internal-elb" = "1" + }) +} + +resource "aws_subnet" "public" { + count = 3 + + vpc_id = aws_vpc.main.id + cidr_block = "10.0.${count.index + 10}.0/24" + availability_zone = data.aws_availability_zones.available.names[count.index] + map_public_ip_on_launch = true + + tags = merge(local.tags, { + Name = "${local.cluster_name}-public-${count.index + 1}" + "kubernetes.io/cluster/${local.cluster_name}" = "owned" + "kubernetes.io/role/elb" = "1" + }) +} + +# NAT Gateways +resource "aws_eip" "nat" { + count = 3 + domain = "vpc" + + tags = merge(local.tags, { + Name = "${local.cluster_name}-nat-${count.index + 1}" + }) + + depends_on = [aws_internet_gateway.main] +} + +resource "aws_nat_gateway" "main" { + count = 3 + + allocation_id = aws_eip.nat[count.index].id + subnet_id = aws_subnet.public[count.index].id + + tags = merge(local.tags, { + Name = "${local.cluster_name}-nat-${count.index + 1}" + }) + + depends_on = [aws_internet_gateway.main] +} + +# Route Tables +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.main.id + } + + tags = merge(local.tags, { + Name = "${local.cluster_name}-public" + }) +} + +resource "aws_route_table" "private" { + count = 3 + + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.main[count.index].id + } + + tags = merge(local.tags, { + Name = "${local.cluster_name}-private-${count.index + 1}" + }) +} + +resource "aws_route_table_association" "public" { + count = 3 + + subnet_id = aws_subnet.public[count.index].id + route_table_id = aws_route_table.public.id +} + +resource "aws_route_table_association" "private" { + count = 3 + + subnet_id = aws_subnet.private[count.index].id + route_table_id = aws_route_table.private[count.index].id +} + +# Security Groups +resource "aws_security_group" "eks_cluster" { + name_prefix = "${local.cluster_name}-cluster-" + vpc_id = aws_vpc.main.id + + ingress { + description = "HTTPS" + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = [aws_vpc.main.cidr_block] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge(local.tags, { + Name = "${local.cluster_name}-cluster-sg" + }) +} + +resource "aws_security_group" "eks_nodes" { + name_prefix = "${local.cluster_name}-node-" + vpc_id = aws_vpc.main.id + + ingress { + description = "Cluster to node communication" + from_port = 0 + to_port = 65535 + protocol = "tcp" + security_groups = [aws_security_group.eks_cluster.id] + } + + ingress { + description = "Node to node communication" + from_port = 0 + to_port = 65535 + protocol = "tcp" + self = true + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge(local.tags, { + Name = "${local.cluster_name}-node-sg" + }) +} + +# EKS Cluster +resource "aws_eks_cluster" "main" { + name = local.cluster_name + role_arn = aws_iam_role.eks_cluster.arn + version = var.cluster_version + + vpc_config { + endpoint_private_access = true + endpoint_public_access = true + public_access_cidrs = ["0.0.0.0/0"] + + subnet_ids = concat( + aws_subnet.private[*].id, + aws_subnet.public[*].id + ) + + security_group_ids = [aws_security_group.eks_cluster.id] + } + + encryption_config { + provider { + key_arn = aws_kms_key.eks.arn + } + resources = ["secrets"] + } + + enabled_cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"] + + depends_on = [ + aws_iam_role_policy_attachment.eks_cluster_policy, + aws_cloudwatch_log_group.eks_cluster, + ] + + tags = local.tags +} + +# CloudWatch Log Group for EKS +resource "aws_cloudwatch_log_group" "eks_cluster" { + name = "/aws/eks/${local.cluster_name}/cluster" + retention_in_days = 7 + kms_key_id = aws_kms_key.eks.arn + + tags = local.tags +} + +# KMS Key for EKS encryption +resource "aws_kms_key" "eks" { + description = "EKS Secret Encryption Key" + deletion_window_in_days = 7 + enable_key_rotation = true + + tags = local.tags +} + +resource "aws_kms_alias" "eks" { + name = "alias/${local.cluster_name}-eks-encryption" + target_key_id = aws_kms_key.eks.key_id +} + +# IAM Roles and Policies +resource "aws_iam_role" "eks_cluster" { + name = "${local.cluster_name}-cluster-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "eks.amazonaws.com" + } + }] + }) + + tags = local.tags +} + +resource "aws_iam_role_policy_attachment" "eks_cluster_policy" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" + role = aws_iam_role.eks_cluster.name +} + +resource "aws_iam_role" "eks_node_group" { + name = "${local.cluster_name}-node-group-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ec2.amazonaws.com" + } + }] + }) + + tags = local.tags +} + +resource "aws_iam_role_policy_attachment" "eks_worker_node_policy" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" + role = aws_iam_role.eks_node_group.name +} + +resource "aws_iam_role_policy_attachment" "eks_cni_policy" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" + role = aws_iam_role.eks_node_group.name +} + +resource "aws_iam_role_policy_attachment" "eks_container_registry_policy" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + role = aws_iam_role.eks_node_group.name +} + +# EKS Node Groups +resource "aws_eks_node_group" "general" { + cluster_name = aws_eks_cluster.main.name + node_group_name = "general" + node_role_arn = aws_iam_role.eks_node_group.arn + subnet_ids = aws_subnet.private[*].id + + capacity_type = "ON_DEMAND" + instance_types = ["t3.medium"] + + scaling_config { + desired_size = 3 + max_size = 6 + min_size = 1 + } + + update_config { + max_unavailable = 1 + } + + # Ensure that IAM Role permissions are created before and deleted after EKS Node Group handling. + depends_on = [ + aws_iam_role_policy_attachment.eks_worker_node_policy, + aws_iam_role_policy_attachment.eks_cni_policy, + aws_iam_role_policy_attachment.eks_container_registry_policy, + ] + + tags = local.tags +} + +resource "aws_eks_node_group" "ml_optimized" { + cluster_name = aws_eks_cluster.main.name + node_group_name = "ml-optimized" + node_role_arn = aws_iam_role.eks_node_group.arn + subnet_ids = aws_subnet.private[*].id + + capacity_type = "SPOT" + instance_types = ["c5.2xlarge", "c5.4xlarge"] + + scaling_config { + desired_size = 1 + max_size = 3 + min_size = 0 + } + + update_config { + max_unavailable = 1 + } + + taint { + key = "node-type" + value = "ml-optimized" + effect = "NO_SCHEDULE" + } + + labels = { + "node-type" = "ml-optimized" + } + + depends_on = [ + aws_iam_role_policy_attachment.eks_worker_node_policy, + aws_iam_role_policy_attachment.eks_cni_policy, + aws_iam_role_policy_attachment.eks_container_registry_policy, + ] + + tags = local.tags } \ No newline at end of file diff --git a/ml/models/anomaly_detection/advanced_anomaly_detector.py b/ml/models/anomaly_detection/advanced_anomaly_detector.py new file mode 100644 index 0000000..e87597c --- /dev/null +++ b/ml/models/anomaly_detection/advanced_anomaly_detector.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +""" +Advanced Anomaly Detection System for CoreState Backup Platform + +This module implements multiple anomaly detection algorithms including: +- Variational Autoencoder for backup pattern anomalies +- Isolation Forest for outlier detection +- LSTM-based time series anomaly detection +- Statistical process control methods +""" + +import logging +import pickle +import warnings +from datetime import datetime, timedelta +from pathlib import Path +from typing import Dict, List, Optional, Tuple, Union + +import joblib +import numpy as np +import pandas as pd +from sklearn.ensemble import IsolationForest +from sklearn.preprocessing import StandardScaler, MinMaxScaler +from sklearn.model_selection import train_test_split +from sklearn.metrics import classification_report, confusion_matrix +import tensorflow as tf +from tensorflow.keras import layers, models, callbacks +from tensorflow.keras.losses import mse +from tensorflow.keras.optimizers import Adam + +warnings.filterwarnings('ignore', category=FutureWarning) +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class FeatureExtractor: + """Extract comprehensive features from backup metadata and system metrics""" + + def __init__(self): + self.scalers = {} + self.feature_names = [] + + def extract_backup_features(self, backup_metadata: Dict) -> np.ndarray: + """Extract features from backup metadata""" + features = [] + + # File-level features + features.extend([ + backup_metadata.get('file_count', 0), + backup_metadata.get('total_size_bytes', 0), + backup_metadata.get('compressed_size_bytes', 0), + backup_metadata.get('compression_ratio', 0), + backup_metadata.get('deduplication_ratio', 0), + backup_metadata.get('backup_duration_seconds', 0), + backup_metadata.get('throughput_mbps', 0), + ]) + + # Time-based features + timestamp = backup_metadata.get('timestamp', datetime.now()) + if isinstance(timestamp, str): + timestamp = pd.to_datetime(timestamp) + + features.extend([ + timestamp.hour, + timestamp.weekday(), + timestamp.day, + timestamp.month, + ]) + + # System resource features + features.extend([ + backup_metadata.get('cpu_usage_percent', 0), + backup_metadata.get('memory_usage_percent', 0), + backup_metadata.get('disk_io_rate', 0), + backup_metadata.get('network_utilization', 0), + ]) + + # Error and retry features + features.extend([ + backup_metadata.get('error_count', 0), + backup_metadata.get('retry_count', 0), + backup_metadata.get('checksum_failures', 0), + ]) + + # Data integrity features + features.extend([ + backup_metadata.get('file_type_diversity', 0), + backup_metadata.get('average_file_size', 0), + backup_metadata.get('modified_files_ratio', 0), + backup_metadata.get('new_files_ratio', 0), + ]) + + return np.array(features, dtype=np.float32) + + def extract_time_series_features(self, time_series_data: pd.DataFrame) -> np.ndarray: + """Extract features from time series backup data""" + # Statistical features + features = [ + time_series_data['backup_size'].mean(), + time_series_data['backup_size'].std(), + time_series_data['backup_size'].min(), + time_series_data['backup_size'].max(), + time_series_data['backup_duration'].mean(), + time_series_data['backup_duration'].std(), + time_series_data['backup_duration'].skew(), + time_series_data['backup_duration'].kurt(), + ] + + # Trend features + backup_sizes = time_series_data['backup_size'].values + if len(backup_sizes) > 1: + features.extend([ + np.polyfit(range(len(backup_sizes)), backup_sizes, 1)[0], # Linear trend + np.corrcoef(backup_sizes[:-1], backup_sizes[1:])[0, 1], # Autocorrelation + ]) + else: + features.extend([0, 0]) + + return np.array(features, dtype=np.float32) + + +class ComprehensiveAnomalyDetector: + """Main anomaly detection system combining multiple approaches""" + + def __init__(self): + self.feature_extractor = FeatureExtractor() + self.isolation_forest = None + self.ensemble_weights = {'isolation_forest': 1.0} + + def train(self, backup_data: List[Dict], time_series_data: Optional[pd.DataFrame] = None): + """Train all anomaly detection models""" + logger.info("Starting comprehensive anomaly detection training...") + + # Extract features + features = np.array([self.feature_extractor.extract_backup_features(data) for data in backup_data]) + + # Train Isolation Forest + logger.info("Training Isolation Forest...") + self.isolation_forest = IsolationForest( + contamination=0.1, + random_state=42, + n_estimators=200 + ) + self.isolation_forest.fit(features) + + logger.info("Anomaly detection training completed!") + + def detect_anomalies(self, backup_metadata: Dict, + time_series_data: Optional[pd.DataFrame] = None) -> Dict: + """Detect anomalies using ensemble approach""" + results = { + 'is_anomaly': False, + 'anomaly_score': 0.0, + 'component_scores': {}, + 'details': {} + } + + # Extract features + features = self.feature_extractor.extract_backup_features(backup_metadata) + features = features.reshape(1, -1) + + # Isolation Forest detection + if self.isolation_forest is not None: + if_prediction = self.isolation_forest.predict(features)[0] + if_score = self.isolation_forest.decision_function(features)[0] + + # Convert to 0-1 scale (negative values indicate anomalies) + if_normalized = max(0, 1 - (-if_score + 1) / 2) + results['component_scores']['isolation_forest'] = if_normalized + results['anomaly_score'] = if_normalized + results['is_anomaly'] = if_prediction == -1 + + # Add detailed analysis + results['details'] = { + 'timestamp': datetime.now().isoformat(), + 'backup_size': backup_metadata.get('total_size_bytes', 0), + 'compression_ratio': backup_metadata.get('compression_ratio', 0), + 'duration': backup_metadata.get('backup_duration_seconds', 0), + 'error_count': backup_metadata.get('error_count', 0) + } + + return results + + +if __name__ == "__main__": + # Example usage and testing + detector = ComprehensiveAnomalyDetector() + + # Generate sample training data + sample_data = [] + for i in range(1000): + sample_data.append({ + 'file_count': np.random.randint(100, 10000), + 'total_size_bytes': np.random.randint(1000000, 100000000), + 'compressed_size_bytes': np.random.randint(500000, 50000000), + 'compression_ratio': np.random.uniform(0.3, 0.8), + 'deduplication_ratio': np.random.uniform(0.1, 0.5), + 'backup_duration_seconds': np.random.randint(300, 7200), + 'throughput_mbps': np.random.uniform(10, 100), + 'timestamp': datetime.now() - timedelta(days=np.random.randint(0, 365)), + 'cpu_usage_percent': np.random.uniform(20, 80), + 'memory_usage_percent': np.random.uniform(30, 90), + 'disk_io_rate': np.random.uniform(0, 100), + 'network_utilization': np.random.uniform(0, 100), + 'error_count': np.random.randint(0, 5), + 'retry_count': np.random.randint(0, 3), + 'checksum_failures': np.random.randint(0, 2), + 'file_type_diversity': np.random.uniform(0, 1), + 'average_file_size': np.random.uniform(1000, 10000000), + 'modified_files_ratio': np.random.uniform(0, 1), + 'new_files_ratio': np.random.uniform(0, 0.5), + }) + + # Train the detector + detector.train(sample_data) + + # Test anomaly detection + test_metadata = sample_data[0] + result = detector.detect_anomalies(test_metadata) + + print(f"Anomaly Detection Result: {result}") + print("Advanced anomaly detection system initialized successfully!") \ No newline at end of file diff --git a/ml/models/anomaly_detection/anomaly_detector.py b/ml/models/anomaly_detection/anomaly_detector.py index 637b107..536c598 100644 --- a/ml/models/anomaly_detection/anomaly_detector.py +++ b/ml/models/anomaly_detection/anomaly_detector.py @@ -1,44 +1,44 @@ -import tensorflow as tf -from tensorflow.keras import layers, models -import numpy as np - -class AnomalyDetector: - def __init__(self): - self.autoencoder = self._build_autoencoder() - self.threshold = 0.05 # Example threshold, determined from validation data - - def _build_autoencoder(self): - """Autoencoder for detecting backup anomalies""" - print("Building Autoencoder model for anomaly detection...") - input_dim = 512 - encoding_dim = 32 - - input_layer = layers.Input(shape=(input_dim,)) - encoded = layers.Dense(256, activation='relu')(input_layer) - encoded = layers.Dense(128, activation='relu')(encoded) - encoded = layers.Dense(encoding_dim, activation='relu')(encoded) - - decoded = layers.Dense(128, activation='relu')(encoded) - decoded = layers.Dense(256, activation='relu')(decoded) - decoded = layers.Dense(input_dim, activation='sigmoid')(decoded) - - autoencoder = models.Model(input_layer, decoded) - autoencoder.compile(optimizer='adam', loss='mse') - print("Autoencoder compiled successfully.") - return autoencoder - - def _extract_features(self, backup_metadata): - # In a real scenario, this would create a feature vector from metadata - print("Extracting features from backup metadata...") - return np.random.rand(1, 512).astype(np.float32) - - def detect_corruption(self, backup_metadata): - """Detect potential data corruption in backups""" - features = self._extract_features(backup_metadata) - print("Detecting potential corruption...") - reconstruction = self.autoencoder.predict(features) - mse = np.mean(np.power(features - reconstruction, 2), axis=1) - - is_anomaly = mse[0] > self.threshold - print(f"Reconstruction error (MSE): {mse[0]:.6f}, Anomaly detected: {is_anomaly}") +import tensorflow as tf +from tensorflow.keras import layers, models +import numpy as np + +class AnomalyDetector: + def __init__(self): + self.autoencoder = self._build_autoencoder() + self.threshold = 0.05 # Example threshold, determined from validation data + + def _build_autoencoder(self): + """Autoencoder for detecting backup anomalies""" + print("Building Autoencoder model for anomaly detection...") + input_dim = 512 + encoding_dim = 32 + + input_layer = layers.Input(shape=(input_dim,)) + encoded = layers.Dense(256, activation='relu')(input_layer) + encoded = layers.Dense(128, activation='relu')(encoded) + encoded = layers.Dense(encoding_dim, activation='relu')(encoded) + + decoded = layers.Dense(128, activation='relu')(encoded) + decoded = layers.Dense(256, activation='relu')(decoded) + decoded = layers.Dense(input_dim, activation='sigmoid')(decoded) + + autoencoder = models.Model(input_layer, decoded) + autoencoder.compile(optimizer='adam', loss='mse') + print("Autoencoder compiled successfully.") + return autoencoder + + def _extract_features(self, backup_metadata): + # In a real scenario, this would create a feature vector from metadata + print("Extracting features from backup metadata...") + return np.random.rand(1, 512).astype(np.float32) + + def detect_corruption(self, backup_metadata): + """Detect potential data corruption in backups""" + features = self._extract_features(backup_metadata) + print("Detecting potential corruption...") + reconstruction = self.autoencoder.predict(features) + mse = np.mean(np.power(features - reconstruction, 2), axis=1) + + is_anomaly = mse[0] > self.threshold + print(f"Reconstruction error (MSE): {mse[0]:.6f}, Anomaly detected: {is_anomaly}") return is_anomaly \ No newline at end of file diff --git a/ml/models/backup_prediction/advanced_backup_predictor.py b/ml/models/backup_prediction/advanced_backup_predictor.py new file mode 100644 index 0000000..ef40780 --- /dev/null +++ b/ml/models/backup_prediction/advanced_backup_predictor.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +""" +Advanced Backup Prediction System for CoreState Platform + +This module implements sophisticated machine learning models for: +- Predicting optimal backup windows based on system patterns +- Forecasting backup duration and resource requirements +- Recommending backup strategies based on historical data +- Adaptive scheduling based on user behavior and system load +""" + +import logging +import warnings +from datetime import datetime, timedelta +from pathlib import Path +from typing import Dict, List, Optional, Tuple, Union + +import joblib +import numpy as np +import pandas as pd +from sklearn.preprocessing import StandardScaler, MinMaxScaler +from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor +from sklearn.model_selection import train_test_split, GridSearchCV +from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score + +warnings.filterwarnings('ignore', category=FutureWarning) +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class AdvancedFeatureExtractor: + """Extract comprehensive features for backup prediction""" + + def __init__(self): + self.scalers = {} + self.feature_columns = [] + + def extract_temporal_features(self, timestamps: pd.Series) -> pd.DataFrame: + """Extract time-based features""" + df = pd.DataFrame() + + df['hour'] = timestamps.dt.hour + df['day_of_week'] = timestamps.dt.dayofweek + df['day_of_month'] = timestamps.dt.day + df['month'] = timestamps.dt.month + df['quarter'] = timestamps.dt.quarter + df['is_weekend'] = (timestamps.dt.dayofweek >= 5).astype(int) + df['is_business_hours'] = ((timestamps.dt.hour >= 9) & (timestamps.dt.hour <= 17)).astype(int) + + # Cyclical encoding for time features + df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24) + df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24) + df['day_sin'] = np.sin(2 * np.pi * df['day_of_week'] / 7) + df['day_cos'] = np.cos(2 * np.pi * df['day_of_week'] / 7) + + return df + + +class OptimalWindowPredictor: + """Predict optimal backup windows using ensemble methods""" + + def __init__(self): + self.models = { + 'random_forest': RandomForestRegressor(n_estimators=200, random_state=42), + 'gradient_boosting': GradientBoostingRegressor(n_estimators=200, random_state=42) + } + self.feature_scaler = StandardScaler() + self.target_scaler = MinMaxScaler() + self.feature_importance = {} + + def prepare_training_data(self, historical_data: pd.DataFrame) -> Tuple[np.ndarray, np.ndarray]: + """Prepare training data for optimal window prediction""" + feature_extractor = AdvancedFeatureExtractor() + + # Extract features + temporal_features = feature_extractor.extract_temporal_features(historical_data['timestamp']) + + # Combine all features + X = temporal_features.copy() + X['system_load'] = historical_data.get('system_load', 0) + X['user_activity'] = historical_data.get('user_activity', 0) + X['backup_success_rate'] = historical_data.get('backup_success_rate', 1.0) + X['resource_availability'] = historical_data.get('resource_availability', 1.0) + + # Target: backup window quality score (0-1) + y = historical_data['window_quality_score'].values + + return X.values, y + + def train(self, historical_data: pd.DataFrame): + """Train the optimal window prediction models""" + logger.info("Training optimal backup window prediction models...") + + X, y = self.prepare_training_data(historical_data) + + # Scale features + X_scaled = self.feature_scaler.fit_transform(X) + y_scaled = self.target_scaler.fit_transform(y.reshape(-1, 1)).ravel() + + # Split data + X_train, X_test, y_train, y_test = train_test_split( + X_scaled, y_scaled, test_size=0.2, random_state=42 + ) + + # Train models + for name, model in self.models.items(): + logger.info(f"Training {name}...") + model.fit(X_train, y_train) + + # Evaluate + y_pred = model.predict(X_test) + mse = mean_squared_error(y_test, y_pred) + r2 = r2_score(y_test, y_pred) + + logger.info(f"{name} - MSE: {mse:.4f}, R2: {r2:.4f}") + + # Store feature importance for tree-based models + if hasattr(model, 'feature_importances_'): + self.feature_importance[name] = model.feature_importances_ + + logger.info("Optimal window prediction training completed!") + + def predict_optimal_windows(self, current_data: Dict, prediction_horizon: int = 24) -> Dict: + """Predict optimal backup windows for the next N hours""" + # Generate future timestamps + future_timestamps = pd.date_range( + start=datetime.now(), + periods=prediction_horizon, + freq='H' + ) + + predictions = [] + + for timestamp in future_timestamps: + # Create feature vector for this time point + features = [] + + # Temporal features + features.extend([ + timestamp.hour, + timestamp.weekday(), + timestamp.day, + timestamp.month, + timestamp.quarter, + 1 if timestamp.weekday() >= 5 else 0, # is_weekend + 1 if 9 <= timestamp.hour <= 17 else 0, # is_business_hours + np.sin(2 * np.pi * timestamp.hour / 24), # hour_sin + np.cos(2 * np.pi * timestamp.hour / 24), # hour_cos + np.sin(2 * np.pi * timestamp.weekday() / 7), # day_sin + np.cos(2 * np.pi * timestamp.weekday() / 7), # day_cos + ]) + + # Current system and user data (use provided or defaults) + features.extend([ + current_data.get('system_load', 0.5), + current_data.get('user_activity', 0.3), + current_data.get('backup_success_rate', 0.95), + current_data.get('resource_availability', 0.8), + ]) + + feature_vector = np.array(features).reshape(1, -1) + feature_vector_scaled = self.feature_scaler.transform(feature_vector) + + # Ensemble prediction + ensemble_pred = 0 + for model in self.models.values(): + pred = model.predict(feature_vector_scaled)[0] + ensemble_pred += pred + + ensemble_pred /= len(self.models) + + # Scale back to original range + final_pred = self.target_scaler.inverse_transform([[ensemble_pred]])[0][0] + + predictions.append({ + 'timestamp': timestamp.isoformat(), + 'hour': timestamp.hour, + 'quality_score': float(final_pred), + 'recommended': final_pred > 0.7 # Threshold for recommendation + }) + + # Find optimal windows (consecutive high-quality periods) + optimal_windows = self._find_optimal_windows(predictions) + + return { + 'predictions': predictions, + 'optimal_windows': optimal_windows, + 'best_window': optimal_windows[0] if optimal_windows else None, + 'prediction_horizon_hours': prediction_horizon + } + + def _find_optimal_windows(self, predictions: List[Dict], min_duration: int = 2) -> List[Dict]: + """Find consecutive high-quality backup windows""" + windows = [] + current_window = None + + for pred in predictions: + if pred['recommended']: + if current_window is None: + current_window = { + 'start_time': pred['timestamp'], + 'start_hour': pred['hour'], + 'scores': [pred['quality_score']] + } + else: + current_window['scores'].append(pred['quality_score']) + else: + if current_window is not None and len(current_window['scores']) >= min_duration: + current_window['end_time'] = predictions[predictions.index(pred) - 1]['timestamp'] + current_window['end_hour'] = predictions[predictions.index(pred) - 1]['hour'] + current_window['duration_hours'] = len(current_window['scores']) + current_window['avg_quality'] = np.mean(current_window['scores']) + windows.append(current_window) + current_window = None + + # Sort by quality score + windows.sort(key=lambda x: x['avg_quality'], reverse=True) + + return windows + + +class ComprehensiveBackupPredictor: + """Main backup prediction system""" + + def __init__(self): + self.window_predictor = OptimalWindowPredictor() + + def train(self, historical_data: pd.DataFrame): + """Train all prediction models""" + logger.info("Starting comprehensive backup prediction training...") + + # Train optimal window predictor + if 'window_quality_score' in historical_data.columns: + self.window_predictor.train(historical_data) + + logger.info("Backup prediction training completed!") + + def predict_optimal_backup_windows(self, current_data: Dict) -> Dict: + """Comprehensive backup prediction""" + window_predictions = self.window_predictor.predict_optimal_windows(current_data) + + # Add overall recommendation + window_predictions['recommendation'] = self._generate_recommendation( + window_predictions, current_data + ) + + return window_predictions + + def _generate_recommendation(self, predictions: Dict, current_data: Dict) -> Dict: + """Generate actionable backup recommendations""" + best_window = predictions.get('best_window') + + if not best_window: + return { + 'action': 'schedule_fallback', + 'message': 'No optimal windows found, use default schedule', + 'suggested_time': (datetime.now() + timedelta(hours=2)).isoformat() + } + + return { + 'action': 'schedule_backup', + 'message': f'Schedule backup for {best_window["start_time"]}', + 'scheduled_time': best_window['start_time'], + 'window_quality': best_window['avg_quality'] + } + + +if __name__ == "__main__": + # Example usage and testing + predictor = ComprehensiveBackupPredictor() + + # Generate sample training data + dates = pd.date_range(start='2023-01-01', end='2023-12-31', freq='H') + sample_data = pd.DataFrame({ + 'timestamp': dates, + 'system_load': np.random.beta(2, 3, len(dates)), + 'user_activity': np.random.beta(1, 4, len(dates)), + 'window_quality_score': np.random.beta(3, 2, len(dates)), + 'backup_success_rate': np.random.beta(9, 1, len(dates)), + 'resource_availability': np.random.beta(4, 2, len(dates)), + }) + + # Train the predictor + predictor.train(sample_data) + + # Test prediction + current_data = { + 'system_load': 0.4, + 'user_activity': 0.2, + 'backup_success_rate': 0.98, + 'resource_availability': 0.9, + } + + result = predictor.predict_optimal_backup_windows(current_data) + + print("Backup Prediction Results:") + print(f"Best window: {result.get('best_window', 'None')}") + print(f"Recommendation: {result.get('recommendation', {})}") + print("Advanced backup prediction system initialized successfully!") \ No newline at end of file diff --git a/ml/models/backup_prediction/backup_predictor.py b/ml/models/backup_prediction/backup_predictor.py index 0589587..4a5c7c7 100644 --- a/ml/models/backup_prediction/backup_predictor.py +++ b/ml/models/backup_prediction/backup_predictor.py @@ -1,47 +1,47 @@ -import tensorflow as tf -from tensorflow.keras import layers, models -import numpy as np - -class FeatureExtractor: - def extract(self, user_patterns, system_load): - # In a real scenario, this would process real data. - # For now, we return a correctly shaped random tensor. - print("Extracting features from user patterns and system load...") - return np.random.rand(1, 168, 15).astype(np.float32) - -class BackupPredictor: - def __init__(self): - self.model = self._build_model() - self.feature_extractor = FeatureExtractor() - - def _build_model(self): - """LSTM-based model for predicting optimal backup times""" - print("Building LSTM model for backup prediction...") - model = models.Sequential([ - layers.LSTM(128, return_sequences=True, input_shape=(168, 15)), - layers.Dropout(0.2), - layers.LSTM(64, return_sequences=True), - layers.Dropout(0.2), - layers.LSTM(32), - layers.Dense(64, activation='relu'), - layers.Dense(24, activation='sigmoid') - ]) - - model.compile( - optimizer='adam', - loss='binary_crossentropy', - metrics=['accuracy', tf.keras.metrics.Precision(), tf.keras.metrics.Recall()] - ) - print("Model compiled successfully.") - return model - - def _post_process_predictions(self, predictions): - # Example: Return hours where prediction > 0.7 - optimal_hours = np.where(predictions[0] > 0.7)[0] - return {"optimal_hours": optimal_hours.tolist(), "raw_predictions": predictions.tolist()} - - def predict_optimal_backup_windows(self, user_patterns, system_load): - features = self.feature_extractor.extract(user_patterns, system_load) - print("Predicting optimal backup windows...") - predictions = self.model.predict(features) +import tensorflow as tf +from tensorflow.keras import layers, models +import numpy as np + +class FeatureExtractor: + def extract(self, user_patterns, system_load): + # In a real scenario, this would process real data. + # For now, we return a correctly shaped random tensor. + print("Extracting features from user patterns and system load...") + return np.random.rand(1, 168, 15).astype(np.float32) + +class BackupPredictor: + def __init__(self): + self.model = self._build_model() + self.feature_extractor = FeatureExtractor() + + def _build_model(self): + """LSTM-based model for predicting optimal backup times""" + print("Building LSTM model for backup prediction...") + model = models.Sequential([ + layers.LSTM(128, return_sequences=True, input_shape=(168, 15)), + layers.Dropout(0.2), + layers.LSTM(64, return_sequences=True), + layers.Dropout(0.2), + layers.LSTM(32), + layers.Dense(64, activation='relu'), + layers.Dense(24, activation='sigmoid') + ]) + + model.compile( + optimizer='adam', + loss='binary_crossentropy', + metrics=['accuracy', tf.keras.metrics.Precision(), tf.keras.metrics.Recall()] + ) + print("Model compiled successfully.") + return model + + def _post_process_predictions(self, predictions): + # Example: Return hours where prediction > 0.7 + optimal_hours = np.where(predictions[0] > 0.7)[0] + return {"optimal_hours": optimal_hours.tolist(), "raw_predictions": predictions.tolist()} + + def predict_optimal_backup_windows(self, user_patterns, system_load): + features = self.feature_extractor.extract(user_patterns, system_load) + print("Predicting optimal backup windows...") + predictions = self.model.predict(features) return self._post_process_predictions(predictions) \ No newline at end of file diff --git a/module/module.prop b/module/module.prop index 5c17632..ac2e4b7 100644 --- a/module/module.prop +++ b/module/module.prop @@ -1,6 +1,6 @@ -id=corestate_v2 -name=CoreState Module v2 -version=v2.0.0 -versionCode=2 -author=Wiktor/overspend1 +id=corestate_v2 +name=CoreState Module v2 +version=v2.0.0 +versionCode=2 +author=Wiktor/overspend1 description=Enhanced system-level operations for CoreState v2.0, including snapshotting and real-time monitoring. \ No newline at end of file diff --git a/module/native/CMakeLists.txt b/module/native/CMakeLists.txt index 3bf3bc4..39eea68 100644 --- a/module/native/CMakeLists.txt +++ b/module/native/CMakeLists.txt @@ -1,15 +1,15 @@ -cmake_minimum_required(VERSION 3.10) - -project(CoreStateModuleNative) - -# Set standard to C++17 -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Add subdirectories for each native component -add_subdirectory(snapshot_manager) -add_subdirectory(fs_monitor) -add_subdirectory(hw_acceleration) - -# Example of creating a shared library (will be expanded later) +cmake_minimum_required(VERSION 3.10) + +project(CoreStateModuleNative) + +# Set standard to C++17 +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add subdirectories for each native component +add_subdirectory(snapshot_manager) +add_subdirectory(fs_monitor) +add_subdirectory(hw_acceleration) + +# Example of creating a shared library (will be expanded later) # add_library(corestate_native SHARED ...) \ No newline at end of file diff --git a/module/native/fs_monitor/block_tracker.cpp b/module/native/fs_monitor/block_tracker.cpp index 4b49651..642ad61 100644 --- a/module/native/fs_monitor/block_tracker.cpp +++ b/module/native/fs_monitor/block_tracker.cpp @@ -1,87 +1,87 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -// --- Placeholder Implementations and Stubs --- - -// A mock B+ Tree implementation for compilation -template -class BPlusTree { -public: - void insert(const K& key, const V& value) {} - std::vector range_query(std::function predicate) { - return {}; - } -}; - -// Mock system/utility functions -uint64_t get_current_timestamp() { - return std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch() - ).count(); -} - -uint32_t calculate_crc32(const void* data, size_t size) { - return 0; // Placeholder -} - -void trigger_incremental_backup() { - std::cout << "Incremental backup triggered!" << std::endl; -} - -// --- Main BlockLevelTracker Class --- - -class BlockLevelTracker { -private: - struct BlockInfo { - uint64_t block_number; - uint64_t last_modified; - uint32_t checksum; - bool is_dirty; - }; - - mutable std::shared_mutex tracker_mutex; - std::unordered_map block_map; - std::unique_ptr> block_index; - - size_t dirty_block_count = 0; - const size_t incremental_threshold = 1000; // Trigger after 1000 dirty blocks - -public: - BlockLevelTracker() : block_index(std::make_unique>()) {} - - void track_write(uint64_t block_num, const void* data, size_t size) { - std::unique_lock lock(tracker_mutex); - - BlockInfo& info = block_map[block_num]; - if (!info.is_dirty) { - dirty_block_count++; - } - - info.block_number = block_num; - info.last_modified = get_current_timestamp(); - info.checksum = calculate_crc32(data, size); - info.is_dirty = true; - - block_index->insert(block_num, info); - - if (dirty_block_count > incremental_threshold) { - trigger_incremental_backup(); - dirty_block_count = 0; // Reset counter - } - } - - std::vector get_dirty_blocks(uint64_t since_timestamp) { - std::shared_lock lock(tracker_mutex); - return block_index->range_query( - [since_timestamp](const BlockInfo& info) { - return info.last_modified > since_timestamp && info.is_dirty; - } - ); - } +#include +#include +#include +#include +#include +#include +#include +#include + +// --- Placeholder Implementations and Stubs --- + +// A mock B+ Tree implementation for compilation +template +class BPlusTree { +public: + void insert(const K& key, const V& value) {} + std::vector range_query(std::function predicate) { + return {}; + } +}; + +// Mock system/utility functions +uint64_t get_current_timestamp() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() + ).count(); +} + +uint32_t calculate_crc32(const void* data, size_t size) { + return 0; // Placeholder +} + +void trigger_incremental_backup() { + std::cout << "Incremental backup triggered!" << std::endl; +} + +// --- Main BlockLevelTracker Class --- + +class BlockLevelTracker { +private: + struct BlockInfo { + uint64_t block_number; + uint64_t last_modified; + uint32_t checksum; + bool is_dirty; + }; + + mutable std::shared_mutex tracker_mutex; + std::unordered_map block_map; + std::unique_ptr> block_index; + + size_t dirty_block_count = 0; + const size_t incremental_threshold = 1000; // Trigger after 1000 dirty blocks + +public: + BlockLevelTracker() : block_index(std::make_unique>()) {} + + void track_write(uint64_t block_num, const void* data, size_t size) { + std::unique_lock lock(tracker_mutex); + + BlockInfo& info = block_map[block_num]; + if (!info.is_dirty) { + dirty_block_count++; + } + + info.block_number = block_num; + info.last_modified = get_current_timestamp(); + info.checksum = calculate_crc32(data, size); + info.is_dirty = true; + + block_index->insert(block_num, info); + + if (dirty_block_count > incremental_threshold) { + trigger_incremental_backup(); + dirty_block_count = 0; // Reset counter + } + } + + std::vector get_dirty_blocks(uint64_t since_timestamp) { + std::shared_lock lock(tracker_mutex); + return block_index->range_query( + [since_timestamp](const BlockInfo& info) { + return info.last_modified > since_timestamp && info.is_dirty; + } + ); + } }; \ No newline at end of file diff --git a/module/native/hw_acceleration/hsm_integration.cpp b/module/native/hw_acceleration/hsm_integration.cpp index e3b0e9a..7d768d1 100644 --- a/module/native/hw_acceleration/hsm_integration.cpp +++ b/module/native/hw_acceleration/hsm_integration.cpp @@ -1,158 +1,158 @@ -#include -#include -#include -#include -#include - -// --- Placeholder PKCS#11 API Definitions --- -// These would be provided by the actual PKCS#11 header (pkcs11.h) - -using CK_RV = unsigned long; -using CK_SESSION_HANDLE = unsigned long; -using CK_OBJECT_HANDLE = unsigned long; -using CK_MECHANISM_TYPE = unsigned long; - -#define CKR_OK 0 -#define CKM_SHA256_HMAC_GENERAL 0x1051 // Example value -#define CKA_CLASS 0x0000 -#define CKA_KEY_TYPE 0x0100 -#define CKA_DERIVE 0x0104 -#define CKA_SENSITIVE 0x0103 -#define CKA_EXTRACTABLE 0x0102 - -struct CK_MECHANISM { - CK_MECHANISM_TYPE mechanism; - void* pParameter; - unsigned long ulParameterLen; -}; - -struct CK_ATTRIBUTE { - unsigned long type; - void* pValue; - unsigned long ulValueLen; -}; - -// Mock PKCS#11 functions -CK_RV C_DeriveKey(CK_SESSION_HANDLE hSession, CK_MECHANISM* pMechanism, CK_OBJECT_HANDLE hBaseKey, CK_ATTRIBUTE* pTemplate, unsigned long ulAttributeCount, CK_OBJECT_HANDLE* phKey) { - *phKey = 12345; // Return a dummy handle - return CKR_OK; -} -CK_RV C_DestroyObject(CK_SESSION_HANDLE hSession, CK_OBJECT_HANDLE hObject) { return CKR_OK; } -CK_RV C_EncryptInit(CK_SESSION_HANDLE hSession, CK_MECHANISM* pMechanism, CK_OBJECT_HANDLE hKey) { return CKR_OK; } -CK_RV C_Encrypt(CK_SESSION_HANDLE hSession, unsigned char* pData, unsigned long ulDataLen, unsigned char* pEncryptedData, unsigned long* pulEncryptedDataLen) { - *pulEncryptedDataLen = ulDataLen; - // "Encrypt" by XORing with a dummy key - for(unsigned long i = 0; i < ulDataLen; ++i) pEncryptedData[i] = pData[i] ^ 0xAB; - return CKR_OK; -} - -class HSMException : public std::exception { -public: - HSMException(const char* msg) : message(msg) {} - const char* what() const noexcept override { return message; } -private: - const char* message; -}; - -// --- Main HSMIntegration Class --- - -class HSMIntegration { -private: - // PKCS11_CTX* pkcs11_ctx; // This would be a context from a real library - CK_SESSION_HANDLE session = 1; // Mock session handle - std::mutex hsm_mutex; - -public: - class MasterKeyManager { - CK_OBJECT_HANDLE master_key_handle = 100; // Mock master key handle - HSMIntegration& parent; - - public: - MasterKeyManager(HSMIntegration& p) : parent(p) {} - - std::vector derive_backup_key(const std::string& backup_id) { - std::lock_guard lock(parent.hsm_mutex); - - CK_MECHANISM mechanism = { - CKM_SHA256_HMAC_GENERAL, - (void*)backup_id.c_str(), - (unsigned long)backup_id.length() - }; - - CK_OBJECT_HANDLE derived_key; - // Dummy attributes - unsigned long key_class, key_type; - bool true_val = true, false_val = false; - CK_ATTRIBUTE key_template[] = { - {CKA_CLASS, &key_class, sizeof(key_class)}, - {CKA_KEY_TYPE, &key_type, sizeof(key_type)}, - {CKA_DERIVE, &true_val, sizeof(true_val)}, - {CKA_SENSITIVE, &true_val, sizeof(true_val)}, - {CKA_EXTRACTABLE, &false_val, sizeof(false_val)} - }; - - CK_RV rv = C_DeriveKey( - parent.session, &mechanism, master_key_handle, - key_template, 5, &derived_key - ); - - if (rv != CKR_OK) { - throw HSMException("Failed to derive backup key"); - } - - // In a real scenario, you'd return a wrapped/encrypted key handle - std::vector key_handle_bytes(sizeof(derived_key)); - memcpy(key_handle_bytes.data(), &derived_key, sizeof(derived_key)); - return key_handle_bytes; - } - - void rotate_master_key() { - std::lock_guard lock(parent.hsm_mutex); - CK_OBJECT_HANDLE new_master_key = 200; // Generate new mock key - // reencrypt_all_keys(master_key_handle, new_master_key); // Placeholder - CK_OBJECT_HANDLE old_key = master_key_handle; - master_key_handle = new_master_key; - C_DestroyObject(parent.session, old_key); - } - }; - - class CryptoAccelerator { - HSMIntegration& parent; - public: - struct AESContext { - CK_OBJECT_HANDLE key_handle; - CK_MECHANISM_TYPE mechanism; - std::vector iv; - }; - - CryptoAccelerator(HSMIntegration& p) : parent(p) {} - - std::future> encrypt_async( - const std::vector& data, - const AESContext& context - ) { - return std::async(std::launch::async, [this, data, context]() { - std::lock_guard lock(parent.hsm_mutex); - - CK_MECHANISM mechanism = { - context.mechanism, - (void*)context.iv.data(), - (unsigned long)context.iv.size() - }; - - C_EncryptInit(parent.session, &mechanism, context.key_handle); - - unsigned long encrypted_len = (unsigned long)data.size() + 16; - std::vector encrypted(encrypted_len); - - C_Encrypt( - parent.session, (unsigned char*)data.data(), (unsigned long)data.size(), - encrypted.data(), &encrypted_len - ); - - encrypted.resize(encrypted_len); - return encrypted; - }); - } - }; +#include +#include +#include +#include +#include + +// --- Placeholder PKCS#11 API Definitions --- +// These would be provided by the actual PKCS#11 header (pkcs11.h) + +using CK_RV = unsigned long; +using CK_SESSION_HANDLE = unsigned long; +using CK_OBJECT_HANDLE = unsigned long; +using CK_MECHANISM_TYPE = unsigned long; + +#define CKR_OK 0 +#define CKM_SHA256_HMAC_GENERAL 0x1051 // Example value +#define CKA_CLASS 0x0000 +#define CKA_KEY_TYPE 0x0100 +#define CKA_DERIVE 0x0104 +#define CKA_SENSITIVE 0x0103 +#define CKA_EXTRACTABLE 0x0102 + +struct CK_MECHANISM { + CK_MECHANISM_TYPE mechanism; + void* pParameter; + unsigned long ulParameterLen; +}; + +struct CK_ATTRIBUTE { + unsigned long type; + void* pValue; + unsigned long ulValueLen; +}; + +// Mock PKCS#11 functions +CK_RV C_DeriveKey(CK_SESSION_HANDLE hSession, CK_MECHANISM* pMechanism, CK_OBJECT_HANDLE hBaseKey, CK_ATTRIBUTE* pTemplate, unsigned long ulAttributeCount, CK_OBJECT_HANDLE* phKey) { + *phKey = 12345; // Return a dummy handle + return CKR_OK; +} +CK_RV C_DestroyObject(CK_SESSION_HANDLE hSession, CK_OBJECT_HANDLE hObject) { return CKR_OK; } +CK_RV C_EncryptInit(CK_SESSION_HANDLE hSession, CK_MECHANISM* pMechanism, CK_OBJECT_HANDLE hKey) { return CKR_OK; } +CK_RV C_Encrypt(CK_SESSION_HANDLE hSession, unsigned char* pData, unsigned long ulDataLen, unsigned char* pEncryptedData, unsigned long* pulEncryptedDataLen) { + *pulEncryptedDataLen = ulDataLen; + // "Encrypt" by XORing with a dummy key + for(unsigned long i = 0; i < ulDataLen; ++i) pEncryptedData[i] = pData[i] ^ 0xAB; + return CKR_OK; +} + +class HSMException : public std::exception { +public: + HSMException(const char* msg) : message(msg) {} + const char* what() const noexcept override { return message; } +private: + const char* message; +}; + +// --- Main HSMIntegration Class --- + +class HSMIntegration { +private: + // PKCS11_CTX* pkcs11_ctx; // This would be a context from a real library + CK_SESSION_HANDLE session = 1; // Mock session handle + std::mutex hsm_mutex; + +public: + class MasterKeyManager { + CK_OBJECT_HANDLE master_key_handle = 100; // Mock master key handle + HSMIntegration& parent; + + public: + MasterKeyManager(HSMIntegration& p) : parent(p) {} + + std::vector derive_backup_key(const std::string& backup_id) { + std::lock_guard lock(parent.hsm_mutex); + + CK_MECHANISM mechanism = { + CKM_SHA256_HMAC_GENERAL, + (void*)backup_id.c_str(), + (unsigned long)backup_id.length() + }; + + CK_OBJECT_HANDLE derived_key; + // Dummy attributes + unsigned long key_class, key_type; + bool true_val = true, false_val = false; + CK_ATTRIBUTE key_template[] = { + {CKA_CLASS, &key_class, sizeof(key_class)}, + {CKA_KEY_TYPE, &key_type, sizeof(key_type)}, + {CKA_DERIVE, &true_val, sizeof(true_val)}, + {CKA_SENSITIVE, &true_val, sizeof(true_val)}, + {CKA_EXTRACTABLE, &false_val, sizeof(false_val)} + }; + + CK_RV rv = C_DeriveKey( + parent.session, &mechanism, master_key_handle, + key_template, 5, &derived_key + ); + + if (rv != CKR_OK) { + throw HSMException("Failed to derive backup key"); + } + + // In a real scenario, you'd return a wrapped/encrypted key handle + std::vector key_handle_bytes(sizeof(derived_key)); + memcpy(key_handle_bytes.data(), &derived_key, sizeof(derived_key)); + return key_handle_bytes; + } + + void rotate_master_key() { + std::lock_guard lock(parent.hsm_mutex); + CK_OBJECT_HANDLE new_master_key = 200; // Generate new mock key + // reencrypt_all_keys(master_key_handle, new_master_key); // Placeholder + CK_OBJECT_HANDLE old_key = master_key_handle; + master_key_handle = new_master_key; + C_DestroyObject(parent.session, old_key); + } + }; + + class CryptoAccelerator { + HSMIntegration& parent; + public: + struct AESContext { + CK_OBJECT_HANDLE key_handle; + CK_MECHANISM_TYPE mechanism; + std::vector iv; + }; + + CryptoAccelerator(HSMIntegration& p) : parent(p) {} + + std::future> encrypt_async( + const std::vector& data, + const AESContext& context + ) { + return std::async(std::launch::async, [this, data, context]() { + std::lock_guard lock(parent.hsm_mutex); + + CK_MECHANISM mechanism = { + context.mechanism, + (void*)context.iv.data(), + (unsigned long)context.iv.size() + }; + + C_EncryptInit(parent.session, &mechanism, context.key_handle); + + unsigned long encrypted_len = (unsigned long)data.size() + 16; + std::vector encrypted(encrypted_len); + + C_Encrypt( + parent.session, (unsigned char*)data.data(), (unsigned long)data.size(), + encrypted.data(), &encrypted_len + ); + + encrypted.resize(encrypted_len); + return encrypted; + }); + } + }; }; \ No newline at end of file diff --git a/module/native/snapshot_manager/cow_snapshot.cpp b/module/native/snapshot_manager/cow_snapshot.cpp index 3d4d180..c90fdd2 100644 --- a/module/native/snapshot_manager/cow_snapshot.cpp +++ b/module/native/snapshot_manager/cow_snapshot.cpp @@ -1,123 +1,123 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// --- Placeholder Linux Headers and System Call Stubs --- -// These would be replaced by actual kernel headers on a Linux build environment. - -#define DM_DEV_CREATE 0 // Placeholder for ioctl command -struct dm_ioctl { - int target_count; - // other fields... -}; -struct dm_target_spec { - long long sector_start; - long long length; - int status; - char target_type[256]; -}; - -// Mock system functions for compilation -int ioctl(int fd, unsigned long request, ...) { - std::cout << "Mock ioctl called" << std::endl; - return 0; -} -long long get_device_size(const std::string& device) { return 1024 * 1024 * 1024; /* 1GB */ } -std::string create_cow_device(const std::string& name) { return "/dev/cow_" + name; } -dm_ioctl* prepare_dm_ioctl(const std::string& name) { return new dm_ioctl(); } -dm_target_spec* get_dm_target(dm_ioctl* io) { return new dm_target_spec(); } -char* get_target_params(dm_target_spec* tgt) { return new char[1024]; } -uint64_t calculate_cow_usage(const auto& snapshot) { return 0; } -void merge_old_chunks(const auto& snapshot) {} - -// --- Class Implementations --- - -class BitmapAllocator { -public: - uint64_t find_and_set_first_zero() { return 0; } -}; - -class COWSnapshotManager { -private: - struct ChunkMapping {}; - struct SnapshotMetadata { - uint64_t origin_size; - uint64_t chunk_size; - std::vector mappings; - std::atomic write_counter; - }; - - class ChunkAllocator { - std::unique_ptr allocator; - std::mutex allocation_mutex; - - public: - ChunkAllocator() : allocator(std::make_unique()) {} - uint64_t allocate_chunk() { - std::lock_guard lock(allocation_mutex); - return allocator->find_and_set_first_zero(); - } - }; - - int dm_fd = 0; // Mock device-mapper file descriptor - bool monitoring = false; - uint64_t threshold = 1000; - std::unordered_map active_snapshots; - std::thread monitor_thread; - -public: - ~COWSnapshotManager() { - monitoring = false; - if (monitor_thread.joinable()) { - monitor_thread.join(); - } - } - - int create_snapshot(const std::string& origin_device, - const std::string& snapshot_name) { - dm_ioctl* io = prepare_dm_ioctl(snapshot_name); - io->target_count = 1; - - dm_target_spec* tgt = get_dm_target(io); - tgt->status = 0; - tgt->sector_start = 0; - tgt->length = get_device_size(origin_device); - strcpy(tgt->target_type, "snapshot"); - - std::string cow_device = create_cow_device(snapshot_name); - sprintf(get_target_params(tgt), "%s %s P 8", - origin_device.c_str(), cow_device.c_str()); - - int result = ioctl(dm_fd, DM_DEV_CREATE, io); - - // Cleanup mock objects - delete[] get_target_params(tgt); - delete tgt; - delete io; - - return result; - } - - void start_monitoring() { - monitoring = true; - monitor_thread = std::thread(&COWSnapshotManager::monitor_cow_usage, this); - } - - void monitor_cow_usage() { - while (monitoring) { - for (auto& [name, snapshot] : active_snapshots) { - uint64_t usage = calculate_cow_usage(snapshot); - if (usage > threshold) { - merge_old_chunks(snapshot); - } - } - std::this_thread::sleep_for(std::chrono::seconds(30)); - } - } +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// --- Placeholder Linux Headers and System Call Stubs --- +// These would be replaced by actual kernel headers on a Linux build environment. + +#define DM_DEV_CREATE 0 // Placeholder for ioctl command +struct dm_ioctl { + int target_count; + // other fields... +}; +struct dm_target_spec { + long long sector_start; + long long length; + int status; + char target_type[256]; +}; + +// Mock system functions for compilation +int ioctl(int fd, unsigned long request, ...) { + std::cout << "Mock ioctl called" << std::endl; + return 0; +} +long long get_device_size(const std::string& device) { return 1024 * 1024 * 1024; /* 1GB */ } +std::string create_cow_device(const std::string& name) { return "/dev/cow_" + name; } +dm_ioctl* prepare_dm_ioctl(const std::string& name) { return new dm_ioctl(); } +dm_target_spec* get_dm_target(dm_ioctl* io) { return new dm_target_spec(); } +char* get_target_params(dm_target_spec* tgt) { return new char[1024]; } +uint64_t calculate_cow_usage(const auto& snapshot) { return 0; } +void merge_old_chunks(const auto& snapshot) {} + +// --- Class Implementations --- + +class BitmapAllocator { +public: + uint64_t find_and_set_first_zero() { return 0; } +}; + +class COWSnapshotManager { +private: + struct ChunkMapping {}; + struct SnapshotMetadata { + uint64_t origin_size; + uint64_t chunk_size; + std::vector mappings; + std::atomic write_counter; + }; + + class ChunkAllocator { + std::unique_ptr allocator; + std::mutex allocation_mutex; + + public: + ChunkAllocator() : allocator(std::make_unique()) {} + uint64_t allocate_chunk() { + std::lock_guard lock(allocation_mutex); + return allocator->find_and_set_first_zero(); + } + }; + + int dm_fd = 0; // Mock device-mapper file descriptor + bool monitoring = false; + uint64_t threshold = 1000; + std::unordered_map active_snapshots; + std::thread monitor_thread; + +public: + ~COWSnapshotManager() { + monitoring = false; + if (monitor_thread.joinable()) { + monitor_thread.join(); + } + } + + int create_snapshot(const std::string& origin_device, + const std::string& snapshot_name) { + dm_ioctl* io = prepare_dm_ioctl(snapshot_name); + io->target_count = 1; + + dm_target_spec* tgt = get_dm_target(io); + tgt->status = 0; + tgt->sector_start = 0; + tgt->length = get_device_size(origin_device); + strcpy(tgt->target_type, "snapshot"); + + std::string cow_device = create_cow_device(snapshot_name); + sprintf(get_target_params(tgt), "%s %s P 8", + origin_device.c_str(), cow_device.c_str()); + + int result = ioctl(dm_fd, DM_DEV_CREATE, io); + + // Cleanup mock objects + delete[] get_target_params(tgt); + delete tgt; + delete io; + + return result; + } + + void start_monitoring() { + monitoring = true; + monitor_thread = std::thread(&COWSnapshotManager::monitor_cow_usage, this); + } + + void monitor_cow_usage() { + while (monitoring) { + for (auto& [name, snapshot] : active_snapshots) { + uint64_t usage = calculate_cow_usage(snapshot); + if (usage > threshold) { + merge_old_chunks(snapshot); + } + } + std::this_thread::sleep_for(std::chrono::seconds(30)); + } + } }; \ No newline at end of file diff --git a/services/analytics-engine/build.sbt b/services/analytics-engine/build.sbt index 7d224ce..e77fcb0 100644 --- a/services/analytics-engine/build.sbt +++ b/services/analytics-engine/build.sbt @@ -1,10 +1,71 @@ -name := "analytics-engine" -version := "2.0.0" -scalaVersion := "2.12.15" - -libraryDependencies ++= Seq( - "org.apache.spark" %% "spark-sql" % "3.3.1", - "org.apache.spark" %% "spark-sql-kafka-0-10" % "3.3.1", - "org.apache.spark" %% "spark-mllib" % "3.3.1" - // Add other connectors like InfluxDB, Delta Lake, etc. as needed -) \ No newline at end of file +name := "analytics-engine" +version := "2.0.0" +scalaVersion := "2.12.15" + +val sparkVersion = "3.4.1" +val akkaVersion = "2.8.5" +val circeVersion = "0.14.6" + +libraryDependencies ++= Seq( + // Spark core + "org.apache.spark" %% "spark-sql" % sparkVersion, + "org.apache.spark" %% "spark-sql-kafka-0-10" % sparkVersion, + "org.apache.spark" %% "spark-mllib" % sparkVersion, + "org.apache.spark" %% "spark-streaming" % sparkVersion, + + // Akka for actor-based processing + "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion, + "com.typesafe.akka" %% "akka-stream" % akkaVersion, + "com.typesafe.akka" %% "akka-http" % "10.5.3", + + // JSON processing + "io.circe" %% "circe-core" % circeVersion, + "io.circe" %% "circe-generic" % circeVersion, + "io.circe" %% "circe-parser" % circeVersion, + + // Database connectors + "org.postgresql" % "postgresql" % "42.6.0", + "com.datastax.spark" %% "spark-cassandra-connector" % "3.4.1", + "org.elasticsearch" %% "elasticsearch-spark-30" % "8.9.0", + + // Monitoring and metrics + "io.prometheus" % "simpleclient" % "0.16.0", + "io.prometheus" % "simpleclient_hotspot" % "0.16.0", + "io.prometheus" % "simpleclient_httpserver" % "0.16.0", + + // Configuration + "com.typesafe" % "config" % "1.4.2", + + // Logging + "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5", + "ch.qos.logback" % "logback-classic" % "1.4.11", + + // Time series databases + "org.influxdb" % "influxdb-java" % "2.23", + + // Testing + "org.scalatest" %% "scalatest" % "3.2.17" % Test, + "com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test, + "com.typesafe.akka" %% "akka-stream-testkit" % akkaVersion % Test +) + +// Assembly plugin for fat JAR creation +assemblyMergeStrategy in assembly := { + case PathList("META-INF", xs @ _*) => MergeStrategy.discard + case "application.conf" => MergeStrategy.concat + case "reference.conf" => MergeStrategy.concat + case _ => MergeStrategy.first +} + +// Compiler options +scalacOptions ++= Seq( + "-deprecation", + "-feature", + "-unchecked", + "-Xlog-reflective-calls", + "-Xlint" +) + +// Test options +Test / parallelExecution := false +Test / fork := true \ No newline at end of file diff --git a/services/analytics-engine/src/main/scala/streaming/BackupAnalytics.scala b/services/analytics-engine/src/main/scala/streaming/BackupAnalytics.scala index 5d13901..069881d 100644 --- a/services/analytics-engine/src/main/scala/streaming/BackupAnalytics.scala +++ b/services/analytics-engine/src/main/scala/streaming/BackupAnalytics.scala @@ -1,93 +1,93 @@ -package streaming - -import org.apache.spark.sql.SparkSession -import org.apache.spark.sql.DataFrame -import org.apache.spark.sql.functions._ -import org.apache.spark.sql.streaming.{StreamingQuery, Trigger} -// import org.apache.spark.ml.linalg.Vector // Correct import path for ML Vector - -// --- Placeholder Objects and Schemas --- - -object backupEventSchema { - // In a real implementation, this would be a StructType defining the event schema -} - -object ModelRegistry { - def getAnomalyModel(tenantId: String): AnomalyModel = new AnomalyModel() -} - -class AnomalyModel { - // def computeDistance(features: Vector): Double = 0.0 -} - -def sendAnomalyAlert(row: org.apache.spark.sql.Row): Unit = { - println(s"Anomaly Detected: ${row.toString()}") -} - -// --- Main BackupAnalyticsEngine Object --- - -object BackupAnalyticsEngine { - def startAnalyticsPipeline(spark: SparkSession): Unit = { // Return type changed for simplicity - import spark.implicits._ - - val backupEvents = spark - .readStream - .format("kafka") - .option("kafka.bootstrap.servers", "kafka-cluster:9092") - .option("subscribe", "backup-events") - .option("startingOffsets", "latest") - .load() - - val parsedEvents = backupEvents - // .select(from_json($"value".cast("string"), backupEventSchema).as("data")) // Schema needs to be defined - .selectExpr("CAST(value AS STRING) as json") - .select(json_tuple($"json", "eventTime", "tenantId", "backupType", "size", "duration", "file_count").as("eventTime", "tenantId", "backupType", "size", "duration", "file_count")) - .withColumn("timestamp", to_timestamp($"eventTime")) - - val stats = parsedEvents - .withWatermark("timestamp", "10 minutes") - .groupBy( - window($"timestamp", "5 minutes", "1 minute"), - $"tenantId", - $"backupType" - ) - .agg( - count("*").as("backup_count"), - avg("size").as("avg_size"), - sum("size").as("total_size"), - avg("duration").as("avg_duration") - ) - - // Anomaly detection part is complex and depends on a trained model and feature vector. - // This is a simplified representation. - val anomalyDetector = parsedEvents - .withColumn("anomaly_score", rand()) // Placeholder for actual anomaly UDF - .filter($"anomaly_score" > 0.95) - - val query = stats - .writeStream - .outputMode("append") - .trigger(Trigger.ProcessingTime("30 seconds")) - .foreachBatch { (batchDF: DataFrame, batchId: Long) => - println(s"--- Batch $batchId ---") - batchDF.show() - - // Placeholder for writing to sinks - // batchDF.write.format("influxdb").save() - // batchDF.write.format("delta").save("s3://...") - - // Placeholder for alerting - // anomalyDetector.filter($"batchId" === batchId).collect().foreach(sendAnomalyAlert) - } - .start() - - query.awaitTermination() - } - - // Placeholder for the UDF function - // def detectAnomaly(features: Vector, tenantId: String): Double = { - // val model = ModelRegistry.getAnomalyModel(tenantId) - // val distance = model.computeDistance(features) - // 1.0 / (1.0 + math.exp(-distance)) - // } +package streaming + +import org.apache.spark.sql.SparkSession +import org.apache.spark.sql.DataFrame +import org.apache.spark.sql.functions._ +import org.apache.spark.sql.streaming.{StreamingQuery, Trigger} +// import org.apache.spark.ml.linalg.Vector // Correct import path for ML Vector + +// --- Placeholder Objects and Schemas --- + +object backupEventSchema { + // In a real implementation, this would be a StructType defining the event schema +} + +object ModelRegistry { + def getAnomalyModel(tenantId: String): AnomalyModel = new AnomalyModel() +} + +class AnomalyModel { + // def computeDistance(features: Vector): Double = 0.0 +} + +def sendAnomalyAlert(row: org.apache.spark.sql.Row): Unit = { + println(s"Anomaly Detected: ${row.toString()}") +} + +// --- Main BackupAnalyticsEngine Object --- + +object BackupAnalyticsEngine { + def startAnalyticsPipeline(spark: SparkSession): Unit = { // Return type changed for simplicity + import spark.implicits._ + + val backupEvents = spark + .readStream + .format("kafka") + .option("kafka.bootstrap.servers", "kafka-cluster:9092") + .option("subscribe", "backup-events") + .option("startingOffsets", "latest") + .load() + + val parsedEvents = backupEvents + // .select(from_json($"value".cast("string"), backupEventSchema).as("data")) // Schema needs to be defined + .selectExpr("CAST(value AS STRING) as json") + .select(json_tuple($"json", "eventTime", "tenantId", "backupType", "size", "duration", "file_count").as("eventTime", "tenantId", "backupType", "size", "duration", "file_count")) + .withColumn("timestamp", to_timestamp($"eventTime")) + + val stats = parsedEvents + .withWatermark("timestamp", "10 minutes") + .groupBy( + window($"timestamp", "5 minutes", "1 minute"), + $"tenantId", + $"backupType" + ) + .agg( + count("*").as("backup_count"), + avg("size").as("avg_size"), + sum("size").as("total_size"), + avg("duration").as("avg_duration") + ) + + // Anomaly detection part is complex and depends on a trained model and feature vector. + // This is a simplified representation. + val anomalyDetector = parsedEvents + .withColumn("anomaly_score", rand()) // Placeholder for actual anomaly UDF + .filter($"anomaly_score" > 0.95) + + val query = stats + .writeStream + .outputMode("append") + .trigger(Trigger.ProcessingTime("30 seconds")) + .foreachBatch { (batchDF: DataFrame, batchId: Long) => + println(s"--- Batch $batchId ---") + batchDF.show() + + // Placeholder for writing to sinks + // batchDF.write.format("influxdb").save() + // batchDF.write.format("delta").save("s3://...") + + // Placeholder for alerting + // anomalyDetector.filter($"batchId" === batchId).collect().foreach(sendAnomalyAlert) + } + .start() + + query.awaitTermination() + } + + // Placeholder for the UDF function + // def detectAnomaly(features: Vector, tenantId: String): Double = { + // val model = ModelRegistry.getAnomalyModel(tenantId) + // val distance = model.computeDistance(features) + // 1.0 / (1.0 + math.exp(-distance)) + // } } \ No newline at end of file diff --git a/services/backup-engine/Dockerfile b/services/backup-engine/Dockerfile index ee31793..91799be 100644 --- a/services/backup-engine/Dockerfile +++ b/services/backup-engine/Dockerfile @@ -1,11 +1,11 @@ -# Build stage -FROM gradle:8.4-jdk17-alpine AS build -WORKDIR /home/gradle/src -COPY --chown=gradle:gradle . . -RUN gradle build --no-daemon - -# Package stage -FROM eclipse-temurin:17-jre-alpine -WORKDIR /app -COPY --from=build /home/gradle/src/build/libs/*.jar app.jar +# Build stage +FROM gradle:8.4-jdk17-alpine AS build +WORKDIR /home/gradle/src +COPY --chown=gradle:gradle . . +RUN gradle build --no-daemon + +# Package stage +FROM eclipse-temurin:17-jre-alpine +WORKDIR /app +COPY --from=build /home/gradle/src/build/libs/*.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/services/backup-engine/build.gradle.kts b/services/backup-engine/build.gradle.kts index bbda35c..c7f36af 100644 --- a/services/backup-engine/build.gradle.kts +++ b/services/backup-engine/build.gradle.kts @@ -1,37 +1,87 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - id("org.springframework.boot") version "3.1.5" - id("io.spring.dependency-management") version "1.1.3" - kotlin("jvm") - kotlin("plugin.spring") version "1.8.22" -} - -group = "com.corestate.services" -version = "2.0.0" - -java { - sourceCompatibility = JavaVersion.VERSION_17 -} - - -dependencies { - implementation("org.springframework.boot:spring-boot-starter-web") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") - implementation("org.jetbrains.kotlin:kotlin-reflect") - implementation("org.springframework.boot:spring-boot-starter-data-jpa") - runtimeOnly("com.h2database:h2") - testImplementation("org.springframework.boot:spring-boot-starter-test") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") -} - -tasks.withType { - kotlinOptions { - freeCompilerArgs += "-Xjsr305=strict" - jvmTarget = "17" - } -} - -tasks.withType { - useJUnitPlatform() +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("org.springframework.boot") version "3.1.5" + id("io.spring.dependency-management") version "1.1.3" + kotlin("jvm") + kotlin("plugin.spring") version "1.8.22" +} + +group = "com.corestate.services" +version = "2.0.0" + +java { + sourceCompatibility = JavaVersion.VERSION_17 +} + + +dependencies { + // Spring Boot core + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.boot:spring-boot-starter-validation") + + // Kotlin support + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3") + + // gRPC and Protocol Buffers + implementation("io.grpc:grpc-netty-shaded:1.58.0") + implementation("io.grpc:grpc-protobuf:1.58.0") + implementation("io.grpc:grpc-stub:1.58.0") + implementation("io.grpc:grpc-kotlin-stub:1.4.0") + implementation("com.google.protobuf:protobuf-kotlin:3.24.4") + + // Database + runtimeOnly("com.h2database:h2") + implementation("org.springframework.boot:spring-boot-starter-data-redis") + implementation("org.postgresql:postgresql:42.6.0") + + // Backup and storage + implementation("commons-codec:commons-codec:1.16.0") + implementation("org.apache.commons:commons-compress:1.24.0") + implementation("net.lingala.zip4j:zip4j:2.11.5") + + // Monitoring and metrics + implementation("io.micrometer:micrometer-registry-prometheus") + implementation("io.micrometer:micrometer-tracing-bridge-brave") + + // AWS SDK for S3 support + implementation("software.amazon.awssdk:s3:2.21.15") + implementation("software.amazon.awssdk:sts:2.21.15") + + // Azure SDK for Blob storage + implementation("com.azure:azure-storage-blob:12.23.1") + + // Google Cloud Storage + implementation("com.google.cloud:google-cloud-storage:2.28.0") + + // Logging + implementation("io.github.microutils:kotlin-logging-jvm:3.0.5") + + // Configuration + implementation("org.springframework.cloud:spring-cloud-starter-kubernetes-config:3.0.5") + + // Testing + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0") + testImplementation("io.kotest:kotest-runner-junit5:5.7.2") + testImplementation("io.kotest:kotest-assertions-core:5.7.2") + testImplementation("org.testcontainers:junit-jupiter:1.19.1") + testImplementation("org.testcontainers:postgresql:1.19.1") +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs += "-Xjsr305=strict" + jvmTarget = "17" + } +} + +tasks.withType { + useJUnitPlatform() } \ No newline at end of file diff --git a/services/backup-engine/src/main/java/com/corestate/backup/enterprise/MultiTenantManager.java b/services/backup-engine/src/main/java/com/corestate/backup/enterprise/MultiTenantManager.java index 30f9c28..8c8f41f 100644 --- a/services/backup-engine/src/main/java/com/corestate/backup/enterprise/MultiTenantManager.java +++ b/services/backup-engine/src/main/java/com/corestate/backup/enterprise/MultiTenantManager.java @@ -1,162 +1,162 @@ -package com.corestate.backup.enterprise; - -import org.springframework.stereotype.Component; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -// --- Placeholder Classes, Enums, and Interfaces --- - -class TenantContextHolder { - private static final ThreadLocal contextHolder = new ThreadLocal<>(); - public static void setContext(TenantContext context) { contextHolder.set(context); } - public static void clearContext() { contextHolder.remove(); } -} - -class ResourceLimiter { - public static void applyLimits(Object limits) { /* Placeholder */ } -} - -interface QuotaStore { - TenantQuota getQuota(String tenantId, ResourceType type); -} - -interface UsageTracker { - long getUsage(String tenantId, ResourceType type); - void incrementUsage(String tenantId, ResourceType type, long amount); -} - -interface AlertingService { - void sendQuotaWarning(String tenantId, ResourceType type, long usage, long limit); -} - -class QuotaExceededException extends RuntimeException { - public QuotaExceededException(String message) { super(message); } -} - -enum ResourceType { STORAGE, BANDWIDTH } -enum IsolationLevel { STRICT, SHARED } - -class TenantQuota { - public long getLimit() { return 1024L * 1024L * 1024L; /* 1GB */ } -} - -class TenantContext implements AutoCloseable { - // Builder pattern for TenantContext - public static class Builder { - public Builder tenantId(String id) { return this; } - public Builder dataPath(String path) { return this; } - public Builder encryptionKey(Object key) { return this; } - public Builder resourceLimits(Object limits) { return this; } - public Builder isolationLevel(IsolationLevel level) { return this; } - public TenantContext build() { return new TenantContext(); } - } - public static Builder builder() { return new Builder(); } - public AutoCloseable enter() { return this; } - public Object getResourceLimits() { return new Object(); } - @Override public void close() { /* Clean up context */ } -} - -interface EncryptionKeyManager { - Object getTenantKey(String tenantId); -} - -// --- Main MultiTenantManager Class and its Components --- - -@Component -public class MultiTenantManager { - private final TenantIsolation isolation; - private final ResourceQuotaManager quotaManager; - private final EncryptionKeyManager keyManager; - - public MultiTenantManager(TenantIsolation isolation, ResourceQuotaManager quotaManager, EncryptionKeyManager keyManager) { - this.isolation = isolation; - this.quotaManager = quotaManager; - this.keyManager = keyManager; - } - - @Service - public static class TenantIsolation { - private final Map contexts = new ConcurrentHashMap<>(); - private final EncryptionKeyManager keyManager; - private final ResourceQuotaManager quotaManager; - - public TenantIsolation(EncryptionKeyManager keyManager, ResourceQuotaManager quotaManager) { - this.keyManager = keyManager; - this.quotaManager = quotaManager; - } - - public void isolateOperation(String tenantId, Runnable operation) { - TenantContext context = contexts.computeIfAbsent( - tenantId, - id -> createTenantContext(id) - ); - - try (var scope = context.enter()) { - TenantContextHolder.setContext(context); - ResourceLimiter.applyLimits(context.getResourceLimits()); - operation.run(); - } finally { - TenantContextHolder.clearContext(); - } - } - - private String generateTenantDataPath(String tenantId) { - return "/data/" + tenantId; - } - - private TenantContext createTenantContext(String tenantId) { - return TenantContext.builder() - .tenantId(tenantId) - .dataPath(generateTenantDataPath(tenantId)) - .encryptionKey(keyManager.getTenantKey(tenantId)) - .resourceLimits(quotaManager.getLimits(tenantId)) - .isolationLevel(IsolationLevel.STRICT) - .build(); - } - } - - @Component - public static class ResourceQuotaManager { - private final QuotaStore quotaStore; - private final UsageTracker usageTracker; - private final AlertingService alertingService; - - public ResourceQuotaManager(QuotaStore quotaStore, UsageTracker usageTracker, AlertingService alertingService) { - this.quotaStore = quotaStore; - this.usageTracker = usageTracker; - this.alertingService = alertingService; - } - - public boolean checkQuota(String tenantId, ResourceType type, long requested) { - TenantQuota quota = quotaStore.getQuota(tenantId, type); - long currentUsage = usageTracker.getUsage(tenantId, type); - return currentUsage + requested <= quota.getLimit(); - } - - public Object getLimits(String tenantId) { - // Return a representation of all quotas for the tenant - return new Object(); - } - - @Transactional - public void consumeQuota(String tenantId, ResourceType type, long amount) { - if (!checkQuota(tenantId, type, amount)) { - throw new QuotaExceededException( - String.format("Tenant %s exceeded %s quota", tenantId, type) - ); - } - - usageTracker.incrementUsage(tenantId, type, amount); - - long usage = usageTracker.getUsage(tenantId, type); - TenantQuota quota = quotaStore.getQuota(tenantId, type); - - if (usage > quota.getLimit() * 0.8) { - alertingService.sendQuotaWarning(tenantId, type, usage, quota.getLimit()); - } - } - } +package com.corestate.backup.enterprise; + +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +// --- Placeholder Classes, Enums, and Interfaces --- + +class TenantContextHolder { + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + public static void setContext(TenantContext context) { contextHolder.set(context); } + public static void clearContext() { contextHolder.remove(); } +} + +class ResourceLimiter { + public static void applyLimits(Object limits) { /* Placeholder */ } +} + +interface QuotaStore { + TenantQuota getQuota(String tenantId, ResourceType type); +} + +interface UsageTracker { + long getUsage(String tenantId, ResourceType type); + void incrementUsage(String tenantId, ResourceType type, long amount); +} + +interface AlertingService { + void sendQuotaWarning(String tenantId, ResourceType type, long usage, long limit); +} + +class QuotaExceededException extends RuntimeException { + public QuotaExceededException(String message) { super(message); } +} + +enum ResourceType { STORAGE, BANDWIDTH } +enum IsolationLevel { STRICT, SHARED } + +class TenantQuota { + public long getLimit() { return 1024L * 1024L * 1024L; /* 1GB */ } +} + +class TenantContext implements AutoCloseable { + // Builder pattern for TenantContext + public static class Builder { + public Builder tenantId(String id) { return this; } + public Builder dataPath(String path) { return this; } + public Builder encryptionKey(Object key) { return this; } + public Builder resourceLimits(Object limits) { return this; } + public Builder isolationLevel(IsolationLevel level) { return this; } + public TenantContext build() { return new TenantContext(); } + } + public static Builder builder() { return new Builder(); } + public AutoCloseable enter() { return this; } + public Object getResourceLimits() { return new Object(); } + @Override public void close() { /* Clean up context */ } +} + +interface EncryptionKeyManager { + Object getTenantKey(String tenantId); +} + +// --- Main MultiTenantManager Class and its Components --- + +@Component +public class MultiTenantManager { + private final TenantIsolation isolation; + private final ResourceQuotaManager quotaManager; + private final EncryptionKeyManager keyManager; + + public MultiTenantManager(TenantIsolation isolation, ResourceQuotaManager quotaManager, EncryptionKeyManager keyManager) { + this.isolation = isolation; + this.quotaManager = quotaManager; + this.keyManager = keyManager; + } + + @Service + public static class TenantIsolation { + private final Map contexts = new ConcurrentHashMap<>(); + private final EncryptionKeyManager keyManager; + private final ResourceQuotaManager quotaManager; + + public TenantIsolation(EncryptionKeyManager keyManager, ResourceQuotaManager quotaManager) { + this.keyManager = keyManager; + this.quotaManager = quotaManager; + } + + public void isolateOperation(String tenantId, Runnable operation) { + TenantContext context = contexts.computeIfAbsent( + tenantId, + id -> createTenantContext(id) + ); + + try (var scope = context.enter()) { + TenantContextHolder.setContext(context); + ResourceLimiter.applyLimits(context.getResourceLimits()); + operation.run(); + } finally { + TenantContextHolder.clearContext(); + } + } + + private String generateTenantDataPath(String tenantId) { + return "/data/" + tenantId; + } + + private TenantContext createTenantContext(String tenantId) { + return TenantContext.builder() + .tenantId(tenantId) + .dataPath(generateTenantDataPath(tenantId)) + .encryptionKey(keyManager.getTenantKey(tenantId)) + .resourceLimits(quotaManager.getLimits(tenantId)) + .isolationLevel(IsolationLevel.STRICT) + .build(); + } + } + + @Component + public static class ResourceQuotaManager { + private final QuotaStore quotaStore; + private final UsageTracker usageTracker; + private final AlertingService alertingService; + + public ResourceQuotaManager(QuotaStore quotaStore, UsageTracker usageTracker, AlertingService alertingService) { + this.quotaStore = quotaStore; + this.usageTracker = usageTracker; + this.alertingService = alertingService; + } + + public boolean checkQuota(String tenantId, ResourceType type, long requested) { + TenantQuota quota = quotaStore.getQuota(tenantId, type); + long currentUsage = usageTracker.getUsage(tenantId, type); + return currentUsage + requested <= quota.getLimit(); + } + + public Object getLimits(String tenantId) { + // Return a representation of all quotas for the tenant + return new Object(); + } + + @Transactional + public void consumeQuota(String tenantId, ResourceType type, long amount) { + if (!checkQuota(tenantId, type, amount)) { + throw new QuotaExceededException( + String.format("Tenant %s exceeded %s quota", tenantId, type) + ); + } + + usageTracker.incrementUsage(tenantId, type, amount); + + long usage = usageTracker.getUsage(tenantId, type); + TenantQuota quota = quotaStore.getQuota(tenantId, type); + + if (usage > quota.getLimit() * 0.8) { + alertingService.sendQuotaWarning(tenantId, type, usage, quota.getLimit()); + } + } + } } \ No newline at end of file diff --git a/services/backup-engine/src/main/kotlin/com/corestate/backup/BackupEngineService.kt b/services/backup-engine/src/main/kotlin/com/corestate/backup/BackupEngineService.kt index 52e7afc..6bc2993 100644 --- a/services/backup-engine/src/main/kotlin/com/corestate/backup/BackupEngineService.kt +++ b/services/backup-engine/src/main/kotlin/com/corestate/backup/BackupEngineService.kt @@ -1,11 +1,11 @@ -package com.corestate.backup - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication - -@SpringBootApplication -class BackupEngineService - -fun main(args: Array) { - runApplication(*args) +package com.corestate.backup + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class BackupEngineService + +fun main(args: Array) { + runApplication(*args) } \ No newline at end of file diff --git a/services/backup-engine/src/main/kotlin/com/corestate/backup/distributed/ChunkDistributor.kt b/services/backup-engine/src/main/kotlin/com/corestate/backup/distributed/ChunkDistributor.kt index e9164b1..25380cb 100644 --- a/services/backup-engine/src/main/kotlin/com/corestate/backup/distributed/ChunkDistributor.kt +++ b/services/backup-engine/src/main/kotlin/com/corestate/backup/distributed/ChunkDistributor.kt @@ -1,159 +1,159 @@ -package com.corestate.backup.distributed - -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import java.util.SortedMap -import java.util.TreeMap -import java.security.MessageDigest - -// --- Placeholder Classes and Enums --- - -data class DataChunk(val data: ByteArray) { - fun calculateHash(): String { - val digest = MessageDigest.getInstance("SHA-256") - return digest.digest(data).fold("") { str, it -> str + "%02x".format(it) } - } -} - -data class StorageNode(val id: String) { - suspend fun uploadChunk(chunk: DataChunk): Boolean { - // Placeholder for actual network upload logic - println("Uploading chunk ${chunk.calculateHash()} to node $id") - // Simulate network delay and potential failure - kotlinx.coroutines.delay(100) - if (Math.random() < 0.1) throw RuntimeException("Failed to upload to $id") - return true - } -} - -enum class ChunkStatus { UPLOADED, FAILED } -enum class DistributionStatus { SUCCESS, PARTIAL, FAILED } - -data class ChunkLocation(val nodeId: String, val chunkId: String, val status: ChunkStatus) -data class DistributionResult( - val chunkId: String, - val locations: List, - val status: DistributionStatus -) - -// --- Consistent Hashing Implementation --- - -class ConsistentHashRing( - private val numberOfReplicas: Int = 3, - private val hashFunction: (String) -> Int = { it.hashCode() } -) { - private val circle: SortedMap = TreeMap() - - fun add(node: T) { - for (i in 0 until numberOfReplicas) { - val hash = hashFunction("${node.id}:$i") - circle[hash] = node - } - } - - fun getNodes(key: String, count: Int): List { - if (circle.isEmpty() || count == 0) return emptyList() - val hash = hashFunction(key) - val result = mutableSetOf() - - val tailMap = circle.tailMap(hash) - val iterator = (tailMap.values + circle.values).iterator() - - while (result.size < count && result.size < circle.values.distinct().size) { - if (iterator.hasNext()) { - result.add(iterator.next()) - } else { - break // Should not happen with circular logic, but for safety - } - } - return result.toList() - } - - fun getNextNode(failedNode: T): T? { - // Simple logic: find the next node in the circle - val hashes = circle.filterValues { it.id == failedNode.id }.keys - if (hashes.isEmpty()) return null - val firstHash = hashes.first() - val tailMap = circle.tailMap(firstHash + 1) - return (tailMap.values + circle.values).firstOrNull { it.id != failedNode.id } - } -} - -// --- Main ChunkDistributor Class --- - -class ChunkDistributor( - storageNodes: List, - private val replicationFactor: Int = 3 -) { - private val consistentHash = ConsistentHashRing() - // private val logger = LoggerFactory.getLogger(ChunkDistributor::class.java) // Placeholder - - init { - storageNodes.forEach { consistentHash.add(it) } - } - - suspend fun distributeChunk(chunk: DataChunk): DistributionResult { - val chunkId = chunk.calculateHash() - val primaryNodes = consistentHash.getNodes(chunkId, replicationFactor) - - if (primaryNodes.isEmpty()) { - // logger.error("No storage nodes available to distribute chunk $chunkId") - return DistributionResult(chunkId, emptyList(), DistributionStatus.FAILED) - } - - return coroutineScope { - val uploadJobs = primaryNodes.map { node -> - async { - try { - node.uploadChunk(chunk) - ChunkLocation(node.id, chunkId, ChunkStatus.UPLOADED) - } catch (e: Exception) { - handleFailedUpload(node, chunk, e) - } - } - } - - val locations = uploadJobs.awaitAll().filterNotNull() - // updateChunkIndex(chunkId, locations) // Placeholder for index update - - val successCount = locations.count { it.status == ChunkStatus.UPLOADED } - val finalStatus = when { - successCount >= replicationFactor -> DistributionStatus.SUCCESS - successCount > 0 -> DistributionStatus.PARTIAL - else -> DistributionStatus.FAILED - } - - DistributionResult( - chunkId = chunkId, - locations = locations, - status = finalStatus - ) - } - } - - private suspend fun handleFailedUpload( - failedNode: StorageNode, - chunk: DataChunk, - error: Exception - ): ChunkLocation? { - // logger.error("Failed to upload chunk to node ${failedNode.id}", error) - println("Failed to upload chunk to node ${failedNode.id}: ${error.message}") - - val alternativeNode = consistentHash.getNextNode(failedNode) - return if (alternativeNode != null) { - try { - alternativeNode.uploadChunk(chunk) - ChunkLocation(alternativeNode.id, chunk.calculateHash(), ChunkStatus.UPLOADED) - } catch (e: Exception) { - // logger.error("Failed to upload chunk to alternative node ${alternativeNode.id}", e) - println("Failed to upload chunk to alternative node ${alternativeNode.id}: ${e.message}") - ChunkLocation(failedNode.id, chunk.calculateHash(), ChunkStatus.FAILED) - } - } else { - // logger.warn("No alternative node found for failed node ${failedNode.id}") - println("No alternative node found for failed node ${failedNode.id}") - null - } - } +package com.corestate.backup.distributed + +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import java.util.SortedMap +import java.util.TreeMap +import java.security.MessageDigest + +// --- Placeholder Classes and Enums --- + +data class DataChunk(val data: ByteArray) { + fun calculateHash(): String { + val digest = MessageDigest.getInstance("SHA-256") + return digest.digest(data).fold("") { str, it -> str + "%02x".format(it) } + } +} + +data class StorageNode(val id: String) { + suspend fun uploadChunk(chunk: DataChunk): Boolean { + // Placeholder for actual network upload logic + println("Uploading chunk ${chunk.calculateHash()} to node $id") + // Simulate network delay and potential failure + kotlinx.coroutines.delay(100) + if (Math.random() < 0.1) throw RuntimeException("Failed to upload to $id") + return true + } +} + +enum class ChunkStatus { UPLOADED, FAILED } +enum class DistributionStatus { SUCCESS, PARTIAL, FAILED } + +data class ChunkLocation(val nodeId: String, val chunkId: String, val status: ChunkStatus) +data class DistributionResult( + val chunkId: String, + val locations: List, + val status: DistributionStatus +) + +// --- Consistent Hashing Implementation --- + +class ConsistentHashRing( + private val numberOfReplicas: Int = 3, + private val hashFunction: (String) -> Int = { it.hashCode() } +) { + private val circle: SortedMap = TreeMap() + + fun add(node: T) { + for (i in 0 until numberOfReplicas) { + val hash = hashFunction("${node.id}:$i") + circle[hash] = node + } + } + + fun getNodes(key: String, count: Int): List { + if (circle.isEmpty() || count == 0) return emptyList() + val hash = hashFunction(key) + val result = mutableSetOf() + + val tailMap = circle.tailMap(hash) + val iterator = (tailMap.values + circle.values).iterator() + + while (result.size < count && result.size < circle.values.distinct().size) { + if (iterator.hasNext()) { + result.add(iterator.next()) + } else { + break // Should not happen with circular logic, but for safety + } + } + return result.toList() + } + + fun getNextNode(failedNode: T): T? { + // Simple logic: find the next node in the circle + val hashes = circle.filterValues { it.id == failedNode.id }.keys + if (hashes.isEmpty()) return null + val firstHash = hashes.first() + val tailMap = circle.tailMap(firstHash + 1) + return (tailMap.values + circle.values).firstOrNull { it.id != failedNode.id } + } +} + +// --- Main ChunkDistributor Class --- + +class ChunkDistributor( + storageNodes: List, + private val replicationFactor: Int = 3 +) { + private val consistentHash = ConsistentHashRing() + // private val logger = LoggerFactory.getLogger(ChunkDistributor::class.java) // Placeholder + + init { + storageNodes.forEach { consistentHash.add(it) } + } + + suspend fun distributeChunk(chunk: DataChunk): DistributionResult { + val chunkId = chunk.calculateHash() + val primaryNodes = consistentHash.getNodes(chunkId, replicationFactor) + + if (primaryNodes.isEmpty()) { + // logger.error("No storage nodes available to distribute chunk $chunkId") + return DistributionResult(chunkId, emptyList(), DistributionStatus.FAILED) + } + + return coroutineScope { + val uploadJobs = primaryNodes.map { node -> + async { + try { + node.uploadChunk(chunk) + ChunkLocation(node.id, chunkId, ChunkStatus.UPLOADED) + } catch (e: Exception) { + handleFailedUpload(node, chunk, e) + } + } + } + + val locations = uploadJobs.awaitAll().filterNotNull() + // updateChunkIndex(chunkId, locations) // Placeholder for index update + + val successCount = locations.count { it.status == ChunkStatus.UPLOADED } + val finalStatus = when { + successCount >= replicationFactor -> DistributionStatus.SUCCESS + successCount > 0 -> DistributionStatus.PARTIAL + else -> DistributionStatus.FAILED + } + + DistributionResult( + chunkId = chunkId, + locations = locations, + status = finalStatus + ) + } + } + + private suspend fun handleFailedUpload( + failedNode: StorageNode, + chunk: DataChunk, + error: Exception + ): ChunkLocation? { + // logger.error("Failed to upload chunk to node ${failedNode.id}", error) + println("Failed to upload chunk to node ${failedNode.id}: ${error.message}") + + val alternativeNode = consistentHash.getNextNode(failedNode) + return if (alternativeNode != null) { + try { + alternativeNode.uploadChunk(chunk) + ChunkLocation(alternativeNode.id, chunk.calculateHash(), ChunkStatus.UPLOADED) + } catch (e: Exception) { + // logger.error("Failed to upload chunk to alternative node ${alternativeNode.id}", e) + println("Failed to upload chunk to alternative node ${alternativeNode.id}: ${e.message}") + ChunkLocation(failedNode.id, chunk.calculateHash(), ChunkStatus.FAILED) + } + } else { + // logger.warn("No alternative node found for failed node ${failedNode.id}") + println("No alternative node found for failed node ${failedNode.id}") + null + } + } } \ No newline at end of file diff --git a/services/backup-engine/src/main/kotlin/com/corestate/backup/strategies/SyntheticBackup.kt b/services/backup-engine/src/main/kotlin/com/corestate/backup/strategies/SyntheticBackup.kt index 3cb61e8..21577f7 100644 --- a/services/backup-engine/src/main/kotlin/com/corestate/backup/strategies/SyntheticBackup.kt +++ b/services/backup-engine/src/main/kotlin/com/corestate/backup/strategies/SyntheticBackup.kt @@ -1,148 +1,148 @@ -package com.corestate.backup.strategies - -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import java.time.Instant -import java.util.UUID -import kotlin.system.measureTimeMillis - -// --- Placeholder Interfaces and Data Classes --- - -interface ChunkStore -interface MetadataStore { - suspend fun storeSynthetic(metadata: BackupMetadata) -} -interface DeduplicationService { - suspend fun process(content: ByteArray): List -} - -enum class BackupType { FULL, INCREMENTAL, SYNTHETIC_FULL } - -data class BackupMetadata( - val id: UUID, - val type: BackupType, - val timestamp: Instant, - val baseBackupId: UUID?, - val incrementalIds: List?, - val files: List, - val compressionRatio: Double, - val deduplicationRatio: Double -) - -data class IncrementalBackup(val id: UUID, val timestamp: Instant) -data class SyntheticFile( - val path: String, - val size: Long, - val checksum: String, - val chunks: List, - val metadata: Map -) - -data class SyntheticFullBackup( - val metadata: BackupMetadata, - val generationTime: Long, - val spacesSaved: Long -) - -// --- Placeholder Timeline and State Reconstruction Logic --- - -class BackupTimeline { - fun addBase(base: BackupMetadata) {} - fun applyIncremental(incremental: IncrementalBackup) {} -} - -data class ReconstructedState(val files: List) -data class FileState( - val path: String, - val size: Long, - val checksum: String, - val metadata: Map -) - -// --- Main SyntheticBackupGenerator Class --- - -class SyntheticBackupGenerator( - private val chunkStore: ChunkStore, - private val metadataStore: MetadataStore, - private val deduplicationService: DeduplicationService -) { - suspend fun generateSyntheticFull( - baseBackup: BackupMetadata, - incrementals: List - ): SyntheticFullBackup = coroutineScope { - var syntheticMetadata: BackupMetadata? = null - val generationTime = measureTimeMillis { - val timeline = buildBackupTimeline(baseBackup, incrementals) - val latestState = reconstructLatestState(timeline) - - val syntheticChunks = latestState.files.map { file -> - async { - val chunks = collectFileChunks(file, timeline) - val mergedContent = mergeChunks(chunks) - val dedupedChunks = deduplicationService.process(mergedContent) - - SyntheticFile( - path = file.path, - size = file.size, - checksum = file.checksum, - chunks = dedupedChunks, - metadata = file.metadata - ) - } - }.awaitAll() - - syntheticMetadata = BackupMetadata( - id = UUID.randomUUID(), - type = BackupType.SYNTHETIC_FULL, - timestamp = Instant.now(), - baseBackupId = baseBackup.id, - incrementalIds = incrementals.map { it.id }, - files = syntheticChunks, - compressionRatio = calculateCompressionRatio(syntheticChunks), - deduplicationRatio = calculateDeduplicationRatio(syntheticChunks) - ) - - metadataStore.storeSynthetic(syntheticMetadata!!) - } - - SyntheticFullBackup( - metadata = syntheticMetadata!!, - generationTime = generationTime, - spacesSaved = calculateSpaceSaved(baseBackup, incrementals, syntheticMetadata!!) - ) - } - - private fun buildBackupTimeline( - base: BackupMetadata, - incrementals: List - ): BackupTimeline { - val timeline = BackupTimeline() - timeline.addBase(base) - incrementals.sortedBy { it.timestamp }.forEach { incremental -> - timeline.applyIncremental(incremental) - } - return timeline - } - - // --- Placeholder Helper Functions --- - - private fun reconstructLatestState(timeline: BackupTimeline): ReconstructedState { - // In a real implementation, this would merge the base and incrementals - return ReconstructedState(listOf(FileState("example/file.txt", 1024, "checksum", emptyMap()))) - } - - private suspend fun collectFileChunks(file: FileState, timeline: BackupTimeline): List { - // In a real implementation, this would fetch chunks from the chunkStore - return listOf(ByteArray(1024)) - } - - private fun mergeChunks(chunks: List): ByteArray { - // Simple concatenation for placeholder - return chunks.fold(ByteArray(0)) { acc, bytes -> acc + bytes } - } - - private fun calculateCompressionRatio(files: List): Double = 1.0 - private fun calculateDeduplicationRatio(files: List): Double = 1.0 - private fun calculateSpaceSaved(base: BackupMetadata, incs: List, synth: BackupMetadata): Long = 0L +package com.corestate.backup.strategies + +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import java.time.Instant +import java.util.UUID +import kotlin.system.measureTimeMillis + +// --- Placeholder Interfaces and Data Classes --- + +interface ChunkStore +interface MetadataStore { + suspend fun storeSynthetic(metadata: BackupMetadata) +} +interface DeduplicationService { + suspend fun process(content: ByteArray): List +} + +enum class BackupType { FULL, INCREMENTAL, SYNTHETIC_FULL } + +data class BackupMetadata( + val id: UUID, + val type: BackupType, + val timestamp: Instant, + val baseBackupId: UUID?, + val incrementalIds: List?, + val files: List, + val compressionRatio: Double, + val deduplicationRatio: Double +) + +data class IncrementalBackup(val id: UUID, val timestamp: Instant) +data class SyntheticFile( + val path: String, + val size: Long, + val checksum: String, + val chunks: List, + val metadata: Map +) + +data class SyntheticFullBackup( + val metadata: BackupMetadata, + val generationTime: Long, + val spacesSaved: Long +) + +// --- Placeholder Timeline and State Reconstruction Logic --- + +class BackupTimeline { + fun addBase(base: BackupMetadata) {} + fun applyIncremental(incremental: IncrementalBackup) {} +} + +data class ReconstructedState(val files: List) +data class FileState( + val path: String, + val size: Long, + val checksum: String, + val metadata: Map +) + +// --- Main SyntheticBackupGenerator Class --- + +class SyntheticBackupGenerator( + private val chunkStore: ChunkStore, + private val metadataStore: MetadataStore, + private val deduplicationService: DeduplicationService +) { + suspend fun generateSyntheticFull( + baseBackup: BackupMetadata, + incrementals: List + ): SyntheticFullBackup = coroutineScope { + var syntheticMetadata: BackupMetadata? = null + val generationTime = measureTimeMillis { + val timeline = buildBackupTimeline(baseBackup, incrementals) + val latestState = reconstructLatestState(timeline) + + val syntheticChunks = latestState.files.map { file -> + async { + val chunks = collectFileChunks(file, timeline) + val mergedContent = mergeChunks(chunks) + val dedupedChunks = deduplicationService.process(mergedContent) + + SyntheticFile( + path = file.path, + size = file.size, + checksum = file.checksum, + chunks = dedupedChunks, + metadata = file.metadata + ) + } + }.awaitAll() + + syntheticMetadata = BackupMetadata( + id = UUID.randomUUID(), + type = BackupType.SYNTHETIC_FULL, + timestamp = Instant.now(), + baseBackupId = baseBackup.id, + incrementalIds = incrementals.map { it.id }, + files = syntheticChunks, + compressionRatio = calculateCompressionRatio(syntheticChunks), + deduplicationRatio = calculateDeduplicationRatio(syntheticChunks) + ) + + metadataStore.storeSynthetic(syntheticMetadata!!) + } + + SyntheticFullBackup( + metadata = syntheticMetadata!!, + generationTime = generationTime, + spacesSaved = calculateSpaceSaved(baseBackup, incrementals, syntheticMetadata!!) + ) + } + + private fun buildBackupTimeline( + base: BackupMetadata, + incrementals: List + ): BackupTimeline { + val timeline = BackupTimeline() + timeline.addBase(base) + incrementals.sortedBy { it.timestamp }.forEach { incremental -> + timeline.applyIncremental(incremental) + } + return timeline + } + + // --- Placeholder Helper Functions --- + + private fun reconstructLatestState(timeline: BackupTimeline): ReconstructedState { + // In a real implementation, this would merge the base and incrementals + return ReconstructedState(listOf(FileState("example/file.txt", 1024, "checksum", emptyMap()))) + } + + private suspend fun collectFileChunks(file: FileState, timeline: BackupTimeline): List { + // In a real implementation, this would fetch chunks from the chunkStore + return listOf(ByteArray(1024)) + } + + private fun mergeChunks(chunks: List): ByteArray { + // Simple concatenation for placeholder + return chunks.fold(ByteArray(0)) { acc, bytes -> acc + bytes } + } + + private fun calculateCompressionRatio(files: List): Double = 1.0 + private fun calculateDeduplicationRatio(files: List): Double = 1.0 + private fun calculateSpaceSaved(base: BackupMetadata, incs: List, synth: BackupMetadata): Long = 0L } \ No newline at end of file diff --git a/services/compression-engine/Cargo.toml b/services/compression-engine/Cargo.toml new file mode 100644 index 0000000..ed49a69 --- /dev/null +++ b/services/compression-engine/Cargo.toml @@ -0,0 +1,78 @@ +[package] +name = "compression-engine" +version = "2.0.0" +edition = "2021" +authors = ["CoreState Team"] +description = "High-performance compression engine for CoreState backup system" +license = "MIT" +repository = "https://github.com/corestate/CoreState-v2" + +[dependencies] +# Compression libraries +zstd = "0.13" +lz4 = "1.24" +brotli = "3.4" +flate2 = "1.0" +xz2 = "0.1" + +# gRPC and async runtime +tonic = "0.10" +prost = "0.12" +tokio = { version = "1.34", features = ["macros", "rt-multi-thread", "fs", "io-util", "net", "time"] } +tokio-util = { version = "0.7", features = ["codec"] } +futures = "0.3" + +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +bincode = "1.3" + +# Error handling +thiserror = "1.0" +anyhow = "1.0" + +# Logging and metrics +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +prometheus = "0.13" + +# Configuration +config = "0.13" + +# UUID generation +uuid = { version = "1.6", features = ["v4", "serde"] } + +# Date and time +chrono = { version = "0.4", features = ["serde"] } + +# Async channels +tokio-stream = "0.1" + +# Threading +rayon = "1.8" + +# Memory mapping +memmap2 = "0.9" + +[dev-dependencies] +tokio-test = "0.4" +tempfile = "3.8" +proptest = "1.4" +criterion = "0.5" + +[build-dependencies] +tonic-build = "0.10" + +[[bin]] +name = "compression-engine" +path = "src/main.rs" + +[[bench]] +name = "compression_benchmarks" +harness = false + +[profile.release] +lto = true +codegen-units = 1 +panic = "abort" +strip = true \ No newline at end of file diff --git a/services/compression-engine/Dockerfile b/services/compression-engine/Dockerfile new file mode 100644 index 0000000..39efb14 --- /dev/null +++ b/services/compression-engine/Dockerfile @@ -0,0 +1,43 @@ +# Compression Engine Dockerfile +FROM rust:1.74-slim as builder + +WORKDIR /app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libssl-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy Cargo files +COPY Cargo.toml Cargo.lock ./ +COPY src/ ./src/ +COPY build.rs ./ + +# Build the application +RUN cargo build --release + +# Runtime stage +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy the binary from builder stage +COPY --from=builder /app/target/release/compression-engine /app/compression-engine + +# Create non-root user +RUN useradd -r -s /bin/false compression && \ + chown -R compression:compression /app + +USER compression + +EXPOSE 8083 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD /app/compression-engine --health-check + +CMD ["/app/compression-engine"] \ No newline at end of file diff --git a/services/compression-engine/src/compression.rs b/services/compression-engine/src/compression.rs new file mode 100644 index 0000000..0ade7ba --- /dev/null +++ b/services/compression-engine/src/compression.rs @@ -0,0 +1,193 @@ +use anyhow::{anyhow, Result}; +use serde::{Deserialize, Serialize}; +use std::io::{Read, Write}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum CompressionType { + Zstd, + Lz4, + Brotli, + Gzip, + Xz, +} + +impl CompressionType { + pub fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "zstd" => Ok(CompressionType::Zstd), + "lz4" => Ok(CompressionType::Lz4), + "brotli" => Ok(CompressionType::Brotli), + "gzip" => Ok(CompressionType::Gzip), + "xz" => Ok(CompressionType::Xz), + _ => Err(anyhow!("Unknown compression type: {}", s)), + } + } + + pub fn as_str(&self) -> &'static str { + match self { + CompressionType::Zstd => "zstd", + CompressionType::Lz4 => "lz4", + CompressionType::Brotli => "brotli", + CompressionType::Gzip => "gzip", + CompressionType::Xz => "xz", + } + } +} + +pub fn compress_data(data: &[u8], compression_type: CompressionType) -> Result> { + match compression_type { + CompressionType::Zstd => compress_zstd(data), + CompressionType::Lz4 => compress_lz4(data), + CompressionType::Brotli => compress_brotli(data), + CompressionType::Gzip => compress_gzip(data), + CompressionType::Xz => compress_xz(data), + } +} + +pub fn decompress_data(data: &[u8], compression_type: CompressionType) -> Result> { + match compression_type { + CompressionType::Zstd => decompress_zstd(data), + CompressionType::Lz4 => decompress_lz4(data), + CompressionType::Brotli => decompress_brotli(data), + CompressionType::Gzip => decompress_gzip(data), + CompressionType::Xz => decompress_xz(data), + } +} + +fn compress_zstd(data: &[u8]) -> Result> { + let compressed = zstd::encode_all(data, 3)?; + Ok(compressed) +} + +fn decompress_zstd(data: &[u8]) -> Result> { + let decompressed = zstd::decode_all(data)?; + Ok(decompressed) +} + +fn compress_lz4(data: &[u8]) -> Result> { + let compressed = lz4::block::compress(data, Some(lz4::block::CompressionMode::HIGHCOMPRESSION(9)), true)?; + Ok(compressed) +} + +fn decompress_lz4(data: &[u8]) -> Result> { + let decompressed = lz4::block::decompress(data, None)?; + Ok(decompressed) +} + +fn compress_brotli(data: &[u8]) -> Result> { + let mut compressed = Vec::new(); + let mut encoder = brotli::CompressorWriter::new(&mut compressed, 4096, 6, 22); + encoder.write_all(data)?; + drop(encoder); + Ok(compressed) +} + +fn decompress_brotli(data: &[u8]) -> Result> { + let mut decompressed = Vec::new(); + let mut decoder = brotli::Decompressor::new(data, 4096); + decoder.read_to_end(&mut decompressed)?; + Ok(decompressed) +} + +fn compress_gzip(data: &[u8]) -> Result> { + use flate2::{write::GzEncoder, Compression}; + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(data)?; + let compressed = encoder.finish()?; + Ok(compressed) +} + +fn decompress_gzip(data: &[u8]) -> Result> { + use flate2::read::GzDecoder; + let mut decoder = GzDecoder::new(data); + let mut decompressed = Vec::new(); + decoder.read_to_end(&mut decompressed)?; + Ok(decompressed) +} + +fn compress_xz(data: &[u8]) -> Result> { + let mut compressed = Vec::new(); + let mut encoder = xz2::write::XzEncoder::new(&mut compressed, 6); + encoder.write_all(data)?; + encoder.finish()?; + Ok(compressed) +} + +fn decompress_xz(data: &[u8]) -> Result> { + let mut decoder = xz2::read::XzDecoder::new(data); + let mut decompressed = Vec::new(); + decoder.read_to_end(&mut decompressed)?; + Ok(decompressed) +} + +pub fn get_compression_ratio(original_size: usize, compressed_size: usize) -> f64 { + if original_size == 0 { + return 0.0; + } + (original_size as f64 - compressed_size as f64) / original_size as f64 +} + +pub fn choose_best_compression(data: &[u8]) -> Result<(CompressionType, Vec)> { + let types = [ + CompressionType::Zstd, + CompressionType::Lz4, + CompressionType::Brotli, + CompressionType::Gzip, + ]; + + let mut best_type = CompressionType::Zstd; + let mut best_compressed = compress_data(data, best_type)?; + let mut best_ratio = get_compression_ratio(data.len(), best_compressed.len()); + + for &compression_type in &types[1..] { + let compressed = compress_data(data, compression_type)?; + let ratio = get_compression_ratio(data.len(), compressed.len()); + + if ratio > best_ratio { + best_type = compression_type; + best_compressed = compressed; + best_ratio = ratio; + } + } + + Ok((best_type, best_compressed)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_zstd_compression() { + let data = b"Hello, World! This is a test string for compression."; + let compressed = compress_zstd(data).unwrap(); + let decompressed = decompress_zstd(&compressed).unwrap(); + assert_eq!(data, decompressed.as_slice()); + } + + #[test] + fn test_all_compression_types() { + let data = b"The quick brown fox jumps over the lazy dog. ".repeat(100); + + for compression_type in [ + CompressionType::Zstd, + CompressionType::Lz4, + CompressionType::Brotli, + CompressionType::Gzip, + CompressionType::Xz, + ] { + let compressed = compress_data(&data, compression_type).unwrap(); + let decompressed = decompress_data(&compressed, compression_type).unwrap(); + assert_eq!(data, decompressed); + assert!(compressed.len() < data.len()); // Should compress well + } + } + + #[test] + fn test_best_compression_choice() { + let data = b"This is a repetitive string. ".repeat(50); + let (best_type, compressed) = choose_best_compression(&data).unwrap(); + let decompressed = decompress_data(&compressed, best_type).unwrap(); + assert_eq!(data, decompressed.as_slice()); + } +} \ No newline at end of file diff --git a/services/compression-engine/src/main.rs b/services/compression-engine/src/main.rs new file mode 100644 index 0000000..b910b0d --- /dev/null +++ b/services/compression-engine/src/main.rs @@ -0,0 +1,80 @@ +use anyhow::Result; +use std::env; +use tokio::signal; +use tracing::{info, warn}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +mod compression; +mod config; +mod metrics; +mod server; + +use crate::config::Config; +use crate::server::CompressionServer; + +#[tokio::main] +async fn main() -> Result<()> { + // Initialize tracing + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "compression_engine=info".into()), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + + // Check for health check flag + let args: Vec = env::args().collect(); + if args.len() > 1 && args[1] == "--health-check" { + return health_check().await; + } + + info!("Starting CoreState Compression Engine v2.0.0"); + + // Load configuration + let config = Config::load()?; + info!("Configuration loaded successfully"); + + // Initialize metrics + metrics::init_metrics(); + + // Start the compression server + let server = CompressionServer::new(config).await?; + let server_task = tokio::spawn(async move { + if let Err(e) = server.serve().await { + warn!("Server error: {}", e); + } + }); + + // Wait for shutdown signal + tokio::select! { + _ = signal::ctrl_c() => { + info!("Received shutdown signal"); + } + _ = server_task => { + info!("Server task completed"); + } + } + + info!("Compression Engine shutting down"); + Ok(()) +} + +async fn health_check() -> Result<()> { + // Simple health check - verify the service can start + let config = Config::load()?; + info!("Health check: Configuration loaded successfully"); + + // Test compression functionality + let test_data = b"Hello, World! This is a test compression string."; + let compressed = compression::compress_data(test_data, compression::CompressionType::Zstd)?; + let decompressed = compression::decompress_data(&compressed, compression::CompressionType::Zstd)?; + + if test_data == decompressed.as_slice() { + info!("Health check: Compression/decompression test passed"); + std::process::exit(0); + } else { + warn!("Health check: Compression/decompression test failed"); + std::process::exit(1); + } +} \ No newline at end of file diff --git a/services/deduplication-service/Dockerfile b/services/deduplication-service/Dockerfile new file mode 100644 index 0000000..95648d9 --- /dev/null +++ b/services/deduplication-service/Dockerfile @@ -0,0 +1,44 @@ +# Deduplication Service Dockerfile +FROM python:3.11-slim as builder + +WORKDIR /app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better caching +COPY requirements.txt . +RUN pip install --no-cache-dir --user -r requirements.txt + +# Runtime stage +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy installed packages from builder +COPY --from=builder /root/.local /root/.local + +# Copy application code +COPY . . + +# Create non-root user +RUN useradd -r -s /bin/false dedup && \ + chown -R dedup:dedup /app + +USER dedup + +# Make sure scripts are in PATH +ENV PATH=/root/.local/bin:$PATH + +EXPOSE 8084 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD python -c "import requests; requests.get('http://localhost:8084/health')" + +CMD ["python", "main.py"] \ No newline at end of file diff --git a/services/deduplication-service/main.py b/services/deduplication-service/main.py new file mode 100644 index 0000000..ca4c728 --- /dev/null +++ b/services/deduplication-service/main.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +""" +CoreState Deduplication Service +High-performance data deduplication service using content-defined chunking +and multiple hashing algorithms for optimal storage efficiency. +""" + +import asyncio +import logging +import signal +import sys +from contextlib import asynccontextmanager + +import uvicorn +from fastapi import FastAPI, HTTPException, BackgroundTasks +from prometheus_client import make_asgi_app +import structlog + +from deduplication import DeduplicationEngine +from models import ChunkRequest, ChunkResponse, DeduplicationStats +from config import Settings + + +# Configure structured logging +structlog.configure( + processors=[ + structlog.stdlib.filter_by_level, + structlog.stdlib.add_logger_name, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + structlog.processors.UnicodeDecoder(), + structlog.processors.JSONRenderer() + ], + context_class=dict, + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.stdlib.BoundLogger, + cache_logger_on_first_use=True, +) + +logger = structlog.get_logger() + + +# Global deduplication engine +dedup_engine = None + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Application lifespan manager""" + global dedup_engine + + logger.info("Starting CoreState Deduplication Service") + + # Initialize deduplication engine + settings = Settings() + dedup_engine = DeduplicationEngine(settings) + await dedup_engine.initialize() + + # Start background tasks + cleanup_task = asyncio.create_task(dedup_engine.cleanup_expired_chunks()) + + yield + + # Cleanup on shutdown + logger.info("Shutting down Deduplication Service") + cleanup_task.cancel() + await dedup_engine.close() + + +# Create FastAPI app +app = FastAPI( + title="CoreState Deduplication Service", + description="High-performance data deduplication service", + version="2.0.0", + lifespan=lifespan +) + +# Add Prometheus metrics endpoint +metrics_app = make_asgi_app() +app.mount("/metrics", metrics_app) + + +@app.get("/health") +async def health_check(): + """Health check endpoint""" + try: + stats = await dedup_engine.get_stats() + return { + "status": "healthy", + "service": "deduplication-service", + "version": "2.0.0", + "stats": stats + } + except Exception as e: + logger.error("Health check failed", error=str(e)) + raise HTTPException(status_code=503, detail="Service unhealthy") + + +@app.post("/deduplicate", response_model=ChunkResponse) +async def deduplicate_chunk(request: ChunkRequest, background_tasks: BackgroundTasks): + """ + Process a data chunk for deduplication + """ + try: + logger.info("Processing chunk", chunk_id=request.chunk_id, size=len(request.data)) + + result = await dedup_engine.process_chunk( + chunk_id=request.chunk_id, + data=request.data, + metadata=request.metadata + ) + + # Schedule background cleanup if needed + if result.is_duplicate: + background_tasks.add_task(dedup_engine.update_reference_count, result.hash_value, 1) + + logger.info( + "Chunk processed", + chunk_id=request.chunk_id, + is_duplicate=result.is_duplicate, + hash_value=result.hash_value + ) + + return result + + except Exception as e: + logger.error("Failed to process chunk", chunk_id=request.chunk_id, error=str(e)) + raise HTTPException(status_code=500, detail=f"Deduplication failed: {str(e)}") + + +@app.get("/chunk/{hash_value}") +async def get_chunk(hash_value: str): + """ + Retrieve a chunk by its hash value + """ + try: + chunk_data = await dedup_engine.get_chunk(hash_value) + if chunk_data is None: + raise HTTPException(status_code=404, detail="Chunk not found") + + return { + "hash_value": hash_value, + "data": chunk_data, + "size": len(chunk_data) + } + + except HTTPException: + raise + except Exception as e: + logger.error("Failed to retrieve chunk", hash_value=hash_value, error=str(e)) + raise HTTPException(status_code=500, detail=f"Retrieval failed: {str(e)}") + + +@app.delete("/chunk/{hash_value}") +async def delete_chunk(hash_value: str): + """ + Delete a chunk and update reference counts + """ + try: + success = await dedup_engine.delete_chunk(hash_value) + if not success: + raise HTTPException(status_code=404, detail="Chunk not found") + + logger.info("Chunk deleted", hash_value=hash_value) + return {"message": "Chunk deleted successfully"} + + except HTTPException: + raise + except Exception as e: + logger.error("Failed to delete chunk", hash_value=hash_value, error=str(e)) + raise HTTPException(status_code=500, detail=f"Deletion failed: {str(e)}") + + +@app.get("/stats", response_model=DeduplicationStats) +async def get_stats(): + """ + Get deduplication statistics + """ + try: + stats = await dedup_engine.get_stats() + return stats + + except Exception as e: + logger.error("Failed to get stats", error=str(e)) + raise HTTPException(status_code=500, detail=f"Stats retrieval failed: {str(e)}") + + +@app.post("/compact") +async def compact_storage(background_tasks: BackgroundTasks): + """ + Trigger storage compaction to remove unreferenced chunks + """ + try: + background_tasks.add_task(dedup_engine.compact_storage) + return {"message": "Compaction started"} + + except Exception as e: + logger.error("Failed to start compaction", error=str(e)) + raise HTTPException(status_code=500, detail=f"Compaction failed: {str(e)}") + + +def signal_handler(signum, frame): + """Handle shutdown signals""" + logger.info("Received shutdown signal", signal=signum) + sys.exit(0) + + +if __name__ == "__main__": + # Register signal handlers + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # Load settings + settings = Settings() + + # Run the server + uvicorn.run( + "main:app", + host=settings.host, + port=settings.port, + log_level=settings.log_level.lower(), + access_log=True, + reload=settings.debug + ) \ No newline at end of file diff --git a/services/deduplication-service/requirements.txt b/services/deduplication-service/requirements.txt new file mode 100644 index 0000000..5827048 --- /dev/null +++ b/services/deduplication-service/requirements.txt @@ -0,0 +1,57 @@ +# Core web framework +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +pydantic==2.5.0 + +# Database connectivity +psycopg2-binary==2.9.9 +redis==5.0.1 +sqlalchemy==2.0.23 +alembic==1.12.1 + +# Async support +aiofiles==23.2.1 +aioredis==2.0.1 +asyncpg==0.29.0 + +# gRPC support +grpcio==1.59.3 +grpcio-tools==1.59.3 +protobuf==4.25.1 + +# Hashing and deduplication +xxhash==3.4.1 +blake3==0.4.1 +mmh3==4.0.1 + +# Monitoring and logging +prometheus-client==0.19.0 +structlog==23.2.0 + +# Configuration and environment +python-dotenv==1.0.0 +pyyaml==6.0.1 + +# HTTP client +httpx==0.25.2 + +# Data structures +bitarray==2.8.4 +bloom-filter==1.3.3 + +# Testing +pytest==7.4.3 +pytest-cov==4.1.0 +pytest-asyncio==0.21.1 + +# Code quality +black==23.11.0 +flake8==6.1.0 +isort==5.12.0 +mypy==1.7.1 + +# Security +cryptography==41.0.7 + +# Development tools +ipython==8.17.2 \ No newline at end of file diff --git a/services/encryption-service/Dockerfile b/services/encryption-service/Dockerfile new file mode 100644 index 0000000..2b9d53b --- /dev/null +++ b/services/encryption-service/Dockerfile @@ -0,0 +1,38 @@ +# Encryption Service Dockerfile +FROM node:18-alpine as builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ +RUN npm ci --only=production + +# Runtime stage +FROM node:18-alpine + +RUN apk add --no-cache \ + ca-certificates \ + tini + +WORKDIR /app + +# Copy node_modules from builder +COPY --from=builder /app/node_modules ./node_modules + +# Copy application code +COPY . . + +# Create non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S encryption -u 1001 -G nodejs && \ + chown -R encryption:nodejs /app + +USER encryption + +EXPOSE 8085 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD node healthcheck.js + +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/services/encryption-service/package.json b/services/encryption-service/package.json new file mode 100644 index 0000000..701471a --- /dev/null +++ b/services/encryption-service/package.json @@ -0,0 +1,100 @@ +{ + "name": "encryption-service", + "version": "2.0.0", + "description": "High-performance encryption service for CoreState backup system", + "main": "dist/index.js", + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "ts-node-dev --respawn --transpile-only src/index.ts", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint src/**/*.ts", + "lint:fix": "eslint src/**/*.ts --fix", + "type-check": "tsc --noEmit", + "clean": "rm -rf dist", + "docker:build": "docker build -t encryption-service .", + "prestart": "npm run build" + }, + "dependencies": { + "@grpc/grpc-js": "^1.9.7", + "@grpc/proto-loader": "^0.7.10", + "express": "^4.18.2", + "crypto": "^1.0.1", + "node-forge": "^1.3.1", + "argon2": "^0.31.2", + "scrypt": "^6.3.0", + "tweetnacl": "^1.0.3", + "libsodium-wrappers": "^0.7.11", + "uuid": "^9.0.1", + "winston": "^3.11.0", + "dotenv": "^16.3.1", + "helmet": "^7.1.0", + "cors": "^2.8.5", + "compression": "^1.7.4", + "prom-client": "^15.0.0", + "node-cron": "^3.0.3", + "ajv": "^8.12.0", + "jsonwebtoken": "^9.0.2", + "jose": "^5.1.1", + "redis": "^4.6.10", + "ioredis": "^5.3.2" + }, + "devDependencies": { + "typescript": "^5.2.2", + "ts-node-dev": "^2.0.0", + "@types/node": "^20.8.6", + "@types/express": "^4.17.20", + "@types/uuid": "^9.0.6", + "@types/cors": "^2.8.15", + "@types/compression": "^1.7.4", + "@types/jsonwebtoken": "^9.0.4", + "@types/node-cron": "^3.0.9", + "@types/node-forge": "^1.3.9", + "jest": "^29.7.0", + "@types/jest": "^29.5.6", + "ts-jest": "^29.1.1", + "eslint": "^8.52.0", + "@typescript-eslint/eslint-plugin": "^6.9.0", + "@typescript-eslint/parser": "^6.9.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", + "prettier": "^3.0.3", + "nodemon": "^3.0.1", + "supertest": "^6.3.3", + "@types/supertest": "^2.0.15" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "roots": ["/src"], + "testMatch": ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"], + "collectCoverageFrom": [ + "src/**/*.ts", + "!src/**/*.d.ts", + "!src/**/*.test.ts", + "!src/**/*.spec.ts" + ] + }, + "keywords": [ + "encryption", + "crypto", + "security", + "backup", + "aes", + "rsa", + "key-management" + ], + "author": "CoreState Team", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/corestate/CoreState-v2.git", + "directory": "services/encryption-service" + } +} \ No newline at end of file diff --git a/services/index-service/Dockerfile b/services/index-service/Dockerfile new file mode 100644 index 0000000..47abcf8 --- /dev/null +++ b/services/index-service/Dockerfile @@ -0,0 +1,45 @@ +# Index Service Dockerfile +FROM openjdk:17-jdk-slim as builder + +WORKDIR /app + +# Install required tools +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy Gradle files +COPY build.gradle.kts settings.gradle.kts gradlew ./ +COPY gradle/ ./gradle/ + +# Copy source code +COPY src/ ./src/ + +# Build the application +RUN ./gradlew build -x test + +# Runtime stage +FROM openjdk:17-jre-slim + +RUN apt-get update && apt-get install -y \ + ca-certificates \ + curl \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy the JAR from builder stage +COPY --from=builder /app/build/libs/*.jar app.jar + +# Create non-root user +RUN useradd -r -s /bin/false indexer && \ + chown -R indexer:indexer /app + +USER indexer + +EXPOSE 8086 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8086/actuator/health || exit 1 + +CMD ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/services/index-service/build.gradle.kts b/services/index-service/build.gradle.kts new file mode 100644 index 0000000..1574d0b --- /dev/null +++ b/services/index-service/build.gradle.kts @@ -0,0 +1,79 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("org.springframework.boot") version "3.1.5" + id("io.spring.dependency-management") version "1.1.3" + kotlin("jvm") + kotlin("plugin.spring") version "1.8.22" + kotlin("plugin.jpa") version "1.8.22" +} + +group = "com.corestate.services" +version = "2.0.0" + +java { + sourceCompatibility = JavaVersion.VERSION_17 +} + +dependencies { + // Spring Boot core + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-data-elasticsearch") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.boot:spring-boot-starter-validation") + + // Kotlin support + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3") + + // gRPC and Protocol Buffers + implementation("io.grpc:grpc-netty-shaded:1.58.0") + implementation("io.grpc:grpc-protobuf:1.58.0") + implementation("io.grpc:grpc-stub:1.58.0") + implementation("io.grpc:grpc-kotlin-stub:1.4.0") + implementation("com.google.protobuf:protobuf-kotlin:3.24.4") + + // Search and indexing + implementation("org.elasticsearch.client:elasticsearch-rest-high-level-client:7.17.15") + implementation("org.apache.lucene:lucene-core:9.8.0") + implementation("org.apache.lucene:lucene-queryparser:9.8.0") + implementation("org.apache.lucene:lucene-analyzers-common:8.11.2") + + // Database + implementation("org.postgresql:postgresql:42.6.0") + implementation("org.springframework.boot:spring-boot-starter-data-redis") + + // Monitoring and metrics + implementation("io.micrometer:micrometer-registry-prometheus") + implementation("io.micrometer:micrometer-tracing-bridge-brave") + + // Configuration + implementation("org.springframework.cloud:spring-cloud-starter-kubernetes-config:3.0.5") + + // Logging + implementation("io.github.microutils:kotlin-logging-jvm:3.0.5") + + // Testing + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0") + testImplementation("io.kotest:kotest-runner-junit5:5.7.2") + testImplementation("io.kotest:kotest-assertions-core:5.7.2") + testImplementation("org.testcontainers:junit-jupiter:1.19.1") + testImplementation("org.testcontainers:postgresql:1.19.1") + testImplementation("org.testcontainers:elasticsearch:1.19.1") +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs += "-Xjsr305=strict" + jvmTarget = "17" + } +} + +tasks.withType { + useJUnitPlatform() +} \ No newline at end of file diff --git a/services/ml-optimizer/main.py b/services/ml-optimizer/main.py index 70ef8dd..f7053f7 100644 --- a/services/ml-optimizer/main.py +++ b/services/ml-optimizer/main.py @@ -1,17 +1,17 @@ -from fastapi import FastAPI - -app = FastAPI( - title="CoreState ML Optimizer Service", - version="2.0.0", -) - -@app.get("/") -def read_root(): - return {"message": "CoreState ML Optimizer Service is running."} - -@app.post("/predict/backup-window") -def predict_backup_window(data: dict): - # Placeholder for prediction logic - return {"optimal_window_hours": [2, 3, 4, 22, 23]} - +from fastapi import FastAPI + +app = FastAPI( + title="CoreState ML Optimizer Service", + version="2.0.0", +) + +@app.get("/") +def read_root(): + return {"message": "CoreState ML Optimizer Service is running."} + +@app.post("/predict/backup-window") +def predict_backup_window(data: dict): + # Placeholder for prediction logic + return {"optimal_window_hours": [2, 3, 4, 22, 23]} + # Further endpoints for anomaly detection, etc., will be added here. \ No newline at end of file diff --git a/services/ml-optimizer/requirements.txt b/services/ml-optimizer/requirements.txt index 7c26bdd..865496c 100644 --- a/services/ml-optimizer/requirements.txt +++ b/services/ml-optimizer/requirements.txt @@ -1,6 +1,84 @@ -fastapi -uvicorn -tensorflow -scikit-learn -pandas -numpy \ No newline at end of file +# Core ML and data science libraries +scikit-learn==1.3.2 +pandas==2.1.3 +numpy==1.24.4 +scipy==1.11.4 + +# Deep learning +tensorflow==2.14.0 +torch==2.1.1 +torchvision==0.16.1 + +# Data visualization +matplotlib==3.8.2 +seaborn==0.13.0 +plotly==5.17.0 + +# Model persistence and job management +joblib==1.3.2 +pickle5==0.0.11 + +# Data processing +pyarrow==14.0.1 +fastparquet==2023.10.1 +openpyxl==3.1.2 + +# API and web framework +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +pydantic==2.5.0 + +# Database connectivity +psycopg2-binary==2.9.9 +redis==5.0.1 +sqlalchemy==2.0.23 + +# gRPC support +grpcio==1.59.3 +grpcio-tools==1.59.3 +protobuf==4.25.1 + +# Monitoring and logging +prometheus-client==0.19.0 +structlog==23.2.0 + +# Configuration and environment +python-dotenv==1.0.0 +pyyaml==6.0.1 +toml==0.10.2 + +# Time series analysis +statsmodels==0.14.0 +tslearn==0.6.2 + +# Hyperparameter optimization +optuna==3.4.0 +hyperopt==0.2.7 + +# Model validation and metrics +mlflow==2.8.1 +wandb==0.16.0 + +# Testing +pytest==7.4.3 +pytest-cov==4.1.0 +pytest-asyncio==0.21.1 + +# Code quality +black==23.11.0 +flake8==6.1.0 +isort==5.12.0 +mypy==1.7.1 + +# Security +cryptography==41.0.7 +bcrypt==4.1.1 + +# Async support +aiofiles==23.2.1 +aioredis==2.0.1 + +# Development tools +ipython==8.17.2 +jupyter==1.0.0 +notebook==7.0.6 \ No newline at end of file diff --git a/services/storage-hal/Cargo.toml b/services/storage-hal/Cargo.toml index f37d3a7..ff4c56e 100644 --- a/services/storage-hal/Cargo.toml +++ b/services/storage-hal/Cargo.toml @@ -1,14 +1,91 @@ -[package] -name = "storage-hal" -version = "2.0.0" -edition = "2021" - -[dependencies] -reed-solomon-erasure = "4.0" -blake3 = "1.5" -tonic = "0.10" -prost = "0.12" -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } - -[build-dependencies] -tonic-build = "0.10" \ No newline at end of file +[package] +name = "storage-hal" +version = "2.0.0" +edition = "2021" +authors = ["CoreState Team"] +description = "Storage Hardware Abstraction Layer for CoreState backup system" +license = "MIT" +repository = "https://github.com/corestate/CoreState-v2" + +[dependencies] +# Erasure coding and compression +reed-solomon-erasure = "6.0" +blake3 = "1.5" +lz4 = "1.24" +zstd = "0.13" + +# gRPC and async runtime +tonic = "0.10" +prost = "0.12" +tokio = { version = "1.34", features = ["macros", "rt-multi-thread", "fs", "io-util", "net", "time"] } +tokio-util = { version = "0.7", features = ["codec"] } +futures = "0.3" + +# Cloud storage SDKs +aws-sdk-s3 = "0.39" +aws-config = "0.57" +azure_storage = "0.19" +azure_storage_blobs = "0.19" +google-cloud-storage = "0.15" + +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +bincode = "1.3" + +# HTTP client +reqwest = { version = "0.11", features = ["json", "stream"] } + +# Error handling +thiserror = "1.0" +anyhow = "1.0" + +# Logging and metrics +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +prometheus = "0.13" + +# Configuration +config = "0.13" + +# UUID generation +uuid = { version = "1.6", features = ["v4", "serde"] } + +# Date and time +chrono = { version = "0.4", features = ["serde"] } + +# Async channels +tokio-stream = "0.1" + +# File system operations +walkdir = "2.4" + +# Encryption +ring = "0.17" +aes-gcm = "0.10" + +[dev-dependencies] +tokio-test = "0.4" +tempfile = "3.8" +proptest = "1.4" +mockall = "0.12" + +[build-dependencies] +tonic-build = "0.10" + +[features] +default = ["aws", "azure", "gcp"] +aws = [] +azure = [] +gcp = [] +local-only = [] + +[[bin]] +name = "storage-hal" +path = "src/main.rs" + +[profile.release] +lto = true +codegen-units = 1 +panic = "abort" +strip = true \ No newline at end of file diff --git a/services/storage-hal/build.rs b/services/storage-hal/build.rs index 15c9b3e..18a4eaa 100644 --- a/services/storage-hal/build.rs +++ b/services/storage-hal/build.rs @@ -1,12 +1,12 @@ -fn main() -> Result<(), Box> { - tonic_build::configure() - .build_server(true) - .compile( - &[ - "../../shared/proto/storage.proto", - "../../shared/proto/backup.proto", // Compile other protos for potential future use - ], - &["../../shared/proto"], // Specify proto include path - )?; - Ok(()) +fn main() -> Result<(), Box> { + tonic_build::configure() + .build_server(true) + .compile( + &[ + "../../shared/proto/storage.proto", + "../../shared/proto/backup.proto", // Compile other protos for potential future use + ], + &["../../shared/proto"], // Specify proto include path + )?; + Ok(()) } \ No newline at end of file diff --git a/services/storage-hal/src/erasure_coding.rs b/services/storage-hal/src/erasure_coding.rs index d065a54..6ea9d63 100644 --- a/services/storage-hal/src/erasure_coding.rs +++ b/services/storage-hal/src/erasure_coding.rs @@ -1,98 +1,98 @@ -use reed_solomon_erasure::ReedSolomon; -use blake3::hash as calculate_blake3; - -// --- Custom Error and Data Structures --- - -#[derive(Debug)] -pub enum ErasureError { - InsufficientShards, - EncodingError(reed_solomon_erasure::Error), -} - -impl From for ErasureError { - fn from(e: reed_solomon_erasure::Error) -> Self { - ErasureError::EncodingError(e) - } -} - -#[derive(PartialEq, Debug)] -pub enum ShardType { - Data, - Parity, -} - -#[derive(Debug)] -pub struct Shard { - pub index: usize, - pub data: Vec, - pub checksum: blake3::Hash, - pub shard_type: ShardType, -} - -// --- ErasureCoder Implementation --- - -pub struct ErasureCoder { - data_shards: usize, - parity_shards: usize, - encoder: ReedSolomon, -} - -impl ErasureCoder { - pub fn new(data_shards: usize, parity_shards: usize) -> Result { - let encoder = ReedSolomon::new(data_shards, parity_shards)?; - Ok(Self { - data_shards, - parity_shards, - encoder, - }) - } - - pub fn encode_backup(&self, data: &[u8]) -> Result, ErasureError> { - let shard_size = (data.len() + self.data_shards - 1) / self.data_shards; - let mut shards_data: Vec> = vec![vec![0u8; shard_size]; self.data_shards + self.parity_shards]; - - for (i, chunk) in data.chunks(shard_size).enumerate() { - shards_data[i][..chunk.len()].copy_from_slice(chunk); - } - - self.encoder.encode(&mut shards_data)?; - - Ok(shards_data.into_iter().enumerate().map(|(index, data)| { - Shard { - index, - checksum: calculate_blake3(&data), - shard_type: if index < self.data_shards { - ShardType::Data - } else { - ShardType::Parity - }, - data, - } - }).collect()) - } - - pub fn reconstruct_backup(&self, available_shards: Vec>) -> Result, ErasureError> { - if available_shards.iter().filter(|s| s.is_some()).count() < self.data_shards { - return Err(ErasureError::InsufficientShards); - } - - let mut shards_data: Vec>> = available_shards - .into_iter() - .map(|shard| shard.map(|s| s.data)) - .collect(); - - self.encoder.reconstruct(&mut shards_data)?; - - let mut result = Vec::new(); - for shard_opt in shards_data.iter().take(self.data_shards) { - if let Some(data) = shard_opt { - result.extend_from_slice(data); - } else { - // This should not happen if reconstruction was successful - return Err(ErasureError::InsufficientShards); - } - } - - Ok(result) - } +use reed_solomon_erasure::ReedSolomon; +use blake3::hash as calculate_blake3; + +// --- Custom Error and Data Structures --- + +#[derive(Debug)] +pub enum ErasureError { + InsufficientShards, + EncodingError(reed_solomon_erasure::Error), +} + +impl From for ErasureError { + fn from(e: reed_solomon_erasure::Error) -> Self { + ErasureError::EncodingError(e) + } +} + +#[derive(PartialEq, Debug)] +pub enum ShardType { + Data, + Parity, +} + +#[derive(Debug)] +pub struct Shard { + pub index: usize, + pub data: Vec, + pub checksum: blake3::Hash, + pub shard_type: ShardType, +} + +// --- ErasureCoder Implementation --- + +pub struct ErasureCoder { + data_shards: usize, + parity_shards: usize, + encoder: ReedSolomon, +} + +impl ErasureCoder { + pub fn new(data_shards: usize, parity_shards: usize) -> Result { + let encoder = ReedSolomon::new(data_shards, parity_shards)?; + Ok(Self { + data_shards, + parity_shards, + encoder, + }) + } + + pub fn encode_backup(&self, data: &[u8]) -> Result, ErasureError> { + let shard_size = (data.len() + self.data_shards - 1) / self.data_shards; + let mut shards_data: Vec> = vec![vec![0u8; shard_size]; self.data_shards + self.parity_shards]; + + for (i, chunk) in data.chunks(shard_size).enumerate() { + shards_data[i][..chunk.len()].copy_from_slice(chunk); + } + + self.encoder.encode(&mut shards_data)?; + + Ok(shards_data.into_iter().enumerate().map(|(index, data)| { + Shard { + index, + checksum: calculate_blake3(&data), + shard_type: if index < self.data_shards { + ShardType::Data + } else { + ShardType::Parity + }, + data, + } + }).collect()) + } + + pub fn reconstruct_backup(&self, available_shards: Vec>) -> Result, ErasureError> { + if available_shards.iter().filter(|s| s.is_some()).count() < self.data_shards { + return Err(ErasureError::InsufficientShards); + } + + let mut shards_data: Vec>> = available_shards + .into_iter() + .map(|shard| shard.map(|s| s.data)) + .collect(); + + self.encoder.reconstruct(&mut shards_data)?; + + let mut result = Vec::new(); + for shard_opt in shards_data.iter().take(self.data_shards) { + if let Some(data) = shard_opt { + result.extend_from_slice(data); + } else { + // This should not happen if reconstruction was successful + return Err(ErasureError::InsufficientShards); + } + } + + Ok(result) + } } \ No newline at end of file diff --git a/services/storage-hal/src/main.rs b/services/storage-hal/src/main.rs index 07d95e8..293d9c0 100644 --- a/services/storage-hal/src/main.rs +++ b/services/storage-hal/src/main.rs @@ -1,91 +1,91 @@ -mod erasure_coding; - -use tonic::{transport::Server, Request, Response, Status}; -use erasure_coding::{ErasureCoder, ErasureError}; - -// Import generated protobuf code -// The actual name will be based on the package defined in the .proto file -pub mod storage { - tonic::include_proto!("corestate.v2.storage"); -} -use storage::{ - storage_hal_server::{StorageHal, StorageHalServer}, - EncodeRequest, EncodeResponse, ReconstructRequest, ReconstructResponse, Shard, -}; - -// --- gRPC Service Implementation --- - -#[derive(Default)] -pub struct StorageHalService {} - -#[tonic::async_trait] -impl StorageHal for StorageHalService { - async fn encode( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - - let coder = ErasureCoder::new(req.data_shards as usize, req.parity_shards as usize) - .map_err(|e| Status::invalid_argument(format!("Failed to create encoder: {:?}", e)))?; - - let encoded_shards = coder.encode_backup(&req.data) - .map_err(|e| Status::internal(format!("Encoding failed: {:?}", e)))?; - - let proto_shards = encoded_shards.into_iter().map(|s| Shard { - index: s.index as u32, - data: s.data, - checksum: s.checksum.as_bytes().to_vec(), - r#type: match s.shard_type { - erasure_coding::ShardType::Data => 0, - erasure_coding::ShardType::Parity => 1, - }, - }).collect(); - - Ok(Response::new(EncodeResponse { shards: proto_shards })) - } - - async fn reconstruct( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - - let coder = ErasureCoder::new(req.data_shards as usize, req.parity_shards as usize) - .map_err(|e| Status::invalid_argument(format!("Failed to create encoder: {:?}", e)))?; - - let shards_to_reconstruct = req.shards.into_iter().map(|s| { - Some(erasure_coding::Shard { - index: s.index as usize, - data: s.data, - checksum: blake3::Hash::from_slice(&s.checksum), - shard_type: match s.r#type { - 0 => erasure_coding::ShardType::Data, - _ => erasure_coding::ShardType::Parity, - }, - }) - }).collect(); - - let reconstructed_data = coder.reconstruct_backup(shards_to_reconstruct) - .map_err(|e| Status::internal(format!("Reconstruction failed: {:?}", e)))?; - - Ok(Response::new(ReconstructResponse { data: reconstructed_data })) - } -} - -// --- Server Entry Point --- - -#[tokio::main] -async fn main() -> Result<(), Box> { - let addr = "[::1]:50051".parse()?; - let storage_service = StorageHalService::default(); - - println!("StorageHAL gRPC server listening on {}", addr); - - Server::builder() - .add_service(StorageHalServer::new(storage_service)) - .serve(addr) - .await?; - - Ok(()) +mod erasure_coding; + +use tonic::{transport::Server, Request, Response, Status}; +use erasure_coding::{ErasureCoder, ErasureError}; + +// Import generated protobuf code +// The actual name will be based on the package defined in the .proto file +pub mod storage { + tonic::include_proto!("corestate.v2.storage"); +} +use storage::{ + storage_hal_server::{StorageHal, StorageHalServer}, + EncodeRequest, EncodeResponse, ReconstructRequest, ReconstructResponse, Shard, +}; + +// --- gRPC Service Implementation --- + +#[derive(Default)] +pub struct StorageHalService {} + +#[tonic::async_trait] +impl StorageHal for StorageHalService { + async fn encode( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + + let coder = ErasureCoder::new(req.data_shards as usize, req.parity_shards as usize) + .map_err(|e| Status::invalid_argument(format!("Failed to create encoder: {:?}", e)))?; + + let encoded_shards = coder.encode_backup(&req.data) + .map_err(|e| Status::internal(format!("Encoding failed: {:?}", e)))?; + + let proto_shards = encoded_shards.into_iter().map(|s| Shard { + index: s.index as u32, + data: s.data, + checksum: s.checksum.as_bytes().to_vec(), + r#type: match s.shard_type { + erasure_coding::ShardType::Data => 0, + erasure_coding::ShardType::Parity => 1, + }, + }).collect(); + + Ok(Response::new(EncodeResponse { shards: proto_shards })) + } + + async fn reconstruct( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + + let coder = ErasureCoder::new(req.data_shards as usize, req.parity_shards as usize) + .map_err(|e| Status::invalid_argument(format!("Failed to create encoder: {:?}", e)))?; + + let shards_to_reconstruct = req.shards.into_iter().map(|s| { + Some(erasure_coding::Shard { + index: s.index as usize, + data: s.data, + checksum: blake3::Hash::from_slice(&s.checksum), + shard_type: match s.r#type { + 0 => erasure_coding::ShardType::Data, + _ => erasure_coding::ShardType::Parity, + }, + }) + }).collect(); + + let reconstructed_data = coder.reconstruct_backup(shards_to_reconstruct) + .map_err(|e| Status::internal(format!("Reconstruction failed: {:?}", e)))?; + + Ok(Response::new(ReconstructResponse { data: reconstructed_data })) + } +} + +// --- Server Entry Point --- + +#[tokio::main] +async fn main() -> Result<(), Box> { + let addr = "[::1]:50051".parse()?; + let storage_service = StorageHalService::default(); + + println!("StorageHAL gRPC server listening on {}", addr); + + Server::builder() + .add_service(StorageHalServer::new(storage_service)) + .serve(addr) + .await?; + + Ok(()) } \ No newline at end of file diff --git a/services/sync-coordinator/package.json b/services/sync-coordinator/package.json index 9f17918..d34b6bd 100644 --- a/services/sync-coordinator/package.json +++ b/services/sync-coordinator/package.json @@ -1,17 +1,116 @@ -{ - "name": "sync-coordinator", - "version": "2.0.0", - "description": "Handles real-time state sync using CRDTs.", - "main": "dist/index.js", - "scripts": { - "build": "tsc", - "start": "node dist/index.js", - "dev": "ts-node-dev src/index.ts" - }, - "dependencies": {}, - "devDependencies": { - "typescript": "^5.0.0", - "ts-node-dev": "^2.0.0", - "@types/node": "^20.0.0" - } +{ + "name": "sync-coordinator", + "version": "2.0.0", + "description": "Handles real-time state sync using CRDTs for CoreState backup system", + "main": "dist/index.js", + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "ts-node-dev --respawn --transpile-only src/index.ts", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint src/**/*.ts", + "lint:fix": "eslint src/**/*.ts --fix", + "type-check": "tsc --noEmit", + "clean": "rm -rf dist", + "docker:build": "docker build -t sync-coordinator .", + "prestart": "npm run build" + }, + "dependencies": { + "@grpc/grpc-js": "^1.9.7", + "@grpc/proto-loader": "^0.7.10", + "yjs": "^13.6.8", + "y-protocols": "^1.0.6", + "ws": "^8.14.2", + "express": "^4.18.2", + "redis": "^4.6.10", + "ioredis": "^5.3.2", + "uuid": "^9.0.1", + "winston": "^3.11.0", + "dotenv": "^16.3.1", + "helmet": "^7.1.0", + "cors": "^2.8.5", + "compression": "^1.7.4", + "prom-client": "^15.0.0", + "node-cron": "^3.0.3", + "ajv": "^8.12.0", + "jsonwebtoken": "^9.0.2" + }, + "devDependencies": { + "typescript": "^5.2.2", + "ts-node-dev": "^2.0.0", + "@types/node": "^20.8.6", + "@types/express": "^4.17.20", + "@types/ws": "^8.5.8", + "@types/uuid": "^9.0.6", + "@types/cors": "^2.8.15", + "@types/compression": "^1.7.4", + "@types/jsonwebtoken": "^9.0.4", + "@types/node-cron": "^3.0.9", + "jest": "^29.7.0", + "@types/jest": "^29.5.6", + "ts-jest": "^29.1.1", + "eslint": "^8.52.0", + "@typescript-eslint/eslint-plugin": "^6.9.0", + "@typescript-eslint/parser": "^6.9.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", + "prettier": "^3.0.3", + "nodemon": "^3.0.1", + "supertest": "^6.3.3", + "@types/supertest": "^2.0.15" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "roots": ["/src"], + "testMatch": ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"], + "collectCoverageFrom": [ + "src/**/*.ts", + "!src/**/*.d.ts", + "!src/**/*.test.ts", + "!src/**/*.spec.ts" + ] + }, + "eslintConfig": { + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "@typescript-eslint/recommended", + "prettier" + ], + "rules": { + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/explicit-function-return-type": "warn", + "no-console": "warn" + } + }, + "prettier": { + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2 + }, + "keywords": [ + "crdt", + "sync", + "backup", + "distributed", + "real-time", + "coordination" + ], + "author": "CoreState Team", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/corestate/CoreState-v2.git", + "directory": "services/sync-coordinator" + } } \ No newline at end of file diff --git a/services/sync-coordinator/src/crdt/backup-state.ts b/services/sync-coordinator/src/crdt/backup-state.ts index e48ef1a..c68fadd 100644 --- a/services/sync-coordinator/src/crdt/backup-state.ts +++ b/services/sync-coordinator/src/crdt/backup-state.ts @@ -1,131 +1,131 @@ -// --- Placeholder CRDT Implementations --- -// This would be replaced by a real CRDT library like @corestate/crdt - -class GCounter { - constructor(public nodeId: string) {} - value(): number { return 0; } - merge(other: GCounter) {} -} -class PNCounter {} -class LWWRegister { - constructor(public nodeId: string) {} - value(): T | null { return null; } - set(value: T, timestamp: number) {} - merge(other: LWWRegister): { hasConflict: boolean } { return { hasConflict: false }; } -} -class ORSet { - constructor(public nodeId: string) {} - add(value: T) {} - remove(value: T) {} - contains(value: T): boolean { return false; } - size(): number { return 0; } - merge(other: ORSet) {} -} - -// --- Placeholder Data Structures --- - -interface FileVersion { - path: string; - timestamp: number; -} - -interface FileConflict { - type: 'delete-update' | 'update-update'; - path: string; - localState: FileVersion | null; - remoteState: FileVersion | null; -} - -interface MergeResult { - conflicts: FileConflict[]; - resolved: any; // Placeholder for resolved state - stats: { - filesAdded: number; - filesDeleted: number; - totalBackups: number; - }; -} - -class ConflictResolver { - resolve(conflicts: FileConflict[]): any { - // Placeholder: default to keeping the remote state in case of conflict - console.log(`Resolving ${conflicts.length} conflicts.`); - return {}; - } -} - -// --- Main BackupStateCRDT Class --- - -export class BackupStateCRDT { - private fileVersions: Map>; - private deletedFiles: ORSet; - private backupCounter: GCounter; - private conflictResolver: ConflictResolver; - private nodeId: string; - - constructor(nodeId: string) { - this.nodeId = nodeId; - this.fileVersions = new Map(); - this.deletedFiles = new ORSet(nodeId); - this.backupCounter = new GCounter(nodeId); - this.conflictResolver = new ConflictResolver(); - } - - updateFile(filePath: string, version: FileVersion): void { - if (!this.fileVersions.has(filePath)) { - this.fileVersions.set(filePath, new LWWRegister(this.nodeId)); - } - - const register = this.fileVersions.get(filePath)!; - register.set(version, version.timestamp); - - this.deletedFiles.remove(filePath); - } - - deleteFile(filePath: string): void { - this.deletedFiles.add(filePath); - this.fileVersions.delete(filePath); - } - - merge(other: BackupStateCRDT): MergeResult { - const conflicts: FileConflict[] = []; - - for (const [path, otherRegister] of other.fileVersions) { - if (this.deletedFiles.contains(path)) { - conflicts.push({ - type: 'delete-update', - path, - localState: null, - remoteState: otherRegister.value() - }); - } else if (this.fileVersions.has(path)) { - const localRegister = this.fileVersions.get(path)!; - const mergeResult = localRegister.merge(otherRegister); - - if (mergeResult.hasConflict) { - conflicts.push({ - type: 'update-update', - path, - localState: localRegister.value(), - remoteState: otherRegister.value() - }); - } - } else { - this.fileVersions.set(path, otherRegister); - } - } - - this.deletedFiles.merge(other.deletedFiles); - this.backupCounter.merge(other.backupCounter); - - return { - conflicts, - resolved: this.conflictResolver.resolve(conflicts), - stats: { - filesAdded: this.fileVersions.size, - filesDeleted: this.deletedFiles.size(), - totalBackups: this.backupCounter.value() - } - }; - } +// --- Placeholder CRDT Implementations --- +// This would be replaced by a real CRDT library like @corestate/crdt + +class GCounter { + constructor(public nodeId: string) {} + value(): number { return 0; } + merge(other: GCounter) {} +} +class PNCounter {} +class LWWRegister { + constructor(public nodeId: string) {} + value(): T | null { return null; } + set(value: T, timestamp: number) {} + merge(other: LWWRegister): { hasConflict: boolean } { return { hasConflict: false }; } +} +class ORSet { + constructor(public nodeId: string) {} + add(value: T) {} + remove(value: T) {} + contains(value: T): boolean { return false; } + size(): number { return 0; } + merge(other: ORSet) {} +} + +// --- Placeholder Data Structures --- + +interface FileVersion { + path: string; + timestamp: number; +} + +interface FileConflict { + type: 'delete-update' | 'update-update'; + path: string; + localState: FileVersion | null; + remoteState: FileVersion | null; +} + +interface MergeResult { + conflicts: FileConflict[]; + resolved: any; // Placeholder for resolved state + stats: { + filesAdded: number; + filesDeleted: number; + totalBackups: number; + }; +} + +class ConflictResolver { + resolve(conflicts: FileConflict[]): any { + // Placeholder: default to keeping the remote state in case of conflict + console.log(`Resolving ${conflicts.length} conflicts.`); + return {}; + } +} + +// --- Main BackupStateCRDT Class --- + +export class BackupStateCRDT { + private fileVersions: Map>; + private deletedFiles: ORSet; + private backupCounter: GCounter; + private conflictResolver: ConflictResolver; + private nodeId: string; + + constructor(nodeId: string) { + this.nodeId = nodeId; + this.fileVersions = new Map(); + this.deletedFiles = new ORSet(nodeId); + this.backupCounter = new GCounter(nodeId); + this.conflictResolver = new ConflictResolver(); + } + + updateFile(filePath: string, version: FileVersion): void { + if (!this.fileVersions.has(filePath)) { + this.fileVersions.set(filePath, new LWWRegister(this.nodeId)); + } + + const register = this.fileVersions.get(filePath)!; + register.set(version, version.timestamp); + + this.deletedFiles.remove(filePath); + } + + deleteFile(filePath: string): void { + this.deletedFiles.add(filePath); + this.fileVersions.delete(filePath); + } + + merge(other: BackupStateCRDT): MergeResult { + const conflicts: FileConflict[] = []; + + for (const [path, otherRegister] of other.fileVersions) { + if (this.deletedFiles.contains(path)) { + conflicts.push({ + type: 'delete-update', + path, + localState: null, + remoteState: otherRegister.value() + }); + } else if (this.fileVersions.has(path)) { + const localRegister = this.fileVersions.get(path)!; + const mergeResult = localRegister.merge(otherRegister); + + if (mergeResult.hasConflict) { + conflicts.push({ + type: 'update-update', + path, + localState: localRegister.value(), + remoteState: otherRegister.value() + }); + } + } else { + this.fileVersions.set(path, otherRegister); + } + } + + this.deletedFiles.merge(other.deletedFiles); + this.backupCounter.merge(other.backupCounter); + + return { + conflicts, + resolved: this.conflictResolver.resolve(conflicts), + stats: { + filesAdded: this.fileVersions.size, + filesDeleted: this.deletedFiles.size(), + totalBackups: this.backupCounter.value() + } + }; + } } \ No newline at end of file diff --git a/services/sync-coordinator/src/index.ts b/services/sync-coordinator/src/index.ts index d2363ff..820a888 100644 --- a/services/sync-coordinator/src/index.ts +++ b/services/sync-coordinator/src/index.ts @@ -1,2 +1,2 @@ -console.log("CoreState Sync Coordinator Service v2.0 starting..."); +console.log("CoreState Sync Coordinator Service v2.0 starting..."); // gRPC server initialization will go here \ No newline at end of file diff --git a/services/sync-coordinator/tsconfig.json b/services/sync-coordinator/tsconfig.json index dba9d7f..f152a57 100644 --- a/services/sync-coordinator/tsconfig.json +++ b/services/sync-coordinator/tsconfig.json @@ -1,11 +1,11 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true - } +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 1e0754b..29ef3ad 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,26 +1,26 @@ -// CoreState-v2/settings.gradle.kts -pluginManagement { - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - } -} - -rootProject.name = "CoreState-v2" - -// Include application modules -include(":apps:android:androidApp") -include(":apps:android:shared") - -// Include service modules -include(":services:backup-engine") -include(":services:analytics-engine") +// CoreState-v2/settings.gradle.kts +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "CoreState-v2" + +// Include application modules +include(":apps:android:androidApp") +include(":apps:android:shared") + +// Include service modules +include(":services:backup-engine") +include(":services:analytics-engine") // Add other service modules here as they are implemented \ No newline at end of file diff --git a/shared/proto/analytics.proto b/shared/proto/analytics.proto index 223b0e1..97347ef 100644 --- a/shared/proto/analytics.proto +++ b/shared/proto/analytics.proto @@ -1,50 +1,50 @@ -syntax = "proto3"; - -package corestate.v2.analytics; - -option java_package = "com.corestate.v2.proto.analytics"; -option java_multiple_files = true; - -// An event to be logged for analytics -message BackupEvent { - string event_id = 1; - int64 timestamp = 2; - string device_id = 3; - string tenant_id = 4; - string user_id = 5; - - oneof event_payload { - BackupStarted started = 6; - BackupCompleted completed = 7; - BackupFailed failed = 8; - } -} - -message BackupStarted { - string backup_id = 1; - string backup_type = 2; -} - -message BackupCompleted { - string backup_id = 1; - int64 final_size_bytes = 2; - int64 duration_seconds = 3; - int32 file_count = 4; - double deduplication_ratio = 5; - double compression_ratio = 6; -} - -message BackupFailed { - string backup_id = 1; - string error_code = 2; - string error_message = 3; -} - -service AnalyticsService { - // Fire-and-forget event logging - rpc LogEvent(BackupEvent) returns (LogEventResponse); -} - -message LogEventResponse { - bool acknowledged = 1; +syntax = "proto3"; + +package corestate.v2.analytics; + +option java_package = "com.corestate.v2.proto.analytics"; +option java_multiple_files = true; + +// An event to be logged for analytics +message BackupEvent { + string event_id = 1; + int64 timestamp = 2; + string device_id = 3; + string tenant_id = 4; + string user_id = 5; + + oneof event_payload { + BackupStarted started = 6; + BackupCompleted completed = 7; + BackupFailed failed = 8; + } +} + +message BackupStarted { + string backup_id = 1; + string backup_type = 2; +} + +message BackupCompleted { + string backup_id = 1; + int64 final_size_bytes = 2; + int64 duration_seconds = 3; + int32 file_count = 4; + double deduplication_ratio = 5; + double compression_ratio = 6; +} + +message BackupFailed { + string backup_id = 1; + string error_code = 2; + string error_message = 3; +} + +service AnalyticsService { + // Fire-and-forget event logging + rpc LogEvent(BackupEvent) returns (LogEventResponse); +} + +message LogEventResponse { + bool acknowledged = 1; } \ No newline at end of file diff --git a/shared/proto/backup.proto b/shared/proto/backup.proto index c84c467..d72cb2c 100644 --- a/shared/proto/backup.proto +++ b/shared/proto/backup.proto @@ -1,64 +1,64 @@ -syntax = "proto3"; - -package corestate.v2.backup; - -option java_package = "com.corestate.v2.proto.backup"; -option java_multiple_files = true; - -// Represents a single chunk of a file -message DataChunk { - string chunk_id = 1; // Blake3 hash of the content - bytes data = 2; -} - -// Metadata for a single file in a backup -message FileMetadata { - string path = 1; - int64 size = 2; - int64 modified_time = 3; - string checksum = 4; // Blake3 checksum of the full file - repeated string chunk_ids = 5; -} - -// A complete backup manifest -message BackupManifest { - string backup_id = 1; - int64 timestamp = 2; - enum BackupType { - FULL = 0; - INCREMENTAL = 1; - SYNTHETIC_FULL = 2; - } - BackupType type = 3; - repeated FileMetadata files = 4; -} - -service BackupService { - rpc StartBackup(StartBackupRequest) returns (StartBackupResponse); - rpc UploadChunk(stream DataChunk) returns (UploadChunkResponse); - rpc FinishBackup(FinishBackupRequest) returns (FinishBackupResponse); -} - -message StartBackupRequest { - BackupManifest.BackupType type = 1; - string device_id = 2; -} - -message StartBackupResponse { - string backup_id = 1; - string upload_token = 2; -} - -message UploadChunkResponse { - string chunk_id = 1; - bool success = 2; -} - -message FinishBackupRequest { - string backup_id = 1; - BackupManifest manifest = 2; -} - -message FinishBackupResponse { - bool success = 1; +syntax = "proto3"; + +package corestate.v2.backup; + +option java_package = "com.corestate.v2.proto.backup"; +option java_multiple_files = true; + +// Represents a single chunk of a file +message DataChunk { + string chunk_id = 1; // Blake3 hash of the content + bytes data = 2; +} + +// Metadata for a single file in a backup +message FileMetadata { + string path = 1; + int64 size = 2; + int64 modified_time = 3; + string checksum = 4; // Blake3 checksum of the full file + repeated string chunk_ids = 5; +} + +// A complete backup manifest +message BackupManifest { + string backup_id = 1; + int64 timestamp = 2; + enum BackupType { + FULL = 0; + INCREMENTAL = 1; + SYNTHETIC_FULL = 2; + } + BackupType type = 3; + repeated FileMetadata files = 4; +} + +service BackupService { + rpc StartBackup(StartBackupRequest) returns (StartBackupResponse); + rpc UploadChunk(stream DataChunk) returns (UploadChunkResponse); + rpc FinishBackup(FinishBackupRequest) returns (FinishBackupResponse); +} + +message StartBackupRequest { + BackupManifest.BackupType type = 1; + string device_id = 2; +} + +message StartBackupResponse { + string backup_id = 1; + string upload_token = 2; +} + +message UploadChunkResponse { + string chunk_id = 1; + bool success = 2; +} + +message FinishBackupRequest { + string backup_id = 1; + BackupManifest manifest = 2; +} + +message FinishBackupResponse { + bool success = 1; } \ No newline at end of file diff --git a/shared/proto/storage.proto b/shared/proto/storage.proto index 2776d54..2e2c00f 100644 --- a/shared/proto/storage.proto +++ b/shared/proto/storage.proto @@ -1,51 +1,51 @@ -syntax = "proto3"; - -package corestate.v2.storage; - -option java_package = "com.corestate.v2.proto.storage"; -option java_multiple_files = true; - -// Represents a single data or parity shard -message Shard { - uint32 index = 1; - bytes data = 2; - bytes checksum = 3; // Blake3 checksum - enum ShardType { - DATA = 0; - PARITY = 1; - } - ShardType type = 4; -} - -// Request to encode a block of data into shards -message EncodeRequest { - bytes data = 1; - uint32 data_shards = 2; - uint32 parity_shards = 3; -} - -// Response containing the generated shards -message EncodeResponse { - repeated Shard shards = 1; -} - -// Request to reconstruct data from available shards -message ReconstructRequest { - repeated Shard shards = 1; // Include all available shards, even if empty - uint32 data_shards = 2; - uint32 parity_shards = 3; -} - -// Response containing the reconstructed data -message ReconstructResponse { - bytes data = 1; -} - -// The Storage Hardware Abstraction Layer service -service StorageHAL { - // Encodes a block of data using Reed-Solomon erasure coding - rpc Encode(EncodeRequest) returns (EncodeResponse); - - // Reconstructs a block of data from its shards - rpc Reconstruct(ReconstructRequest) returns (ReconstructResponse); +syntax = "proto3"; + +package corestate.v2.storage; + +option java_package = "com.corestate.v2.proto.storage"; +option java_multiple_files = true; + +// Represents a single data or parity shard +message Shard { + uint32 index = 1; + bytes data = 2; + bytes checksum = 3; // Blake3 checksum + enum ShardType { + DATA = 0; + PARITY = 1; + } + ShardType type = 4; +} + +// Request to encode a block of data into shards +message EncodeRequest { + bytes data = 1; + uint32 data_shards = 2; + uint32 parity_shards = 3; +} + +// Response containing the generated shards +message EncodeResponse { + repeated Shard shards = 1; +} + +// Request to reconstruct data from available shards +message ReconstructRequest { + repeated Shard shards = 1; // Include all available shards, even if empty + uint32 data_shards = 2; + uint32 parity_shards = 3; +} + +// Response containing the reconstructed data +message ReconstructResponse { + bytes data = 1; +} + +// The Storage Hardware Abstraction Layer service +service StorageHAL { + // Encodes a block of data using Reed-Solomon erasure coding + rpc Encode(EncodeRequest) returns (EncodeResponse); + + // Reconstructs a block of data from its shards + rpc Reconstruct(ReconstructRequest) returns (ReconstructResponse); } \ No newline at end of file diff --git a/shared/proto/sync.proto b/shared/proto/sync.proto index e6cc10a..ba5bbc9 100644 --- a/shared/proto/sync.proto +++ b/shared/proto/sync.proto @@ -1,27 +1,27 @@ -syntax = "proto3"; - -package corestate.v2.sync; - -option java_package = "com.corestate.v2.proto.sync"; -option java_multiple_files = true; - -// CRDT-based state for a file -message FileVersion { - string path = 1; - int64 version = 2; // Lamport timestamp - int64 timestamp = 3; // Wall clock time - string checksum = 4; - string node_id = 5; -} - -// Represents the sync state of a device -message SyncState { - string node_id = 1; - map file_versions = 2; // LWW-Register Set - repeated string deleted_files = 3; // OR-Set -} - -service SyncService { - // P2P sync between devices - rpc SyncWithPeer(stream SyncState) returns (stream SyncState); +syntax = "proto3"; + +package corestate.v2.sync; + +option java_package = "com.corestate.v2.proto.sync"; +option java_multiple_files = true; + +// CRDT-based state for a file +message FileVersion { + string path = 1; + int64 version = 2; // Lamport timestamp + int64 timestamp = 3; // Wall clock time + string checksum = 4; + string node_id = 5; +} + +// Represents the sync state of a device +message SyncState { + string node_id = 1; + map file_versions = 2; // LWW-Register Set + repeated string deleted_files = 3; // OR-Set +} + +service SyncService { + // P2P sync between devices + rpc SyncWithPeer(stream SyncState) returns (stream SyncState); } \ No newline at end of file diff --git a/tools/cli/main.py b/tools/cli/main.py index e079eeb..df73ed5 100644 --- a/tools/cli/main.py +++ b/tools/cli/main.py @@ -1,29 +1,29 @@ -import click - -@click.group() -def cli(): - """CoreState v2.0 Command-Line Interface.""" - pass - -@cli.command() -@click.option('--backup-id', required=True, help='The ID of the backup to restore.') -@click.option('--destination', default='/tmp/restore', help='The destination path for the restore.') -def restore(backup_id, destination): - """ - Restores a specific backup to a local destination. - """ - click.echo(f"Initiating restore for backup {backup_id} to {destination}...") - # Restore logic would be implemented here - click.echo("Restore placeholder complete.") - -@cli.command() -def status(): - """ - Checks the status of the CoreState services. - """ - click.echo("Checking service status...") - # Service status check logic would be implemented here - click.echo("All services are operational (placeholder).") - -if __name__ == '__main__': +import click + +@click.group() +def cli(): + """CoreState v2.0 Command-Line Interface.""" + pass + +@cli.command() +@click.option('--backup-id', required=True, help='The ID of the backup to restore.') +@click.option('--destination', default='/tmp/restore', help='The destination path for the restore.') +def restore(backup_id, destination): + """ + Restores a specific backup to a local destination. + """ + click.echo(f"Initiating restore for backup {backup_id} to {destination}...") + # Restore logic would be implemented here + click.echo("Restore placeholder complete.") + +@cli.command() +def status(): + """ + Checks the status of the CoreState services. + """ + click.echo("Checking service status...") + # Service status check logic would be implemented here + click.echo("All services are operational (placeholder).") + +if __name__ == '__main__': cli() \ No newline at end of file