|
| 1 | +""" |
| 2 | +FTP server for camera feed: accepts FTP connections, |
| 3 | +authenticates a single user, and stores uploaded files in /app/images. |
| 4 | +""" |
| 5 | + |
| 6 | +import os |
| 7 | +import logging |
| 8 | +from pyftpdlib.authorizers import DummyAuthorizer |
| 9 | +from pyftpdlib.handlers import FTPHandler |
| 10 | +from pyftpdlib.servers import FTPServer |
| 11 | + |
| 12 | +# Configuration Paths |
| 13 | +SAVE_DIR = "/app/images" |
| 14 | +LOG_DIR = "/app/logs" |
| 15 | +os.makedirs(LOG_DIR, exist_ok=True) |
| 16 | +os.makedirs(SAVE_DIR, exist_ok=True) |
| 17 | +LOG_FILE = os.path.join(LOG_DIR, "ftp_server.log") |
| 18 | + |
| 19 | +# Setup Logging & Folders |
| 20 | +logging.basicConfig( |
| 21 | + level=logging.INFO, |
| 22 | + format="%(asctime)s %(levelname)s %(message)s", |
| 23 | + handlers=[ |
| 24 | + logging.FileHandler(LOG_FILE), |
| 25 | + logging.StreamHandler() |
| 26 | + ] |
| 27 | +) |
| 28 | +logger = logging.getLogger(__name__) |
| 29 | + |
| 30 | +FTP_USER = os.getenv("FTP_USER", "camera") |
| 31 | +FTP_PASS = os.getenv("FTP_PASS") |
| 32 | +FTP_PORT = 21 |
| 33 | + |
| 34 | +class CustomFTPHandler(FTPHandler): |
| 35 | + permit_foreign_addresses = True |
| 36 | + |
| 37 | + def on_connect(self): |
| 38 | + logger.info(f"New connection from {self.remote_ip}:{self.remote_port}") |
| 39 | + |
| 40 | + def on_disconnect(self): |
| 41 | + logger.info(f"Disconnected: {self.remote_ip}") |
| 42 | + |
| 43 | + def on_login(self, username): |
| 44 | + logger.info(f"User logged in: {username} from {self.remote_ip}") |
| 45 | + |
| 46 | + def on_login_failed(self, username, password): |
| 47 | + logger.warning(f"Failed login attempt: username={username} ip={self.remote_ip}") |
| 48 | + |
| 49 | + def on_file_received(self, file_path): |
| 50 | + logger.info(f"File received and saved: {file_path}") |
| 51 | + |
| 52 | + def on_incomplete_file_received(self, file_path): |
| 53 | + logger.warning(f"Incomplete file received (removed): {file_path}") |
| 54 | + |
| 55 | + |
| 56 | +def main(): |
| 57 | + authorizer = DummyAuthorizer() |
| 58 | + authorizer.add_user(FTP_USER, FTP_PASS, SAVE_DIR, perm="elradfmw") |
| 59 | + |
| 60 | + handler = CustomFTPHandler |
| 61 | + handler.authorizer = authorizer |
| 62 | + handler.banner = "Reolink FTP server ready." |
| 63 | + |
| 64 | + address = ("0.0.0.0", FTP_PORT) |
| 65 | + server = FTPServer(address, handler) |
| 66 | + |
| 67 | + # Connection limits |
| 68 | + server.max_cons = 256 |
| 69 | + server.max_cons_per_ip = 10 |
| 70 | + |
| 71 | + logger.info(f"FTP server listening on 0.0.0.0:{FTP_PORT}, saving to {SAVE_DIR}") |
| 72 | + server.serve_forever() |
| 73 | + |
| 74 | + |
| 75 | +if __name__ == "__main__": |
| 76 | + main() |
0 commit comments