OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / backend-manager.js

317 lines · javascript ref:
import { GitBrowser } from './git-browser.js'
import { DirectGitBackend } from './backends/direct-git.js'
import { HyperdriveBackend } from './backends/hyperdrive.js'

/**
 * Backend Manager
 *
 * Routes repository operations to appropriate storage backend.
 * Supports: direct-git (bare git repos) and hyperdrive (P2P content).
 * Per-repo configuration stored in RepoRegistry.
 */
export class BackendManager {
  constructor(options = {}) {
    this.registry = options.registry
    this.gitBrowser = new GitBrowser(options)

    // Initialize backends
    this.backends = {
      'direct-git': new DirectGitBackend(this.gitBrowser),
      'hyperdrive': new HyperdriveBackend({
        corestore: options.corestore,
        swarm: options.hyperswarm,
        gitBrowser: this.gitBrowser
      })
    }

    // Default strategy
    this.defaultStrategy = options.defaultStrategy || 'direct-git'
  }

  /**
   * Get backend for a repository
   * @param {string} repoName - Repository name
   * @returns {Promise<RepositoryBackend>}
   */
  async getBackend(repoName) {
    // Get repo configuration from registry
    if (this.registry) {
      const repoData = await this.registry.get(repoName)

      if (repoData && repoData.storage && repoData.storage.strategy) {
        const strategy = repoData.storage.strategy
        const backend = this.backends[strategy]

        if (backend) {
          return backend
        }

        console.warn(`[BackendManager] Unknown strategy "${strategy}" for ${repoName}, using default`)
      }
    }

    // Return default backend
    return this.backends[this.defaultStrategy]
  }

  /**
   * Seed all Hyperdrives for repos with hyperdrive strategy
   * Called on server startup after repos are registered
   */
  async seedHyperdrives(repoNames) {
    if (!this.registry) return

    const hyperdriveRepos = []
    for (const repoName of repoNames) {
      const repoData = await this.registry.get(repoName)
      if (repoData && repoData.storage && repoData.storage.strategy === 'hyperdrive') {
        hyperdriveRepos.push(repoName)
      }
    }

    if (hyperdriveRepos.length > 0) {
      const backend = this.backends['hyperdrive']
      await backend.seedExistingDrives(hyperdriveRepos)
    }
  }

  /**
   * Get backend by strategy name
   * @param {string} strategy - Strategy name
   * @returns {RepositoryBackend|null}
   */
  getBackendByStrategy(strategy) {
    return this.backends[strategy] || null
  }

  /**
   * List available strategies
   * @returns {Array<string>}
   */
  listStrategies() {
    return Object.keys(this.backends)
  }

  /**
   * Get strategy info
   * @param {string} strategy - Strategy name
   * @returns {Object|null}
   */
  getStrategyInfo(strategy) {
    const backend = this.backends[strategy]
    if (!backend) return null

    return {
      name: strategy,
      description: backend.getDescription(),
      canReplicate: backend.canReplicate()
    }
  }

  // Delegate methods to appropriate backend

  async listFiles(repoName, ref, path) {
    const backend = await this.getBackend(repoName)
    return backend.listFiles(repoName, ref, path)
  }

  async getCommits(repoName, ref, limit) {
    const backend = await this.getBackend(repoName)
    return backend.getCommits(repoName, ref, limit)
  }

  async readFile(repoName, filePath, ref) {
    const backend = await this.getBackend(repoName)
    return backend.readFile(repoName, filePath, ref)
  }

  async getBranches(repoName) {
    const backend = await this.getBackend(repoName)
    return backend.getBranches(repoName)
  }

  async getTags(repoName) {
    const backend = await this.getBackend(repoName)
    return backend.getTags(repoName)
  }

  async getStats(repoName) {
    const backend = await this.getBackend(repoName)
    return backend.getStats(repoName)
  }

  async getReadme(repoName, ref) {
    const backend = await this.getBackend(repoName)
    return backend.getReadme(repoName, ref)
  }

  async getSubmodules(repoName, ref) {
    const backend = await this.getBackend(repoName)
    return backend.getSubmodules(repoName, ref)
  }

  async getSubmoduleInfo(repoName, submodulePath, ref) {
    const backend = await this.getBackend(repoName)
    return backend.getSubmoduleInfo(repoName, submodulePath, ref)
  }

  async getCommitDiff(repoName, commitHash) {
    const backend = await this.getBackend(repoName)
    return backend.getCommitDiff(repoName, commitHash)
  }

  async getFileAtCommit(repoName, filePath, commitHash) {
    const backend = await this.getBackend(repoName)
    return backend.getFileAtCommit(repoName, filePath, commitHash)
  }

  parseRepoNameFromURL(url) {
    // This is a utility method, use gitBrowser directly
    return this.gitBrowser.parseRepoNameFromURL(url)
  }

  // Backend management methods

  /**
   * Get current strategy for a repository
   * @param {string} repoName - Repository name
   * @returns {Promise<string>}
   */
  async getRepoStrategy(repoName) {
    const backend = await this.getBackend(repoName)
    return backend.getStrategy()
  }

  /**
   * Set storage strategy for a repository
   * @param {string} repoName - Repository name
   * @param {string} strategy - Strategy name
   * @param {Object} options - Strategy options
   */
  async setRepoStrategy(repoName, strategy, options = {}) {
    if (!this.registry) {
      throw new Error('Registry required to set repo strategy')
    }

    if (!this.backends[strategy]) {
      throw new Error(`Unknown strategy: ${strategy}`)
    }

    await this.registry.setStorageStrategy(repoName, strategy, options)
    console.log(`[BackendManager] Set ${repoName} strategy to ${strategy}`)
  }

  /**
   * Migrate repository to new strategy
   * @param {string} repoName - Repository name
   * @param {string} newStrategy - New strategy name
   * @param {Object} options - Migration options
   */
  async migrateRepo(repoName, newStrategy, options = {}) {
    const oldBackend = await this.getBackend(repoName)
    const newBackend = this.backends[newStrategy]

    if (!newBackend) {
      throw new Error(`Unknown strategy: ${newStrategy}`)
    }

    console.log(`[BackendManager] Migrating ${repoName}: ${oldBackend.getStrategy()} → ${newStrategy}`)

    // Strategy-specific migration logic
    if (newStrategy === 'hyperdrive') {
      const result = await newBackend.importRepo(repoName)
      options.driveKey = result.driveKey
      options.discoveryKey = result.discoveryKey
      console.log(`[BackendManager] ✓ Hyperdrive: ${result.driveKey.slice(0, 16)}... (${result.filesImported} files)`)
    }

    // Update registry
    await this.setRepoStrategy(repoName, newStrategy, options)

    console.log(`[BackendManager] Migration complete: ${repoName} → ${newStrategy}`)
    return options
  }

  /**
   * Get storage statistics for a repository
   * @param {string} repoName - Repository name
   * @returns {Promise<Object>}
   */
  async getStorageStats(repoName) {
    const backend = await this.getBackend(repoName)
    const strategy = backend.getStrategy()

    return {
      repo: repoName,
      strategy,
      description: backend.getDescription(),
      canReplicate: backend.canReplicate(),
      storageSize: await backend.getStorageSize(repoName),
      replicationKey: await backend.getReplicationKey(repoName)
    }
  }

  /**
   * Get storage statistics for all repositories
   * @param {Array<string>} repoNames - Repository names
   * @returns {Promise<Object>}
   */
  async getAllStorageStats(repoNames) {
    const stats = []
    let totalSize = 0

    for (const repoName of repoNames) {
      const repoStats = await this.getStorageStats(repoName)
      stats.push(repoStats)
      totalSize += repoStats.storageSize
    }

    // Count by strategy
    const byStrategy = {}
    for (const stat of stats) {
      if (!byStrategy[stat.strategy]) {
        byStrategy[stat.strategy] = { count: 0, size: 0 }
      }
      byStrategy[stat.strategy].count++
      byStrategy[stat.strategy].size += stat.storageSize
    }

    return {
      total: {
        repos: repoNames.length,
        storageSize: totalSize
      },
      byStrategy,
      repos: stats
    }
  }

  /**
   * Get Hyperdrive key for a repository (if using hyperdrive backend)
   * @param {string} repoName - Repository name
   * @returns {Promise<string|null>}
   */
  async getDriveKey (repoName) {
    // Check if backend has a drive key
    const backend = await this.getBackend(repoName)
    if (backend.getDriveKey) {
      try {
        return await backend.getDriveKey(repoName)
      } catch {
        // Fall through to registry check
      }
    }

    // Check registry for stored drive key
    if (this.registry) {
      const repoData = await this.registry.get(repoName)
      if (repoData && repoData.storage && repoData.storage.options) {
        return repoData.storage.options.driveKey || null
      }
    }

    return null
  }

}