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

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View 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
}

View 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
View 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
}

View 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)
}

View 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
}

View 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))
})
}
})
}

View 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 + "\"")
}

View 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)
})
}
}

View 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
}

View 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)
}
})
}
}

View 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
}

View 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
}

View 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)
})
}