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>
322 lines
13 KiB
TypeScript
322 lines
13 KiB
TypeScript
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 + 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>
|
||
)
|
||
}
|