1
+
1
2
"""
2
3
A module to manage user sessions.
3
4
4
- This module provides classes and functions to manage user sessions, storing the data in Redis.
5
+ This module provides classes and functions to manage user sessions. It now supports storing the data in Redis,
6
+ and falls back to file-based storage when REDIS_URL is not provided.
5
7
"""
6
8
7
9
from __future__ import annotations
8
10
import json
9
11
import logging
10
12
import random
11
13
import string
12
-
13
- from os import getenv
14
+ import os
15
+ from typing import Optional
14
16
15
17
import bottle
16
18
from swa .spotifyoauthredis import access_token
17
19
from swa .utils import redis_client , redis_session_data_key
18
20
19
- COOKIE_SECRET = str (getenv ("SPOTIPY_CLIENT_SECRET" , "default" ))
21
+ COOKIE_SECRET = str (os .getenv ("SPOTIPY_CLIENT_SECRET" , "default" ))
22
+
23
+ # File-based storage directory
24
+ FILE_STORAGE_PATH = './session_data'
25
+
26
+
27
+ def is_redis_enabled () -> bool :
28
+ """
29
+ Check if Redis is enabled by looking for REDIS_URL.
30
+ Returns True if REDIS_URL is set, False otherwise.
31
+ """
32
+ return 'REDIS_URL' in os .environ
33
+
34
+
35
+ def get_file_storage_path (session_id : str ) -> str :
36
+ return os .path .join (FILE_STORAGE_PATH , f'{ session_id } .json' )
20
37
21
38
22
39
class SessionData :
@@ -76,16 +93,15 @@ def to_json(self) -> str:
76
93
77
94
78
95
def session_start () -> str :
79
- """
80
- Starts a session, setting the data in Redis.
96
+ session_id = '' .join (random .choices (
97
+ string .ascii_letters + string .digits , k = 16 ))
98
+ bottle .response .set_cookie ('SID' , session_id , secret = COOKIE_SECRET )
81
99
82
- :return: The session ID for the new session.
83
- """
84
- session_id = '' .join (random .choices (string .ascii_letters + string .digits , k = 16 ))
85
- bottle .response .set_cookie ('SID' , session_id , secret = COOKIE_SECRET , path = '/' , httponly = True )
86
- redis_key = redis_session_data_key (session_id )
87
- redis_data = SessionData ({'id' : session_id }).to_json ()
88
- redis_client ().set (name = redis_key , value = redis_data )
100
+ # Initialize empty session data
101
+ if not is_redis_enabled ():
102
+ os .makedirs (FILE_STORAGE_PATH , exist_ok = True )
103
+ with open (get_file_storage_path (session_id ), 'w' ) as file :
104
+ json .dump ({}, file )
89
105
90
106
return session_id
91
107
@@ -97,62 +113,51 @@ def session_get_id(auto_start: bool = True) -> str | None:
97
113
:param auto_start: If True (the default), a new session will be started if necesary.
98
114
:return: The session ID or None if no session is active and `auto_start` is False.
99
115
"""
100
- session_id = bottle .request .get_cookie ('SID' )
116
+ session_id = bottle .request .get_cookie ('SID' , secret = COOKIE_SECRET )
101
117
if session_id is None :
102
118
return session_start () if auto_start else None
103
119
104
120
return str (session_id )
105
121
106
122
107
- def session_get_data (session_id = None ) -> SessionData :
108
- """
109
- Returns the SessionData instance for the current session or the given session ID.
110
-
111
- :param session_id: The session ID to get the data for.
112
- If not provided, the current session ID will be used.
113
-
114
- :return: The `SessionData` instance for the current or given session.
115
- :raises RuntimeError: If no session is active and no `session_id` is provided.
116
- """
123
+ def session_get_data (session_id : str = None ) -> SessionData :
117
124
if not session_id :
118
125
session_id = session_get_id (auto_start = False )
119
126
120
127
if not session_id :
121
128
raise RuntimeError ('No valid session and no session_id provided!' )
122
129
123
- redis_data = redis_client ().get (redis_session_data_key (session_id ))
124
- logging .debug ("Session data: " , end = " " )
125
- logging .debug ({session_id : redis_data })
126
- if redis_data :
127
- return SessionData .from_json (redis_data )
130
+ if is_redis_enabled ():
131
+ redis_data = redis_client ().get (redis_session_data_key (session_id ))
132
+ if redis_data :
133
+ return SessionData .from_json (redis_data )
134
+ else :
135
+ try :
136
+ with open (get_file_storage_path (session_id ), 'r' ) as file :
137
+ return SessionData (json .load (file ))
138
+ except FileNotFoundError :
139
+ return SessionData ()
128
140
129
141
return SessionData ()
130
142
131
- def session_set_data (data : SessionData , session_id : str = None ) -> bool :
132
- """
133
- Sets the session data for the current or given session.
134
-
135
- :param data: The `SessionData` instance to set for the session.
136
- :param session_id: The session ID to set the data for.
137
- If not provided, the current session ID will be used.
138
143
139
- :return: True if the data was successfully set, or False otherwise.
140
- :raises RuntimeError: If no session is active and no `session_id` is provided.
141
- """
144
+ def session_set_data (data : SessionData , session_id : str = None ) -> bool :
142
145
if not session_id :
143
146
session_id = session_get_id (False )
144
147
145
148
if not session_id :
146
149
raise RuntimeError ('No valid session and no session_id provided!' )
147
150
148
- redis_key = redis_session_data_key (session_id )
149
- redis_data = data .to_json ()
150
- logging .debug ("Set session data: " , end = " " )
151
- logging .debug ({session_id : redis_data })
152
- if not redis_client ().set (name = redis_key , value = redis_data ):
153
- return False
151
+ if is_redis_enabled ():
152
+ redis_key = redis_session_data_key (session_id )
153
+ redis_data = data .to_json ()
154
+ return redis_client ().set (name = redis_key , value = redis_data )
155
+ else :
156
+ os .makedirs (FILE_STORAGE_PATH , exist_ok = True )
157
+ with open (get_file_storage_path (session_id ), 'w' ) as file :
158
+ json .dump (data .all (), file )
159
+ return True
154
160
155
- return True
156
161
157
162
def session_get_oauth_token () -> tuple (SessionData , str ):
158
163
"""
0 commit comments