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