Avinash commited on
Commit
2e5ad46
·
1 Parent(s): 1561669

SFTP: serve PDFs + store configs in /inserio

Browse files
Files changed (3) hide show
  1. backend/api.py +23 -5
  2. backend/sftp_store.py +113 -0
  3. requirements.txt +2 -0
backend/api.py CHANGED
@@ -9,6 +9,7 @@ from dotenv import load_dotenv
9
  from fastapi import FastAPI, HTTPException
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from fastapi.responses import FileResponse
 
12
 
13
 
14
  # ------------------------------------------------------------------
@@ -102,9 +103,16 @@ def root() -> Dict[str, str]:
102
 
103
  @app.get("/api/pdf/{pdf_id}")
104
  def get_pdf(pdf_id: str):
105
- pdf_path = UPLOADS_DIR / pdf_id
106
- if not pdf_path.exists():
 
107
  raise HTTPException(status_code=404, detail="PDF not found")
 
 
 
 
 
 
108
 
109
  return FileResponse(
110
  path=pdf_path,
@@ -115,9 +123,19 @@ def get_pdf(pdf_id: str):
115
 
116
  @app.post("/api/send-config")
117
  def send_config(payload: Dict[str, Any]):
118
- out_path = BACKEND_DIR / "trainer_config.json"
119
- out_path.write_text(json.dumps(payload, indent=2))
120
- return {"ok": True, "written_to": str(out_path)}
 
 
 
 
 
 
 
 
 
 
121
 
122
 
123
  @app.post("/api/notify-unknown")
 
9
  from fastapi import FastAPI, HTTPException
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from fastapi.responses import FileResponse
12
+ from backend.sftp_store import SFTPStore
13
 
14
 
15
  # ------------------------------------------------------------------
 
103
 
104
  @app.get("/api/pdf/{pdf_id}")
105
  def get_pdf(pdf_id: str):
106
+ store = SFTPStore()
107
+ remote_rel = f"pdfs/{pdf_id}.pdf"
108
+ if not store.exists(remote_rel):
109
  raise HTTPException(status_code=404, detail="PDF not found")
110
+ data = store.get_bytes(remote_rel)
111
+
112
+ # Return bytes directly (no local disk dependency)
113
+ from fastapi.responses import Response
114
+ return Response(content=data, media_type="application/pdf")
115
+
116
 
117
  return FileResponse(
118
  path=pdf_path,
 
123
 
124
  @app.post("/api/send-config")
125
  def send_config(payload: Dict[str, Any]):
126
+ # Expect payload to include at least: pdf_id, template_id, config
127
+ pdf_id = str(payload.get("pdf_id", "")).strip()
128
+ template_id = str(payload.get("template_id", "")).strip()
129
+ config = payload.get("config")
130
+
131
+ if not pdf_id or not template_id or config is None:
132
+ raise HTTPException(status_code=400, detail="Missing pdf_id/template_id/config")
133
+
134
+ store = SFTPStore()
135
+ remote_json = f"configs/{pdf_id}_{template_id}.json"
136
+ store.put_bytes(remote_json, json.dumps(payload, indent=2).encode("utf-8"))
137
+
138
+ return {"ok": True, "stored": remote_json}
139
 
140
 
141
  @app.post("/api/notify-unknown")
backend/sftp_store.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import posixpath
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ import paramiko
7
+
8
+
9
+ @dataclass
10
+ class SFTPConfig:
11
+ host: str
12
+ port: int
13
+ user: str
14
+ password: str
15
+ root: str
16
+
17
+
18
+ def _cfg() -> SFTPConfig:
19
+ host = os.getenv("SFTP_HOST", "").strip()
20
+ port = int(os.getenv("SFTP_PORT", "2222"))
21
+ user = os.getenv("SFTP_USER", "").strip()
22
+ password = os.getenv("SFTP_PASS", "")
23
+ root = os.getenv("SFTP_ROOT", "/inserio").strip() or "/inserio"
24
+
25
+ if not host or not user or not password:
26
+ raise RuntimeError("Missing SFTP_HOST/SFTP_USER/SFTP_PASS")
27
+
28
+ # normalize root to POSIX
29
+ if not root.startswith("/"):
30
+ root = "/" + root
31
+ return SFTPConfig(host=host, port=port, user=user, password=password, root=root)
32
+
33
+
34
+ class SFTPStore:
35
+ def __init__(self, cfg: Optional[SFTPConfig] = None):
36
+ self.cfg = cfg or _cfg()
37
+
38
+ def _connect(self):
39
+ t = paramiko.Transport((self.cfg.host, self.cfg.port))
40
+ t.connect(username=self.cfg.user, password=self.cfg.password)
41
+ sftp = paramiko.SFTPClient.from_transport(t)
42
+ return t, sftp
43
+
44
+ def _abs(self, rel: str) -> str:
45
+ rel = rel.lstrip("/")
46
+ return posixpath.join(self.cfg.root, rel)
47
+
48
+ def mkdir_p(self, remote_dir_rel: str) -> None:
49
+ t, sftp = self._connect()
50
+ try:
51
+ path = self._abs(remote_dir_rel).rstrip("/")
52
+ parts = path.split("/")
53
+ cur = ""
54
+ for part in parts:
55
+ if not part:
56
+ continue
57
+ cur = cur + "/" + part
58
+ try:
59
+ sftp.stat(cur)
60
+ except FileNotFoundError:
61
+ sftp.mkdir(cur)
62
+ finally:
63
+ sftp.close()
64
+ t.close()
65
+
66
+ def put_bytes(self, remote_path_rel: str, data: bytes) -> None:
67
+ remote_abs = self._abs(remote_path_rel)
68
+ remote_dir = posixpath.dirname(remote_abs)
69
+
70
+ t, sftp = self._connect()
71
+ try:
72
+ # mkdir -p
73
+ parts = remote_dir.split("/")
74
+ cur = ""
75
+ for part in parts:
76
+ if not part:
77
+ continue
78
+ cur = cur + "/" + part
79
+ try:
80
+ sftp.stat(cur)
81
+ except FileNotFoundError:
82
+ sftp.mkdir(cur)
83
+
84
+ tmp = remote_abs + ".tmp"
85
+ with sftp.file(tmp, "wb") as f:
86
+ f.write(data)
87
+ f.flush()
88
+ sftp.rename(tmp, remote_abs)
89
+ finally:
90
+ sftp.close()
91
+ t.close()
92
+
93
+ def get_bytes(self, remote_path_rel: str) -> bytes:
94
+ remote_abs = self._abs(remote_path_rel)
95
+ t, sftp = self._connect()
96
+ try:
97
+ with sftp.file(remote_abs, "rb") as f:
98
+ return f.read()
99
+ finally:
100
+ sftp.close()
101
+ t.close()
102
+
103
+ def exists(self, remote_path_rel: str) -> bool:
104
+ remote_abs = self._abs(remote_path_rel)
105
+ t, sftp = self._connect()
106
+ try:
107
+ sftp.stat(remote_abs)
108
+ return True
109
+ except FileNotFoundError:
110
+ return False
111
+ finally:
112
+ sftp.close()
113
+ t.close()
requirements.txt CHANGED
@@ -4,3 +4,5 @@ python-dotenv
4
  google-auth
5
  google-auth-oauthlib
6
  google-api-python-client
 
 
 
4
  google-auth
5
  google-auth-oauthlib
6
  google-api-python-client
7
+
8
+ paramiko