added cargo files

This commit is contained in:
2026-03-03 10:57:43 -05:00
parent 478a90e01b
commit 169df46bc2
813 changed files with 227273 additions and 9 deletions

View 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);
`,
},
}
}