CORVO-AI commited on
Commit
7c6f376
·
verified ·
1 Parent(s): 86fda11

Create file-manager/app.py

Browse files
Files changed (1) hide show
  1. file-manager/app.py +162 -0
file-manager/app.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, shutil, html, urllib.parse
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
+
6
+ app = Flask(__name__)
7
+ app.config["APPLICATION_ROOT"] = "/fm"
8
+
9
+ # Guide rule 10: mask secret-bearing values so a stray read can't leak credentials.
10
+ SECRET_HINTS = ("TOKEN", "KEY", "SECRET", "PASSWORD", "AUTH", "COOKIE")
11
+
12
+ def is_sensitive(name):
13
+ up = name.upper()
14
+ return any(h in up for h in SECRET_HINTS)
15
+
16
+ PAGE = """<!doctype html><html><head><title>File Manager</title>
17
+ <style>
18
+ body{{font-family:monospace;margin:20px;background:#111;color:#eee}}
19
+ a{{color:#29BEFD;text-decoration:none}} a:hover{{text-decoration:underline}}
20
+ table{{border-collapse:collapse;width:100%}} td,th{{padding:4px 8px;border-bottom:1px solid #333;text-align:left}}
21
+ .bar{{background:#222;padding:10px;margin-bottom:10px;border-radius:6px}}
22
+ textarea{{width:100%;height:60vh;background:#000;color:#0f0;border:1px solid #333}}
23
+ button,input[type=submit]{{background:#F46821;color:#fff;border:0;padding:6px 12px;border-radius:4px;cursor:pointer}}
24
+ </style></head><body>{body}</body></html>"""
25
+
26
+ @app.route("/")
27
+ def browse():
28
+ path = request.args.get("path", "/")
29
+ path = os.path.abspath(path)
30
+ if not os.path.exists(path):
31
+ return Response(PAGE.format(body=f"<p>Not found: {html.escape(path)}</p>"), status=404)
32
+
33
+ if os.path.isfile(path):
34
+ return redirect(f"/fm/view?path={urllib.parse.quote(path)}")
35
+
36
+ rows = ""
37
+ parent = os.path.dirname(path.rstrip("/")) or "/"
38
+ rows += f'<tr><td><a href="/fm/?path={urllib.parse.quote(parent)}">.. (up)</a></td><td></td><td></td></tr>'
39
+ try:
40
+ entries = sorted(os.listdir(path))
41
+ except PermissionError:
42
+ return Response(PAGE.format(body=f"<p>Permission denied: {html.escape(path)}</p>"), status=403)
43
+
44
+ for name in entries:
45
+ full = os.path.join(path, name)
46
+ q = urllib.parse.quote(full)
47
+ try:
48
+ size = os.path.getsize(full) if os.path.isfile(full) else ""
49
+ except OSError:
50
+ size = "?"
51
+ if os.path.isdir(full):
52
+ link = f'<a href="/fm/?path={q}">📁 {html.escape(name)}/</a>'
53
+ actions = f'<a href="/fm/delete?path={q}">delete</a>'
54
+ else:
55
+ link = f'<a href="/fm/view?path={q}">📄 {html.escape(name)}</a>'
56
+ actions = (f'<a href="/fm/download?path={q}">download</a> | '
57
+ f'<a href="/fm/edit?path={q}">edit</a> | '
58
+ f'<a href="/fm/delete?path={q}">delete</a>')
59
+ rows += f"<tr><td>{link}</td><td>{size}</td><td>{actions}</td></tr>"
60
+
61
+ body = f"""
62
+ <div class="bar"><b>Path:</b> {html.escape(path)}</div>
63
+ <div class="bar">
64
+ <form action="/fm/mkdir" method="post" style="display:inline">
65
+ <input type="hidden" name="path" value="{html.escape(path)}">
66
+ <input name="name" placeholder="new folder name">
67
+ <input type="submit" value="Create folder">
68
+ </form>
69
+ <form action="/fm/upload" method="post" enctype="multipart/form-data" style="display:inline">
70
+ <input type="hidden" name="path" value="{html.escape(path)}">
71
+ <input type="file" name="file">
72
+ <input type="submit" value="Upload">
73
+ </form>
74
+ </div>
75
+ <table><tr><th>Name</th><th>Size</th><th>Actions</th></tr>{rows}</table>
76
+ <p style="color:#777">Test mode: no auth. Storage is ephemeral — edits reset on rebuild.</p>
77
+ """
78
+ return Response(PAGE.format(body=body))
79
+
80
+ @app.route("/view")
81
+ def view():
82
+ path = os.path.abspath(request.args.get("path", ""))
83
+ if not os.path.isfile(path):
84
+ return redirect("/fm/")
85
+ if is_sensitive(os.path.basename(path)):
86
+ content = "*** masked (sensitive filename) ***"
87
+ else:
88
+ try:
89
+ with open(path, "r", errors="replace") as f:
90
+ content = f.read(200000)
91
+ except Exception as e:
92
+ content = f"[cannot read as text: {e}]"
93
+ q = urllib.parse.quote(path)
94
+ body = (f'<div class="bar"><b>{html.escape(path)}</b> | '
95
+ f'<a href="/fm/edit?path={q}">edit</a> | '
96
+ f'<a href="/fm/download?path={q}">download</a> | '
97
+ f'<a href="/fm/?path={urllib.parse.quote(os.path.dirname(path))}">back</a></div>'
98
+ f"<pre>{html.escape(content)}</pre>")
99
+ return Response(PAGE.format(body=body))
100
+
101
+ @app.route("/edit", methods=["GET"])
102
+ def edit():
103
+ path = os.path.abspath(request.args.get("path", ""))
104
+ try:
105
+ with open(path, "r", errors="replace") as f:
106
+ content = f.read()
107
+ except Exception as e:
108
+ content = f"[cannot open: {e}]"
109
+ body = f"""
110
+ <div class="bar"><b>Editing:</b> {html.escape(path)}</div>
111
+ <form action="/fm/save" method="post">
112
+ <input type="hidden" name="path" value="{html.escape(path)}">
113
+ <textarea name="content">{html.escape(content)}</textarea>
114
+ <br><input type="submit" value="Save (live, no refresh)">
115
+ <a href="/fm/?path={urllib.parse.quote(os.path.dirname(path))}">cancel</a>
116
+ </form>"""
117
+ return Response(PAGE.format(body=body))
118
+
119
+ @app.route("/save", methods=["POST"])
120
+ def save():
121
+ path = os.path.abspath(request.form["path"])
122
+ with open(path, "w") as f:
123
+ f.write(request.form["content"])
124
+ return redirect(f"/fm/view?path={urllib.parse.quote(path)}")
125
+
126
+ @app.route("/download")
127
+ def download():
128
+ path = os.path.abspath(request.args.get("path", ""))
129
+ if is_sensitive(os.path.basename(path)):
130
+ return Response("masked (sensitive filename)", status=403)
131
+ return send_file(path, as_attachment=True)
132
+
133
+ @app.route("/mkdir", methods=["POST"])
134
+ def mkdir():
135
+ base = os.path.abspath(request.form["path"])
136
+ os.makedirs(os.path.join(base, request.form["name"]), exist_ok=True)
137
+ return redirect(f"/fm/?path={urllib.parse.quote(base)}")
138
+
139
+ @app.route("/upload", methods=["POST"])
140
+ def upload():
141
+ base = os.path.abspath(request.form["path"])
142
+ f = request.files.get("file")
143
+ if f and f.filename:
144
+ f.save(os.path.join(base, f.filename))
145
+ return redirect(f"/fm/?path={urllib.parse.quote(base)}")
146
+
147
+ @app.route("/delete")
148
+ def delete():
149
+ path = os.path.abspath(request.args.get("path", ""))
150
+ parent = os.path.dirname(path)
151
+ try:
152
+ if os.path.isdir(path):
153
+ shutil.rmtree(path)
154
+ else:
155
+ os.remove(path)
156
+ except Exception as e:
157
+ return Response(PAGE.format(body=f"<p>Delete failed: {e}</p>"), status=500)
158
+ return redirect(f"/fm/?path={urllib.parse.quote(parent)}")
159
+
160
+ if __name__ == "__main__":
161
+ wrapped = DispatcherMiddleware(Flask("empty"), {"/fm": app})
162
+ run_simple("0.0.0.0", 9001, wrapped)