diff --git a/internal/forms/multipart_test.go b/internal/forms/multipart_test.go new file mode 100644 index 0000000..632c515 --- /dev/null +++ b/internal/forms/multipart_test.go @@ -0,0 +1,53 @@ +package forms_test + +import ( + "bytes" + "encoding/json" + "mime/multipart" + "net/http" + "strconv" + + "dynatron.me/x/stillbox/pkg/talkgroups/xport" +) + +func perr(err error) { + if err != nil { + panic(err) + } +} + +func makeExportRequest(ej *xport.ExportJob, url string) *http.Request { + var buf bytes.Buffer + body := multipart.NewWriter(&buf) + + perr(body.WriteField("type", string(ej.Type))) + + perr(body.WriteField("systemID", strconv.Itoa(int(ej.SystemID)))) + + w, err := body.CreateFormFile("template", ej.TemplateFileName) + perr(err) + + _, err = w.Write(ej.Template) + perr(err) + + r, err := http.NewRequest(http.MethodPost, url, &buf) + perr(err) + + r.Header.Set("Content-Type", body.FormDataContentType()) + + return r +} + +func makeFunkyJSONExportRequest(ej *xport.ExportJob, url string) *http.Request { + var buf bytes.Buffer + je := json.NewEncoder(&buf) + + err := je.Encode(ej) + perr(err) + + r, err := http.NewRequest(http.MethodPost, url, &buf) + perr(err) + r.Header.Set("Content-Type", "application/json") + + return r +} diff --git a/internal/forms/unmarshal_test.go b/internal/forms/unmarshal_test.go index be0b6bc..25ec5d2 100644 --- a/internal/forms/unmarshal_test.go +++ b/internal/forms/unmarshal_test.go @@ -15,6 +15,7 @@ import ( "dynatron.me/x/stillbox/pkg/alerting" "dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/talkgroups/tgstore" + "dynatron.me/x/stillbox/pkg/talkgroups/xport" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -124,6 +125,13 @@ var ( }, OrderBy: common.PtrTo(tgstore.TGOrderID), } + + ExpJob1 = xport.ExportJob{ + Type: xport.FormatSDRTrunk, + SystemID: 197, + Template: []byte("this is a template\n\r\nthingy"), + TemplateFileName: "template.xml", + } ) func makeRequest(fixture string) *http.Request { @@ -238,6 +246,13 @@ func TestUnmarshal(t *testing.T) { expect: &Pag1, opts: []forms.Option{forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty()}, }, + { + name: "non multipart byte field", + r: makeFunkyJSONExportRequest(&ExpJob1, "http://somewhere/export"), + dest: &xport.ExportJob{}, + expect: &ExpJob1, + opts: []forms.Option{forms.WithAcceptBlank(), forms.WithOmitEmpty()}, + }, } for _, tc := range tests { diff --git a/pkg/rest/talkgroups.go b/pkg/rest/talkgroups.go index 9845764..21b443e 100644 --- a/pkg/rest/talkgroups.go +++ b/pkg/rest/talkgroups.go @@ -1,6 +1,7 @@ package rest import ( + "fmt" "net/http" "dynatron.me/x/stillbox/internal/forms" @@ -32,6 +33,8 @@ func (tga *talkgroupAPI) Subrouter() http.Handler { r.Post("/import", tga.tgImport) + r.Post("/export", tga.tgExport) + return r } @@ -156,6 +159,25 @@ func (tga *talkgroupAPI) put(w http.ResponseWriter, r *http.Request) { respond(w, r, record) } +func (tga *talkgroupAPI) tgExport(w http.ResponseWriter, r *http.Request) { + var expJob xport.ExportJob + ctx := r.Context() + + err := forms.Unmarshal(r, &expJob, forms.WithAcceptBlank(), forms.WithOmitEmpty()) + if err != nil { + wErr(w, r, badRequest(err)) + return + } + + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=stillbox_%s", expJob.TemplateFileName)) + w.Header().Set("Content-Type", "text/xml") + + err = expJob.Export(ctx, w) + if err != nil { + wErr(w, r, autoError(err)) + } +} + func (tga *talkgroupAPI) tgImport(w http.ResponseWriter, r *http.Request) { var impJob xport.ImportJob err := forms.Unmarshal(r, &impJob, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty()) diff --git a/pkg/talkgroups/xport/export.go b/pkg/talkgroups/xport/export.go index 4fdcda9..8ca136a 100644 --- a/pkg/talkgroups/xport/export.go +++ b/pkg/talkgroups/xport/export.go @@ -15,9 +15,10 @@ type Exporter interface { } type ExportJob struct { - Type Format `json:"type"` - SystemID int `json:"systemID"` - Template []byte `json:"template"` + Type Format `json:"type" form:"type"` + SystemID int `json:"systemID" form:"systemID"` + Template []byte `json:"template" form:"template" filenameField:"TemplateFileName"` + TemplateFileName string filter.TalkgroupFilter Exporter diff --git a/pkg/talkgroups/xport/sdrtrunk/sdrtrunk.go b/pkg/talkgroups/xport/sdrtrunk/sdrtrunk.go index 9d58745..012274e 100644 --- a/pkg/talkgroups/xport/sdrtrunk/sdrtrunk.go +++ b/pkg/talkgroups/xport/sdrtrunk/sdrtrunk.go @@ -15,6 +15,24 @@ type Playlist struct { Aliases []Alias `xml:"alias"` Channels []Channel `xml:"channel,omitempty"` Streams []Stream `xml:"stream,omitempty"` + + streams map[string]struct{} +} + +func (p *Playlist) buildMaps() { + p.streams = make(map[string]struct{}) + + for _, s := range p.Streams { + if s.Type == "RDIOSCANNER_CALL" { + p.streams[s.Name] = struct{}{} + } + } +} + +func (p *Playlist) HasStream(name string) bool { + _, has := p.streams[name] + + return has } type Alias struct { @@ -27,20 +45,32 @@ type Alias struct { IDs []ID `xml:"id"` } -func tgToAlias(tg *talkgroups.Talkgroup) Alias { - return Alias{ +func (p *Playlist) tgToAlias(tg *talkgroups.Talkgroup) Alias { + a := Alias{ XMLName: xml.Name{Local: "alias"}, Name: common.ZeroIfNil(tg.Name), Group: common.ZeroIfNil(tg.TGGroup), List: "Stillbox", IDs: []ID{ - ID{ + { XMLName: xml.Name{Local: "id"}, Type: "talkgroup", Value: common.PtrTo(int(tg.TGID)), }, }, } + + // be nice and assign it to stream to ourselves + // TODO: make this more dynamic (exporter can have options, enumerate fields into a map[string]blah?) + // with which to specify the stillbox streamer + if p.HasStream("stillbox") { + a.IDs = append(a.IDs, ID{ + Type: "broadcastChannel", + Channel: common.PtrTo("stillbox"), + }) + } + + return a } type ID struct { @@ -91,6 +121,8 @@ type RecordConfig struct { } type Stream struct { + Type string `xml:"type,attr"` + Name string `xml:"name,attr"` Attributes []xml.Attr `xml:",any,attr"` Stream []byte `xml:",innerxml"` } @@ -113,8 +145,10 @@ func (st *Driver) ExportTalkgroups(ctx context.Context, w io.Writer, tgs []*talk pl.Aliases = nil } + pl.buildMaps() + for _, tg := range tgs { - pl.Aliases = append(pl.Aliases, tgToAlias(tg)) + pl.Aliases = append(pl.Aliases, pl.tgToAlias(tg)) } enc := xml.NewEncoder(w)