Книга: Выразительный JavaScript
Простой файловый сервер
Простой файловый сервер
Давайте скомбинируем наши новые знания о серверах HTTP и работе с файловой системой, и наведём мостик между ними: HTTP-сервер, предоставляющий удалённый доступ к файлам. У такого сервера много вариантов использования. Он позволяет веб-приложениям хранить и делиться данными, или может дать группе людей доступ к набору файлов.
Когда мы относимся к файлам, как к ресурсам HTTP, методы GET
, PUT
и DELETE
можно использовать для чтения, записи и удаления файлов. Мы будем интерпретировать путь в запросе как путь к файлу.
Нам не надо открывать доступ ко всей файловой системе, поэтому мы будем интерпретировать эти пути как заданные относительно корневого каталога, и это будет каталог запуска скрипта. Если я запущу сервер из /home/marijn/public/
(или C:Usersmarijnpublic
на Windows), то запрос на /file.txt
должен указать на /home/marijn/public/file.txt
(или C:Usersmarijnpublicfile.txt
).
Программу мы будем строить постепенно, используя объект methods
для хранения функций, обрабатывающих разные методы HTTP.
var http = require("http"), fs = require("fs");
var methods = Object.create(null);
http.createServer(function(request, response) {
function respond(code, body, type) {
if (!type) type = "text/plain";
response.writeHead(code, {"Content-Type": type});
if (body && body.pipe)
body.pipe(response);
else
response.end(body);
}
if (request.method in methods)
methods[request.method](urlToPath(request.url),
respond, request);
else
respond(405, "Method " + request.method
" not allowed.");
}).listen(8000);
Этот код запустит сервер, возвращающий ошибки 405 – этот код используется для обозначения того, что запрошенный метод сервером не поддерживается.
Функция respond
передаётся функциям, обрабатывающим разные методы, и работает как обратный вызов для окончания запроса. Она принимает код статуса HTTP, тело, и, возможно, тип содержимого. Если переданное тело – поток с возможностью чтения, у него будет метод pipe
, который используется для передачи читаемого потока в записываемый. Если нет – предполагается, что это либо null
(тело пустое), или строка, и тогда она передаётся напрямую в метод ответа end
.
Чтобы получить путь из URL в запросе, функция urlToPath
, используя встроенный модуль Node “url”
, разбирает URL. Она принимает имя пути, нечто вроде /file.txt
, декодирует, чтобы убрать экранирующие коды %20
, и вставляет в начале точку, чтобы получить путь относительно текущего каталога.
function urlToPath(url) {
var path = require("url").parse(url).pathname;
return "." + decodeURIComponent(path);
}
Вам кажется, что функция urlToPath
небезопасна? Вы правы. Вернёмся к этому вопросу в упражнениях.
Мы устроим метод GET
так, чтобы он возвращал список файлов при чтении директории, и содержимое файла при чтении файла.
Вопрос на засыпку – какой тип заголовка Content-Type
мы должны возвращать, читая файл. Поскольку в файле может быть всё, что угодно, сервер не может просто вернуть один и тот же тип для всех. Но NPM с этим может помочь. Модуль mime
(индикаторы типа содержимого файла вроде text/plain
также называются MIME types) знает правильный тип для огромного количества расширений файлов.
Запустив следующую команду npm
в директории, где живёт скрипт сервера, вы сможете использовать require("mime")
для запросов к библиотеке типов.
$ npm install mime
npm http GET https://registry.npmjs.org/mime
npm http 304 https://registry.npmjs.org/mime
[email protected] node_modules/mime
Когда запрошенного файла не существует, правильным кодом ошибки для этого случая будет 404. Мы будем использовать fs.stat
для возврата информации по файлу, чтобы выяснить, есть ли такой файл, и не директория ли это.
methods.GET = function(path, respond) {
fs.stat(path, function(error, stats) {
if (error && error.code == "ENOENT")
respond(404, "File not found");
else if (error)
respond(500, error.toString());
else if (stats.isDirectory())
fs.readdir(path, function(error, files) {
if (error)
respond(500, error.toString());
else
respond(200, files.join("n"));
});
else
respond(200, fs.createReadStream(path),
require("mime").lookup(path));
});
};
Поскольку запросы к диску занимают время, fs.stat
работает асинхронно. Когда файла не существует, fs.stat
передаст объект error
с кодовым свойством "ENOENT"
в функцию обратного вызова. Было бы здорово, если бы Node определил разные типы ошибок для разных ошибок, но такого нет. Вместо этого он выдаёт запутанные коды в стиле Unix.
Все неожиданные ошибки мы будем выдавать с кодом 500, обозначающим, что на сервере проблема – в отличие от кодов, начинающихся на 4, говорящих о проблеме с запросом. В некоторых ситуациях это будет не совсем аккуратно, но для небольшой примерной программы этого будет достаточно.
Объект stats
возвращаемый fs.stat
, рассказывает нам о файле всё. Например, size
– размер файла, mtime
– дата модификации. Здесь нам нужно узнать, директория это или обычный файл – это нам сообщит метод isDirectory
.
Для чтения списка файлов в директории мы используем fs.readdir
, и через ещё один обратный вызов, возвращаем его пользователю. Для обычных файлов мы создаём читаемый поток через fs.createReadStream
и передаём его в ответ, вместе с типом содержимого, который модуль "mime"
выдал для этого файла.
Код обработки DELETE
будет проще:
methods.DELETE = function(path, respond) {
fs.stat(path, function(error, stats) {
if (error && error.code == "ENOENT")
respond(204);
else if (error)
respond(500, error.toString());
else if (stats.isDirectory())
fs.rmdir(path, respondErrorOrNothing(respond));
else
fs.unlink(path, respondErrorOrNothing(respond));
});
};
Возможно, вам интересно, почему попытка удаления несуществующего файла возвращает статус 204 вместо ошибки. Можно сказать, что при попытке удалить несуществующий файл, так как файла там уже нет, то запрос уже исполнен. Стандарт HTTP поощряет людей делать идемпотентные запросы – то есть такие, при которых многократный повтор одного и того же действия не приводит к разным результатам.
function respondErrorOrNothing(respond) {
return function(error) {
if (error)
respond(500, error.toString());
else
respond(204);
};
}
Когда ответ HTTP не содержит данных, можно использовать код статуса 204 (“no content”). Так как нам нужно обеспечить функции обратного вызова, которые либо сообщают об ошибки, или возвращают ответ 204 в разных ситуациях, я написал специальную функцию respondErrorOrNothing
, которая создаёт такой обратный вызов.
Вот обработчик запросов PUT
:
methods.PUT = function(path, respond, request) {
var outStream = fs.createWriteStream(path);
outStream.on("error", function(error) {
respond(500, error.toString());
});
outStream.on("finish", function() {
respond(204);
});
request.pipe(outStream);
};
Здесь нам не нужно проверять существование файла – если он есть, мы его просто перезапишем. Опять мы используем pipe
для передачи данных из читаемого потока в записываемый, в нашем случае – из запроса в файл. Если создать поток не удаётся, создаётся событие “error”
, о чём мы сообщаем в ответе. Когда данные переданы успешно, pipe
закроет оба потока, что приведёт к запуску события “finish”
. А после этого мы можем отчитаться об успехе с кодом 204.
Полный скрипт сервера лежит тут: eloquentjavascript.net/code/file_server.js. Его можно скачать и запустить через Node. Конечно, его можно менять и дополнять для решения упражнений или экспериментов.
Утилита командной строки curl
, общедоступная на unix-системах, может использоваться для создания HTTP запросов. Следующий фрагмент тестирует наш сервер. –X
используется для задания метода запроса, а –d
для включения тела запроса.
$ curl http://localhost:8000/file.txt
File not found
$ curl -X PUT -d hello http://localhost:8000/file.txt
$ curl http://localhost:8000/file.txt
hello
$ curl -X DELETE http://localhost:8000/file.txt
$ curl http://localhost:8000/file.txt
File not found
Первый запрос к file.txt
завершается с ошибкой, поскольку файла ещё нет. Запрос PUT
создаёт файл, и глядите-ка – следующий запрос его успешно получает. После его удаления через DELETE
файл снова отсутствует.
- Запуск InterBase-сервера
- Расширенная установка InterBase-сервера
- Совместимость клиентов и серверов различных версий
- СТРУКТУРА ПРОСТОЙ ПРОГРАММЫ
- Статистика InterBase-сервера
- Сервер для InterBase
- 1.3.3. Достоинства и недостатки анонимных прокси-серверов
- Минимальный состав сервера InterBase SuperServer
- ПРИМЕР ПРОСТОЙ ПРОГРАММЫ НА ЯЗЫКЕ СИ
- Отличительные особенности сервера Yaffil
- Встраиваемый сервер
- Использование сервера Yaffil внутри процесса