99from settings import SettingsManager # pyright: ignore[reportMissingImports]
1010
1111import decky
12+ from utils import ensure_pyftpdlib , get_local_ip
1213
1314if TYPE_CHECKING :
1415 from pyftpdlib .servers import FTPServer
1516
1617PY_MODULES_DIR = os .path .join (os .path .dirname (os .path .realpath (__file__ )), "py_modules" )
1718
1819
19- def _ensure_pyftpdlib () -> None :
20- if PY_MODULES_DIR not in sys .path :
21- sys .path .insert (0 , PY_MODULES_DIR )
22-
23-
24- def _get_local_ip () -> str :
25- try :
26- s = socket .socket (socket .AF_INET , socket .SOCK_DGRAM )
27- s .connect (("8.8.8.8" , 80 ))
28- ip = s .getsockname ()[0 ]
29- s .close ()
30- return ip
31- except Exception :
32- return "Unknown"
33-
34-
3520class Plugin :
3621 _server : "FTPServer | None" = None
3722 _server_thread = None
@@ -48,7 +33,7 @@ class Plugin:
4833
4934 async def _main (self ):
5035 self ._loop = asyncio .get_running_loop ()
51- _ensure_pyftpdlib ()
36+ ensure_pyftpdlib ()
5237
5338 settings = SettingsManager (
5439 name = "settings" ,
@@ -79,7 +64,7 @@ async def _emit_status(self):
7964 "ftpd_status" ,
8065 {
8166 "running" : self ._running ,
82- "ip" : _get_local_ip () if self ._running else "" ,
67+ "ip" : get_local_ip () if self ._running else "" ,
8368 "port" : self ._get ("port" ),
8469 "root" : self ._get ("root_dir" ),
8570 },
@@ -92,7 +77,7 @@ async def start_server(self) -> dict:
9277 return {"success" : True , "already" : True }
9378
9479 try :
95- _ensure_pyftpdlib ()
80+ ensure_pyftpdlib ()
9681 from pyftpdlib .authorizers import DummyAuthorizer
9782 from pyftpdlib .handlers import FTPHandler
9883 from pyftpdlib .servers import FTPServer
@@ -139,9 +124,16 @@ def _serve():
139124
140125 self ._server_thread = threading .Thread (target = _serve , daemon = True )
141126 self ._server_thread .start ()
127+
128+ self ._server_thread .join (timeout = 0.2 )
129+ if not self ._server_thread .is_alive ():
130+ return {
131+ "success" : False ,
132+ "error" : "Server failed to start (check port availability)." ,
133+ }
134+
142135 self ._running = True
143136 await self ._emit_status ()
144-
145137 return {"success" : True }
146138
147139 except Exception as exc :
@@ -169,7 +161,7 @@ async def stop_server(self) -> dict:
169161 async def get_status (self ) -> dict :
170162 return {
171163 "running" : self ._running ,
172- "ip" : _get_local_ip () if self ._running else "" ,
164+ "ip" : get_local_ip () if self ._running else "" ,
173165 "port" : self ._get ("port" ),
174166 "root" : self ._get ("root_dir" ),
175167 }
@@ -185,19 +177,36 @@ async def save_settings(self, new_settings: dict) -> dict:
185177 try :
186178 assert self ._settings is not None
187179
188- port = int (new_settings .get ("port" , self .DEFAULTS ["port" ]))
189- root = str (new_settings .get ("root_dir" , self .DEFAULTS ["root_dir" ]))
180+ port = int (new_settings .get ("port" , self ._get ("port" )))
181+ root = str (new_settings .get ("root_dir" , self ._get ("root_dir" )))
182+ p_start = int (
183+ new_settings .get ("passive_port_start" , self ._get ("passive_port_start" ))
184+ )
185+ p_end = int (
186+ new_settings .get ("passive_port_end" , self ._get ("passive_port_end" ))
187+ )
190188
191189 if not (1024 <= port <= 65535 ):
192190 return {"success" : False , "error" : "Port must be 1024–65535." }
193191 if not root .startswith ("/" ):
192+ return {"success" : False , "error" : "Root must be an absolute path." }
193+ if not (1024 <= p_start <= 65535 and 1024 <= p_end <= 65535 ):
194+ return {"success" : False , "error" : "Passive ports must be 1024–65535." }
195+ if p_end <= p_start :
196+ return {
197+ "success" : False ,
198+ "error" : "Passive end must be greater than start." ,
199+ }
200+ if p_start <= port <= p_end :
194201 return {
195202 "success" : False ,
196- "error" : "Root must be an absolute path ." ,
203+ "error" : "Control port must not sit inside the passive range ." ,
197204 }
198205
199206 self ._settings .setSetting ("port" , port )
200207 self ._settings .setSetting ("root_dir" , root )
208+ self ._settings .setSetting ("passive_port_start" , p_start )
209+ self ._settings .setSetting ("passive_port_end" , p_end )
201210 self ._settings .commit ()
202211
203212 restarted = False
@@ -210,6 +219,8 @@ async def save_settings(self, new_settings: dict) -> dict:
210219 "error" : f"Saved, but restart failed: { res .get ('error' )} " ,
211220 }
212221 restarted = True
222+ else :
223+ await self ._emit_status ()
213224
214225 return {"success" : True , "restarted" : restarted }
215226 except Exception as exc :
0 commit comments