CORVO-AI commited on
Commit
9f99ee1
·
verified ·
1 Parent(s): 0acb5a1

Update file-manager/app.py

Browse files
Files changed (1) hide show
  1. file-manager/app.py +49 -27
file-manager/app.py CHANGED
@@ -1,4 +1,4 @@
1
- import os, shutil, html, urllib.parse, subprocess, pty, select, termios, struct, fcntl, signal, threading, json
2
  from flask import Flask, request, redirect, send_file, Response
3
  from werkzeug.middleware.dispatcher import DispatcherMiddleware
4
  from werkzeug.serving import run_simple
@@ -21,19 +21,17 @@ def mask_secrets(text):
21
  return "".join(out)
22
 
23
  # =================================================================
24
- # PERSISTENT PTY SHELL (real terminal: proper buffering, prompts,
25
- # interactive input, cd persistence, colors)
26
  # =================================================================
27
  class PtyShell:
28
  def __init__(self):
29
  self.pid, self.fd = pty.fork()
30
  if self.pid == 0:
31
- # child: become an interactive bash inside the PTY
32
  os.environ["TERM"] = "xterm-256color"
33
- os.chdir("/") # <-- start at the system root
34
  os.execvp("bash", ["bash", "-i"])
35
  else:
36
- # parent: set the fd non-blocking so reads never hang
37
  flags = fcntl.fcntl(self.fd, fcntl.F_GETFL)
38
  fcntl.fcntl(self.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
39
  self._set_size(40, 120)
@@ -86,11 +84,15 @@ class PtyShell:
86
  except OSError:
87
  pass
88
 
89
- _shell = {"s": None}
90
- def get_shell():
91
- if _shell["s"] is None or not _shell["s"].alive():
92
- _shell["s"] = PtyShell()
93
- return _shell["s"]
 
 
 
 
94
 
95
  PAGE = """<!doctype html><html><head><title>Box</title>
96
  <style>
@@ -104,39 +106,41 @@ pre{{background:#000;color:#e6e6e6;padding:12px;border-radius:6px;overflow:auto;
104
  input[type=text]{{background:#000;color:#eee;border:1px solid #333;padding:8px;font-family:monospace}}
105
  button{{background:#F46821;color:#fff;border:0;padding:8px 14px;border-radius:4px;cursor:pointer}}
106
  </style></head><body>
107
- <div class="tabs bar"><a href="/fm/">📁 Files</a><a href="/fm/term">🖥️ Terminal</a>
 
108
  <a href="/reload">♻️ Reload</a><a href="/gpuinfo">🎮 GPU</a>
109
  <span style="color:#777">test mode · ephemeral</span></div>
110
  {body}</body></html>"""
111
 
112
- # ---------------- REAL TERMINAL PAGE ----------------
113
  @app.route("/term")
114
  def term_page():
115
  body = """
116
- <div class="bar"><b>Live terminal (xterm.js)</b> — full color, progress bars, prompts.</div>
117
- <div id="term" style="height:65vh;background:#000;border-radius:6px"></div>
118
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css"/>
119
  <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
120
  <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
121
  <script>
122
- const term = new Terminal({convertEol:false, cursorBlink:true, fontFamily:'monospace', fontSize:13});
 
 
 
123
  const fit = new FitAddon.FitAddon();
124
  term.loadAddon(fit);
125
  term.open(document.getElementById('term'));
126
  fit.fit();
127
  window.addEventListener('resize', () => fit.fit());
128
 
129
- // Send every keystroke straight to the PTY (real interactive terminal)
130
  term.onData(d => {
131
  fetch('/fm/term_in', {method:'POST',
132
  headers:{'Content-Type':'application/json'},
133
- body: JSON.stringify({data: d})});
134
  });
135
 
136
- // Poll for new PTY output and write it raw — xterm.js renders the ANSI codes
137
  async function poll(){
138
  try{
139
- const r = await fetch('/fm/term_out');
140
  const j = await r.json();
141
  if(j.data) term.write(j.data);
142
  }catch(e){}
@@ -149,19 +153,22 @@ def term_page():
149
  @app.route("/term_in", methods=["POST"])
150
  def term_in():
151
  data = request.get_json(force=True)
152
- get_shell().write(data.get("data", ""))
153
  return Response(json.dumps({"ok": True}), mimetype="application/json")
154
 
155
  @app.route("/term_out")
156
  def term_out():
157
- return Response(json.dumps({"data": get_shell().read_new()}), mimetype="application/json")
 
158
 
159
  @app.route("/term_reset", methods=["POST"])
160
  def term_reset():
161
- if _shell["s"]:
162
- _shell["s"].kill()
163
- _shell["s"] = None
164
- get_shell()
 
 
165
  return Response(json.dumps({"ok": True}), mimetype="application/json")
166
 
167
  # ---------------- FILE MANAGER ----------------
@@ -200,6 +207,10 @@ def browse():
200
  <input type="hidden" name="path" value="{html.escape(path)}">
201
  <input name="name" placeholder="new folder"><button type="submit">Create folder</button>
202
  </form>
 
 
 
 
203
  <form action="/fm/upload" method="post" enctype="multipart/form-data" style="display:inline">
204
  <input type="hidden" name="path" value="{html.escape(path)}">
205
  <input type="file" name="file"><button type="submit">Upload</button>
@@ -267,6 +278,17 @@ def mkdir():
267
  os.makedirs(os.path.join(base, request.form["name"]), exist_ok=True)
268
  return redirect(f"/fm/?path={urllib.parse.quote(base)}")
269
 
 
 
 
 
 
 
 
 
 
 
 
270
  @app.route("/upload", methods=["POST"])
271
  def upload():
272
  base = os.path.abspath(request.form["path"])
@@ -287,4 +309,4 @@ def delete():
287
 
288
  if __name__ == "__main__":
289
  wrapped = DispatcherMiddleware(Flask("empty"), {"/fm": app})
290
- run_simple("0.0.0.0", 9001, wrapped, threaded=True) # threaded=True for concurrent poll+input
 
1
+ import os, shutil, html, urllib.parse, pty, select, termios, struct, fcntl, signal, threading, json
2
  from flask import Flask, request, redirect, send_file, Response
3
  from werkzeug.middleware.dispatcher import DispatcherMiddleware
4
  from werkzeug.serving import run_simple
 
21
  return "".join(out)
22
 
23
  # =================================================================
24
+ # PERSISTENT PTY SHELL (real terminal: buffering, prompts, input,
25
+ # cd persistence, colors). Starts at the system root "/".
26
  # =================================================================
27
  class PtyShell:
28
  def __init__(self):
29
  self.pid, self.fd = pty.fork()
30
  if self.pid == 0:
 
31
  os.environ["TERM"] = "xterm-256color"
32
+ os.chdir("/") # start at the system root
33
  os.execvp("bash", ["bash", "-i"])
34
  else:
 
35
  flags = fcntl.fcntl(self.fd, fcntl.F_GETFL)
36
  fcntl.fcntl(self.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
37
  self._set_size(40, 120)
 
84
  except OSError:
85
  pass
86
 
87
+ # --- Multiple independent PTY shells, keyed by session id ---
88
+ _shells = {}
89
+
90
+ def get_shell(sid):
91
+ sh = _shells.get(sid)
92
+ if sh is None or not sh.alive():
93
+ sh = PtyShell()
94
+ _shells[sid] = sh
95
+ return sh
96
 
97
  PAGE = """<!doctype html><html><head><title>Box</title>
98
  <style>
 
106
  input[type=text]{{background:#000;color:#eee;border:1px solid #333;padding:8px;font-family:monospace}}
107
  button{{background:#F46821;color:#fff;border:0;padding:8px 14px;border-radius:4px;cursor:pointer}}
108
  </style></head><body>
109
+ <div class="tabs bar"><a href="/fm/">📁 Files</a>
110
+ <a href="/fm/term" target="_blank">🖥️ New Terminal</a>
111
  <a href="/reload">♻️ Reload</a><a href="/gpuinfo">🎮 GPU</a>
112
  <span style="color:#777">test mode · ephemeral</span></div>
113
  {body}</body></html>"""
114
 
115
+ # ---------------- TERMINAL PAGE (xterm.js, unique session per tab) ----------------
116
  @app.route("/term")
117
  def term_page():
118
  body = """
119
+ <div class="bar"><b>Live terminal (xterm.js)</b> — each tab is its own shell, starts at /.</div>
120
+ <div id="term" style="height:70vh;background:#000;border-radius:6px"></div>
121
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css"/>
122
  <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
123
  <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
124
  <script>
125
+ const SID = (crypto.randomUUID ? crypto.randomUUID()
126
+ : String(Date.now()) + Math.random());
127
+ const term = new Terminal({convertEol:false, cursorBlink:true,
128
+ fontFamily:'monospace', fontSize:13});
129
  const fit = new FitAddon.FitAddon();
130
  term.loadAddon(fit);
131
  term.open(document.getElementById('term'));
132
  fit.fit();
133
  window.addEventListener('resize', () => fit.fit());
134
 
 
135
  term.onData(d => {
136
  fetch('/fm/term_in', {method:'POST',
137
  headers:{'Content-Type':'application/json'},
138
+ body: JSON.stringify({sid: SID, data: d})});
139
  });
140
 
 
141
  async function poll(){
142
  try{
143
+ const r = await fetch('/fm/term_out?sid=' + SID);
144
  const j = await r.json();
145
  if(j.data) term.write(j.data);
146
  }catch(e){}
 
153
  @app.route("/term_in", methods=["POST"])
154
  def term_in():
155
  data = request.get_json(force=True)
156
+ get_shell(data.get("sid", "default")).write(data.get("data", ""))
157
  return Response(json.dumps({"ok": True}), mimetype="application/json")
158
 
159
  @app.route("/term_out")
160
  def term_out():
161
+ sid = request.args.get("sid", "default")
162
+ return Response(json.dumps({"data": get_shell(sid).read_new()}), mimetype="application/json")
163
 
164
  @app.route("/term_reset", methods=["POST"])
165
  def term_reset():
166
+ data = request.get_json(force=True)
167
+ sid = data.get("sid", "default")
168
+ if sid in _shells:
169
+ _shells[sid].kill()
170
+ del _shells[sid]
171
+ get_shell(sid)
172
  return Response(json.dumps({"ok": True}), mimetype="application/json")
173
 
174
  # ---------------- FILE MANAGER ----------------
 
207
  <input type="hidden" name="path" value="{html.escape(path)}">
208
  <input name="name" placeholder="new folder"><button type="submit">Create folder</button>
209
  </form>
210
+ <form action="/fm/touch" method="post" style="display:inline">
211
+ <input type="hidden" name="path" value="{html.escape(path)}">
212
+ <input name="name" placeholder="new file name"><button type="submit">Create file</button>
213
+ </form>
214
  <form action="/fm/upload" method="post" enctype="multipart/form-data" style="display:inline">
215
  <input type="hidden" name="path" value="{html.escape(path)}">
216
  <input type="file" name="file"><button type="submit">Upload</button>
 
278
  os.makedirs(os.path.join(base, request.form["name"]), exist_ok=True)
279
  return redirect(f"/fm/?path={urllib.parse.quote(base)}")
280
 
281
+ @app.route("/touch", methods=["POST"])
282
+ def touch():
283
+ base = os.path.abspath(request.form["path"])
284
+ name = request.form.get("name", "").strip()
285
+ if name:
286
+ full = os.path.join(base, name)
287
+ if not os.path.exists(full):
288
+ open(full, "a").close() # create empty file
289
+ return redirect(f"/fm/edit?path={urllib.parse.quote(full)}")
290
+ return redirect(f"/fm/?path={urllib.parse.quote(base)}")
291
+
292
  @app.route("/upload", methods=["POST"])
293
  def upload():
294
  base = os.path.abspath(request.form["path"])
 
309
 
310
  if __name__ == "__main__":
311
  wrapped = DispatcherMiddleware(Flask("empty"), {"/fm": app})
312
+ run_simple("0.0.0.0", 9001, wrapped, threaded=True) # threaded for concurrent poll+input