Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
453 changes: 453 additions & 0 deletions b2-manager/Changeset.md

Large diffs are not rendered by default.

40 changes: 24 additions & 16 deletions b2-manager/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import (
"path/filepath"
"strings"

"github.com/BurntSushi/toml"
"github.com/knadh/koanf/parsers/toml/v2"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"

"b2m/core"
"b2m/model"
)

var k = koanf.New(".")

// InitializeConfig sets up global configuration variables
func InitializeConfig() error {
var err error
Expand Down Expand Up @@ -45,6 +49,14 @@ func InitializeConfig() error {
model.AppConfig.LocalAnchorDir = filepath.Join(model.AppConfig.LocalB2MDir, "local-version")
model.AppConfig.MigrationsDir = filepath.Join(model.AppConfig.ProjectRoot, "b2m-migration")

// Changeset Paths
model.AppConfig.ChangesetDir = filepath.Join(model.AppConfig.ProjectRoot, "changeset")
model.AppConfig.ChangesetScriptsDir = filepath.Join(model.AppConfig.ChangesetDir, "scripts")
model.AppConfig.ChangesetLogsDir = filepath.Join(model.AppConfig.ChangesetDir, "logs")
model.AppConfig.ChangesetDBsDir = filepath.Join(model.AppConfig.ChangesetDir, "dbs")

model.AppConfig.FrontendTomlPath = filepath.Join(model.AppConfig.ProjectRoot, "db", "all_dbs", "db.toml")

return nil
}

Expand All @@ -71,24 +83,20 @@ func loadTOMLConfig() error {
return fmt.Errorf("couldn't find fdt-dev.toml file at %s: %w", tomlPath, err)
}

var tomlConf struct {
B2M struct {
Discord string `toml:"b2m_discord_webhook"`
RootBucket string `toml:"b2m_remote_root_bucket"`
LocalDBDir string `toml:"b2m_db_dir"`
} `toml:"b2m"`
}
if _, err := toml.DecodeFile(tomlPath, &tomlConf); err != nil {
return fmt.Errorf("failed to decode fdt-dev.toml: %w", err)
// Load TOML file
if err := k.Load(file.Provider(tomlPath), toml.Parser()); err != nil {
return fmt.Errorf("failed to load fdt-dev.toml: %w", err)
}

model.AppConfig.RootBucket = tomlConf.B2M.RootBucket
model.AppConfig.DiscordWebhookURL = tomlConf.B2M.Discord
if tomlConf.B2M.LocalDBDir != "" {
if filepath.IsAbs(tomlConf.B2M.LocalDBDir) {
model.AppConfig.LocalDBDir = tomlConf.B2M.LocalDBDir
model.AppConfig.RootBucket = k.String("b2m.b2m_remote_root_bucket")
model.AppConfig.DiscordWebhookURL = k.String("b2m.b2m_discord_webhook")

localDBDir := k.String("b2m.b2m_db_dir")
if localDBDir != "" {
if filepath.IsAbs(localDBDir) {
model.AppConfig.LocalDBDir = localDBDir
} else {
model.AppConfig.LocalDBDir = filepath.Join(model.AppConfig.ProjectRoot, tomlConf.B2M.LocalDBDir)
model.AppConfig.LocalDBDir = filepath.Join(model.AppConfig.ProjectRoot, localDBDir)
}
}

Expand Down
160 changes: 160 additions & 0 deletions b2-manager/core/changeset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package core

import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"
"time"

"b2m/model"
)

// CreateChangeset generates a new changeset python script from a template
func CreateChangeset(phrase string) error {
timestamp := time.Now().UnixNano()
filename := fmt.Sprintf("%d_%s.py", timestamp, phrase)

scriptDir := model.AppConfig.ChangesetScriptsDir
if err := os.MkdirAll(scriptDir, 0755); err != nil {
return fmt.Errorf("failed to create scripts directory: %w", err)
}

scriptPath := filepath.Join(scriptDir, filename)

// Get template path
// Assuming b2m runs from frontend, the templates dir would be in ../b2-manager/templates/
// We should probably rely on ProjectRoot
templatePath := filepath.Join(model.AppConfig.ProjectRoot, "..", "b2-manager", "templates", "changeset_template.py")

tmpl, err := template.ParseFiles(templatePath)
if err != nil {
return fmt.Errorf("failed to parse template file %s: %w", templatePath, err)
}

f, err := os.Create(scriptPath)
if err != nil {
return fmt.Errorf("failed to create script file: %w", err)
}
defer f.Close()

data := struct {
Timestamp int64
Phrase string
}{
Timestamp: timestamp,
Phrase: phrase,
}

if err := tmpl.Execute(f, data); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}

fmt.Printf("Changeset script created at: %s\n", scriptPath)
return nil
}

// ExecuteChangeset runs the specified python script securely
func ExecuteChangeset(scriptName string) error {
scriptDir := model.AppConfig.ChangesetScriptsDir

// Ensure ".py" extension is present if not provided
if filepath.Ext(scriptName) != ".py" {
scriptName += ".py"
}
// Sanitize to prevent directory traversal
scriptName = filepath.Base(scriptName)

scriptPath := filepath.Join(scriptDir, scriptName)

if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
return fmt.Errorf("script %s not found in %s", scriptName, scriptDir)
}

fmt.Printf("Executing Changeset Script: %s\n", scriptPath)

cmd := exec.Command("python3", scriptPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
return fmt.Errorf("script execution failed: %w", err)
}

return nil
}

// RunCLIStatus runs a status check for a specific database and prints the status natively for python
func RunCLIStatus(dbName string) error {
ctx := context.Background()

// In order to get status, we fetch local DBs, Remote Metas, and Locks...
// FetchDBStatusData logic does exactly this.
statusData, err := FetchDBStatusData(ctx, nil)
if err != nil {
return fmt.Errorf("failed to fetch status data: %w", err)
}

found := false
for _, info := range statusData {
// handle both exact match and extension-less
baseName := strings.TrimSuffix(info.DB.Name, filepath.Ext(info.DB.Name))
reqBaseName := strings.TrimSuffix(dbName, filepath.Ext(dbName))

if baseName == reqBaseName || info.DB.Name == dbName {
found = true
// Translate core statuses to the 3 Python required statuses
switch info.StatusCode {
case model.StatusCodeLocalNewer, model.StatusCodeNewLocal, model.StatusCodeLockedByYou:
fmt.Println("ready_to_upload")
case model.StatusCodeUpToDate:
fmt.Println("up_to_date")
default:
fmt.Println("outdated_db") // fallback for safety
}
return nil
}
}

if !found {
fmt.Println("outdated_db")
}
return nil
}

// RunCLIUpload runs a database upload without UI components
func RunCLIUpload(dbName string) error {
ctx := context.Background()
// Using empty functions to keep it quiet
return PerformUpload(ctx, dbName, false, nil, nil)
}

// RunCLIDownload runs a database download without UI components
func RunCLIDownload(dbName string) error {
ctx := context.Background()
return DownloadDatabase(ctx, dbName, true, nil)
}

// RunCLIFetchDBToml downloads db.toml from backblaze
func RunCLIFetchDBToml() error {
ctx := context.Background()
localPath := model.AppConfig.FrontendTomlPath
remotePath := model.AppConfig.RootBucket + filepath.Base(localPath)

if err := os.MkdirAll(filepath.Dir(localPath), 0755); err != nil {
return fmt.Errorf("failed to create directory for db.toml: %w", err)
}

// Use RcloneCopy to pull specific file to localPath
description := "Fetching db.toml"
err := RcloneCopy(ctx, "copyto", remotePath, localPath, description, true, nil)
if err != nil {
return fmt.Errorf("failed to fetch db.toml: %w", err)
}

fmt.Printf("db.toml downloaded to %s\n", localPath)
return nil
}
Loading