package server import ( "net/http" "os" "path/filepath" "time" "github.com/danielgtaylor/huma/v2" ) // fileResponses documents an endpoint that returns a file with Range support. func fileResponses(description string, contentTypes ...string) map[string]*huma.Response { content := map[string]*huma.MediaType{} for _, contentType := range contentTypes { content[contentType] = &huma.MediaType{Schema: &huma.Schema{Type: "string", Format: "binary"}} } return map[string]*huma.Response{ "200": {Description: description, Content: content}, "206": {Description: "Part of the file, for a Range request", Content: content}, } } // streamFile opens path and returns a response that sends it with Range support. // contentType may be empty to detect it from the file extension. Audio has no write // deadline because songs can take long to send to slow clients. func streamFile(path, contentType string, audio bool) (*huma.StreamResponse, error) { f, err := os.Open(path) if err != nil { return nil, huma.Error404NotFound("Not Found") } return streamOpenFile(f, contentType, audio) } func streamOpenFile(f *os.File, contentType string, audio bool) (*huma.StreamResponse, error) { info, err := f.Stat() if err != nil || info.IsDir() { f.Close() return nil, huma.Error404NotFound("Not Found") } return &huma.StreamResponse{Body: func(ctx huma.Context) { defer f.Close() c := echoContext(ctx) if contentType != "" { c.Response().Header().Set("Content-Type", contentType) } if audio { _ = http.NewResponseController(c.Response()).SetWriteDeadline(time.Time{}) } http.ServeContent(c.Response(), c.Request(), filepath.Base(f.Name()), info.ModTime(), f) }}, nil } // streamSong streams the song at path, or returns err mapped to an API error. // The same URL returns a different song each time (random songs, next, previous), // so browsers must not cache it. Without this they cache the response because of // its Last-Modified header, and play the same song again. func streamSong(path string, err error) (*huma.StreamResponse, error) { if err != nil { return nil, apiError(err) } resp, err := streamFile(path, "audio/mpeg", true) if err != nil { return nil, err } body := resp.Body resp.Body = func(ctx huma.Context) { ctx.SetHeader("Cache-Control", "no-store") body(ctx) } return resp, nil }