feat(hamqth): upload the whole log in one file
The per-QSO API is the only correct way to send a SELECTION, and at the pace it must be driven a 14k backlog costs the better part of an hour. HamQTH's other endpoint takes a whole log as one file — and REPLACES what is on the site with it: its documentation says plainly that partial uploads do not exist. So it is offered as its own deliberate act behind a confirmation, never as the batch path behind 'send these', where it would delete every QSO the operator had not selected. Scoped to the callsign this profile uploads as, so a database holding two operators' contacts cannot push one into the other's log; tar.gz above 12 MB because the ceiling is 20 and a six-figure log passes it as text; and every QSO not already stamped is marked sent afterwards, in bulk, so the backlog list agrees with reality.
This commit is contained in:
@@ -11460,6 +11460,120 @@ func (a *App) TestClublogUpload() (string, error) {
|
||||
return extsvc.TestClublog(a.ctx, a.loadExternalServices().Clublog)
|
||||
}
|
||||
|
||||
// UploadFullLogHamQTH replaces the HamQTH log with this one, in one request.
|
||||
//
|
||||
// The per-QSO API is the only correct way to send a SELECTION, and at the pace
|
||||
// it has to be driven a full backlog costs the better part of an hour. This is
|
||||
// the other endpoint HamQTH offers: a whole log as one file, which is why it
|
||||
// only ever runs on an explicit "replace my HamQTH log" — the site keeps
|
||||
// nothing that is not in the file.
|
||||
//
|
||||
// Scoped to the callsign this profile uploads as: a database holding two
|
||||
// operators' contacts must not push one operator's QSOs into the other's log.
|
||||
func (a *App) UploadFullLogHamQTH() error {
|
||||
if a.qso == nil {
|
||||
return fmt.Errorf("db not initialized")
|
||||
}
|
||||
cfg := a.loadExternalServices().HamQTH
|
||||
if strings.TrimSpace(cfg.Username) == "" || cfg.Password == "" {
|
||||
return fmt.Errorf("set the HamQTH username and password first")
|
||||
}
|
||||
go a.runFullLogHamQTH(cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) runFullLogHamQTH(cfg extsvc.ServiceConfig) {
|
||||
emit := func(line string) {
|
||||
if a.ctx != nil {
|
||||
wruntime.EventsEmit(a.ctx, "qslmgr:log", line)
|
||||
}
|
||||
}
|
||||
done := func(n int) {
|
||||
if a.ctx != nil {
|
||||
wruntime.EventsEmit(a.ctx, "qslmgr:done", map[string]any{"uploaded": n, "total": n})
|
||||
}
|
||||
}
|
||||
ctx := a.ctx
|
||||
owner := a.uploadOwnerCall(extsvc.ServiceHamQTH)
|
||||
|
||||
// Written to a temp file rather than a buffer so the ordinary, tested
|
||||
// exporter does the work — the same one the Export menu uses.
|
||||
tmp, err := os.CreateTemp("", "opslog-hamqth-*.adi")
|
||||
if err != nil {
|
||||
emit("Export failed: " + err.Error())
|
||||
done(0)
|
||||
return
|
||||
}
|
||||
path := tmp.Name()
|
||||
_ = tmp.Close()
|
||||
defer os.Remove(path)
|
||||
|
||||
emit("Exporting the log…")
|
||||
var res adif.ExportResult
|
||||
if owner != "" {
|
||||
// station_callsign empty OR the owner call — an old QSO logged before
|
||||
// the field existed belongs to whoever is uploading now.
|
||||
f := qso.QueryFilter{Match: "OR", Conditions: []qso.Condition{
|
||||
{Field: "station_callsign", Op: "eq", Value: ""},
|
||||
{Field: "station_callsign", Op: "eq", Value: owner},
|
||||
}}
|
||||
res, err = a.ExportADIFFiltered(path, false, f, nil)
|
||||
} else {
|
||||
res, err = a.ExportADIF(path, false, nil)
|
||||
}
|
||||
if err != nil {
|
||||
emit("Export failed: " + err.Error())
|
||||
done(0)
|
||||
return
|
||||
}
|
||||
data, rerr := os.ReadFile(path)
|
||||
if rerr != nil {
|
||||
emit("Export failed: " + rerr.Error())
|
||||
done(0)
|
||||
return
|
||||
}
|
||||
emit(fmt.Sprintf("Uploading %d QSO(s) to HamQTH — this REPLACES the log there…", res.Count))
|
||||
|
||||
up, uerr := extsvc.UploadHamQTHFullLog(ctx, nil, cfg, string(data))
|
||||
if uerr != nil || !up.OK {
|
||||
msg := up.Message
|
||||
if uerr != nil {
|
||||
msg = uerr.Error()
|
||||
}
|
||||
emit("Upload failed: " + msg)
|
||||
applog.Printf("hamqth: full-log upload failed: %s", msg)
|
||||
done(0)
|
||||
return
|
||||
}
|
||||
emit("HamQTH accepted the log: " + up.Message)
|
||||
emit("HamQTH imports it in the background — errors in the ADIF are e-mailed to you, not reported here.")
|
||||
|
||||
// Everything is on HamQTH now, so nothing is still waiting to be sent. Only
|
||||
// the rows that are not already stamped need writing.
|
||||
pending, lerr := a.qso.ListMissingExtra(ctx, hamqthSentKey)
|
||||
if lerr != nil {
|
||||
applog.Printf("hamqth: marking sent: %v", lerr)
|
||||
} else if len(pending) > 0 {
|
||||
ids := make([]int64, 0, len(pending))
|
||||
for _, q := range pending {
|
||||
ids = append(ids, q.ID)
|
||||
}
|
||||
date := time.Now().UTC().Format("20060102")
|
||||
if _, e := a.qso.BulkSetExtra(ctx, ids, hamqthSentKey, "Y"); e != nil {
|
||||
applog.Printf("hamqth: marking sent: %v", e)
|
||||
}
|
||||
if _, e := a.qso.BulkSetExtra(ctx, ids, hamqthSentDateKey, date); e != nil {
|
||||
applog.Printf("hamqth: marking sent date: %v", e)
|
||||
}
|
||||
emit(fmt.Sprintf("Marked %d QSO(s) as sent to HamQTH.", len(ids)))
|
||||
}
|
||||
applog.Printf("hamqth: full-log upload OK (%d QSOs)", res.Count)
|
||||
if a.ctx != nil {
|
||||
wruntime.EventsEmit(a.ctx, "toast", fmt.Sprintf("HamQTH: %d QSO uploaded", res.Count))
|
||||
}
|
||||
done(res.Count)
|
||||
}
|
||||
|
||||
// TestHamQTHUpload checks the HamQTH credentials against the callbook login —
|
||||
// authenticated, and unable to touch the log.
|
||||
func (a *App) TestHamQTHUpload() (string, error) {
|
||||
|
||||
Reference in New Issue
Block a user