up
This commit is contained in:
@@ -1,19 +1,26 @@
|
||||
package extsvc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// clublogRealtimeURL is Club Log's real-time single-QSO upload endpoint.
|
||||
// (Batch ADIF goes to putlogs.php; we push one record per logged QSO.)
|
||||
// clublogRealtimeURL is Club Log's real-time single-QSO upload endpoint, used
|
||||
// when a QSO is logged. Bulk/manual uploads go to clublogBatchURL instead.
|
||||
const clublogRealtimeURL = "https://clublog.org/realtime.php"
|
||||
|
||||
// clublogBatchURL is Club Log's batch ADIF endpoint: it accepts a whole ADIF
|
||||
// file in one multipart request and dedupes server-side, so a manual upload of
|
||||
// N QSOs is one HTTP request instead of N realtime.php calls.
|
||||
const clublogBatchURL = "https://clublog.org/putlogs.php"
|
||||
|
||||
// clublogAppAPIKey is OpsLog's Club Log *application* API key. Club Log
|
||||
// requires an api parameter that identifies the client software (not the
|
||||
// user) — the same way Log4OM embeds its own key — so we ship it baked in
|
||||
@@ -67,6 +74,73 @@ func UploadClublog(ctx context.Context, client *http.Client, cfg ServiceConfig,
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// UploadClublogADIF pushes a whole ADIF document (header + many records) to
|
||||
// Club Log's batch endpoint (putlogs.php) in a single multipart request. Use
|
||||
// this for manual/bulk uploads instead of calling UploadClublog per QSO. Club
|
||||
// Log dedupes server-side, so re-uploading QSOs it already holds is harmless.
|
||||
//
|
||||
// Multipart form fields: email, password, callsign, api, clientident, and the
|
||||
// ADIF as a "file" upload. Returns HTTP 200 on success with a summary body.
|
||||
func UploadClublogADIF(ctx context.Context, client *http.Client, cfg ServiceConfig, adifDoc string) (UploadResult, error) {
|
||||
email := strings.TrimSpace(cfg.Email)
|
||||
call := strings.ToUpper(strings.TrimSpace(cfg.Callsign))
|
||||
switch {
|
||||
case email == "":
|
||||
return UploadResult{}, fmt.Errorf("clublog: account email not set")
|
||||
case cfg.Password == "":
|
||||
return UploadResult{}, fmt.Errorf("clublog: password not set")
|
||||
case call == "":
|
||||
return UploadResult{}, fmt.Errorf("clublog: logbook callsign not set")
|
||||
case strings.TrimSpace(adifDoc) == "":
|
||||
return UploadResult{}, fmt.Errorf("clublog: empty adif document")
|
||||
}
|
||||
api := strings.TrimSpace(cfg.APIKey)
|
||||
if api == "" {
|
||||
api = clublogAppAPIKey
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
_ = mw.WriteField("email", email)
|
||||
_ = mw.WriteField("password", cfg.Password)
|
||||
_ = mw.WriteField("callsign", call)
|
||||
_ = mw.WriteField("api", api)
|
||||
_ = mw.WriteField("clientident", "OpsLog")
|
||||
fw, err := mw.CreateFormFile("file", "opslog.adi")
|
||||
if err != nil {
|
||||
return UploadResult{}, fmt.Errorf("clublog: build form: %w", err)
|
||||
}
|
||||
if _, err := io.WriteString(fw, adifDoc); err != nil {
|
||||
return UploadResult{}, fmt.Errorf("clublog: write adif: %w", err)
|
||||
}
|
||||
if err := mw.Close(); err != nil {
|
||||
return UploadResult{}, fmt.Errorf("clublog: finalise form: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, clublogBatchURL, &buf)
|
||||
if err != nil {
|
||||
return UploadResult{}, fmt.Errorf("clublog: build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 120 * time.Second}
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return UploadResult{}, fmt.Errorf("clublog: request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
|
||||
msg := strings.TrimSpace(string(body))
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return UploadResult{OK: true, Message: msg}, nil
|
||||
}
|
||||
if msg == "" {
|
||||
msg = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
return UploadResult{OK: false, Message: msg}, fmt.Errorf("clublog: batch upload failed: %s", msg)
|
||||
}
|
||||
|
||||
// TestClublog validates the configured credentials by attempting a no-op
|
||||
// style check. Club Log has no dedicated status endpoint, so we report the
|
||||
// fields look complete; a real failure surfaces on the first upload.
|
||||
|
||||
Reference in New Issue
Block a user