mirror of
https://github.com/m1k1o/neko.git
synced 2024-07-24 14:40:50 +12:00
move server to server directory.
This commit is contained in:
14
server/pkg/utils/array.go
Normal file
14
server/pkg/utils/array.go
Normal file
@ -0,0 +1,14 @@
|
||||
package utils
|
||||
|
||||
func ArrayIn[T comparable](val T, array []T) (exists bool, index int) {
|
||||
exists, index = false, -1
|
||||
|
||||
for i, a := range array {
|
||||
if a == val {
|
||||
exists, index = true, i
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
34
server/pkg/utils/color.go
Normal file
34
server/pkg/utils/color.go
Normal file
@ -0,0 +1,34 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
const (
|
||||
char = "&"
|
||||
)
|
||||
|
||||
// Colors: http://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html
|
||||
var re = regexp.MustCompile(char + `(?m)([0-9]{1,2};[0-9]{1,2}|[0-9]{1,2})`)
|
||||
|
||||
func Color(str string) string {
|
||||
result := ""
|
||||
lastIndex := 0
|
||||
|
||||
for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) {
|
||||
groups := []string{}
|
||||
for i := 0; i < len(v); i += 2 {
|
||||
groups = append(groups, str[v[i]:v[i+1]])
|
||||
}
|
||||
|
||||
result += str[lastIndex:v[0]] + "\033[" + groups[1] + "m"
|
||||
lastIndex = v[1]
|
||||
}
|
||||
|
||||
return result + str[lastIndex:]
|
||||
}
|
||||
|
||||
func Colorf(format string, a ...any) string {
|
||||
return fmt.Sprintf(Color(format), a...)
|
||||
}
|
35
server/pkg/utils/deocde.go
Normal file
35
server/pkg/utils/deocde.go
Normal file
@ -0,0 +1,35 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
func Decode(input interface{}, output interface{}) error {
|
||||
return mapstructure.Decode(input, output)
|
||||
}
|
||||
|
||||
func Unmarshal(in any, raw []byte, callback func() error) error {
|
||||
if err := json.Unmarshal(raw, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
return callback()
|
||||
}
|
||||
|
||||
func JsonStringAutoDecode(m any) func(rf reflect.Kind, rt reflect.Kind, data any) (any, error) {
|
||||
return func(rf reflect.Kind, rt reflect.Kind, data any) (any, error) {
|
||||
if rf != reflect.String || rt == reflect.String {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
raw := data.(string)
|
||||
if raw != "" && (raw[0:1] == "{" || raw[0:1] == "[") {
|
||||
err := json.Unmarshal([]byte(raw), &m)
|
||||
return m, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
}
|
133
server/pkg/utils/http.go
Normal file
133
server/pkg/utils/http.go
Normal file
@ -0,0 +1,133 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func HttpJsonRequest(w http.ResponseWriter, r *http.Request, res any) error {
|
||||
err := json.NewDecoder(r.Body).Decode(res)
|
||||
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return HttpBadRequest("no data provided").WithInternalErr(err)
|
||||
}
|
||||
|
||||
return HttpBadRequest("unable to parse provided data").WithInternalErr(err)
|
||||
}
|
||||
|
||||
func HttpJsonResponse(w http.ResponseWriter, code int, res any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||
log.Err(err).Str("module", "http").Msg("sending http json response failed")
|
||||
}
|
||||
}
|
||||
|
||||
func HttpSuccess(w http.ResponseWriter, res ...any) error {
|
||||
if len(res) == 0 {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
} else {
|
||||
HttpJsonResponse(w, http.StatusOK, res[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HTTPError is an error with a message and an HTTP status code.
|
||||
type HTTPError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
|
||||
InternalErr error `json:"-"`
|
||||
InternalMsg string `json:"-"`
|
||||
}
|
||||
|
||||
func (e *HTTPError) Error() string {
|
||||
if e.InternalMsg != "" {
|
||||
return e.InternalMsg
|
||||
}
|
||||
return fmt.Sprintf("%d: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func (e *HTTPError) Cause() error {
|
||||
if e.InternalErr != nil {
|
||||
return e.InternalErr
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// WithInternalErr adds internal error information to the error
|
||||
func (e *HTTPError) WithInternalErr(err error) *HTTPError {
|
||||
e.InternalErr = err
|
||||
return e
|
||||
}
|
||||
|
||||
// WithInternalMsg adds internal message information to the error
|
||||
func (e *HTTPError) WithInternalMsg(msg string) *HTTPError {
|
||||
e.InternalMsg = msg
|
||||
return e
|
||||
}
|
||||
|
||||
// WithInternalMsg adds internal formated message information to the error
|
||||
func (e *HTTPError) WithInternalMsgf(fmtStr string, args ...any) *HTTPError {
|
||||
e.InternalMsg = fmt.Sprintf(fmtStr, args...)
|
||||
return e
|
||||
}
|
||||
|
||||
// Sends error with custom formated message
|
||||
func (e *HTTPError) Msgf(fmtSt string, args ...any) *HTTPError {
|
||||
e.Message = fmt.Sprintf(fmtSt, args...)
|
||||
return e
|
||||
}
|
||||
|
||||
// Sends error with custom message
|
||||
func (e *HTTPError) Msg(str string) *HTTPError {
|
||||
e.Message = str
|
||||
return e
|
||||
}
|
||||
|
||||
func HttpError(code int, res ...string) *HTTPError {
|
||||
err := &HTTPError{
|
||||
Code: code,
|
||||
Message: http.StatusText(code),
|
||||
}
|
||||
|
||||
if len(res) == 1 {
|
||||
err.Message = res[0]
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func HttpBadRequest(res ...string) *HTTPError {
|
||||
return HttpError(http.StatusBadRequest, res...)
|
||||
}
|
||||
|
||||
func HttpUnauthorized(res ...string) *HTTPError {
|
||||
return HttpError(http.StatusUnauthorized, res...)
|
||||
}
|
||||
|
||||
func HttpForbidden(res ...string) *HTTPError {
|
||||
return HttpError(http.StatusForbidden, res...)
|
||||
}
|
||||
|
||||
func HttpNotFound(res ...string) *HTTPError {
|
||||
return HttpError(http.StatusNotFound, res...)
|
||||
}
|
||||
|
||||
func HttpUnprocessableEntity(res ...string) *HTTPError {
|
||||
return HttpError(http.StatusUnprocessableEntity, res...)
|
||||
}
|
||||
|
||||
func HttpInternalServerError(res ...string) *HTTPError {
|
||||
return HttpError(http.StatusInternalServerError, res...)
|
||||
}
|
39
server/pkg/utils/image.go
Normal file
39
server/pkg/utils/image.go
Normal file
@ -0,0 +1,39 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
)
|
||||
|
||||
func CreatePNGImage(img *image.RGBA) ([]byte, error) {
|
||||
out := new(bytes.Buffer)
|
||||
err := png.Encode(out, img)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
func CreateJPGImage(img *image.RGBA, quality int) ([]byte, error) {
|
||||
out := new(bytes.Buffer)
|
||||
err := jpeg.Encode(out, img, &jpeg.Options{Quality: quality})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
func CreatePNGImageURI(img *image.RGBA) (string, error) {
|
||||
data, err := CreatePNGImage(img)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
uri := "data:image/png;base64," + base64.StdEncoding.EncodeToString(data)
|
||||
return uri, nil
|
||||
}
|
22
server/pkg/utils/request.go
Normal file
22
server/pkg/utils/request.go
Normal file
@ -0,0 +1,22 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func HttpRequestGET(url string) (string, error) {
|
||||
rsp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer rsp.Body.Close()
|
||||
|
||||
buf, err := io.ReadAll(rsp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(bytes.TrimSpace(buf)), nil
|
||||
}
|
153
server/pkg/utils/trenddetector.go
Normal file
153
server/pkg/utils/trenddetector.go
Normal file
@ -0,0 +1,153 @@
|
||||
// From https://github.com/livekit/livekit/blob/master/pkg/sfu/streamallocator/trenddetector.go
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type TrendDirection int
|
||||
|
||||
const (
|
||||
TrendDirectionNeutral TrendDirection = iota
|
||||
TrendDirectionUpward
|
||||
TrendDirectionDownward
|
||||
)
|
||||
|
||||
func (t TrendDirection) String() string {
|
||||
switch t {
|
||||
case TrendDirectionNeutral:
|
||||
return "NEUTRAL"
|
||||
case TrendDirectionUpward:
|
||||
return "UPWARD"
|
||||
case TrendDirectionDownward:
|
||||
return "DOWNWARD"
|
||||
default:
|
||||
return fmt.Sprintf("%d", int(t))
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type TrendDetectorParams struct {
|
||||
RequiredSamples int
|
||||
DownwardTrendThreshold float64
|
||||
CollapseValues bool
|
||||
}
|
||||
|
||||
type TrendDetector struct {
|
||||
params TrendDetectorParams
|
||||
|
||||
startTime time.Time
|
||||
numSamples int
|
||||
values []int64
|
||||
lowestValue int64
|
||||
highestValue int64
|
||||
|
||||
direction TrendDirection
|
||||
}
|
||||
|
||||
func NewTrendDetector(params TrendDetectorParams) *TrendDetector {
|
||||
return &TrendDetector{
|
||||
params: params,
|
||||
startTime: time.Now(),
|
||||
direction: TrendDirectionNeutral,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TrendDetector) Seed(value int64) {
|
||||
if len(t.values) != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
t.values = append(t.values, value)
|
||||
}
|
||||
|
||||
func (t *TrendDetector) AddValue(value int64) {
|
||||
t.numSamples++
|
||||
if t.lowestValue == 0 || value < t.lowestValue {
|
||||
t.lowestValue = value
|
||||
}
|
||||
if value > t.highestValue {
|
||||
t.highestValue = value
|
||||
}
|
||||
|
||||
// ignore duplicate values
|
||||
if t.params.CollapseValues && len(t.values) != 0 && t.values[len(t.values)-1] == value {
|
||||
return
|
||||
}
|
||||
|
||||
if len(t.values) == t.params.RequiredSamples {
|
||||
t.values = t.values[1:]
|
||||
}
|
||||
t.values = append(t.values, value)
|
||||
|
||||
t.updateDirection()
|
||||
}
|
||||
|
||||
func (t *TrendDetector) GetLowest() int64 {
|
||||
return t.lowestValue
|
||||
}
|
||||
|
||||
func (t *TrendDetector) GetHighest() int64 {
|
||||
return t.highestValue
|
||||
}
|
||||
|
||||
func (t *TrendDetector) GetValues() []int64 {
|
||||
return t.values
|
||||
}
|
||||
|
||||
func (t *TrendDetector) GetDirection() TrendDirection {
|
||||
return t.direction
|
||||
}
|
||||
|
||||
func (t *TrendDetector) ToString() string {
|
||||
now := time.Now()
|
||||
elapsed := now.Sub(t.startTime).Seconds()
|
||||
str := fmt.Sprintf("t: %+v|%+v|%.2fs", t.startTime.Format(time.UnixDate), now.Format(time.UnixDate), elapsed)
|
||||
str += fmt.Sprintf(", v: %d|%d|%d|%+v|%.2f", t.numSamples, t.lowestValue, t.highestValue, t.values, kendallsTau(t.values))
|
||||
return str
|
||||
}
|
||||
|
||||
func (t *TrendDetector) updateDirection() {
|
||||
if len(t.values) < t.params.RequiredSamples {
|
||||
t.direction = TrendDirectionNeutral
|
||||
return
|
||||
}
|
||||
|
||||
// using Kendall's Tau to find trend
|
||||
kt := kendallsTau(t.values)
|
||||
|
||||
t.direction = TrendDirectionNeutral
|
||||
switch {
|
||||
case kt > 0:
|
||||
t.direction = TrendDirectionUpward
|
||||
case kt < t.params.DownwardTrendThreshold:
|
||||
t.direction = TrendDirectionDownward
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
func kendallsTau(values []int64) float64 {
|
||||
concordantPairs := 0
|
||||
discordantPairs := 0
|
||||
|
||||
for i := 0; i < len(values)-1; i++ {
|
||||
for j := i + 1; j < len(values); j++ {
|
||||
if values[i] < values[j] {
|
||||
concordantPairs++
|
||||
} else if values[i] > values[j] {
|
||||
discordantPairs++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (concordantPairs + discordantPairs) == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
return (float64(concordantPairs) - float64(discordantPairs)) / (float64(concordantPairs) + float64(discordantPairs))
|
||||
}
|
98
server/pkg/utils/uid.go
Normal file
98
server/pkg/utils/uid.go
Normal file
@ -0,0 +1,98 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultAlphabet = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // len=64
|
||||
defaultSize = 21
|
||||
defaultMaskSize = 5
|
||||
)
|
||||
|
||||
// Generator function
|
||||
type Generator func([]byte) (int, error)
|
||||
|
||||
// BytesGenerator is the default bytes generator
|
||||
var BytesGenerator Generator = rand.Read
|
||||
|
||||
func initMasks(params ...int) []uint {
|
||||
var size int
|
||||
if len(params) == 0 {
|
||||
size = defaultMaskSize
|
||||
} else {
|
||||
size = params[0]
|
||||
}
|
||||
masks := make([]uint, size)
|
||||
for i := 0; i < size; i++ {
|
||||
shift := 3 + i
|
||||
masks[i] = (2 << uint(shift)) - 1
|
||||
}
|
||||
return masks
|
||||
}
|
||||
|
||||
func getMask(alphabet string, masks []uint) int {
|
||||
for i := 0; i < len(masks); i++ {
|
||||
curr := int(masks[i])
|
||||
if curr >= len(alphabet)-1 {
|
||||
return curr
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GenerateUID is a low-level function to change alphabet and ID size.
|
||||
func GenerateUID(alphabet string, size int) (string, error) {
|
||||
if len(alphabet) == 0 || len(alphabet) > 255 {
|
||||
return "", fmt.Errorf("alphabet must not empty and contain no more than 255 chars. Current len is %d", len(alphabet))
|
||||
}
|
||||
if size <= 0 {
|
||||
return "", fmt.Errorf("size must be positive integer")
|
||||
}
|
||||
|
||||
masks := initMasks(size)
|
||||
mask := getMask(alphabet, masks)
|
||||
ceilArg := 1.6 * float64(mask*size) / float64(len(alphabet))
|
||||
step := int(math.Ceil(ceilArg))
|
||||
|
||||
id := make([]byte, size)
|
||||
bytes := make([]byte, step)
|
||||
for j := 0; ; {
|
||||
_, err := BytesGenerator(bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i := 0; i < step; i++ {
|
||||
currByte := bytes[i] & byte(mask)
|
||||
if currByte < byte(len(alphabet)) {
|
||||
id[j] = alphabet[currByte]
|
||||
j++
|
||||
if j == size {
|
||||
return string(id[:size]), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewUID generates secure URL-friendly unique ID.
|
||||
func NewUID(param ...int) (string, error) {
|
||||
var size int
|
||||
if len(param) == 0 {
|
||||
size = defaultSize
|
||||
} else {
|
||||
size = param[0]
|
||||
}
|
||||
bytes := make([]byte, size)
|
||||
_, err := BytesGenerator(bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
id := make([]byte, size)
|
||||
for i := 0; i < size; i++ {
|
||||
id[i] = defaultAlphabet[bytes[i]&63]
|
||||
}
|
||||
return string(id[:size]), nil
|
||||
}
|
114
server/pkg/utils/zip.go
Normal file
114
server/pkg/utils/zip.go
Normal file
@ -0,0 +1,114 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Zip(source, zipPath string) error {
|
||||
archiveFile, err := os.Create(zipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer archiveFile.Close()
|
||||
|
||||
archive := zip.NewWriter(archiveFile)
|
||||
defer archive.Close()
|
||||
|
||||
return filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() && !info.Mode().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header.Name = strings.TrimPrefix(path, source)
|
||||
|
||||
if info.IsDir() {
|
||||
header.Name += "/"
|
||||
} else {
|
||||
header.Method = zip.Deflate
|
||||
}
|
||||
|
||||
writer, err := archive.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(writer, file)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func Unzip(zipPath, target string) error {
|
||||
reader, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(target, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range reader.File {
|
||||
path := filepath.Join(target, file.Name)
|
||||
if file.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(path, file.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
fileReader, err := file.Open()
|
||||
if err != nil {
|
||||
if fileReader != nil {
|
||||
fileReader.Close()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
targetFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||
if err != nil {
|
||||
fileReader.Close()
|
||||
|
||||
if targetFile != nil {
|
||||
targetFile.Close()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(targetFile, fileReader); err != nil {
|
||||
fileReader.Close()
|
||||
targetFile.Close()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
fileReader.Close()
|
||||
targetFile.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user