feat: Unterstützung für Laufwerksformatierung hinzugefügt; Versionsnummer auf 1.0.66 erhöht

This commit is contained in:
2026-05-10 16:31:29 +02:00
parent a15d6b0e27
commit b5881fc248
2 changed files with 153 additions and 2 deletions

153
app.py
View File

@@ -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')">✓&nbsp;Als festes Ziel speichern</button> <div style="display:flex;gap:.5rem">
<button class="btn pri" style="flex:1" onclick="assignPort('dest')">✓&nbsp;Als festes Ziel speichern</button>
<button class="btn" id="fmt-toggle-btn" style="flex:0 0 auto;display:none" onclick="toggleFmtBox()" title="Laufwerk formatieren">&#128290;&nbsp;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 &rarr; aus Liste wählen &rarr; 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 &rarr; aus Liste wählen &rarr; 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">&#128290;&nbsp;Laufwerk formatieren</div>
<div class="field">
<label>Dateisystem</label>
<select id="fmt-fs">
<option value="exfat">exFAT empfohlen (Mac &amp; Windows, keine 4-GB-Grenze)</option>
<option value="fat32">FAT32 Mac &amp; 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">
&#9888;&nbsp;<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:''},

View File

@@ -1 +1 @@
1.0.65 1.0.66