feat: rename the database from Settings, keeping all config
Adds a Rename button to Settings -> Database: renames the current SQLite database (e.g. opslog.db -> F4BPO.db) while preserving every setting/profile, unlike New database which starts empty. Implemented as a VACUUM INTO copy + pointer switch; the old file (open and locked now) is scheduled via config.json delete_pending and removed, with its -wal/-shm sidecars, on the next launch.
This commit is contained in:
@@ -699,6 +699,20 @@ func (a *App) startup(ctx context.Context) {
|
|||||||
usingDefault = false
|
usingDefault = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// A rename in a previous session left the OLD file to delete now that it's no
|
||||||
|
// longer open. Only ever delete a file that ISN'T the one we're about to use.
|
||||||
|
if boot := readBootstrap(dataDir); strings.TrimSpace(boot.DeletePending) != "" {
|
||||||
|
old := strings.TrimSpace(boot.DeletePending)
|
||||||
|
if old != a.dbPath {
|
||||||
|
for _, p := range []string{old, old + "-wal", old + "-shm"} {
|
||||||
|
if err := os.Remove(p); err == nil {
|
||||||
|
fmt.Printf("OpsLog: removed old database file %s\n", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boot.DeletePending = ""
|
||||||
|
_ = writeBootstrap(dataDir, boot)
|
||||||
|
}
|
||||||
if err := os.MkdirAll(filepath.Dir(a.dbPath), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(a.dbPath), 0o755); err != nil {
|
||||||
a.startupErr = "cannot create db folder: " + err.Error()
|
a.startupErr = "cannot create db folder: " + err.Error()
|
||||||
fmt.Println("OpsLog:", a.startupErr)
|
fmt.Println("OpsLog:", a.startupErr)
|
||||||
@@ -1340,6 +1354,11 @@ func copyFileData(src, dst string) error {
|
|||||||
type dbPointer struct {
|
type dbPointer struct {
|
||||||
DBPath string `json:"db_path"`
|
DBPath string `json:"db_path"`
|
||||||
MySQL *MySQLSettings `json:"mysql,omitempty"`
|
MySQL *MySQLSettings `json:"mysql,omitempty"`
|
||||||
|
// DeletePending is the previous database file to remove on the NEXT launch —
|
||||||
|
// set by a rename, which can't delete the still-open old file in-process. The
|
||||||
|
// startup path deletes it (with its -wal/-shm sidecars) once the new DB is the
|
||||||
|
// one in use, then clears this.
|
||||||
|
DeletePending string `json:"delete_pending,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func dbPointerPath(dataDir string) string { return filepath.Join(dataDir, "config.json") }
|
func dbPointerPath(dataDir string) string { return filepath.Join(dataDir, "config.json") }
|
||||||
@@ -1653,9 +1672,10 @@ func (a *App) PickSaveDatabase() (string, error) {
|
|||||||
return "", fmt.Errorf("no app context")
|
return "", fmt.Errorf("no app context")
|
||||||
}
|
}
|
||||||
return wruntime.SaveFileDialog(a.ctx, wruntime.SaveDialogOptions{
|
return wruntime.SaveFileDialog(a.ctx, wruntime.SaveDialogOptions{
|
||||||
Title: "Save the OpsLog database to…",
|
Title: "Save the OpsLog database to…",
|
||||||
DefaultFilename: "opslog.db",
|
DefaultDirectory: filepath.Dir(a.dbPath),
|
||||||
Filters: []wruntime.FileFilter{{DisplayName: "SQLite database (*.db)", Pattern: "*.db"}},
|
DefaultFilename: "opslog.db",
|
||||||
|
Filters: []wruntime.FileFilter{{DisplayName: "SQLite database (*.db)", Pattern: "*.db"}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1697,6 +1717,44 @@ func (a *App) MoveDatabase(dest string) error {
|
|||||||
return writeDBPointer(a.dataDir, dest)
|
return writeDBPointer(a.dataDir, dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenameDatabase renames the current database file to dest — keeping ALL config
|
||||||
|
// (it is the same database under a new name), unlike "New database" which starts
|
||||||
|
// empty. Implemented as a consistent copy (VACUUM INTO) plus a switch, then the
|
||||||
|
// ORIGINAL file is scheduled for deletion on the next launch (it is open now and
|
||||||
|
// can't be removed in-process on Windows). dest must not already exist.
|
||||||
|
func (a *App) RenameDatabase(dest string) error {
|
||||||
|
dest = strings.TrimSpace(dest)
|
||||||
|
if dest == "" {
|
||||||
|
return fmt.Errorf("no destination given")
|
||||||
|
}
|
||||||
|
if a.db == nil {
|
||||||
|
return fmt.Errorf("database not open")
|
||||||
|
}
|
||||||
|
old := a.dbPath
|
||||||
|
if strings.EqualFold(filepath.Clean(dest), filepath.Clean(old)) {
|
||||||
|
return fmt.Errorf("that is already the current database name")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(dest); err == nil {
|
||||||
|
return fmt.Errorf("a file already exists at %s — pick a new name", dest)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
||||||
|
return fmt.Errorf("create folder: %w", err)
|
||||||
|
}
|
||||||
|
safe := strings.ReplaceAll(dest, "'", "''")
|
||||||
|
if _, err := a.db.ExecContext(a.ctx, "VACUUM INTO '"+safe+"'"); err != nil {
|
||||||
|
return fmt.Errorf("copy database: %w", err)
|
||||||
|
}
|
||||||
|
boot := readBootstrap(a.dataDir)
|
||||||
|
boot.DBPath = dest
|
||||||
|
// Only schedule the old file for deletion when it's a real, on-disk file (a
|
||||||
|
// custom path or the default opslog.db) — never something we somehow share
|
||||||
|
// with the destination.
|
||||||
|
if strings.TrimSpace(old) != "" && !strings.EqualFold(filepath.Clean(old), filepath.Clean(dest)) {
|
||||||
|
boot.DeletePending = old
|
||||||
|
}
|
||||||
|
return writeBootstrap(a.dataDir, boot)
|
||||||
|
}
|
||||||
|
|
||||||
// CreateDatabase creates a fresh, empty logbook at dest (schema migrated) and
|
// CreateDatabase creates a fresh, empty logbook at dest (schema migrated) and
|
||||||
// points OpsLog at it for the next launch. dest must not already exist.
|
// points OpsLog at it for the next launch. dest must not already exist.
|
||||||
func (a *App) CreateDatabase(dest string) error {
|
func (a *App) CreateDatabase(dest string) error {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
ArrowDown, ArrowUp, ArrowLeft, ArrowRight, Copy, Plus, Star, StarOff, Trash2,
|
ArrowDown, ArrowUp, ArrowLeft, ArrowRight, Copy, Plus, Star, StarOff, Trash2,
|
||||||
ChevronDown, ChevronRight,
|
ChevronDown, ChevronRight,
|
||||||
User, Database, Radio, Cog, Server, Award, Antenna as AntennaIcon,
|
User, Database, Radio, Cog, Server, Award, Antenna as AntennaIcon,
|
||||||
Compass, Wifi, Construction, UploadCloud, Loader2, FolderOpen, Play, Power, Check, Eye, EyeOff,
|
Compass, Wifi, Construction, UploadCloud, Loader2, FolderOpen, Play, Power, Check, Eye, EyeOff, Pencil,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
GetLookupSettings, SaveLookupSettings, ClearLookupCache, TestLookupProvider,
|
GetLookupSettings, SaveLookupSettings, ClearLookupCache, TestLookupProvider,
|
||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
ConnectClusterServer, DisconnectClusterServer,
|
ConnectClusterServer, DisconnectClusterServer,
|
||||||
ConnectAllClusters, DisconnectAllClusters, GetClusterStatus,
|
ConnectAllClusters, DisconnectAllClusters, GetClusterStatus,
|
||||||
GetBackupSettings, SaveBackupSettings, RunBackupNow, PickBackupFolder,
|
GetBackupSettings, SaveBackupSettings, RunBackupNow, PickBackupFolder,
|
||||||
GetDatabaseSettings, PickOpenDatabase, PickSaveDatabase, OpenDatabase, MoveDatabase, ResetDatabaseToDefault, RestartApp, CreateDatabase,
|
GetDatabaseSettings, PickOpenDatabase, PickSaveDatabase, OpenDatabase, MoveDatabase, ResetDatabaseToDefault, RestartApp, CreateDatabase, RenameDatabase,
|
||||||
GetMySQLSettings, SaveMySQLSettings, TestMySQLConnection, GetDBBackendStatus,
|
GetMySQLSettings, SaveMySQLSettings, TestMySQLConnection, GetDBBackendStatus,
|
||||||
GetAutostartPrograms, SaveAutostartPrograms, BrowseExecutable, LaunchAutostartProgram,
|
GetAutostartPrograms, SaveAutostartPrograms, BrowseExecutable, LaunchAutostartProgram,
|
||||||
GetTelemetryEnabled, SetTelemetryEnabled,
|
GetTelemetryEnabled, SetTelemetryEnabled,
|
||||||
@@ -3978,6 +3978,17 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
setDbMsg(p);
|
setDbMsg(p);
|
||||||
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||||
}
|
}
|
||||||
|
// Rename the CURRENT database (keeps all config), unlike New database which
|
||||||
|
// starts empty. The old file is removed on the next launch.
|
||||||
|
async function renameDb() {
|
||||||
|
try {
|
||||||
|
const p = await PickSaveDatabase();
|
||||||
|
if (!p) return;
|
||||||
|
await RenameDatabase(p);
|
||||||
|
await refreshDb();
|
||||||
|
setDbMsg(p);
|
||||||
|
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||||
|
}
|
||||||
async function resetDefault() {
|
async function resetDefault() {
|
||||||
try {
|
try {
|
||||||
await ResetDatabaseToDefault();
|
await ResetDatabaseToDefault();
|
||||||
@@ -4056,6 +4067,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Button size="sm" onClick={createNew}><Plus className="size-3.5" /> {t('db.newDb')}</Button>
|
<Button size="sm" onClick={createNew}><Plus className="size-3.5" /> {t('db.newDb')}</Button>
|
||||||
<Button variant="outline" size="sm" onClick={openExisting}><FolderOpen className="size-3.5" /> {t('db.openExisting')}</Button>
|
<Button variant="outline" size="sm" onClick={openExisting}><FolderOpen className="size-3.5" /> {t('db.openExisting')}</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={renameDb} title={t('db.renameTip')}><Pencil className="size-3.5" /> {t('db.rename')}</Button>
|
||||||
<Button variant="outline" size="sm" onClick={saveCopy}><Copy className="size-3.5" /> {t('db.saveCopy')}</Button>
|
<Button variant="outline" size="sm" onClick={saveCopy}><Copy className="size-3.5" /> {t('db.saveCopy')}</Button>
|
||||||
{dbSettings.is_custom && <Button variant="ghost" size="sm" onClick={resetDefault}>{t('db.resetDefault')}</Button>}
|
{dbSettings.is_custom && <Button variant="ghost" size="sm" onClick={resetDefault}>{t('db.resetDefault')}</Button>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ const en: Dict = {
|
|||||||
'db.backend': 'Backend', 'db.configLocal': 'settings stay in the local SQLite file', 'db.connectUse': 'Connect & use',
|
'db.backend': 'Backend', 'db.configLocal': 'settings stay in the local SQLite file', 'db.connectUse': 'Connect & use',
|
||||||
'db.savedRestart': 'Saved. Restart OpsLog to open this logbook:', 'db.restartNow': 'Restart OpsLog', 'db.restartHint': '(reopens automatically)',
|
'db.savedRestart': 'Saved. Restart OpsLog to open this logbook:', 'db.restartNow': 'Restart OpsLog', 'db.restartHint': '(reopens automatically)',
|
||||||
'db.current': 'Current database', 'db.customLoc': '(custom location)', 'db.default': '(default)', 'db.defaultLabel': 'Default:',
|
'db.current': 'Current database', 'db.customLoc': '(custom location)', 'db.default': '(default)', 'db.defaultLabel': 'Default:',
|
||||||
'db.newDb': 'New database…', 'db.openExisting': 'Open existing…', 'db.saveCopy': 'Save a copy & switch…', 'db.resetDefault': 'Reset to default', 'db.quitNow': 'Quit now',
|
'db.newDb': 'New database…', 'db.openExisting': 'Open existing…', 'db.saveCopy': 'Save a copy & switch…', 'db.rename': 'Rename…', 'db.renameTip': 'Rename this database (keeps all your config). The old file is removed on the next launch.', 'db.resetDefault': 'Reset to default', 'db.quitNow': 'Quit now',
|
||||||
'db.host': 'Host', 'db.port': 'Port', 'db.database': 'Database', 'db.user': 'User', 'db.testCreate': 'Test & create database', 'db.testing': 'Testing…', 'db.connectedReady': 'Connected — database ready ✓', 'db.failed': 'Failed: ',
|
'db.host': 'Host', 'db.port': 'Port', 'db.database': 'Database', 'db.user': 'User', 'db.testCreate': 'Test & create database', 'db.testing': 'Testing…', 'db.connectedReady': 'Connected — database ready ✓', 'db.failed': 'Failed: ',
|
||||||
'db.mysqlHint': 'Several OpsLog instances pointed at one MySQL database see each other\'s QSOs live (refreshed every 2 s). Test & create the database, then Save & switch logbook above to start logging there.',
|
'db.mysqlHint': 'Several OpsLog instances pointed at one MySQL database see each other\'s QSOs live (refreshed every 2 s). Test & create the database, then Save & switch logbook above to start logging there.',
|
||||||
'db.dataLocation': 'Data location', 'db.currentDataDir': 'Current data directory',
|
'db.dataLocation': 'Data location', 'db.currentDataDir': 'Current data directory',
|
||||||
@@ -544,7 +544,7 @@ const fr: Dict = {
|
|||||||
'db.backend': 'Base de données', 'db.configLocal': 'les réglages restent dans le fichier SQLite local', 'db.connectUse': 'Connecter et utiliser',
|
'db.backend': 'Base de données', 'db.configLocal': 'les réglages restent dans le fichier SQLite local', 'db.connectUse': 'Connecter et utiliser',
|
||||||
'db.savedRestart': 'Enregistré. Redémarrez OpsLog pour ouvrir cette base :', 'db.restartNow': 'Redémarrer OpsLog', 'db.restartHint': '(réouverture automatique)',
|
'db.savedRestart': 'Enregistré. Redémarrez OpsLog pour ouvrir cette base :', 'db.restartNow': 'Redémarrer OpsLog', 'db.restartHint': '(réouverture automatique)',
|
||||||
'db.current': 'Base actuelle', 'db.customLoc': '(emplacement personnalisé)', 'db.default': '(par défaut)', 'db.defaultLabel': 'Par défaut :',
|
'db.current': 'Base actuelle', 'db.customLoc': '(emplacement personnalisé)', 'db.default': '(par défaut)', 'db.defaultLabel': 'Par défaut :',
|
||||||
'db.newDb': 'Nouvelle base…', 'db.openExisting': 'Ouvrir existante…', 'db.saveCopy': 'Enregistrer une copie & basculer…', 'db.resetDefault': 'Réinitialiser par défaut', 'db.quitNow': 'Quitter maintenant',
|
'db.newDb': 'Nouvelle base…', 'db.openExisting': 'Ouvrir existante…', 'db.saveCopy': 'Enregistrer une copie & basculer…', 'db.rename': 'Renommer…', 'db.renameTip': 'Renomme cette base (garde toute ta config). L’ancien fichier est supprimé au prochain lancement.', 'db.resetDefault': 'Réinitialiser par défaut', 'db.quitNow': 'Quitter maintenant',
|
||||||
'db.host': 'Hôte', 'db.port': 'Port', 'db.database': 'Base', 'db.user': 'Utilisateur', 'db.testCreate': 'Tester & créer la base', 'db.testing': 'Test…', 'db.connectedReady': 'Connecté — base prête ✓', 'db.failed': 'Échec : ',
|
'db.host': 'Hôte', 'db.port': 'Port', 'db.database': 'Base', 'db.user': 'Utilisateur', 'db.testCreate': 'Tester & créer la base', 'db.testing': 'Test…', 'db.connectedReady': 'Connecté — base prête ✓', 'db.failed': 'Échec : ',
|
||||||
'db.mysqlHint': "Plusieurs instances d'OpsLog pointées vers une même base MySQL voient leurs QSO en direct (rafraîchi toutes les 2 s). Teste & crée la base, puis Enregistrer & basculer le journal ci-dessus pour commencer à logger là-bas.",
|
'db.mysqlHint': "Plusieurs instances d'OpsLog pointées vers une même base MySQL voient leurs QSO en direct (rafraîchi toutes les 2 s). Teste & crée la base, puis Enregistrer & basculer le journal ci-dessus pour commencer à logger là-bas.",
|
||||||
'db.dataLocation': 'Emplacement des données', 'db.currentDataDir': 'Dossier de données actuel',
|
'db.dataLocation': 'Emplacement des données', 'db.currentDataDir': 'Dossier de données actuel',
|
||||||
|
|||||||
Vendored
+2
@@ -678,6 +678,8 @@ export function ReloadUDPIntegrations():Promise<Array<string>>;
|
|||||||
|
|
||||||
export function RemovePassphrase(arg1:string):Promise<void>;
|
export function RemovePassphrase(arg1:string):Promise<void>;
|
||||||
|
|
||||||
|
export function RenameDatabase(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function RenderEQSL(arg1:number,arg2:number):Promise<string>;
|
export function RenderEQSL(arg1:number,arg2:number):Promise<string>;
|
||||||
|
|
||||||
export function ReplaceAwardReferences(arg1:string,arg2:Array<awardref.Ref>):Promise<number>;
|
export function ReplaceAwardReferences(arg1:string,arg2:Array<awardref.Ref>):Promise<number>;
|
||||||
|
|||||||
@@ -1314,6 +1314,10 @@ export function RemovePassphrase(arg1) {
|
|||||||
return window['go']['main']['App']['RemovePassphrase'](arg1);
|
return window['go']['main']['App']['RemovePassphrase'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RenameDatabase(arg1) {
|
||||||
|
return window['go']['main']['App']['RenameDatabase'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function RenderEQSL(arg1, arg2) {
|
export function RenderEQSL(arg1, arg2) {
|
||||||
return window['go']['main']['App']['RenderEQSL'](arg1, arg2);
|
return window['go']['main']['App']['RenderEQSL'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user