Spaces:
Sleeping
Sleeping
Upload app.py
Browse files- static/app.py +204 -261
static/app.py
CHANGED
|
@@ -1,283 +1,226 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
# Flask server: serveste HTML + proxy B2
|
| 4 |
-
# Auto-seed: adauga elevii automat la primul start
|
| 5 |
-
# ================================================
|
| 6 |
-
|
| 7 |
-
from flask import Flask, send_from_directory, request, jsonify
|
| 8 |
-
import requests, base64, time, threading
|
| 9 |
-
import firebase_admin
|
| 10 |
-
from firebase_admin import credentials, firestore
|
| 11 |
|
| 12 |
app = Flask(__name__, static_folder='static')
|
| 13 |
|
| 14 |
-
# ──
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
"type": "service_account",
|
| 17 |
-
"project_id": "
|
| 18 |
-
"private_key_id": "
|
| 19 |
-
"
|
|
|
|
| 20 |
"token_uri": "https://oauth2.googleapis.com/token",
|
| 21 |
-
"apiKey": "AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU",
|
| 22 |
-
"authDomain": "vservers1.firebaseapp.com",
|
| 23 |
}
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
# ──
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
# ──
|
| 37 |
ELEVI = [
|
| 38 |
-
{"pozitie":
|
| 39 |
-
{"pozitie":
|
| 40 |
-
{"pozitie":
|
| 41 |
-
{"pozitie":
|
| 42 |
-
{"pozitie":
|
| 43 |
-
{"pozitie":
|
| 44 |
-
{"pozitie":
|
| 45 |
-
{"pozitie":
|
| 46 |
-
{"pozitie":
|
| 47 |
-
{"pozitie":
|
| 48 |
-
{"pozitie":
|
| 49 |
-
{"pozitie":
|
| 50 |
-
{"pozitie":
|
| 51 |
-
{"pozitie":
|
| 52 |
-
{"pozitie":
|
| 53 |
-
{"pozitie":
|
| 54 |
-
{"pozitie":
|
| 55 |
-
{"pozitie":
|
| 56 |
-
{"pozitie":
|
| 57 |
-
{"pozitie":
|
| 58 |
-
{"pozitie":
|
| 59 |
-
{"pozitie":
|
| 60 |
-
{"pozitie":
|
| 61 |
-
{"pozitie":
|
| 62 |
-
{"pozitie":
|
| 63 |
-
{"pozitie":
|
| 64 |
-
{"pozitie":
|
| 65 |
-
{"pozitie":
|
| 66 |
-
{"pozitie":
|
| 67 |
-
{"pozitie":
|
| 68 |
-
{"pozitie":
|
| 69 |
-
{"pozitie":
|
| 70 |
]
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
)
|
| 80 |
-
|
| 81 |
-
return r.json().get("documents", [])
|
| 82 |
-
return []
|
| 83 |
-
|
| 84 |
-
def fs_add(collection, data):
|
| 85 |
-
"""Adauga un document nou (auto-ID)."""
|
| 86 |
-
# Converteste dict Python la format Firestore
|
| 87 |
-
fields = {}
|
| 88 |
-
for k, v in data.items():
|
| 89 |
-
if v is None:
|
| 90 |
-
fields[k] = {"nullValue": None}
|
| 91 |
-
elif isinstance(v, bool):
|
| 92 |
-
fields[k] = {"booleanValue": v}
|
| 93 |
-
elif isinstance(v, int):
|
| 94 |
-
fields[k] = {"integerValue": str(v)}
|
| 95 |
-
elif isinstance(v, str):
|
| 96 |
-
fields[k] = {"stringValue": v}
|
| 97 |
-
else:
|
| 98 |
-
fields[k] = {"stringValue": str(v)}
|
| 99 |
-
|
| 100 |
-
r = requests.post(
|
| 101 |
-
f"{FIRESTORE_BASE}/{collection}?key={FIREBASE_API_KEY}",
|
| 102 |
-
json={"fields": fields},
|
| 103 |
-
timeout=15
|
| 104 |
-
)
|
| 105 |
-
return r.status_code == 200
|
| 106 |
-
|
| 107 |
-
# ── AUTO-SEED ───────────────────────────────────
|
| 108 |
|
| 109 |
def auto_seed():
|
| 110 |
-
|
| 111 |
-
print("[VSERVERS] Verificare
|
| 112 |
-
time.sleep(3) # asteapta serverul sa porneasca complet
|
| 113 |
-
|
| 114 |
try:
|
| 115 |
-
|
| 116 |
-
if
|
| 117 |
-
print(
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
print("[VSERVERS] Baza de date goala — pornire seed automat...")
|
| 121 |
-
ok = 0
|
| 122 |
for e in ELEVI:
|
| 123 |
-
|
| 124 |
-
"
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
"confirmed": False
|
| 129 |
-
})
|
| 130 |
-
if success:
|
| 131 |
-
ok += 1
|
| 132 |
-
print(f"[VSERVERS] ✓ {e['vpassId']} — {e['nume']}")
|
| 133 |
-
else:
|
| 134 |
-
print(f"[VSERVERS] ✗ {e['vpassId']} — eroare")
|
| 135 |
-
time.sleep(0.1) # pauza mica intre scrieri
|
| 136 |
-
|
| 137 |
-
print(f"[VSERVERS] Seed finalizat: {ok}/{len(ELEVI)} elevi adaugati.")
|
| 138 |
-
|
| 139 |
-
except Exception as ex:
|
| 140 |
-
print(f"[VSERVERS] Eroare seed: {ex}")
|
| 141 |
-
|
| 142 |
-
# Porneste seed in background thread la startup
|
| 143 |
-
threading.Thread(target=auto_seed, daemon=True).start()
|
| 144 |
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
def get_auth():
|
| 148 |
-
if _auth["token"] and _auth["expires"] > time.time():
|
| 149 |
-
return _auth
|
| 150 |
-
cred = base64.b64encode(f"{B2_KEY_ID}:{B2_APP_KEY}".encode()).decode()
|
| 151 |
-
r = requests.get(
|
| 152 |
-
"https://api.backblazeb2.com/b2api/v2/b2_authorize_account",
|
| 153 |
-
headers={"Authorization": f"Basic {cred}"},
|
| 154 |
-
timeout=15
|
| 155 |
-
)
|
| 156 |
-
r.raise_for_status()
|
| 157 |
-
data = r.json()
|
| 158 |
-
_auth.update({
|
| 159 |
-
"token": data["authorizationToken"],
|
| 160 |
-
"api_url": data["apiUrl"],
|
| 161 |
-
"download_url": data["downloadUrl"],
|
| 162 |
-
"expires": time.time() + 23 * 3600
|
| 163 |
-
})
|
| 164 |
-
return _auth
|
| 165 |
-
|
| 166 |
-
# ── ROUTES ──────────────────────────────────────
|
| 167 |
|
| 168 |
@app.route("/")
|
| 169 |
-
def index():
|
| 170 |
-
return send_from_directory("static", "index.html")
|
| 171 |
-
|
| 172 |
-
@app.route("/<path:filename>")
|
| 173 |
-
def static_files(filename):
|
| 174 |
-
return send_from_directory("static", filename)
|
| 175 |
-
|
| 176 |
-
@app.route("/b2proxy", methods=["POST", "OPTIONS"])
|
| 177 |
-
def b2proxy():
|
| 178 |
-
if request.method == "OPTIONS":
|
| 179 |
-
return _cors("", 204)
|
| 180 |
-
|
| 181 |
-
action = request.headers.get("X-Action", "")
|
| 182 |
-
path = request.headers.get("X-Path", "")
|
| 183 |
-
|
| 184 |
-
try:
|
| 185 |
-
auth = get_auth()
|
| 186 |
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
f"{auth['api_url']}/b2api/v2/b2_list_file_names",
|
| 190 |
-
headers={"Authorization": auth["token"], "Content-Type": "application/json"},
|
| 191 |
-
json={"bucketId": B2_BUCKET_ID, "prefix": path, "delimiter": "/", "maxFileCount": 1000},
|
| 192 |
-
timeout=15
|
| 193 |
-
)
|
| 194 |
-
data = r.json()
|
| 195 |
-
files = [
|
| 196 |
-
{
|
| 197 |
-
"name": f["fileName"].split("/")[-1],
|
| 198 |
-
"key": f["fileName"],
|
| 199 |
-
"url": f"{auth['download_url']}/file/{B2_BUCKET}/{f['fileName']}?Authorization={auth['token']}"
|
| 200 |
-
}
|
| 201 |
-
for f in data.get("files", [])
|
| 202 |
-
if f["fileName"].split("/")[-1]
|
| 203 |
-
]
|
| 204 |
-
return _cors(jsonify({"files": files}))
|
| 205 |
-
|
| 206 |
-
if action == "upload-url":
|
| 207 |
-
r = requests.post(
|
| 208 |
-
f"{auth['api_url']}/b2api/v2/b2_get_upload_url",
|
| 209 |
-
headers={"Authorization": auth["token"], "Content-Type": "application/json"},
|
| 210 |
-
json={"bucketId": B2_BUCKET_ID},
|
| 211 |
-
timeout=15
|
| 212 |
-
)
|
| 213 |
-
data = r.json()
|
| 214 |
-
return _cors(jsonify({
|
| 215 |
-
"uploadUrl": data["uploadUrl"],
|
| 216 |
-
"uploadToken": data["authorizationToken"]
|
| 217 |
-
}))
|
| 218 |
-
|
| 219 |
-
if action == "upload":
|
| 220 |
-
file_data = request.data
|
| 221 |
-
content_type = request.headers.get("X-Content-Type", "application/octet-stream")
|
| 222 |
-
sha1 = request.headers.get("X-Sha1", "do_not_verify")
|
| 223 |
-
r = requests.post(
|
| 224 |
-
f"{auth['api_url']}/b2api/v2/b2_get_upload_url",
|
| 225 |
-
headers={"Authorization": auth["token"], "Content-Type": "application/json"},
|
| 226 |
-
json={"bucketId": B2_BUCKET_ID}, timeout=15
|
| 227 |
-
)
|
| 228 |
-
up = r.json()
|
| 229 |
-
r2 = requests.post(
|
| 230 |
-
up["uploadUrl"],
|
| 231 |
-
headers={
|
| 232 |
-
"Authorization": up["authorizationToken"],
|
| 233 |
-
"X-Bz-File-Name": requests.utils.quote(path, safe=""),
|
| 234 |
-
"Content-Type": content_type,
|
| 235 |
-
"X-Bz-Content-Sha1": sha1,
|
| 236 |
-
"Content-Length": str(len(file_data))
|
| 237 |
-
},
|
| 238 |
-
data=file_data, timeout=120
|
| 239 |
-
)
|
| 240 |
-
if r2.status_code == 200:
|
| 241 |
-
return _cors(jsonify({"ok": True}))
|
| 242 |
-
return _cors(jsonify({"error": r2.text}), 500)
|
| 243 |
-
|
| 244 |
-
if action == "delete":
|
| 245 |
-
r = requests.post(
|
| 246 |
-
f"{auth['api_url']}/b2api/v2/b2_list_file_names",
|
| 247 |
-
headers={"Authorization": auth["token"], "Content-Type": "application/json"},
|
| 248 |
-
json={"bucketId": B2_BUCKET_ID, "prefix": path, "maxFileCount": 1}, timeout=15
|
| 249 |
-
)
|
| 250 |
-
files = r.json().get("files", [])
|
| 251 |
-
if files:
|
| 252 |
-
f = files[0]
|
| 253 |
-
requests.post(
|
| 254 |
-
f"{auth['api_url']}/b2api/v2/b2_delete_file_version",
|
| 255 |
-
headers={"Authorization": auth["token"], "Content-Type": "application/json"},
|
| 256 |
-
json={"fileId": f["fileId"], "fileName": f["fileName"]}, timeout=15
|
| 257 |
-
)
|
| 258 |
-
return _cors(jsonify({"ok": True}))
|
| 259 |
-
|
| 260 |
-
return _cors(jsonify({"error": "Unknown action"}), 400)
|
| 261 |
-
|
| 262 |
-
except requests.exceptions.Timeout:
|
| 263 |
-
return _cors(jsonify({"error": "err-015 — Timeout"}), 504)
|
| 264 |
-
except requests.exceptions.ConnectionError:
|
| 265 |
-
return _cors(jsonify({"error": "err-027 — Retea"}), 503)
|
| 266 |
-
except Exception as e:
|
| 267 |
-
return _cors(jsonify({"error": f"err-016 — {str(e)}"}), 500)
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
def _cors(response, status=200):
|
| 271 |
-
if isinstance(response, str):
|
| 272 |
-
from flask import make_response
|
| 273 |
-
response = make_response(response, status)
|
| 274 |
-
else:
|
| 275 |
-
response.status_code = status
|
| 276 |
-
response.headers["Access-Control-Allow-Origin"] = "*"
|
| 277 |
-
response.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS"
|
| 278 |
-
response.headers["Access-Control-Allow-Headers"] = "Content-Type,X-Action,X-Path,X-File-Name,X-Content-Type,X-Sha1"
|
| 279 |
-
return response
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
-
if __name__
|
| 283 |
-
app.run(host="0.0.0.0", port=7860, debug=False)
|
|
|
|
| 1 |
+
from flask import Flask, send_from_directory, request, jsonify, Response
|
| 2 |
+
import requests, time, threading, json, os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
app = Flask(__name__, static_folder='static')
|
| 5 |
|
| 6 |
+
# ── Firebase ──────────────────────────────────────────────────
|
| 7 |
+
FIREBASE_API_KEY = "AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU"
|
| 8 |
+
FIRESTORE_BASE = "https://firestore.googleapis.com/v1/projects/vservers1/databases/(default)/documents"
|
| 9 |
+
|
| 10 |
+
# ── Google Drive ──────────────────────────────────────────────
|
| 11 |
+
DRIVE_FOLDER_ID = "10c-8bQnYWCz-XgZZT4DfF67cQRsHipNo"
|
| 12 |
+
DRIVE_SA = {
|
| 13 |
"type": "service_account",
|
| 14 |
+
"project_id": "vservers-40",
|
| 15 |
+
"private_key_id": "f6cf589fcc62e4bab8e557032d3b6b483d12dc8d",
|
| 16 |
+
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCyrlnxR+n8hSkE\nYym7ewU6mUezQnUtBRuxWb/AupzIygJqHmMG7r4BztRkZ/dmBQXWmb9VhUpPmEvR\nP1tvXyz2Hh2wY7T/BdzYLOB3uQCXgRuNHFD104kRyPoQQPPuL7ctAHryrFh69614\n6xZ/4i+i8hbOwqjfe9tkHjuaKNrk7w9bCth1fmEPGyavTIy+8OL9RYIppapscFbn\nxOg9tP2VEaKH3GxvQ/Iafi9xQG1gmXT0rGES1+AoJ/O1eL/5SJO9zaZWSBQvq/eg\n0m+V4S2liXdnuvlivHEP1ucIqTc8jUtJ93fuDVhfqNgy/1pahWBNqypkLrnRIvhn\n8CwBWEQbAgMBAAECggEAGNtSXdNwRJXNr/OPkao6fv1l0RU0sc+dG53tpAUR3Ijl\nrxeNFSDPQ/ce6tzfuMyIFGOND3uca9g26+QRdzvZSF/GJ4ynWDbbkyMjEuSkpW7r\ng0TmKlyEG/pGd05h4Me4hsUjVXEOWgTsl/60QZQYpmVhMOA5l+VmDtZ52idG1EQF\nyQR2pydQRG1QPK14s/Se5L4aTe44bH7ri0uLbyw7y01pCyneBsIhzn2cj3rn78Om\n2ZZkv4rx0iySDhlL2KqwuxnNK+UFk3veFYkCJFVO/7rkg98qnSUUIT0jj6skF1Bq\nCF9xDWBrVEukDRyEi3gTtClGusb7XBxOvOWSYEGZ6QKBgQDlyv2nZXDzb0wANzYg\ngL6pITP42fR7E/fkfhU1F40eaQteV+ZVaKxO3oiz1l2L23XTp2qqgkrqYHome9U3\nYFZkDH4YfGuo1MZTIRmHKdbOmc41ZKH6lVyDkEhTe9dI+GuDwpLbDPDxi0Bt7dbe\n2aRzif60VnrtghctzvodTz8TJwKBgQDHDxqYQ+qDgWID2PERB26oOsfMa49sbSCV\n18+wmgx+ydM1UTW2aj3hRbDTxZT6GUw3Ngc23HxpPTyF20g4i0jM1LQ2DiOzI9Ij\ntyYBZzK9ec5J7+Z3gcP3irJpS8S2SoUWKD8kl50nbzXj+SjF26uDUci9fG10jtoR\nJ8Os3zHP7QKBgQDMCugayM93yT7R/jR4vfkOUuZENLyKwRtf77jDEOuEsj+fASwM\ncMp3qc/26ATel/tS+hiT2OfOn+Y238RezJNJeXJKKciq/GwyCnUReMw9XYMmE/pk\neFXSmL4wKwnpyHQnZhFiomYcBMssEYG3FciZs1HQLe4vkVElouCiP+jBBwKBgQCD\nfXOo3zwbUC0Js5VSFWHAWMvAOdDM460hriQwWSIl4nXVA2cCr11e4GU1DpAhQPK6\nicLsN2srLVs8ZKRpTYByZZMBHgfw/pmCJCpDxQKcbMiayJCpoptrej/uFDHF5KXT\nBBTpvAkAkpK7m8uWH0xFe5GpsXawBuj/ag/0sp655QKBgG3xU85qag5+89t5Wn8h\nn84B7ES734nbx6ya9/gDAWRIEvs8BHzgHaelXtfaXu+tm3cjqNsQtbprZZnNE7O6\nJoHIwiYFqlkgA9qGQnMyIT/vYCcJTtnokIoVdTN9IpMU7Lyo2rsxy0I0Q/rtUekg\nBOjMnuzHdt91ngSyg3BCbhKn\n-----END PRIVATE KEY-----\n",
|
| 17 |
+
"client_email": "vservers-uploader@vservers-40.iam.gserviceaccount.com",
|
| 18 |
"token_uri": "https://oauth2.googleapis.com/token",
|
|
|
|
|
|
|
| 19 |
}
|
| 20 |
|
| 21 |
+
_drive_token = None
|
| 22 |
+
_drive_token_exp = 0
|
| 23 |
+
|
| 24 |
+
def get_drive_token():
|
| 25 |
+
global _drive_token, _drive_token_exp
|
| 26 |
+
now = time.time()
|
| 27 |
+
if _drive_token and now < _drive_token_exp - 60:
|
| 28 |
+
return _drive_token
|
| 29 |
+
import jwt as pyjwt
|
| 30 |
+
private_key = DRIVE_SA["private_key"]
|
| 31 |
+
claim = {
|
| 32 |
+
"iss": DRIVE_SA["client_email"],
|
| 33 |
+
"scope": "https://www.googleapis.com/auth/drive",
|
| 34 |
+
"aud": DRIVE_SA["token_uri"],
|
| 35 |
+
"iat": int(now),
|
| 36 |
+
"exp": int(now) + 3600,
|
| 37 |
+
}
|
| 38 |
+
signed = pyjwt.encode(claim, private_key, algorithm="RS256")
|
| 39 |
+
r = requests.post(DRIVE_SA["token_uri"], data={
|
| 40 |
+
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
| 41 |
+
"assertion": signed,
|
| 42 |
+
}, timeout=15)
|
| 43 |
+
r.raise_for_status()
|
| 44 |
+
_drive_token = r.json()["access_token"]
|
| 45 |
+
_drive_token_exp = now + 3600
|
| 46 |
+
return _drive_token
|
| 47 |
+
|
| 48 |
+
def drive_headers():
|
| 49 |
+
return {"Authorization": f"Bearer {get_drive_token()}"}
|
| 50 |
+
|
| 51 |
+
def get_or_create_folder(name, parent_id):
|
| 52 |
+
"""Găsește sau creează un subfolder în Drive."""
|
| 53 |
+
token = get_drive_token()
|
| 54 |
+
q = f"name='{name}' and '{parent_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false"
|
| 55 |
+
r = requests.get("https://www.googleapis.com/drive/v3/files",
|
| 56 |
+
params={"q": q, "fields": "files(id,name)"},
|
| 57 |
+
headers=drive_headers(), timeout=15)
|
| 58 |
+
files = r.json().get("files", [])
|
| 59 |
+
if files:
|
| 60 |
+
return files[0]["id"]
|
| 61 |
+
# Creaza folder
|
| 62 |
+
r2 = requests.post("https://www.googleapis.com/drive/v3/files",
|
| 63 |
+
headers={**drive_headers(), "Content-Type": "application/json"},
|
| 64 |
+
json={"name": name, "mimeType": "application/vnd.google-apps.folder", "parents": [parent_id]},
|
| 65 |
+
timeout=15)
|
| 66 |
+
return r2.json()["id"]
|
| 67 |
+
|
| 68 |
+
# ── Ruta upload ───────────────────────────────────────────────
|
| 69 |
+
@app.route("/drive/upload", methods=["POST"])
|
| 70 |
+
def drive_upload():
|
| 71 |
+
try:
|
| 72 |
+
elev_id = request.form.get("elevId", "unknown")
|
| 73 |
+
materie = request.form.get("materieId", "general")
|
| 74 |
+
f = request.files.get("file")
|
| 75 |
+
if not f:
|
| 76 |
+
return jsonify({"error": "no file"}), 400
|
| 77 |
+
|
| 78 |
+
# Structura: VSERVERS / elevId / materieId / fisier.ext
|
| 79 |
+
elev_folder = get_or_create_folder(elev_id, DRIVE_FOLDER_ID)
|
| 80 |
+
materie_folder = get_or_create_folder(materie, elev_folder)
|
| 81 |
+
|
| 82 |
+
# Upload multipart
|
| 83 |
+
metadata = json.dumps({"name": f.filename, "parents": [materie_folder]})
|
| 84 |
+
r = requests.post(
|
| 85 |
+
"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name,size",
|
| 86 |
+
headers={"Authorization": f"Bearer {get_drive_token()}"},
|
| 87 |
+
files={
|
| 88 |
+
"metadata": ("metadata", metadata, "application/json"),
|
| 89 |
+
"file": (f.filename, f.stream, f.mimetype or "application/octet-stream"),
|
| 90 |
+
},
|
| 91 |
+
timeout=120
|
| 92 |
+
)
|
| 93 |
+
data = r.json()
|
| 94 |
+
return jsonify({"fileId": data.get("id"), "name": data.get("name"), "size": data.get("size")})
|
| 95 |
+
except Exception as e:
|
| 96 |
+
print(f"[DRIVE UPLOAD ERROR] {e}")
|
| 97 |
+
return jsonify({"error": str(e)}), 500
|
| 98 |
+
|
| 99 |
+
# ── Ruta list ─────────────────────────────────────────────────
|
| 100 |
+
@app.route("/drive/list", methods=["GET"])
|
| 101 |
+
def drive_list():
|
| 102 |
+
try:
|
| 103 |
+
elev_id = request.args.get("elevId", "")
|
| 104 |
+
materie = request.args.get("materieId", "")
|
| 105 |
+
|
| 106 |
+
# Gaseste folderele
|
| 107 |
+
q_elev = f"name='{elev_id}' and '{DRIVE_FOLDER_ID}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false"
|
| 108 |
+
r1 = requests.get("https://www.googleapis.com/drive/v3/files",
|
| 109 |
+
params={"q": q_elev, "fields": "files(id)"}, headers=drive_headers(), timeout=15)
|
| 110 |
+
ef = r1.json().get("files", [])
|
| 111 |
+
if not ef:
|
| 112 |
+
return jsonify({"files": []})
|
| 113 |
+
|
| 114 |
+
q_mat = f"name='{materie}' and '{ef[0]['id']}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false"
|
| 115 |
+
r2 = requests.get("https://www.googleapis.com/drive/v3/files",
|
| 116 |
+
params={"q": q_mat, "fields": "files(id)"}, headers=drive_headers(), timeout=15)
|
| 117 |
+
mf = r2.json().get("files", [])
|
| 118 |
+
if not mf:
|
| 119 |
+
return jsonify({"files": []})
|
| 120 |
+
|
| 121 |
+
# Lista fisiere din folder
|
| 122 |
+
q_files = f"'{mf[0]['id']}' in parents and trashed=false"
|
| 123 |
+
r3 = requests.get("https://www.googleapis.com/drive/v3/files",
|
| 124 |
+
params={"q": q_files, "fields": "files(id,name,size,mimeType,createdTime)"},
|
| 125 |
+
headers=drive_headers(), timeout=15)
|
| 126 |
+
return jsonify({"files": r3.json().get("files", [])})
|
| 127 |
+
except Exception as e:
|
| 128 |
+
print(f"[DRIVE LIST ERROR] {e}")
|
| 129 |
+
return jsonify({"error": str(e)}), 500
|
| 130 |
|
| 131 |
+
# ── Ruta download (proxy) ─────────────────────────────────────
|
| 132 |
+
@app.route("/drive/download/<file_id>", methods=["GET"])
|
| 133 |
+
def drive_download(file_id):
|
| 134 |
+
try:
|
| 135 |
+
# Metadata
|
| 136 |
+
r_meta = requests.get(f"https://www.googleapis.com/drive/v3/files/{file_id}",
|
| 137 |
+
params={"fields": "name,mimeType"}, headers=drive_headers(), timeout=15)
|
| 138 |
+
meta = r_meta.json()
|
| 139 |
+
# Continut
|
| 140 |
+
r_file = requests.get(f"https://www.googleapis.com/drive/v3/files/{file_id}?alt=media",
|
| 141 |
+
headers=drive_headers(), timeout=120, stream=True)
|
| 142 |
+
headers = {
|
| 143 |
+
"Content-Disposition": f"attachment; filename=\"{meta.get('name','file')}\"",
|
| 144 |
+
"Content-Type": meta.get("mimeType", "application/octet-stream"),
|
| 145 |
+
}
|
| 146 |
+
return Response(r_file.iter_content(chunk_size=8192), headers=headers)
|
| 147 |
+
except Exception as e:
|
| 148 |
+
print(f"[DRIVE DOWNLOAD ERROR] {e}")
|
| 149 |
+
return jsonify({"error": str(e)}), 500
|
| 150 |
|
| 151 |
+
# ── Seed Firestore ────────────────────────────────────────────
|
| 152 |
ELEVI = [
|
| 153 |
+
{"pozitie":1,"nume":"Andronic Aniela","vpassId":"AA-0001"},
|
| 154 |
+
{"pozitie":2,"nume":"Badan Alexandra","vpassId":"BA-0002"},
|
| 155 |
+
{"pozitie":3,"nume":"Bunescu Lavinia","vpassId":"BL-0003"},
|
| 156 |
+
{"pozitie":4,"nume":"Bunescu Lorena","vpassId":"BL-0004"},
|
| 157 |
+
{"pozitie":5,"nume":"Burdujan Damian","vpassId":"BD-0005"},
|
| 158 |
+
{"pozitie":6,"nume":"Ceban Madalina","vpassId":"CM-0006"},
|
| 159 |
+
{"pozitie":7,"nume":"Ceban Valeria","vpassId":"CV-0007"},
|
| 160 |
+
{"pozitie":8,"nume":"Cebanu Nichita","vpassId":"CN-0008"},
|
| 161 |
+
{"pozitie":9,"nume":"Chele Ilinca","vpassId":"CI-0009"},
|
| 162 |
+
{"pozitie":10,"nume":"Chihai Mirela","vpassId":"CM-0010"},
|
| 163 |
+
{"pozitie":11,"nume":"Cojocari Ciprian","vpassId":"CC-0011"},
|
| 164 |
+
{"pozitie":12,"nume":"Creciun Leonard","vpassId":"CL-0012"},
|
| 165 |
+
{"pozitie":13,"nume":"Lozovan Sanda","vpassId":"LS-0013"},
|
| 166 |
+
{"pozitie":14,"nume":"Lungu Sergiu","vpassId":"LS-0014"},
|
| 167 |
+
{"pozitie":15,"nume":"Mahnea Adelina","vpassId":"MA-0015"},
|
| 168 |
+
{"pozitie":16,"nume":"Morcan Ina","vpassId":"MI-0016"},
|
| 169 |
+
{"pozitie":17,"nume":"Morcan Ion","vpassId":"MI-0017"},
|
| 170 |
+
{"pozitie":18,"nume":"Popa Cristina","vpassId":"PC-0018"},
|
| 171 |
+
{"pozitie":19,"nume":"Pricob Adelina","vpassId":"PA-0019"},
|
| 172 |
+
{"pozitie":20,"nume":"Prinos Catalina","vpassId":"PC-0020"},
|
| 173 |
+
{"pozitie":21,"nume":"Radu Daniela","vpassId":"RD-0021"},
|
| 174 |
+
{"pozitie":22,"nume":"Rosca Victor","vpassId":"RV-0022"},
|
| 175 |
+
{"pozitie":23,"nume":"Rotari Cristian","vpassId":"RC-0023"},
|
| 176 |
+
{"pozitie":24,"nume":"Rotaru Camelia","vpassId":"RC-0024"},
|
| 177 |
+
{"pozitie":25,"nume":"Sambris Stanislav","vpassId":"SS-0025"},
|
| 178 |
+
{"pozitie":26,"nume":"Sirbu Delia","vpassId":"SD-0026"},
|
| 179 |
+
{"pozitie":27,"nume":"Soltoian Elena","vpassId":"SE-0027"},
|
| 180 |
+
{"pozitie":28,"nume":"Tabuncic Chiril","vpassId":"TC-0028"},
|
| 181 |
+
{"pozitie":29,"nume":"Turcan Gabriel","vpassId":"TG-0029"},
|
| 182 |
+
{"pozitie":30,"nume":"Turcan Mihai","vpassId":"TM-0030"},
|
| 183 |
+
{"pozitie":31,"nume":"Turcanu Ilie","vpassId":"TI-0031"},
|
| 184 |
+
{"pozitie":32,"nume":"Virlan Daniela","vpassId":"VD-0032"},
|
| 185 |
]
|
| 186 |
|
| 187 |
+
def fs_add(col, data):
|
| 188 |
+
fields={}
|
| 189 |
+
for k,v in data.items():
|
| 190 |
+
if v is None: fields[k]={"nullValue":None}
|
| 191 |
+
elif isinstance(v,bool): fields[k]={"booleanValue":v}
|
| 192 |
+
elif isinstance(v,int): fields[k]={"integerValue":str(v)}
|
| 193 |
+
else: fields[k]={"stringValue":str(v)}
|
| 194 |
+
r=requests.post(f"{FIRESTORE_BASE}/{col}?key={FIREBASE_API_KEY}",json={"fields":fields},timeout=15)
|
| 195 |
+
return r.status_code==200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
|
| 197 |
def auto_seed():
|
| 198 |
+
time.sleep(4)
|
| 199 |
+
print("[VSERVERS] Verificare DB...")
|
|
|
|
|
|
|
| 200 |
try:
|
| 201 |
+
r=requests.get(f"{FIRESTORE_BASE}/elevi?key={FIREBASE_API_KEY}&pageSize=1",timeout=15)
|
| 202 |
+
if r.json().get("documents"):
|
| 203 |
+
print("[VSERVERS] DB populat — seed omis."); return
|
| 204 |
+
print("[VSERVERS] Seed automat pornit...")
|
| 205 |
+
ok=0
|
|
|
|
|
|
|
| 206 |
for e in ELEVI:
|
| 207 |
+
if fs_add("elevi",{"nume":e["nume"],"vpassId":e["vpassId"],"pozitie":e["pozitie"],"pin":None,"confirmed":False}):
|
| 208 |
+
ok+=1; print(f"[VSERVERS] ✓ {e['vpassId']}")
|
| 209 |
+
time.sleep(0.12)
|
| 210 |
+
print(f"[VSERVERS] Seed gata: {ok}/{len(ELEVI)}")
|
| 211 |
+
except Exception as ex: print(f"[VSERVERS] Seed eroare: {ex}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
|
| 213 |
+
threading.Thread(target=auto_seed,daemon=True).start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
@app.route("/")
|
| 216 |
+
def index(): return send_from_directory("static","index.html")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
+
@app.errorhandler(404)
|
| 219 |
+
def not_found(e): return send_from_directory("static","404.html"),404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
+
@app.route("/<path:filename>")
|
| 222 |
+
def serve_files(filename):
|
| 223 |
+
try: return send_from_directory("static",filename)
|
| 224 |
+
except: return send_from_directory("static","404.html"),404
|
| 225 |
|
| 226 |
+
if __name__=="__main__": app.run(host="0.0.0.0",port=7860,debug=False)
|
|
|