feat(backup): "keep every backup" option — timestamped files per exit
Sub-option of "back up on every exit": when on, backup.Run/RunADIF stamp the filename with the time (opslog-YYYY-MM-DD-HHMMSS.db) so each run is a distinct file instead of overwriting the day's snapshot. Rotation still trims to the newest N (raise Rotation for a longer history). Threaded as a `unique` flag through runConfiguredBackup/backupLogADIF from BackupSettings.KeepAll (keyBackupKeepAll); UI sub-checkbox shown only when EveryExit is on.
This commit is contained in:
@@ -296,6 +296,7 @@ const (
|
|||||||
keyBackupRotation = "backup.rotation"
|
keyBackupRotation = "backup.rotation"
|
||||||
keyBackupZip = "backup.zip"
|
keyBackupZip = "backup.zip"
|
||||||
keyBackupEveryExit = "backup.every_exit" // "1" → back up on every quit, not just once/day
|
keyBackupEveryExit = "backup.every_exit" // "1" → back up on every quit, not just once/day
|
||||||
|
keyBackupKeepAll = "backup.keep_all" // "1" → each backup is a distinct time-stamped file (no same-day overwrite)
|
||||||
keyBackupLast = "backup.last_at"
|
keyBackupLast = "backup.last_at"
|
||||||
|
|
||||||
keyQSLDefaultQSLSent = "qsl.qsl_sent"
|
keyQSLDefaultQSLSent = "qsl.qsl_sent"
|
||||||
@@ -1535,7 +1536,7 @@ func (a *App) runBackupForShutdown() error {
|
|||||||
if done && !s.EveryExit { // "every exit" backs up regardless of a prior backup today
|
if done && !s.EveryExit { // "every exit" backs up regardless of a prior backup today
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if _, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip); err != nil {
|
if _, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip, s.KeepAll); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return a.settings.Set(a.ctx, keyBackupLast, time.Now().UTC().Format(time.RFC3339))
|
return a.settings.Set(a.ctx, keyBackupLast, time.Now().UTC().Format(time.RFC3339))
|
||||||
@@ -11784,6 +11785,7 @@ type BackupSettings struct {
|
|||||||
Rotation int `json:"rotation"`
|
Rotation int `json:"rotation"`
|
||||||
Zip bool `json:"zip"`
|
Zip bool `json:"zip"`
|
||||||
EveryExit bool `json:"every_exit"` // back up on every quit, not just the first of the day
|
EveryExit bool `json:"every_exit"` // back up on every quit, not just the first of the day
|
||||||
|
KeepAll bool `json:"keep_all"` // time-stamped file per backup instead of one-per-day (kept up to Rotation)
|
||||||
LastBackupAt string `json:"last_backup_at"`
|
LastBackupAt string `json:"last_backup_at"`
|
||||||
DefaultFolder string `json:"default_folder"` // computed, read-only — shown as a hint
|
DefaultFolder string `json:"default_folder"` // computed, read-only — shown as a hint
|
||||||
}
|
}
|
||||||
@@ -11798,7 +11800,7 @@ func (a *App) GetBackupSettings() (BackupSettings, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
m, err := a.settings.GetMany(a.ctx,
|
m, err := a.settings.GetMany(a.ctx,
|
||||||
keyBackupEnabled, keyBackupFolder, keyBackupRotation, keyBackupZip, keyBackupEveryExit, keyBackupLast)
|
keyBackupEnabled, keyBackupFolder, keyBackupRotation, keyBackupZip, keyBackupEveryExit, keyBackupKeepAll, keyBackupLast)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
@@ -11809,6 +11811,7 @@ func (a *App) GetBackupSettings() (BackupSettings, error) {
|
|||||||
}
|
}
|
||||||
out.Zip = m[keyBackupZip] == "1"
|
out.Zip = m[keyBackupZip] == "1"
|
||||||
out.EveryExit = m[keyBackupEveryExit] == "1"
|
out.EveryExit = m[keyBackupEveryExit] == "1"
|
||||||
|
out.KeepAll = m[keyBackupKeepAll] == "1"
|
||||||
out.LastBackupAt = m[keyBackupLast]
|
out.LastBackupAt = m[keyBackupLast]
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
@@ -11836,6 +11839,7 @@ func (a *App) SaveBackupSettings(s BackupSettings) error {
|
|||||||
keyBackupRotation: strconv.Itoa(s.Rotation),
|
keyBackupRotation: strconv.Itoa(s.Rotation),
|
||||||
keyBackupZip: doZip,
|
keyBackupZip: doZip,
|
||||||
keyBackupEveryExit: boolStr(s.EveryExit),
|
keyBackupEveryExit: boolStr(s.EveryExit),
|
||||||
|
keyBackupKeepAll: boolStr(s.KeepAll),
|
||||||
} {
|
} {
|
||||||
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -11852,15 +11856,15 @@ func (a *App) SaveBackupSettings(s BackupSettings) error {
|
|||||||
// backing up the QSOs once the logbook was split out. On MySQL the contacts
|
// backing up the QSOs once the logbook was split out. On MySQL the contacts
|
||||||
// aren't in a local file, so they're exported to ADIF instead. Returns the path
|
// aren't in a local file, so they're exported to ADIF instead. Returns the path
|
||||||
// of the contacts backup.
|
// of the contacts backup.
|
||||||
func (a *App) runConfiguredBackup(folder string, rotation int, zip bool) (string, error) {
|
func (a *App) runConfiguredBackup(folder string, rotation int, zip, unique bool) (string, error) {
|
||||||
// Config snapshot (profiles, hardware, awards). Best-effort — a config-backup
|
// Config snapshot (profiles, hardware, awards). Best-effort — a config-backup
|
||||||
// failure must never stop the contacts from being protected.
|
// failure must never stop the contacts from being protected.
|
||||||
if _, err := backup.Run(a.ctx, a.db, a.dbPath, folder, rotation, zip, "opslogcfg"); err != nil {
|
if _, err := backup.Run(a.ctx, a.db, a.dbPath, folder, rotation, zip, "opslogcfg", unique); err != nil {
|
||||||
applog.Printf("backup: config snapshot failed: %v", err)
|
applog.Printf("backup: config snapshot failed: %v", err)
|
||||||
}
|
}
|
||||||
if a.dbBackend == "mysql" {
|
if a.dbBackend == "mysql" {
|
||||||
// The live log is on MySQL; VACUUM INTO can't reach it — export to ADIF.
|
// The live log is on MySQL; VACUUM INTO can't reach it — export to ADIF.
|
||||||
return a.backupLogADIF(folder, rotation, zip)
|
return a.backupLogADIF(folder, rotation, zip, unique)
|
||||||
}
|
}
|
||||||
// SQLite: snapshot the logbook file that holds the contacts. Fall back to the
|
// SQLite: snapshot the logbook file that holds the contacts. Fall back to the
|
||||||
// settings db only when it doubles as the logbook (rare split-failure case).
|
// settings db only when it doubles as the logbook (rare split-failure case).
|
||||||
@@ -11868,7 +11872,7 @@ func (a *App) runConfiguredBackup(folder string, rotation int, zip bool) (string
|
|||||||
if conn == nil || path == "" {
|
if conn == nil || path == "" {
|
||||||
conn, path = a.db, a.dbPath
|
conn, path = a.db, a.dbPath
|
||||||
}
|
}
|
||||||
return backup.Run(a.ctx, conn, path, folder, rotation, zip, "opslog")
|
return backup.Run(a.ctx, conn, path, folder, rotation, zip, "opslog", unique)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunBackupNow forces an immediate backup using the persisted settings.
|
// RunBackupNow forces an immediate backup using the persisted settings.
|
||||||
@@ -11882,7 +11886,7 @@ func (a *App) RunBackupNow() (string, error) {
|
|||||||
if folder == "" {
|
if folder == "" {
|
||||||
folder = s.DefaultFolder
|
folder = s.DefaultFolder
|
||||||
}
|
}
|
||||||
path, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip)
|
path, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip, s.KeepAll)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return path, err
|
return path, err
|
||||||
}
|
}
|
||||||
@@ -11893,11 +11897,11 @@ func (a *App) RunBackupNow() (string, error) {
|
|||||||
// backupLogADIF writes a rotating ADIF export of the (MySQL) logbook into the
|
// backupLogADIF writes a rotating ADIF export of the (MySQL) logbook into the
|
||||||
// backup folder. The full set of ADIF + app fields is included so the backup is
|
// backup folder. The full set of ADIF + app fields is included so the backup is
|
||||||
// a complete, re-importable copy of the log.
|
// a complete, re-importable copy of the log.
|
||||||
func (a *App) backupLogADIF(folder string, rotation int, zip bool) (string, error) {
|
func (a *App) backupLogADIF(folder string, rotation int, zip, unique bool) (string, error) {
|
||||||
if a.qso == nil {
|
if a.qso == nil {
|
||||||
return "", fmt.Errorf("logbook not initialized")
|
return "", fmt.Errorf("logbook not initialized")
|
||||||
}
|
}
|
||||||
return backup.RunADIF(folder, rotation, zip, func(p string) error {
|
return backup.RunADIF(folder, rotation, zip, unique, func(p string) error {
|
||||||
ex := &adif.Exporter{Repo: a.qso, AppName: "OpsLog", AppVersion: "0.1", IncludeAppFields: true}
|
ex := &adif.Exporter{Repo: a.qso, AppName: "OpsLog", AppVersion: "0.1", IncludeAppFields: true}
|
||||||
_, e := ex.ExportFile(a.ctx, p)
|
_, e := ex.ExportFile(a.ctx, p)
|
||||||
return e
|
return e
|
||||||
@@ -11931,7 +11935,7 @@ func (a *App) maybeShutdownBackup() {
|
|||||||
if done && !s.EveryExit { // "every exit" backs up regardless of a prior backup today
|
if done && !s.EveryExit { // "every exit" backs up regardless of a prior backup today
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip); err != nil {
|
if _, err := a.runConfiguredBackup(folder, s.Rotation, s.Zip, s.KeepAll); err != nil {
|
||||||
fmt.Println("OpsLog: shutdown backup failed:", err)
|
fmt.Println("OpsLog: shutdown backup failed:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -7,14 +7,16 @@
|
|||||||
"Cluster details (My station): removed the example placeholders in My rig / My antenna (and the satellite name/mode) — they looked like real values, so an empty field read as already filled.",
|
"Cluster details (My station): removed the example placeholders in My rig / My antenna (and the satellite name/mode) — they looked like real values, so an empty field read as already filled.",
|
||||||
"QSL: new \"OpsLog QSL received\" marker on the QSO edit window (next to QSL Message), with a live PSE QSL / TNX indicator. When a QSL was received the card prints TNX (thanks), otherwise PSE QSL (please send one) — automatic. The stamp uses the {qso.pse_tnx} token (in the default card footer; placeable anywhere in the designer). A \"QSL received\" column was added to Recent QSOs.",
|
"QSL: new \"OpsLog QSL received\" marker on the QSO edit window (next to QSL Message), with a live PSE QSL / TNX indicator. When a QSL was received the card prints TNX (thanks), otherwise PSE QSL (please send one) — automatic. The stamp uses the {qso.pse_tnx} token (in the default card footer; placeable anywhere in the designer). A \"QSL received\" column was added to Recent QSOs.",
|
||||||
"QSL: a default QSL message (Settings → E-mail, under the QSL card e-mail) now prints on the card when a QSO has no QSL Message of its own — a QSO's own QSL Message always takes precedence.",
|
"QSL: a default QSL message (Settings → E-mail, under the QSL card e-mail) now prints on the card when a QSO has no QSL Message of its own — a QSO's own QSL Message always takes precedence.",
|
||||||
"UDP outbound: a QSO logged from WSJT-X / JTDX / MSHV (via the inbound UDP listener) is now forwarded to the outbound ADIF integrations too — previously only QSOs typed directly into OpsLog were re-emitted."
|
"UDP outbound: a QSO logged from WSJT-X / JTDX / MSHV (via the inbound UDP listener) is now forwarded to the outbound ADIF integrations too — previously only QSOs typed directly into OpsLog were re-emitted.",
|
||||||
|
"Backup: with \"back up on every exit\", a new \"Keep every backup\" sub-option writes a distinct time-stamped file per exit (opslog-YYYY-MM-DD-HHMMSS.db) instead of overwriting the day's file. Rotation still keeps the newest N — raise it for more history."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Sauvegarde : nouvelle option \"Sauvegarder à chaque fermeture\" (Réglages → Sauvegarde). Activée, OpsLog sauvegarde la base à chaque fois que tu quittes au lieu d'une seule fois par jour — les QSO d'une seconde session sont ainsi toujours pris.",
|
"Sauvegarde : nouvelle option \"Sauvegarder à chaque fermeture\" (Réglages → Sauvegarde). Activée, OpsLog sauvegarde la base à chaque fois que tu quittes au lieu d'une seule fois par jour — les QSO d'une seconde session sont ainsi toujours pris.",
|
||||||
"Détails cluster (Ma station) : suppression des exemples en filigrane dans Mon rig / Mon antenne (et nom/mode satellite) — ils ressemblaient à de vraies valeurs, faisant croire qu'un champ vide était déjà rempli.",
|
"Détails cluster (Ma station) : suppression des exemples en filigrane dans Mon rig / Mon antenne (et nom/mode satellite) — ils ressemblaient à de vraies valeurs, faisant croire qu'un champ vide était déjà rempli.",
|
||||||
"QSL : nouveau marqueur \"QSL OpsLog reçue\" dans la fenêtre d'édition du QSO (à côté du Message QSL), avec un indicateur PSE QSL / TNX en direct. Si une QSL a été reçue, la carte imprime TNX (merci), sinon PSE QSL (envoie-moi une carte) — automatique. Le tampon utilise le token {qso.pse_tnx} (dans le pied de carte par défaut ; plaçable où tu veux dans le designer). Une colonne \"QSL reçue\" a été ajoutée aux QSO récents.",
|
"QSL : nouveau marqueur \"QSL OpsLog reçue\" dans la fenêtre d'édition du QSO (à côté du Message QSL), avec un indicateur PSE QSL / TNX en direct. Si une QSL a été reçue, la carte imprime TNX (merci), sinon PSE QSL (envoie-moi une carte) — automatique. Le tampon utilise le token {qso.pse_tnx} (dans le pied de carte par défaut ; plaçable où tu veux dans le designer). Une colonne \"QSL reçue\" a été ajoutée aux QSO récents.",
|
||||||
"QSL : un message QSL par défaut (Réglages → E-mail, sous l'e-mail de carte QSL) s'imprime désormais sur la carte quand un QSO n'a pas son propre Message QSL — le Message QSL du QSO l'emporte toujours.",
|
"QSL : un message QSL par défaut (Réglages → E-mail, sous l'e-mail de carte QSL) s'imprime désormais sur la carte quand un QSO n'a pas son propre Message QSL — le Message QSL du QSO l'emporte toujours.",
|
||||||
"UDP sortant : un QSO enregistré depuis WSJT-X / JTDX / MSHV (via l'écouteur UDP entrant) est désormais aussi transmis aux intégrations ADIF sortantes — avant, seuls les QSO saisis directement dans OpsLog étaient réémis."
|
"UDP sortant : un QSO enregistré depuis WSJT-X / JTDX / MSHV (via l'écouteur UDP entrant) est désormais aussi transmis aux intégrations ADIF sortantes — avant, seuls les QSO saisis directement dans OpsLog étaient réémis.",
|
||||||
|
"Sauvegarde : avec \"sauvegarder à chaque fermeture\", une nouvelle sous-option \"Garder chaque sauvegarde\" écrit un fichier horodaté distinct par fermeture (opslog-AAAA-MM-JJ-HHMMSS.db) au lieu d'écraser le fichier du jour. La rotation garde les N plus récents — augmente-la pour plus d'historique."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4368,13 +4368,26 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
/>
|
/>
|
||||||
<span>{t('bk.zip')}</span>
|
<span>{t('bk.zip')}</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2 text-sm cursor-pointer pb-2">
|
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={!!(backupCfg as any).every_exit}
|
checked={!!(backupCfg as any).every_exit}
|
||||||
onCheckedChange={(c) => setBackupCfg((b) => ({ ...b, every_exit: !!c } as any))}
|
onCheckedChange={(c) => setBackupCfg((b) => ({ ...b, every_exit: !!c } as any))}
|
||||||
/>
|
/>
|
||||||
<span>{t('bk.everyExit')}</span>
|
<span>{t('bk.everyExit')}</span>
|
||||||
</label>
|
</label>
|
||||||
|
{!!(backupCfg as any).every_exit && (
|
||||||
|
<label className="flex items-start gap-2 text-sm cursor-pointer pl-6 pb-2">
|
||||||
|
<Checkbox
|
||||||
|
className="mt-0.5"
|
||||||
|
checked={!!(backupCfg as any).keep_all}
|
||||||
|
onCheckedChange={(c) => setBackupCfg((b) => ({ ...b, keep_all: !!c } as any))}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{t('bk.keepAll')}
|
||||||
|
<span className="block text-xs text-muted-foreground mt-0.5">{t('bk.keepAllHint')}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-border/60 pt-3 flex items-center gap-3">
|
<div className="border-t border-border/60 pt-3 flex items-center gap-3">
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ const en: Dict = {
|
|||||||
'bk.hintMysql': 'On close (once/day) OpsLog snapshots the local SQLite (config) AND exports the shared MySQL log to ADIF — opslog-log-<date>.adi — so your contacts are protected even though they live on the server. Rotation keeps the last N of each.',
|
'bk.hintMysql': 'On close (once/day) OpsLog snapshots the local SQLite (config) AND exports the shared MySQL log to ADIF — opslog-log-<date>.adi — so your contacts are protected even though they live on the server. Rotation keeps the last N of each.',
|
||||||
'bk.hint': 'OpsLog can copy the SQLite database to a folder of your choice when you close it, once per day. Rotation keeps the last N copies and deletes older ones.',
|
'bk.hint': 'OpsLog can copy the SQLite database to a folder of your choice when you close it, once per day. Rotation keeps the last N copies and deletes older ones.',
|
||||||
'bk.auto': 'Automatic backup when closing OpsLog (max once per day)', 'bk.folder': 'Backup folder', 'bk.folderPh': 'leave empty for default', 'bk.browse': 'Browse…',
|
'bk.auto': 'Automatic backup when closing OpsLog (max once per day)', 'bk.folder': 'Backup folder', 'bk.folderPh': 'leave empty for default', 'bk.browse': 'Browse…',
|
||||||
'bk.effective': 'Effective folder:', 'bk.defaultUse': 'If empty, OpsLog uses the default:', 'bk.rotation': 'Rotation (copies to keep)', 'bk.zip': 'ZIP backup (smaller file)', 'bk.everyExit': 'Back up on every exit (not just once a day)',
|
'bk.effective': 'Effective folder:', 'bk.defaultUse': 'If empty, OpsLog uses the default:', 'bk.rotation': 'Rotation (copies to keep)', 'bk.zip': 'ZIP backup (smaller file)', 'bk.everyExit': 'Back up on every exit (not just once a day)', 'bk.keepAll': 'Keep every backup (timestamped files)', 'bk.keepAllHint': 'Each exit writes a separate opslog-YYYY-MM-DD-HHMMSS.db instead of overwriting the day’s file. Rotation still keeps the newest N — raise it for more history.',
|
||||||
'bk.lastRun': 'Last run:', 'bk.never': 'never', 'bk.backupNow': 'Back up now', 'bk.backingUp': 'Backing up…', 'bk.writtenTo': 'Backup written to',
|
'bk.lastRun': 'Last run:', 'bk.never': 'never', 'bk.backupNow': 'Back up now', 'bk.backingUp': 'Backing up…', 'bk.writtenTo': 'Backup written to',
|
||||||
// Section hints (hardware/software panel headers)
|
// Section hints (hardware/software panel headers)
|
||||||
'autostart.hint': 'Launch external programs (WSJT-X, JTAlert, rotator control…) when OpsLog starts. A program already running is not started again. Saved per profile.',
|
'autostart.hint': 'Launch external programs (WSJT-X, JTAlert, rotator control…) when OpsLog starts. A program already running is not started again. Saved per profile.',
|
||||||
@@ -681,7 +681,7 @@ const fr: Dict = {
|
|||||||
'bk.hintMysql': "À la fermeture (1×/jour) OpsLog sauvegarde le SQLite local (config) ET exporte le log MySQL partagé en ADIF — opslog-log-<date>.adi — pour protéger tes contacts même s'ils sont sur le serveur. La rotation garde les N derniers de chaque.",
|
'bk.hintMysql': "À la fermeture (1×/jour) OpsLog sauvegarde le SQLite local (config) ET exporte le log MySQL partagé en ADIF — opslog-log-<date>.adi — pour protéger tes contacts même s'ils sont sur le serveur. La rotation garde les N derniers de chaque.",
|
||||||
'bk.hint': "OpsLog peut copier la base SQLite dans un dossier de ton choix à la fermeture, une fois par jour. La rotation garde les N dernières copies et supprime les plus anciennes.",
|
'bk.hint': "OpsLog peut copier la base SQLite dans un dossier de ton choix à la fermeture, une fois par jour. La rotation garde les N dernières copies et supprime les plus anciennes.",
|
||||||
'bk.auto': 'Sauvegarde auto à la fermeture d\'OpsLog (max 1×/jour)', 'bk.folder': 'Dossier de sauvegarde', 'bk.folderPh': 'vide = dossier par défaut', 'bk.browse': 'Parcourir…',
|
'bk.auto': 'Sauvegarde auto à la fermeture d\'OpsLog (max 1×/jour)', 'bk.folder': 'Dossier de sauvegarde', 'bk.folderPh': 'vide = dossier par défaut', 'bk.browse': 'Parcourir…',
|
||||||
'bk.effective': 'Dossier effectif :', 'bk.defaultUse': 'Si vide, OpsLog utilise le défaut :', 'bk.rotation': 'Rotation (copies à garder)', 'bk.zip': 'Sauvegarde ZIP (fichier plus petit)', 'bk.everyExit': 'Sauvegarder à chaque fermeture (pas une fois par jour)',
|
'bk.effective': 'Dossier effectif :', 'bk.defaultUse': 'Si vide, OpsLog utilise le défaut :', 'bk.rotation': 'Rotation (copies à garder)', 'bk.zip': 'Sauvegarde ZIP (fichier plus petit)', 'bk.everyExit': 'Sauvegarder à chaque fermeture (pas une fois par jour)', 'bk.keepAll': 'Garder chaque sauvegarde (fichiers horodatés)', 'bk.keepAllHint': 'Chaque fermeture écrit un opslog-AAAA-MM-JJ-HHMMSS.db distinct au lieu d’écraser le fichier du jour. La rotation garde les N plus récents — augmente-la pour plus d’historique.',
|
||||||
'bk.lastRun': 'Dernière exécution :', 'bk.never': 'jamais', 'bk.backupNow': 'Sauvegarder maintenant', 'bk.backingUp': 'Sauvegarde…', 'bk.writtenTo': 'Sauvegarde écrite dans',
|
'bk.lastRun': 'Dernière exécution :', 'bk.never': 'jamais', 'bk.backupNow': 'Sauvegarder maintenant', 'bk.backingUp': 'Sauvegarde…', 'bk.writtenTo': 'Sauvegarde écrite dans',
|
||||||
'autostart.hint': "Lance des programmes externes (WSJT-X, JTAlert, contrôle rotator…) au démarrage d'OpsLog. Un programme déjà lancé n'est pas relancé. Enregistré par profil.",
|
'autostart.hint': "Lance des programmes externes (WSJT-X, JTAlert, contrôle rotator…) au démarrage d'OpsLog. Un programme déjà lancé n'est pas relancé. Enregistré par profil.",
|
||||||
'cat.hint': "Lit la fréquence / bande / mode du poste et les injecte dans le bandeau de saisie en temps réel. Utilise OmniRig (gratuit, tout poste) ou — pour FlexRadio — l'API native SmartSDR (sans OmniRig, temps réel, sans le bug du mode au 2ᵉ clic).",
|
'cat.hint': "Lit la fréquence / bande / mode du poste et les injecte dans le bandeau de saisie en temps réel. Utilise OmniRig (gratuit, tout poste) ou — pour FlexRadio — l'API native SmartSDR (sans OmniRig, temps réel, sans le bug du mode au 2ᵉ clic).",
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func DefaultFolder(dataDir string) string {
|
|||||||
// statement (no torn-copy window while the app keeps writing), and compacts
|
// statement (no torn-copy window while the app keeps writing), and compacts
|
||||||
// the destination as a bonus. It replaces the old "checkpoint + raw io.Copy",
|
// the destination as a bonus. It replaces the old "checkpoint + raw io.Copy",
|
||||||
// which could capture a half-written page during a concurrent write.
|
// which could capture a half-written page during a concurrent write.
|
||||||
func Run(ctx context.Context, dbConn *sql.DB, dbPath, folder string, rotation int, doZip bool, prefix string) (string, error) {
|
func Run(ctx context.Context, dbConn *sql.DB, dbPath, folder string, rotation int, doZip bool, prefix string, unique bool) (string, error) {
|
||||||
if dbConn == nil {
|
if dbConn == nil {
|
||||||
return "", fmt.Errorf("nil db connection")
|
return "", fmt.Errorf("nil db connection")
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,12 @@ func Run(ctx context.Context, dbConn *sql.DB, dbPath, folder string, rotation in
|
|||||||
return "", fmt.Errorf("create backup folder: %w", err)
|
return "", fmt.Errorf("create backup folder: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One file per DAY by default (same-day runs overwrite). With `unique`, add the
|
||||||
|
// time so every run is a distinct file — rotation still keeps the newest N.
|
||||||
stamp := time.Now().Format("2006-01-02")
|
stamp := time.Now().Format("2006-01-02")
|
||||||
|
if unique {
|
||||||
|
stamp = time.Now().Format("2006-01-02-150405")
|
||||||
|
}
|
||||||
base := fmt.Sprintf("%s-%s", prefix, stamp)
|
base := fmt.Sprintf("%s-%s", prefix, stamp)
|
||||||
|
|
||||||
// VACUUM INTO requires a non-existent target → use a temp file, then
|
// VACUUM INTO requires a non-existent target → use a temp file, then
|
||||||
@@ -106,7 +111,7 @@ func Run(ctx context.Context, dbConn *sql.DB, dbPath, folder string, rotation in
|
|||||||
// log lives on a shared MySQL server, where VACUUM INTO can't snapshot it — an
|
// log lives on a shared MySQL server, where VACUUM INTO can't snapshot it — an
|
||||||
// ADIF export is a portable, re-importable backup of the actual contacts.
|
// ADIF export is a portable, re-importable backup of the actual contacts.
|
||||||
// writeADIF must write the full ADIF to the path it's handed.
|
// writeADIF must write the full ADIF to the path it's handed.
|
||||||
func RunADIF(folder string, rotation int, doZip bool, writeADIF func(path string) error) (string, error) {
|
func RunADIF(folder string, rotation int, doZip bool, unique bool, writeADIF func(path string) error) (string, error) {
|
||||||
if rotation <= 0 {
|
if rotation <= 0 {
|
||||||
rotation = 5
|
rotation = 5
|
||||||
}
|
}
|
||||||
@@ -116,7 +121,11 @@ func RunADIF(folder string, rotation int, doZip bool, writeADIF func(path string
|
|||||||
if err := os.MkdirAll(folder, 0o755); err != nil {
|
if err := os.MkdirAll(folder, 0o755); err != nil {
|
||||||
return "", fmt.Errorf("create backup folder: %w", err)
|
return "", fmt.Errorf("create backup folder: %w", err)
|
||||||
}
|
}
|
||||||
base := "opslog-log-" + time.Now().Format("2006-01-02")
|
stamp := time.Now().Format("2006-01-02")
|
||||||
|
if unique {
|
||||||
|
stamp = time.Now().Format("2006-01-02-150405")
|
||||||
|
}
|
||||||
|
base := "opslog-log-" + stamp
|
||||||
tmp := filepath.Join(folder, base+".adi.tmp")
|
tmp := filepath.Join(folder, base+".adi.tmp")
|
||||||
_ = os.Remove(tmp)
|
_ = os.Remove(tmp)
|
||||||
if err := writeADIF(tmp); err != nil {
|
if err := writeADIF(tmp); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user