Skip to content

Commit 1db76ae

Browse files
committed
Add streamlined methods for download/upload of WxCC audio files
1 parent d8a4545 commit 1db76ae

1 file changed

Lines changed: 249 additions & 0 deletions

File tree

cmd/cc/custom_audio_files.go

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
package cc
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"mime/multipart"
9+
"net/http"
10+
"net/textproto"
11+
"os"
12+
"path/filepath"
13+
"strings"
14+
15+
"github.com/Cloverhound/webex-cli/internal/client"
16+
"github.com/Cloverhound/webex-cli/internal/config"
17+
"github.com/Cloverhound/webex-cli/internal/output"
18+
"github.com/spf13/cobra"
19+
)
20+
21+
func init() {
22+
registerAudioFileDownload()
23+
registerAudioFileUpload()
24+
}
25+
26+
func registerAudioFileDownload() {
27+
var orgid string
28+
var id string
29+
var outputPath string
30+
31+
cmd := &cobra.Command{
32+
Use: "download",
33+
Short: "Download an audio file to disk",
34+
Long: `Download a Contact Center audio file binary to a local file.
35+
36+
Fetches the audio file metadata (with download URL), then downloads the
37+
binary content and writes it to the specified output path.
38+
39+
Examples:
40+
webex cc audio-files download --id <audio-file-id> --output prompt.wav
41+
webex cc audio-files download --id <audio-file-id> --output /tmp/greeting.wav --debug`,
42+
RunE: func(c *cobra.Command, args []string) error {
43+
// Step 1: GET metadata with includeUrl=true
44+
req := client.NewRequest(config.CcBaseURL, "GET", "/organization/{orgid}/audio-file/{id}")
45+
req.PathParam("orgid", orgid)
46+
req.PathParam("id", id)
47+
req.QueryParam("includeUrl", "true")
48+
49+
resp, statusCode, err := req.Do()
50+
if err != nil {
51+
return err
52+
}
53+
54+
// Step 2: Extract download URL from response
55+
var meta map[string]any
56+
if err := json.Unmarshal(resp, &meta); err != nil {
57+
return fmt.Errorf("parsing audio file metadata: %w", err)
58+
}
59+
60+
dlURL, ok := meta["url"].(string)
61+
if !ok || dlURL == "" {
62+
return fmt.Errorf("no download URL in response (status %d)", statusCode)
63+
}
64+
65+
if config.Debug() {
66+
fmt.Fprintf(os.Stderr, "DEBUG: Downloading from %s\n", dlURL)
67+
}
68+
69+
// Step 3: Download the binary (pre-signed S3 URL, no auth header needed)
70+
dlReq, err := http.NewRequest("GET", dlURL, nil)
71+
if err != nil {
72+
return fmt.Errorf("building download request: %w", err)
73+
}
74+
75+
dlResp, err := http.DefaultClient.Do(dlReq)
76+
if err != nil {
77+
return fmt.Errorf("downloading audio file: %w", err)
78+
}
79+
defer dlResp.Body.Close()
80+
81+
if dlResp.StatusCode >= 400 {
82+
body, _ := io.ReadAll(dlResp.Body)
83+
return fmt.Errorf("download failed with status %d: %s", dlResp.StatusCode, string(body))
84+
}
85+
86+
// Step 4: Write to output file
87+
f, err := os.Create(outputPath)
88+
if err != nil {
89+
return fmt.Errorf("creating output file: %w", err)
90+
}
91+
defer f.Close()
92+
93+
n, err := io.Copy(f, dlResp.Body)
94+
if err != nil {
95+
return fmt.Errorf("writing audio file: %w", err)
96+
}
97+
98+
if config.Debug() {
99+
fmt.Fprintf(os.Stderr, "DEBUG: Wrote %d bytes to %s\n", n, outputPath)
100+
}
101+
102+
// Step 5: Print metadata to stdout
103+
return output.Print(resp, statusCode)
104+
},
105+
}
106+
107+
cmd.Flags().StringVar(&orgid, "orgid", "", "Organization ID")
108+
cmd.MarkFlagRequired("orgid")
109+
cmd.Flags().StringVar(&id, "id", "", "Audio file resource ID")
110+
cmd.MarkFlagRequired("id")
111+
cmd.Flags().StringVar(&outputPath, "output", "", "File path to write audio to")
112+
cmd.MarkFlagRequired("output")
113+
114+
audioFilesCmd.AddCommand(cmd)
115+
}
116+
117+
func registerAudioFileUpload() {
118+
var orgid string
119+
var filePath string
120+
var name string
121+
var description string
122+
123+
cmd := &cobra.Command{
124+
Use: "upload",
125+
Short: "Upload a WAV file as a new audio file",
126+
Long: `Upload an audio file (WAV) to Contact Center using multipart/form-data.
127+
128+
The upload sends two parts: audioFileInfo (JSON metadata) and audioFile (binary).
129+
If --name is omitted, the filename (with .wav extension) is used.
130+
131+
Examples:
132+
webex cc audio-files upload --file prompt.wav
133+
webex cc audio-files upload --file prompt.wav --name "Main Greeting"
134+
webex cc audio-files upload --file prompt.wav --name "Main Greeting" --description "IVR main menu"
135+
webex cc audio-files upload --file prompt.wav --dry-run
136+
webex cc audio-files upload --file prompt.wav --debug`,
137+
RunE: func(c *cobra.Command, args []string) error {
138+
// Open and validate the file
139+
f, err := os.Open(filePath)
140+
if err != nil {
141+
return fmt.Errorf("opening audio file: %w", err)
142+
}
143+
defer f.Close()
144+
145+
// Default name to filename; ensure it ends with .wav (API requires it)
146+
if name == "" {
147+
name = filepath.Base(filePath)
148+
}
149+
if !strings.HasSuffix(strings.ToLower(name), ".wav") {
150+
name += ".wav"
151+
}
152+
153+
// Build the URL
154+
url := config.CcBaseURL + "/organization/" + orgid + "/audio-file"
155+
156+
// Build multipart body
157+
var buf bytes.Buffer
158+
writer := multipart.NewWriter(&buf)
159+
160+
// Part 1: audioFileInfo JSON
161+
infoHeader := make(textproto.MIMEHeader)
162+
infoHeader.Set("Content-Disposition", `form-data; name="audioFileInfo"`)
163+
infoHeader.Set("Content-Type", "application/json")
164+
infoPart, err := writer.CreatePart(infoHeader)
165+
if err != nil {
166+
return fmt.Errorf("creating audioFileInfo part: %w", err)
167+
}
168+
169+
info := map[string]any{
170+
"name": name,
171+
"contentType": "AUDIO_WAV",
172+
"systemDefault": false,
173+
}
174+
if description != "" {
175+
info["description"] = description
176+
}
177+
infoJSON, _ := json.Marshal(info)
178+
infoPart.Write(infoJSON)
179+
180+
// Part 2: audioFile binary
181+
fileHeader := make(textproto.MIMEHeader)
182+
fileHeader.Set("Content-Disposition", fmt.Sprintf(`form-data; name="audioFile"; filename="%s"`, filepath.Base(filePath)))
183+
fileHeader.Set("Content-Type", "audio/wav")
184+
filePart, err := writer.CreatePart(fileHeader)
185+
if err != nil {
186+
return fmt.Errorf("creating audioFile part: %w", err)
187+
}
188+
if _, err := io.Copy(filePart, f); err != nil {
189+
return fmt.Errorf("writing audio file part: %w", err)
190+
}
191+
192+
writer.Close()
193+
194+
if config.Debug() {
195+
fmt.Fprintf(os.Stderr, "DEBUG: POST %s\n", url)
196+
fmt.Fprintf(os.Stderr, "DEBUG: Content-Type: %s\n", writer.FormDataContentType())
197+
fmt.Fprintf(os.Stderr, "DEBUG: audioFileInfo: %s\n", string(infoJSON))
198+
fmt.Fprintf(os.Stderr, "DEBUG: audioFile: %s (%d bytes)\n", filepath.Base(filePath), buf.Len())
199+
}
200+
201+
// Dry-run: print what would be sent and return
202+
if config.DryRun() {
203+
fmt.Fprintf(os.Stderr, "[DRY RUN] POST %s\n", url)
204+
fmt.Fprintf(os.Stderr, "[DRY RUN] Content-Type: %s\n", writer.FormDataContentType())
205+
fmt.Fprintf(os.Stderr, "[DRY RUN] audioFileInfo: %s\n", string(infoJSON))
206+
fmt.Fprintf(os.Stderr, "[DRY RUN] audioFile: %s\n", filepath.Base(filePath))
207+
return client.ErrDryRun
208+
}
209+
210+
// Execute the request
211+
req, err := http.NewRequest("POST", url, &buf)
212+
if err != nil {
213+
return fmt.Errorf("building upload request: %w", err)
214+
}
215+
req.Header.Set("Authorization", "Bearer "+config.Token())
216+
req.Header.Set("Content-Type", writer.FormDataContentType())
217+
218+
resp, err := http.DefaultClient.Do(req)
219+
if err != nil {
220+
return fmt.Errorf("uploading audio file: %w", err)
221+
}
222+
defer resp.Body.Close()
223+
224+
body, err := io.ReadAll(resp.Body)
225+
if err != nil {
226+
return fmt.Errorf("reading upload response: %w", err)
227+
}
228+
229+
if config.Debug() {
230+
fmt.Fprintf(os.Stderr, "DEBUG: Response %d (%d bytes)\n", resp.StatusCode, len(body))
231+
}
232+
233+
if resp.StatusCode >= 400 {
234+
return fmt.Errorf("upload failed with status %d: %s", resp.StatusCode, string(body))
235+
}
236+
237+
return output.Print(body, resp.StatusCode)
238+
},
239+
}
240+
241+
cmd.Flags().StringVar(&orgid, "orgid", "", "Organization ID")
242+
cmd.MarkFlagRequired("orgid")
243+
cmd.Flags().StringVar(&filePath, "file", "", "Path to WAV file to upload")
244+
cmd.MarkFlagRequired("file")
245+
cmd.Flags().StringVar(&name, "name", "", "Audio file name (defaults to filename without extension)")
246+
cmd.Flags().StringVar(&description, "description", "", "Audio file description")
247+
248+
audioFilesCmd.AddCommand(cmd)
249+
}

0 commit comments

Comments
 (0)