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
}
}