safing-portmaster/service/profile/migrations.go

229 lines
6.7 KiB
Go

package profile
import (
"context"
"fmt"
"regexp"
"github.com/hashicorp/go-version"
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/migration"
"github.com/safing/portbase/database/query"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/service/profile/binmeta"
)
func registerMigrations() error {
return migrations.Add(
migration.Migration{
Description: "Migrate from LinkedPath to Fingerprints and PresentationPath",
Version: "v0.9.9",
MigrateFunc: migrateLinkedPath,
},
migration.Migration{
Description: "Migrate from Icon Fields to Icon List",
Version: "v1.4.7",
MigrateFunc: migrateIcons,
},
migration.Migration{
Description: "Migrate from random profile IDs to fingerprint-derived IDs",
Version: "v1.6.3", // Re-run after mixed results in v1.6.0
MigrateFunc: migrateToDerivedIDs,
},
)
}
func migrateLinkedPath(ctx context.Context, _, to *version.Version, db *database.Interface) error {
// Get iterator over all profiles.
it, err := db.Query(query.New(ProfilesDBPath))
if err != nil {
log.Tracer(ctx).Errorf("profile: failed to migrate from linked path: failed to start query: %s", err)
return nil
}
// Migrate all profiles.
for r := range it.Next {
// Parse profile.
profile, err := EnsureProfile(r)
if err != nil {
log.Tracer(ctx).Debugf("profiles: failed to parse profile %s for migration: %s", r.Key(), err)
continue
}
// Skip if there is no LinkedPath to migrate from.
if profile.LinkedPath == "" {
continue
}
// Update metadata and save if changed.
if profile.updateMetadata("") {
err = db.Put(profile)
if err != nil {
log.Tracer(ctx).Debugf("profiles: failed to save profile %s after migration: %s", r.Key(), err)
} else {
log.Tracer(ctx).Tracef("profiles: migrated profile %s to %s", r.Key(), to)
}
}
}
// Check if there was an error while iterating.
if err := it.Err(); err != nil {
log.Tracer(ctx).Errorf("profile: failed to migrate from linked path: failed to iterate over profiles for migration: %s", err)
}
return nil
}
func migrateIcons(ctx context.Context, _, to *version.Version, db *database.Interface) error {
// Get iterator over all profiles.
it, err := db.Query(query.New(ProfilesDBPath))
if err != nil {
log.Tracer(ctx).Errorf("profile: failed to migrate from icon fields: failed to start query: %s", err)
return nil
}
// Migrate all profiles.
var (
lastErr error
failed int
total int
)
for r := range it.Next {
// Parse profile.
profile, err := EnsureProfile(r)
if err != nil {
log.Tracer(ctx).Debugf("profiles: failed to parse profile %s for migration: %s", r.Key(), err)
continue
}
// Skip if there is no (valid) icon defined or the icon list is already populated.
if profile.Icon == "" || profile.IconType == "" || len(profile.Icons) > 0 {
continue
}
// Migrate to icon list.
profile.Icons = []binmeta.Icon{{
Type: profile.IconType,
Value: profile.Icon,
}}
// Save back to DB.
err = db.Put(profile)
if err != nil {
failed++
lastErr = err
log.Tracer(ctx).Debugf("profiles: failed to save profile %s after migration: %s", r.Key(), err)
} else {
log.Tracer(ctx).Tracef("profiles: migrated profile %s to %s", r.Key(), to)
}
total++
}
// Check if there was an error while iterating.
if err := it.Err(); err != nil {
log.Tracer(ctx).Errorf("profile: failed to migrate from icon fields: failed to iterate over profiles for migration: %s", err)
}
// Log migration failure and try again next time.
if lastErr != nil {
// Normally, an icon migration would not be such a big error, but this is a test
// run for the profile IDs and we absolutely need to know if anything went wrong.
module.Error(
"migration-failed",
"Profile Migration Failed",
fmt.Sprintf("Failed to migrate icons of %d profiles (out of %d pending). The last error was: %s\n\nPlease restart Portmaster to try the migration again.", failed, total, lastErr),
)
return fmt.Errorf("failed to migrate %d profiles (out of %d pending) - last error: %w", failed, total, lastErr)
}
return lastErr
}
var randomUUIDRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
func migrateToDerivedIDs(ctx context.Context, _, to *version.Version, db *database.Interface) error {
var profilesToDelete []string //nolint:prealloc // We don't know how many profiles there are.
// Get iterator over all profiles.
it, err := db.Query(query.New(ProfilesDBPath))
if err != nil {
log.Tracer(ctx).Errorf("profile: failed to migrate to derived profile IDs: failed to start query: %s", err)
return nil
}
// Migrate all profiles.
var (
lastErr error
failed int
total int
)
for r := range it.Next {
// Parse profile.
profile, err := EnsureProfile(r)
if err != nil {
failed++
lastErr = err
log.Tracer(ctx).Debugf("profiles: failed to parse profile %s for migration: %s", r.Key(), err)
continue
}
// Skip if the ID does not look like a random UUID.
if !randomUUIDRegex.MatchString(profile.ID) {
continue
}
// Generate new ID.
oldScopedID := profile.ScopedID()
newID := DeriveProfileID(profile.Fingerprints)
// If they match, skip migration for this profile.
if profile.ID == newID {
continue
}
// Reset key.
profile.ResetKey()
// Set new ID and rebuild the key.
profile.ID = newID
profile.makeKey()
// Save back to DB.
err = db.Put(profile)
if err != nil {
failed++
lastErr = err
log.Tracer(ctx).Debugf("profiles: failed to save profile %s after migration: %s", r.Key(), err)
} else {
log.Tracer(ctx).Tracef("profiles: migrated profile %s to %s", r.Key(), to)
// Add old ID to profiles that we need to delete.
profilesToDelete = append(profilesToDelete, oldScopedID)
}
total++
}
// Check if there was an error while iterating.
if err := it.Err(); err != nil {
log.Tracer(ctx).Errorf("profile: failed to migrate to derived profile IDs: failed to iterate over profiles for migration: %s", err)
}
// Delete old migrated profiles.
for _, scopedID := range profilesToDelete {
if err := db.Delete(ProfilesDBPath + scopedID); err != nil {
log.Tracer(ctx).Errorf("profile: failed to delete old profile %s during migration: %s", scopedID, err)
}
}
// Log migration failure and try again next time.
if lastErr != nil {
module.Error(
"migration-failed",
"Profile Migration Failed",
fmt.Sprintf("Failed to migrate profile IDs of %d profiles (out of %d pending). The last error was: %s\n\nPlease restart Portmaster to try the migration again.", failed, total, lastErr),
)
return fmt.Errorf("failed to migrate %d profiles (out of %d pending) - last error: %w", failed, total, lastErr)
}
return nil
}