1515package push
1616
1717import (
18+ "encoding/json"
1819 "net/http"
20+ "os"
21+ "sync"
1922
2023 _ "github.com/blinklabs-io/adder/docs"
24+ "github.com/blinklabs-io/adder/internal/logging"
2125 "github.com/gin-gonic/gin"
2226)
2327
2428type TokenStore struct {
25- FCMTokens map [string ]string
29+ FCMTokens map [string ]string `json:"fcm_tokens"`
30+ filePath string
31+ mu sync.RWMutex
32+ persistMutex sync.Mutex
2633}
2734
2835// TokenRequest represents a request containing an FCM token.
@@ -43,23 +50,116 @@ type ErrorResponse struct {
4350 Error string `json:"error"`
4451}
4552
46- // TODO add support for persistence (#335)
4753var fcmStore * TokenStore
4854
4955func init () {
50- fcmStore = newTokenStore ()
56+ fcmStore = newTokenStore ("" )
5157}
5258
53- func newTokenStore () * TokenStore {
54- return & TokenStore {
59+ func newTokenStore (filePath string ) * TokenStore {
60+ store := & TokenStore {
5561 FCMTokens : make (map [string ]string ),
62+ filePath : filePath ,
63+ }
64+ // Load existing tokens if persistence is enabled
65+ if filePath != "" {
66+ store .loadTokens ()
67+ }
68+ return store
69+ }
70+
71+ // SetPersistenceFile configures the file path for token persistence
72+ // If called with a non-empty path, tokens will be loaded from and saved to this file
73+ func SetPersistenceFile (filePath string ) {
74+ if fcmStore == nil {
75+ fcmStore = newTokenStore (filePath )
76+ return
77+ }
78+ fcmStore .persistMutex .Lock ()
79+ fcmStore .filePath = filePath
80+ fcmStore .persistMutex .Unlock ()
81+ if filePath != "" {
82+ fcmStore .loadTokens ()
5683 }
5784}
5885
5986func getTokenStore () * TokenStore {
6087 return fcmStore
6188}
6289
90+ // loadTokens loads tokens from the persistence file
91+ func (s * TokenStore ) loadTokens () {
92+ s .persistMutex .Lock ()
93+ filePath := s .filePath
94+ s .persistMutex .Unlock ()
95+
96+ if filePath == "" {
97+ return
98+ }
99+
100+ logger := logging .GetLogger ()
101+
102+ data , err := os .ReadFile (filePath )
103+ if err != nil {
104+ if os .IsNotExist (err ) {
105+ // File doesn't exist yet, that's fine for first run
106+ logger .Debug ("FCM token persistence file does not exist yet" , "path" , filePath )
107+ return
108+ }
109+ logger .Error ("failed to read FCM tokens from file" , "error" , err , "path" , filePath )
110+ return
111+ }
112+
113+ var loadedStore struct {
114+ FCMTokens map [string ]string `json:"fcm_tokens"`
115+ }
116+ if err := json .Unmarshal (data , & loadedStore ); err != nil {
117+ logger .Error ("failed to parse FCM tokens from file" , "error" , err , "path" , filePath )
118+ return
119+ }
120+
121+ s .mu .Lock ()
122+ if loadedStore .FCMTokens != nil {
123+ s .FCMTokens = loadedStore .FCMTokens
124+ }
125+ s .mu .Unlock ()
126+
127+ logger .Info ("loaded FCM tokens from persistence file" , "count" , len (loadedStore .FCMTokens ), "path" , filePath )
128+ }
129+
130+ // saveTokens saves tokens to the persistence file
131+ func (s * TokenStore ) saveTokens () {
132+ s .persistMutex .Lock ()
133+ filePath := s .filePath
134+ s .persistMutex .Unlock ()
135+
136+ if filePath == "" {
137+ return
138+ }
139+
140+ logger := logging .GetLogger ()
141+
142+ s .mu .RLock ()
143+ data , err := json .MarshalIndent (struct {
144+ FCMTokens map [string ]string `json:"fcm_tokens"`
145+ }{
146+ FCMTokens : s .FCMTokens ,
147+ }, "" , " " )
148+ s .mu .RUnlock ()
149+
150+ if err != nil {
151+ logger .Error ("failed to marshal FCM tokens" , "error" , err )
152+ return
153+ }
154+
155+ if err := os .WriteFile (filePath , data , 0o600 ); err != nil {
156+ logger .Error ("failed to write FCM tokens to file" , "error" , err , "path" , filePath )
157+ return
158+ }
159+
160+ logger .Debug ("saved FCM tokens to persistence file" , "path" , filePath )
161+ }
162+
63163// @Summary Store FCM Token
64164// @Description Store a new FCM token
65165// @Accept json
@@ -84,7 +184,10 @@ func storeFCMToken(c *gin.Context) {
84184 )
85185 return
86186 }
187+ store .mu .Lock ()
87188 store .FCMTokens [req .FCMToken ] = req .FCMToken
189+ store .mu .Unlock ()
190+ store .saveTokens ()
88191 c .Status (http .StatusCreated )
89192}
90193
@@ -106,7 +209,9 @@ func readFCMToken(c *gin.Context) {
106209 )
107210 return
108211 }
212+ store .mu .RLock ()
109213 storedToken , exists := store .FCMTokens [token ]
214+ store .mu .RUnlock ()
110215 if ! exists {
111216 c .Status (http .StatusNotFound )
112217 return
@@ -132,20 +237,32 @@ func deleteFCMToken(c *gin.Context) {
132237 )
133238 return
134239 }
240+ store .mu .Lock ()
135241 _ , exists := store .FCMTokens [token ]
136242 if exists {
137243 delete (store .FCMTokens , token )
244+ }
245+ store .mu .Unlock ()
246+ if exists {
247+ store .saveTokens ()
138248 c .Status (http .StatusNoContent )
139249 } else {
140250 c .Status (http .StatusNotFound )
141251 }
142252}
143253
144- // GetFcmTokens returns the current in-memory FCM tokens
254+ // GetFcmTokens returns a copy of the current in-memory FCM tokens
145255func GetFcmTokens () map [string ]string {
146256 store := getTokenStore ()
147257 if store == nil {
148258 return make (map [string ]string )
149259 }
150- return store .FCMTokens
260+ store .mu .RLock ()
261+ defer store .mu .RUnlock ()
262+ // Return a copy to avoid race conditions
263+ tokens := make (map [string ]string , len (store .FCMTokens ))
264+ for k , v := range store .FCMTokens {
265+ tokens [k ] = v
266+ }
267+ return tokens
151268}
0 commit comments