From d0e491e6e791f92d2587e6da99d5c6d7b9bc7393 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Wed, 4 Feb 2026 22:23:12 +0100 Subject: [PATCH 1/2] fix: normalize legacy .json suffix from database filenames Old database entries store filenames as missionname.json which causes double extensions (missionname.json.json.gz) when loading or converting. Normalize filenames when reading from the database so all consumers (handlers, converter, cleanup) get clean base names. --- internal/server/handler.go | 4 ++++ internal/server/operation.go | 11 +++++++++++ internal/server/operation_test.go | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/internal/server/handler.go b/internal/server/handler.go index 7437164d..b043ca3d 100644 --- a/internal/server/handler.go +++ b/internal/server/handler.go @@ -416,6 +416,8 @@ func (h *Handler) GetCapture(c echo.Context) error { if err != nil { return err } + name = strings.TrimSuffix(name, ".gz") + name = strings.TrimSuffix(name, ".json") upath := filepath.Join(h.setting.Data, filepath.Base(name+".json.gz")) @@ -430,6 +432,8 @@ func (h *Handler) GetCaptureFile(c echo.Context) error { if err != nil { return err } + name = strings.TrimSuffix(name, ".gz") + name = strings.TrimSuffix(name, ".json") filename := filepath.Base(name + ".json.gz") diff --git a/internal/server/operation.go b/internal/server/operation.go index 73491fb8..75feea20 100644 --- a/internal/server/operation.go +++ b/internal/server/operation.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "strings" _ "github.com/mattn/go-sqlite3" ) @@ -270,6 +271,7 @@ func (*RepoOperation) scan(ctx context.Context, rows *sql.Rows) ([]Operation, er if err != nil { return nil, err } + o.Filename = normalizeFilename(o.Filename) ops = append(ops, o) } return ops, nil @@ -287,6 +289,7 @@ func (r *RepoOperation) GetByID(ctx context.Context, id string) (*Operation, err if err != nil { return nil, err } + op.Filename = normalizeFilename(op.Filename) return &op, nil } @@ -302,6 +305,7 @@ func (r *RepoOperation) GetByFilename(ctx context.Context, filename string) (*Op if err != nil { return nil, err } + op.Filename = normalizeFilename(op.Filename) return &op, nil } @@ -387,3 +391,10 @@ func (r *RepoOperation) UpdateMissionDuration(ctx context.Context, id int64, dur `UPDATE operations SET mission_duration = ? WHERE id = ?`, duration, id) return err } + +// normalizeFilename strips legacy .json and .gz suffixes from filenames +func normalizeFilename(name string) string { + name = strings.TrimSuffix(name, ".gz") + name = strings.TrimSuffix(name, ".json") + return name +} diff --git a/internal/server/operation_test.go b/internal/server/operation_test.go index 5601c401..e86c54de 100644 --- a/internal/server/operation_test.go +++ b/internal/server/operation_test.go @@ -462,7 +462,7 @@ func TestGetByFilename(t *testing.T) { result, err := repo.GetByFilename(ctx, "test_file.json") assert.NoError(t, err) assert.NotNil(t, result) - assert.Equal(t, "test_file.json", result.Filename) + assert.Equal(t, "test_file", result.Filename) assert.Equal(t, "altis", result.WorldName) } From 5be02bd1a34f3e5cc596ca7f56365bf4c967b371 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Wed, 4 Feb 2026 22:30:02 +0100 Subject: [PATCH 2/2] fix: add v5 migration to normalize legacy .json filenames in database Replace runtime normalizeFilename workaround with a proper database migration that strips .json and .json.gz suffixes from filenames at startup. This fixes old database entries that caused double extensions (missionname.json.json.gz) when loading or converting missions. --- internal/server/handler.go | 4 --- internal/server/operation.go | 26 +++++++++------ internal/server/operation_test.go | 53 +++++++++++++++++++++++++++++-- 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/internal/server/handler.go b/internal/server/handler.go index b043ca3d..7437164d 100644 --- a/internal/server/handler.go +++ b/internal/server/handler.go @@ -416,8 +416,6 @@ func (h *Handler) GetCapture(c echo.Context) error { if err != nil { return err } - name = strings.TrimSuffix(name, ".gz") - name = strings.TrimSuffix(name, ".json") upath := filepath.Join(h.setting.Data, filepath.Base(name+".json.gz")) @@ -432,8 +430,6 @@ func (h *Handler) GetCaptureFile(c echo.Context) error { if err != nil { return err } - name = strings.TrimSuffix(name, ".gz") - name = strings.TrimSuffix(name, ".json") filename := filepath.Base(name + ".json.gz") diff --git a/internal/server/operation.go b/internal/server/operation.go index 75feea20..85a8b70d 100644 --- a/internal/server/operation.go +++ b/internal/server/operation.go @@ -5,7 +5,6 @@ import ( "database/sql" "errors" "fmt" - "strings" _ "github.com/mattn/go-sqlite3" ) @@ -138,6 +137,22 @@ func (r *RepoOperation) migration() (err error) { } } + if version < 5 { + // Strip legacy .json.gz and .json suffixes from filenames + _, err = r.db.Exec(` + UPDATE operations SET filename = REPLACE(filename, '.json.gz', '') WHERE filename LIKE '%.json.gz'; + UPDATE operations SET filename = REPLACE(filename, '.json', '') WHERE filename LIKE '%.json'; + `) + if err != nil { + return fmt.Errorf("merge db to v5 failed (normalize filenames): %w", err) + } + + _, err = r.db.Exec(`INSERT INTO version (db) VALUES (5)`) + if err != nil { + return fmt.Errorf("failed to increase version 5: %w", err) + } + } + return nil } @@ -271,7 +286,6 @@ func (*RepoOperation) scan(ctx context.Context, rows *sql.Rows) ([]Operation, er if err != nil { return nil, err } - o.Filename = normalizeFilename(o.Filename) ops = append(ops, o) } return ops, nil @@ -289,7 +303,6 @@ func (r *RepoOperation) GetByID(ctx context.Context, id string) (*Operation, err if err != nil { return nil, err } - op.Filename = normalizeFilename(op.Filename) return &op, nil } @@ -305,7 +318,6 @@ func (r *RepoOperation) GetByFilename(ctx context.Context, filename string) (*Op if err != nil { return nil, err } - op.Filename = normalizeFilename(op.Filename) return &op, nil } @@ -392,9 +404,3 @@ func (r *RepoOperation) UpdateMissionDuration(ctx context.Context, id int64, dur return err } -// normalizeFilename strips legacy .json and .gz suffixes from filenames -func normalizeFilename(name string) string { - name = strings.TrimSuffix(name, ".gz") - name = strings.TrimSuffix(name, ".json") - return name -} diff --git a/internal/server/operation_test.go b/internal/server/operation_test.go index e86c54de..a9207133 100644 --- a/internal/server/operation_test.go +++ b/internal/server/operation_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMigration(t *testing.T) { @@ -35,6 +36,54 @@ func TestMigrationV3StorageFormat(t *testing.T) { assert.ErrorIs(t, err, sql.ErrNoRows) } +func TestMigrationV5NormalizeFilenames(t *testing.T) { + dir := t.TempDir() + pathDB := filepath.Join(dir, "test.db") + + // Create DB manually with legacy filenames (pre-v5) + db, err := sql.Open("sqlite3", pathDB) + require.NoError(t, err) + + _, err = db.Exec(` + CREATE TABLE version (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, db INTEGER); + CREATE TABLE operations ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + world_name TEXT NOT NULL, mission_name TEXT NOT NULL, mission_duration INTEGER NOT NULL, + filename TEXT NOT NULL, date TEXT NOT NULL, tag TEXT NOT NULL DEFAULT '', + storage_format TEXT DEFAULT 'json', conversion_status TEXT DEFAULT 'completed', + schema_version INTEGER DEFAULT 1 + ); + INSERT INTO version (db) VALUES (4); + INSERT INTO operations (world_name, mission_name, mission_duration, filename, date) + VALUES ('altis', 'M1', 3600, 'mission_one.json', '2026-01-01'); + INSERT INTO operations (world_name, mission_name, mission_duration, filename, date) + VALUES ('altis', 'M2', 3600, 'mission_two.json.gz', '2026-01-02'); + INSERT INTO operations (world_name, mission_name, mission_duration, filename, date) + VALUES ('altis', 'M3', 3600, 'mission_clean', '2026-01-03'); + `) + require.NoError(t, err) + db.Close() + + // Open via NewRepoOperation which runs migrations + repo, err := NewRepoOperation(pathDB) + require.NoError(t, err) + defer repo.db.Close() + + ctx := context.Background() + ops, err := repo.Select(ctx, Filter{Older: "2099-12-31", Newer: "2000-01-01"}) + require.NoError(t, err) + require.Len(t, ops, 3) + + // All filenames should be normalized (newest first by default) + filenames := map[string]bool{} + for _, op := range ops { + filenames[op.Filename] = true + } + assert.True(t, filenames["mission_one"]) + assert.True(t, filenames["mission_two"]) + assert.True(t, filenames["mission_clean"]) +} + func TestOperationStorageFormat(t *testing.T) { dir := t.TempDir() pathDB := filepath.Join(dir, "test.db") @@ -387,7 +436,7 @@ func TestMigrationRerun(t *testing.T) { var version int err = repo2.db.QueryRow("SELECT db FROM version ORDER BY db DESC LIMIT 1").Scan(&version) assert.NoError(t, err) - assert.Equal(t, 4, version) + assert.Equal(t, 5, version) } func TestGetTypesEmpty(t *testing.T) { @@ -462,7 +511,7 @@ func TestGetByFilename(t *testing.T) { result, err := repo.GetByFilename(ctx, "test_file.json") assert.NoError(t, err) assert.NotNil(t, result) - assert.Equal(t, "test_file", result.Filename) + assert.Equal(t, "test_file.json", result.Filename) assert.Equal(t, "altis", result.WorldName) }