Несмотря на то, что главным моим хобби так и остаются роботы, я трачу немало усилий, чтобы оставаться в трендах своей основной стези – программирования. Волей судьбы недавно удалось познакомиться с Node.js, я узнал о его web фреймворке express, подружился с новым для себя template engine Jade и в довершение ко всему связал все это с папкой в Dropbox.
В этом посте я постараюсь коротко рассказать, как можно организовать web-сервис для хранения файлов, используя лишь бесплатные решения.
Всех заинтересованных – прошу под кат.
Подготовим плацдарм
Итак, нам понадобится:
- Node.js установленный на локальной машине
- Аккаунт в Dropbox
- Сервер Node.js приложений (если захочется запустить сервис не только локально)
Если с первыми двумя пунктами все должно быть понятно, то на третьем мне бы хотелось остановиться чуть подробнее. Я уже упоминал, что все должно получиться бесплатно, и не собираюсь отступать от своих слов.
В процессе моего “барахтанья” в Node.js мире, я наткнулся на целый ряд платформ готовых предоставить в наше распоряжение Node.js server бесплатно. Лично я испытывал две из них: Heroku и Nodester. В результате я все же остановился на втором, хотя, честно сказать, это решение ничем не обосновано.
Для регистрации в Nodester необходимо получить купон. Сделать это можно на их сайте или в командной строке через nodester-cli. Мне купон пришел на следующий день после отправления запроса. Это очень быстро, хотя я не исключаю, что мне просто повезло.
Создадим проект
Локально
С чего-то ведь надо начинать. Для этого создадим в любом удобном для нас месте папку (у меня называется habr-nodebox) и в ней файл package.json:
{
"name": "habr-nodebox",
"version": "0.0.1",
"node": "0.6.17",
"author": "andbas",
"dependencies": {
"express": "2.5.x",
"jade": "0.26.x",
"dbox": "0.4.x"
}
}
Поля name, version, author – просто дают некоторую информацию о проекте и могут быть изменены без каких-либо проблем; node – версия Node.js используемая в проекте; в секции dependencies перечисляются все используемые сторонние модули. Как я уже упоминал, в проекте будет использоваться express и jade. Плагин dbox, как понятно из названия, будет использоваться для работы с Dropbox. Я пробовал и другой плагин под названием dropbox, но он, к сожалению, не позволял авторизовать приложение, так как в нем был реализован старый API Dropbox, в котором использовался /token. На данный момент для аутентификации Dropbox использует стандарт oauth.
После сохранения этого файла в командной строке вызовем:
npm install
Если все было написано правильно, то npm скачает все упомянутые в dependencies модули и установит их в текущую директорию.
Помимо этого, создадим еще две папки public и view. Первая будет использоваться для статических файлов (CSS, JS и других), в то время как view будет использоваться для шаблонов Jade.
Тем временем в Dropbox
Если мы хотим получить возможность складывать какие-то файлы в Dropbox, нам необходимо выполнить несколько действий, первым из которых будет получить ключ и секретную строку для нашего приложения. Для этого зайдем на страницу приложений нашего Dropbox аккаунта через браузер и создадим там новое приложение. В поле Access type устанавливаем значение App folder (приложение будет иметь ограниченный доступ только к собственной папке в Dropbox).
На странице приложения запишем себе куда-нибудь App key и App Secret. Вот собственно первый шаг уже пройден.
Для автоматизации последующих шагов в авторизации я предлагаю написать небольшой скрипт на все том же node.js. Скрипт следующий (dbox-init.js в папке нашего приложения):
var dbox = require("dbox"),
stdin = process.stdin,
stdout = process.stdout;
ask('App key', /^S+$/, function(app_key) {
ask('App secret', /^S+$/, function(app_secret) {
var app = dbox.app({ 'app_key': app_key, 'app_secret': app_secret });
app.request_token(function(status, request_token){
if(request_token){
console.log('Please visit ', request_token.authorize_url, ' to authorize your app.');
ask('Is this done? (yes)', /^yes$/, function(answer) {
app.access_token(request_token, function(status, access_token){
console.log('app_key: ' + app_key);
console.log('app_secret: ' + app_secret);
console.log('oauth_token: ' + access_token.oauth_token);
console.log('oauth_token_secret: ' + access_token.oauth_token_secret);
console.log('uid: ' + access_token.uid);
process.exit();
});
});
}
});
});
});
function ask(question, format, callback) {
stdin.resume();
stdout.write(question + ": ");
stdin.once('data', function(data) {
data = data.toString().trim();
if (format.test(data)) {
callback(data);
} else {
stdout.write("It should match: "+ format +"n");
ask(question, format, callback);
}
});
}
Для того, чтобы запустить скрипт просто набираем в командной строке:
node dbox-init
Скрипт проходит вместе с вами все стадии oauth аутентификации в Dropbox и помогает получить все ключи необходимые нам. Шаги следующие:
- скрипт запрашивает App key и App Secret (те, которые мы получили ранее) и генерирует на их основании ссылку
- мы копируем ссылку в браузер и авторизуем приложение на работу с нашей учетной записью dropbox, получаем уведомление, что приложение авторизовано
- на вопрос скрипта о том, прошли ли мы авторизацию смело пишем “yes”
- получаем и записываем в укромное место данные необходимые для авторизации, а конкретно: app_key, app_secret, oauth_token, oauth_token_secret и uid
На этом подготовительные работы пройдены, можем перейти к написанию самого приложения.
В бой: набросаем контроллер
Переходим, на мой взгляд, к самому интересному – написание контроллера. Основные действия такие: просматривать все файлы, добавлять новые, получать уже существующие. Не буду томить и сразу предоставлю код (web.js).
var express = require('express'),
app = express.createServer(express.logger()),
fs = require('fs'),
dropbox = require("dbox").app({"app_key": process.env['dbox_app_key'], "app_secret": process.env['dbox_app_secret'] }),
client = dropbox.createClient({oauth_token_secret: process.env['dbox_oauth_token_secret'], oauth_token: process.env['dbox_oauth_token'], uid: process.env['dbox_uid']});
app.use(express.static(__dirname+'/public'));
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.set('view options', { layout: false });
app.use(express.bodyParser());
app.get('/', function(req, res) {
client.metadata(".", function(status, reply) {
res.render('index', {
content : reply
});
});
});
app.get('/:path', function(req, res) {
var path = req.params.path;
client.get(path, function(status, reply, metadata){
res.send(reply);
});
});
app.post('/', function(req, res) {
var fileMeta = req.files['file-input'];
if (fileMeta) {
fs.readFile(fileMeta.path, function(err, data) {
if (err) throw err;
client.put(fileMeta.name, data, function(status, reply) {
res.redirect('/');
});
});
} else {
res.redirect('/');
}
});
var port = process.env['app_port'] || 5000;
app.listen(port, function() {
console.log("Listening on " + port);
});
Я думаю немного разъяснений, что же здесь происходит, не помешает.
var express = require('express'),
app = express.createServer(express.logger()),
fs = require('fs'),
dropbox = require("dbox").app({"app_key": process.env['dbox_app_key'], "app_secret": process.env['dbox_app_secret'] }),
client = dropbox.createClient({oauth_token_secret: process.env['dbox_oauth_token_secret'], oauth_token: process.env['dbox_oauth_token'], uid: process.env['dbox_uid']});
Объявление основных кирпичиков нашего приложения:
- express – как уже упоминалось ранее, web framework
- app – это собственно само web приложение
- fs – интерфейс для работы с файловой системой
- dropbox – фабрика для создания клиента Dropbox
- client – клиент Dropbox упрощающий работу с API
app.use(express.static(__dirname+'/public'));
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.set('view options', { layout: false });
app.use(express.bodyParser());
Инициализация нашего web приложения. Здесь задаем основные параметры: пути к статическим файлам (public), директорию для шаблонов (views), движок этих самых шаблонов (jade), отключаем основной layout, чтобы немного упростить написание и обойтись одним шаблоном, и, в конце концов, передаем нашему приложению bodyParser, который будет разбирать тело приходящих запросов.
А теперь перейдем к главной магии. Далее будут следовать три основных обработчика нашего сервиса.
app.get('/', function(req, res) {
client.metadata(".", function(status, reply) {
res.render('index', {
content : reply
});
});
});
Этот код отвечает за главную страницу. На ней мы будем отображать список файлов в нашей папке Dropbox, поэтому делаем запрос через client и получаем метаинформацию. Именно она содержит информацию обо всех файлах в нашем приложении. Эту информацию мы передаем нашему движку шаблонов, который и займется рендерингом страницы.
app.get('/:path', function(req, res) {
var path = req.params.path;
client.get(path, function(status, reply, metadata){
res.send(reply);
});
});
Метод, написанный выше, будет обрабатывать запросы на получение файлов. Все предельно просто – получаем имя файла и отправляем запрос в Dropbox. Полученный ответ перенаправляем пользователю.
app.post('/', function(req, res) {
var fileMeta = req.files['file-input'];
if (fileMeta) {
fs.readFile(fileMeta.path, function(err, data) {
if (err) throw err;
client.put(fileMeta.name, data, function(status, reply) {
res.redirect('/');
});
});
} else {
res.redirect('/');
}
});
Практически всю работу за нас сделал express. Файл, отправленный в теле post запроса на сервер, был временно сохранен на файловую систему. Нам была представлена вся необходимая для нас информация в объекте req.files['file-input'], где file-input – это атрибут name элемента input формы в html. Нам остается только взять файл из файловой системы и отправить в Dropbox. После этого мы будем перенаправлены на главную страницу.
var port = process.env['app_port'] || 5000;
app.listen(port, function() {
console.log("Listening on " + port);
});
В конце мы устанавливаем порт для нашего приложения, значение по умолчанию будет 5000. Осталось только написать одну простую страничку, используя jade шаблон, этим и займемся.
Jade шаблон – все дело в отступах
Первое знакомство с Jade для меня лично было болезненным. Обычный html как-то ближе.
Однако дискомфорт быстро прошел. Вторая страничка была написана уже без неприязни. В общем, привыкаешь быстро. Рекомендую попробовать.
Подискутировать на тему удобства jade это конечно здорово, но пора и код показать. Для этого создадим в папке views файл index.jade и в нем пишем:
!!! 5
html(lang="en")
head
title habr-nodebox
body
each item, i in content.contents
div
a(href="#{item.path}") #{item.path} - #{item.size}
div
form#upload-form(action="/", method="POST", enctype="multipart/form-data")
input#file-input(name="file-input", type="file")
input#submit(value="Upload file", type="submit")
Я постарался сделать шаблон наиболее прозрачным и понятным. Конечно, добиться при этом выдающихся стилистических результатов не получится, но чем не пожертвуешь ради понятности.
Мы лишь создали простейшую HTML страничку со списком файлов в нашей папке. Для этого мы прошлись в цикле each по всем записям о файлах. Напомню, метаданные content были заботливо получены для нашего шаблона в контроллере. Каждый элемент списка – это ссылка, ведущая нас к методу контроллера, для запросов вида “/:path ”. После списка следует форма для загрузки новых файлов. Она состоит из двух input элементов, один для файла, второй для отправки формы.
Запуск
Вот собственно наше приложение и готово, осталось только его запустить. Возможно кто-то обратил внимание, когда читал, что все ключи от Dropbox были записаны как переменные массива process.env[]. Сделано это для того, чтобы не оставлять их в коде и иметь возможность опубликовать приложение без страха быть скомпрометированным. Для того чтобы передать в массив process.env свои значения, достаточно записать их в виде key=value перед вызовом node. В командной строке должно получиться что-то вида:
$ dbox_app_key=abc1qwe2rty3asd dbox_app_secret=123asd123asd123 dbox_oauth_token_secret=aaabbbccc111222 dbox_oauth_token=123asd123asd123 dbox_uid=12345678 node web
На таких сервисах как Nodester существует возможность установить process.env, так что это был единственный способ без лишних заморочек, что пришел мне в голову.
Результат, после добавления нескольких файлов, будет выглядеть следующим образом.
Вот собственно и все, если у кого-то есть желание запустить подобный код online – просто загрузите его на любой Node.js сервер. Я испытывал на Nodester – все работало, думаю, с остальными тоже не возникнет проблем.
Код можно посмотреть здесь
Автор: andbas