Dateien per Webbrowser auf SFTP-Server exportieren
Ein Browser kann eine Datei an ein HTTP-Gateway senden, das sie über SSH auf einen SFTP-Server schreibt. Bauen Sie diesen Übertragungsweg lokal mit einem temporären OpenSSH-Server, einem fest hinterlegten Hostschlüssel und einem kurzlebigen Zugriffstoken auf. Sie wählen eine Datei im Browser aus und prüfen, ob dieselben Bytes im privaten Eingangsverzeichnis ankommen.
Einführung
Dieses Beispiel akzeptiert Dateien von leer bis 5 MiB und weist jeder eine zufällige Empfangs-ID zu. Der Browser bestimmt die Bytes; das Gateway bestimmt Server, Konto, Verzeichnis und Dateiname. Es exportiert die Daten, ohne ihren Inhalt zu interpretieren, auf Malware zu scannen oder Dokumente für eine sichere Ausführung aufzubereiten.
Die Herausforderung verstehen
Normale Webseiten können keine SSH-Verbindung öffnen. Deshalb brauchen Uploads vom Browser zu SFTP ein Gateway. Der Browser authentifiziert sich dort mit einem Bearer-Token. Unabhängig davon prüft das Gateway die Identität des SSH-Servers und authentifiziert sich mit seinem eigenen privaten Schlüssel. Bewahren Sie diesen Schlüssel serverseitig auf. Für programmatische Übertragungen ohne Browser lesen Sie die SFTP-Anleitung für Node.js.
Ein isoliertes lokales Projekt erstellen
Verwenden Sie Linux mit Bash, Docker Engine, ssh-keygen von OpenSSH,
Node.js 24.15.0 oder eine neuere Version von Node.js 24 sowie
Corepack, das Yarn 4.12.0 bereitstellt.
Node.js 24 ist eine gepflegte LTS-Versionslinie; dieses Beispiel
läuft auch mit Node.js 26.8.1. Die Dateien mit der Erweiterung .mts
nutzen die native TypeScript-Ausführung von Node.js und übernehmen
daher nicht den Modultyp eines übergeordneten Projekts.
Fügen Sie Folgendes in Bash ein, ausgehend vom Verzeichnis, in dem Sie das Projekt anlegen möchten.
Die Subshell lässt Ihr aktuelles Verzeichnis und Ihre Shell-Optionen sowohl bei Erfolg als auch
bei einem Fehler unverändert. Eine lokale Lockdatei und ein expliziter Linker isolieren die
Installation von einem übergeordneten Yarn-Workspace. Bereits vorhandene Verzeichnisse namens
sftp-upload werden abgelehnt; eine unterbrochene Installation kann ein neues,
unvollständiges Verzeichnis hinterlassen, das Sie untersuchen können.
(
set -eu
mkdir sftp-upload
cd sftp-upload
printf '%s\n' '{"private":true,"type":"module","packageManager":"yarn@4.12.0"}' > package.json
touch yarn.lock
printf 'nodeLinker: node-modules\nenableGlobalCache: false\n' > .yarnrc.yml
mkdir .corepack
printf '%s\n' '{"type":"commonjs"}' > .corepack/package.json
COREPACK_HOME="$PWD/.corepack" YARN_IGNORE_PATH=1 corepack yarn add --exact express@5.2.1 helmet@8.3.0 jose@6.2.12 ssh2-sftp-client@12.1.1 execa@9.6.1
mkdir public
)
Wechseln Sie erst nach erfolgreicher Installation in das Projektverzeichnis:
cd sftp-upload
Eingangsverzeichnis und Token-Aussteller für die Entwicklung einrichten
Speichern Sie Folgendes als demo.mts. Es erstellt ein Ubuntu-24.04-Image mit
OpenSSH, richtet ein Konto ausschließlich für SFTP ein und stellt SSH über einen zufällig
zugewiesenen Loopback-Port bereit. Das Chroot-Verzeichnis gehört root; nur
/incoming ist für das Konto beschreibbar, mit Modus 700.
OpenSSH verlangt Chroot-Verzeichnisse im Besitz von root.
Alle Zugangsdaten für diese Demo werden im privaten Verzeichnis .demo
erzeugt. Bestehende Konten, SSH-Schlüssel und Known-Hosts-Dateien bleiben unverändert.
Der fest hinterlegte Hostschlüssel wird aus dem öffentlichen Schlüssel abgeleitet, der über Ihren
lokalen Docker-Daemon ausgelesen wird. Diesen kontrollieren Sie selbst; eine ungeprüfte
SSH-Verbindung wird dafür nicht verwendet. Der lokale RSA-Schlüssel stellt mit
SignJWT von jose RS256-Token aus, die fünf Minuten gültig sind.
Dieser Aussteller für die Entwicklung hat weder eine Anmeldung noch ein Benutzerverzeichnis:
Setzen Sie ihn niemals produktiv ein und geben Sie seinen Signaturschlüssel niemals weiter.
import { createHash, generateKeyPairSync, randomUUID } from 'node:crypto'
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { execa } from 'execa'
import { importPKCS8, SignJWT } from 'jose'
import SftpClient from 'ssh2-sftp-client'
const directory = join(import.meta.dirname, '.demo')
const statePath = join(directory, 'state.json')
const docker = (args, options = {}) => execa('docker', args, { timeout: 180_000, ...options })
async function setup() {
const prefix = process.env.SFTP_DEMO_PREFIX ?? 'sftp-demo'
if (!/^[a-z0-9-]{1,40}$/.test(prefix)) throw new Error('Invalid demo prefix')
const name = prefix + '-' + randomUUID()
await mkdir(directory, { mode: 0o700 }) // Refuse to replace an existing demo.
let ready = false
try {
await mkdir(join(directory, 'staging'), { mode: 0o700 })
await execa('ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-f', join(directory, 'client')])
const pair = generateKeyPairSync('rsa', { modulusLength: 2048 })
await writeFile(join(directory, 'jwt-private.pem'), pair.privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600 })
await writeFile(join(directory, 'jwt-public.pem'), pair.publicKey.export({ type: 'spki', format: 'pem' }), { mode: 0o600 })
const [keyType, keyBytes] = (await readFile(join(directory, 'client.pub'), 'utf8')).split(' ')
const config = `Port 22
HostKey /etc/ssh/ssh_host_ed25519_key
AuthorizedKeysFile /etc/ssh/demo_authorized_keys
PasswordAuthentication no
KbdInteractiveAuthentication no
UsePAM no
PermitRootLogin no
AllowUsers uploader
Subsystem sftp internal-sftp
Match User uploader
ChrootDirectory /srv/sftp
ForceCommand internal-sftp -u 077
DisableForwarding yes
PermitTTY no`
await docker(['build', '--tag', name, '-'], { input: `FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends openssh-server && rm -rf /var/lib/apt/lists/* /etc/ssh/ssh_host_*
RUN useradd --no-create-home --home-dir /incoming --shell /usr/sbin/nologin uploader && passwd -d uploader && mkdir -p /run/sshd /srv/sftp/incoming && chown uploader:uploader /srv/sftp/incoming && chmod 700 /srv/sftp/incoming && chmod 755 /srv/sftp
RUN printf '%s\\n' '${keyType} ${keyBytes}' > /etc/ssh/demo_authorized_keys
RUN printf '%s\\n' '${config.replaceAll('\n', "' '")}' > /etc/ssh/sshd_config
CMD ["sh", "-c", "ssh-keygen -A && exec /usr/sbin/sshd -D -e"]
` })
await docker(['run', '--detach', '--rm', '--name', name, '--publish', '127.0.0.1::22', name])
let publicKey
for (let attempt = 0; attempt < 50; attempt++) {
const probe = await docker(['exec', name, 'cat', '/etc/ssh/ssh_host_ed25519_key.pub'], { reject: false })
if (probe.exitCode === 0) { publicKey = probe.stdout; break }
const running = await docker(['inspect', '--format', '{{.State.Running}}', name], { reject: false })
if (running.stdout !== 'true') throw new Error('Disposable SFTP server exited during startup')
await new Promise((resolve) => setTimeout(resolve, 100))
}
if (!publicKey) throw new Error('Disposable SFTP server did not become ready')
const mapping = await docker(['port', name, '22/tcp'])
const port = Number(mapping.stdout.split(':').at(-1))
const hostKey = createHash('sha256').update(Buffer.from(publicKey.split(' ')[1], 'base64')).digest('hex')
const sftp = new SftpClient()
try {
await sftp.connect({ host: '127.0.0.1', port, username: 'uploader', privateKey: await readFile(join(directory, 'client')), hostHash: 'sha256', hostVerifier: (hash) => hash === hostKey, readyTimeout: 10_000 })
if (await sftp.realPath('/incoming') !== '/incoming') throw new Error('Unexpected demo inbox')
} finally { await sftp.end() }
await writeFile(statePath, JSON.stringify({ name, port, hostKey }) + '\n', { mode: 0o600 })
ready = true
console.log('Local SFTP inbox ready. Next: node demo.mts token')
} finally {
if (!ready) {
await docker(['rm', '--force', name], { reject: false })
await docker(['image', 'rm', '--force', name], { reject: false })
await rm(directory, { recursive: true, force: true })
}
}
}
async function main() {
const [command, id, original] = process.argv.slice(2)
if (command === 'setup') return setup()
const state = JSON.parse(await readFile(statePath, 'utf8'))
if (command === 'stop') {
await docker(['rm', '--force', state.name])
await docker(['image', 'rm', '--force', state.name])
await rm(directory, { recursive: true })
console.log('Removed the disposable server, inbox, and demo credentials')
return
}
if (command === 'verify') {
if (!/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(id ?? '') || !original) throw new Error('Usage: node demo.mts verify RECEIPT_ID ORIGINAL_FILE')
const path = '/srv/sftp/incoming/' + id + '.bin'
const remote = await docker(['exec', state.name, 'cat', path], { encoding: 'buffer', stripFinalNewline: false })
const expected = await readFile(original)
const mode = await docker(['exec', state.name, 'stat', '-c', '%a', path])
if (Buffer.compare(remote.stdout, expected) !== 0 || mode.stdout !== '600') throw new Error('Remote bytes or file permissions differ')
console.log('Verified ' + expected.length + ' bytes; mode 600')
return
}
if (command === 'token') {
const key = await importPKCS8(await readFile(join(directory, 'jwt-private.pem'), 'utf8'), 'RS256')
console.log(await new SignJWT({ scope: 'sftp:upload' }).setProtectedHeader({ alg: 'RS256' }).setIssuer('urn:local-sftp-demo').setAudience('sftp-gateway').setSubject('local-uploader').setIssuedAt().setExpirationTime('5m').sign(key))
return
}
if (command !== 'serve') throw new Error('Use setup, token, serve, verify, or stop')
const port = Number(process.env.PORT ?? 3000)
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Invalid HTTP port')
Object.assign(process.env, {
PORT: String(port), APP_ORIGIN: 'http://127.0.0.1:' + port,
JWT_PUBLIC_KEY: await readFile(join(directory, 'jwt-public.pem'), 'utf8'),
JWT_ISSUER: 'urn:local-sftp-demo', JWT_AUDIENCE: 'sftp-gateway',
SFTP_HOST: '127.0.0.1', SFTP_PORT: String(state.port), SFTP_USERNAME: 'uploader',
SFTP_PRIVATE_KEY: await readFile(join(directory, 'client'), 'utf8'), SFTP_HOST_SHA256: state.hostKey,
TMPDIR: join(directory, 'staging'),
})
await import('./index.mts')
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : 'Demo failed')
process.exitCode = 1
})
Das Frontend einrichten
Speichern Sie dies als public/index.html. Das Token-Feld dient in dieser lokalen Demo
zur manuellen Eingabe. Eine Anwendung sollte das kurzlebige Token jedes autorisierten Benutzers
über ihren Anmeldeablauf beziehen.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SFTP file export</title>
</head>
<body>
<h1>Export a file</h1>
<form id="uploadForm">
<label>Your access token <input id="token" type="password" autocomplete="off" required /></label>
<label>File (up to 5 MiB) <input type="file" id="fileInput" required /></label>
<button type="submit">Export to SFTP</button>
</form>
<p id="status" role="status"></p>
<script src="/upload.js" defer></script>
</body>
</html>
Speichern Sie dies als public/upload.js. Der Browser sendet ein File-Objekt als
Anfragekörper, auch bei einer leeren Datei, und übermittelt dessen Content-Length.
Die Bedienelemente bleiben deaktiviert, solange die Anfrage aussteht; erneutes Absenden wird
ignoriert. Das Token wird vor dem Senden aus dem Eingabefeld gelöscht.
const form = document.getElementById('uploadForm')
const token = document.getElementById('token')
const input = document.getElementById('fileInput')
const status = document.getElementById('status')
const button = form.querySelector('button')
form.addEventListener('submit', async (event) => {
event.preventDefault()
if (button.disabled) return
const file = input.files[0]
if (!file || file.size > 5 * 1024 * 1024) {
status.textContent = 'Choose a file of up to 5 MiB.'
return
}
button.disabled = true
input.disabled = true
token.disabled = true
status.textContent = 'Exporting…'
const accessToken = token.value
token.value = ''
try {
const response = await fetch('/upload', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + accessToken,
'Content-Type': 'application/octet-stream',
},
body: file,
signal: AbortSignal.timeout(35_000),
})
if (!response.ok) throw new Error('Export rejected')
const result = await response.json()
status.textContent = 'File exported. Receipt: ' + result.id
} catch {
status.textContent = 'Export could not be confirmed. Check your inbox before retrying.'
} finally {
button.disabled = false
input.disabled = false
token.disabled = false
}
})
Die Backend-API mit Node.js erstellen
Speichern Sie dies als index.mts. Die
ssh2-sftp-client-API stellt die SFTP-Streams und den Vorgang zum
Umbenennen bereit. Ihre ssh2-Verbindungsoptionen definieren
hostHash und hostVerifier: Der fest hinterlegte Prüfwert
ist ein 64-stelliger hexadezimaler SHA-256-Hash des unverarbeiteten Hostschlüssels, nicht die
Anzeigezeichenfolge SHA256:base64 von OpenSSH.
Das Gateway prüft Signatur, Aussteller, Zielgruppe, Alter und Upload-Scope des Tokens, bevor es den
Anfragekörper liest. Es speichert den vollständigen, größenbegrenzten Anfragekörper lokal zwischen,
schreibt eine private Datei mit der Erweiterung .part und benennt sie
anschließend in .bin um, bevor es HTTP 201 und
{ id } zurückgibt. Weiterverarbeitende Systeme müssen Dateien mit der
Erweiterung .part ignorieren.
import { randomUUID } from 'node:crypto'
import { createReadStream, createWriteStream } from 'node:fs'
import { mkdtemp, rm } from 'node:fs/promises'
import { createConnection } from 'node:net'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Transform } from 'node:stream'
import { pipeline } from 'node:stream/promises'
import { fileURLToPath } from 'node:url'
import express from 'express'
import helmet from 'helmet'
import { importSPKI, jwtVerify } from 'jose'
import SftpClient from 'ssh2-sftp-client'
const {
APP_ORIGIN, JWT_PUBLIC_KEY, JWT_ISSUER, JWT_AUDIENCE,
SFTP_HOST, SFTP_USERNAME, SFTP_PRIVATE_KEY, SFTP_HOST_SHA256,
} = process.env
if (!APP_ORIGIN || !JWT_PUBLIC_KEY || !JWT_ISSUER || !JWT_AUDIENCE ||
!SFTP_HOST || !SFTP_USERNAME || !SFTP_PRIVATE_KEY ||
!/^[a-f0-9]{64}$/.test(SFTP_HOST_SHA256 ?? '')) {
throw new Error('Gateway configuration is incomplete')
}
const sshPort = Number(process.env.SFTP_PORT ?? 22)
if (!Number.isInteger(sshPort) || sshPort < 1 || sshPort > 65535) {
throw new Error('Invalid SFTP port')
}
const key = await importSPKI(JWT_PUBLIC_KEY, 'RS256')
const webOrigin = new URL(APP_ORIGIN)
const localHttp = webOrigin.protocol === 'http:' &&
['localhost', '127.0.0.1', '[::1]'].includes(webOrigin.hostname)
if (webOrigin.origin !== APP_ORIGIN || (!localHttp && webOrigin.protocol !== 'https:')) {
throw new Error('Use an HTTPS origin or loopback HTTP for local development')
}
const remoteRoot = '/incoming'
const maxBytes = 5 * 1024 * 1024
let active = 0
export const app = express()
app.disable('x-powered-by')
app.use(helmet({
contentSecurityPolicy: {
// The local demo uses HTTP, so its assets must not be upgraded to HTTPS.
directives: { 'upgrade-insecure-requests': localHttp ? null : [] },
},
strictTransportSecurity: localHttp ? false : undefined,
}))
app.use((_req, res, next) => {
res.locals.requestId = randomUUID()
res.set('X-Request-ID', res.locals.requestId)
res.set('Cache-Control', 'no-store')
next()
})
app.post('/upload', async (req, res) => {
const fail = (status, error) => {
if (!res.destroyed && !res.headersSent) {
res.status(status).json({ error, requestId: res.locals.requestId })
}
}
if (req.get('origin') !== APP_ORIGIN || req.originalUrl !== '/upload') {
return fail(403, 'Request not allowed')
}
let claims
try {
const match = /^Bearer ([A-Za-z0-9_.-]+)$/.exec(req.get('authorization') ?? '')
if (!match) return fail(401, 'Authentication required')
const verified = await jwtVerify(match[1], key, {
algorithms: ['RS256'], issuer: JWT_ISSUER, audience: JWT_AUDIENCE,
requiredClaims: ['sub', 'exp', 'iat'], maxTokenAge: '15m',
})
claims = verified.payload
} catch {
return fail(401, 'Invalid access token')
}
if (typeof claims.scope !== 'string' || !claims.scope.split(' ').includes('sftp:upload')) {
return fail(403, 'Permission denied')
}
const length = req.get('content-length') ?? ''
if (!/^(0|[1-9][0-9]*)$/.test(length)) return fail(411, 'Content length required')
const expected = Number(length)
if (!Number.isSafeInteger(expected) || expected > maxBytes) return fail(413, 'File too large')
if (req.get('content-type') !== 'application/octet-stream') {
return fail(415, 'Send an octet-stream file')
}
if (active >= 2) return fail(503, 'Gateway busy')
active += 1
const id = randomUUID()
const remotePart = remoteRoot + '/' + id + '.part'
const remoteFinal = remoteRoot + '/' + id + '.bin'
const abort = new AbortController()
const timer = setTimeout(() => abort.abort(), 30_000)
const disconnect = () => { if (!res.writableFinished) abort.abort() }
req.once('aborted', disconnect)
res.once('close', disconnect)
let directory
let socket
let connected = false
let pending = false
const sftp = new SftpClient('gateway', {
error: () => abort.abort(),
end: () => abort.abort(),
close: () => abort.abort(),
})
const cancelSocket = () => socket?.destroy()
abort.signal.addEventListener('abort', cancelSocket)
try {
directory = await mkdtemp(join(tmpdir(), 'sftp-upload-'))
const localPath = join(directory, 'payload')
let received = 0
const limit = new Transform({
transform(chunk, _encoding, callback) {
received += chunk.length
callback(received > expected ? new Error('Byte limit exceeded') : null, chunk)
},
flush(callback) {
callback(received !== expected ? new Error('Incomplete upload') : null)
},
})
await pipeline(req, limit, createWriteStream(localPath, { flags: 'wx', mode: 0o600 }), {
signal: abort.signal,
})
abort.signal.throwIfAborted()
socket = createConnection({ host: SFTP_HOST, port: sshPort })
// ssh2 observes socket failures; this also covers the handoff before its listeners attach.
socket.on('error', () => {})
await sftp.connect({
sock: socket,
username: SFTP_USERNAME,
privateKey: SFTP_PRIVATE_KEY,
hostHash: 'sha256',
hostVerifier: (hash) => hash === SFTP_HOST_SHA256,
readyTimeout: 10_000,
})
connected = true
abort.signal.throwIfAborted()
if (await sftp.realPath(remoteRoot) !== remoteRoot) throw new Error('Unexpected inbox')
pending = true
// The private inbox has no other writers; exclusive creation refuses an existing .part file.
await pipeline(
createReadStream(localPath),
sftp.createWriteStream(remotePart, { flags: 'wx', mode: 0o600 }),
{ signal: abort.signal },
)
abort.signal.throwIfAborted()
await sftp.rename(remotePart, remoteFinal)
pending = false
if (!abort.signal.aborted) res.status(201).json({ id })
} catch {
console.error(JSON.stringify({ event: 'export_failed', requestId: res.locals.requestId }))
fail(502, 'Export could not be confirmed')
} finally {
// A disconnected SSH session cannot guarantee deletion; the inbox janitor handles those parts.
if (connected && pending && !socket.destroyed) {
await sftp.delete(remotePart, true).catch(() => {
console.error(JSON.stringify({ event: 'cleanup_pending', requestId: res.locals.requestId }))
})
}
socket?.destroy()
await sftp.end().catch(() => {})
if (directory) await rm(directory, { recursive: true, force: true }).catch(() => {
console.error(JSON.stringify({ event: 'local_cleanup_failed', requestId: res.locals.requestId }))
})
clearTimeout(timer)
abort.signal.removeEventListener('abort', cancelSocket)
req.off('aborted', disconnect)
res.off('close', disconnect)
active -= 1
}
})
app.use(express.static(fileURLToPath(new URL('./public/', import.meta.url))))
app.use((_req, res) => res.status(404).json({ error: 'Not found' }))
app.use((_error, _req, res, _next) => {
if (!res.headersSent) res.status(500).json({ error: 'Request failed' })
})
const httpPort = Number(process.env.PORT ?? 3000)
if (!Number.isInteger(httpPort) || httpPort < 0 || httpPort > 65535) {
throw new Error('Invalid HTTP port')
}
const server = app.listen(httpPort, '127.0.0.1', (error) => {
if (error) {
console.error('Gateway could not bind its HTTP port: ' + error.code)
process.exitCode = 1
return
}
console.log('Open ' + APP_ORIGIN)
})
server.requestTimeout = 35_000
server.headersTimeout = 10_000
Sicherheitsaspekte
Die lokale Seite verwendet HTTP über Loopback. Eine produktiv bereitgestellte Seite und ihr Gateway
benötigen HTTPS, exakt den konfigurierten Origin und einen echten Identitätsanbieter, der
sftp:upload nur autorisierten Benutzern zuweist. Die Origin-Prüfung ergänzt die
Token-Prüfung; sie authentifiziert den Aufrufer nicht. CORS bleibt deaktiviert.
Das Eingangsverzeichnis hat genau einen vertrauenswürdigen Schreibzugriff. Kein nicht
vertrauenswürdiger Prozess darf /incoming umbenennen, symbolische Links
hinzufügen oder dort mit der Dateierstellung konkurrieren. Die Prüfung des kanonischen Pfads
ersetzt diese Berechtigungen nicht. Zufällige Namen und die exklusive Erstellung temporärer Dateien
verhindern die Wiederverwendung eines übermittelten Dateinamens; jeder erneute Versuch erzeugt eine
neue ID.
Dieser einzelne Prozess lässt zwei aktive Anfragen zu, jeweils mit höchstens 5 MiB lokalem Zwischenspeicher und einer Übertragungsfrist von 30 Sekunden. Er streamt beide Übertragungen, statt ganze Dateien im Arbeitsspeicher zu halten. Die Grenzen gelten für aktive Anfragen in einem Prozess. Wenn Sie die Demo anpassen, benötigen abgeschlossene Dateien auf dem SFTP-Server sowie lokale temporäre Dateien, die nach einem Absturz oder einer fehlgeschlagenen Bereinigung zurückbleiben, separate Speicherplatzquoten und eine Bereinigung.
Eine unterbrochene SSH-Sitzung kann eine Datei mit der Erweiterung .part
hinterlassen. Geht die HTTP-Antwort nach dem Umbenennen verloren, kann eine vollständige Datei mit
der Erweiterung .bin zurückbleiben, obwohl der Browser einen unklaren
Status meldet. Die Empfangsbestätigung bestätigt das erfolgreiche Umbenennen; sie ist weder ein
Protokoll für eine genau einmalige Auslieferung noch eine Garantie für dauerhafte Speicherung.
Prüfen Sie das Eingangsverzeichnis, bevor Sie eine Übertragung mit unklarem Status erneut versuchen.
Beim Beenden dieser Demo wird das gesamte temporäre Eingangsverzeichnis gelöscht; ein dauerhafter
Dienst benötigt stattdessen eine Richtlinie für verwaiste temporäre Dateien.
Den Dateiexport testen
Nachdem Sie alle vier Dateien gespeichert haben, richten Sie den Server ein:
node demo.mts setup
Erzeugen Sie ein Token und kopieren Sie die Ausgabe. Halten Sie dieses Terminal privat; das Token gewährt bis zu seinem Ablauf Upload-Zugriff. Führen Sie denselben Befehl erneut aus, wenn Sie ein neues Token benötigen:
node demo.mts token
Starten Sie das Gateway im Vordergrund:
node demo.mts serve
Öffnen Sie http://127.0.0.1:3000/, fügen Sie das Token in
Your access token ein, wählen Sie eine Datei aus und
klicken Sie auf Export to SFTP. Während der
Übertragung zeigt die Seite Exporting… an.
Nach HTTP 201 zeigt sie File exported. Receipt: gefolgt
von einer ID an. Das Öffnen von file:// oder die Verwendung von
localhost anstelle des ausgegebenen Origins funktioniert nicht. Wenn Port 3000
belegt ist, beenden Sie den anderen Dienst oder verwenden Sie PORT=3002 node demo.mts serve
und öffnen Sie den ausgegebenen Origin.
Wechseln Sie in einem zweiten Terminal in dasselbe Projektverzeichnis. Ersetzen Sie
RECEIPT_ID durch diese ID und ORIGINAL_FILE durch genau die
Datei, die Sie ausgewählt haben. Dieser Befehl liest die private Datei auf dem SFTP-Server unabhängig
über Docker, vergleicht ihre Bytes mit Ihrem Original und prüft Modus 600:
node demo.mts verify RECEIPT_ID ORIGINAL_FILE
Bei einer Eingabe von sieben Bytes gibt er Verified 7 bytes; mode 600 aus. Testen Sie auch eine
leere Datei und eine Binärdatei. Ein falsches Token, ein falscher Aussteller, Scope oder Origin, ein
zu großer oder unterbrochener Anfragekörper, ein falscher fest hinterlegter Hostschlüssel oder ein
nicht beschreibbares Eingangsverzeichnis auf dem Server dürfen nicht zur Veröffentlichung einer
endgültigen Datei führen. Eine Upload-Fehlerantwort enthält eine allgemeine Meldung und eine
Anfrage-ID, niemals SSH-Zugangsdaten oder interne Fehlerdetails. Der Browser meldet
Export could not be confirmed. Check your inbox before retrying.
bei einer abgelehnten Übertragung oder einer verlorenen Bestätigung.
Drücken Sie Strg+C im Serverterminal, um zur Shell zurückzukehren. Entfernen Sie anschließend Demoserver, Image, Eingangsverzeichnis und Zugangsdaten. Dabei werden nur die Ressourcen entfernt, die bei der Einrichtung dieses Projekts erfasst wurden:
node demo.mts stop
Fazit
Ersetzen Sie den Aussteller für die Entwicklung und das temporäre Konto, wenn Sie diesen Ablauf in eine bestehende Anwendung integrieren. Behalten Sie die Autorisierungsgrenze für den Browser und die vertrauenswürdige Serverkonfiguration bei. Zur Verarbeitung von Dateien vor dem Export lesen Sie mehr über den Robot für den SFTP-Export von Transloadit.
