mirror of
https://github.com/kerberos-io/agent.git
synced 2026-10-21 17:58:24 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64e6901574 |
@@ -99,6 +99,9 @@ back to the legacy filename format and configured Agent key. A structured
|
||||
checkpoint takes precedence over both the filename and current configured key;
|
||||
an invalid or unreadable checkpoint is reported as an upload error, not silently
|
||||
replaced with filename-derived values.
|
||||
For older structured checkpoints, missing camera name, region, and change count
|
||||
are recovered from a validated legacy filename. Populated checkpoint fields
|
||||
take precedence, and the checkpoint timestamp and device key remain authoritative.
|
||||
|
||||
The shorter filename cannot be decoded by legacy filename-only consumers.
|
||||
Integrations must preserve structured recording metadata, including the device
|
||||
|
||||
@@ -146,6 +146,13 @@ func s3RecordingUploadDetails(config models.Config, fileName, publicKey string)
|
||||
deviceKey = metadata.DeviceKey
|
||||
startRecording = metadata.Timestamp
|
||||
userMetadata["event-instancename"] = config.Name
|
||||
if metadata.DeviceName == "" || metadata.RegionCoordinates == "" || metadata.NumberOfChanges == "" {
|
||||
if description, ok := s3LegacyRecordingDescription(fileName); ok {
|
||||
for key, value := range description {
|
||||
userMetadata[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
if metadata.DeviceName != "" {
|
||||
userMetadata["event-instancename"] = metadata.DeviceName
|
||||
}
|
||||
@@ -203,3 +210,47 @@ func s3RecordingUploadDetails(config models.Config, fileName, publicKey string)
|
||||
userMetadata["productid"] = deviceKey
|
||||
return s3RecordingObjectName(config.S3.Username, deviceKey, startRecording), userMetadata, nil
|
||||
}
|
||||
|
||||
func s3LegacyRecordingDescription(fileName string) (map[string]string, bool) {
|
||||
base := filepath.Base(fileName)
|
||||
if filepath.Ext(base) != ".mp4" {
|
||||
return nil, false
|
||||
}
|
||||
parts := strings.Split(strings.TrimSuffix(base, ".mp4"), "_")
|
||||
if len(parts) != 6 || parts[2] == "" {
|
||||
return nil, false
|
||||
}
|
||||
seconds, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil || seconds <= 0 || seconds > math.MaxInt64/1000 {
|
||||
return nil, false
|
||||
}
|
||||
digits, fraction, found := strings.Cut(parts[1], "-")
|
||||
precision, err := strconv.Atoi(digits)
|
||||
if !found || err != nil || precision < 1 || precision > 6 || len(fraction) != precision {
|
||||
return nil, false
|
||||
}
|
||||
if _, err := strconv.ParseUint(fraction, 10, 64); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
coordinates := strings.Split(parts[3], "-")
|
||||
if len(coordinates) != 4 {
|
||||
return nil, false
|
||||
}
|
||||
for _, coordinate := range coordinates {
|
||||
if _, err := strconv.ParseUint(coordinate, 10, 64); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
changes, err := strconv.ParseInt(parts[4], 10, 64)
|
||||
if err != nil || changes < -1 {
|
||||
return nil, false
|
||||
}
|
||||
if _, err := strconv.ParseUint(parts[5], 10, 64); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return map[string]string{
|
||||
"event-instancename": parts[2],
|
||||
"event-regioncoordinates": parts[3],
|
||||
"event-numberofchanges": parts[4],
|
||||
}, true
|
||||
}
|
||||
|
||||
@@ -70,6 +70,109 @@ func TestS3RecordingUploadDetailsUsesCheckpoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3RecordingUploadDetailsBackfillsLegacyCheckpointDescriptions(t *testing.T) {
|
||||
const legacyName = "1564859471_6-474162_recorded-camera_1-2-3-4_1153_27.mp4"
|
||||
tests := []struct {
|
||||
name string
|
||||
fileName string
|
||||
camera string
|
||||
region string
|
||||
changes string
|
||||
wantCamera string
|
||||
wantRegion string
|
||||
wantChanges string
|
||||
}{
|
||||
{"pre-upgrade checkpoint", legacyName, "", "", "", "recorded-camera", "1-2-3-4", "1153"},
|
||||
{"checkpoint camera", legacyName, "snapshot-camera", "", "", "snapshot-camera", "1-2-3-4", "1153"},
|
||||
{"checkpoint region", legacyName, "", "5-6-7-8", "", "recorded-camera", "5-6-7-8", "1153"},
|
||||
{"checkpoint zero changes", legacyName, "", "", "0", "recorded-camera", "1-2-3-4", "0"},
|
||||
{"dated reference", "2019/08/03/" + legacyName, "", "", "", "recorded-camera", "1-2-3-4", "1153"},
|
||||
{"continuous recording", "1564859471_3-474_recorded-camera_0-0-0-0_-1_27.mp4", "", "", "", "recorded-camera", "0-0-0-0", "-1"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
withRecording(t, filepath.Base(tt.fileName), []byte("recording"))
|
||||
metadata := models.RecordingUploadMetadata{
|
||||
FileName: filepath.Base(tt.fileName),
|
||||
DeviceKey: "checkpoint-key",
|
||||
Timestamp: 1767355200123,
|
||||
Duration: 20452,
|
||||
FPS: 29.97,
|
||||
Encrypted: true,
|
||||
DeviceName: tt.camera,
|
||||
RegionCoordinates: tt.region,
|
||||
NumberOfChanges: tt.changes,
|
||||
}
|
||||
payload, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
withQueuedRecordingFPS(t, tt.fileName, string(payload))
|
||||
config := models.Config{Key: "current-key", Name: "current-camera", S3: &models.S3{Username: "user"}}
|
||||
objectName, got, err := s3RecordingUploadDetails(config, tt.fileName, "public-key")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := "user/2026/01/02/1767355200123-checkpoint-key.mp4"; objectName != want {
|
||||
t.Fatalf("object name = %q, want %q", objectName, want)
|
||||
}
|
||||
want := map[string]string{
|
||||
"publickey": "public-key",
|
||||
"uploadtime": "now",
|
||||
"event-instancename": tt.wantCamera,
|
||||
"event-regioncoordinates": tt.wantRegion,
|
||||
"event-numberofchanges": tt.wantChanges,
|
||||
"event-microseconds": "123000",
|
||||
"event-timestamp": "1767355200",
|
||||
"productid": "checkpoint-key",
|
||||
"duration": "20452",
|
||||
"fps": "29.97",
|
||||
"encrypted": "true",
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("metadata = %#v, want %#v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3RecordingUploadDetailsDoesNotBackfillInvalidLegacyNames(t *testing.T) {
|
||||
for _, fileName := range []string{
|
||||
"1767355200123-checkpoint-key.mp4",
|
||||
"invalid_6-474162_camera_0-0-0-0_1153_27.mp4",
|
||||
"1564859471_6-invalid_camera_0-0-0-0_1153_27.mp4",
|
||||
"1564859471_3-474162_camera_0-0-0-0_1153_27.mp4",
|
||||
"1564859471_6-474162__0-0-0-0_1153_27.mp4",
|
||||
"1564859471_6-474162_camera_invalid_1153_27.mp4",
|
||||
"1564859471_6-474162_camera_0-0-0-0_invalid_27.mp4",
|
||||
"1564859471_6-474162_camera_0-0-0-0_1153_invalid.mp4",
|
||||
"1564859471_6-474162_camera_0-0-0-0_1153_27_extra.mp4",
|
||||
} {
|
||||
t.Run(fileName, func(t *testing.T) {
|
||||
withRecording(t, fileName, []byte("recording"))
|
||||
metadata := models.RecordingUploadMetadata{
|
||||
FileName: fileName, DeviceKey: "checkpoint-key", Timestamp: 1767355200123, Duration: 20452,
|
||||
}
|
||||
payload, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
withQueuedRecordingFPS(t, fileName, string(payload))
|
||||
config := models.Config{Key: "current-key", Name: "current-camera", S3: &models.S3{Username: "user"}}
|
||||
objectName, got, err := s3RecordingUploadDetails(config, fileName, "public-key")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if objectName != "user/2026/01/02/1767355200123-checkpoint-key.mp4" ||
|
||||
got["event-instancename"] != "current-camera" || got["event-regioncoordinates"] != "" ||
|
||||
got["event-numberofchanges"] != "" || got["event-timestamp"] != "1767355200" ||
|
||||
got["productid"] != "checkpoint-key" {
|
||||
t.Fatalf("unexpected fallback for invalid legacy name: %q, %#v", objectName, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3RecordingUploadDetailsLegacyMarkers(t *testing.T) {
|
||||
for _, marker := range []string{"missing", "empty", "fps-only", "old-marker-name"} {
|
||||
t.Run(marker, func(t *testing.T) {
|
||||
|
||||
@@ -107,9 +107,16 @@ func (file recordingFileInfo) Name() string {
|
||||
// ReadRecordingDirectory returns MP4s with root-relative names, preserving the
|
||||
// file information used by the existing sorting and media formatting helpers.
|
||||
func ReadRecordingDirectory(directory string) ([]os.FileInfo, error) {
|
||||
return readRecordingDirectory(directory, filepath.WalkDir)
|
||||
}
|
||||
|
||||
func readRecordingDirectory(directory string, walkDir func(string, fs.WalkDirFunc) error) ([]os.FileInfo, error) {
|
||||
files := []os.FileInfo{}
|
||||
err := filepath.WalkDir(directory, func(filePath string, entry fs.DirEntry, err error) error {
|
||||
err := walkDir(directory, func(filePath string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) && filePath != directory {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if entry.IsDir() && entry.Name() == "decrypted" && filePath != directory {
|
||||
@@ -120,6 +127,9 @@ func ReadRecordingDirectory(directory string) ([]os.FileInfo, error) {
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
name, err := filepath.Rel(directory, filePath)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -202,6 +204,80 @@ func TestReadRecordingDirectoryIncludesFlatAndDatedMedia(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRecordingDirectoryContinuesAfterConcurrentDeletion(t *testing.T) {
|
||||
for _, origin := range []string{"Info", "WalkDir"} {
|
||||
t.Run(origin, func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
for _, name := range []string{"deleted.mp4", "surviving.mp4"} {
|
||||
if err := os.WriteFile(filepath.Join(directory, name), []byte("video"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
files, err := readRecordingDirectory(directory, func(root string, visit fs.WalkDirFunc) error {
|
||||
deleted := filepath.Join(root, entries[0].Name())
|
||||
if err := os.Remove(deleted); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var err error
|
||||
if origin == "Info" {
|
||||
err = visit(deleted, entries[0], nil)
|
||||
} else {
|
||||
err = visit(deleted, nil, &fs.PathError{Op: "lstat", Path: deleted, Err: fs.ErrNotExist})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return visit(filepath.Join(root, entries[1].Name()), entries[1], nil)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(files) != 1 || files[0].Name() != "surviving.mp4" || files[0].Size() != 5 {
|
||||
t.Fatalf("expected surviving recording after concurrent deletion, got %v", files)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type recordingDirEntryInfoError struct {
|
||||
fs.DirEntry
|
||||
err error
|
||||
}
|
||||
|
||||
func (entry recordingDirEntryInfoError) Info() (fs.FileInfo, error) {
|
||||
return nil, entry.err
|
||||
}
|
||||
|
||||
func TestReadRecordingDirectoryReportsOtherScanErrors(t *testing.T) {
|
||||
for _, origin := range []string{"Info", "WalkDir"} {
|
||||
t.Run(origin, func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
filePath := filepath.Join(directory, "recording.mp4")
|
||||
if err := os.WriteFile(filePath, []byte("video"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantErr := &fs.PathError{Op: "lstat", Path: filePath, Err: fs.ErrPermission}
|
||||
files, err := readRecordingDirectory(directory, func(root string, visit fs.WalkDirFunc) error {
|
||||
if origin == "Info" {
|
||||
return visit(filePath, recordingDirEntryInfoError{DirEntry: entries[0], err: wantErr}, nil)
|
||||
}
|
||||
return visit(filePath, nil, wantErr)
|
||||
})
|
||||
if !errors.Is(err, fs.ErrPermission) || files != nil {
|
||||
t.Fatalf("scan = %v, %v; want permission error without partial results", files, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecryptDirectoryIncludesDatedRecordingsAndSkipsOutputs(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
key := []byte("0123456789abcdef0123456789abcdef")
|
||||
|
||||
Reference in New Issue
Block a user