added cargo files
This commit is contained in:
290
PinePods-0.8.2/gpodder-api/internal/db/database.go
Normal file
290
PinePods-0.8.2/gpodder-api/internal/db/database.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"pinepods/gpodder-api/config"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql" // MySQL driver
|
||||
_ "github.com/lib/pq" // PostgreSQL driver
|
||||
)
|
||||
|
||||
// Database represents a database connection that can be either PostgreSQL or MySQL
|
||||
type Database struct {
|
||||
*sql.DB
|
||||
Type string // "postgresql" or "mysql"
|
||||
}
|
||||
|
||||
// NewDatabase creates a new database connection based on the DB_TYPE environment variable
|
||||
func NewDatabase(cfg config.DatabaseConfig) (*Database, error) {
|
||||
// Print connection details for debugging (hide password for security)
|
||||
fmt.Printf("Connecting to %s database: host=%s port=%d user=%s dbname=%s\n",
|
||||
cfg.Type, cfg.Host, cfg.Port, cfg.User, cfg.DBName)
|
||||
|
||||
var db *sql.DB
|
||||
var err error
|
||||
|
||||
switch cfg.Type {
|
||||
case "postgresql":
|
||||
db, err = connectPostgreSQL(cfg)
|
||||
case "mysql", "mariadb":
|
||||
db, err = connectMySQL(cfg)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database type: %s", cfg.Type)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Test the connection
|
||||
if err := db.Ping(); err != nil {
|
||||
db.Close()
|
||||
if strings.Contains(err.Error(), "password authentication failed") {
|
||||
// Print environment variables (hide password)
|
||||
fmt.Println("Password authentication failed. Environment variables:")
|
||||
fmt.Printf("DB_HOST=%s\n", os.Getenv("DB_HOST"))
|
||||
fmt.Printf("DB_PORT=%s\n", os.Getenv("DB_PORT"))
|
||||
fmt.Printf("DB_USER=%s\n", os.Getenv("DB_USER"))
|
||||
fmt.Printf("DB_NAME=%s\n", os.Getenv("DB_NAME"))
|
||||
fmt.Printf("DB_PASSWORD=*** (length: %d)\n", len(os.Getenv("DB_PASSWORD")))
|
||||
}
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Successfully connected to the database")
|
||||
|
||||
// Migrations are now handled by the Python migration system
|
||||
// Skip Go migrations to avoid conflicts
|
||||
log.Println("Skipping Go migrations - now handled by Python migration system")
|
||||
|
||||
return &Database{DB: db, Type: cfg.Type}, nil
|
||||
}
|
||||
|
||||
// runMigrationsWithRetry - DISABLED: migrations now handled by Python system
|
||||
// func runMigrationsWithRetry(db *sql.DB, dbType string) error {
|
||||
// All migration logic has been moved to the Python migration system
|
||||
// to ensure consistency and centralized management
|
||||
// This function is kept for reference but is no longer used
|
||||
// }
|
||||
|
||||
// connectPostgreSQL connects to a PostgreSQL database
|
||||
func connectPostgreSQL(cfg config.DatabaseConfig) (*sql.DB, error) {
|
||||
// Escape special characters in password
|
||||
escapedPassword := url.QueryEscape(cfg.Password)
|
||||
|
||||
// Use a connection string without password for logging
|
||||
logConnStr := fmt.Sprintf(
|
||||
"host=%s port=%d user=%s dbname=%s sslmode=%s",
|
||||
cfg.Host, cfg.Port, cfg.User, cfg.DBName, cfg.SSLMode,
|
||||
)
|
||||
fmt.Printf("PostgreSQL connection string (without password): %s\n", logConnStr)
|
||||
|
||||
// Build the actual connection string with password
|
||||
connStr := fmt.Sprintf(
|
||||
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode,
|
||||
)
|
||||
|
||||
// Try standard connection string first
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
// Try URL format connection string
|
||||
urlConnStr := fmt.Sprintf(
|
||||
"postgres://%s:%s@%s:%d/%s?sslmode=%s",
|
||||
cfg.User, escapedPassword, cfg.Host, cfg.Port, cfg.DBName, cfg.SSLMode,
|
||||
)
|
||||
fmt.Println("First connection attempt failed, trying URL format...")
|
||||
db, err = sql.Open("postgres", urlConnStr)
|
||||
}
|
||||
|
||||
return db, err
|
||||
}
|
||||
|
||||
// Replace the existing connectMySQL function with this version
|
||||
func connectMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
|
||||
// Add needed parameters for MySQL authentication
|
||||
connStr := fmt.Sprintf(
|
||||
"%s:%s@tcp(%s:%d)/%s?parseTime=true&allowNativePasswords=true&multiStatements=true",
|
||||
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName,
|
||||
)
|
||||
|
||||
fmt.Printf("Attempting MySQL connection to %s:%d as user '%s'\n",
|
||||
cfg.Host, cfg.Port, cfg.User)
|
||||
|
||||
// Open the connection
|
||||
db, err := sql.Open("mysql", connStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open MySQL connection: %w", err)
|
||||
}
|
||||
|
||||
// Configure connection pool
|
||||
db.SetConnMaxLifetime(time.Minute * 3)
|
||||
db.SetMaxOpenConns(10)
|
||||
db.SetMaxIdleConns(5)
|
||||
|
||||
// Explicitly test the connection
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
fmt.Println("Testing MySQL connection with ping...")
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
db.Close()
|
||||
fmt.Printf("MySQL connection failed: %v\n", err)
|
||||
return nil, fmt.Errorf("failed to ping MySQL database: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("MySQL connection successful!")
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func (db *Database) Close() error {
|
||||
return db.DB.Close()
|
||||
}
|
||||
|
||||
// IsMySQLDB returns true if the database is MySQL/MariaDB
|
||||
func (db *Database) IsMySQLDB() bool {
|
||||
return db.Type == "mysql"
|
||||
}
|
||||
|
||||
// IsPostgreSQLDB returns true if the database is PostgreSQL
|
||||
func (db *Database) IsPostgreSQLDB() bool {
|
||||
return db.Type == "postgresql"
|
||||
}
|
||||
|
||||
// FormatQuery formats a query for the specific database type
|
||||
func (db *Database) FormatQuery(query string) string {
|
||||
if db.Type == "postgresql" {
|
||||
return query // PostgreSQL queries already have correct format
|
||||
}
|
||||
|
||||
// For MySQL:
|
||||
result := query
|
||||
|
||||
// First, replace quoted table names
|
||||
knownTables := []string{
|
||||
"Users", "GpodderDevices", "GpodderSyncSettings",
|
||||
"GpodderSyncSubscriptions", "GpodderSyncEpisodeActions",
|
||||
"GpodderSyncPodcastLists", "GpodderSyncState", "GpodderSessions",
|
||||
"GpodderSyncMigrations", "Podcasts", "Episodes", "SavedEpisodes",
|
||||
"UserEpisodeHistory", "UserSettings", "APIKeys",
|
||||
}
|
||||
|
||||
for _, table := range knownTables {
|
||||
quoted := fmt.Sprintf("\"%s\"", table)
|
||||
result = strings.ReplaceAll(result, quoted, table)
|
||||
}
|
||||
|
||||
// Replace column quotes (double quotes to backticks)
|
||||
re := regexp.MustCompile(`"([^"]+)"`)
|
||||
result = re.ReplaceAllString(result, "`$1`")
|
||||
|
||||
// Then replace placeholders
|
||||
for i := 10; i > 0; i-- {
|
||||
old := fmt.Sprintf("$%d", i)
|
||||
result = strings.ReplaceAll(result, old, "?")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Exec executes a query with the correct formatting for the database type
|
||||
func (db *Database) Exec(query string, args ...interface{}) (sql.Result, error) {
|
||||
formattedQuery := db.FormatQuery(query)
|
||||
return db.DB.Exec(formattedQuery, args...)
|
||||
}
|
||||
|
||||
// Query executes a query with the correct formatting for the database type
|
||||
func (db *Database) Query(query string, args ...interface{}) (*sql.Rows, error) {
|
||||
formattedQuery := db.FormatQuery(query)
|
||||
return db.DB.Query(formattedQuery, args...)
|
||||
}
|
||||
|
||||
// QueryRow executes a query with the correct formatting for the database type
|
||||
func (db *Database) QueryRow(query string, args ...interface{}) *sql.Row {
|
||||
formattedQuery := db.FormatQuery(query)
|
||||
return db.DB.QueryRow(formattedQuery, args...)
|
||||
}
|
||||
|
||||
// Begin starts a transaction with the correct formatting for the database type
|
||||
func (db *Database) Begin() (*Transaction, error) {
|
||||
tx, err := db.DB.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Transaction{tx: tx, dbType: db.Type}, nil
|
||||
}
|
||||
|
||||
// Transaction is a wrapper around sql.Tx that formats queries correctly
|
||||
type Transaction struct {
|
||||
tx *sql.Tx
|
||||
dbType string
|
||||
}
|
||||
|
||||
// Commit commits the transaction
|
||||
func (tx *Transaction) Commit() error {
|
||||
return tx.tx.Commit()
|
||||
}
|
||||
|
||||
// Rollback rolls back the transaction
|
||||
func (tx *Transaction) Rollback() error {
|
||||
return tx.tx.Rollback()
|
||||
}
|
||||
|
||||
// Exec executes a query in the transaction with correct formatting
|
||||
func (tx *Transaction) Exec(query string, args ...interface{}) (sql.Result, error) {
|
||||
formattedQuery := formatQuery(query, tx.dbType)
|
||||
return tx.tx.Exec(formattedQuery, args...)
|
||||
}
|
||||
|
||||
// Query executes a query in the transaction with correct formatting
|
||||
func (tx *Transaction) Query(query string, args ...interface{}) (*sql.Rows, error) {
|
||||
formattedQuery := formatQuery(query, tx.dbType)
|
||||
return tx.tx.Query(formattedQuery, args...)
|
||||
}
|
||||
|
||||
// QueryRow executes a query in the transaction with correct formatting
|
||||
func (tx *Transaction) QueryRow(query string, args ...interface{}) *sql.Row {
|
||||
formattedQuery := formatQuery(query, tx.dbType)
|
||||
return tx.tx.QueryRow(formattedQuery, args...)
|
||||
}
|
||||
|
||||
// Helper function to format queries
|
||||
func formatQuery(query string, dbType string) string {
|
||||
if dbType == "postgresql" {
|
||||
return query
|
||||
}
|
||||
|
||||
// For MySQL:
|
||||
// Same logic as FormatQuery method
|
||||
result := query
|
||||
|
||||
knownTables := []string{
|
||||
"Users", "GpodderDevices", "GpodderSyncSettings",
|
||||
"GpodderSyncSubscriptions", "GpodderSyncEpisodeActions",
|
||||
"GpodderSyncPodcastLists", "GpodderSyncState", "GpodderSessions",
|
||||
"GpodderSyncMigrations", "Podcasts", "Episodes", "SavedEpisodes",
|
||||
"UserEpisodeHistory", "UserSettings", "APIKeys",
|
||||
}
|
||||
|
||||
for _, table := range knownTables {
|
||||
quoted := fmt.Sprintf("\"%s\"", table)
|
||||
result = strings.ReplaceAll(result, quoted, table)
|
||||
}
|
||||
|
||||
for i := 10; i > 0; i-- {
|
||||
old := fmt.Sprintf("$%d", i)
|
||||
result = strings.ReplaceAll(result, old, "?")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
176
PinePods-0.8.2/gpodder-api/internal/db/helpers.go
Normal file
176
PinePods-0.8.2/gpodder-api/internal/db/helpers.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetTableName returns the properly formatted table name based on DB type
|
||||
func GetTableName(tableName string, dbType string) string {
|
||||
if dbType == "postgresql" {
|
||||
return fmt.Sprintf("\"%s\"", tableName)
|
||||
}
|
||||
return tableName
|
||||
}
|
||||
|
||||
// GetPlaceholder returns the correct parameter placeholder based on DB type and index
|
||||
func GetPlaceholder(index int, dbType string) string {
|
||||
if dbType == "postgresql" {
|
||||
return fmt.Sprintf("$%d", index)
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
// GetPlaceholders returns a comma-separated list of placeholders
|
||||
func GetPlaceholders(count int, dbType string) string {
|
||||
placeholders := make([]string, count)
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
if dbType == "postgresql" {
|
||||
placeholders[i] = fmt.Sprintf("$%d", i+1)
|
||||
} else {
|
||||
placeholders[i] = "?"
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(placeholders, ", ")
|
||||
}
|
||||
|
||||
// GetColumnDefinition returns the appropriate column definition
|
||||
func GetColumnDefinition(columnName, dataType string, dbType string) string {
|
||||
// Handle special cases for different database types
|
||||
switch dataType {
|
||||
case "serial":
|
||||
if dbType == "postgresql" {
|
||||
return fmt.Sprintf("%s SERIAL", columnName)
|
||||
}
|
||||
return fmt.Sprintf("%s INT AUTO_INCREMENT", columnName)
|
||||
case "boolean":
|
||||
if dbType == "postgresql" {
|
||||
return fmt.Sprintf("%s BOOLEAN", columnName)
|
||||
}
|
||||
return fmt.Sprintf("%s TINYINT(1)", columnName)
|
||||
case "timestamp":
|
||||
if dbType == "postgresql" {
|
||||
return fmt.Sprintf("%s TIMESTAMP", columnName)
|
||||
}
|
||||
return fmt.Sprintf("%s TIMESTAMP", columnName)
|
||||
default:
|
||||
return fmt.Sprintf("%s %s", columnName, dataType)
|
||||
}
|
||||
}
|
||||
|
||||
// GetSerialPrimaryKey returns a serial primary key definition
|
||||
func GetSerialPrimaryKey(columnName string, dbType string) string {
|
||||
if dbType == "postgresql" {
|
||||
return fmt.Sprintf("%s SERIAL PRIMARY KEY", columnName)
|
||||
}
|
||||
return fmt.Sprintf("%s INT AUTO_INCREMENT PRIMARY KEY", columnName)
|
||||
}
|
||||
|
||||
// GetTimestampDefault returns a timestamp with default value
|
||||
func GetTimestampDefault(columnName string, dbType string) string {
|
||||
if dbType == "postgresql" {
|
||||
return fmt.Sprintf("%s TIMESTAMP DEFAULT CURRENT_TIMESTAMP", columnName)
|
||||
}
|
||||
return fmt.Sprintf("%s TIMESTAMP DEFAULT CURRENT_TIMESTAMP", columnName)
|
||||
}
|
||||
|
||||
// GetAutoUpdateTimestamp returns a timestamp that updates automatically
|
||||
func GetAutoUpdateTimestamp(columnName string, dbType string) string {
|
||||
if dbType == "postgresql" {
|
||||
// PostgreSQL doesn't have a direct equivalent to MySQL's ON UPDATE
|
||||
// In PostgreSQL this would typically be handled with a trigger
|
||||
return fmt.Sprintf("%s TIMESTAMP DEFAULT CURRENT_TIMESTAMP", columnName)
|
||||
}
|
||||
return fmt.Sprintf("%s TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP", columnName)
|
||||
}
|
||||
|
||||
// BuildInsertQuery builds an INSERT query with the correct placeholder syntax
|
||||
func BuildInsertQuery(tableName string, columns []string, dbType string) string {
|
||||
columnsStr := strings.Join(columns, ", ")
|
||||
placeholders := GetPlaceholders(len(columns), dbType)
|
||||
|
||||
if dbType == "postgresql" {
|
||||
return fmt.Sprintf("INSERT INTO \"%s\" (%s) VALUES (%s)", tableName, columnsStr, placeholders)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", tableName, columnsStr, placeholders)
|
||||
}
|
||||
|
||||
// BuildSelectQuery builds a SELECT query with the correct table name syntax
|
||||
func BuildSelectQuery(tableName string, columns []string, whereClause string, dbType string) string {
|
||||
columnsStr := strings.Join(columns, ", ")
|
||||
|
||||
if dbType == "postgresql" {
|
||||
if whereClause != "" {
|
||||
return fmt.Sprintf("SELECT %s FROM \"%s\" WHERE %s", columnsStr, tableName, whereClause)
|
||||
}
|
||||
return fmt.Sprintf("SELECT %s FROM \"%s\"", columnsStr, tableName)
|
||||
}
|
||||
|
||||
if whereClause != "" {
|
||||
return fmt.Sprintf("SELECT %s FROM %s WHERE %s", columnsStr, tableName, whereClause)
|
||||
}
|
||||
return fmt.Sprintf("SELECT %s FROM %s", columnsStr, tableName)
|
||||
}
|
||||
|
||||
// BuildUpdateQuery builds an UPDATE query with the correct syntax
|
||||
func BuildUpdateQuery(tableName string, setColumns []string, whereClause string, dbType string) string {
|
||||
setClauses := make([]string, len(setColumns))
|
||||
|
||||
for i, col := range setColumns {
|
||||
if dbType == "postgresql" {
|
||||
setClauses[i] = fmt.Sprintf("%s = $%d", col, i+1)
|
||||
} else {
|
||||
setClauses[i] = fmt.Sprintf("%s = ?", col)
|
||||
}
|
||||
}
|
||||
|
||||
setClauseStr := strings.Join(setClauses, ", ")
|
||||
|
||||
if dbType == "postgresql" {
|
||||
return fmt.Sprintf("UPDATE \"%s\" SET %s WHERE %s", tableName, setClauseStr, whereClause)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("UPDATE %s SET %s WHERE %s", tableName, setClauseStr, whereClause)
|
||||
}
|
||||
|
||||
// RewriteQuery rewrites a PostgreSQL query to MySQL syntax
|
||||
func RewriteQuery(query, dbType string) string {
|
||||
if dbType == "postgresql" {
|
||||
return query
|
||||
}
|
||||
|
||||
// Replace placeholders
|
||||
rewritten := query
|
||||
|
||||
// Replace placeholders first, starting from highest number to avoid conflicts
|
||||
for i := 20; i > 0; i-- {
|
||||
placeholder := fmt.Sprintf("$%d", i)
|
||||
rewritten = strings.ReplaceAll(rewritten, placeholder, "?")
|
||||
}
|
||||
|
||||
// Replace quoted table names
|
||||
knownTables := []string{
|
||||
"Users", "GpodderDevices", "GpodderSyncSettings",
|
||||
"GpodderSyncSubscriptions", "GpodderSyncEpisodeActions",
|
||||
"GpodderSyncPodcastLists", "GpodderSyncState", "GpodderSessions",
|
||||
"GpodderSyncMigrations", "Podcasts", "Episodes", "SavedEpisodes",
|
||||
"UserEpisodeHistory", "UserSettings", "APIKeys", "UserVideoHistory",
|
||||
"SavedVideos", "DownloadedEpisodes", "DownloadedVideos", "EpisodeQueue",
|
||||
}
|
||||
|
||||
for _, table := range knownTables {
|
||||
quotedTable := fmt.Sprintf("\"%s\"", table)
|
||||
rewritten = strings.ReplaceAll(rewritten, quotedTable, table)
|
||||
}
|
||||
|
||||
// Handle RETURNING clause (MySQL doesn't support it)
|
||||
returningIdx := strings.Index(strings.ToUpper(rewritten), "RETURNING")
|
||||
if returningIdx > 0 {
|
||||
rewritten = rewritten[:returningIdx]
|
||||
}
|
||||
|
||||
return rewritten
|
||||
}
|
||||
538
PinePods-0.8.2/gpodder-api/internal/db/migrations.go
Normal file
538
PinePods-0.8.2/gpodder-api/internal/db/migrations.go
Normal file
@@ -0,0 +1,538 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Migration represents a database migration
|
||||
type Migration struct {
|
||||
Version int
|
||||
Description string
|
||||
PostgreSQLSQL string
|
||||
MySQLSQL string
|
||||
}
|
||||
|
||||
// MigrationRecord represents a record of an applied migration
|
||||
type MigrationRecord struct {
|
||||
Version int
|
||||
Description string
|
||||
AppliedAt time.Time
|
||||
}
|
||||
|
||||
// EnsureMigrationsTable creates the migrations table if it doesn't exist
|
||||
func EnsureMigrationsTable(db *sql.DB, dbType string) error {
|
||||
log.Println("Creating GpodderSyncMigrations table if it doesn't exist...")
|
||||
|
||||
var query string
|
||||
if dbType == "postgresql" {
|
||||
query = `
|
||||
CREATE TABLE IF NOT EXISTS "GpodderSyncMigrations" (
|
||||
Version INT PRIMARY KEY,
|
||||
Description TEXT NOT NULL,
|
||||
AppliedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`
|
||||
} else {
|
||||
query = `
|
||||
CREATE TABLE IF NOT EXISTS GpodderSyncMigrations (
|
||||
Version INT PRIMARY KEY,
|
||||
Description TEXT NOT NULL,
|
||||
AppliedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`
|
||||
}
|
||||
|
||||
_, err := db.Exec(query)
|
||||
if err != nil {
|
||||
log.Printf("Error creating migrations table: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Println("GpodderSyncMigrations table is ready")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAppliedMigrations returns a list of already applied migrations
|
||||
func GetAppliedMigrations(db *sql.DB, dbType string) ([]MigrationRecord, error) {
|
||||
log.Println("Checking previously applied migrations...")
|
||||
|
||||
var query string
|
||||
if dbType == "postgresql" {
|
||||
query = `
|
||||
SELECT Version, Description, AppliedAt
|
||||
FROM "GpodderSyncMigrations"
|
||||
ORDER BY Version ASC
|
||||
`
|
||||
} else {
|
||||
query = `
|
||||
SELECT Version, Description, AppliedAt
|
||||
FROM GpodderSyncMigrations
|
||||
ORDER BY Version ASC
|
||||
`
|
||||
}
|
||||
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
log.Printf("Error checking applied migrations: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var migrations []MigrationRecord
|
||||
for rows.Next() {
|
||||
var m MigrationRecord
|
||||
if err := rows.Scan(&m.Version, &m.Description, &m.AppliedAt); err != nil {
|
||||
log.Printf("Error scanning migration record: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
migrations = append(migrations, m)
|
||||
}
|
||||
|
||||
if len(migrations) > 0 {
|
||||
log.Printf("Found %d previously applied migrations", len(migrations))
|
||||
} else {
|
||||
log.Println("No previously applied migrations found")
|
||||
}
|
||||
return migrations, nil
|
||||
}
|
||||
|
||||
// ApplyMigration applies a single migration
|
||||
func ApplyMigration(db *sql.DB, migration Migration, dbType string) error {
|
||||
log.Printf("Applying migration %d: %s", migration.Version, migration.Description)
|
||||
|
||||
// Select the appropriate SQL based on database type
|
||||
var sql string
|
||||
if dbType == "postgresql" {
|
||||
sql = migration.PostgreSQLSQL
|
||||
} else {
|
||||
sql = migration.MySQLSQL
|
||||
}
|
||||
|
||||
// Begin transaction
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
log.Printf("Error beginning transaction for migration %d: %v", migration.Version, err)
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.Printf("Rolling back migration %d due to error", migration.Version)
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// Execute the migration SQL
|
||||
_, err = tx.Exec(sql)
|
||||
if err != nil {
|
||||
log.Printf("Failed to apply migration %d: %v", migration.Version, err)
|
||||
return fmt.Errorf("failed to apply migration %d: %w", migration.Version, err)
|
||||
}
|
||||
|
||||
// Record the migration
|
||||
var insertQuery string
|
||||
if dbType == "postgresql" {
|
||||
insertQuery = `
|
||||
INSERT INTO "GpodderSyncMigrations" (Version, Description)
|
||||
VALUES ($1, $2)
|
||||
`
|
||||
} else {
|
||||
insertQuery = `
|
||||
INSERT INTO GpodderSyncMigrations (Version, Description)
|
||||
VALUES (?, ?)
|
||||
`
|
||||
}
|
||||
|
||||
_, err = tx.Exec(insertQuery, migration.Version, migration.Description)
|
||||
if err != nil {
|
||||
log.Printf("Failed to record migration %d: %v", migration.Version, err)
|
||||
return fmt.Errorf("failed to record migration %d: %w", migration.Version, err)
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
log.Printf("Failed to commit migration %d: %v", migration.Version, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Successfully applied migration %d", migration.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkRequiredTables verifies that required PinePods tables exist before running migrations
|
||||
func checkRequiredTables(db *sql.DB, dbType string) error {
|
||||
log.Println("Checking for required PinePods tables...")
|
||||
|
||||
requiredTables := []string{"Users", "GpodderDevices"}
|
||||
|
||||
for _, table := range requiredTables {
|
||||
var query string
|
||||
if dbType == "postgresql" {
|
||||
query = `SELECT 1 FROM "` + table + `" LIMIT 1`
|
||||
} else {
|
||||
query = `SELECT 1 FROM ` + table + ` LIMIT 1`
|
||||
}
|
||||
|
||||
_, err := db.Exec(query)
|
||||
if err != nil {
|
||||
log.Printf("Required table %s does not exist or is not accessible: %v", table, err)
|
||||
return fmt.Errorf("required table %s does not exist - please ensure PinePods main migrations have run first", table)
|
||||
}
|
||||
log.Printf("Required table %s exists", table)
|
||||
}
|
||||
|
||||
log.Println("All required tables found")
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunMigrations runs all pending migrations
|
||||
func RunMigrations(db *sql.DB, dbType string) error {
|
||||
log.Println("Starting gpodder API migrations...")
|
||||
|
||||
// Check that required PinePods tables exist first
|
||||
if err := checkRequiredTables(db, dbType); err != nil {
|
||||
return fmt.Errorf("prerequisite check failed: %w", err)
|
||||
}
|
||||
|
||||
// Ensure migrations table exists
|
||||
if err := EnsureMigrationsTable(db, dbType); err != nil {
|
||||
return fmt.Errorf("failed to create migrations table: %w", err)
|
||||
}
|
||||
|
||||
// Get applied migrations
|
||||
appliedMigrations, err := GetAppliedMigrations(db, dbType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get applied migrations: %w", err)
|
||||
}
|
||||
|
||||
// Build a map of applied migration versions for quick lookup
|
||||
appliedVersions := make(map[int]bool)
|
||||
for _, m := range appliedMigrations {
|
||||
appliedVersions[m.Version] = true
|
||||
}
|
||||
|
||||
// Get all migrations
|
||||
migrations := GetMigrations()
|
||||
log.Printf("Found %d total migrations to check", len(migrations))
|
||||
|
||||
// Apply pending migrations
|
||||
appliedCount := 0
|
||||
for _, migration := range migrations {
|
||||
if appliedVersions[migration.Version] {
|
||||
// Migration already applied, skip
|
||||
log.Printf("Migration %d already applied, skipping", migration.Version)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("Applying migration %d: %s", migration.Version, migration.Description)
|
||||
if err := ApplyMigration(db, migration, dbType); err != nil {
|
||||
return err
|
||||
}
|
||||
appliedCount++
|
||||
}
|
||||
|
||||
if appliedCount > 0 {
|
||||
log.Printf("Successfully applied %d new migrations", appliedCount)
|
||||
} else {
|
||||
log.Println("No new migrations to apply")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMigrations returns all migrations with SQL variants for both database types
|
||||
func GetMigrations() []Migration {
|
||||
return []Migration{
|
||||
{
|
||||
Version: 1,
|
||||
Description: "Initial schema creation",
|
||||
PostgreSQLSQL: `
|
||||
-- Device sync state for the API
|
||||
CREATE TABLE IF NOT EXISTS "GpodderSyncDeviceState" (
|
||||
DeviceStateID SERIAL PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
DeviceID INT NOT NULL,
|
||||
SubscriptionCount INT DEFAULT 0,
|
||||
LastUpdated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (UserID) REFERENCES "Users"(UserID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (DeviceID) REFERENCES "GpodderDevices"(DeviceID) ON DELETE CASCADE,
|
||||
UNIQUE(UserID, DeviceID)
|
||||
);
|
||||
|
||||
-- Subscription changes
|
||||
CREATE TABLE IF NOT EXISTS "GpodderSyncSubscriptions" (
|
||||
SubscriptionID SERIAL PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
DeviceID INT NOT NULL,
|
||||
PodcastURL TEXT NOT NULL,
|
||||
Action VARCHAR(10) NOT NULL,
|
||||
Timestamp BIGINT NOT NULL,
|
||||
FOREIGN KEY (UserID) REFERENCES "Users"(UserID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (DeviceID) REFERENCES "GpodderDevices"(DeviceID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Episode actions
|
||||
CREATE TABLE IF NOT EXISTS "GpodderSyncEpisodeActions" (
|
||||
ActionID SERIAL PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
DeviceID INT,
|
||||
PodcastURL TEXT NOT NULL,
|
||||
EpisodeURL TEXT NOT NULL,
|
||||
Action VARCHAR(20) NOT NULL,
|
||||
Timestamp BIGINT NOT NULL,
|
||||
Started INT,
|
||||
Position INT,
|
||||
Total INT,
|
||||
FOREIGN KEY (UserID) REFERENCES "Users"(UserID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (DeviceID) REFERENCES "GpodderDevices"(DeviceID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Podcast lists
|
||||
CREATE TABLE IF NOT EXISTS "GpodderSyncPodcastLists" (
|
||||
ListID SERIAL PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
Name VARCHAR(255) NOT NULL,
|
||||
Title VARCHAR(255) NOT NULL,
|
||||
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (UserID) REFERENCES "Users"(UserID) ON DELETE CASCADE,
|
||||
UNIQUE(UserID, Name)
|
||||
);
|
||||
|
||||
-- Podcast list entries
|
||||
CREATE TABLE IF NOT EXISTS "GpodderSyncPodcastListEntries" (
|
||||
EntryID SERIAL PRIMARY KEY,
|
||||
ListID INT NOT NULL,
|
||||
PodcastURL TEXT NOT NULL,
|
||||
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (ListID) REFERENCES "GpodderSyncPodcastLists"(ListID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Synchronization relationships between devices
|
||||
CREATE TABLE IF NOT EXISTS "GpodderSyncDevicePairs" (
|
||||
PairID SERIAL PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
DeviceID1 INT NOT NULL,
|
||||
DeviceID2 INT NOT NULL,
|
||||
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (UserID) REFERENCES "Users"(UserID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (DeviceID1) REFERENCES "GpodderDevices"(DeviceID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (DeviceID2) REFERENCES "GpodderDevices"(DeviceID) ON DELETE CASCADE,
|
||||
UNIQUE(UserID, DeviceID1, DeviceID2)
|
||||
);
|
||||
|
||||
-- Settings storage
|
||||
CREATE TABLE IF NOT EXISTS "GpodderSyncSettings" (
|
||||
SettingID SERIAL PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
Scope VARCHAR(20) NOT NULL,
|
||||
DeviceID INT,
|
||||
PodcastURL TEXT,
|
||||
EpisodeURL TEXT,
|
||||
SettingKey VARCHAR(255) NOT NULL,
|
||||
SettingValue TEXT,
|
||||
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
LastUpdated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (UserID) REFERENCES "Users"(UserID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (DeviceID) REFERENCES "GpodderDevices"(DeviceID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Create indexes for faster queries
|
||||
CREATE INDEX IF NOT EXISTS idx_gpodder_sync_subscriptions_userid ON "GpodderSyncSubscriptions"(UserID);
|
||||
CREATE INDEX IF NOT EXISTS idx_gpodder_sync_subscriptions_deviceid ON "GpodderSyncSubscriptions"(DeviceID);
|
||||
CREATE INDEX IF NOT EXISTS idx_gpodder_sync_episode_actions_userid ON "GpodderSyncEpisodeActions"(UserID);
|
||||
CREATE INDEX IF NOT EXISTS idx_gpodder_sync_podcast_lists_userid ON "GpodderSyncPodcastLists"(UserID);
|
||||
`,
|
||||
MySQLSQL: `
|
||||
-- Device sync state for the API
|
||||
CREATE TABLE IF NOT EXISTS GpodderSyncDeviceState (
|
||||
DeviceStateID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
DeviceID INT NOT NULL,
|
||||
SubscriptionCount INT DEFAULT 0,
|
||||
LastUpdated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (DeviceID) REFERENCES GpodderDevices(DeviceID) ON DELETE CASCADE,
|
||||
UNIQUE(UserID, DeviceID)
|
||||
);
|
||||
|
||||
-- Subscription changes
|
||||
CREATE TABLE IF NOT EXISTS GpodderSyncSubscriptions (
|
||||
SubscriptionID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
DeviceID INT NOT NULL,
|
||||
PodcastURL TEXT NOT NULL,
|
||||
Action VARCHAR(10) NOT NULL,
|
||||
Timestamp BIGINT NOT NULL,
|
||||
FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (DeviceID) REFERENCES GpodderDevices(DeviceID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Episode actions
|
||||
CREATE TABLE IF NOT EXISTS GpodderSyncEpisodeActions (
|
||||
ActionID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
DeviceID INT,
|
||||
PodcastURL TEXT NOT NULL,
|
||||
EpisodeURL TEXT NOT NULL,
|
||||
Action VARCHAR(20) NOT NULL,
|
||||
Timestamp BIGINT NOT NULL,
|
||||
Started INT,
|
||||
Position INT,
|
||||
Total INT,
|
||||
FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (DeviceID) REFERENCES GpodderDevices(DeviceID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Podcast lists
|
||||
CREATE TABLE IF NOT EXISTS GpodderSyncPodcastLists (
|
||||
ListID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
Name VARCHAR(255) NOT NULL,
|
||||
Title VARCHAR(255) NOT NULL,
|
||||
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE,
|
||||
UNIQUE(UserID, Name)
|
||||
);
|
||||
|
||||
-- Podcast list entries
|
||||
CREATE TABLE IF NOT EXISTS GpodderSyncPodcastListEntries (
|
||||
EntryID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
ListID INT NOT NULL,
|
||||
PodcastURL TEXT NOT NULL,
|
||||
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (ListID) REFERENCES GpodderSyncPodcastLists(ListID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Synchronization relationships between devices
|
||||
CREATE TABLE IF NOT EXISTS GpodderSyncDevicePairs (
|
||||
PairID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
DeviceID1 INT NOT NULL,
|
||||
DeviceID2 INT NOT NULL,
|
||||
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (DeviceID1) REFERENCES GpodderDevices(DeviceID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (DeviceID2) REFERENCES GpodderDevices(DeviceID) ON DELETE CASCADE,
|
||||
UNIQUE(UserID, DeviceID1, DeviceID2)
|
||||
);
|
||||
|
||||
-- Settings storage
|
||||
CREATE TABLE IF NOT EXISTS GpodderSyncSettings (
|
||||
SettingID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
Scope VARCHAR(20) NOT NULL,
|
||||
DeviceID INT,
|
||||
PodcastURL TEXT,
|
||||
EpisodeURL TEXT,
|
||||
SettingKey VARCHAR(255) NOT NULL,
|
||||
SettingValue TEXT,
|
||||
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
LastUpdated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (DeviceID) REFERENCES GpodderDevices(DeviceID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Create indexes for faster queries
|
||||
CREATE INDEX idx_gpodder_sync_subscriptions_userid ON GpodderSyncSubscriptions(UserID);
|
||||
CREATE INDEX idx_gpodder_sync_subscriptions_deviceid ON GpodderSyncSubscriptions(DeviceID);
|
||||
CREATE INDEX idx_gpodder_sync_episode_actions_userid ON GpodderSyncEpisodeActions(UserID);
|
||||
CREATE INDEX idx_gpodder_sync_podcast_lists_userid ON GpodderSyncPodcastLists(UserID);
|
||||
`,
|
||||
},
|
||||
{
|
||||
Version: 2,
|
||||
Description: "Add API version column to GpodderSyncSettings",
|
||||
PostgreSQLSQL: `
|
||||
ALTER TABLE "GpodderSyncSettings"
|
||||
ADD COLUMN IF NOT EXISTS APIVersion VARCHAR(10) DEFAULT '2.0';
|
||||
`,
|
||||
MySQLSQL: `
|
||||
-- Check if column exists first
|
||||
SET @s = (SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
'ALTER TABLE GpodderSyncSettings ADD COLUMN APIVersion VARCHAR(10) DEFAULT "2.0"',
|
||||
'SELECT 1'
|
||||
) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_NAME = 'GpodderSyncSettings'
|
||||
AND COLUMN_NAME = 'APIVersion');
|
||||
|
||||
PREPARE stmt FROM @s;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
`,
|
||||
},
|
||||
{
|
||||
Version: 3,
|
||||
Description: "Create GpodderSessions table for API sessions",
|
||||
PostgreSQLSQL: `
|
||||
CREATE TABLE IF NOT EXISTS "GpodderSessions" (
|
||||
SessionID SERIAL PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
SessionToken TEXT NOT NULL,
|
||||
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
ExpiresAt TIMESTAMP NOT NULL,
|
||||
LastActive TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UserAgent TEXT,
|
||||
ClientIP TEXT,
|
||||
FOREIGN KEY (UserID) REFERENCES "Users"(UserID) ON DELETE CASCADE,
|
||||
UNIQUE(SessionToken)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_gpodder_sessions_token ON "GpodderSessions"(SessionToken);
|
||||
CREATE INDEX IF NOT EXISTS idx_gpodder_sessions_userid ON "GpodderSessions"(UserID);
|
||||
CREATE INDEX IF NOT EXISTS idx_gpodder_sessions_expires ON "GpodderSessions"(ExpiresAt);
|
||||
`,
|
||||
MySQLSQL: `
|
||||
CREATE TABLE IF NOT EXISTS GpodderSessions (
|
||||
SessionID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
SessionToken TEXT NOT NULL,
|
||||
CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
ExpiresAt TIMESTAMP NOT NULL,
|
||||
LastActive TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UserAgent TEXT,
|
||||
ClientIP TEXT,
|
||||
FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_gpodder_sessions_userid ON GpodderSessions(UserID);
|
||||
CREATE INDEX idx_gpodder_sessions_expires ON GpodderSessions(ExpiresAt);
|
||||
`,
|
||||
},
|
||||
{
|
||||
Version: 4,
|
||||
Description: "Add sync state table for tracking device sync status",
|
||||
PostgreSQLSQL: `
|
||||
CREATE TABLE IF NOT EXISTS "GpodderSyncState" (
|
||||
SyncStateID SERIAL PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
DeviceID INT NOT NULL,
|
||||
LastTimestamp BIGINT DEFAULT 0,
|
||||
LastSync TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (UserID) REFERENCES "Users"(UserID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (DeviceID) REFERENCES "GpodderDevices"(DeviceID) ON DELETE CASCADE,
|
||||
UNIQUE(UserID, DeviceID)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_gpodder_syncstate_userid_deviceid ON "GpodderSyncState"(UserID, DeviceID);
|
||||
`,
|
||||
MySQLSQL: `
|
||||
CREATE TABLE IF NOT EXISTS GpodderSyncState (
|
||||
SyncStateID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
UserID INT NOT NULL,
|
||||
DeviceID INT NOT NULL,
|
||||
LastTimestamp BIGINT DEFAULT 0,
|
||||
LastSync TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (DeviceID) REFERENCES GpodderDevices(DeviceID) ON DELETE CASCADE,
|
||||
UNIQUE(UserID, DeviceID)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_gpodder_syncstate_userid_deviceid ON GpodderSyncState(UserID, DeviceID);
|
||||
`,
|
||||
},
|
||||
}
|
||||
}
|
||||
91
PinePods-0.8.2/gpodder-api/internal/db/postgres.go
Normal file
91
PinePods-0.8.2/gpodder-api/internal/db/postgres.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"pinepods/gpodder-api/config"
|
||||
"strings"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
// PostgresDB represents a connection to the PostgreSQL database
|
||||
type PostgresDB struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
// NewPostgresDB creates a new PostgreSQL database connection
|
||||
func NewPostgresDB(cfg config.DatabaseConfig) (*PostgresDB, error) {
|
||||
// Print connection details for debugging (hide password for security)
|
||||
fmt.Printf("Connecting to database: host=%s port=%d user=%s dbname=%s sslmode=%s\n",
|
||||
cfg.Host, cfg.Port, cfg.User, cfg.DBName, cfg.SSLMode)
|
||||
|
||||
// Get password directly from environment to handle special characters
|
||||
password := os.Getenv("DB_PASSWORD")
|
||||
if password == "" {
|
||||
// Fall back to config if env var is empty
|
||||
password = cfg.Password
|
||||
}
|
||||
|
||||
// Escape special characters in password
|
||||
escapedPassword := url.QueryEscape(password)
|
||||
|
||||
// Use a connection string without password for logging
|
||||
logConnStr := fmt.Sprintf(
|
||||
"host=%s port=%d user=%s dbname=%s sslmode=%s",
|
||||
cfg.Host, cfg.Port, cfg.User, cfg.DBName, cfg.SSLMode,
|
||||
)
|
||||
fmt.Printf("Connection string (without password): %s\n", logConnStr)
|
||||
|
||||
// Build the actual connection string with password
|
||||
connStr := fmt.Sprintf(
|
||||
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||
cfg.Host, cfg.Port, cfg.User, password, cfg.DBName, cfg.SSLMode,
|
||||
)
|
||||
|
||||
// Try alternate connection string format if the first fails
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
// Try URL format connection string
|
||||
urlConnStr := fmt.Sprintf(
|
||||
"postgres://%s:%s@%s:%d/%s?sslmode=%s",
|
||||
cfg.User, escapedPassword, cfg.Host, cfg.Port, cfg.DBName, cfg.SSLMode,
|
||||
)
|
||||
fmt.Println("First connection attempt failed, trying URL format...")
|
||||
db, err = sql.Open("postgres", urlConnStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database connection: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test the connection
|
||||
if err := db.Ping(); err != nil {
|
||||
db.Close()
|
||||
// Check if error contains password authentication failure
|
||||
if strings.Contains(err.Error(), "password authentication failed") {
|
||||
// Print environment variables (hide password)
|
||||
fmt.Println("Password authentication failed. Environment variables:")
|
||||
fmt.Printf("DB_HOST=%s\n", os.Getenv("DB_HOST"))
|
||||
fmt.Printf("DB_PORT=%s\n", os.Getenv("DB_PORT"))
|
||||
fmt.Printf("DB_USER=%s\n", os.Getenv("DB_USER"))
|
||||
fmt.Printf("DB_NAME=%s\n", os.Getenv("DB_NAME"))
|
||||
fmt.Printf("DB_PASSWORD=*** (length: %d)\n", len(os.Getenv("DB_PASSWORD")))
|
||||
}
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Successfully connected to the database")
|
||||
|
||||
// Migrations are now handled by the Python migration system
|
||||
// Skip Go migrations to avoid conflicts
|
||||
fmt.Println("Skipping Go migrations - now handled by Python migration system")
|
||||
|
||||
return &PostgresDB{DB: db}, nil
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func (db *PostgresDB) Close() error {
|
||||
return db.DB.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user