Node.JS: Initiate Filedownload (Tiny)

Veröffentlicht von Sascha am

Mit Node.JS ist es nicht wirklich schwer, einen File-Downloader bzw. URL-Redirector zu implementieren. Dank der nähe zum HTTP-Protokoll des HTTP-Servers in Node.JS wird einem fast alles in die Hand gegeben was man benötigt.

Man stelle sich einen eigenen kleinen Web Dienst wie tinyurl bzw. tinyfile vor. Über die URL (bspw: mydomain.tld/abc123) soll anhand des nach der TLD folgenden Strings entschieden werden, ob ein Datei-Download initiiert oder ein Redirect durchgeführt werden soll.

In unserem Beispiel arbeitet im Hintergrund die No-SQL Datenbank MongoDB welche von Node.JS mittels dem MongoSkin Module angesprochen wird.

Unsere benötigten Module:

var http = require("http");
var url = require("url");
var fs = require("fs");

var mongo = require("mongoskin");

Bis auf das mongoskin Module sollten bei einer korrekten Installation von Node.JS bereits alle enthalten sein.
Um das mongoskin Module nachzuinstallieren nutzt ihr den Node-Package-Manager (npm install mongoskin)

Da wir in unserem Mini Tiny Projekt von einem Download und einem Redirect ausgehen, definieren wir zwei Actions

var actions = {
    'download': function(res, mapping) {
        var options = url.parse(mapping.url);
        console.log(options.protocol);

        switch (options.protocol) {
            case 'http:':
                http.get(url.parse(mapping.url), function(data) {
                    deliverDownload(res, mapping, data);
                });
            break;

            case 'file:':
                var data = fs.createReadStream(options.host + options.path);
                data.statusCode = 200;
                deliverDownload(res, mapping, data);
            break;
        }
    },

    'redirect': function(res, mapping) {
        res.writeHead(301, {'Location': mapping.url});
        res.end();
    },

    'error': function(res, mapping) {
        res.writeHead(mapping.statusCode, {'Content-Type': 'text/html'});
        res.end(mapping.data);
    }
};

Es wurden hier drei Actions definiert, da wir noch eine Error Action hinzugenommen haben, um unbehandelte Ausnahmen zu handlen.

Die Action für einen Redirect sind ziemlich simple. Wir senden einen 301-HTTP-Header (Permanently Moved) und geben die neue Location an. Das entspricht dem aus PHP bekannten header(„Location: http://www.domain.tld“);.

Die Erklärung für die Download Action erfolgt später. Kommen wir erstmal zu unserem Server Konstrukt:

http.createServer(function(req, res) {
    var alias = req.url.substr(1);
    var db = mongo.db('mongo://127.0.0.1/db');
    db.collection('tiny').find({uuid: alias}).toArray(function(err, result) {
        if (err) throw err;

        actions[result[0].data.action](res, result[0].data);
    });
}).listen(3000);

Im ersten Step lesen wir die URL aus, bzw. den Path der hinter dem ersten Slash steht (abc123). Wir stellen eine Verbindung zu unserer lokalen MongoDB her und greifen im Connection-String direkt auf die Datenbank ‚db‘ zu. Nun können wir innerhalb der Datenbank ‚db‘ die Collection namens ‚tiny‘ nach einem passenden Eintrag zu unserem alias (abc123) durchsuchen.

Ein von uns dort abgelegtes Document (Document = MongoDB Synonym für den Datensatz einer relationalen Datenbank) sollte in etwa so aussehen:

{ 
  "uuid" : "abc123", 
  "data" : { 
      "action" : "download",
      "url" : "file://./1b5729cff3462d242e8e91160fe0c688.png",
      "filename" : "Loewe.png",
      "contentType" : "image/png",
      "forceDownload" : true
  } 
}

Als Ergebnis unserer Abfrage db.collection(‚tiny‘).find({uuid: alias}) ist nun in der Variablen result[0] abgelegt.
Dank unserer zuvor definierten ‚actions‘ Objektliterals können wir die entsprechende Action nun via eines Array-Offsets aufrufen

// Anstatt:
action['download'](null, null); 

// Können wir:
action[result[0].data.action](res, result[0].data);

Gehen wir nun etwas genauer auf die Action für einen Download ein.

var actions = {
    'download': function(res, mapping) {
        var options = url.parse(mapping.url);
        console.log(options.protocol);

        switch (options.protocol) {
            case 'http:':
                http.get(url.parse(mapping.url), function(data) {
                    deliverDownload(res, mapping, data);
                });
            break;

            case 'file:':
                var data = fs.createReadStream(options.host + options.path);
                data.statusCode = 200;
                deliverDownload(res, mapping, data);
            break;
        }
    },
    // .......

Dank des url Modules von Node.JS können wir die in Mapping (result[0].data) enthaltene URL in seine Anteile (Protocol, Host, Port, Path) zerlegen. So können wir unterscheiden, ob es sich bei der herunterzuladenen Datei um eine lokale auf dem Filesystem des Servers vorhandene Datei, oder um eine externe Datei handelt.

Bei einer externen Datei können wir diese mittels htt.get(url, callback(data)); abrufen (dies ähnelt in etwa dem jQuery pendant $.get(url, callback(data));)
Die zurück gegebene Resource (data) übergeben wir nun der deliverDownload Funktion (diese kommt später…).

Sollte es sich allerdings um eine lokale auf dem Filesystem vorhandene Resource handeln, können wir diese dank des fs Modules von Node.JS vom Dateisystem lesen bzw. besser streamen.
In diesem Fall handelt es sich bei data anstatt der ganzen Resource lediglich um einen lesbaren Stream!

Die deliverDownload Funktion kann nun sowohl mit einem Stream als auch einer ganzen Resource umgehen.

var deliverDownload = function(res, mapping, data) {
    var contentDisposition = mapping.forceDownload ? "attachment" : "inline";
    res.writeHead(data.statusCode, {
        'Content-Type': mapping.contentType,
        'Content-Disposition': contentDisposition + '; filename=' + mapping.filename + ';'
    });

    data.pipe(res);
};

Anhand des mapping (zuvor result[0].data) können wir die forceDownload Eigenschaft auslesen und ein entsprechenden Content-Disposition HTTP-Header vorbereiten.
Bei einem Content-Disposition: attachment wird der Browser einen Download der Datei veranlassen, bei einem Content-Disposition: inline jedoch versucht er die Datei mit einem geeigneten Programm innerhalb des Content-Frames anzuzeigen (bei Bildern oder PDF Dateien zum Beispiel).

Die interessanteste Zeile in dieser Funktion ist jedoch data.pipe(res). Die data Variable enthält entweder einen lesbaren Stream auf die lokale Datei oder die gesamte Resource von einem externen Server. Die mit geschliffene Variable res ist die ursprünglich an den Callback des http.createServer Aufrufs übergebene „Result“ Variable des Requests an den Server.

Durch data.pipe(res) wird also der gesamte Inhalt von data in res ‚gepiped‘ und somit an den Browser übermittelt.

Da Node.JS seine Performance Stärken besonders dann gut ausspielen kann, wenn es um Browser Anfragen mit IO-Background-Processing geht, ist dieser Dateidownload ein perfekter Einsatzbereich für Node.JS.

Kategorien: Allgemein

Kommentar verfassen

%d Bloggern gefällt das:

Durch das Fortsetzen der Benutzung dieser Seite, stimmst du der Benutzung von Cookies zu. Weitere Informationen

Wir verwenden Cookies, um Inhalte und Anzeigen zu personalisieren, Funktionen für soziale Medien anbieten zu können und die Zugriffe auf unsere Website zu analysieren. Außerdem geben wir Informationen zu Ihrer Nutzung unserer Website an unsere Partner für soziale Medien, Werbung und Analysen weiter.

Schließen