snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
68
server/public/utils/array.go
Normal file
68
server/public/utils/array.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package utils
|
||||
|
||||
// Dedup returns a new slice with duplicate elements removed, preserving the order in which
|
||||
// each element first appears. A nil input returns nil, so callers that distinguish "absent"
|
||||
// from "empty" keep that distinction.
|
||||
func Dedup[T comparable](arr []T) []T {
|
||||
if arr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[T]struct{}, len(arr))
|
||||
result := make([]T, 0, len(arr))
|
||||
|
||||
for _, elem := range arr {
|
||||
if _, ok := seen[elem]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
seen[elem] = struct{}{}
|
||||
result = append(result, elem)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FindExclusives returns three arrays:
|
||||
// 1. Items exclusive to arr1
|
||||
// 2. Items exclusive to arr2
|
||||
// 3. Items common to both arr1 and arr2
|
||||
func FindExclusives[T comparable](arr1, arr2 []T) ([]T, []T, []T) {
|
||||
// Create maps to track the presence of elements in each array
|
||||
existsInArr1 := make(map[T]bool)
|
||||
existsInArr2 := make(map[T]bool)
|
||||
|
||||
// Populate the maps with the elements from both arrays
|
||||
for _, elem := range arr1 {
|
||||
existsInArr1[elem] = true
|
||||
}
|
||||
for _, elem := range arr2 {
|
||||
existsInArr2[elem] = true
|
||||
}
|
||||
|
||||
// Slices for results
|
||||
var uniqueToArr1 []T
|
||||
var uniqueToArr2 []T
|
||||
var common []T
|
||||
|
||||
// Find elements unique to arr1 and common elements
|
||||
for elem := range existsInArr1 {
|
||||
if existsInArr2[elem] {
|
||||
common = append(common, elem)
|
||||
} else {
|
||||
uniqueToArr1 = append(uniqueToArr1, elem)
|
||||
}
|
||||
}
|
||||
|
||||
// Find elements unique to arr2
|
||||
for elem := range existsInArr2 {
|
||||
if !existsInArr1[elem] {
|
||||
uniqueToArr2 = append(uniqueToArr2, elem)
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueToArr1, uniqueToArr2, common
|
||||
}
|
||||
499
server/public/utils/array_test.go
Normal file
499
server/public/utils/array_test.go
Normal file
@@ -0,0 +1,499 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDedup(t *testing.T) {
|
||||
t.Run("strings", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
expected []string
|
||||
}{
|
||||
{name: "nil stays nil", input: nil, expected: nil},
|
||||
{name: "empty stays empty", input: []string{}, expected: []string{}},
|
||||
{name: "no duplicates is unchanged", input: []string{"a", "b", "c"}, expected: []string{"a", "b", "c"}},
|
||||
{name: "adjacent duplicates", input: []string{"a", "a", "b"}, expected: []string{"a", "b"}},
|
||||
{name: "non-adjacent duplicates keep first occurrence order", input: []string{"b", "a", "b", "c", "a"}, expected: []string{"b", "a", "c"}},
|
||||
{name: "all duplicates", input: []string{"a", "a", "a"}, expected: []string{"a"}},
|
||||
{name: "empty string is a value like any other", input: []string{"", "a", ""}, expected: []string{"", "a"}},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := Dedup(tc.input)
|
||||
if !reflect.DeepEqual(result, tc.expected) {
|
||||
t.Errorf("Dedup(%v) = %v, want %v", tc.input, result, tc.expected)
|
||||
}
|
||||
if tc.input == nil && result != nil {
|
||||
t.Errorf("Dedup(nil) should stay nil, got %v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("integers", func(t *testing.T) {
|
||||
result := Dedup([]int{3, 1, 3, 2, 1})
|
||||
expected := []int{3, 1, 2}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Dedup() = %v, want %v", result, expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("structs", func(t *testing.T) {
|
||||
type key struct {
|
||||
Group string
|
||||
ID int
|
||||
}
|
||||
|
||||
result := Dedup([]key{{"a", 1}, {"a", 1}, {"a", 2}, {"b", 1}})
|
||||
expected := []key{{"a", 1}, {"a", 2}, {"b", 1}}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Dedup() = %v, want %v", result, expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("does not modify the input", func(t *testing.T) {
|
||||
input := []string{"a", "b", "a"}
|
||||
Dedup(input)
|
||||
|
||||
if !reflect.DeepEqual(input, []string{"a", "b", "a"}) {
|
||||
t.Errorf("Dedup mutated its input: %v", input)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFindExclusives(t *testing.T) {
|
||||
t.Run("integers", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arr1, arr2 []int
|
||||
expectedExclusive1 []int
|
||||
expectedExclusive2 []int
|
||||
expectedCommon []int
|
||||
}{
|
||||
// Basic test with non-overlapping elements
|
||||
{
|
||||
name: "No overlap",
|
||||
arr1: []int{1, 2, 3},
|
||||
arr2: []int{4, 5, 6},
|
||||
expectedExclusive1: []int{1, 2, 3},
|
||||
expectedExclusive2: []int{4, 5, 6},
|
||||
expectedCommon: nil,
|
||||
},
|
||||
// Fully overlapping arrays
|
||||
{
|
||||
name: "Full overlap",
|
||||
arr1: []int{1, 2, 3},
|
||||
arr2: []int{1, 2, 3},
|
||||
expectedExclusive1: nil,
|
||||
expectedExclusive2: nil,
|
||||
expectedCommon: []int{1, 2, 3},
|
||||
},
|
||||
// Partial overlap
|
||||
{
|
||||
name: "Partial overlap",
|
||||
arr1: []int{1, 2, 3, 4},
|
||||
arr2: []int{3, 4, 5, 6},
|
||||
expectedExclusive1: []int{1, 2},
|
||||
expectedExclusive2: []int{5, 6},
|
||||
expectedCommon: []int{3, 4},
|
||||
},
|
||||
// Duplicates within arrays
|
||||
{
|
||||
name: "Duplicates in arr1",
|
||||
arr1: []int{1, 2, 2, 3},
|
||||
arr2: []int{2, 4, 4},
|
||||
expectedExclusive1: []int{1, 3},
|
||||
expectedExclusive2: []int{4},
|
||||
expectedCommon: []int{2},
|
||||
},
|
||||
{
|
||||
name: "Duplicates in arr2",
|
||||
arr1: []int{1, 2, 3},
|
||||
arr2: []int{2, 2, 3, 3},
|
||||
expectedExclusive1: []int{1},
|
||||
expectedExclusive2: nil,
|
||||
expectedCommon: []int{2, 3},
|
||||
},
|
||||
// Edge cases
|
||||
{
|
||||
name: "Both arrays nil",
|
||||
arr1: nil,
|
||||
arr2: nil,
|
||||
expectedExclusive1: nil,
|
||||
expectedExclusive2: nil,
|
||||
expectedCommon: nil,
|
||||
},
|
||||
{
|
||||
name: "Both arrays empty",
|
||||
arr1: []int{},
|
||||
arr2: []int{},
|
||||
expectedExclusive1: nil,
|
||||
expectedExclusive2: nil,
|
||||
expectedCommon: nil,
|
||||
},
|
||||
{
|
||||
name: "One empty array",
|
||||
arr1: []int{1, 2, 3},
|
||||
arr2: nil,
|
||||
expectedExclusive1: []int{1, 2, 3},
|
||||
expectedExclusive2: nil,
|
||||
expectedCommon: nil,
|
||||
},
|
||||
{
|
||||
name: "One element in each array",
|
||||
arr1: []int{1},
|
||||
arr2: []int{2},
|
||||
expectedExclusive1: []int{1},
|
||||
expectedExclusive2: []int{2},
|
||||
expectedCommon: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
exclusive1, exclusive2, common := FindExclusives(tt.arr1, tt.arr2)
|
||||
|
||||
slices.Sort(exclusive1)
|
||||
slices.Sort(exclusive2)
|
||||
slices.Sort(common)
|
||||
slices.Sort(tt.expectedExclusive1)
|
||||
slices.Sort(tt.expectedExclusive2)
|
||||
slices.Sort(tt.expectedCommon)
|
||||
|
||||
if !reflect.DeepEqual(exclusive1, tt.expectedExclusive1) {
|
||||
t.Errorf("Exclusive to arr1: expected %v, got %v", tt.expectedExclusive1, exclusive1)
|
||||
}
|
||||
if !reflect.DeepEqual(exclusive2, tt.expectedExclusive2) {
|
||||
t.Errorf("Exclusive to arr2: expected %v, got %v", tt.expectedExclusive2, exclusive2)
|
||||
}
|
||||
if !reflect.DeepEqual(common, tt.expectedCommon) {
|
||||
t.Errorf("Common elements: expected %v, got %v", tt.expectedCommon, common)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("strings", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arr1, arr2 []string
|
||||
expectedExclusive1 []string
|
||||
expectedExclusive2 []string
|
||||
expectedCommon []string
|
||||
}{
|
||||
// Basic test with non-overlapping elements
|
||||
{
|
||||
name: "No overlap",
|
||||
arr1: []string{"a", "b", "c"},
|
||||
arr2: []string{"d", "e", "f"},
|
||||
expectedExclusive1: []string{"a", "b", "c"},
|
||||
expectedExclusive2: []string{"d", "e", "f"},
|
||||
expectedCommon: nil,
|
||||
},
|
||||
// Fully overlapping arrays
|
||||
{
|
||||
name: "Full overlap",
|
||||
arr1: []string{"a", "b", "c"},
|
||||
arr2: []string{"a", "b", "c"},
|
||||
expectedExclusive1: nil,
|
||||
expectedExclusive2: nil,
|
||||
expectedCommon: []string{"a", "b", "c"},
|
||||
},
|
||||
// Partial overlap
|
||||
{
|
||||
name: "Partial overlap",
|
||||
arr1: []string{"a", "b", "c", "d"},
|
||||
arr2: []string{"c", "d", "e", "f"},
|
||||
expectedExclusive1: []string{"a", "b"},
|
||||
expectedExclusive2: []string{"e", "f"},
|
||||
expectedCommon: []string{"c", "d"},
|
||||
},
|
||||
// Duplicates within arrays
|
||||
{
|
||||
name: "Duplicates in arr1",
|
||||
arr1: []string{"a", "b", "b", "c"},
|
||||
arr2: []string{"b", "d", "d"},
|
||||
expectedExclusive1: []string{"a", "c"},
|
||||
expectedExclusive2: []string{"d"},
|
||||
expectedCommon: []string{"b"},
|
||||
},
|
||||
{
|
||||
name: "Duplicates in arr2",
|
||||
arr1: []string{"a", "b", "c"},
|
||||
arr2: []string{"b", "b", "c", "c"},
|
||||
expectedExclusive1: []string{"a"},
|
||||
expectedExclusive2: nil,
|
||||
expectedCommon: []string{"b", "c"},
|
||||
},
|
||||
// Edge cases
|
||||
{
|
||||
name: "Both arrays nil",
|
||||
arr1: nil,
|
||||
arr2: nil,
|
||||
expectedExclusive1: nil,
|
||||
expectedExclusive2: nil,
|
||||
expectedCommon: nil,
|
||||
},
|
||||
{
|
||||
name: "Both arrays empty",
|
||||
arr1: []string{},
|
||||
arr2: []string{},
|
||||
expectedExclusive1: nil,
|
||||
expectedExclusive2: nil,
|
||||
expectedCommon: nil,
|
||||
},
|
||||
{
|
||||
name: "One empty array",
|
||||
arr1: []string{"a", "b", "c"},
|
||||
arr2: nil,
|
||||
expectedExclusive1: []string{"a", "b", "c"},
|
||||
expectedExclusive2: nil,
|
||||
expectedCommon: nil,
|
||||
},
|
||||
{
|
||||
name: "One element in each array",
|
||||
arr1: []string{"a"},
|
||||
arr2: []string{"b"},
|
||||
expectedExclusive1: []string{"a"},
|
||||
expectedExclusive2: []string{"b"},
|
||||
expectedCommon: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
exclusive1, exclusive2, common := FindExclusives(tt.arr1, tt.arr2)
|
||||
|
||||
slices.Sort(exclusive1)
|
||||
slices.Sort(exclusive2)
|
||||
slices.Sort(common)
|
||||
slices.Sort(tt.expectedExclusive1)
|
||||
slices.Sort(tt.expectedExclusive2)
|
||||
slices.Sort(tt.expectedCommon)
|
||||
|
||||
if !reflect.DeepEqual(exclusive1, tt.expectedExclusive1) {
|
||||
t.Errorf("Exclusive to arr1: expected %v, got %v", tt.expectedExclusive1, exclusive1)
|
||||
}
|
||||
if !reflect.DeepEqual(exclusive2, tt.expectedExclusive2) {
|
||||
t.Errorf("Exclusive to arr2: expected %v, got %v", tt.expectedExclusive2, exclusive2)
|
||||
}
|
||||
if !reflect.DeepEqual(common, tt.expectedCommon) {
|
||||
t.Errorf("Common elements: expected %v, got %v", tt.expectedCommon, common)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("dates", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arr1, arr2 []time.Time
|
||||
expectedExclusive1 []time.Time
|
||||
expectedExclusive2 []time.Time
|
||||
expectedCommon []time.Time
|
||||
}{
|
||||
// Basic test with non-overlapping elements
|
||||
{
|
||||
name: "No overlap",
|
||||
arr1: []time.Time{
|
||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
arr2: []time.Time{
|
||||
time.Date(2023, 1, 4, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 5, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 6, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedExclusive1: []time.Time{
|
||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedExclusive2: []time.Time{
|
||||
time.Date(2023, 1, 4, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 5, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 6, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedCommon: nil,
|
||||
},
|
||||
// Fully overlapping arrays
|
||||
{
|
||||
name: "Full overlap",
|
||||
arr1: []time.Time{
|
||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
arr2: []time.Time{
|
||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedExclusive1: nil,
|
||||
expectedExclusive2: nil,
|
||||
expectedCommon: []time.Time{
|
||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
// Partial overlap
|
||||
{
|
||||
name: "Partial overlap",
|
||||
arr1: []time.Time{
|
||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 4, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
arr2: []time.Time{
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 4, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 5, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 6, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedExclusive1: []time.Time{
|
||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedExclusive2: []time.Time{
|
||||
time.Date(2023, 1, 5, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 6, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedCommon: []time.Time{
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 4, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
// Duplicates within arrays
|
||||
{
|
||||
name: "Duplicates in arr1",
|
||||
arr1: []time.Time{
|
||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
arr2: []time.Time{
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 4, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 4, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedExclusive1: []time.Time{
|
||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedExclusive2: []time.Time{
|
||||
time.Date(2023, 1, 4, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedCommon: []time.Time{
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Duplicates in arr2",
|
||||
arr1: []time.Time{
|
||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
arr2: []time.Time{
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedExclusive1: []time.Time{
|
||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedExclusive2: nil,
|
||||
expectedCommon: []time.Time{
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
// Edge cases
|
||||
{
|
||||
name: "Both arrays nil",
|
||||
arr1: nil,
|
||||
arr2: nil,
|
||||
expectedExclusive1: nil,
|
||||
expectedExclusive2: nil,
|
||||
expectedCommon: nil,
|
||||
},
|
||||
{
|
||||
name: "Both arrays empty",
|
||||
arr1: []time.Time{},
|
||||
arr2: []time.Time{},
|
||||
expectedExclusive1: nil,
|
||||
expectedExclusive2: nil,
|
||||
expectedCommon: nil,
|
||||
},
|
||||
{
|
||||
name: "One empty array",
|
||||
arr1: []time.Time{
|
||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
arr2: nil,
|
||||
expectedExclusive1: []time.Time{
|
||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2023, 1, 3, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedExclusive2: nil,
|
||||
expectedCommon: nil,
|
||||
},
|
||||
{
|
||||
name: "One element in each array",
|
||||
arr1: []time.Time{
|
||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
arr2: []time.Time{
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedExclusive1: []time.Time{
|
||||
time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedExclusive2: []time.Time{
|
||||
time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
expectedCommon: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
exclusive1, exclusive2, common := FindExclusives(tt.arr1, tt.arr2)
|
||||
|
||||
slices.SortFunc(exclusive1, func(a, b time.Time) int { return a.Compare(b) })
|
||||
slices.SortFunc(exclusive2, func(a, b time.Time) int { return a.Compare(b) })
|
||||
slices.SortFunc(common, func(a, b time.Time) int { return a.Compare(b) })
|
||||
slices.SortFunc(tt.expectedExclusive1, func(a, b time.Time) int { return a.Compare(b) })
|
||||
slices.SortFunc(tt.expectedExclusive2, func(a, b time.Time) int { return a.Compare(b) })
|
||||
slices.SortFunc(tt.expectedCommon, func(a, b time.Time) int { return a.Compare(b) })
|
||||
|
||||
if !reflect.DeepEqual(exclusive1, tt.expectedExclusive1) {
|
||||
t.Errorf("Exclusive to arr1: expected %v, got %v", tt.expectedExclusive1, exclusive1)
|
||||
}
|
||||
if !reflect.DeepEqual(exclusive2, tt.expectedExclusive2) {
|
||||
t.Errorf("Exclusive to arr2: expected %v, got %v", tt.expectedExclusive2, exclusive2)
|
||||
}
|
||||
if !reflect.DeepEqual(common, tt.expectedCommon) {
|
||||
t.Errorf("Common elements: expected %v, got %v", tt.expectedCommon, common)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
124
server/public/utils/file.go
Normal file
124
server/public/utils/file.go
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// CopyFile will copy a file from src path to dst path.
|
||||
// Overwrites any existing files at dst.
|
||||
// Permissions are copied from file at src to the new file at dst.
|
||||
func CopyFile(src, dst string) (err error) {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(dst), os.ModePerm); err != nil {
|
||||
return
|
||||
}
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if e := out.Close(); e != nil {
|
||||
err = e
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = out.Sync()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stat, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = os.Chmod(dst, stat.Mode())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CopyDir will copy a directory and all contained files and directories.
|
||||
// src must exist and dst must not exist.
|
||||
// Permissions are preserved when possible. Symlinks are skipped.
|
||||
func CopyDir(src string, dst string) (err error) {
|
||||
src, err = filepath.Abs(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
dst, err = filepath.Abs(dst)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stat, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !stat.IsDir() {
|
||||
return fmt.Errorf("source must be a directory")
|
||||
}
|
||||
|
||||
_, err = os.Stat(dst)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
return fmt.Errorf("destination already exists")
|
||||
}
|
||||
|
||||
err = os.MkdirAll(dst, stat.Mode())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
items, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
srcPath := filepath.Join(src, item.Name())
|
||||
dstPath := filepath.Join(dst, item.Name())
|
||||
|
||||
if item.IsDir() {
|
||||
err = CopyDir(srcPath, dstPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
info, ierr := item.Info()
|
||||
if ierr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
err = CopyFile(srcPath, dstPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
63
server/public/utils/file_test.go
Normal file
63
server/public/utils/file_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCopyDir(t *testing.T) {
|
||||
srcDir, err := os.MkdirTemp("", "src")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(srcDir)
|
||||
|
||||
dstParentDir, err := os.MkdirTemp("", "dstparent")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dstParentDir)
|
||||
|
||||
dstDir := filepath.Join(dstParentDir, "dst")
|
||||
|
||||
tempFile := "temp.txt"
|
||||
err = os.WriteFile(filepath.Join(srcDir, tempFile), []byte("test file"), 0655)
|
||||
require.NoError(t, err)
|
||||
|
||||
childDir := "child"
|
||||
err = os.Mkdir(filepath.Join(srcDir, childDir), 0777)
|
||||
require.NoError(t, err)
|
||||
|
||||
childTempFile := "childtemp.txt"
|
||||
err = os.WriteFile(filepath.Join(srcDir, childDir, childTempFile), []byte("test file"), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = CopyDir(srcDir, dstDir)
|
||||
assert.NoError(t, err)
|
||||
|
||||
stat, err := os.Stat(filepath.Join(dstDir, tempFile))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, uint32(0655), uint32(stat.Mode()))
|
||||
assert.False(t, stat.IsDir())
|
||||
data, err := os.ReadFile(filepath.Join(dstDir, tempFile))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test file", string(data))
|
||||
|
||||
stat, err = os.Stat(filepath.Join(dstDir, childDir))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, stat.IsDir())
|
||||
|
||||
stat, err = os.Stat(filepath.Join(dstDir, childDir, childTempFile))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, uint32(0755), uint32(stat.Mode()))
|
||||
assert.False(t, stat.IsDir())
|
||||
data, err = os.ReadFile(filepath.Join(dstDir, childDir, childTempFile))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test file", string(data))
|
||||
|
||||
err = CopyDir(srcDir, dstDir)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
137
server/public/utils/fileutils.go
Normal file
137
server/public/utils/fileutils.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CommonBaseSearchPaths() []string {
|
||||
paths := []string{
|
||||
".",
|
||||
"..",
|
||||
"../..",
|
||||
"../../..",
|
||||
"../../../..",
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
func findPath(path string, baseSearchPaths []string, workingDirFirst bool, filter func(os.FileInfo) bool) string {
|
||||
if filepath.IsAbs(path) {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
searchPaths := []string{}
|
||||
if workingDirFirst {
|
||||
searchPaths = append(searchPaths, baseSearchPaths...)
|
||||
}
|
||||
|
||||
// Attempt to search relative to the location of the running binary either before
|
||||
// or after searching relative to the working directory, depending on `workingDirFirst`.
|
||||
var binaryDir string
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
if exe, err = filepath.EvalSymlinks(exe); err == nil {
|
||||
if exe, err = filepath.Abs(exe); err == nil {
|
||||
binaryDir = filepath.Dir(exe)
|
||||
}
|
||||
}
|
||||
}
|
||||
if binaryDir != "" {
|
||||
for _, baseSearchPath := range baseSearchPaths {
|
||||
searchPaths = append(
|
||||
searchPaths,
|
||||
filepath.Join(binaryDir, baseSearchPath),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if !workingDirFirst {
|
||||
searchPaths = append(searchPaths, baseSearchPaths...)
|
||||
}
|
||||
|
||||
for _, parent := range searchPaths {
|
||||
found, err := filepath.Abs(filepath.Join(parent, path))
|
||||
if err != nil {
|
||||
continue
|
||||
} else if fileInfo, err := os.Stat(found); err == nil {
|
||||
if filter != nil {
|
||||
if filter(fileInfo) {
|
||||
return found
|
||||
}
|
||||
} else {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func FindPath(path string, baseSearchPaths []string, filter func(os.FileInfo) bool) string {
|
||||
return findPath(path, baseSearchPaths, true, filter)
|
||||
}
|
||||
|
||||
// FindFile looks for the given file in nearby ancestors relative to the current working
|
||||
// directory as well as the directory of the executable.
|
||||
func FindFile(path string) string {
|
||||
return FindPath(path, CommonBaseSearchPaths(), func(fileInfo os.FileInfo) bool {
|
||||
return !fileInfo.IsDir()
|
||||
})
|
||||
}
|
||||
|
||||
// fileutils.FindDir looks for the given directory in nearby ancestors relative to the current working
|
||||
// directory as well as the directory of the executable, falling back to `./` if not found.
|
||||
func FindDir(dir string) (string, bool) {
|
||||
found := FindPath(dir, CommonBaseSearchPaths(), func(fileInfo os.FileInfo) bool {
|
||||
return fileInfo.IsDir()
|
||||
})
|
||||
if found == "" {
|
||||
return "./", false
|
||||
}
|
||||
|
||||
return found, true
|
||||
}
|
||||
|
||||
// FindDirRelBinary looks for the given directory in nearby ancestors relative to the
|
||||
// directory of the executable, then relative to the working directory, falling back to `./` if not found.
|
||||
func FindDirRelBinary(dir string) (string, bool) {
|
||||
found := findPath(dir, CommonBaseSearchPaths(), false, func(fileInfo os.FileInfo) bool {
|
||||
return fileInfo.IsDir()
|
||||
})
|
||||
if found == "" {
|
||||
return "./", false
|
||||
}
|
||||
return found, true
|
||||
}
|
||||
|
||||
// Valid characters are: alphanumeric, dash, underscore
|
||||
var safeFileNameRegex = regexp.MustCompile(`[^\w\-\_]`)
|
||||
|
||||
// SanitizeFileName takes a string and returns a safe file name without an extension.
|
||||
func SanitizeFileName(input string) string {
|
||||
// Trim leading or trailing dots or spaces
|
||||
safeName := strings.Trim(input, ". ")
|
||||
// Replace dots with nothing
|
||||
safeName = strings.ReplaceAll(safeName, ".", "")
|
||||
|
||||
// Replace all invalid characters with an underscore
|
||||
safeName = safeFileNameRegex.ReplaceAllString(safeName, "_")
|
||||
|
||||
// Limit length
|
||||
const maxLength = 100
|
||||
if len(safeName) > maxLength {
|
||||
safeName = safeName[:maxLength]
|
||||
}
|
||||
|
||||
return safeName
|
||||
}
|
||||
177
server/public/utils/fileutils_test.go
Normal file
177
server/public/utils/fileutils_test.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSanitizeFileName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "normal characters",
|
||||
input: "normal-file_name",
|
||||
expected: "normal-file_name",
|
||||
},
|
||||
{
|
||||
name: "special characters",
|
||||
input: "file*name@#$",
|
||||
expected: "file_name___",
|
||||
},
|
||||
{
|
||||
name: "spaces",
|
||||
input: "my file name",
|
||||
expected: "my_file_name",
|
||||
},
|
||||
{
|
||||
name: "leading/trailing dots and spaces",
|
||||
input: " .filename. ",
|
||||
expected: "filename",
|
||||
},
|
||||
{
|
||||
name: "very long filename",
|
||||
input: strings.Repeat("a", 150) + ".txt",
|
||||
expected: strings.Repeat("a", 100),
|
||||
},
|
||||
{
|
||||
name: "unicode characters",
|
||||
input: "résumé",
|
||||
expected: "r_sum_",
|
||||
},
|
||||
{
|
||||
name: "german umlaute",
|
||||
input: "äëïöüß",
|
||||
expected: "______",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := SanitizeFileName(tt.input)
|
||||
assert.Equal(t, tt.expected, result, tt.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindFile(t *testing.T) {
|
||||
t.Run("files from various paths", func(t *testing.T) {
|
||||
// Create the following directory structure:
|
||||
// tmpDir1/
|
||||
// file1.json
|
||||
// file2.xml
|
||||
// other.txt
|
||||
// tmpDir2/
|
||||
// other.txt/ [directory]
|
||||
// tmpDir3/
|
||||
// tmpDir4/
|
||||
// tmpDir5/
|
||||
tmpDir1, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir1)
|
||||
|
||||
tmpDir2, err := os.MkdirTemp(tmpDir1, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.Mkdir(filepath.Join(tmpDir2, "other.txt"), 0700)
|
||||
require.NoError(t, err)
|
||||
|
||||
tmpDir3, err := os.MkdirTemp(tmpDir2, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
tmpDir4, err := os.MkdirTemp(tmpDir3, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
tmpDir5, err := os.MkdirTemp(tmpDir4, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
type testCase struct {
|
||||
Description string
|
||||
Cwd *string
|
||||
FileName string
|
||||
Expected string
|
||||
}
|
||||
|
||||
testCases := []testCase{}
|
||||
|
||||
for _, fileName := range []string{"file1.json", "file2.xml", "other.txt"} {
|
||||
filePath := filepath.Join(tmpDir1, fileName)
|
||||
require.NoError(t, os.WriteFile(filePath, []byte("{}"), 0600))
|
||||
|
||||
// Relative paths end up getting symlinks fully resolved, so use this below as necessary.
|
||||
filePathResolved, err := filepath.EvalSymlinks(filePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases = append(testCases, []testCase{
|
||||
{
|
||||
fmt.Sprintf("absolute path to %s", fileName),
|
||||
nil,
|
||||
filePath,
|
||||
filePath,
|
||||
},
|
||||
{
|
||||
fmt.Sprintf("absolute path to %s from containing directory", fileName),
|
||||
&tmpDir1,
|
||||
filePath,
|
||||
filePath,
|
||||
},
|
||||
{
|
||||
fmt.Sprintf("relative path to %s from containing directory", fileName),
|
||||
&tmpDir1,
|
||||
fileName,
|
||||
filePathResolved,
|
||||
},
|
||||
{
|
||||
fmt.Sprintf("%s: subdirectory of containing directory", fileName),
|
||||
&tmpDir2,
|
||||
fileName,
|
||||
filePathResolved,
|
||||
},
|
||||
{
|
||||
fmt.Sprintf("%s: twice-nested subdirectory of containing directory", fileName),
|
||||
&tmpDir3,
|
||||
fileName,
|
||||
filePathResolved,
|
||||
},
|
||||
{
|
||||
fmt.Sprintf("%s: thrice-nested subdirectory of containing directory", fileName),
|
||||
&tmpDir4,
|
||||
fileName,
|
||||
filePathResolved,
|
||||
},
|
||||
{
|
||||
fmt.Sprintf("%s: quadruple-nested subdirectory of containing directory", fileName),
|
||||
&tmpDir5,
|
||||
fileName,
|
||||
filePathResolved,
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.Description, func(t *testing.T) {
|
||||
if testCase.Cwd != nil {
|
||||
t.Chdir(*testCase.Cwd)
|
||||
}
|
||||
|
||||
assert.Equal(t, testCase.Expected, FindFile(testCase.FileName))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
98
server/public/utils/json.go
Normal file
98
server/public/utils/json.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type HumanizedJSONError struct {
|
||||
Err error
|
||||
Line int
|
||||
Character int
|
||||
}
|
||||
|
||||
func (e *HumanizedJSONError) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
// HumanizeJSONError extracts error offsets and annotates the error with useful context
|
||||
func HumanizeJSONError(err error, data []byte) error {
|
||||
if syntaxError, ok := err.(*json.SyntaxError); ok {
|
||||
return NewHumanizedJSONError(syntaxError, data, syntaxError.Offset)
|
||||
} else if unmarshalError, ok := err.(*json.UnmarshalTypeError); ok {
|
||||
return NewHumanizedJSONError(unmarshalError, data, unmarshalError.Offset)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func NewHumanizedJSONError(err error, data []byte, offset int64) *HumanizedJSONError {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if offset < 0 || offset > int64(len(data)) {
|
||||
return &HumanizedJSONError{
|
||||
Err: errors.Wrapf(err, "invalid offset %d", offset),
|
||||
}
|
||||
}
|
||||
|
||||
lineSep := []byte{'\n'}
|
||||
|
||||
line := bytes.Count(data[:offset], lineSep) + 1
|
||||
lastLineOffset := bytes.LastIndex(data[:offset], lineSep)
|
||||
character := int(offset) - (lastLineOffset + 1) + 1
|
||||
|
||||
return &HumanizedJSONError{
|
||||
Line: line,
|
||||
Character: character,
|
||||
Err: errors.Wrapf(err, "parsing error at line %d, character %d", line, character),
|
||||
}
|
||||
}
|
||||
|
||||
func IsEmptyJSON(j json.RawMessage) bool {
|
||||
if len(j) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// remove all whitespace
|
||||
jj := make([]byte, 0, len(j))
|
||||
for _, b := range j {
|
||||
if !unicode.IsSpace(rune(b)) {
|
||||
jj = append(jj, b)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jj) == 0 || bytes.Equal(jj, []byte("{}")) || bytes.Equal(jj, []byte("\"\"")) || bytes.Equal(jj, []byte("[]")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func StringPtrToJSON(ptr *string) json.RawMessage {
|
||||
if ptr == nil || len(*ptr) == 0 {
|
||||
return []byte("{}")
|
||||
}
|
||||
s := *ptr
|
||||
|
||||
if strings.HasPrefix(s, "{") && strings.HasSuffix(s, "}") {
|
||||
return []byte(s)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") {
|
||||
return []byte(s)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(s, "\"") && strings.HasSuffix(s, "\"") {
|
||||
return []byte(s)
|
||||
}
|
||||
|
||||
// This must be a bare string which will need quotes to make a valid JSON document.
|
||||
return []byte("\"" + s + "\"")
|
||||
}
|
||||
352
server/public/utils/json_test.go
Normal file
352
server/public/utils/json_test.go
Normal file
@@ -0,0 +1,352 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package utils_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/utils"
|
||||
)
|
||||
|
||||
func TestHumanizeJsonError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type testType struct{}
|
||||
|
||||
testCases := []struct {
|
||||
Description string
|
||||
Data []byte
|
||||
Err error
|
||||
ExpectedErr string
|
||||
}{
|
||||
{
|
||||
"nil error",
|
||||
[]byte{},
|
||||
nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"non-special error",
|
||||
[]byte{},
|
||||
errors.New("test"),
|
||||
"test",
|
||||
},
|
||||
{
|
||||
"syntax error, offset 17, middle of line 3",
|
||||
[]byte("line 1\nline 2\nline 3"),
|
||||
&json.SyntaxError{
|
||||
// msg can't be set
|
||||
Offset: 17,
|
||||
},
|
||||
"parsing error at line 3, character 4: ",
|
||||
},
|
||||
{
|
||||
"unmarshal type error, offset 17, middle of line 3",
|
||||
[]byte("line 1\nline 2\nline 3"),
|
||||
&json.UnmarshalTypeError{
|
||||
Value: "bool",
|
||||
Type: reflect.TypeFor[testType](),
|
||||
Offset: 17,
|
||||
Struct: "struct",
|
||||
Field: "field",
|
||||
},
|
||||
"parsing error at line 3, character 4: json: cannot unmarshal bool into Go struct field struct.field of type utils_test.testType",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.Description, func(t *testing.T) {
|
||||
actual := utils.HumanizeJSONError(testCase.Err, testCase.Data)
|
||||
if testCase.ExpectedErr == "" {
|
||||
assert.NoError(t, actual)
|
||||
} else {
|
||||
assert.EqualError(t, actual, testCase.ExpectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHumanizedJSONError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
Description string
|
||||
Data []byte
|
||||
Offset int64
|
||||
Err error
|
||||
Expected *utils.HumanizedJSONError
|
||||
}{
|
||||
{
|
||||
"nil error",
|
||||
[]byte{},
|
||||
0,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"offset -1, before start of string",
|
||||
[]byte("line 1\nline 2\nline 3"),
|
||||
-1,
|
||||
errors.New("message"),
|
||||
&utils.HumanizedJSONError{
|
||||
Err: errors.Wrap(errors.New("message"), "invalid offset -1"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"offset 0, start of string",
|
||||
[]byte("line 1\nline 2\nline 3"),
|
||||
0,
|
||||
errors.New("message"),
|
||||
&utils.HumanizedJSONError{
|
||||
Err: errors.Wrap(errors.New("message"), "parsing error at line 1, character 1"),
|
||||
Line: 1,
|
||||
Character: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"offset 5, end of line 1",
|
||||
[]byte("line 1\nline 2\nline 3"),
|
||||
5,
|
||||
errors.New("message"),
|
||||
&utils.HumanizedJSONError{
|
||||
Err: errors.Wrap(errors.New("message"), "parsing error at line 1, character 6"),
|
||||
Line: 1,
|
||||
Character: 6,
|
||||
},
|
||||
},
|
||||
{
|
||||
"offset 6, new line at end end of line 1",
|
||||
[]byte("line 1\nline 2\nline 3"),
|
||||
6,
|
||||
errors.New("message"),
|
||||
&utils.HumanizedJSONError{
|
||||
Err: errors.Wrap(errors.New("message"), "parsing error at line 1, character 7"),
|
||||
Line: 1,
|
||||
Character: 7,
|
||||
},
|
||||
},
|
||||
{
|
||||
"offset 7, start of line 2",
|
||||
[]byte("line 1\nline 2\nline 3"),
|
||||
7,
|
||||
errors.New("message"),
|
||||
&utils.HumanizedJSONError{
|
||||
Err: errors.Wrap(errors.New("message"), "parsing error at line 2, character 1"),
|
||||
Line: 2,
|
||||
Character: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"offset 12, end of line 2",
|
||||
[]byte("line 1\nline 2\nline 3"),
|
||||
12,
|
||||
errors.New("message"),
|
||||
&utils.HumanizedJSONError{
|
||||
Err: errors.Wrap(errors.New("message"), "parsing error at line 2, character 6"),
|
||||
Line: 2,
|
||||
Character: 6,
|
||||
},
|
||||
},
|
||||
{
|
||||
"offset 13, newline at end of line 2",
|
||||
[]byte("line 1\nline 2\nline 3"),
|
||||
13,
|
||||
errors.New("message"),
|
||||
&utils.HumanizedJSONError{
|
||||
Err: errors.Wrap(errors.New("message"), "parsing error at line 2, character 7"),
|
||||
Line: 2,
|
||||
Character: 7,
|
||||
},
|
||||
},
|
||||
{
|
||||
"offset 17, middle of line 3",
|
||||
[]byte("line 1\nline 2\nline 3"),
|
||||
17,
|
||||
errors.New("message"),
|
||||
&utils.HumanizedJSONError{
|
||||
Err: errors.Wrap(errors.New("message"), "parsing error at line 3, character 4"),
|
||||
Line: 3,
|
||||
Character: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
"offset 19, end of string",
|
||||
[]byte("line 1\nline 2\nline 3"),
|
||||
19,
|
||||
errors.New("message"),
|
||||
&utils.HumanizedJSONError{
|
||||
Err: errors.Wrap(errors.New("message"), "parsing error at line 3, character 6"),
|
||||
Line: 3,
|
||||
Character: 6,
|
||||
},
|
||||
},
|
||||
{
|
||||
"offset 20, offset = length of string",
|
||||
[]byte("line 1\nline 2\nline 3"),
|
||||
20,
|
||||
errors.New("message"),
|
||||
&utils.HumanizedJSONError{
|
||||
Err: errors.Wrap(errors.New("message"), "parsing error at line 3, character 7"),
|
||||
Line: 3,
|
||||
Character: 7,
|
||||
},
|
||||
},
|
||||
{
|
||||
"offset 21, offset = length of string, after newline",
|
||||
[]byte("line 1\nline 2\nline 3\n"),
|
||||
21,
|
||||
errors.New("message"),
|
||||
&utils.HumanizedJSONError{
|
||||
Err: errors.Wrap(errors.New("message"), "parsing error at line 4, character 1"),
|
||||
Line: 4,
|
||||
Character: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"offset 21, offset > length of string",
|
||||
[]byte("line 1\nline 2\nline 3"),
|
||||
21,
|
||||
errors.New("message"),
|
||||
&utils.HumanizedJSONError{
|
||||
Err: errors.Wrap(errors.New("message"), "invalid offset 21"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.Description, func(t *testing.T) {
|
||||
actual := utils.NewHumanizedJSONError(testCase.Err, testCase.Data, testCase.Offset)
|
||||
if testCase.Expected != nil && actual.Err != nil {
|
||||
if assert.EqualValues(t, testCase.Expected.Err.Error(), actual.Err.Error()) {
|
||||
actual.Err = testCase.Expected.Err
|
||||
}
|
||||
}
|
||||
assert.Equal(t, testCase.Expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsJSONEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
Description string
|
||||
Data []byte
|
||||
Empty bool
|
||||
}{
|
||||
{
|
||||
"nil []byte is empty",
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Zero length slice is empty",
|
||||
[]byte(""),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"braces are empty",
|
||||
[]byte("{}"),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"square brackets are empty",
|
||||
[]byte("[]"),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty string is empty",
|
||||
[]byte("\"\""),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"map is not empty",
|
||||
[]byte("{\"foo\":7}"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"array is not empty",
|
||||
[]byte("[1,2,3]"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"string is not empty",
|
||||
[]byte("\"hello\""),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"whitespace still empty",
|
||||
[]byte(" \n { \t } "),
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.Description, func(t *testing.T) {
|
||||
empty := utils.IsEmptyJSON(testCase.Data)
|
||||
assert.Equal(t, testCase.Empty, empty)
|
||||
|
||||
if !testCase.Empty {
|
||||
// don't really need to test the JSON unmarshaller but this is included
|
||||
// to ensure the test cases stay valid.
|
||||
var v any
|
||||
err := json.Unmarshal(testCase.Data, &v)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringPtrToJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
Description string
|
||||
Ptr *string
|
||||
Expect json.RawMessage
|
||||
}{
|
||||
{
|
||||
"nil string ptr",
|
||||
nil,
|
||||
[]byte("{}"),
|
||||
},
|
||||
{
|
||||
"Zero length string",
|
||||
new(""),
|
||||
[]byte("{}"),
|
||||
},
|
||||
{
|
||||
"JSON map",
|
||||
new("{\"foo\":7}"),
|
||||
[]byte("{\"foo\":7}"),
|
||||
},
|
||||
{
|
||||
"JSON array",
|
||||
new("[1,2,3]"),
|
||||
[]byte("[1,2,3]"),
|
||||
},
|
||||
{
|
||||
"JSON string",
|
||||
new("\"hello\""),
|
||||
[]byte("\"hello\""),
|
||||
},
|
||||
{
|
||||
"bare string",
|
||||
new("hello"),
|
||||
[]byte("\"hello\""),
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.Description, func(t *testing.T) {
|
||||
j := utils.StringPtrToJSON(testCase.Ptr)
|
||||
assert.Equal(t, testCase.Expect, j)
|
||||
})
|
||||
}
|
||||
}
|
||||
43
server/public/utils/page.go
Normal file
43
server/public/utils/page.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package utils
|
||||
|
||||
// Pager fetches all items from a paginated API.
|
||||
// Pager is a generic function that fetches and aggregates paginated data.
|
||||
// It takes a fetch function and a perPage parameter as arguments.
|
||||
//
|
||||
// The fetch function is responsible for retrieving a slice of items of type T
|
||||
// for a given page number. It returns the fetched items and an error, if any.
|
||||
// Ideally a developer may want to use a closure to create a fetch function.
|
||||
//
|
||||
// The perPage parameter specifies the number of items to fetch per page.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// items, err := Pager(fetchFunc, 10)
|
||||
// if err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
// // process items
|
||||
func Pager[T any](fetch func(page int) ([]T, error), perPage int) ([]T, error) {
|
||||
var list []T
|
||||
var page int
|
||||
|
||||
for {
|
||||
fetched, err := fetch(page)
|
||||
if err != nil {
|
||||
return list, err
|
||||
}
|
||||
|
||||
list = append(list, fetched...)
|
||||
|
||||
if len(fetched) < perPage {
|
||||
break
|
||||
}
|
||||
|
||||
page++
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
69
server/public/utils/page_test.go
Normal file
69
server/public/utils/page_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPager(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fetch func(page int) ([]int, error)
|
||||
perPage int
|
||||
expected []int
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "successful fetch",
|
||||
fetch: func(page int) ([]int, error) {
|
||||
if page > 2 {
|
||||
return nil, nil
|
||||
}
|
||||
return []int{page*10 + 1, page*10 + 2, page*10 + 3}, nil
|
||||
},
|
||||
perPage: 3,
|
||||
expected: []int{1, 2, 3, 11, 12, 13, 21, 22, 23},
|
||||
},
|
||||
{
|
||||
name: "fetch with error",
|
||||
fetch: func(page int) ([]int, error) {
|
||||
if page == 1 {
|
||||
return nil, errors.New("fetch error")
|
||||
}
|
||||
return []int{page*10 + 1, page*10 + 2, page*10 + 3}, nil
|
||||
},
|
||||
perPage: 3,
|
||||
expected: []int{1, 2, 3},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "fetch with fewer items than perPage",
|
||||
fetch: func(page int) ([]int, error) {
|
||||
if page > 0 {
|
||||
return []int{11, 12}, nil
|
||||
}
|
||||
return []int{page*10 + 1, page*10 + 2, page*10 + 3}, nil
|
||||
},
|
||||
perPage: 3,
|
||||
expected: []int{1, 2, 3, 11, 12},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := Pager(tt.fetch, tt.perPage)
|
||||
if tt.expectErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
76
server/public/utils/sql/sql_utils.go
Normal file
76
server/public/utils/sql/sql_utils.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
dbsql "database/sql"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
DBPingTimeout = 10 * time.Second
|
||||
DBConnRetrySleep = 2 * time.Second
|
||||
|
||||
replicaLagPrefix = "replica-lag"
|
||||
)
|
||||
|
||||
// SetupConnection sets up the connection to the database and pings it to make sure it's alive.
|
||||
// It also applies any database configuration settings that are required.
|
||||
func SetupConnection(logger mlog.LoggerIFace, connType string, dataSource string, settings *model.SqlSettings, attempts int) (*dbsql.DB, error) {
|
||||
db, err := dbsql.Open(*settings.DriverName, dataSource)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to open SQL connection")
|
||||
}
|
||||
|
||||
// At this point, we have passed sql.Open, so we deliberately ignore any errors.
|
||||
sanitized, _ := model.SanitizeDataSource(*settings.DriverName, dataSource)
|
||||
|
||||
logger = logger.With(
|
||||
mlog.String("database", connType),
|
||||
mlog.String("dataSource", sanitized),
|
||||
)
|
||||
|
||||
for attempt := 1; attempt <= attempts; attempt++ {
|
||||
if attempt > 1 {
|
||||
logger.Info("Pinging SQL", mlog.Int("attempt", attempt))
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), DBPingTimeout)
|
||||
defer cancel()
|
||||
err = db.PingContext(ctx)
|
||||
if err != nil {
|
||||
if attempt == attempts {
|
||||
return nil, err
|
||||
}
|
||||
logger.Error("Failed to ping DB", mlog.Float("retrying in seconds", DBConnRetrySleep.Seconds()), mlog.Err(err))
|
||||
time.Sleep(DBConnRetrySleep)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if strings.HasPrefix(connType, replicaLagPrefix) {
|
||||
// If this is a replica lag connection, we just open one connection.
|
||||
//
|
||||
// Arguably, if the query doesn't require a special credential, it does take up
|
||||
// one extra connection from the replica DB. But falling back to the replica
|
||||
// data source when the replica lag data source is null implies an ordering constraint
|
||||
// which makes things brittle and is not a good design.
|
||||
// If connections are an overhead, it is advised to use a connection pool.
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
} else {
|
||||
db.SetMaxIdleConns(*settings.MaxIdleConns)
|
||||
db.SetMaxOpenConns(*settings.MaxOpenConns)
|
||||
}
|
||||
db.SetConnMaxLifetime(time.Duration(*settings.ConnMaxLifetimeMilliseconds) * time.Millisecond)
|
||||
db.SetConnMaxIdleTime(time.Duration(*settings.ConnMaxIdleTimeMilliseconds) * time.Millisecond)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
29
server/public/utils/timeutils/time.go
Normal file
29
server/public/utils/timeutils/time.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package timeutils
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
RFC3339Milli = "2006-01-02T15:04:05.999Z07:00"
|
||||
)
|
||||
|
||||
func FormatMillis(millis int64) string {
|
||||
return time.UnixMilli(millis).Format(RFC3339Milli)
|
||||
}
|
||||
|
||||
func ParseFormatedMillis(s string) (millis int64, err error) {
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
t, err := time.Parse(RFC3339Milli, s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return t.UnixMilli(), nil
|
||||
}
|
||||
57
server/public/utils/timeutils/time_test.go
Normal file
57
server/public/utils/timeutils/time_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package timeutils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFormatMillis(t *testing.T) {
|
||||
t.Run("zero time", func(t *testing.T) {
|
||||
result := FormatMillis(0)
|
||||
// The concrete time depends on the timezone, so we can't test the exact time
|
||||
assert.Contains(t, result, "1970-01-01")
|
||||
assert.Contains(t, result, "00:00")
|
||||
})
|
||||
|
||||
t.Run("positive time", func(t *testing.T) {
|
||||
result := FormatMillis(1609459200000) // 2021-01-01 00:00:00 UTC
|
||||
assert.Contains(t, result, "2021-01-01")
|
||||
assert.Contains(t, result, "00:00")
|
||||
})
|
||||
|
||||
t.Run("negative time", func(t *testing.T) {
|
||||
result := FormatMillis(-1609459200000) // 1919-01-01 00:00:00 UTC
|
||||
assert.Contains(t, result, "1919-01-01")
|
||||
assert.Contains(t, result, "00:00")
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseFormatedMillis(t *testing.T) {
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
result, err := ParseFormatedMillis("")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), result)
|
||||
})
|
||||
|
||||
t.Run("valid timestamp", func(t *testing.T) {
|
||||
result, err := ParseFormatedMillis("2021-01-01T00:00:00.000Z")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1609459200000), result)
|
||||
})
|
||||
|
||||
t.Run("invalid format", func(t *testing.T) {
|
||||
result, err := ParseFormatedMillis("2021-01-01")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, int64(0), result)
|
||||
})
|
||||
|
||||
t.Run("invalid date", func(t *testing.T) {
|
||||
result, err := ParseFormatedMillis("2021-13-01T00:00:00.000Z")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, int64(0), result)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user