mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 08:06:37 +08:00
refactor(backend): extract to pkg/cap
This commit is contained in:
@@ -18,9 +18,9 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/apps/admin"
|
"github.com/Rain-kl/Wavelet/internal/apps/admin"
|
||||||
"github.com/Rain-kl/Wavelet/internal/config"
|
"github.com/Rain-kl/Wavelet/internal/config"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/otel_trace"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
|
otel_trace "github.com/Rain-kl/Wavelet/pkg/trace"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/task"
|
"github.com/Rain-kl/Wavelet/internal/task"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
pkgpush "github.com/Rain-kl/Wavelet/pkg/push"
|
pkgpush "github.com/Rain-kl/Wavelet/pkg/push"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/task"
|
"github.com/Rain-kl/Wavelet/internal/task"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/storage"
|
"github.com/Rain-kl/Wavelet/internal/storage"
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
mail "github.com/Rain-kl/Wavelet/internal/util/mail"
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
|
mail "github.com/Rain-kl/Wavelet/pkg/mail"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/admin"
|
"github.com/Rain-kl/Wavelet/internal/apps/admin"
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/task"
|
"github.com/Rain-kl/Wavelet/internal/task"
|
||||||
taskhandlers "github.com/Rain-kl/Wavelet/internal/task/handlers"
|
taskhandlers "github.com/Rain-kl/Wavelet/internal/task/handlers"
|
||||||
"github.com/Rain-kl/Wavelet/internal/task/scheduler"
|
"github.com/Rain-kl/Wavelet/internal/task/scheduler"
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/buildinfo"
|
"github.com/Rain-kl/Wavelet/internal/buildinfo"
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
"golang.org/x/mod/semver"
|
"golang.org/x/mod/semver"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
const installedBinaryMode = 0o755
|
const installedBinaryMode = 0o755
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
caputil "github.com/Rain-kl/Wavelet/internal/util/cap"
|
caputil "github.com/Rain-kl/Wavelet/internal/service/cap"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ package cap
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/util/cap"
|
capService "github.com/Rain-kl/Wavelet/internal/service/cap"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,10 +38,10 @@ func Challenge(c *gin.Context) {
|
|||||||
req.Scope = "login"
|
req.Scope = "login"
|
||||||
}
|
}
|
||||||
|
|
||||||
mgr := cap.GetDefaultManager()
|
mgr := capService.GetDefaultManager()
|
||||||
resp, err := mgr.Generate(c.Request.Context(), req.Scope)
|
resp, err := mgr.Generate(c.Request.Context(), req.Scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, cap.RedeemResponse{
|
c.JSON(http.StatusInternalServerError, capService.RedeemResponse{
|
||||||
Success: false,
|
Success: false,
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
@@ -65,7 +65,7 @@ func Challenge(c *gin.Context) {
|
|||||||
func Redeem(c *gin.Context) {
|
func Redeem(c *gin.Context) {
|
||||||
var req redeemRequest
|
var req redeemRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, cap.RedeemResponse{
|
c.JSON(http.StatusBadRequest, capService.RedeemResponse{
|
||||||
Success: false,
|
Success: false,
|
||||||
Error: "无效的参数",
|
Error: "无效的参数",
|
||||||
})
|
})
|
||||||
@@ -76,10 +76,10 @@ func Redeem(c *gin.Context) {
|
|||||||
req.Scope = "login"
|
req.Scope = "login"
|
||||||
}
|
}
|
||||||
|
|
||||||
mgr := cap.GetDefaultManager()
|
mgr := capService.GetDefaultManager()
|
||||||
resp, err := mgr.Redeem(c.Request.Context(), req.Token, req.Solutions, req.Scope)
|
resp, err := mgr.Redeem(c.Request.Context(), req.Token, req.Solutions, req.Scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, cap.RedeemResponse{
|
c.JSON(http.StatusInternalServerError, capService.RedeemResponse{
|
||||||
Success: false,
|
Success: false,
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
capUtil "github.com/Rain-kl/Wavelet/internal/util/cap"
|
capUtil "github.com/Rain-kl/Wavelet/internal/service/cap"
|
||||||
|
pkgcap "github.com/Rain-kl/Wavelet/pkg/cap"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ func TestCapEndpointsAndMiddleware(t *testing.T) {
|
|||||||
t.Fatalf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
|
t.Fatalf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
var challengeResp capUtil.ChallengeResponse
|
var challengeResp pkgcap.ChallengeResponse
|
||||||
if err := json.Unmarshal(w.Body.Bytes(), &challengeResp); err != nil {
|
if err := json.Unmarshal(w.Body.Bytes(), &challengeResp); err != nil {
|
||||||
t.Fatalf("failed to unmarshal challenge response: %v", err)
|
t.Fatalf("failed to unmarshal challenge response: %v", err)
|
||||||
}
|
}
|
||||||
@@ -89,7 +90,7 @@ func TestCapEndpointsAndMiddleware(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. Solve the challenge
|
// 5. Solve the challenge
|
||||||
solutions := capUtil.Solve(challengeResp.Token, challengeResp.Challenge.C, challengeResp.Challenge.S, challengeResp.Challenge.D)
|
solutions := pkgcap.Solve(challengeResp.Token, challengeResp.Challenge.C, challengeResp.Challenge.S, challengeResp.Challenge.D)
|
||||||
|
|
||||||
// 6. Redeem solutions
|
// 6. Redeem solutions
|
||||||
redeemReqPayload := redeemRequest{
|
redeemReqPayload := redeemRequest{
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/common"
|
"github.com/Rain-kl/Wavelet/internal/common"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/otel_trace"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
|
otel_trace "github.com/Rain-kl/Wavelet/pkg/trace"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ package oauth
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/common"
|
"github.com/Rain-kl/Wavelet/internal/common"
|
||||||
"github.com/Rain-kl/Wavelet/internal/config"
|
"github.com/Rain-kl/Wavelet/internal/config"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/config"
|
"github.com/Rain-kl/Wavelet/internal/config"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var logChan chan *UserAccessLog
|
var logChan chan *UserAccessLog
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/common"
|
"github.com/Rain-kl/Wavelet/internal/common"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/diskcache"
|
"github.com/Rain-kl/Wavelet/internal/diskcache"
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/common"
|
"github.com/Rain-kl/Wavelet/internal/common"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/storage"
|
"github.com/Rain-kl/Wavelet/internal/storage"
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/storage"
|
"github.com/Rain-kl/Wavelet/internal/storage"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StorageReadOnly checks if the storage system is in read-only maintenance mode.
|
// StorageReadOnly checks if the storage system is in read-only maintenance mode.
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/task"
|
"github.com/Rain-kl/Wavelet/internal/task"
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
|
pkgu "github.com/Rain-kl/Wavelet/pkg/util"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -171,7 +172,7 @@ func handleLoginEmailVerification(ctx context.Context, c *gin.Context, req *logi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
maskedEmail := util.MaskEmail(user.Email)
|
maskedEmail := pkgu.MaskEmail(user.Email)
|
||||||
c.JSON(http.StatusOK, util.Err(errNeedEmailCodePrefix+maskedEmail))
|
c.JSON(http.StatusOK, util.Err(errNeedEmailCodePrefix+maskedEmail))
|
||||||
return errors.New("handled")
|
return errors.New("handled")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/config"
|
"github.com/Rain-kl/Wavelet/internal/config"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/task"
|
"github.com/Rain-kl/Wavelet/internal/task"
|
||||||
"github.com/Rain-kl/Wavelet/internal/util/mail"
|
"github.com/Rain-kl/Wavelet/pkg/mail"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 异步任务名称与管理类型定义
|
// 异步任务名称与管理类型定义
|
||||||
|
|||||||
@@ -8,12 +8,31 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/buildinfo"
|
"github.com/Rain-kl/Wavelet/internal/buildinfo"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/config"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db/migrator"
|
"github.com/Rain-kl/Wavelet/internal/db/migrator"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/trace"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "wavelet",
|
Use: "wavelet",
|
||||||
|
PersistentPreRun: func(_ *cobra.Command, _ []string) {
|
||||||
|
logger.Init(logger.Config{
|
||||||
|
Level: config.Config.Log.Level,
|
||||||
|
Format: config.Config.Log.Format,
|
||||||
|
Output: config.Config.Log.Output,
|
||||||
|
FilePath: config.Config.Log.FilePath,
|
||||||
|
MaxSize: config.Config.Log.MaxSize,
|
||||||
|
MaxAge: config.Config.Log.MaxAge,
|
||||||
|
MaxBackups: config.Config.Log.MaxBackups,
|
||||||
|
Compress: config.Config.Log.Compress,
|
||||||
|
})
|
||||||
|
trace.Init(trace.Config{
|
||||||
|
AppName: config.Config.App.AppName,
|
||||||
|
SamplingRate: config.Config.Otel.SamplingRate,
|
||||||
|
})
|
||||||
|
},
|
||||||
PreRun: func(_ *cobra.Command, _ []string) {
|
PreRun: func(_ *cobra.Command, _ []string) {
|
||||||
migrator.Migrate()
|
migrator.Migrate()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
gormLogger "gorm.io/gorm/logger"
|
gormLogger "gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
|
|||||||
+20
-360
@@ -1,261 +1,68 @@
|
|||||||
// Copyright 2026 Arctel.net
|
// Copyright 2026 Arctel.net
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
// Package diskcache implements a platform-level disk-backed cache with size limit, TTL, and LRU eviction.
|
// Package diskcache wraps the generic pkg/diskcache to provide database configuration integration.
|
||||||
package diskcache
|
package diskcache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"container/list"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/peterbourgon/diskv/v3"
|
pkgcache "github.com/Rain-kl/Wavelet/pkg/diskcache"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrCacheMiss represents a cache miss.
|
// Status represents the runtime cache statistics.
|
||||||
var ErrCacheMiss = errors.New("cache miss")
|
type Status = pkgcache.Status
|
||||||
|
|
||||||
// Constants for disk cache configuration and sizing
|
|
||||||
const (
|
const (
|
||||||
defaultCacheDir = "uploads/diskcache"
|
defaultCacheDir = "uploads/diskcache"
|
||||||
headerSize = 8 // 8 bytes metadata prefix for expiration UnixNano timestamp
|
|
||||||
defaultMaxSizeMB = 100
|
defaultMaxSizeMB = 100
|
||||||
defaultTTLMinutes = 60
|
defaultTTLMinutes = 60
|
||||||
defaultCleanupInterval = 10
|
defaultCleanupInterval = 10
|
||||||
|
|
||||||
// DefaultExpiration applies the cache-wide default TTL.
|
// DefaultExpiration applies the cache-wide default TTL.
|
||||||
DefaultExpiration time.Duration = 0
|
DefaultExpiration = pkgcache.DefaultExpiration
|
||||||
// NoExpiration stores the item without a TTL. Size limits and LRU eviction still apply.
|
// NoExpiration stores the item without a TTL. Size limits and LRU eviction still apply.
|
||||||
NoExpiration time.Duration = -1
|
NoExpiration = pkgcache.NoExpiration
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrCacheMiss represents a cache miss.
|
||||||
|
var ErrCacheMiss = pkgcache.ErrCacheMiss
|
||||||
|
|
||||||
|
// DiskCache is a wrapper around the generic pkg/diskcache that integrates with the DB for configs.
|
||||||
|
type DiskCache struct {
|
||||||
|
*pkgcache.DiskCache
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
globalCache *DiskCache
|
globalCache *DiskCache
|
||||||
globalCacheOnce sync.Once
|
globalCacheOnce sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
// Status represents the runtime cache statistics.
|
|
||||||
type Status struct {
|
|
||||||
TotalSize int64 `json:"total_size"`
|
|
||||||
KeysCount int `json:"keys_count"`
|
|
||||||
MaxSizeMB int64 `json:"max_size_mb"`
|
|
||||||
TTLMinutes int64 `json:"ttl_minutes"`
|
|
||||||
LRUEnabled bool `json:"lru_enabled"`
|
|
||||||
BasePath string `json:"base_path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiskCache implements the disk-backed cache with size limits, TTL, and LRU eviction.
|
|
||||||
type DiskCache struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
d *diskv.Diskv
|
|
||||||
basePath string
|
|
||||||
maxSize int64 // in bytes
|
|
||||||
defaultTTL time.Duration
|
|
||||||
lruEnabled bool
|
|
||||||
|
|
||||||
// LRU and Size tracking
|
|
||||||
currentSize int64
|
|
||||||
items map[string]*list.Element
|
|
||||||
evictList *list.List
|
|
||||||
}
|
|
||||||
|
|
||||||
type cacheItem struct {
|
|
||||||
key string
|
|
||||||
size int64
|
|
||||||
expiredAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGlobalCache returns the global singleton DiskCache instance.
|
// GetGlobalCache returns the global singleton DiskCache instance.
|
||||||
func GetGlobalCache() *DiskCache {
|
func GetGlobalCache() *DiskCache {
|
||||||
globalCacheOnce.Do(func() {
|
globalCacheOnce.Do(func() {
|
||||||
globalCache = New(defaultCacheDir)
|
pureCache := pkgcache.New(defaultCacheDir)
|
||||||
|
globalCache = &DiskCache{pureCache}
|
||||||
// Load initial configs from database
|
// Load initial configs from database
|
||||||
globalCache.ReloadConfig(context.Background())
|
globalCache.ReloadConfig(context.Background())
|
||||||
// Start background routine to clean expired items every 10 minutes
|
// Start background routine to clean expired items every 10 minutes
|
||||||
go globalCache.startCleanupWorker(defaultCleanupInterval * time.Minute)
|
go globalCache.StartCleanupWorker(defaultCleanupInterval * time.Minute)
|
||||||
})
|
})
|
||||||
return globalCache
|
return globalCache
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new DiskCache instance.
|
// New creates a new DiskCache wrapper.
|
||||||
func New(basePath string) *DiskCache {
|
func New(basePath string) *DiskCache {
|
||||||
d := diskv.New(diskv.Options{
|
return &DiskCache{pkgcache.New(basePath)}
|
||||||
BasePath: basePath,
|
|
||||||
Transform: func(_ string) []string { return []string{} }, // flat structure for easy walk
|
|
||||||
CacheSizeMax: 1024 * 1024, // 1MB in-memory cache size for diskv itself
|
|
||||||
})
|
|
||||||
|
|
||||||
c := &DiskCache{
|
|
||||||
d: d,
|
|
||||||
basePath: basePath,
|
|
||||||
maxSize: defaultMaxSizeMB * 1024 * 1024, // 100MB default
|
|
||||||
defaultTTL: defaultTTLMinutes * time.Minute, // 60 minutes default
|
|
||||||
lruEnabled: true,
|
|
||||||
items: make(map[string]*list.Element),
|
|
||||||
evictList: list.New(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan directory on startup to rebuild LRU and size tracking
|
|
||||||
_ = c.loadTracker()
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set stores a key-value pair in the cache.
|
|
||||||
// Use DefaultExpiration for the configured default TTL, NoExpiration for no
|
|
||||||
// TTL, or a positive duration for a business-specific TTL.
|
|
||||||
func (c *DiskCache) Set(key string, value []byte, ttl time.Duration) error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if ttl == DefaultExpiration {
|
|
||||||
ttl = c.defaultTTL
|
|
||||||
}
|
|
||||||
|
|
||||||
var expiredAt time.Time
|
|
||||||
if ttl > 0 {
|
|
||||||
expiredAt = time.Now().Add(ttl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare data layout: 8 bytes expiration timestamp + raw payload
|
|
||||||
buf := make([]byte, headerSize+len(value))
|
|
||||||
var expNano int64
|
|
||||||
if !expiredAt.IsZero() {
|
|
||||||
expNano = expiredAt.UnixNano()
|
|
||||||
}
|
|
||||||
binary.BigEndian.PutUint64(buf[0:headerSize], uint64(expNano))
|
|
||||||
copy(buf[headerSize:], value)
|
|
||||||
|
|
||||||
// Write to diskv
|
|
||||||
if err := c.d.Write(key, buf); err != nil {
|
|
||||||
return fmt.Errorf("failed to write key to disk: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get file size on disk (approximate)
|
|
||||||
size := int64(len(buf))
|
|
||||||
|
|
||||||
// Update memory tracker
|
|
||||||
if elem, ok := c.items[key]; ok {
|
|
||||||
item := elem.Value.(*cacheItem)
|
|
||||||
c.currentSize += size - item.size
|
|
||||||
item.size = size
|
|
||||||
item.expiredAt = expiredAt
|
|
||||||
c.evictList.MoveToFront(elem)
|
|
||||||
} else {
|
|
||||||
item := &cacheItem{
|
|
||||||
key: key,
|
|
||||||
size: size,
|
|
||||||
expiredAt: expiredAt,
|
|
||||||
}
|
|
||||||
elem := c.evictList.PushFront(item)
|
|
||||||
c.items[key] = elem
|
|
||||||
c.currentSize += size
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evict items if size limit exceeded and LRU is enabled
|
|
||||||
c.evict()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get retrieves a key's value from the cache.
|
|
||||||
func (c *DiskCache) Get(key string) ([]byte, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
elem, ok := c.items[key]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrCacheMiss
|
|
||||||
}
|
|
||||||
|
|
||||||
item := elem.Value.(*cacheItem)
|
|
||||||
|
|
||||||
// Check expiration
|
|
||||||
if !item.expiredAt.IsZero() && time.Now().After(item.expiredAt) {
|
|
||||||
// Lazily delete expired item
|
|
||||||
_ = c.deleteUnlocked(key)
|
|
||||||
return nil, ErrCacheMiss
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read from diskv
|
|
||||||
data, err := c.d.Read(key)
|
|
||||||
if err != nil {
|
|
||||||
// Key exists in memory but not on disk, sync state
|
|
||||||
_ = c.deleteUnlocked(key)
|
|
||||||
return nil, ErrCacheMiss
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data) < headerSize {
|
|
||||||
_ = c.deleteUnlocked(key)
|
|
||||||
return nil, ErrCacheMiss
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update LRU access order
|
|
||||||
c.evictList.MoveToFront(elem)
|
|
||||||
|
|
||||||
// Slice off the metadata header
|
|
||||||
return data[headerSize:], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes a key-value pair from the cache.
|
|
||||||
func (c *DiskCache) Delete(key string) error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
return c.deleteUnlocked(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DiskCache) deleteUnlocked(key string) error {
|
|
||||||
if elem, ok := c.items[key]; ok {
|
|
||||||
item := elem.Value.(*cacheItem)
|
|
||||||
c.currentSize -= item.size
|
|
||||||
c.evictList.Remove(elem)
|
|
||||||
delete(c.items, key)
|
|
||||||
}
|
|
||||||
return c.d.Erase(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear flushes all cached elements.
|
|
||||||
func (c *DiskCache) Clear() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
c.currentSize = 0
|
|
||||||
c.items = make(map[string]*list.Element)
|
|
||||||
c.evictList.Init()
|
|
||||||
|
|
||||||
return c.d.EraseAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status returns the cache status.
|
|
||||||
func (c *DiskCache) Status() Status {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
|
|
||||||
return Status{
|
|
||||||
TotalSize: c.currentSize,
|
|
||||||
KeysCount: len(c.items),
|
|
||||||
MaxSizeMB: c.maxSize / (1024 * 1024),
|
|
||||||
TTLMinutes: int64(c.defaultTTL.Minutes()),
|
|
||||||
LRUEnabled: c.lruEnabled,
|
|
||||||
BasePath: c.basePath,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReloadConfig reloads policies from database configs dynamically.
|
// ReloadConfig reloads policies from database configs dynamically.
|
||||||
func (c *DiskCache) ReloadConfig(ctx context.Context) {
|
func (c *DiskCache) ReloadConfig(ctx context.Context) {
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
// Ensure DB is initialized before querying
|
// Ensure DB is initialized before querying
|
||||||
if db.DB(ctx) == nil {
|
if db.DB(ctx) == nil {
|
||||||
return
|
return
|
||||||
@@ -269,7 +76,6 @@ func (c *DiskCache) ReloadConfig(ctx context.Context) {
|
|||||||
maxSizeMB = val
|
maxSizeMB = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.maxSize = maxSizeMB * 1024 * 1024
|
|
||||||
|
|
||||||
// 2. Default TTL
|
// 2. Default TTL
|
||||||
var scTTL model.SystemConfig
|
var scTTL model.SystemConfig
|
||||||
@@ -279,7 +85,6 @@ func (c *DiskCache) ReloadConfig(ctx context.Context) {
|
|||||||
ttlMinutes = val
|
ttlMinutes = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.defaultTTL = time.Duration(ttlMinutes) * time.Minute
|
|
||||||
|
|
||||||
// 3. LRU Enabled
|
// 3. LRU Enabled
|
||||||
var scLRU model.SystemConfig
|
var scLRU model.SystemConfig
|
||||||
@@ -289,151 +94,6 @@ func (c *DiskCache) ReloadConfig(ctx context.Context) {
|
|||||||
lruEnabled = val
|
lruEnabled = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.lruEnabled = lruEnabled
|
|
||||||
|
|
||||||
// Apply eviction immediately under new configs
|
c.UpdatePolicy(maxSizeMB, ttlMinutes, lruEnabled)
|
||||||
c.evict()
|
|
||||||
}
|
|
||||||
|
|
||||||
// evict evicts oldest items if current size exceeds maxSize and LRU is enabled.
|
|
||||||
func (c *DiskCache) evict() {
|
|
||||||
if !c.lruEnabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for c.currentSize > c.maxSize && c.evictList.Len() > 0 {
|
|
||||||
elem := c.evictList.Back()
|
|
||||||
if elem == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
item := elem.Value.(*cacheItem)
|
|
||||||
key := item.key
|
|
||||||
|
|
||||||
// Remove from memory
|
|
||||||
c.currentSize -= item.size
|
|
||||||
c.evictList.Remove(elem)
|
|
||||||
delete(c.items, key)
|
|
||||||
|
|
||||||
// Delete from disk
|
|
||||||
_ = c.d.Erase(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadTracker walks the directory to rebuild the LRU and size tracking structures.
|
|
||||||
func (c *DiskCache) loadTracker() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
c.currentSize = 0
|
|
||||||
c.items = make(map[string]*list.Element)
|
|
||||||
c.evictList = list.New()
|
|
||||||
|
|
||||||
files, err := os.ReadDir(c.basePath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type tempItem struct {
|
|
||||||
key string
|
|
||||||
size int64
|
|
||||||
expiredAt time.Time
|
|
||||||
mtime time.Time
|
|
||||||
}
|
|
||||||
var loadedItems []tempItem
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
if file.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name := file.Name()
|
|
||||||
// Skip temporary files
|
|
||||||
if strings.HasPrefix(name, ".") || strings.Contains(name, "temp") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err := file.Info()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
filePath := filepath.Join(c.basePath, name)
|
|
||||||
// #nosec G304
|
|
||||||
f, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var expiredAt time.Time
|
|
||||||
var expNano int64
|
|
||||||
err = binary.Read(f, binary.BigEndian, &expNano)
|
|
||||||
_ = f.Close()
|
|
||||||
if err != nil {
|
|
||||||
// Corrupt metadata header: delete file
|
|
||||||
_ = os.Remove(filePath)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if expNano > 0 {
|
|
||||||
expiredAt = time.Unix(0, expNano)
|
|
||||||
// Expired: delete file
|
|
||||||
if time.Now().After(expiredAt) {
|
|
||||||
_ = os.Remove(filePath)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadedItems = append(loadedItems, tempItem{
|
|
||||||
key: name,
|
|
||||||
size: info.Size(),
|
|
||||||
expiredAt: expiredAt,
|
|
||||||
mtime: info.ModTime(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort loaded items by modification time ascending (oldest first)
|
|
||||||
sort.Slice(loadedItems, func(i, j int) bool {
|
|
||||||
return loadedItems[i].mtime.Before(loadedItems[j].mtime)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Populate LRU (PushFront so that newest items are at the front, oldest at the back)
|
|
||||||
for _, item := range loadedItems {
|
|
||||||
entry := &cacheItem{
|
|
||||||
key: item.key,
|
|
||||||
size: item.size,
|
|
||||||
expiredAt: item.expiredAt,
|
|
||||||
}
|
|
||||||
element := c.evictList.PushFront(entry)
|
|
||||||
c.items[item.key] = element
|
|
||||||
c.currentSize += item.size
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// startCleanupWorker periodically cleans up expired cache items.
|
|
||||||
func (c *DiskCache) startCleanupWorker(interval time.Duration) {
|
|
||||||
ticker := time.NewTicker(interval)
|
|
||||||
for range ticker.C {
|
|
||||||
c.cleanExpired()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanExpired scans memory for expired items and removes them.
|
|
||||||
func (c *DiskCache) cleanExpired() {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
for key, elem := range c.items {
|
|
||||||
item := elem.Value.(*cacheItem)
|
|
||||||
if !item.expiredAt.IsZero() && now.After(item.expiredAt) {
|
|
||||||
c.currentSize -= item.size
|
|
||||||
c.evictList.Remove(elem)
|
|
||||||
delete(c.items, key)
|
|
||||||
_ = c.d.Erase(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,218 +4,15 @@
|
|||||||
package diskcache
|
package diskcache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDiskCacheBasic(t *testing.T) {
|
|
||||||
testDir := "uploads/test_diskcache_basic"
|
|
||||||
defer func() { _ = os.RemoveAll(testDir) }()
|
|
||||||
_ = os.RemoveAll(testDir)
|
|
||||||
|
|
||||||
c := New(testDir)
|
|
||||||
defer func() { _ = c.Clear() }()
|
|
||||||
|
|
||||||
key := "key1"
|
|
||||||
val := []byte("value1")
|
|
||||||
|
|
||||||
// Get non-existent
|
|
||||||
_, err := c.Get(key)
|
|
||||||
if err != ErrCacheMiss {
|
|
||||||
t.Fatalf("expected ErrCacheMiss, got %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set & Get
|
|
||||||
err = c.Set(key, val, 10*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to set cache: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := c.Get(key)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get cache: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(got, val) {
|
|
||||||
t.Errorf("expected %s, got %s", val, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete
|
|
||||||
err = c.Delete(key)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to delete: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = c.Get(key)
|
|
||||||
if err != ErrCacheMiss {
|
|
||||||
t.Errorf("expected ErrCacheMiss after delete, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDiskCacheTTL(t *testing.T) {
|
|
||||||
testDir := "uploads/test_diskcache_ttl"
|
|
||||||
defer func() { _ = os.RemoveAll(testDir) }()
|
|
||||||
_ = os.RemoveAll(testDir)
|
|
||||||
|
|
||||||
c := New(testDir)
|
|
||||||
defer func() { _ = c.Clear() }()
|
|
||||||
|
|
||||||
key := "ttlkey"
|
|
||||||
val := []byte("ttlval")
|
|
||||||
|
|
||||||
// Set with 200ms TTL
|
|
||||||
err := c.Set(key, val, 200*time.Millisecond)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to set: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Immediate Get should succeed
|
|
||||||
got, err := c.Get(key)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get: %v", err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(got, val) {
|
|
||||||
t.Errorf("expected %s, got %s", val, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sleep 250ms to expire
|
|
||||||
time.Sleep(250 * time.Millisecond)
|
|
||||||
|
|
||||||
// Get should fail with cache miss
|
|
||||||
_, err = c.Get(key)
|
|
||||||
if err != ErrCacheMiss {
|
|
||||||
t.Errorf("expected ErrCacheMiss after TTL expiration, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDiskCacheExpirationPolicies(t *testing.T) {
|
|
||||||
testDir := "uploads/test_diskcache_expiration_policies"
|
|
||||||
defer func() { _ = os.RemoveAll(testDir) }()
|
|
||||||
_ = os.RemoveAll(testDir)
|
|
||||||
|
|
||||||
c := New(testDir)
|
|
||||||
defer func() { _ = c.Clear() }()
|
|
||||||
c.defaultTTL = 50 * time.Millisecond
|
|
||||||
|
|
||||||
if err := c.Set("default", []byte("default"), DefaultExpiration); err != nil {
|
|
||||||
t.Fatalf("Set(default, DefaultExpiration) returned error: %v", err)
|
|
||||||
}
|
|
||||||
if err := c.Set("custom", []byte("custom"), 100*time.Millisecond); err != nil {
|
|
||||||
t.Fatalf("Set(custom, 100ms) returned error: %v", err)
|
|
||||||
}
|
|
||||||
if err := c.Set("permanent", []byte("permanent"), NoExpiration); err != nil {
|
|
||||||
t.Fatalf("Set(permanent, NoExpiration) returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(75 * time.Millisecond)
|
|
||||||
|
|
||||||
if _, err := c.Get("default"); err != ErrCacheMiss {
|
|
||||||
t.Errorf("Get(default) error = %v, want ErrCacheMiss", err)
|
|
||||||
}
|
|
||||||
if _, err := c.Get("custom"); err != nil {
|
|
||||||
t.Errorf("Get(custom) returned error before custom TTL elapsed: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := c.Get("permanent"); err != nil {
|
|
||||||
t.Errorf("Get(permanent) returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
if _, err := c.Get("custom"); err != ErrCacheMiss {
|
|
||||||
t.Errorf("Get(custom) error = %v, want ErrCacheMiss", err)
|
|
||||||
}
|
|
||||||
if _, err := c.Get("permanent"); err != nil {
|
|
||||||
t.Errorf("Get(permanent) returned error after other entries expired: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDiskCacheNoExpirationSurvivesReload(t *testing.T) {
|
|
||||||
testDir := "uploads/test_diskcache_no_expiration_reload"
|
|
||||||
defer func() { _ = os.RemoveAll(testDir) }()
|
|
||||||
_ = os.RemoveAll(testDir)
|
|
||||||
|
|
||||||
c := New(testDir)
|
|
||||||
if err := c.Set("permanent", []byte("value"), NoExpiration); err != nil {
|
|
||||||
t.Fatalf("Set(permanent, NoExpiration) returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
reloaded := New(testDir)
|
|
||||||
defer func() { _ = reloaded.Clear() }()
|
|
||||||
|
|
||||||
got, err := reloaded.Get("permanent")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("reloaded Get(permanent) returned error: %v", err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(got, []byte("value")) {
|
|
||||||
t.Errorf("reloaded Get(permanent) = %q, want %q", got, "value")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDiskCacheLRUEviction(t *testing.T) {
|
|
||||||
testDir := "uploads/test_diskcache_lru"
|
|
||||||
defer func() { _ = os.RemoveAll(testDir) }()
|
|
||||||
_ = os.RemoveAll(testDir)
|
|
||||||
|
|
||||||
c := New(testDir)
|
|
||||||
defer func() { _ = c.Clear() }()
|
|
||||||
|
|
||||||
// Force a very small max size of 20 bytes for testing (8 bytes header + payload)
|
|
||||||
// So 2 items of 2 bytes payload = 2 * (8 + 2) = 20 bytes max.
|
|
||||||
c.maxSize = 20
|
|
||||||
c.lruEnabled = true
|
|
||||||
|
|
||||||
// Write item 1: 8 + 2 = 10 bytes
|
|
||||||
err := c.Set("k1", []byte("v1"), DefaultExpiration)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to set k1: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write item 2: 8 + 2 = 10 bytes
|
|
||||||
err = c.Set("k2", []byte("v2"), DefaultExpiration)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to set k2: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both should exist
|
|
||||||
if _, err := c.Get("k1"); err != nil {
|
|
||||||
t.Errorf("k1 should exist: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := c.Get("k2"); err != nil {
|
|
||||||
t.Errorf("k2 should exist: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write item 3: 8 + 2 = 10 bytes -> total size would be 30, exceeding 20.
|
|
||||||
// This should evict the oldest item. Since k1 was accessed, but then k2 was accessed,
|
|
||||||
// wait, let's access k1 again to make it the most recently used, so k2 becomes oldest!
|
|
||||||
_, _ = c.Get("k1") // k1 is now MRU, k2 is LRU
|
|
||||||
|
|
||||||
err = c.Set("k3", []byte("v3"), DefaultExpiration)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to set k3: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// k2 should be evicted, k1 and k3 should exist
|
|
||||||
_, err = c.Get("k2")
|
|
||||||
if err != ErrCacheMiss {
|
|
||||||
t.Errorf("expected k2 to be evicted, got error %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := c.Get("k1"); err != nil {
|
|
||||||
t.Errorf("k1 should still exist: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := c.Get("k3"); err != nil {
|
|
||||||
t.Errorf("k3 should exist: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDiskCacheReloadConfig(t *testing.T) {
|
func TestDiskCacheReloadConfig(t *testing.T) {
|
||||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/common"
|
"github.com/Rain-kl/Wavelet/internal/common"
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/pkg/util"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/config"
|
"github.com/Rain-kl/Wavelet/internal/config"
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/otel_trace"
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
|
otel_trace "github.com/Rain-kl/Wavelet/pkg/trace"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.opentelemetry.io/otel/codes"
|
"go.opentelemetry.io/otel/codes"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/apps/user"
|
"github.com/Rain-kl/Wavelet/internal/apps/user"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/util"
|
"github.com/Rain-kl/Wavelet/internal/util"
|
||||||
capUtil "github.com/Rain-kl/Wavelet/internal/util/cap"
|
capUtil "github.com/Rain-kl/Wavelet/internal/service/cap"
|
||||||
|
|
||||||
// Swagger 文档生成
|
// Swagger 文档生成
|
||||||
_ "github.com/Rain-kl/Wavelet/docs"
|
_ "github.com/Rain-kl/Wavelet/docs"
|
||||||
@@ -42,7 +42,7 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/apps/admin/system_config"
|
"github.com/Rain-kl/Wavelet/internal/apps/admin/system_config"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||||||
"github.com/Rain-kl/Wavelet/internal/config"
|
"github.com/Rain-kl/Wavelet/internal/config"
|
||||||
"github.com/Rain-kl/Wavelet/internal/otel_trace"
|
otel_trace "github.com/Rain-kl/Wavelet/pkg/trace"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-contrib/sessions/redis"
|
"github.com/gin-contrib/sessions/redis"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright 2026 Arctel.net
|
// Copyright 2026 Arctel.net
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Package cap provides CAPTCHA and proof-of-work (PoW) verification services.
|
||||||
package cap
|
package cap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/config"
|
"github.com/Rain-kl/Wavelet/internal/config"
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
pkgcap "github.com/Rain-kl/Wavelet/pkg/cap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -42,11 +44,11 @@ type Config struct {
|
|||||||
// Manager orchestrates challenge generation and solution validation
|
// Manager orchestrates challenge generation and solution validation
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
conf Config
|
conf Config
|
||||||
store Store
|
store pkgcap.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager creates a new CAPTCHA Manager
|
// NewManager creates a new CAPTCHA Manager
|
||||||
func NewManager(conf Config, store Store) *Manager {
|
func NewManager(conf Config, store pkgcap.Store) *Manager {
|
||||||
if conf.ChallengeCount <= 0 {
|
if conf.ChallengeCount <= 0 {
|
||||||
conf.ChallengeCount = managerDefaultChallengeCount
|
conf.ChallengeCount = managerDefaultChallengeCount
|
||||||
}
|
}
|
||||||
@@ -69,19 +71,27 @@ func NewManager(conf Config, store Store) *Manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate creates a challenge response
|
// Generate creates a challenge response
|
||||||
func (m *Manager) Generate(ctx context.Context, scope string) (*ChallengeResponse, error) {
|
func (m *Manager) Generate(ctx context.Context, scope string) (*pkgcap.ChallengeResponse, error) {
|
||||||
c := ChallengeConfig{
|
c := pkgcap.ChallengeConfig{
|
||||||
Count: m.getChallengeCount(ctx),
|
Count: m.getChallengeCount(ctx),
|
||||||
Size: m.getChallengeSize(ctx),
|
Size: m.getChallengeSize(ctx),
|
||||||
Difficulty: m.getChallengeDifficulty(ctx),
|
Difficulty: m.getChallengeDifficulty(ctx),
|
||||||
Expires: m.getChallengeTTL(ctx),
|
Expires: m.getChallengeTTL(ctx),
|
||||||
}
|
}
|
||||||
return GenerateChallenge(m.conf.Secret, c, scope)
|
return pkgcap.GenerateChallenge(m.conf.Secret, c, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedeemResponse is returned to the client on redeem
|
||||||
|
type RedeemResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
Expires int64 `json:"expires,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redeem verifies PoW solutions and returns a one-time redeem token
|
// Redeem verifies PoW solutions and returns a one-time redeem token
|
||||||
func (m *Manager) Redeem(ctx context.Context, token string, solutions []int, scope string) (*RedeemResponse, error) {
|
func (m *Manager) Redeem(ctx context.Context, token string, solutions []int, scope string) (*RedeemResponse, error) {
|
||||||
sigHex := jwtSigHex(token)
|
sigHex := pkgcap.JwtSigHex(token)
|
||||||
if sigHex == "" {
|
if sigHex == "" {
|
||||||
return &RedeemResponse{Success: false, Error: "invalid_token"}, nil
|
return &RedeemResponse{Success: false, Error: "invalid_token"}, nil
|
||||||
}
|
}
|
||||||
@@ -89,10 +99,7 @@ func (m *Manager) Redeem(ctx context.Context, token string, solutions []int, sco
|
|||||||
nonceKey := "cap:nonce:" + sigHex
|
nonceKey := "cap:nonce:" + sigHex
|
||||||
|
|
||||||
// Atomically claim the nonce slot BEFORE verifying solutions.
|
// Atomically claim the nonce slot BEFORE verifying solutions.
|
||||||
// SetNX returns true only when the key did not previously exist, so two
|
payload, err := pkgcap.VerifyChallengeSolutions(token, solutions, m.conf.Secret, scope)
|
||||||
// concurrent requests carrying the same JWT can never both succeed here.
|
|
||||||
// TTL is set to the challenge's remaining lifetime so the slot auto-expires.
|
|
||||||
payload, err := VerifyChallengeSolutions(token, solutions, m.conf.Secret, scope)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &RedeemResponse{Success: false, Error: err.Error()}, nil //nolint:nilerr // expected behavior: validation error is returned as response, not system error
|
return &RedeemResponse{Success: false, Error: err.Error()}, nil //nolint:nilerr // expected behavior: validation error is returned as response, not system error
|
||||||
}
|
}
|
||||||
@@ -115,8 +122,8 @@ func (m *Manager) Redeem(ctx context.Context, token string, solutions []int, sco
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate a redeem token formatted as "id:verToken"
|
// Generate a redeem token formatted as "id:verToken"
|
||||||
id := randomHex(redeemTokenIDLength)
|
id := pkgcap.RandomHex(redeemTokenIDLength)
|
||||||
verToken := randomHex(redeemVerTokenLength)
|
verToken := pkgcap.RandomHex(redeemVerTokenLength)
|
||||||
verHashBytes := sha256.Sum256([]byte(verToken))
|
verHashBytes := sha256.Sum256([]byte(verToken))
|
||||||
verHashHex := hex.EncodeToString(verHashBytes[:])
|
verHashHex := hex.EncodeToString(verHashBytes[:])
|
||||||
|
|
||||||
@@ -139,8 +146,6 @@ func (m *Manager) Redeem(ctx context.Context, token string, solutions []int, sco
|
|||||||
}
|
}
|
||||||
|
|
||||||
// VerifyToken validates and consumes the redeem token (single-use).
|
// VerifyToken validates and consumes the redeem token (single-use).
|
||||||
// GetAndDelete is used so that retrieval and removal happen atomically:
|
|
||||||
// two concurrent requests carrying the same token can never both see a value.
|
|
||||||
func (m *Manager) VerifyToken(ctx context.Context, token string, expectedScope string) (bool, error) {
|
func (m *Manager) VerifyToken(ctx context.Context, token string, expectedScope string) (bool, error) {
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return false, nil
|
return false, nil
|
||||||
@@ -157,8 +162,7 @@ func (m *Manager) VerifyToken(ctx context.Context, token string, expectedScope s
|
|||||||
|
|
||||||
tokenKey := "cap:token:" + id + ":" + verHashHex
|
tokenKey := "cap:token:" + id + ":" + verHashHex
|
||||||
|
|
||||||
// Atomically retrieve-and-delete: the first caller gets the value, any
|
// Atomically retrieve-and-delete
|
||||||
// subsequent caller (even concurrent) receives (false, nil) immediately.
|
|
||||||
val, exists, err := sGetAndDelete(ctx, m.store, tokenKey)
|
val, exists, err := sGetAndDelete(ctx, m.store, tokenKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -190,7 +194,7 @@ func (m *Manager) VerifyToken(ctx context.Context, token string, expectedScope s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sGetAndDelete safely calls store.GetAndDelete, treating a nil store as a miss.
|
// sGetAndDelete safely calls store.GetAndDelete, treating a nil store as a miss.
|
||||||
func sGetAndDelete(ctx context.Context, store Store, key string) (string, bool, error) {
|
func sGetAndDelete(ctx context.Context, store pkgcap.Store, key string) (string, bool, error) {
|
||||||
if store == nil {
|
if store == nil {
|
||||||
return "", false, nil
|
return "", false, nil
|
||||||
}
|
}
|
||||||
@@ -258,11 +262,11 @@ func GetDefaultManager() *Manager {
|
|||||||
challengeTTL := defaultChallengeTTL
|
challengeTTL := defaultChallengeTTL
|
||||||
tokenTTL := defaultTokenTTL
|
tokenTTL := defaultTokenTTL
|
||||||
|
|
||||||
var store Store
|
var store pkgcap.Store
|
||||||
if config.Config != nil && config.Config.Redis.Enabled && db.Redis != nil {
|
if config.Config != nil && config.Config.Redis.Enabled && db.Redis != nil {
|
||||||
store = NewRedisStore(db.Redis)
|
store = pkgcap.NewRedisStore(db.Redis)
|
||||||
} else {
|
} else {
|
||||||
store = NewMemoryStore(1 * time.Minute)
|
store = pkgcap.NewMemoryStore(1 * time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultManager = NewManager(Config{
|
defaultManager = NewManager(Config{
|
||||||
@@ -9,11 +9,13 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
pkgcap "github.com/Rain-kl/Wavelet/pkg/cap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCapFullFlow(t *testing.T) {
|
func TestCapFullFlow(t *testing.T) {
|
||||||
secret := []byte("a-very-long-secret-key-at-least-16-bytes")
|
secret := []byte("a-very-long-secret-key-at-least-16-bytes")
|
||||||
store := NewMemoryStore(1 * time.Minute)
|
store := pkgcap.NewMemoryStore(1 * time.Minute)
|
||||||
|
|
||||||
manager := NewManager(Config{
|
manager := NewManager(Config{
|
||||||
Secret: secret,
|
Secret: secret,
|
||||||
@@ -36,7 +38,7 @@ func TestCapFullFlow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Solve the challenge (acting as client)
|
// Solve the challenge (acting as client)
|
||||||
solutions := Solve(resp.Token, resp.Challenge.C, resp.Challenge.S, resp.Challenge.D)
|
solutions := pkgcap.Solve(resp.Token, resp.Challenge.C, resp.Challenge.S, resp.Challenge.D)
|
||||||
|
|
||||||
// Redeem
|
// Redeem
|
||||||
redeemResp, err := manager.Redeem(ctx, resp.Token, solutions, scope)
|
redeemResp, err := manager.Redeem(ctx, resp.Token, solutions, scope)
|
||||||
@@ -76,7 +78,7 @@ func TestRedeemConcurrentRace(t *testing.T) {
|
|||||||
const goroutines = 50
|
const goroutines = 50
|
||||||
|
|
||||||
secret := []byte("race-test-secret-key-at-least-16-bytes")
|
secret := []byte("race-test-secret-key-at-least-16-bytes")
|
||||||
store := NewMemoryStore(1 * time.Minute)
|
store := pkgcap.NewMemoryStore(1 * time.Minute)
|
||||||
manager := NewManager(Config{
|
manager := NewManager(Config{
|
||||||
Secret: secret,
|
Secret: secret,
|
||||||
ChallengeCount: 1,
|
ChallengeCount: 1,
|
||||||
@@ -91,7 +93,7 @@ func TestRedeemConcurrentRace(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Generate failed: %v", err)
|
t.Fatalf("Generate failed: %v", err)
|
||||||
}
|
}
|
||||||
solutions := Solve(resp.Token, resp.Challenge.C, resp.Challenge.S, resp.Challenge.D)
|
solutions := pkgcap.Solve(resp.Token, resp.Challenge.C, resp.Challenge.S, resp.Challenge.D)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
@@ -125,7 +127,7 @@ func TestVerifyTokenConcurrentRace(t *testing.T) {
|
|||||||
const goroutines = 50
|
const goroutines = 50
|
||||||
|
|
||||||
secret := []byte("race-test-secret-key-at-least-16-bytes")
|
secret := []byte("race-test-secret-key-at-least-16-bytes")
|
||||||
store := NewMemoryStore(1 * time.Minute)
|
store := pkgcap.NewMemoryStore(1 * time.Minute)
|
||||||
manager := NewManager(Config{
|
manager := NewManager(Config{
|
||||||
Secret: secret,
|
Secret: secret,
|
||||||
ChallengeCount: 1,
|
ChallengeCount: 1,
|
||||||
@@ -137,7 +139,7 @@ func TestVerifyTokenConcurrentRace(t *testing.T) {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
resp, _ := manager.Generate(ctx, "login")
|
resp, _ := manager.Generate(ctx, "login")
|
||||||
solutions := Solve(resp.Token, resp.Challenge.C, resp.Challenge.S, resp.Challenge.D)
|
solutions := pkgcap.Solve(resp.Token, resp.Challenge.C, resp.Challenge.S, resp.Challenge.D)
|
||||||
redeemResp, err := manager.Redeem(ctx, resp.Token, solutions, "login")
|
redeemResp, err := manager.Redeem(ctx, resp.Token, solutions, "login")
|
||||||
if err != nil || !redeemResp.Success {
|
if err != nil || !redeemResp.Success {
|
||||||
t.Fatalf("Redeem failed: %v %+v", err, redeemResp)
|
t.Fatalf("Redeem failed: %v %+v", err, redeemResp)
|
||||||
@@ -11,10 +11,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/storage"
|
"github.com/Rain-kl/Wavelet/internal/storage"
|
||||||
"github.com/Rain-kl/Wavelet/internal/task"
|
"github.com/Rain-kl/Wavelet/internal/task"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/httppool"
|
"github.com/Rain-kl/Wavelet/pkg/httppool"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getHTTPObject(ctx context.Context, baseURL, key string) (*Object, error) {
|
func getHTTPObject(ctx context.Context, baseURL, key string) (*Object, error) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/httppool"
|
"github.com/Rain-kl/Wavelet/pkg/httppool"
|
||||||
"github.com/studio-b12/gowebdav"
|
"github.com/studio-b12/gowebdav"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ func (b *webDAVBackend) Put(_ context.Context, key string, body io.Reader, size
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := b.client.WriteStreamWithLength(key, body, size, storageFilePerm); err != nil {
|
if err := b.client.WriteStreamWithLength(key, body, size, storageFilePerm); err != nil {
|
||||||
return PutResult{}, fmt.Errorf("put WebDAV object: %w", err)
|
return PutResult{}, fmt.Errorf("put WebDAV object: %w", err)
|
||||||
}
|
}
|
||||||
return PutResult{Key: key}, nil
|
return PutResult{Key: key}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/otel_trace"
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
|
otel_trace "github.com/Rain-kl/Wavelet/pkg/trace"
|
||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"go.opentelemetry.io/otel/codes"
|
"go.opentelemetry.io/otel/codes"
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/logger"
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/task"
|
"github.com/Rain-kl/Wavelet/internal/task"
|
||||||
|
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||||
|
|
||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,12 +7,4 @@ const (
|
|||||||
errCreateHTTPRequestFailed = "创建HTTP请求失败: %w"
|
errCreateHTTPRequestFailed = "创建HTTP请求失败: %w"
|
||||||
errHTTPRequestFailed = "请求%s接口失败: %w"
|
errHTTPRequestFailed = "请求%s接口失败: %w"
|
||||||
errInvalidCustomValue = "invalid value: %v"
|
errInvalidCustomValue = "invalid value: %v"
|
||||||
errInvalidSignKey = "invalid sign key: %w"
|
|
||||||
errSignKeyLengthInvalid = "sign key must be 32 bytes (64 hex characters)"
|
|
||||||
errCreateCipherFailed = "failed to create cipher: %w"
|
|
||||||
errCreateGCMFailed = "failed to create GCM: %w"
|
|
||||||
errGenerateNonceFailed = "failed to generate nonce: %w"
|
|
||||||
errDecodeCiphertextFailed = "failed to decode ciphertext: %w"
|
|
||||||
errCiphertextTooShort = "ciphertext too short"
|
|
||||||
errDecryptFailed = "failed to decrypt: %w"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
"github.com/Rain-kl/Wavelet/pkg/httppool"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsLocalhost 检查 URL 是否为 localhost
|
// IsLocalhost 检查 URL 是否为 localhost
|
||||||
@@ -35,12 +35,8 @@ const (
|
|||||||
|
|
||||||
// 配置HTTP客户端 使用 otelhttp 自动注入 trace span
|
// 配置HTTP客户端 使用 otelhttp 自动注入 trace span
|
||||||
var httpClient = &http.Client{
|
var httpClient = &http.Client{
|
||||||
Timeout: httpClientTimeout * time.Second,
|
Timeout: httpClientTimeout * time.Second,
|
||||||
Transport: otelhttp.NewTransport(&http.Transport{
|
Transport: httppool.DefaultTransport(),
|
||||||
MaxIdleConns: httpMaxIdleConns,
|
|
||||||
MaxIdleConnsPerHost: httpMaxIdleConnsPerHost,
|
|
||||||
IdleConnTimeout: httpIdleConnTimeout * time.Second,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetHTTPClient 替换全局 HTTP 客户端实例
|
// SetHTTPClient 替换全局 HTTP 客户端实例
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ func b64urlDecode(str string) ([]byte, error) {
|
|||||||
return base64.RawURLEncoding.DecodeString(str)
|
return base64.RawURLEncoding.DecodeString(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func randomHex(byteLen int) string {
|
// RandomHex generates a cryptographically secure random hexadecimal string of the specified byte length.
|
||||||
|
func RandomHex(byteLen int) string {
|
||||||
bytes := make([]byte, byteLen)
|
bytes := make([]byte, byteLen)
|
||||||
if _, err := rand.Read(bytes); err != nil {
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -129,7 +130,8 @@ func jwtVerify(token string, secret []byte) ([]byte, error) {
|
|||||||
return payload, nil
|
return payload, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func jwtSigHex(token string) string {
|
// JwtSigHex extracts the signature part of a JWT token and returns it as a hexadecimal string.
|
||||||
|
func JwtSigHex(token string) string {
|
||||||
parts := strings.Split(token, ".")
|
parts := strings.Split(token, ".")
|
||||||
if len(parts) != jwtPartsCount {
|
if len(parts) != jwtPartsCount {
|
||||||
return ""
|
return ""
|
||||||
@@ -160,7 +162,7 @@ func GenerateChallenge(secret []byte, conf ChallengeConfig, scope string) (*Chal
|
|||||||
expires := now + int64(conf.Expires/time.Millisecond)
|
expires := now + int64(conf.Expires/time.Millisecond)
|
||||||
|
|
||||||
payload := ChallengePayload{
|
payload := ChallengePayload{
|
||||||
Nonce: randomHex(defaultNonceLength),
|
Nonce: RandomHex(defaultNonceLength),
|
||||||
Count: conf.Count,
|
Count: conf.Count,
|
||||||
Size: conf.Size,
|
Size: conf.Size,
|
||||||
Difficulty: conf.Difficulty,
|
Difficulty: conf.Difficulty,
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Package diskcache implements a platform-level disk-backed cache with size limit, TTL, and LRU eviction.
|
||||||
|
package diskcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/diskv/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrCacheMiss represents a cache miss.
|
||||||
|
var ErrCacheMiss = errors.New("cache miss")
|
||||||
|
|
||||||
|
// Constants for disk cache configuration and sizing
|
||||||
|
const (
|
||||||
|
headerSize = 8 // 8 bytes metadata prefix for expiration UnixNano timestamp
|
||||||
|
defaultMaxSizeMB = 100
|
||||||
|
defaultTTLMinutes = 60
|
||||||
|
cacheDirPerm = 0750
|
||||||
|
|
||||||
|
// DefaultExpiration applies the cache-wide default TTL.
|
||||||
|
DefaultExpiration time.Duration = 0
|
||||||
|
// NoExpiration stores the item without a TTL. Size limits and LRU eviction still apply.
|
||||||
|
NoExpiration time.Duration = -1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Status represents the runtime cache statistics.
|
||||||
|
type Status struct {
|
||||||
|
TotalSize int64 `json:"total_size"`
|
||||||
|
KeysCount int `json:"keys_count"`
|
||||||
|
MaxSizeMB int64 `json:"max_size_mb"`
|
||||||
|
TTLMinutes int64 `json:"ttl_minutes"`
|
||||||
|
LRUEnabled bool `json:"lru_enabled"`
|
||||||
|
BasePath string `json:"base_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiskCache implements the disk-backed cache with size limits, TTL, and LRU eviction.
|
||||||
|
type DiskCache struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
d *diskv.Diskv
|
||||||
|
basePath string
|
||||||
|
maxSize int64 // in bytes
|
||||||
|
defaultTTL time.Duration
|
||||||
|
lruEnabled bool
|
||||||
|
|
||||||
|
// LRU and Size tracking
|
||||||
|
currentSize int64
|
||||||
|
items map[string]*list.Element
|
||||||
|
evictList *list.List
|
||||||
|
}
|
||||||
|
|
||||||
|
type cacheItem struct {
|
||||||
|
key string
|
||||||
|
size int64
|
||||||
|
expiredAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new DiskCache instance.
|
||||||
|
func New(basePath string) *DiskCache {
|
||||||
|
d := diskv.New(diskv.Options{
|
||||||
|
BasePath: basePath,
|
||||||
|
Transform: func(_ string) []string { return []string{} }, // flat structure for easy walk
|
||||||
|
CacheSizeMax: 1024 * 1024, // 1MB in-memory cache size for diskv itself
|
||||||
|
})
|
||||||
|
|
||||||
|
c := &DiskCache{
|
||||||
|
d: d,
|
||||||
|
basePath: basePath,
|
||||||
|
maxSize: defaultMaxSizeMB * 1024 * 1024, // 100MB default
|
||||||
|
defaultTTL: defaultTTLMinutes * time.Minute, // 60 minutes default
|
||||||
|
lruEnabled: true,
|
||||||
|
items: make(map[string]*list.Element),
|
||||||
|
evictList: list.New(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan directory on startup to rebuild LRU and size tracking
|
||||||
|
_ = c.loadTracker()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set stores a key-value pair in the cache.
|
||||||
|
// Use DefaultExpiration for the configured default TTL, NoExpiration for no
|
||||||
|
// TTL, or a positive duration for a business-specific TTL.
|
||||||
|
func (c *DiskCache) Set(key string, value []byte, ttl time.Duration) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if ttl == DefaultExpiration {
|
||||||
|
ttl = c.defaultTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiredAt time.Time
|
||||||
|
if ttl > 0 {
|
||||||
|
expiredAt = time.Now().Add(ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare data layout: 8 bytes expiration timestamp + raw payload
|
||||||
|
buf := make([]byte, headerSize+len(value))
|
||||||
|
var expNano int64
|
||||||
|
if !expiredAt.IsZero() {
|
||||||
|
expNano = expiredAt.UnixNano()
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint64(buf[0:headerSize], uint64(expNano))
|
||||||
|
copy(buf[headerSize:], value)
|
||||||
|
|
||||||
|
// Write to diskv
|
||||||
|
if err := c.d.Write(key, buf); err != nil {
|
||||||
|
return fmt.Errorf("failed to write key to disk: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file size on disk (approximate)
|
||||||
|
size := int64(len(buf))
|
||||||
|
|
||||||
|
// Update memory tracker
|
||||||
|
if elem, ok := c.items[key]; ok {
|
||||||
|
item := elem.Value.(*cacheItem)
|
||||||
|
c.currentSize += size - item.size
|
||||||
|
item.size = size
|
||||||
|
item.expiredAt = expiredAt
|
||||||
|
c.evictList.MoveToFront(elem)
|
||||||
|
} else {
|
||||||
|
item := &cacheItem{
|
||||||
|
key: key,
|
||||||
|
size: size,
|
||||||
|
expiredAt: expiredAt,
|
||||||
|
}
|
||||||
|
elem := c.evictList.PushFront(item)
|
||||||
|
c.items[key] = elem
|
||||||
|
c.currentSize += size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evict items if size limit exceeded and LRU is enabled
|
||||||
|
c.evict()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a key's value from the cache.
|
||||||
|
func (c *DiskCache) Get(key string) ([]byte, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
elem, ok := c.items[key]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrCacheMiss
|
||||||
|
}
|
||||||
|
|
||||||
|
item := elem.Value.(*cacheItem)
|
||||||
|
|
||||||
|
// Check expiration
|
||||||
|
if !item.expiredAt.IsZero() && time.Now().After(item.expiredAt) {
|
||||||
|
// Lazily delete expired item
|
||||||
|
_ = c.deleteUnlocked(key)
|
||||||
|
return nil, ErrCacheMiss
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from diskv
|
||||||
|
data, err := c.d.Read(key)
|
||||||
|
if err != nil {
|
||||||
|
// Key exists in memory but not on disk, sync state
|
||||||
|
_ = c.deleteUnlocked(key)
|
||||||
|
return nil, ErrCacheMiss
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) < headerSize {
|
||||||
|
_ = c.deleteUnlocked(key)
|
||||||
|
return nil, ErrCacheMiss
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update LRU access order
|
||||||
|
c.evictList.MoveToFront(elem)
|
||||||
|
|
||||||
|
// Slice off the metadata header
|
||||||
|
return data[headerSize:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes a key-value pair from the cache.
|
||||||
|
func (c *DiskCache) Delete(key string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return c.deleteUnlocked(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DiskCache) deleteUnlocked(key string) error {
|
||||||
|
if elem, ok := c.items[key]; ok {
|
||||||
|
item := elem.Value.(*cacheItem)
|
||||||
|
c.currentSize -= item.size
|
||||||
|
c.evictList.Remove(elem)
|
||||||
|
delete(c.items, key)
|
||||||
|
}
|
||||||
|
return c.d.Erase(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear flushes all cached elements.
|
||||||
|
func (c *DiskCache) Clear() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
c.currentSize = 0
|
||||||
|
c.items = make(map[string]*list.Element)
|
||||||
|
c.evictList.Init()
|
||||||
|
|
||||||
|
return c.d.EraseAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status returns the cache status.
|
||||||
|
func (c *DiskCache) Status() Status {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
return Status{
|
||||||
|
TotalSize: c.currentSize,
|
||||||
|
KeysCount: len(c.items),
|
||||||
|
MaxSizeMB: c.maxSize / (1024 * 1024),
|
||||||
|
TTLMinutes: int64(c.defaultTTL.Minutes()),
|
||||||
|
LRUEnabled: c.lruEnabled,
|
||||||
|
BasePath: c.basePath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePolicy dynamically updates policies.
|
||||||
|
func (c *DiskCache) UpdatePolicy(maxSizeMB int64, ttlMinutes int64, lruEnabled bool) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
c.maxSize = maxSizeMB * 1024 * 1024
|
||||||
|
c.defaultTTL = time.Duration(ttlMinutes) * time.Minute
|
||||||
|
c.lruEnabled = lruEnabled
|
||||||
|
c.evict()
|
||||||
|
}
|
||||||
|
|
||||||
|
// evict evicts oldest items if current size exceeds maxSize and LRU is enabled.
|
||||||
|
func (c *DiskCache) evict() {
|
||||||
|
if !c.lruEnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for c.currentSize > c.maxSize && c.evictList.Len() > 0 {
|
||||||
|
elem := c.evictList.Back()
|
||||||
|
item := elem.Value.(*cacheItem)
|
||||||
|
c.currentSize -= item.size
|
||||||
|
c.evictList.Remove(elem)
|
||||||
|
delete(c.items, item.key)
|
||||||
|
_ = c.d.Erase(item.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadTracker scans the cache directory on startup to rebuild memory state.
|
||||||
|
func (c *DiskCache) loadTracker() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if err := os.MkdirAll(c.basePath, cacheDirPerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type loadedItem struct {
|
||||||
|
key string
|
||||||
|
size int64
|
||||||
|
expiredAt time.Time
|
||||||
|
modTime time.Time
|
||||||
|
}
|
||||||
|
var loadedItems []loadedItem
|
||||||
|
|
||||||
|
// Walk keys through diskv
|
||||||
|
keysChan := c.d.Keys(nil)
|
||||||
|
for key := range keysChan {
|
||||||
|
// Read raw bytes to parse expiration prefix
|
||||||
|
data, err := c.d.Read(key)
|
||||||
|
if err != nil || len(data) < headerSize {
|
||||||
|
_ = c.d.Erase(key) // corrupted file, wipe
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
expNano := int64(binary.BigEndian.Uint64(data[0:headerSize])) //nolint:gosec // false positive: UnixNano fits within int64
|
||||||
|
var expiredAt time.Time
|
||||||
|
if expNano > 0 {
|
||||||
|
expiredAt = time.Unix(0, expNano)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check mod time for ordering
|
||||||
|
path := filepath.Join(c.basePath, key)
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedItems = append(loadedItems, loadedItem{
|
||||||
|
key: key,
|
||||||
|
size: int64(len(data)),
|
||||||
|
expiredAt: expiredAt,
|
||||||
|
modTime: info.ModTime(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by ModTime ascending (oldest first) so we rebuild LRU correctly
|
||||||
|
sort.Slice(loadedItems, func(i, j int) bool {
|
||||||
|
return loadedItems[i].modTime.Before(loadedItems[j].modTime)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Populate LRU (PushFront so that newest items are at the front, oldest at the back)
|
||||||
|
for _, item := range loadedItems {
|
||||||
|
entry := &cacheItem{
|
||||||
|
key: item.key,
|
||||||
|
size: item.size,
|
||||||
|
expiredAt: item.expiredAt,
|
||||||
|
}
|
||||||
|
element := c.evictList.PushFront(entry)
|
||||||
|
c.items[item.key] = element
|
||||||
|
c.currentSize += item.size
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartCleanupWorker periodically cleans up expired cache items.
|
||||||
|
func (c *DiskCache) StartCleanupWorker(interval time.Duration) {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
for range ticker.C {
|
||||||
|
c.cleanExpired()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanExpired scans memory for expired items and removes them.
|
||||||
|
func (c *DiskCache) cleanExpired() {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for key, elem := range c.items {
|
||||||
|
item := elem.Value.(*cacheItem)
|
||||||
|
if !item.expiredAt.IsZero() && now.After(item.expiredAt) {
|
||||||
|
c.currentSize -= item.size
|
||||||
|
c.evictList.Remove(elem)
|
||||||
|
delete(c.items, key)
|
||||||
|
_ = c.d.Erase(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package diskcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDiskCacheBasic(t *testing.T) {
|
||||||
|
testDir := "uploads/test_diskcache_basic"
|
||||||
|
defer func() { _ = os.RemoveAll(testDir) }()
|
||||||
|
_ = os.RemoveAll(testDir)
|
||||||
|
|
||||||
|
c := New(testDir)
|
||||||
|
defer func() { _ = c.Clear() }()
|
||||||
|
|
||||||
|
key := "key1"
|
||||||
|
val := []byte("value1")
|
||||||
|
|
||||||
|
// Get non-existent
|
||||||
|
_, err := c.Get(key)
|
||||||
|
if err != ErrCacheMiss {
|
||||||
|
t.Fatalf("expected ErrCacheMiss, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set & Get
|
||||||
|
err = c.Set(key, val, 10*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to set cache: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := c.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get cache: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(got, val) {
|
||||||
|
t.Errorf("expected %s, got %s", val, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
err = c.Delete(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to delete: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.Get(key)
|
||||||
|
if err != ErrCacheMiss {
|
||||||
|
t.Errorf("expected ErrCacheMiss after delete, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiskCacheTTL(t *testing.T) {
|
||||||
|
testDir := "uploads/test_diskcache_ttl"
|
||||||
|
defer func() { _ = os.RemoveAll(testDir) }()
|
||||||
|
_ = os.RemoveAll(testDir)
|
||||||
|
|
||||||
|
c := New(testDir)
|
||||||
|
defer func() { _ = c.Clear() }()
|
||||||
|
|
||||||
|
key := "ttlkey"
|
||||||
|
val := []byte("ttlval")
|
||||||
|
|
||||||
|
// Set with 200ms TTL
|
||||||
|
err := c.Set(key, val, 200*time.Millisecond)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to set: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Immediate Get should succeed
|
||||||
|
got, err := c.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, val) {
|
||||||
|
t.Errorf("expected %s, got %s", val, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep 250ms to expire
|
||||||
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
|
||||||
|
// Get should fail with cache miss
|
||||||
|
_, err = c.Get(key)
|
||||||
|
if err != ErrCacheMiss {
|
||||||
|
t.Errorf("expected ErrCacheMiss after TTL expiration, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiskCacheExpirationPolicies(t *testing.T) {
|
||||||
|
testDir := "uploads/test_diskcache_expiration_policies"
|
||||||
|
defer func() { _ = os.RemoveAll(testDir) }()
|
||||||
|
_ = os.RemoveAll(testDir)
|
||||||
|
|
||||||
|
c := New(testDir)
|
||||||
|
defer func() { _ = c.Clear() }()
|
||||||
|
c.defaultTTL = 50 * time.Millisecond
|
||||||
|
|
||||||
|
if err := c.Set("default", []byte("default"), DefaultExpiration); err != nil {
|
||||||
|
t.Fatalf("Set(default, DefaultExpiration) returned error: %v", err)
|
||||||
|
}
|
||||||
|
if err := c.Set("custom", []byte("custom"), 100*time.Millisecond); err != nil {
|
||||||
|
t.Fatalf("Set(custom, 100ms) returned error: %v", err)
|
||||||
|
}
|
||||||
|
if err := c.Set("permanent", []byte("permanent"), NoExpiration); err != nil {
|
||||||
|
t.Fatalf("Set(permanent, NoExpiration) returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(75 * time.Millisecond)
|
||||||
|
|
||||||
|
if _, err := c.Get("default"); err != ErrCacheMiss {
|
||||||
|
t.Errorf("Get(default) error = %v, want ErrCacheMiss", err)
|
||||||
|
}
|
||||||
|
if _, err := c.Get("custom"); err != nil {
|
||||||
|
t.Errorf("Get(custom) returned error before custom TTL elapsed: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := c.Get("permanent"); err != nil {
|
||||||
|
t.Errorf("Get(permanent) returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
if _, err := c.Get("custom"); err != ErrCacheMiss {
|
||||||
|
t.Errorf("Get(custom) error = %v, want ErrCacheMiss", err)
|
||||||
|
}
|
||||||
|
if _, err := c.Get("permanent"); err != nil {
|
||||||
|
t.Errorf("Get(permanent) returned error after other entries expired: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiskCacheNoExpirationSurvivesReload(t *testing.T) {
|
||||||
|
testDir := "uploads/test_diskcache_no_expiration_reload"
|
||||||
|
defer func() { _ = os.RemoveAll(testDir) }()
|
||||||
|
_ = os.RemoveAll(testDir)
|
||||||
|
|
||||||
|
c := New(testDir)
|
||||||
|
if err := c.Set("permanent", []byte("value"), NoExpiration); err != nil {
|
||||||
|
t.Fatalf("Set(permanent, NoExpiration) returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reloaded := New(testDir)
|
||||||
|
defer func() { _ = reloaded.Clear() }()
|
||||||
|
|
||||||
|
got, err := reloaded.Get("permanent")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reloaded Get(permanent) returned error: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, []byte("value")) {
|
||||||
|
t.Errorf("reloaded Get(permanent) = %q, want %q", got, "value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiskCacheLRUEviction(t *testing.T) {
|
||||||
|
testDir := "uploads/test_diskcache_lru"
|
||||||
|
defer func() { _ = os.RemoveAll(testDir) }()
|
||||||
|
_ = os.RemoveAll(testDir)
|
||||||
|
|
||||||
|
c := New(testDir)
|
||||||
|
defer func() { _ = c.Clear() }()
|
||||||
|
|
||||||
|
// Force a very small max size of 20 bytes for testing (8 bytes header + payload)
|
||||||
|
// So 2 items of 2 bytes payload = 2 * (8 + 2) = 20 bytes max.
|
||||||
|
c.maxSize = 20
|
||||||
|
c.lruEnabled = true
|
||||||
|
|
||||||
|
// Write item 1: 8 + 2 = 10 bytes
|
||||||
|
err := c.Set("k1", []byte("v1"), DefaultExpiration)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to set k1: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write item 2: 8 + 2 = 10 bytes
|
||||||
|
err = c.Set("k2", []byte("v2"), DefaultExpiration)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to set k2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both should exist
|
||||||
|
if _, err := c.Get("k1"); err != nil {
|
||||||
|
t.Errorf("k1 should exist: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := c.Get("k2"); err != nil {
|
||||||
|
t.Errorf("k2 should exist: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write item 3: 8 + 2 = 10 bytes -> total size would be 30, exceeding 20.
|
||||||
|
// This should evict the oldest item. Since k1 was accessed, but then k2 was accessed,
|
||||||
|
// wait, let's access k1 again to make it the most recently used, so k2 becomes oldest!
|
||||||
|
_, _ = c.Get("k1") // k1 is now MRU, k2 is LRU
|
||||||
|
|
||||||
|
err = c.Set("k3", []byte("v3"), DefaultExpiration)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to set k3: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// k2 should be evicted, k1 and k3 should exist
|
||||||
|
_, err = c.Get("k2")
|
||||||
|
if err != ErrCacheMiss {
|
||||||
|
t.Errorf("expected k2 to be evicted, got error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := c.Get("k1"); err != nil {
|
||||||
|
t.Errorf("k1 should still exist: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := c.Get("k3"); err != nil {
|
||||||
|
t.Errorf("k3 should exist: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,18 @@ import (
|
|||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config represents the logging configuration.
|
||||||
|
type Config struct {
|
||||||
|
Level string
|
||||||
|
Format string
|
||||||
|
Output string
|
||||||
|
FilePath string
|
||||||
|
MaxSize int
|
||||||
|
MaxAge int
|
||||||
|
MaxBackups int
|
||||||
|
Compress bool
|
||||||
|
}
|
||||||
|
|
||||||
var logger *otelzap.Logger
|
var logger *otelzap.Logger
|
||||||
|
|
||||||
// ringBufferCapacity 环形缓冲区容量
|
// ringBufferCapacity 环形缓冲区容量
|
||||||
@@ -22,14 +34,16 @@ const ringBufferCapacity = 5000
|
|||||||
// GlobalRingBuffer 全局日志环形缓冲区,供 Admin 日志查询和 WebSocket 推送使用
|
// GlobalRingBuffer 全局日志环形缓冲区,供 Admin 日志查询和 WebSocket 推送使用
|
||||||
var GlobalRingBuffer *LogRingBuffer
|
var GlobalRingBuffer *LogRingBuffer
|
||||||
|
|
||||||
func init() {
|
func doInit(cfg Config) {
|
||||||
logWriter, err := GetLogWriter()
|
logWriter, err := getLogWriterForConfig(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("[Logger] get log writer err: %v\n", err)
|
log.Fatalf("[Logger] get log writer err: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化 ring buffer(保留最近 5000 行日志)
|
// 初始化 ring buffer(保留最近 5000 行日志),如果是多次调用 Init,不需要重复创建 GlobalRingBuffer
|
||||||
GlobalRingBuffer = NewLogRingBuffer(ringBufferCapacity)
|
if GlobalRingBuffer == nil {
|
||||||
|
GlobalRingBuffer = NewLogRingBuffer(ringBufferCapacity)
|
||||||
|
}
|
||||||
|
|
||||||
// 使用 multi writer 同时写入原始输出和 ring buffer
|
// 使用 multi writer 同时写入原始输出和 ring buffer
|
||||||
multiWriter := zapcore.NewMultiWriteSyncer(
|
multiWriter := zapcore.NewMultiWriteSyncer(
|
||||||
@@ -38,7 +52,7 @@ func init() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
zapLogger := zap.New(
|
zapLogger := zap.New(
|
||||||
zapcore.NewCore(getEncoder(), multiWriter, getLogLevel()),
|
zapcore.NewCore(getEncoderForConfig(cfg), multiWriter, getLogLevelForConfig(cfg)),
|
||||||
zap.AddCaller(),
|
zap.AddCaller(),
|
||||||
zap.AddCallerSkip(1),
|
zap.AddCallerSkip(1),
|
||||||
)
|
)
|
||||||
@@ -46,8 +60,21 @@ func init() {
|
|||||||
zapLogger,
|
zapLogger,
|
||||||
otelzap.WithMinLevel(zapLogger.Level()),
|
otelzap.WithMinLevel(zapLogger.Level()),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("[Logger] %s\n", logger.Level())
|
func init() {
|
||||||
|
// 默认使用 console stdout INFO 日志输出,避免在 Init 前或测试中发生空指针崩溃
|
||||||
|
defaultCfg := Config{
|
||||||
|
Level: "info",
|
||||||
|
Format: "console",
|
||||||
|
Output: "stdout",
|
||||||
|
}
|
||||||
|
doInit(defaultCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the logger with a custom configuration.
|
||||||
|
func Init(cfg Config) {
|
||||||
|
doInit(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DebugF 输出 Debug 级别日志
|
// DebugF 输出 Debug 级别日志
|
||||||
@@ -10,39 +10,20 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/config"
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
"gopkg.in/natefinch/lumberjack.v2"
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
logWriter zapcore.WriteSyncer
|
|
||||||
initLogWriterOnce sync.Once
|
|
||||||
initLogWriterErr error
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetLogWriter 获取日志输出写入器
|
|
||||||
func GetLogWriter() (zapcore.WriteSyncer, error) {
|
|
||||||
initLogWriterOnce.Do(func() {
|
|
||||||
logWriter, initLogWriterErr = initWriter()
|
|
||||||
})
|
|
||||||
|
|
||||||
return logWriter, initLogWriterErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// logDirPerm 日志目录权限
|
// logDirPerm 日志目录权限
|
||||||
const logDirPerm = 0750
|
const logDirPerm = 0750
|
||||||
|
|
||||||
func initWriter() (zapcore.WriteSyncer, error) {
|
func getLogWriterForConfig(cfg Config) (zapcore.WriteSyncer, error) {
|
||||||
logConfig := config.Config.Log
|
if cfg.Output == "file" {
|
||||||
|
|
||||||
if logConfig.Output == "file" {
|
|
||||||
// 初始化日志目录
|
// 初始化日志目录
|
||||||
logPath := logConfig.FilePath
|
logPath := cfg.FilePath
|
||||||
logDir := filepath.Dir(logPath)
|
logDir := filepath.Dir(logPath)
|
||||||
if err := os.MkdirAll(logDir, logDirPerm); err != nil {
|
if err := os.MkdirAll(logDir, logDirPerm); err != nil {
|
||||||
return nil, fmt.Errorf(errCreateLogFileDirFailed, err)
|
return nil, fmt.Errorf(errCreateLogFileDirFailed, err)
|
||||||
@@ -51,10 +32,10 @@ func initWriter() (zapcore.WriteSyncer, error) {
|
|||||||
// 配置日志轮转
|
// 配置日志轮转
|
||||||
logOutput := &lumberjack.Logger{
|
logOutput := &lumberjack.Logger{
|
||||||
Filename: logPath,
|
Filename: logPath,
|
||||||
MaxSize: logConfig.MaxSize,
|
MaxSize: cfg.MaxSize,
|
||||||
MaxBackups: logConfig.MaxBackups,
|
MaxBackups: cfg.MaxBackups,
|
||||||
MaxAge: logConfig.MaxAge,
|
MaxAge: cfg.MaxAge,
|
||||||
Compress: logConfig.Compress,
|
Compress: cfg.Compress,
|
||||||
}
|
}
|
||||||
|
|
||||||
return zapcore.AddSync(logOutput), nil
|
return zapcore.AddSync(logOutput), nil
|
||||||
@@ -63,8 +44,8 @@ func initWriter() (zapcore.WriteSyncer, error) {
|
|||||||
return zapcore.AddSync(os.Stdout), nil
|
return zapcore.AddSync(os.Stdout), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEncoder 获取日志编码器
|
// getEncoderForConfig 获取日志编码器
|
||||||
func getEncoder() zapcore.Encoder {
|
func getEncoderForConfig(cfg Config) zapcore.Encoder {
|
||||||
// 编码器配置
|
// 编码器配置
|
||||||
encoderConfig := zapcore.EncoderConfig{
|
encoderConfig := zapcore.EncoderConfig{
|
||||||
TimeKey: "time",
|
TimeKey: "time",
|
||||||
@@ -80,15 +61,15 @@ func getEncoder() zapcore.Encoder {
|
|||||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Config.Log.Format == "json" {
|
if cfg.Format == "json" {
|
||||||
return zapcore.NewJSONEncoder(encoderConfig)
|
return zapcore.NewJSONEncoder(encoderConfig)
|
||||||
}
|
}
|
||||||
return zapcore.NewConsoleEncoder(encoderConfig)
|
return zapcore.NewConsoleEncoder(encoderConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getLogLevel 获取日志级别
|
// getLogLevelForConfig 获取日志级别
|
||||||
func getLogLevel() zapcore.Level {
|
func getLogLevelForConfig(cfg Config) zapcore.Level {
|
||||||
level := config.Config.Log.Level
|
level := cfg.Level
|
||||||
|
|
||||||
switch level {
|
switch level {
|
||||||
case "debug":
|
case "debug":
|
||||||
@@ -100,7 +81,7 @@ func getLogLevel() zapcore.Level {
|
|||||||
case "error":
|
case "error":
|
||||||
return zapcore.ErrorLevel
|
return zapcore.ErrorLevel
|
||||||
default:
|
default:
|
||||||
log.Fatalf("[Logger] invalid log level: %s\n", level)
|
log.Printf("[Logger] invalid log level: %s, defaulting to info\n", level)
|
||||||
return zapcore.InfoLevel
|
return zapcore.InfoLevel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
// Copyright 2026 Arctel.net
|
// Copyright 2026 Arctel.net
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
// Package otel_trace 提供 OpenTelemetry 链路追踪封装工具
|
// Package trace 提供 OpenTelemetry 链路追踪封装工具
|
||||||
package otel_trace
|
package trace
|
||||||
|
|
||||||
import "go.opentelemetry.io/otel/propagation"
|
import "go.opentelemetry.io/otel/propagation"
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// Copyright 2026 Arctel.net
|
// Copyright 2026 Arctel.net
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package otel_trace
|
package trace
|
||||||
|
|
||||||
import (
|
import (
|
||||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// Copyright 2026 Arctel.net
|
// Copyright 2026 Arctel.net
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package otel_trace
|
package trace
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -21,15 +21,26 @@ func init() {
|
|||||||
prop := newPropagator()
|
prop := newPropagator()
|
||||||
otel.SetTextMapPropagator(prop)
|
otel.SetTextMapPropagator(prop)
|
||||||
|
|
||||||
// 初始化 Trace Provider
|
// 初始化 Tracer 实例为 No-op 默认以避免未初始化前或测试环境崩溃
|
||||||
tracerProvider, err := newTracerProvider()
|
Tracer = otel.GetTracerProvider().Tracer("github.com/Rain-kl/Wavelet")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config 链路追踪配置
|
||||||
|
type Config struct {
|
||||||
|
AppName string
|
||||||
|
SamplingRate float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init 初始化 Tracer Provider 并关联全局 Tracer 实例
|
||||||
|
func Init(cfg Config) {
|
||||||
|
tracerProvider, err := newTracerProvider(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("[Trace] init trace provider failed: %v", err)
|
log.Fatalf("[Trace] init trace provider failed: %v", err)
|
||||||
}
|
}
|
||||||
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
|
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
|
||||||
otel.SetTracerProvider(tracerProvider)
|
otel.SetTracerProvider(tracerProvider)
|
||||||
|
|
||||||
// 初始化 Tracer
|
// 更新 Tracer
|
||||||
Tracer = tracerProvider.Tracer("github.com/Rain-kl/Wavelet")
|
Tracer = tracerProvider.Tracer("github.com/Rain-kl/Wavelet")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +49,6 @@ func Shutdown(ctx context.Context) {
|
|||||||
for _, fn := range shutdownFuncs {
|
for _, fn := range shutdownFuncs {
|
||||||
_ = fn(ctx)
|
_ = fn(ctx)
|
||||||
}
|
}
|
||||||
shutdownFuncs = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start 创建一个新的 Trace Span
|
// Start 创建一个新的 Trace Span
|
||||||
@@ -2,20 +2,19 @@
|
|||||||
// Copyright 2026 Arctel.net
|
// Copyright 2026 Arctel.net
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package otel_trace
|
package trace
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/config"
|
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||||
"go.opentelemetry.io/otel/sdk/resource"
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTracerProvider() (*sdktrace.TracerProvider, error) {
|
func newTracerProvider(cfg Config) (*sdktrace.TracerProvider, error) {
|
||||||
// 获取主机名和容器信息
|
// 获取主机名和容器信息
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -27,7 +26,7 @@ func newTracerProvider() (*sdktrace.TracerProvider, error) {
|
|||||||
resource.Default(),
|
resource.Default(),
|
||||||
resource.NewWithAttributes(
|
resource.NewWithAttributes(
|
||||||
semconv.SchemaURL,
|
semconv.SchemaURL,
|
||||||
semconv.ServiceName(config.Config.App.AppName),
|
semconv.ServiceName(cfg.AppName),
|
||||||
semconv.HostName(hostname),
|
semconv.HostName(hostname),
|
||||||
semconv.K8SNamespaceName(os.Getenv("KUBERNETES_NAMESPACE")),
|
semconv.K8SNamespaceName(os.Getenv("KUBERNETES_NAMESPACE")),
|
||||||
semconv.K8SPodName(os.Getenv("KUBERNETES_POD_NAME")),
|
semconv.K8SPodName(os.Getenv("KUBERNETES_POD_NAME")),
|
||||||
@@ -48,7 +47,7 @@ func newTracerProvider() (*sdktrace.TracerProvider, error) {
|
|||||||
tracerProvider := sdktrace.NewTracerProvider(
|
tracerProvider := sdktrace.NewTracerProvider(
|
||||||
sdktrace.WithBatcher(traceExporter),
|
sdktrace.WithBatcher(traceExporter),
|
||||||
sdktrace.WithResource(r),
|
sdktrace.WithResource(r),
|
||||||
sdktrace.WithSampler(ParentBasedErrorAwareSampler(config.Config.Otel.SamplingRate)),
|
sdktrace.WithSampler(ParentBasedErrorAwareSampler(cfg.SamplingRate)),
|
||||||
)
|
)
|
||||||
return tracerProvider, nil
|
return tracerProvider, nil
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// Copyright 2026 Arctel.net
|
// Copyright 2026 Arctel.net
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Package util provides generic utility functions.
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -16,8 +17,18 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
// aesKeyLength AES-256 密钥字节长度
|
const (
|
||||||
const aesKeyLength = 32
|
aesKeyLength = 32
|
||||||
|
|
||||||
|
errInvalidSignKey = "invalid sign key: %w"
|
||||||
|
errSignKeyLengthInvalid = "sign key must be 32 bytes (64 hex characters)"
|
||||||
|
errCreateCipherFailed = "failed to create cipher: %w"
|
||||||
|
errCreateGCMFailed = "failed to create GCM: %w"
|
||||||
|
errGenerateNonceFailed = "failed to generate nonce: %w"
|
||||||
|
errDecodeCiphertextFailed = "failed to decode ciphertext: %w"
|
||||||
|
errCiphertextTooShort = "ciphertext too short"
|
||||||
|
errDecryptFailed = "failed to decrypt: %w"
|
||||||
|
)
|
||||||
|
|
||||||
// Encrypt 使用 SignKey 加密字符串数据
|
// Encrypt 使用 SignKey 加密字符串数据
|
||||||
// signKey: 64 字符 hex 编码的密钥(对应 32 字节,用于 AES-256)
|
// signKey: 64 字符 hex 编码的密钥(对应 32 字节,用于 AES-256)
|
||||||
Reference in New Issue
Block a user