feat: Unterstützung für Laufwerksformatierung hinzugefügt; Versionsnummer auf 1.0.66 erhöht
This commit is contained in:
153
app.py
153
app.py
@@ -2023,6 +2023,84 @@ def r_update_check():
|
|||||||
return jsonify(ok=True)
|
return jsonify(ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
FORMAT_FILESYSTEMS = {
|
||||||
|
'exfat': {
|
||||||
|
'label': 'exFAT',
|
||||||
|
'desc': 'Empfohlen – Mac & Windows, keine 4-GB-Dateigrößenbeschränkung',
|
||||||
|
'cmd': lambda dev, name: ['mkfs.exfat', '-n', name, dev],
|
||||||
|
'pkg': 'exfatprogs',
|
||||||
|
},
|
||||||
|
'fat32': {
|
||||||
|
'label': 'FAT32',
|
||||||
|
'desc': 'Mac & Windows, max. 4 GB pro Datei',
|
||||||
|
'cmd': lambda dev, name: ['mkfs.vfat', '-F', '32', '-n', name[:11], dev],
|
||||||
|
'pkg': 'dosfstools',
|
||||||
|
},
|
||||||
|
'ntfs': {
|
||||||
|
'label': 'NTFS',
|
||||||
|
'desc': 'Windows nativ, Mac nur lesen',
|
||||||
|
'cmd': lambda dev, name: ['mkfs.ntfs', '-f', '-L', name[:32], dev],
|
||||||
|
'pkg': 'ntfs-3g',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
format_state = {'running': False, 'error': None, 'done': False, 'fs': '', 'device': ''}
|
||||||
|
|
||||||
|
@app.route('/api/format/status')
|
||||||
|
def r_format_status():
|
||||||
|
return jsonify(dict(format_state))
|
||||||
|
|
||||||
|
@app.route('/api/format', methods=['POST'])
|
||||||
|
def r_format():
|
||||||
|
if format_state['running']:
|
||||||
|
return jsonify(error='Formatierung läuft bereits'), 409
|
||||||
|
if copy_state.get('running'):
|
||||||
|
return jsonify(error='Kopiervorgang läuft – bitte warten'), 409
|
||||||
|
|
||||||
|
body = request.get_json(force=True)
|
||||||
|
fs = body.get('fs', '').lower()
|
||||||
|
name = (body.get('name') or 'PICOPY').upper()
|
||||||
|
dev = body.get('device', '')
|
||||||
|
|
||||||
|
if fs not in FORMAT_FILESYSTEMS:
|
||||||
|
return jsonify(error=f'Unbekanntes Dateisystem: {fs}'), 400
|
||||||
|
if not dev.startswith('/dev/'):
|
||||||
|
return jsonify(error='Ungültiges Gerät'), 400
|
||||||
|
|
||||||
|
# Sicherheitscheck: Gerät muss ein bekanntes USB-Gerät sein
|
||||||
|
known = [d['device'] for d in usb_devices()]
|
||||||
|
if dev not in known:
|
||||||
|
return jsonify(error='Gerät nicht als USB-Laufwerk erkannt'), 400
|
||||||
|
|
||||||
|
def do_format():
|
||||||
|
format_state.update(running=True, error=None, done=False, fs=fs, device=dev)
|
||||||
|
try:
|
||||||
|
# Aushängen falls gemountet
|
||||||
|
subprocess.run(['umount', dev], capture_output=True)
|
||||||
|
|
||||||
|
cmd = FORMAT_FILESYSTEMS[fs]['cmd'](dev, name)
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||||
|
if r.returncode != 0:
|
||||||
|
err = r.stderr.strip() or r.stdout.strip() or 'Unbekannter Fehler'
|
||||||
|
# Hilfreiche Meldung wenn Paket fehlt
|
||||||
|
pkg = FORMAT_FILESYSTEMS[fs]['pkg']
|
||||||
|
if 'not found' in err or r.returncode == 127:
|
||||||
|
err = f'Befehl nicht gefunden – bitte installieren: apt install {pkg}'
|
||||||
|
format_state.update(error=err)
|
||||||
|
return
|
||||||
|
format_state.update(done=True)
|
||||||
|
log.info(f'Formatierung {fs} auf {dev} abgeschlossen')
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
format_state.update(error='Timeout – Formatierung dauerte zu lange')
|
||||||
|
except Exception as e:
|
||||||
|
format_state.update(error=str(e))
|
||||||
|
finally:
|
||||||
|
format_state['running'] = False
|
||||||
|
|
||||||
|
threading.Thread(target=do_format, daemon=True).start()
|
||||||
|
return jsonify(ok=True)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/system/reboot', methods=['POST'])
|
@app.route('/api/system/reboot', methods=['POST'])
|
||||||
def r_system_reboot():
|
def r_system_reboot():
|
||||||
threading.Thread(target=lambda: (
|
threading.Thread(target=lambda: (
|
||||||
@@ -2432,10 +2510,35 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
|||||||
<option value="">- Gerät einstecken, dann hier wählen -</option>
|
<option value="">- Gerät einstecken, dann hier wählen -</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn pri" style="width:100%" onclick="assignPort('dest')">✓ Als festes Ziel speichern</button>
|
<div style="display:flex;gap:.5rem">
|
||||||
|
<button class="btn pri" style="flex:1" onclick="assignPort('dest')">✓ Als festes Ziel speichern</button>
|
||||||
|
<button class="btn" id="fmt-toggle-btn" style="flex:0 0 auto;display:none" onclick="toggleFmtBox()" title="Laufwerk formatieren">🔢 Formatieren</button>
|
||||||
|
</div>
|
||||||
<div id="dst-flash" class="flash" style="margin-top:.4rem"></div>
|
<div id="dst-flash" class="flash" style="margin-top:.4rem"></div>
|
||||||
<div class="hint-box" id="dst-hint">Gerät in den gewünschten Port → aus Liste wählen → Speichern. Ab dann wird dieser Port immer als Ziel verwendet.</div>
|
<div class="hint-box" id="dst-hint">Gerät in den gewünschten Port → aus Liste wählen → Speichern. Ab dann wird dieser Port immer als Ziel verwendet.</div>
|
||||||
|
|
||||||
|
<!-- Formatieren -->
|
||||||
|
<div id="fmt-box" style="display:none;margin-top:.75rem;padding:.7rem .75rem;background:var(--bg2);border:1px solid var(--brd);border-radius:.5rem">
|
||||||
|
<div style="font-weight:700;font-size:.83rem;margin-bottom:.55rem">🔢 Laufwerk formatieren</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Dateisystem</label>
|
||||||
|
<select id="fmt-fs">
|
||||||
|
<option value="exfat">exFAT – empfohlen (Mac & Windows, keine 4-GB-Grenze)</option>
|
||||||
|
<option value="fat32">FAT32 – Mac & Windows, max. 4 GB pro Datei</option>
|
||||||
|
<option value="ntfs">NTFS – Windows nativ, Mac nur lesen</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Bezeichnung (Volume-Name)</label>
|
||||||
|
<input type="text" id="fmt-name" value="PICOPY" maxlength="32" style="text-transform:uppercase">
|
||||||
|
</div>
|
||||||
|
<div style="background:rgba(248,113,113,.08);border:1px solid rgba(248,113,113,.35);border-radius:.4rem;padding:.45rem .6rem;font-size:.75rem;color:var(--red);margin-bottom:.55rem">
|
||||||
|
⚠ <strong>Achtung:</strong> Alle Daten auf dem Laufwerk werden unwiderruflich gelöscht!
|
||||||
|
</div>
|
||||||
|
<button class="btn" style="width:100%;background:rgba(248,113,113,.15);border-color:var(--red);color:var(--red)" onclick="startFormat()">Jetzt formatieren</button>
|
||||||
|
<div id="fmt-flash" class="flash" style="margin-top:.4rem"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="internal-share-box" style="display:none;margin-top:.75rem;padding:.65rem .75rem;background:var(--bg2);border:1px solid var(--brd);border-radius:.5rem">
|
<div id="internal-share-box" style="display:none;margin-top:.75rem;padding:.65rem .75rem;background:var(--bg2);border:1px solid var(--brd);border-radius:.5rem">
|
||||||
<div style="display:flex;align-items:center;gap:.55rem;justify-content:space-between">
|
<div style="display:flex;align-items:center;gap:.55rem;justify-content:space-between">
|
||||||
<div style="min-width:0">
|
<div style="min-width:0">
|
||||||
@@ -2971,6 +3074,8 @@ function populateSel(){
|
|||||||
dstEl.innerHTML = blank('Gerät einstecken, dann hier wählen')
|
dstEl.innerHTML = blank('Gerät einstecken, dann hier wählen')
|
||||||
+ mkOpts(d => !srcSet.has(d.usb_port));
|
+ mkOpts(d => !srcSet.has(d.usb_port));
|
||||||
if(dstPrev && devs.find(d=>d.usb_port===dstPrev)) dstEl.value=dstPrev;
|
if(dstPrev && devs.find(d=>d.usb_port===dstPrev)) dstEl.value=dstPrev;
|
||||||
|
dstEl.onchange=updateFmtToggleBtn;
|
||||||
|
updateFmtToggleBtn();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDestTypeChange(markDirty=true){
|
function onDestTypeChange(markDirty=true){
|
||||||
@@ -2985,6 +3090,8 @@ function onDestTypeChange(markDirty=true){
|
|||||||
updateInternalShareBox();
|
updateInternalShareBox();
|
||||||
renderSlot('dst',cfg.dest_port,cfg.dest_label);
|
renderSlot('dst',cfg.dest_port,cfg.dest_label);
|
||||||
renderExplorerTabs();
|
renderExplorerTabs();
|
||||||
|
updateFmtToggleBtn();
|
||||||
|
if(type==='internal'){$('fmt-box').style.display='none';}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderUnassigned(){
|
function renderUnassigned(){
|
||||||
@@ -3267,6 +3374,50 @@ async function toggleInternalShare(){
|
|||||||
updateInternalShareBox(r.status);
|
updateInternalShareBox(r.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- Format -------------------------------------------------------------------
|
||||||
|
let fmtPolling=null;
|
||||||
|
|
||||||
|
function toggleFmtBox(){
|
||||||
|
const box=$('fmt-box');
|
||||||
|
const visible=box.style.display!=='none';
|
||||||
|
box.style.display=visible?'none':'block';
|
||||||
|
if(!visible) $('fmt-flash').textContent='';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFmtToggleBtn(){
|
||||||
|
const btn=$('fmt-toggle-btn');
|
||||||
|
if(!btn) return;
|
||||||
|
const sel=$('dst-select').value;
|
||||||
|
const isUsb=($('dst-type')||{}).value!=='internal';
|
||||||
|
btn.style.display=(isUsb && sel)?'':'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startFormat(){
|
||||||
|
const sel=$('dst-select').value;
|
||||||
|
if(!sel){flash('fmt-flash','err','Kein Gerät ausgewählt');return;}
|
||||||
|
const dev=devs.find(d=>d.usb_port===sel);
|
||||||
|
if(!dev){flash('fmt-flash','err','Gerät nicht gefunden');return;}
|
||||||
|
const fs=$('fmt-fs').value;
|
||||||
|
const name=($('fmt-name').value||'PICOPY').toUpperCase();
|
||||||
|
const fsLabel={'exfat':'exFAT','fat32':'FAT32','ntfs':'NTFS'}[fs]||fs;
|
||||||
|
if(!confirm(`Laufwerk "${dev.label||dev.device}" (${dev.size}) wirklich mit ${fsLabel} formatieren?\n\nAlle Daten werden gelöscht!`))return;
|
||||||
|
|
||||||
|
flash('fmt-flash','ok','Formatierung läuft...');
|
||||||
|
const r=await api('/format','POST',{fs,name,device:dev.device});
|
||||||
|
if(r.error){flash('fmt-flash','err',r.error);return;}
|
||||||
|
|
||||||
|
clearInterval(fmtPolling);
|
||||||
|
fmtPolling=setInterval(async()=>{
|
||||||
|
const s=await api('/format/status');
|
||||||
|
if(s.error){clearInterval(fmtPolling);flash('fmt-flash','err',s.error);return;}
|
||||||
|
if(s.done){
|
||||||
|
clearInterval(fmtPolling);
|
||||||
|
flash('fmt-flash','ok',`✓ Erfolgreich als ${fsLabel} formatiert`);
|
||||||
|
setTimeout(pollDevices,1500);
|
||||||
|
}
|
||||||
|
},800);
|
||||||
|
}
|
||||||
|
|
||||||
// -- File Explorer -------------------------------------------------------------
|
// -- File Explorer -------------------------------------------------------------
|
||||||
const expl={
|
const expl={
|
||||||
role:'src_0', paths:{dst:''},
|
role:'src_0', paths:{dst:''},
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1.0.65
|
1.0.66
|
||||||
|
|||||||
Reference in New Issue
Block a user