first commit
This commit is contained in:
205
backend/internal/storage/redis.go
Normal file
205
backend/internal/storage/redis.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"filefast/backend/internal/config"
|
||||
"filefast/backend/internal/model"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const (
|
||||
RedisRealtimeRelayChannel = "filefast:ws:relay"
|
||||
RedisRealtimePresenceChannel = "filefast:ws:presence"
|
||||
)
|
||||
|
||||
type RedisClient struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
type RealtimeMessage struct {
|
||||
Channel string
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func NewRedisClient(cfg config.RedisConfig) *RedisClient {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: cfg.Addr,
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
MaxRetries: 1,
|
||||
DialTimeout: 3 * time.Second,
|
||||
ReadTimeout: 3 * time.Second,
|
||||
WriteTimeout: 3 * time.Second,
|
||||
})
|
||||
|
||||
return &RedisClient{client: client}
|
||||
}
|
||||
|
||||
func (c *RedisClient) Ping(ctx context.Context) error {
|
||||
if !c.available() {
|
||||
return redis.ErrClosed
|
||||
}
|
||||
return c.client.Ping(ctx).Err()
|
||||
}
|
||||
|
||||
func (c *RedisClient) SaveAdminSession(ctx context.Context, session model.AdminSession, ttl time.Duration) error {
|
||||
if !c.available() {
|
||||
return redis.ErrClosed
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.client.Set(ctx, adminSessionKey(session.Token), payload, ttl).Err()
|
||||
}
|
||||
|
||||
func (c *RedisClient) HasAdminSession(ctx context.Context, token string) (bool, error) {
|
||||
if !c.available() {
|
||||
return false, redis.ErrClosed
|
||||
}
|
||||
|
||||
result, err := c.client.Exists(ctx, adminSessionKey(token)).Result()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return result > 0, nil
|
||||
}
|
||||
|
||||
func (c *RedisClient) SaveDeviceSession(ctx context.Context, session model.DeviceSession, ttl time.Duration) error {
|
||||
if !c.available() {
|
||||
return redis.ErrClosed
|
||||
}
|
||||
|
||||
return c.client.Set(ctx, deviceSessionKey(session.DeviceID), session.Token, ttl).Err()
|
||||
}
|
||||
|
||||
func (c *RedisClient) ValidateDeviceSession(ctx context.Context, deviceID, token string) (bool, error) {
|
||||
if !c.available() {
|
||||
return false, redis.ErrClosed
|
||||
}
|
||||
|
||||
value, err := c.client.Get(ctx, deviceSessionKey(deviceID)).Result()
|
||||
if err != nil {
|
||||
if err == redis.Nil {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
return value == token, nil
|
||||
}
|
||||
|
||||
func (c *RedisClient) SetDevicePresence(ctx context.Context, deviceID string, online bool, lastSeen time.Time, ttl time.Duration) error {
|
||||
if !c.available() {
|
||||
return redis.ErrClosed
|
||||
}
|
||||
|
||||
key := devicePresenceKey(deviceID)
|
||||
if !online {
|
||||
return c.client.Del(ctx, key).Err()
|
||||
}
|
||||
|
||||
return c.client.Set(ctx, key, lastSeen.UTC().Format(time.RFC3339Nano), ttl).Err()
|
||||
}
|
||||
|
||||
func (c *RedisClient) GetDevicePresence(ctx context.Context, deviceIDs []string) (map[string]bool, error) {
|
||||
if !c.available() {
|
||||
return nil, redis.ErrClosed
|
||||
}
|
||||
|
||||
statuses := make(map[string]bool, len(deviceIDs))
|
||||
if len(deviceIDs) == 0 {
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(deviceIDs))
|
||||
for _, id := range deviceIDs {
|
||||
keys = append(keys, devicePresenceKey(id))
|
||||
}
|
||||
|
||||
values, err := c.client.MGet(ctx, keys...).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for index, id := range deviceIDs {
|
||||
statuses[id] = values[index] != nil
|
||||
}
|
||||
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func (c *RedisClient) PublishRealtime(ctx context.Context, channel string, payload []byte) error {
|
||||
if !c.available() {
|
||||
return redis.ErrClosed
|
||||
}
|
||||
return c.client.Publish(ctx, channel, payload).Err()
|
||||
}
|
||||
|
||||
func (c *RedisClient) SubscribeRealtime(ctx context.Context, channels ...string) (<-chan RealtimeMessage, error) {
|
||||
if !c.available() {
|
||||
return nil, redis.ErrClosed
|
||||
}
|
||||
|
||||
pubsub := c.client.Subscribe(ctx, channels...)
|
||||
if _, err := pubsub.Receive(ctx); err != nil {
|
||||
_ = pubsub.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
source := pubsub.Channel()
|
||||
messages := make(chan RealtimeMessage, 64)
|
||||
|
||||
go func() {
|
||||
defer close(messages)
|
||||
defer pubsub.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case message, ok := <-source:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
messages <- RealtimeMessage{
|
||||
Channel: message.Channel,
|
||||
Payload: []byte(message.Payload),
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func (c *RedisClient) Close() error {
|
||||
if !c.available() {
|
||||
return nil
|
||||
}
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
func (c *RedisClient) available() bool {
|
||||
return c != nil && c.client != nil
|
||||
}
|
||||
|
||||
func adminSessionKey(token string) string {
|
||||
return fmt.Sprintf("filefast:admin:session:%s", token)
|
||||
}
|
||||
|
||||
func devicePresenceKey(deviceID string) string {
|
||||
return fmt.Sprintf("filefast:device:online:%s", deviceID)
|
||||
}
|
||||
|
||||
func deviceSessionKey(deviceID string) string {
|
||||
return fmt.Sprintf("filefast:device:session:%s", deviceID)
|
||||
}
|
||||
Reference in New Issue
Block a user