Files
mediamtx/internal/api/api_recordings_test.go
T
Alessandro RosandGitHub a56c635f8e fix deadlock when changing configuration through file and API (#6077) (#6101)
When changing configuration in parallel by editing the configuration
file and calling the API, the server could get into a deadlock that
prevented any further action. This is fixed.
2026-08-17 11:14:01 +00:00

277 lines
7.6 KiB
Go

package api //nolint:revive
import (
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/test"
)
func TestRecordingsList(t *testing.T) {
dir := t.TempDir()
cnf := tempConf(t, "pathDefaults:\n"+
" recordPath: "+filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f")+"\n"+
"paths:\n"+
" mypath1:\n"+
" all_others:\n")
api := API{
Address: "localhost:9997",
ReadTimeout: conf.Duration(10 * time.Second),
WriteTimeout: conf.Duration(10 * time.Second),
AuthManager: test.NilAuthManager,
Parent: &testParent{conf: cnf},
}
err := api.Initialize()
require.NoError(t, err)
defer api.Close()
err = os.Mkdir(filepath.Join(dir, "mypath1"), 0o755)
require.NoError(t, err)
err = os.Mkdir(filepath.Join(dir, "mypath2"), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(dir, "mypath1", "2008-11-07_11-22-00-500000.mp4"), []byte(""), 0o644)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(dir, "mypath1", "2009-11-07_11-22-00-900000.mp4"), []byte(""), 0o644)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(dir, "mypath2", "2009-11-07_11-22-00-900000.mp4"), []byte(""), 0o644)
require.NoError(t, err)
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
var out any
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/recordings/list", nil, &out)
require.Equal(t, map[string]any{
"itemCount": float64(2),
"pageCount": float64(1),
"items": []any{
map[string]any{
"name": "mypath1",
"segments": []any{
map[string]any{
"start": time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
},
map[string]any{
"start": time.Date(2009, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),
},
},
},
map[string]any{
"name": "mypath2",
"segments": []any{
map[string]any{
"start": time.Date(2009, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),
},
},
},
},
}, out)
}
func TestRecordingsGet(t *testing.T) {
dir := t.TempDir()
cnf := tempConf(t, "pathDefaults:\n"+
" recordPath: "+filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f")+"\n"+
"paths:\n"+
" all_others:\n")
api := API{
Address: "localhost:9997",
ReadTimeout: conf.Duration(10 * time.Second),
WriteTimeout: conf.Duration(10 * time.Second),
AuthManager: test.NilAuthManager,
Parent: &testParent{conf: cnf},
}
err := api.Initialize()
require.NoError(t, err)
defer api.Close()
err = os.Mkdir(filepath.Join(dir, "mypath1"), 0o755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(dir, "mypath1", "2008-11-07_11-22-00-000000.mp4"), []byte(""), 0o644)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(dir, "mypath1", "2009-11-07_11-22-00-900000.mp4"), []byte(""), 0o644)
require.NoError(t, err)
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
var out any
httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/recordings/get/mypath1", nil, &out)
require.Equal(t, map[string]any{
"name": "mypath1",
"segments": []any{
map[string]any{
"start": time.Date(2008, 11, 7, 11, 22, 0, 0, time.Local).Format(time.RFC3339Nano),
},
map[string]any{
"start": time.Date(2009, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),
},
},
}, out)
}
func TestRecordingsDeleteSegment(t *testing.T) {
dir := t.TempDir()
cnf := tempConf(t, "pathDefaults:\n"+
" recordPath: "+filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f")+"\n"+
"paths:\n"+
" all_others:\n")
api := API{
Address: "localhost:9997",
ReadTimeout: conf.Duration(10 * time.Second),
WriteTimeout: conf.Duration(10 * time.Second),
AuthManager: test.NilAuthManager,
Parent: &testParent{conf: cnf},
}
err := api.Initialize()
require.NoError(t, err)
defer api.Close()
err = os.MkdirAll(filepath.Join(dir, "group", "cam1"), 0o755)
require.NoError(t, err)
segmentPath := filepath.Join(dir, "group", "cam1", "2008-11-07_11-22-00-900000.mp4")
err = os.WriteFile(segmentPath, []byte(""), 0o644)
require.NoError(t, err)
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
u, err := url.Parse("http://localhost:9997/v3/recordings/deletesegment")
require.NoError(t, err)
v := url.Values{}
v.Set("path", "group/cam1")
v.Set("start", time.Date(2008, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano))
u.RawQuery = v.Encode()
req, err := http.NewRequest(http.MethodDelete, u.String(), nil)
require.NoError(t, err)
resp, err := hc.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
_, err = os.Stat(segmentPath)
require.Error(t, err)
require.True(t, os.IsNotExist(err))
}
func TestRecordingsDeleteSegmentInvalidPath(t *testing.T) {
dir := t.TempDir()
cnf := tempConf(t, "pathDefaults:\n"+
" recordPath: "+filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f")+"\n"+
"paths:\n"+
" all_others:\n")
api := API{
Address: "localhost:9997",
ReadTimeout: conf.Duration(10 * time.Second),
WriteTimeout: conf.Duration(10 * time.Second),
AuthManager: test.NilAuthManager,
Parent: &testParent{conf: cnf},
}
err := api.Initialize()
require.NoError(t, err)
defer api.Close()
err = os.MkdirAll(filepath.Join(dir, "group", "cam1"), 0o755)
require.NoError(t, err)
segmentPath := filepath.Join(dir, "group", "cam1", "2008-11-07_11-22-00-900000.mp4")
err = os.WriteFile(segmentPath, []byte(""), 0o644)
require.NoError(t, err)
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
u, err := url.Parse("http://localhost:9997/v3/recordings/deletesegment")
require.NoError(t, err)
v := url.Values{}
v.Set("path", "group/../cam1")
v.Set("start", time.Date(2008, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano))
u.RawQuery = v.Encode()
req, err := http.NewRequest(http.MethodDelete, u.String(), nil)
require.NoError(t, err)
resp, err := hc.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
_, err = os.Stat(segmentPath)
require.NoError(t, err)
}
func TestRecordingsSegmentGetInvalidPath(t *testing.T) {
dir := t.TempDir()
cnf := tempConf(t, "pathDefaults:\n"+
" recordPath: "+filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f")+"\n"+
"paths:\n"+
" all_others:\n")
api := API{
Address: "localhost:9997",
ReadTimeout: conf.Duration(10 * time.Second),
WriteTimeout: conf.Duration(10 * time.Second),
AuthManager: test.NilAuthManager,
Parent: &testParent{conf: cnf},
}
err := api.Initialize()
require.NoError(t, err)
defer api.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
resp, err := hc.Get("http://localhost:9997/v3/recordings/get/group/../cam1")
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
}
func TestAbsolutePathInside(t *testing.T) {
base := t.TempDir()
inside, err := absolutePathInside(base, filepath.Join(base, "sub", "file.mp4"))
require.NoError(t, err)
require.Equal(t, filepath.Join(base, "sub", "file.mp4"), inside)
_, err = absolutePathInside(base, filepath.Join(base, "..", "escape.mp4"))
require.EqualError(t, err, "path escapes base directory")
_, err = absolutePathInside(base, fmt.Sprintf("%s-sibling/../file.mp4", base))
require.EqualError(t, err, "path escapes base directory")
}