Elliot223 commited on
Commit
e318c1f
·
0 Parent(s):

Initial Brain3: SFTP Tunnel

Browse files
Files changed (5) hide show
  1. Dockerfile +26 -0
  2. README.md +14 -0
  3. app.py +52 -0
  4. azure_bridge.py +111 -0
  5. requirements.txt +4 -0
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Brain3 Dockerfile: SFTP Tunnel Edition
2
+ FROM python:3.10
3
+
4
+ # Create user first
5
+ RUN useradd -m -u 1000 user
6
+
7
+ # Install Cloudflared (No apt-get needed, just curl the binary)
8
+ RUN curl -L --output /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 && \
9
+ chmod +x /usr/local/bin/cloudflared
10
+
11
+ # Setup workdir
12
+ WORKDIR /app
13
+
14
+ # Install deps
15
+ COPY --chown=user ./requirements.txt requirements.txt
16
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
17
+
18
+ # Copy App
19
+ COPY --chown=user . /app
20
+
21
+ # Switch to user
22
+ USER user
23
+ ENV PATH="/home/user/.local/bin:$PATH"
24
+
25
+ # Run App
26
+ CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Brain3 SFTP Bridge
3
+ emoji: 🌉
4
+ colorFrom: indigo
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ app_port: 7860
9
+ ---
10
+
11
+ # Brain3: SFTP Tunnel Bridge
12
+
13
+ Uses `cloudflared` in TCP mode + `paramiko` to access Azure Storage without FUSE/SSHFS.
14
+ User-space only. No root required.
app.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from azure_bridge import AzureSFTPBridge
3
+ import os
4
+ import time
5
+
6
+ app = FastAPI()
7
+ bridge = None
8
+
9
+ @app.on_event("startup")
10
+ def startup_event():
11
+ global bridge
12
+ print("🚀 Starting Brain3...")
13
+ bridge = AzureSFTPBridge()
14
+ if os.environ.get("SSH_KEY"):
15
+ bridge.start_tunnel()
16
+ else:
17
+ print("⚠️ SSH_KEY missing. Bridge disabled.")
18
+
19
+ @app.get("/")
20
+ def home():
21
+ return {"status": "Brain3 Online", "mode": "SFTP Tunnel"}
22
+
23
+ @app.get("/test-storage")
24
+ def test_storage():
25
+ """Writes a test file to Azure and lists directory."""
26
+ global bridge
27
+ if not bridge:
28
+ return {"error": "Bridge not initialized"}
29
+
30
+ try:
31
+ # 1. Create dummy file
32
+ filename = f"brain3_probe_{int(time.time())}.txt"
33
+ with open(filename, "w") as f:
34
+ f.write(f"Hello from Brain3 via SFTP at {time.time()}")
35
+
36
+ # 2. Upload
37
+ remote_path = f"/mnt/lightrag/{filename}" # Assuming user has write access here
38
+ # Or try home dir if unsure:
39
+ # remote_path = f"/home/azureuser/{filename}"
40
+
41
+ bridge.upload_file(filename, remote_path)
42
+
43
+ # 3. List
44
+ files = bridge.list_files("/mnt/lightrag")
45
+
46
+ return {
47
+ "upload_status": "success",
48
+ "remote_path": remote_path,
49
+ "directory_listing": files
50
+ }
51
+ except Exception as e:
52
+ return {"error": str(e)}
azure_bridge.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import subprocess
4
+ import socket
5
+ import threading
6
+ import paramiko
7
+ import io
8
+ import logging
9
+
10
+ # Configure Logging
11
+ logging.basicConfig(level=logging.INFO)
12
+ logger = logging.getLogger("azure_bridge")
13
+
14
+ class AzureSFTPBridge:
15
+ def __init__(self, hostname="ssh.my-robot.dev", local_port=2222, username="azureuser"):
16
+ self.hostname = hostname
17
+ self.local_port = local_port
18
+ self.username = username
19
+ self.tunnel_process = None
20
+ self.ssh_key = None
21
+ self._load_key()
22
+
23
+ def _load_key(self):
24
+ """Load SSH Key from Env Var"""
25
+ key_content = os.environ.get("SSH_KEY")
26
+ if not key_content:
27
+ logger.error("SSH_KEY environment variable not found!")
28
+ return
29
+
30
+ # Determine key type and load
31
+ try:
32
+ self.ssh_key = paramiko.RSAKey.from_private_key(io.StringIO(key_content))
33
+ logger.info("Loaded RSA Key successfully.")
34
+ except Exception:
35
+ try:
36
+ self.ssh_key = paramiko.Ed25519Key.from_private_key(io.StringIO(key_content))
37
+ logger.info("Loaded Ed25519 Key successfully.")
38
+ except Exception as e:
39
+ logger.error(f"Failed to load SSH Key: {e}")
40
+
41
+ def start_tunnel(self):
42
+ """Starts cloudflared access tcp in background."""
43
+ logger.info(f"Starting Cloudflare Tunnel to {self.hostname} on localhost:{self.local_port}...")
44
+
45
+ cmd = [
46
+ "cloudflared",
47
+ "access", "tcp",
48
+ "--hostname", self.hostname,
49
+ "--url", f"tcp://localhost:{self.local_port}"
50
+ ]
51
+
52
+ self.tunnel_process = subprocess.Popen(
53
+ cmd,
54
+ stdout=subprocess.PIPE,
55
+ stderr=subprocess.PIPE
56
+ )
57
+
58
+ # Wait for port to be open
59
+ retries = 10
60
+ for i in range(retries):
61
+ if self._check_port():
62
+ logger.info("Tunnel established successfully.")
63
+ return True
64
+ time.sleep(1)
65
+
66
+ logger.error("Failed to establish tunnel.")
67
+ return False
68
+
69
+ def _check_port(self):
70
+ try:
71
+ with socket.create_connection(("localhost", self.local_port), timeout=1):
72
+ return True
73
+ except (socket.timeout, ConnectionRefusedError):
74
+ return False
75
+
76
+ def get_sftp(self):
77
+ """Connects via Paramiko and returns SFTPClient."""
78
+ if not self.ssh_key:
79
+ raise ValueError("SSH Key not loaded.")
80
+
81
+ if not self._check_port():
82
+ logger.warning("Tunnel port closed. Restarting tunnel...")
83
+ self.start_tunnel()
84
+
85
+ try:
86
+ logger.info("Connecting to SFTP via Tunnel...")
87
+ transport = paramiko.Transport(("localhost", self.local_port))
88
+ transport.connect(username=self.username, pkey=self.ssh_key)
89
+
90
+ sftp = paramiko.SFTPClient.from_transport(transport)
91
+ logger.info("SFTP Connection Established.")
92
+ return sftp
93
+ except Exception as e:
94
+ logger.error(f"SFTP Connection Failed: {e}")
95
+ raise
96
+
97
+ # --- High Level API ---
98
+
99
+ def list_files(self, remote_path):
100
+ with self.get_sftp() as sftp:
101
+ return sftp.listdir(remote_path)
102
+
103
+ def upload_file(self, local_path, remote_path):
104
+ with self.get_sftp() as sftp:
105
+ sftp.put(local_path, remote_path)
106
+ logger.info(f"Uploaded {local_path} -> {remote_path}")
107
+
108
+ def download_file(self, remote_path, local_path):
109
+ with self.get_sftp() as sftp:
110
+ sftp.get(remote_path, local_path)
111
+ logger.info(f"Downloaded {remote_path} -> {local_path}")
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ paramiko
4
+ requests