Files
Aether/frontend/src/components/WebDavGuide.tsx
T
marcandClaude Opus 5 0ef7bd67ab feat(webdav): Anleitung zum Einhaengen neben den App-Passwoertern
Ein Info-Knopf unter Einstellungen -> Access oeffnet drei Wege:
Windows (Netzlaufwerk), Linux ueber davfs2 samt fstab und secrets, und
Linux ueber den Dateimanager. Dazu WebDAV-Adresse, Benutzername und
Cloud-Adresse zum Kopieren.

Die WebDAV-Adresse stand bisher nirgends in der Oberflaeche - sie ist
zwar ableitbar, aber nur wenn man weiss, dass es sie gibt. Sie entsteht
aus window.location statt aus PUBLIC_URL: Was im Browser traegt, traegt
auch im Dateimanager.

Mitgenommen sind die drei Stellen, an denen es sonst scheitert: Windows
bricht ohne HTTPS ab und laedt nichts ueber 50 MB, die Konsole braucht
davfs2, und Dateimanager wollen davs:// statt https://.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 14:49:57 +02:00

322 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import { useOverlayDismiss } from '../hooks/useOverlayDismiss'
import Tabs from './Tabs'
/**
* Wie hänge ich diese Cloud als Laufwerk ein?
*
* Die Adresse steht bisher nirgends. Sie ist zwar ableitbar – Cloud-Adresse
* plus `/dav/` –, aber „ableitbar" heißt: Man muss wissen, dass es sie gibt.
* Und die drei Wege dorthin (Windows, Konsole, Dateimanager) haben je eine
* Eigenheit, an der es sonst scheitert:
*
* * Windows verlangt HTTPS und lädt ohne Registry-Eingriff nichts über 50 MB.
* * Die Konsole braucht ein zusätzliches Paket (davfs2).
* * Dateimanager wollen ein eigenes Schema: `davs://` statt `https://`.
*
* Die Adressen entstehen aus `window.location`, nicht aus einer Einstellung:
* Was im Browser funktioniert, funktioniert auch im Dateimanager – eine
* falsch gesetzte PUBLIC_URL würde hier sonst eine Adresse anbieten, die
* nachweislich nicht die ist, über die der Betrachter gerade hereinkam.
*/
interface Props {
username: string
onClose: () => void
}
type Tab = 'windows' | 'terminal' | 'files'
/** Eine Zeile zum Abschreiben – mit Knopf, weil genau das sonst schiefgeht. */
function CopyRow({ label, value, hint }: { label: string; value: string; hint?: string }) {
const [copied, setCopied] = useState(false)
function copy() {
navigator.clipboard?.writeText(value).then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
})
}
return (
<div style={{ marginBottom: 10 }}>
<div style={{ fontSize: 11, color: 'var(--color-text-muted)', marginBottom: 4 }}>{label}</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<code style={{
flex: 1, minWidth: 0, padding: '8px 10px', borderRadius: 8,
background: 'var(--color-surface-2)', color: 'var(--color-text)',
fontSize: 12.5, fontFamily: 'monospace',
overflowX: 'auto', whiteSpace: 'nowrap', userSelect: 'all',
}}>{value}</code>
<button
onClick={copy}
style={{
padding: '8px 12px', borderRadius: 8, border: '1px solid var(--color-border)',
background: 'transparent', color: 'var(--color-text)', flexShrink: 0,
fontSize: 11.5, fontWeight: 600, cursor: 'pointer',
}}
>
{copied ? 'Copied' : 'Copy'}
</button>
</div>
{hint && (
<div style={{ fontSize: 11, color: 'var(--color-text-muted)', marginTop: 4, lineHeight: 1.5 }}>
{hint}
</div>
)}
</div>
)
}
function Step({ n, children }: { n: number; children: React.ReactNode }) {
return (
<div style={{ display: 'flex', gap: 10, marginBottom: 9 }}>
<div style={{
width: 19, height: 19, borderRadius: '50%', flexShrink: 0, marginTop: 1,
background: 'var(--color-surface-2)', color: 'var(--color-text-muted)',
fontSize: 11, fontWeight: 700,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>{n}</div>
<div style={{ fontSize: 13, color: 'var(--color-text)', lineHeight: 1.55 }}>{children}</div>
</div>
)
}
/** Ein Kasten für das, was an dieser Stelle erfahrungsgemäß schiefgeht. */
function Caveat({ children }: { children: React.ReactNode }) {
return (
<div style={{
marginTop: 12, padding: '10px 12px', borderRadius: 9,
border: '1px solid var(--color-border)',
background: 'var(--color-surface-2)',
fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.6,
}}>
{children}
</div>
)
}
function Code({ children }: { children: string }) {
const [copied, setCopied] = useState(false)
return (
<div style={{ position: 'relative', marginBottom: 10 }}>
<pre style={{
margin: 0, padding: '10px 12px', paddingRight: 62, borderRadius: 8,
background: 'var(--color-surface-2)', color: 'var(--color-text)',
fontSize: 12, lineHeight: 1.65, overflowX: 'auto',
fontFamily: 'monospace',
}}>{children}</pre>
<button
onClick={() => {
navigator.clipboard?.writeText(children).then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
})
}}
style={{
position: 'absolute', top: 6, right: 6,
padding: '4px 9px', borderRadius: 6, border: '1px solid var(--color-border)',
background: 'var(--color-surface)', color: 'var(--color-text-muted)',
fontSize: 10.5, fontWeight: 600, cursor: 'pointer',
}}
>
{copied ? 'Copied' : 'Copy'}
</button>
</div>
)
}
export default function WebDavGuide({ username, onClose }: Props) {
const overlay = useOverlayDismiss(onClose)
const [tab, setTab] = useState<Tab>('windows')
const origin = window.location.origin
const davURL = `${origin}/dav/`
// Dateimanager sprechen nicht https, sondern davs (verschlüsselt) bzw. dav.
const davsURL = davURL.replace(/^https:/, 'davs:').replace(/^http:/, 'dav:')
const isHTTPS = window.location.protocol === 'https:'
const host = window.location.host
// Dolphin kennt beide Schreibweisen; die verschlüsselte heißt anders.
const kioURL = `${isHTTPS ? 'webdavs' : 'webdav'}://${host}/dav/`
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4"
style={{ background: 'rgba(0,0,0,0.6)' }}
{...overlay}>
<div className="w-full rounded-xl shadow-2xl border"
style={{
maxWidth: 620, maxHeight: '86vh', overflowY: 'auto',
padding: 20,
background: 'var(--color-surface)', borderColor: 'var(--color-border)',
}}>
<h2 className="font-semibold mb-1" style={{ color: 'var(--color-text)' }}>
Connect as a drive
</h2>
<p style={{ margin: '0 0 16px', fontSize: 12.5, color: 'var(--color-text-muted)', lineHeight: 1.55 }}>
Your files over WebDAV — same folders, in your file manager. Sign in with
your username and an <strong style={{ color: 'var(--color-text)' }}>app password</strong>;
your account password is refused here on purpose.
</p>
<CopyRow label="WebDAV address" value={davURL} />
<CopyRow label="Username" value={username} />
<CopyRow
label="Cloud address (browser)"
value={origin}
hint="The normal address — for sharing links or opening the cloud on another device."
/>
{!isHTTPS && (
<Caveat>
<strong style={{ color: 'var(--color-danger)' }}>This page is not using HTTPS.</strong>{' '}
Windows refuses WebDAV over plain HTTP outright, and every other client
sends your app password unencrypted. Put a reverse proxy with TLS in
front before you connect a device.
</Caveat>
)}
<div style={{ marginTop: 18 }}>
<Tabs<Tab>
tabs={[
{ id: 'windows', label: 'Windows' },
{ id: 'terminal', label: 'Linux · terminal' },
{ id: 'files', label: 'Linux · file manager' },
]}
active={tab}
onChange={setTab}
/>
</div>
{tab === 'windows' && (
<div>
<Step n={1}>Open Explorer, right-click <strong>This PC</strong> → <strong>Map network drive…</strong></Step>
<Step n={2}>Folder: <code style={{ fontSize: 12 }}>{davURL}</code></Step>
<Step n={3}>Tick <strong>Connect using different credentials</strong>.</Step>
<Step n={4}>Username <strong>{username}</strong>, password = your app password.</Step>
<Caveat>
<strong style={{ color: 'var(--color-text)' }}>Two things Windows will do to you.</strong>
<br />
Downloads stop at <strong>50 MB</strong> — that is a built-in limit of the
Windows WebDAV client, not of this server. Raise it in
<code style={{ fontSize: 11.5 }}> HKLM\SYSTEM\CurrentControlSet\Services\WebClient\Parameters </code>
→ <code style={{ fontSize: 11.5 }}>FileSizeLimitInBytes</code> (decimal
4294967295 = 4 GB), then restart the <em>WebClient</em> service.
<br /><br />
If mapping fails immediately, that service is usually not running:
<code style={{ fontSize: 11.5 }}> services.msc </code>→ WebClient → start,
set to Automatic.
<br /><br />
For large files, a dedicated client (WinSCP, Cyberduck, RaiDrive) is the
calmer route — none of them have that limit.
</Caveat>
</div>
)}
{tab === 'terminal' && (
<div>
<p style={{ margin: '0 0 10px', fontSize: 13, color: 'var(--color-text)', lineHeight: 1.55 }}>
Mount it with <strong>davfs2</strong>. It asks for username and password
on the first mount.
</p>
<Code>{`sudo apt install davfs2 # Debian/Ubuntu
# sudo dnf install davfs2 # Fedora
# sudo pacman -S davfs2 # Arch
sudo mkdir -p /mnt/aether
sudo mount -t davfs ${davURL} /mnt/aether`}</Code>
<p style={{ margin: '14px 0 8px', fontSize: 13, fontWeight: 600, color: 'var(--color-text)' }}>
Without typing the password every time
</p>
<p style={{ margin: '0 0 8px', fontSize: 12.5, color: 'var(--color-text-muted)', lineHeight: 1.55 }}>
Put the credentials in <code style={{ fontSize: 12 }}>~/.davfs2/secrets</code> — the
file must be readable by you alone, davfs2 refuses it otherwise:
</p>
<Code>{`mkdir -p ~/.davfs2
echo '${davURL} ${username} YOUR-APP-PASSWORD' >> ~/.davfs2/secrets
chmod 600 ~/.davfs2/secrets`}</Code>
<p style={{ margin: '14px 0 8px', fontSize: 13, fontWeight: 600, color: 'var(--color-text)' }}>
Mount it at boot
</p>
<p style={{ margin: '0 0 8px', fontSize: 12.5, color: 'var(--color-text-muted)', lineHeight: 1.55 }}>
One line in <code style={{ fontSize: 12 }}>/etc/fstab</code>:
</p>
<Code>{`${davURL} /mnt/aether davfs user,noauto,_netdev 0 0`}</Code>
<Caveat>
<code style={{ fontSize: 11.5 }}>_netdev</code> matters: without it the
system tries to mount before the network is up and the boot stalls.
<br /><br />
davfs2 caches in <code style={{ fontSize: 11.5 }}>~/.davfs2/cache</code>.
A large file is written there first and only then uploaded — so a copy
can look finished while it is still on its way.
<br /><br />
Prefer <strong>rclone</strong>? <code style={{ fontSize: 11.5 }}>rclone config</code> →
type <code style={{ fontSize: 11.5 }}>webdav</code> → vendor{' '}
<code style={{ fontSize: 11.5 }}>other</code> → URL as above. It handles
interrupted transfers better.
</Caveat>
</div>
)}
{tab === 'files' && (
<div>
<p style={{ margin: '0 0 12px', fontSize: 13, color: 'var(--color-text)', lineHeight: 1.55 }}>
GNOME Files, Dolphin, Nemo and Thunar all speak WebDAV natively — but they
want their own scheme, not <code style={{ fontSize: 12 }}>https://</code>:
</p>
<CopyRow
label="Address for the file manager"
value={davsURL}
hint={isHTTPS
? 'davs:// means WebDAV over TLS. Plain dav:// would send your password in the clear.'
: 'dav:// (unencrypted) because this page is not on HTTPS — see the warning above.'}
/>
<Step n={1}>
<strong>GNOME Files:</strong> <em>Other Locations</em> at the bottom of the
sidebar → <em>Connect to Server</em> → paste the address → Connect.
</Step>
<Step n={2}>
<strong>KDE Dolphin:</strong> type the address straight into the location
bar (Ctrl&nbsp;+&nbsp;L). Dolphin also accepts{' '}
<code style={{ fontSize: 12 }}>{kioURL}</code>.
</Step>
<Step n={3}>
Username <strong>{username}</strong>, password = your app password. Tick
“remember” so the keyring keeps it.
</Step>
<Step n={4}>
Right-click the mount in the sidebar → <em>Add Bookmark</em>, and it stays
one click away.
</Step>
<Caveat>
These mounts live in GVFS/KIO, not in the kernel — they show up in the file
manager, but not always for other programs. If a tool cannot see the files,
look under <code style={{ fontSize: 11.5 }}>/run/user/$(id -u)/gvfs/</code>{' '}
or mount with davfs2 instead (the terminal tab).
</Caveat>
</div>
)}
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 18 }}>
<button
onClick={onClose}
style={{
padding: '8px 18px', borderRadius: 8, border: 'none',
background: 'var(--color-primary)', color: '#fff',
fontSize: 13, fontWeight: 600, cursor: 'pointer',
}}
>
Close
</button>
</div>
</div>
</div>
)
}