Next.js: SFTP-Dateitransfer via Serverless Functions

Next.js: SFTP-Dateitransfer via Serverless Functions

Dieser Artikel behandelt den Dateitransfer per SFTP-Protokoll über Serverless Functions von Next.js.

Auf Grund weniger ausführlicher Anleitungen im WWW, wie mit Hilfe der Serverless Functions von Next.js Dateien per SFTP-Protokoll an einen entsprechenden Server gesendet werden können, folgt hier nun meine Lösung.

Das Problem

Next.js ermöglicht mit Hilfe der standardmäßig verfügbaren Serverless Functions die Entwicklung eigener APIs, ohne hierfür auf externe (Express) Server ausweichen zu müssen. Hierbei ist es ebenfalls möglich, per POST-Request größere Dateien an eine API Route zu schicken, welche diese dann verarbeitet. Wie der Name schon impliziert, sind o.g. Funktionen serverless, weshalb üblicherweise verfügbare Speicherkapazitäten und Read/Write/Delete Operationen entweder stark eingeschränkt oder gar nicht verfügbar sind. Wenn nun eine Datei per POST-Request an eine API Route gesendet wird, kann die Datei zum Beispiel nicht mit Hilfe des Filesystem-Moduls fs explizit gespeichert werden. Für die weitere Verarbeitung und das Versenden der Datei per SFTP-Protokoll heißt dies, dass kein relativer Pfad zu der Datei auf dem Server vorliegt, obwohl dieser notwendig ist.

Die Lösung

Die Implementierung sieht folgende Dateistruktur des Projekts vor:

/root
- /pages
- - /api
- - - /sftp
- - - - index.js
- - _app.js
- - index.js
- /public
- /styles
- .env

Implementierung des Clients

Auf Client-Seite ist repräsentativ die simple Implementierung einer automatischen Dateigenerierung und das Versenden dieser mittels dem Modul axios vorgesehen:

/**
*  index.js
*/
import React from "react";
import axios from "axios";

export default function Home() {
  const uploadFile = async (e) => {
    const file = new File(
      [
        "QUIKK Software\n\n",
        "Übertragung von Dateien per SFTP und Serverless Functions"
      ],
      "testFile.txt",
      {
        type: "text/plain",
      }
    );
    const formData = new FormData();

    formData.append("testFile", file);

    const data = await axios.post("/api/sftp", formData, {
      headers: {
        "Content-Type": "multipart/form-data",
      },
    });
  };

  return (
    <>
      <button onClick={uploadFile}>UPLOAD</button>
    </>
  );
}

Implementierung der Serverless Functions

Nach einiger Zeit des Ausprobierens und der Einsicht, dass auf Grund der Serverless Functions keine relativen Pfade zu Dateien feststellbar sind, lag die Annahme nahe, dass für eine adäquate Lösung auf eine komplizierte externe Lösung ausgewichen werden musste. Nach einer letzten Recherche zu Gunsten der deutlich besseren Lösungen mit Serverless Functions, stieß ich auf diesen Stack Overflow Eintrag.

formidable ist ein Modul zum Einlesen von FormData aus POST-Requests, welche neben den Inhalten der FormData auch einen temporären Pfad zu den nun auf dem Server befindlichen Dateien vorhält.

Wie bereits beschrieben bezieht sich diese Lösung auf Next.js. Ein Einstieg zu Next.js kann hier gefunden werden.

Folgende Module werden benötigt:

import { IncomingForm } from "formidable";
import fs from "fs";
import { Client } from "ssh2";
require("dotenv").config();

Zu aller erst muss die API Route api/stfp/index.js konfiguriert werden, hierbei wird das Attribute bodyParser auf false gesetzt, um stattdessen formidable das Einlesen des POST-Requests zu ermöglichen.

export const config = {
  api: {
    bodyParser: false,
  },
};

Das folgende Code Beispiel zeigt eine API Route ohne Funktionalität:

export default async (req, res) => {
  const { method } = req;
  switch (req.method) {
    case "POST":
      try {
        /* CODE HERE */
      } catch (error) {
        console.error(error);
        res.status(409).json({
          error,
        });
      }
      break;
    default:
      res.setHeader("Allow", ["POST"]);
      res.status(405).send(`Method ${method} Not Allowed`);
      break;
  }
};

Um mit formidable den POST-Request einlesen zu können, kann folgende Promise basierende Funktion genutzt werden:

const data = await new Promise((resolve, reject) => {
  const form = new IncomingForm();
  form.parse(req, (err, fields, files) => {
    if (err) return reject(err);
    resolve({ fields, files });
  });
});

Nun folgt auch schon der verschlüsselte Dateitransfer mit Hilfe des Moduls ssh2 und des Protokolls sftp.

var conn = new Client();
conn
    .on("ready", function () {
    console.log("Client :: ready");
    conn.sftp(function (err, sftp) {
        if (err) throw err;

        var readStream = fs.createReadStream(data?.files?.testFile?.path);
        var writeStream = sftp.createWriteStream(
        "/testFile.txt"
        );
        writeStream.on("close", () => {
        console.log("- file transferred succesfully");
        conn.end();
        res.status(200).json({
            message: "file transfer successful",
        });
        });

        writeStream.on("end", () => {
        console.log("sftp connection closed");
        conn.end();
        });

        readStream.pipe(writeStream);
    });
    })
    .connect({
    host: process.env.HOST,
    port: 22,
    username: process.env.USERNAME,
    password: process.env.PASSWORD,
    });

Das Modul dotenv ermöglicht die serverseitige und gesicherte Nutzung von kritischen Daten wie Passwörtern direkt im Code und wird dringend empfohlen.

Auf dem SFTP-Server sollte nach erfolgreicher Ausführung die Datei testFile.txt mit dem Inhalt

"QUIKK Software

Übertragung von Dateien per SFTP und Serverless Functions"

vorliegen.

Github Repo

Entdecken Sie die Vorteile­ unserer Individual­entwicklung

In einem kostenlosen Erstgespräch vor Ort - wahlweise auch am Telefon oder über Microsoft Teams - lernen wir uns unverbindlich kennen und erarbeiten gemeinsam mit Ihnen ein Lösungskonzept für Ihre Ziele. Wir freuen uns auf Sie!