feat: Unterstützung für internen Speicher und SMB-Freigabe hinzugefügt; Versionsnummer auf 1.0.51 erhöht
This commit is contained in:
314
app.py
314
app.py
@@ -38,6 +38,7 @@ CONFIG_FILE = BASE_DIR / 'config.json'
|
||||
STATE_FILE = BASE_DIR / 'state.json'
|
||||
LOG_DIR = BASE_DIR / 'logs'
|
||||
LOG_FILE = LOG_DIR / 'picopy.log'
|
||||
INTERNAL_DEST_DIR = BASE_DIR / 'internal'
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logging.basicConfig(
|
||||
@@ -56,6 +57,8 @@ DEFAULT_CONFIG = {
|
||||
'source_ports': [], # [{port, label}, ...]
|
||||
'source_port': None, 'source_label': '', # Migration legacy
|
||||
'dest_port': None, 'dest_label': '',
|
||||
'dest_type': 'usb', 'internal_dest_label': 'Interner Speicher',
|
||||
'internal_share_enabled': False,
|
||||
'folder_format': '%Y-%m-%d', 'add_time': True,
|
||||
'subfolder': True, 'auto_copy': True,
|
||||
'file_filter': '', 'exclude_system': True,
|
||||
@@ -408,6 +411,163 @@ def wg_save_config(content: str):
|
||||
return False, str(e)
|
||||
|
||||
|
||||
# -- Interner Speicher / SMB-Freigabe -----------------------------------------
|
||||
|
||||
SAMBA_CONF = Path('/etc/samba/smb.conf')
|
||||
SAMBA_BEGIN = '# BEGIN PICOPY INTERNAL SHARE'
|
||||
SAMBA_END = '# END PICOPY INTERNAL SHARE'
|
||||
|
||||
internal_share_state = {
|
||||
'installed': False,
|
||||
'enabled': False,
|
||||
'active': False,
|
||||
'path': str(INTERNAL_DEST_DIR),
|
||||
'share': 'PiCopy',
|
||||
'pkg_running': False,
|
||||
'pkg_error': None,
|
||||
'error': None,
|
||||
}
|
||||
internal_share_lock = threading.Lock()
|
||||
|
||||
|
||||
def _internal_usage():
|
||||
INTERNAL_DEST_DIR.mkdir(parents=True, exist_ok=True)
|
||||
usage = shutil.disk_usage(INTERNAL_DEST_DIR)
|
||||
return {
|
||||
'path': str(INTERNAL_DEST_DIR),
|
||||
'total': usage.total,
|
||||
'used': usage.used,
|
||||
'free': usage.free,
|
||||
}
|
||||
|
||||
|
||||
def internal_dest_device(cfg=None):
|
||||
cfg = cfg or load_cfg()
|
||||
usage = _internal_usage()
|
||||
return {
|
||||
'device': 'internal',
|
||||
'usb_port': '__internal__',
|
||||
'mount': str(INTERNAL_DEST_DIR),
|
||||
'label': cfg.get('internal_dest_label') or 'Interner Speicher',
|
||||
'size': _fmt_bytes(usage['free']) + ' frei',
|
||||
'internal': True,
|
||||
}
|
||||
|
||||
|
||||
def smbd_installed():
|
||||
return shutil.which('smbd') is not None
|
||||
|
||||
|
||||
def _systemctl(*args, timeout=20):
|
||||
try:
|
||||
return subprocess.run(['systemctl'] + list(args), capture_output=True,
|
||||
text=True, timeout=timeout)
|
||||
except Exception as e:
|
||||
return subprocess.CompletedProcess(['systemctl'] + list(args), 1,
|
||||
stdout='', stderr=str(e))
|
||||
|
||||
|
||||
def _smbd_active():
|
||||
if not smbd_installed():
|
||||
return False
|
||||
r = _systemctl('is-active', 'smbd', timeout=5)
|
||||
return r.returncode == 0 and r.stdout.strip() == 'active'
|
||||
|
||||
|
||||
def internal_share_update_state():
|
||||
cfg = load_cfg()
|
||||
usage = _internal_usage()
|
||||
with internal_share_lock:
|
||||
internal_share_state.update(
|
||||
installed=smbd_installed(),
|
||||
enabled=bool(cfg.get('internal_share_enabled')),
|
||||
active=_smbd_active(),
|
||||
path=usage['path'],
|
||||
total=usage['total'],
|
||||
used=usage['used'],
|
||||
free=usage['free'],
|
||||
)
|
||||
return dict(internal_share_state)
|
||||
|
||||
|
||||
def _write_samba_share(enabled: bool):
|
||||
old = SAMBA_CONF.read_text(encoding='utf-8') if SAMBA_CONF.exists() else ''
|
||||
pattern = re.compile(rf'\n?{re.escape(SAMBA_BEGIN)}.*?{re.escape(SAMBA_END)}\n?', re.S)
|
||||
cleaned = pattern.sub('\n', old).rstrip() + '\n'
|
||||
if enabled:
|
||||
INTERNAL_DEST_DIR.mkdir(parents=True, exist_ok=True)
|
||||
INTERNAL_DEST_DIR.chmod(0o755)
|
||||
block = f"""
|
||||
{SAMBA_BEGIN}
|
||||
[PiCopy]
|
||||
path = {INTERNAL_DEST_DIR}
|
||||
browseable = yes
|
||||
read only = yes
|
||||
guest ok = yes
|
||||
force user = root
|
||||
{SAMBA_END}
|
||||
"""
|
||||
cleaned += block
|
||||
tmp = SAMBA_CONF.with_suffix('.conf.picopy_tmp')
|
||||
tmp.write_text(cleaned, encoding='utf-8')
|
||||
os.replace(str(tmp), str(SAMBA_CONF))
|
||||
|
||||
|
||||
def _install_samba_if_needed():
|
||||
if smbd_installed():
|
||||
return True, ''
|
||||
with internal_share_lock:
|
||||
internal_share_state.update(pkg_running=True, pkg_error=None)
|
||||
try:
|
||||
r = subprocess.run(['apt-get', 'install', '-y', 'samba'],
|
||||
capture_output=True, text=True, timeout=300,
|
||||
env={**os.environ, 'DEBIAN_FRONTEND': 'noninteractive'})
|
||||
if r.returncode != 0:
|
||||
err = (r.stderr.strip().splitlines()[-1]
|
||||
if r.stderr.strip() else 'samba-Installation fehlgeschlagen')
|
||||
with internal_share_lock:
|
||||
internal_share_state['pkg_error'] = err
|
||||
return False, err
|
||||
return True, ''
|
||||
except Exception as e:
|
||||
with internal_share_lock:
|
||||
internal_share_state['pkg_error'] = str(e)
|
||||
return False, str(e)
|
||||
finally:
|
||||
with internal_share_lock:
|
||||
internal_share_state['pkg_running'] = False
|
||||
|
||||
|
||||
def set_internal_share_enabled(enabled: bool):
|
||||
ok, err = (True, '')
|
||||
if enabled:
|
||||
ok, err = _install_samba_if_needed()
|
||||
if not ok:
|
||||
return False, err
|
||||
elif not smbd_installed():
|
||||
cfg = load_cfg()
|
||||
cfg['internal_share_enabled'] = False
|
||||
save_cfg(cfg)
|
||||
internal_share_update_state()
|
||||
return True, ''
|
||||
try:
|
||||
_write_samba_share(enabled)
|
||||
if enabled:
|
||||
_systemctl('enable', '--now', 'smbd', timeout=60)
|
||||
_systemctl('restart', 'smbd', timeout=60)
|
||||
else:
|
||||
_systemctl('restart', 'smbd', timeout=60)
|
||||
cfg = load_cfg()
|
||||
cfg['internal_share_enabled'] = enabled
|
||||
save_cfg(cfg)
|
||||
internal_share_update_state()
|
||||
return True, ''
|
||||
except Exception as e:
|
||||
with internal_share_lock:
|
||||
internal_share_state['error'] = str(e)
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def wg_monitor():
|
||||
while True:
|
||||
try:
|
||||
@@ -485,6 +645,9 @@ def usb_devices():
|
||||
return result
|
||||
|
||||
def ensure_mount(dev_info):
|
||||
if dev_info.get('internal'):
|
||||
INTERNAL_DEST_DIR.mkdir(parents=True, exist_ok=True)
|
||||
return str(INTERNAL_DEST_DIR), False
|
||||
mp = dev_info.get('mount')
|
||||
if mp:
|
||||
return mp, False
|
||||
@@ -586,6 +749,12 @@ def _resolve_source_ports(cfg) -> list:
|
||||
return ports
|
||||
|
||||
|
||||
def _configured_destination(cfg, devs):
|
||||
if cfg.get('dest_type') == 'internal':
|
||||
return internal_dest_device(cfg)
|
||||
return next((d for d in devs if d['usb_port'] == cfg.get('dest_port')), None)
|
||||
|
||||
|
||||
def do_copy(src_devs, dst_dev, cfg):
|
||||
"""Kopiert von einer oder mehreren Quellen auf ein Ziel."""
|
||||
dst_mp = None
|
||||
@@ -849,7 +1018,9 @@ def do_copy(src_devs, dst_dev, cfg):
|
||||
def check_auto_copy():
|
||||
cfg = load_cfg()
|
||||
src_ports = _resolve_source_ports(cfg)
|
||||
if not cfg.get('auto_copy') or not src_ports or not cfg.get('dest_port'):
|
||||
if not cfg.get('auto_copy') or not src_ports:
|
||||
return
|
||||
if cfg.get('dest_type') != 'internal' and not cfg.get('dest_port'):
|
||||
return
|
||||
with copy_lock:
|
||||
if copy_state['running'] or copy_state['error']:
|
||||
@@ -857,7 +1028,7 @@ def check_auto_copy():
|
||||
devs = usb_devices()
|
||||
srcs = [next((d for d in devs if d['usb_port'] == sp['port']), None) for sp in src_ports]
|
||||
srcs = [s for s in srcs if s is not None]
|
||||
dst = next((d for d in devs if d['usb_port'] == cfg['dest_port']), None)
|
||||
dst = _configured_destination(cfg, devs)
|
||||
if srcs and dst:
|
||||
log.info(f'Auto-Copy: {len(srcs)} Quelle(n) und Ziel verbunden')
|
||||
threading.Thread(target=do_copy, args=(srcs, dst, cfg), daemon=True).start()
|
||||
@@ -1272,7 +1443,8 @@ def r_status():
|
||||
ws = dict(wifi_state)
|
||||
with wg_lock:
|
||||
wgs = dict(wg_state)
|
||||
return jsonify(copy=cs, wifi=ws, vpn=wgs)
|
||||
share = internal_share_update_state()
|
||||
return jsonify(copy=cs, wifi=ws, vpn=wgs, internal_share=share)
|
||||
|
||||
@app.route('/api/copy/start', methods=['POST'])
|
||||
def r_start():
|
||||
@@ -1292,7 +1464,7 @@ def r_start():
|
||||
if wanted_ports is not None:
|
||||
srcs = [s for s in srcs if s['usb_port'] in wanted_ports]
|
||||
if not srcs: return jsonify(error='Keine Quellgeräte gefunden (Ports nicht verbunden)'), 400
|
||||
dst = next((d for d in devs if d['usb_port'] == cfg.get('dest_port')), None)
|
||||
dst = _configured_destination(cfg, devs)
|
||||
if not dst: return jsonify(error='Zielgerät nicht gefunden'), 400
|
||||
_copy_thread = threading.Thread(target=do_copy, args=(srcs, dst, cfg), daemon=True)
|
||||
_copy_thread.start()
|
||||
@@ -1304,6 +1476,21 @@ def r_cancel():
|
||||
copy_state['running'] = False
|
||||
return jsonify(ok=True)
|
||||
|
||||
|
||||
@app.route('/api/internal-share/status')
|
||||
def r_internal_share_status():
|
||||
return jsonify(internal_share_update_state())
|
||||
|
||||
|
||||
@app.route('/api/internal-share', methods=['POST'])
|
||||
def r_internal_share_set():
|
||||
data = request.get_json(force=True) or {}
|
||||
enabled = bool(data.get('enabled'))
|
||||
ok, err = set_internal_share_enabled(enabled)
|
||||
if not ok:
|
||||
return jsonify(error=err), 500
|
||||
return jsonify(ok=True, status=internal_share_update_state())
|
||||
|
||||
@app.route('/api/wifi/scan')
|
||||
def r_wifi_scan():
|
||||
nets = scan_wifi_networks()
|
||||
@@ -1561,6 +1748,9 @@ def _drop_browse_mount(port):
|
||||
|
||||
|
||||
def get_browse_mp(dev):
|
||||
if dev.get('internal'):
|
||||
INTERNAL_DEST_DIR.mkdir(parents=True, exist_ok=True)
|
||||
return str(INTERNAL_DEST_DIR)
|
||||
port = dev.get('usb_port', '')
|
||||
|
||||
# Auto-mount vom System bevorzugen
|
||||
@@ -1590,6 +1780,8 @@ def r_browse():
|
||||
rpath = request.args.get('path', '').lstrip('/')
|
||||
|
||||
devs = usb_devices()
|
||||
dev = internal_dest_device(load_cfg()) if port == '__internal__' else None
|
||||
if dev is None:
|
||||
dev = next((d for d in devs if d['usb_port'] == port), None)
|
||||
if not dev:
|
||||
return jsonify(error='Gerät nicht verbunden - bitte neu einstecken'), 404
|
||||
@@ -2065,9 +2257,16 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Bezeichnung</label>
|
||||
<input type="text" id="dst-label" placeholder="z.B. rechter schwarzer Port">
|
||||
<input type="text" id="dst-label" placeholder="z.B. Zielplatte oder Interner Speicher">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Zieltyp</label>
|
||||
<select id="dst-type" onchange="onDestTypeChange()">
|
||||
<option value="usb">USB-Laufwerk</option>
|
||||
<option value="internal">Interner Speicher vom Raspberry Pi</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" id="dst-usb-field">
|
||||
<label>Port lernen - Gerät wählen</label>
|
||||
<select id="dst-select">
|
||||
<option value="">- Gerät einstecken, dann hier wählen -</option>
|
||||
@@ -2075,7 +2274,18 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
||||
</div>
|
||||
<button class="btn pri" style="width:100%" onclick="assignPort('dest')">✓ Als festes Ziel speichern</button>
|
||||
<div id="dst-flash" class="flash" style="margin-top:.4rem"></div>
|
||||
<div class="hint-box">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>
|
||||
|
||||
<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="min-width:0">
|
||||
<div style="font-weight:700;font-size:.83rem">SMB-Freigabe</div>
|
||||
<div id="internal-share-detail" style="font-size:.72rem;color:var(--sub);margin-top:.15rem"></div>
|
||||
</div>
|
||||
<button class="btn sm" id="internal-share-btn" onclick="toggleInternalShare()">Freigeben</button>
|
||||
</div>
|
||||
<div id="internal-share-flash" class="flash" style="margin-top:.35rem"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /port-pair -->
|
||||
@@ -2421,12 +2631,17 @@ function toggleSrc(port, on){
|
||||
|
||||
function renderExplorerTabs(){
|
||||
const ports = cfg.source_ports || [];
|
||||
$('src-tabs').innerHTML = ports.map((sp, i) => {
|
||||
let tabs = ports.map((sp, i) => {
|
||||
const r = 'src_'+i;
|
||||
const label = sp.label || ('Quelle '+(i+1));
|
||||
return `<button class="etab ${expl.role===r?'on':''}" id="etab-${r}"
|
||||
onclick="expl.switchRole('${r}')">▲ ${label}</button>`;
|
||||
}).join('');
|
||||
if((cfg.dest_type||'usb')==='internal'){
|
||||
tabs += `<button class="etab ${expl.role==='dst'?'on':''}" id="etab-dst"
|
||||
onclick="expl.switchRole('dst')">▼ Intern</button>`;
|
||||
}
|
||||
$('src-tabs').innerHTML = tabs;
|
||||
// Fallback: falls aktive Rolle nicht mehr existiert
|
||||
if(expl.role!=='dst' && !ports.some((_,i)=>expl.role==='src_'+i)){
|
||||
expl.role = ports.length>0 ? 'src_0' : 'dst';
|
||||
@@ -2434,6 +2649,16 @@ function renderExplorerTabs(){
|
||||
}
|
||||
|
||||
function renderSlot(r, port, label){
|
||||
if(r==='dst' && (cfg.dest_type||'usb')==='internal'){
|
||||
const dot=$('dst-dot'), pp=$('dst-port-path'), pi=$('dst-dev-info');
|
||||
const sl=$('slot-dst'), lb=$('dst-label');
|
||||
sl.classList.add('dst-on');
|
||||
dot.className='dot on';
|
||||
pp.textContent='Interner Speicher';
|
||||
pi.textContent=(label||cfg.internal_dest_label||'Interner Speicher')+' | /opt/picopy/internal';
|
||||
if(lb && !lb.dataset.dirty) lb.value=label||cfg.internal_dest_label||'Interner Speicher';
|
||||
return;
|
||||
}
|
||||
const dev=devs.find(d=>d.usb_port===port);
|
||||
const dot=$(r+'-dot'), pp=$(r+'-port-path'), pi=$(r+'-dev-info');
|
||||
const sl=$('slot-'+r), lb=$(r+'-label');
|
||||
@@ -2457,7 +2682,7 @@ function populateSel(){
|
||||
|
||||
const srcEl=$('src-select'), srcPrev=srcEl.value;
|
||||
srcEl.innerHTML = blank('Gerät einstecken, dann hier wählen')
|
||||
+ mkOpts(d => !srcSet.has(d.usb_port) && d.usb_port !== cfg.dest_port);
|
||||
+ mkOpts(d => !srcSet.has(d.usb_port) && ((cfg.dest_type||'usb')==='internal' || d.usb_port !== cfg.dest_port));
|
||||
if(srcPrev && devs.find(d=>d.usb_port===srcPrev)) srcEl.value=srcPrev;
|
||||
|
||||
const dstEl=$('dst-select'), dstPrev=dstEl.value;
|
||||
@@ -2466,9 +2691,23 @@ function populateSel(){
|
||||
if(dstPrev && devs.find(d=>d.usb_port===dstPrev)) dstEl.value=dstPrev;
|
||||
}
|
||||
|
||||
function onDestTypeChange(markDirty=true){
|
||||
const type=$('dst-type').value;
|
||||
$('dst-usb-field').style.display=type==='internal'?'none':'';
|
||||
$('internal-share-box').style.display=type==='internal'?'block':'none';
|
||||
$('dst-hint').textContent=type==='internal'
|
||||
? 'Kopiert auf /opt/picopy/internal. Die Daten können optional als SMB-Freigabe im Netzwerk bereitgestellt werden.'
|
||||
: 'Gerät in den gewünschten Port → aus Liste wählen → Speichern. Ab dann wird dieser Port immer als Ziel verwendet.';
|
||||
if(type==='internal' && !$('dst-label').value) $('dst-label').value='Interner Speicher';
|
||||
if(markDirty) $('dst-label').dataset.dirty='1';
|
||||
updateInternalShareBox();
|
||||
renderSlot('dst',cfg.dest_port,cfg.dest_label);
|
||||
renderExplorerTabs();
|
||||
}
|
||||
|
||||
function renderUnassigned(){
|
||||
const srcSet = new Set((cfg.source_ports||[]).map(sp=>sp.port));
|
||||
const list=devs.filter(d=>!srcSet.has(d.usb_port)&&d.usb_port!==cfg.dest_port);
|
||||
const list=devs.filter(d=>!srcSet.has(d.usb_port)&&(((cfg.dest_type||'usb')==='internal')||d.usb_port!==cfg.dest_port));
|
||||
const w=$('unassigned-wrap');
|
||||
if(!list.length){w.style.display='none';return;}
|
||||
w.style.display='block';
|
||||
@@ -2483,7 +2722,7 @@ function renderUnassigned(){
|
||||
async function addSource(){
|
||||
const port=$('src-select').value, label=$('src-label').value.trim();
|
||||
if(!port){flash('src-flash','err','Bitte zuerst ein Gerät wählen.');return;}
|
||||
if(port===cfg.dest_port){flash('src-flash','err','Port bereits als Ziel konfiguriert!');return;}
|
||||
if((cfg.dest_type||'usb')!=='internal' && port===cfg.dest_port){flash('src-flash','err','Port bereits als Ziel konfiguriert!');return;}
|
||||
if((cfg.source_ports||[]).some(sp=>sp.port===port)){flash('src-flash','err','Port bereits als Quelle hinzugefügt!');return;}
|
||||
cfg.source_ports = [...(cfg.source_ports||[]), {port, label}];
|
||||
selectedPortSet.add(port);
|
||||
@@ -2503,9 +2742,22 @@ async function removeSource(port){
|
||||
async function assignPort(role){
|
||||
const sid='dst-select', lid='dst-label';
|
||||
const fid='dst-flash', pk='dest_port', lk='dest_label';
|
||||
const type=$('dst-type').value;
|
||||
const port=$(sid).value, label=$(lid).value.trim();
|
||||
if(type==='internal'){
|
||||
cfg.dest_type='internal';
|
||||
cfg.internal_dest_label=label||'Interner Speicher';
|
||||
cfg[lk]=cfg.internal_dest_label;
|
||||
$(lid).dataset.dirty='';
|
||||
await api('/config','POST',cfg);
|
||||
flash(fid,'ok','✓ Interner Speicher als Ziel gespeichert.');
|
||||
renderSlot('dst',cfg.dest_port,cfg.dest_label);
|
||||
renderExplorerTabs(); expl.reload();
|
||||
return;
|
||||
}
|
||||
if(!port){flash(fid,'err','Bitte zuerst ein Gerät wählen.');return;}
|
||||
if((cfg.source_ports||[]).some(sp=>sp.port===port)){flash(fid,'err','Port bereits als Quelle konfiguriert!');return;}
|
||||
cfg.dest_type='usb';
|
||||
cfg[pk]=port; cfg[lk]=label; $(lid).dataset.dirty='';
|
||||
await api('/config','POST',cfg);
|
||||
flash(fid,'ok','✓ Port '+port+' als Ziel gespeichert.');
|
||||
@@ -2545,6 +2797,8 @@ async function loadCfg(){
|
||||
$('c-verify').checked=!!cfg.verify_checksum;
|
||||
$('c-delsrc').checked=!!cfg.delete_source;
|
||||
$('w-ssid').value=cfg.wifi_ssid||''; $('ap-ssid').value=cfg.ap_ssid||'PiCopy';
|
||||
$('dst-type').value=cfg.dest_type||'usb';
|
||||
onDestTypeChange(false);
|
||||
}
|
||||
async function saveCopyCfg(){
|
||||
cfg.folder_format=$('c-fmt').value; cfg.add_time=$('c-time').checked;
|
||||
@@ -2681,6 +2935,37 @@ async function utDel(id,name){
|
||||
await api('/upload/targets/'+id,'DELETE');await loadUTs();
|
||||
}
|
||||
|
||||
async function updateInternalShareBox(state=null){
|
||||
if(!$('internal-share-box'))return;
|
||||
const s=state||await api('/internal-share/status');
|
||||
const btn=$('internal-share-btn'), detail=$('internal-share-detail');
|
||||
const free=s.free!=null?fmtBytes(s.free)+' frei':'';
|
||||
if(s.pkg_running){
|
||||
btn.disabled=true; btn.textContent='Installiere...';
|
||||
detail.textContent='Samba wird installiert. '+free;
|
||||
return;
|
||||
}
|
||||
btn.disabled=false;
|
||||
btn.textContent=s.enabled?'Freigabe stoppen':'Freigeben';
|
||||
const status=s.enabled
|
||||
? ((s.active?'Aktiv':'Konfiguriert')+' | \\\\'+(location.hostname||'picopy')+'\\PiCopy')
|
||||
: 'Nicht freigegeben';
|
||||
detail.textContent=status+(free?' | '+free:'');
|
||||
}
|
||||
|
||||
async function toggleInternalShare(){
|
||||
const current=await api('/internal-share/status');
|
||||
const enable=!current.enabled;
|
||||
if(enable && !current.installed){
|
||||
if(!confirm('Samba installieren und /opt/picopy/internal als Netzwerkfreigabe PiCopy bereitstellen?\n\nDie Freigabe ist lesbar im Netzwerk erreichbar.'))return;
|
||||
}
|
||||
flash('internal-share-flash','ok',enable?'Aktiviere Freigabe...':'Deaktiviere Freigabe...');
|
||||
const r=await api('/internal-share','POST',{enabled:enable});
|
||||
if(r.error){flash('internal-share-flash','err',r.error);return;}
|
||||
flash('internal-share-flash','ok',enable?'✓ Freigabe aktiv':'✓ Freigabe deaktiviert');
|
||||
updateInternalShareBox(r.status);
|
||||
}
|
||||
|
||||
// -- File Explorer -------------------------------------------------------------
|
||||
const expl={
|
||||
role:'src_0', paths:{dst:''},
|
||||
@@ -2695,14 +2980,16 @@ const expl={
|
||||
async load(path=''){
|
||||
let port;
|
||||
if(this.role==='dst'){
|
||||
port=cfg.dest_port;
|
||||
port=(cfg.dest_type||'usb')==='internal'?'__internal__':cfg.dest_port;
|
||||
} else {
|
||||
const idx=parseInt(this.role.replace('src_',''),10);
|
||||
port=cfg.source_ports&&cfg.source_ports[idx]?cfg.source_ports[idx].port:null;
|
||||
}
|
||||
const body=$('expl-body'), bread=$('expl-bread');
|
||||
if(!port){body.innerHTML='<div class="expl-empty">Kein Port konfiguriert</div>';bread.innerHTML='';return;}
|
||||
const dev=devs.find(d=>d.usb_port===port);
|
||||
const dev=port==='__internal__'
|
||||
? {usb_port:'__internal__',label:'Interner Speicher',device:'internal'}
|
||||
: devs.find(d=>d.usb_port===port);
|
||||
if(!dev){body.innerHTML='<div class="expl-empty">Gerät nicht verbunden</div>';bread.innerHTML='<span style="color:var(--sub)">Port '+port+' - nicht verbunden</span>';return;}
|
||||
body.innerHTML='<div class="expl-empty">Lade...</div>';
|
||||
try{
|
||||
@@ -2782,7 +3069,8 @@ function fmtSpd(bps){
|
||||
// -- Poll ----------------------------------------------------------------------
|
||||
async function poll(){
|
||||
try{
|
||||
const {copy:c,wifi:w,vpn:v}=await api('/status');
|
||||
const {copy:c,wifi:w,vpn:v,internal_share:is}=await api('/status');
|
||||
if(is) updateInternalShareBox(is);
|
||||
// VPN Topbar + Card
|
||||
if(v){
|
||||
const vp=$('vpn-pill'),vdot=$('vpn-dot'),vl=$('vpn-label'),vi=$('vpn-ip');
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.0.50
|
||||
1.0.51
|
||||
|
||||
Reference in New Issue
Block a user