mirror of
https://github.com/go-sigma/sigma.git
synced 2026-10-28 23:07:22 +08:00
Replace every `// <Identifier> ...` placeholder doc comment with a real description of the declared symbol. 276 comments in 71 files were rewritten: 149 top-level placeholders first, then a follow-up scan caught another 127 indented ones (interface methods and const blocks) that a column-anchored match had missed. Comments now open with the declared identifier and record the actual contract - behaviour, side effects, error/panic conditions, units, HTTP endpoints, lock keys and cache prefixes - instead of restating the signature. Three placeholders carried a stale identifier and were corrected to the declared symbol: ConfigurationDatabasePostgresql, GetImageRef and Initialize. No functional change: every modified line is a comment. Verification: - grep '^\s*//\s+\S+\s+\.\.\.\s*$' over all *.go returns no matches - gofmt -l over the changed files prints nothing - CGO_ENABLED=1 go build -tags "netgo,timetzdata,exclude_graphdriver_btrfs,containers_image_openpgp" ./... exits 0 - git diff HEAD -U0 filtered to non-comment changed lines is empty
307 lines
8.3 KiB
Go
307 lines
8.3 KiB
Go
// Copyright 2024 sigma
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package utils
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/base32"
|
|
"encoding/base64"
|
|
"encoding/json/v2"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/opencontainers/go-digest"
|
|
|
|
"github.com/go-sigma/sigma/pkg/api"
|
|
"github.com/go-sigma/sigma/pkg/consts"
|
|
"github.com/go-sigma/sigma/pkg/dal/models"
|
|
"github.com/go-sigma/sigma/pkg/server/errcode"
|
|
"github.com/go-sigma/sigma/pkg/utils/ptr"
|
|
)
|
|
|
|
// GetFromCtx reads key from the gin context and asserts it to T; it returns the
|
|
// zero value of T and false when the key is missing or holds a different type.
|
|
func GetFromCtx[T any](c *gin.Context, key string) (T, bool) {
|
|
val, exists := c.Get(key)
|
|
if !exists {
|
|
var zero T
|
|
return zero, false
|
|
}
|
|
v, ok := val.(T)
|
|
return v, ok
|
|
}
|
|
|
|
// GetContentLength returns the content length of the request.
|
|
func GetContentLength(req *http.Request) (int64, error) {
|
|
if req == nil {
|
|
return 0, fmt.Errorf("request is nil")
|
|
}
|
|
str := req.Header.Get(consts.HeaderContentLength)
|
|
if str == "" {
|
|
return 0, nil
|
|
}
|
|
length, err := strconv.ParseInt(str, 10, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("content length is not a number")
|
|
}
|
|
return length, nil
|
|
}
|
|
|
|
// Scheme returns the request URL scheme, honoring the X-Forwarded-Proto header.
|
|
func Scheme(c *gin.Context) string {
|
|
if proto := c.GetHeader("X-Forwarded-Proto"); proto != "" {
|
|
return proto
|
|
}
|
|
if c.Request.TLS != nil {
|
|
return "https"
|
|
}
|
|
return "http"
|
|
}
|
|
|
|
// GenPathByDigest generates the path by the digest.
|
|
func GenPathByDigest(digest digest.Digest) string {
|
|
hex := digest.Hex()
|
|
return fmt.Sprintf("%s/%s/%s", hex[0:2], hex[2:4], hex[4:])
|
|
}
|
|
|
|
// GenBlobPathByDigest generates the blob storage path by the digest.
|
|
func GenBlobPathByDigest(digest digest.Digest) string {
|
|
return path.Join(consts.Blobs, GenPathByDigest(digest))
|
|
}
|
|
|
|
// GenManifestPathByDigest generates the manifest storage path by the digest.
|
|
func GenManifestPathByDigest(digest digest.Digest) string {
|
|
return path.Join(consts.Manifests, GenPathByDigest(digest))
|
|
}
|
|
|
|
// BindValidate binds and validates the request body. gin's ShouldBind
|
|
// already invokes the registered validator, so this is a thin wrapper.
|
|
func BindValidate(c *gin.Context, data any) error {
|
|
return c.ShouldBind(data)
|
|
}
|
|
|
|
// PanicIf panics if err is not nil
|
|
func PanicIf(err error) {
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// NormalizePagination normalizes the pagination
|
|
func NormalizePagination(in api.Pagination) api.Pagination {
|
|
if in.Page == nil || ptr.To(in.Page) < 1 {
|
|
in.Page = new(int(1))
|
|
}
|
|
if in.Limit == nil || ptr.To(in.Limit) > 100 || ptr.To(in.Limit) <= 0 {
|
|
in.Limit = new(int(10))
|
|
}
|
|
return in
|
|
}
|
|
|
|
// TrimHTTP strips a leading "http://" or "https://" scheme from in, or, when no
|
|
// scheme is present, a trailing "/".
|
|
func TrimHTTP(in string) string {
|
|
if after, ok := strings.CutPrefix(in, "http://"); ok {
|
|
return after
|
|
} else if after, ok := strings.CutPrefix(in, "https://"); ok {
|
|
return after
|
|
}
|
|
return strings.TrimSuffix(in, "/")
|
|
}
|
|
|
|
// IsDir returns true if given path is a directory,
|
|
// or returns false when it's a file or does not exist.
|
|
func IsDir(dir string) bool {
|
|
f, e := os.Stat(dir)
|
|
if e != nil {
|
|
return false
|
|
}
|
|
return f.IsDir()
|
|
}
|
|
|
|
// IsFile returns true if given path is a file,
|
|
// or returns false when it's a directory or does not exist.
|
|
func IsFile(filePath string) bool {
|
|
f, e := os.Stat(filePath)
|
|
if e != nil {
|
|
return false
|
|
}
|
|
return !f.IsDir()
|
|
}
|
|
|
|
// IsExist checks whether a file or directory exists.
|
|
// It returns false when the file or directory does not exist.
|
|
func IsExist(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil || os.IsExist(err)
|
|
}
|
|
|
|
// MustMarshal marshals the given object to bytes.
|
|
func MustMarshal(in any) []byte {
|
|
result, err := json.Marshal(in)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Must get the result of return val
|
|
func Must[T any](res T, err error) T {
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return res
|
|
}
|
|
|
|
// DirWithSlash returns the dir with slash
|
|
func DirWithSlash(id string) string {
|
|
// judge if the string has two slashes
|
|
if len(strings.Split(id, "/")) == 3 {
|
|
return id
|
|
}
|
|
if len(id) > 2 {
|
|
if !strings.Contains(id, "/") {
|
|
return DirWithSlash(fmt.Sprintf("%s/%s", id[0:2], id[2:]))
|
|
}
|
|
// remove the str before the last slash
|
|
str := id[strings.LastIndex(id, "/")+1:]
|
|
if len(str) > 2 {
|
|
str = fmt.Sprintf("%s/%s", str[0:2], str[2:])
|
|
} else {
|
|
return fmt.Sprintf("%s/%s", strings.TrimSuffix(id[0:strings.LastIndex(id, "/")+1], "/"), str)
|
|
}
|
|
return DirWithSlash(fmt.Sprintf("%s/%s", strings.TrimSuffix(id[0:strings.LastIndex(id, "/")+1], "/"), str))
|
|
}
|
|
return id
|
|
}
|
|
|
|
type stringsJoin interface {
|
|
String() string
|
|
}
|
|
|
|
// StringsJoin joins the String() form of each element of strs with sep, returning
|
|
// an empty string for an empty slice.
|
|
func StringsJoin[T stringsJoin](strs []T, sep string) string {
|
|
if len(strs) == 0 {
|
|
return ""
|
|
}
|
|
if len(strs) == 1 {
|
|
return strs[0].String()
|
|
}
|
|
b := make([]string, len(strs))
|
|
for i, str := range strs {
|
|
b[i] = str.String()
|
|
}
|
|
return strings.Join(b, sep)
|
|
}
|
|
|
|
// UnwrapJoinedErrors flattens an errors.Join-style error into a single ": "-joined
|
|
// string, falling back to err.Error() when it does not implement Unwrap() []error.
|
|
func UnwrapJoinedErrors(err error) string {
|
|
e, ok := err.(interface{ Unwrap() []error })
|
|
if !ok {
|
|
return err.Error()
|
|
}
|
|
es := e.Unwrap()
|
|
var ss = make([]string, len(es))
|
|
for index, e := range es {
|
|
ss[index] = e.Error()
|
|
}
|
|
return strings.Join(ss, ": ")
|
|
}
|
|
|
|
// UserCtxErrorFormat is the response format used when the user is missing.
|
|
type UserCtxErrorFormat int
|
|
|
|
const (
|
|
// UserCtxErrorHTTP writes the regular API error response.
|
|
UserCtxErrorHTTP UserCtxErrorFormat = iota
|
|
// UserCtxErrorDistribution writes the OCI distribution-spec error response.
|
|
UserCtxErrorDistribution
|
|
)
|
|
|
|
// GetUserFromCtx returns the authenticated user from gin context.
|
|
// If the user is missing, it writes an unauthorized response and returns true.
|
|
func GetUserFromCtx(c *gin.Context, format UserCtxErrorFormat) (*models.User, bool) {
|
|
user, ok := GetFromCtx[*models.User](c, consts.ContextUser)
|
|
if !ok {
|
|
slog.Error("get user from context failed")
|
|
if format == UserCtxErrorDistribution {
|
|
errcode.NewDSError(c, errcode.DSErrCodeUnauthorized)
|
|
} else {
|
|
errcode.NewHTTPError(c, errcode.HTTPErrCodeUnauthorized)
|
|
}
|
|
return nil, true
|
|
}
|
|
return user, false
|
|
}
|
|
|
|
// OnceWithErr runs fn at most once via once and returns its error; a panic inside
|
|
// fn (or a second call racing the first) yields an error instead of propagating it.
|
|
func OnceWithErr(once *sync.Once, fn func() error) error {
|
|
var errChan = make(chan error, 1)
|
|
defer close(errChan)
|
|
once.Do(func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
errChan <- fmt.Errorf("%v", r)
|
|
}
|
|
}()
|
|
err := fn()
|
|
errChan <- err
|
|
})
|
|
return <-errChan
|
|
}
|
|
|
|
// GenRsaPriKey generates an RSA private key of 1024, 2048 or 4096 bits and returns
|
|
// it as a base64-encoded PKCS#1 PEM block; other lengths are rejected.
|
|
func GenRsaPriKey(length int) (string, error) {
|
|
if length != 1024 && length != 2048 && length != 4096 {
|
|
return "", fmt.Errorf("rsa length is not allow")
|
|
}
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, length)
|
|
if err != nil {
|
|
return "", fmt.Errorf("generate rsa private key failed")
|
|
}
|
|
|
|
privateKeyPEM := &pem.Block{
|
|
Type: "RSA PRIVATE KEY",
|
|
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
|
}
|
|
|
|
return base64.StdEncoding.EncodeToString(pem.EncodeToMemory(privateKeyPEM)), nil
|
|
}
|
|
|
|
// GenSecureID returns a lowercase base32 (hex alphabet) encoding of n
|
|
// cryptographically random bytes; it panics if the system entropy source fails.
|
|
func GenSecureID(n int) string {
|
|
b := make([]byte, n)
|
|
_, err := rand.Read(b)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("gen secure rand id failed: %v", err))
|
|
}
|
|
return strings.ToLower(base32.HexEncoding.EncodeToString(b))
|
|
}
|