Как стать автором
Обновить

Домашний файлообменник на базе Sinatra и DataMapper. Часть 1 — The Begining

Время на прочтение8 мин
Количество просмотров11K
Продолжение статьи

Длительное вступление


Логотип  SinatraМне и моей жене часто приходится передавать файлы из точки А в точку Б, где точка А — один из домашних компьютеров, а точка Б — либо компьютер кого-нибудь из знакомых, либо, например, компьютер на работе (где, порой, запрещены флешки). К тому же идти за флешкой (которая может лежать в любой части квартиры) и копировать файл на нее — тоже достаточно лениво :) В общем, я обычно использую для этих целей сервисы вроде webfile.ru и box.net, а жена тупо посылает файл себе (или корреспонденту) по почте.


Все эти варианты, сами понимаете, достаточно неудобны. Я решил, что файлы надо хранить на домашнем сервере, а скачивать каким-нибудь удобным способом. Я рассмотрел следующие варианты:
  • Поднять FTP. Но я не люблю FTP, и не везде есть возможность им воспользоваться
  • Поднять WebDAV сервер. Но это, во-первых, не очень секьюрно, а во-вторых, с WebDAV проблем еще больше, чем с FTP
  • Скачивать файлы через браузер. На этом варианте я и остановился


Можно было, конечно, просто настроить Apache или Lighttpd на показ содержимого каталогов и прикрутить авторизацию, но это некрасиво выглядит внешне и создает неудобства при поиске файла, если их загружено много. К тому же сложно ограничить доступ к конкретному файлу (а, если вы помните, некоторые файлы хотелось передавать знакомым, а не только скачивать себе).

В общем, я решил, что я всё-таки программист, а не хрен с горы и мне вполне по силам написать простенький файлообменник для своих нужд.

Выбор платформы


В своей жизни мне много доводилось писать на ASP.NET, но он мне надоел; PHP я знаю недостаточно хорошо и почему-то недолюбливаю; сейчас я пишу на Ruby on Rails, но он несколько монструозен для такой задачи. Однако сам язык Ruby мне очень нравится, поэтому я решил взять какой-нибудь другой framework на Ruby — заодно будет повод немного расширить свой кругозор. Сначала мой выбор пал на Rack, но быстро выяснилось, что они слишком аскетичен для моих требований. На ступеньку выше стоят два небольших фреймворка: Sinatra и Camping. По непонятным мне причинам, я выбрал Синатру.

Знакомьтесь — Синатра


На официальном сайте написано, что Синатра это даже не фреймворк, а DSL для создания веб-приложений, и я готов согласиться с таким определением. Для начала хотелось бы его установить. Нет ничего проще!

sudo gem install sinatra

Теперь напишем простейшее веб-приложение


Создаем файл, скажем, myapp.rb и пишем в нем следующий код:

  1. require 'rubygems'
  2. require 'sinatra'
  3.  
  4. get '/' do
  5.  'Hello from Sinatra'
  6. end
* This source code was highlighted with Source Code Highlighter.


Запускаем

ruby myapp.rb

И открываем в броузере адрес localhost:4567. Вуаля! Видим Hello from Sinatra и радуемся.

Что же мы написали? В первых двух строчках мы подключаем Синатру. А далее начинается как раз тот самый DSL: приложение на Синатре состоит из набора блоков вида:

METHOD PATTERN do
OUTPUT
end


Где METHOD — это HTTP-метод (get, post, put, delete), PATTERN — шаблон URL'а, а OUTPUT — код, генерирующий ответ для браузера. При поступлении запроса от браузера Sinatra определяет первый подходящий блок, соответствующий запросу по HTTP-методу и шаблону URL'а, выполняет код из этого блока и возвращает полученную строку клиенту.

Остановимся чуть подробнее на PATTERN. Как я уже писал, это шаблон URL'а, и он может быть задан несколькими способами:
  • Константой (например, '/', '/about', '/hello/world')
  • Константой с именованными параметрами (например, '/:id', '/post/:id/show', '/user/:name/:action') — в этом случае, в OUTPUT можно обращаться к значениям соответствующих параметров через хэш params (params[:id], params[:name], params[:action])
  • Константой с использованием wildcard (например, '/select/*/from/*', '/load/*') — в этой ситуации в хэше params будет присутствовать массив params[:splat], содержащий все соответствия звездочкам (в первом случае при обращении, скажем, к '/select/id/from/users' params[:splat] будет содержать ['id', 'users'], а во втором, при обращении к '/load/all/my/money' в params[:splat] будет лежать ['all/my/money'] )
  • Регулярным выражением (например, '%r{/hello/([\w]+)}') — тут мы имеем дело уже с массивом params[:captures]


Вот пример использования различных паттернов:

require 'rubygems'
require 'sinatra'

get '/' do
  'Index page has you. Follow the white rabit.'
end

get '/give/me/the/:key' do
   "I don't have any #{params[:key]}. Maybe you should try another url."
end

get '/buy/*/in/*' do
  "#{params[:splat][1]} doesn't sell #{params[:splat][0].pluralize}"
end

get %r{^/where/is/(habr)(a\1)?.ru$} do
  "You're looking for <a href=\"http://habrahabr.ru\">habrahabr.ru</a>. It's not here."
end


* This source code was highlighted with Source Code Highlighter.


Запустите приложение (ruby myapp.ru) и попробуйте следующие адреса:
  • localhost:4567/
  • localhost:4567/give/me/the/money
  • localhost:4567/buy/cars/in/Walmart
  • localhost:4567/where/is/habr.ru
  • localhost:4567/where/is/habrahabr.ru
.

Немного проектирования


Теперь пришла пора заняться планированием нашего файлообменника. Что хотелось бы по-максимуму:
  1. Upload файлов
  2. Download файлов
  3. Счетчик скачиваний (для статистики)
  4. Удаление ненужных файлов
  5. Защита файла паролем
  6. Ограничение доступа
  7. Адреса, которые сложно подобрать (если ссылка имеет вид, например /download/4, то очевидно, что есть и /download/3, /download/5 и так далее; если же ссылка выглядит как /download/6a1941fb0cd47, то подобрать другие URL'ы гораздо сложнее)
  8. Возможность загружать файлы не только через веб-интерфейс, но и через Samba


В рамках этой статьи мы рассмотрим следующий упрощенный вариант:
  • Upload/download только через веб-интерфейс
  • Защита с помощью Basic-HTTP аутентификации
  • Простые ссылки на скачивание


Итак, начнем со структуры URL'ов.
Главная страница ('/') по методу GET выдает список файлов. Она же занимается обслуживанием upload'а файлов по POST'у. Страница '/:id' — отдает файл броузеру. Страница '/:id/delete' файл удаляет.

Давайте запишем это на Синатре:

get '/' do
# ... file index ...
end

post '/' do
# ... file upload ...
end

get '/:id' do
# ... send file to browser ...
end

get '/:id/delete' do
# ... delete file ...
end


Далее у нас возникает проблема — как мы храним файлы?

Сначала я хотел использовать следующее решение: хранить файлы в папке /public в директории с нашим приложением (Sinatra позволяет получать доступ к файлам из папки /public напрямую, без описания каких-либо PATTERN'ов; То есть файл photo.jpg, лежащий в директории public, находящейся в той же директории, что и наш myapp.rb всегда будет доступен по адресу /photo.jpg). В этом случае список файлов мы получаем просто вызывая 'ls ./public' и отдаем их по прямым ссылкам. Проблемы: прямые ссылки, без возможности их сокрытия, проблемы с русскими именами файлов и спецсимволами, отсутствие статистики и так далее. В общем, в этом случае мы фактически имеем аналог directory listing апача — а это не то, что мы хотим.

Отсюда вывод: нам нужна база данных


Глупо использовать для такой простой системы что-либо сложнее, чем sqlite. Ее и возьмем. Остается проблема работы с БД из ruby. Можно было, конечно, взять ActiveRecord и его ORM, но я решил, что это тоже довольно тяжелая штука, которая мне не понадобиться. Итак, я решил изучить не только Sinatra, но и DataMapper.

DataMapper — это небольшой легковесный фреймворк, позволяющий работать с записями БД, как с объектами Ruby.

Установка и настройка DataMapper


Установка проста:

sudo gem install data_mapper

Подлючаем к приложению, дописывая после requre 'sinatra'

require 'dm-core' # ядро
require 'dm-validations' # валидации (not_null и тому подобное)
require 'dm-timestamps' # автоматическое проставление времени создания и обновления для записей


Указываем имя БД, с которой будем работать.

DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/mydatabase.sqlite3")

Затем нам надо описать структуру таблицы, в которой мы будем хранить наши данные.

class StoredFile
include DataMapper::Resource

property :id, Integer, :serial => true
property :filename, String, :nullable => false
property :created_at, DateTime

default_scope(:default).update(:order => [:created_at.desc])
end



Рассмотрим, что мы тут натворили.
class StoredFile — класс, экземпляры которого представляют наши файлы.
include DataMapper::Resource — объявляем, что класс StoredFile является ресурсом DataMapper'а (то есть описывает записи в БД). Это означает, что в базе будет создана таблица stored_files и что мы получаем методы для работы с ней. Заметьте, что в отличие от ActiveRecord, мы не наследуемся от какого-то класса, а подключаем модуль, что гораздо удобнее с точки зрения ООП.
Далее мы описываем три свойства (=столбца в БД) наших файлов — id, имя файла, дата создания. :serial => true означает, что столбец будет auto_increment (автоматически получать значения), :nullable => false, очевидно, означает, что имя файла не может быть NULL, столбец created_at автоматически будет заполнятся временем создания записи (для этого мы подключали dm-timestamps).
Наконец, default_scope(:default).update(:order => [:created_at.desc]) говорит нам о том, что записи по умолчанию надо доставать упорядоченными по убыванию времени создания.

После описания StoredFile пишем

DataMapper.auto_upgrade!

Эта строчка заставит DataMapper обновлять структуру БД при изменениях нашего класса StoredFile.

Работаем с данными



Итак, с файлами мы будем работать следующим образом: при аплоаде, мы будем сохранять файл под именем ID.upload в папку files (ID — это id файла в БД) и записывать в БД оригинальное имя файла. При скачивании мы будем отдавать браузеру файл и в качестве имени передавать значение filename из БД. Таким образом в файловой системе у нас будет файл вида 242.upload, а броузер будет скачивать его под именем «Отчет по откатам за 2008 год.docx».

Пришла пора заполнить наши блоки кодом, который вы так ждали.

# список файлов
get '/' do
@files = StoredFile.all # Достаем все файлы
erb :list # Рендерим шаблон, см. далее
end

# upload файла
post '/' do
tempfile = params['file'][:tempfile] # достаем файл из POST
@file = StoredFile.new :filename => params['file'][:filename] # создаем новый объект StoredFile
@file.save! # сохраняем его в БД
File.copy(tempfile.path, "./files/#{@file.id}.upload") # записываем наш файл под специальным именем в нужную папку
redirect '/' # редирект на главную
end

# download файла
get '/:id' do
@file = StoredFile.get(params[:id]) # загружаем информацию о файле из БД
send_file "./files/#{@file.id}.upload", :filename => @file.filename, :type => 'Application/octet-stream' # отдаем файл броузеру в виде аттачмента
redirect '/' # редирект на главную
end

# удаление файла
get '/:id/delete' do
StoredFile.get(params[:id]).destroy # удаляем файл из БД
File.delete("./files/#{params[:id]}.upload") # удаляем файл из ФС
redirect '/' # редирект на главную
end


Надеюсь, комментарии и происходящее достаточно очевидны, за исключением строчки «erb :list». Обратите внимание, что upload/download и удаление происходят без вывода какой-либо информации пользователю (в конце его редиректит на главную страницу). На главной же мы хотим отображать список файлов. Конечно, это можно было сделать и сформировав ответ в виде строки прямо в коде, но к счастью, sinatra позволяет вам использовать несколько шаблонизаторов для генерации HTML страниц. Среди поддерживаемых шаблонизаторов есть: HAML, Erb, Builder, Sass. Cами шаблоны Синатра по умолчанию ищет в папке ./views. В нашем случае, я написал там довольно примитивный Erb шаблон, назвал его list.erb и поместил в папку views. Теперь я могу вызывать его командой erb :list. Также я мог написать, например list.haml и рендерить его командой haml :list.

Вот код шаблона:

<h2>Список файлов:</h2>
<table>
<tr>
<th>Скачать</th>
<th>Загружен</th>
<th>Удалить</th>
</tr>
<% @files.each do |file| %>
<tr>
<td><a href="/<%= file.id %>" title="<%=file.filename%>"><%= file.filename %></a></td>
<td><%= file.created_at.strftime("%d %b")%></td>
<td><a href="/<%= file.id %>/delete" title="Удалить файл">Удалить</a></td>
</tr>
<% end %>
</table>
<h3>Добавить</h3>
<form name="new_file" id="new_file" method="POST" enctype="multipart/form-data">
<input type="file" name="file"/><br />
<input type="submit" value="Загрузить"/>
</form>


Напоследок: защита паролем


Мы сделаем, как я и обещал, только простую HTTP-авторизацию. Делается она так:

use Rack::Auth::Basic do |username, password|
username == 'admin' && password == 'secret'
end


Между use Rack::Auth::Basic do |username, password| и end может находится произвольный код, возвращающий true, когда доступ разрешен и false в противном случае. Я захардкодил логин и пароль, но ни чего не мешает брать их, к примеру, из БД.

Заключение


Полный код приложения находится тут — http://pastie.org/368694

Если статья оказалась интересной, я продолжу рассказ про организацию домашнего файлообменника.

Спасибо за внимание!

Теги:
Хабы:
+61
Комментарии49

Публикации

Истории

Работа

Ruby on Rails
11 вакансий
Программист Ruby
8 вакансий

Ближайшие события