CORVO-AI commited on
Commit
7289b1b
·
verified ·
1 Parent(s): b55ae70

Update file-manager/app.py

Browse files
Files changed (1) hide show
  1. file-manager/app.py +151 -63
file-manager/app.py CHANGED
@@ -1,5 +1,5 @@
1
- import os, shutil, html, urllib.parse, subprocess
2
- from flask import Flask, request, redirect, send_file, Response
3
  from werkzeug.middleware.dispatcher import DispatcherMiddleware
4
  from werkzeug.serving import run_simple
5
 
@@ -9,8 +9,7 @@ app.config["APPLICATION_ROOT"] = "/fm"
9
  SECRET_HINTS = ("TOKEN", "KEY", "SECRET", "PASSWORD", "AUTH", "COOKIE")
10
 
11
  def is_sensitive(name):
12
- up = name.upper()
13
- return any(h in up for h in SECRET_HINTS)
14
 
15
  def mask_secrets(text):
16
  out = []
@@ -21,6 +20,52 @@ def mask_secrets(text):
21
  out.append(line)
22
  return "\n".join(out)
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  PAGE = """<!doctype html><html><head><title>Box</title>
25
  <style>
26
  body{{font-family:monospace;margin:20px;background:#0b0b0b;color:#eee}}
@@ -29,66 +74,112 @@ table{{border-collapse:collapse;width:100%}} td,th{{padding:4px 8px;border-botto
29
  .bar{{background:#1a1a1a;padding:10px;margin-bottom:10px;border-radius:6px}}
30
  .tabs a{{margin-right:14px;font-weight:bold}}
31
  textarea{{width:100%;background:#000;color:#47E6C1;border:1px solid #333;padding:8px}}
32
- pre{{background:#000;color:#78D64B;padding:12px;border-radius:6px;overflow:auto;max-height:60vh}}
33
- input[type=text],select{{background:#000;color:#eee;border:1px solid #333;padding:6px}}
34
- button,input[type=submit]{{background:#F46821;color:#fff;border:0;padding:6px 14px;border-radius:4px;cursor:pointer}}
35
  </style></head><body>
36
- <div class="tabs bar"><a href="/fm/">📁 Files</a><a href="/fm/term">🖥️ Terminal</a>
37
- <a href="/reload">♻️ Reload</a><a href="/gpuinfo">🎮 GPU</a>
38
- <span style="color:#777">test mode · storage is ephemeral</span></div>
39
  {body}</body></html>"""
40
 
41
- # ---------------- TERMINAL ----------------
42
- @app.route("/term", methods=["GET", "POST"])
43
- def term():
44
- cmd = ""
45
- cwd = request.form.get("cwd", "/") if request.method == "POST" else "/"
46
- output = ""
47
- if request.method == "POST":
48
- cmd = request.form.get("cmd", "")
49
- if cmd.strip().startswith("cd "):
50
- target = cmd.strip()[3:].strip()
51
- new = target if target.startswith("/") else os.path.abspath(os.path.join(cwd, target))
52
- if os.path.isdir(new):
53
- cwd, output = new, f"(cwd -> {new})"
54
- else:
55
- output = f"cd: no such directory: {new}"
56
- elif cmd.strip():
57
- try:
58
- r = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True,
59
- text=True, timeout=120)
60
- output = (r.stdout or "") + (r.stderr or "")
61
- if r.returncode != 0 and not output:
62
- output = f"[exit code {r.returncode}]"
63
- except subprocess.TimeoutExpired:
64
- output = "[timed out after 120s]"
65
- except Exception as e:
66
- output = f"[error: {e}]"
67
- output = mask_secrets(output)
 
 
 
 
 
 
68
 
69
- body = f"""
70
- <div class="bar"><b>cwd:</b> {html.escape(cwd)}</div>
71
- <form method="post">
72
- <input type="hidden" name="cwd" value="{html.escape(cwd)}">
73
- <input type="text" name="cmd" placeholder="ls -la, df -h, free -h, nproc, pip install ..."
74
- style="width:80%" autofocus value="{html.escape(cmd)}">
75
- <input type="submit" value="Run">
76
- </form>
77
- <div class="bar" style="margin-top:10px">Quick:
78
- <a href="#" onclick="q('ls -la')">ls -la</a> ·
79
- <a href="#" onclick="q('df -h')">df -h</a> ·
80
- <a href="#" onclick="q('free -h')">free -h</a> ·
81
- <a href="#" onclick="q('nproc')">nproc</a> ·
82
- <a href="#" onclick="q('cat /etc/os-release')">os-release</a>
83
- </div>
84
- <pre>{html.escape(output)}</pre>
 
 
 
 
 
 
 
 
85
  <script>
86
- function q(c){{document.querySelector('[name=cmd]').value=c;
87
- document.querySelector('form').submit();}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  </script>"""
89
  return Response(PAGE.format(body=body))
90
 
91
- # ---------------- FILE MANAGER ----------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  @app.route("/")
93
  def browse():
94
  path = os.path.abspath(request.args.get("path", "/"))
@@ -96,14 +187,12 @@ def browse():
96
  return Response(PAGE.format(body=f"<p>Not found: {html.escape(path)}</p>"), status=404)
97
  if os.path.isfile(path):
98
  return redirect(f"/fm/view?path={urllib.parse.quote(path)}")
99
-
100
  parent = os.path.dirname(path.rstrip("/")) or "/"
101
  rows = f'<tr><td><a href="/fm/?path={urllib.parse.quote(parent)}">.. (up)</a></td><td></td><td></td></tr>'
102
  try:
103
  entries = sorted(os.listdir(path))
104
  except PermissionError:
105
  return Response(PAGE.format(body=f"<p>Permission denied: {html.escape(path)}</p>"), status=403)
106
-
107
  for name in entries:
108
  full = os.path.join(path, name); q = urllib.parse.quote(full)
109
  try:
@@ -119,17 +208,16 @@ def browse():
119
  f'<a href="/fm/edit?path={q}">edit</a> | '
120
  f'<a href="/fm/delete?path={q}">delete</a>')
121
  rows += f"<tr><td>{link}</td><td>{size}</td><td>{actions}</td></tr>"
122
-
123
  body = f"""
124
  <div class="bar"><b>Path:</b> {html.escape(path)}</div>
125
  <div class="bar">
126
  <form action="/fm/mkdir" method="post" style="display:inline">
127
  <input type="hidden" name="path" value="{html.escape(path)}">
128
- <input name="name" placeholder="new folder"><input type="submit" value="Create folder">
129
  </form>
130
  <form action="/fm/upload" method="post" enctype="multipart/form-data" style="display:inline">
131
  <input type="hidden" name="path" value="{html.escape(path)}">
132
- <input type="file" name="file"><input type="submit" value="Upload">
133
  </form>
134
  </div>
135
  <table><tr><th>Name</th><th>Size</th><th>Actions</th></tr>{rows}</table>"""
@@ -169,7 +257,7 @@ def edit():
169
  <form action="/fm/save" method="post">
170
  <input type="hidden" name="path" value="{html.escape(path)}">
171
  <textarea name="content" style="height:60vh">{html.escape(content)}</textarea>
172
- <br><input type="submit" value="Save (live)">
173
  <a href="/fm/?path={urllib.parse.quote(os.path.dirname(path))}">cancel</a>
174
  </form>"""
175
  return Response(PAGE.format(body=body))
@@ -214,4 +302,4 @@ def delete():
214
 
215
  if __name__ == "__main__":
216
  wrapped = DispatcherMiddleware(Flask("empty"), {"/fm": app})
217
- run_simple("0.0.0.0", 9001, wrapped)
 
1
+ import os, shutil, html, urllib.parse, subprocess, threading, queue, sys, time, json
2
+ from flask import Flask, request, redirect, send_file, Response, stream_with_context
3
  from werkzeug.middleware.dispatcher import DispatcherMiddleware
4
  from werkzeug.serving import run_simple
5
 
 
9
  SECRET_HINTS = ("TOKEN", "KEY", "SECRET", "PASSWORD", "AUTH", "COOKIE")
10
 
11
  def is_sensitive(name):
12
+ return any(h in name.upper() for h in SECRET_HINTS)
 
13
 
14
  def mask_secrets(text):
15
  out = []
 
20
  out.append(line)
21
  return "\n".join(out)
22
 
23
+ # =================================================================
24
+ # PERSISTENT INTERACTIVE PYTHON REPL (supports input + streaming)
25
+ # =================================================================
26
+ class Repl:
27
+ def __init__(self):
28
+ self.p = subprocess.Popen(
29
+ [sys.executable, "-i", "-u"], # -i interactive, -u unbuffered
30
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
31
+ text=True, bufsize=1,
32
+ )
33
+ self.q = queue.Queue()
34
+ threading.Thread(target=self._reader, daemon=True).start()
35
+
36
+ def _reader(self):
37
+ for line in self.p.stdout:
38
+ self.q.put(line)
39
+
40
+ def send(self, text):
41
+ if self.p.poll() is None:
42
+ self.p.stdin.write(text + "\n")
43
+ self.p.stdin.flush()
44
+
45
+ def drain(self, wait=0.3):
46
+ # collect whatever the REPL has produced so far
47
+ time.sleep(wait)
48
+ chunks = []
49
+ while not self.q.empty():
50
+ chunks.append(self.q.get())
51
+ return mask_secrets("".join(chunks))
52
+
53
+ def alive(self):
54
+ return self.p.poll() is None
55
+
56
+ def kill(self):
57
+ try:
58
+ self.p.kill()
59
+ except Exception:
60
+ pass
61
+
62
+ _repl = {"r": None}
63
+ def get_repl():
64
+ if _repl["r"] is None or not _repl["r"].alive():
65
+ _repl["r"] = Repl()
66
+ _repl["r"].drain(0.5) # swallow the startup banner
67
+ return _repl["r"]
68
+
69
  PAGE = """<!doctype html><html><head><title>Box</title>
70
  <style>
71
  body{{font-family:monospace;margin:20px;background:#0b0b0b;color:#eee}}
 
74
  .bar{{background:#1a1a1a;padding:10px;margin-bottom:10px;border-radius:6px}}
75
  .tabs a{{margin-right:14px;font-weight:bold}}
76
  textarea{{width:100%;background:#000;color:#47E6C1;border:1px solid #333;padding:8px}}
77
+ pre{{background:#000;color:#78D64B;padding:12px;border-radius:6px;overflow:auto;height:55vh;white-space:pre-wrap}}
78
+ input[type=text]{{background:#000;color:#eee;border:1px solid #333;padding:8px}}
79
+ button{{background:#F46821;color:#fff;border:0;padding:8px 14px;border-radius:4px;cursor:pointer}}
80
  </style></head><body>
81
+ <div class="tabs bar"><a href="/fm/">📁 Files</a><a href="/fm/term">🖥️ Shell (stream)</a>
82
+ <a href="/fm/py">🐍 Python REPL</a><a href="/reload">♻️ Reload</a><a href="/gpuinfo">🎮 GPU</a>
83
+ <span style="color:#777">test mode · ephemeral</span></div>
84
  {body}</body></html>"""
85
 
86
+ # ---------------- INTERACTIVE PYTHON REPL PAGE ----------------
87
+ @app.route("/py")
88
+ def py_page():
89
+ body = """
90
+ <div class="bar"><b>Interactive Python</b> persistent session. Try: <code>2+2</code>,
91
+ then <code>name = input("your name? ")</code> and answer in the same box.</div>
92
+ <pre id="out">Python REPL ready. Type code below and press Run (or Enter).\n</pre>
93
+ <input type="text" id="line" style="width:80%" autofocus
94
+ placeholder="python code or input to a running prompt">
95
+ <button onclick="send()">Run</button>
96
+ <button onclick="reset()">Restart REPL</button>
97
+ <script>
98
+ const out = document.getElementById('out');
99
+ const line = document.getElementById('line');
100
+ function append(t){ out.textContent += t; out.scrollTop = out.scrollHeight; }
101
+ async function send(){
102
+ const code = line.value;
103
+ append('>>> ' + code + '\\n');
104
+ line.value = '';
105
+ const r = await fetch('/fm/py_send', {
106
+ method:'POST', headers:{'Content-Type':'application/json'},
107
+ body: JSON.stringify({code})
108
+ });
109
+ const data = await r.json();
110
+ if (data.output) append(data.output);
111
+ }
112
+ async function reset(){
113
+ await fetch('/fm/py_reset', {method:'POST'});
114
+ out.textContent = 'REPL restarted.\\n';
115
+ }
116
+ line.addEventListener('keydown', e => { if(e.key==='Enter'){ e.preventDefault(); send(); }});
117
+ </script>"""
118
+ return Response(PAGE.format(body=body))
119
 
120
+ @app.route("/py_send", methods=["POST"])
121
+ def py_send():
122
+ data = request.get_json(force=True)
123
+ repl = get_repl()
124
+ repl.send(data.get("code", ""))
125
+ return Response(json.dumps({"output": repl.drain(0.4)}), mimetype="application/json")
126
+
127
+ @app.route("/py_reset", methods=["POST"])
128
+ def py_reset():
129
+ if _repl["r"]:
130
+ _repl["r"].kill()
131
+ _repl["r"] = None
132
+ get_repl()
133
+ return Response(json.dumps({"ok": True}), mimetype="application/json")
134
+
135
+ # ---------------- STREAMING ONE-SHOT SHELL ----------------
136
+ @app.route("/term")
137
+ def term_page():
138
+ body = """
139
+ <div class="bar"><b>Streaming shell</b> — output appears live, line by line.</div>
140
+ <input type="text" id="cmd" style="width:80%" autofocus
141
+ placeholder="e.g. python -c 'print(2+2)' , pip install rich , ls -la">
142
+ <button onclick="run()">Run</button>
143
+ <pre id="out"></pre>
144
  <script>
145
+ const out = document.getElementById('out');
146
+ async function run(){
147
+ const cmd = document.getElementById('cmd').value;
148
+ out.textContent = '$ ' + cmd + '\\n';
149
+ const resp = await fetch('/fm/term_stream?cmd=' + encodeURIComponent(cmd));
150
+ const reader = resp.body.getReader();
151
+ const dec = new TextDecoder();
152
+ while(true){
153
+ const {value, done} = await reader.read();
154
+ if(done) break;
155
+ out.textContent += dec.decode(value);
156
+ out.scrollTop = out.scrollHeight;
157
+ }
158
+ }
159
+ document.getElementById('cmd').addEventListener('keydown',
160
+ e => { if(e.key==='Enter'){ e.preventDefault(); run(); }});
161
  </script>"""
162
  return Response(PAGE.format(body=body))
163
 
164
+ @app.route("/term_stream")
165
+ def term_stream():
166
+ cmd = request.args.get("cmd", "")
167
+ @stream_with_context
168
+ def gen():
169
+ if not cmd.strip():
170
+ yield "[empty command]\n"; return
171
+ p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
172
+ stderr=subprocess.STDOUT, text=True, bufsize=1)
173
+ try:
174
+ for line in p.stdout:
175
+ yield mask_secrets(line)
176
+ p.wait(timeout=300)
177
+ yield f"\n[exit code {p.returncode}]\n"
178
+ except Exception as e:
179
+ yield f"\n[error: {e}]\n"
180
+ return Response(gen(), mimetype="text/plain")
181
+
182
+ # ---------------- FILE MANAGER (unchanged core) ----------------
183
  @app.route("/")
184
  def browse():
185
  path = os.path.abspath(request.args.get("path", "/"))
 
187
  return Response(PAGE.format(body=f"<p>Not found: {html.escape(path)}</p>"), status=404)
188
  if os.path.isfile(path):
189
  return redirect(f"/fm/view?path={urllib.parse.quote(path)}")
 
190
  parent = os.path.dirname(path.rstrip("/")) or "/"
191
  rows = f'<tr><td><a href="/fm/?path={urllib.parse.quote(parent)}">.. (up)</a></td><td></td><td></td></tr>'
192
  try:
193
  entries = sorted(os.listdir(path))
194
  except PermissionError:
195
  return Response(PAGE.format(body=f"<p>Permission denied: {html.escape(path)}</p>"), status=403)
 
196
  for name in entries:
197
  full = os.path.join(path, name); q = urllib.parse.quote(full)
198
  try:
 
208
  f'<a href="/fm/edit?path={q}">edit</a> | '
209
  f'<a href="/fm/delete?path={q}">delete</a>')
210
  rows += f"<tr><td>{link}</td><td>{size}</td><td>{actions}</td></tr>"
 
211
  body = f"""
212
  <div class="bar"><b>Path:</b> {html.escape(path)}</div>
213
  <div class="bar">
214
  <form action="/fm/mkdir" method="post" style="display:inline">
215
  <input type="hidden" name="path" value="{html.escape(path)}">
216
+ <input name="name" placeholder="new folder"><button type="submit">Create folder</button>
217
  </form>
218
  <form action="/fm/upload" method="post" enctype="multipart/form-data" style="display:inline">
219
  <input type="hidden" name="path" value="{html.escape(path)}">
220
+ <input type="file" name="file"><button type="submit">Upload</button>
221
  </form>
222
  </div>
223
  <table><tr><th>Name</th><th>Size</th><th>Actions</th></tr>{rows}</table>"""
 
257
  <form action="/fm/save" method="post">
258
  <input type="hidden" name="path" value="{html.escape(path)}">
259
  <textarea name="content" style="height:60vh">{html.escape(content)}</textarea>
260
+ <br><button type="submit">Save (live)</button>
261
  <a href="/fm/?path={urllib.parse.quote(os.path.dirname(path))}">cancel</a>
262
  </form>"""
263
  return Response(PAGE.format(body=body))
 
302
 
303
  if __name__ == "__main__":
304
  wrapped = DispatcherMiddleware(Flask("empty"), {"/fm": app})
305
+ run_simple("0.0.0.0", 9001, wrapped, threaded=True) # threaded=True for streaming