Продолжение статьи
Мне и моей жене часто приходится передавать файлы из точки А в точку Б, где точка А — один из домашних компьютеров, а точка Б — либо компьютер кого-нибудь из знакомых, либо, например, компьютер на работе (где, порой, запрещены флешки). К тому же идти за флешкой (которая может лежать в любой части квартиры) и копировать файл на нее — тоже достаточно лениво :) В общем, я обычно использую для этих целей сервисы вроде webfile.ru и box.net, а жена тупо посылает файл себе (или корреспонденту) по почте.
Все эти варианты, сами понимаете, достаточно неудобны. Я решил, что файлы надо хранить на домашнем сервере, а скачивать каким-нибудь удобным способом. Я рассмотрел следующие варианты:
Можно было, конечно, просто настроить Apache или Lighttpd на показ содержимого каталогов и прикрутить авторизацию, но это некрасиво выглядит внешне и создает неудобства при поиске файла, если их загружено много. К тому же сложно ограничить доступ к конкретному файлу (а, если вы помните, некоторые файлы хотелось передавать знакомым, а не только скачивать себе).
В общем, я решил, что я всё-таки программист, а не хрен с горы и мне вполне по силам написать простенький файлообменник для своих нужд.
В своей жизни мне много доводилось писать на ASP.NET, но он мне надоел; PHP я знаю недостаточно хорошо и почему-то недолюбливаю; сейчас я пишу на Ruby on Rails, но он несколько монструозен для такой задачи. Однако сам язык Ruby мне очень нравится, поэтому я решил взять какой-нибудь другой framework на Ruby — заодно будет повод немного расширить свой кругозор. Сначала мой выбор пал на Rack, но быстро выяснилось, что они слишком аскетичен для моих требований. На ступеньку выше стоят два небольших фреймворка: Sinatra и Camping. По непонятным мне причинам, я выбрал Синатру.
На официальном сайте написано, что Синатра это даже не фреймворк, а DSL для создания веб-приложений, и я готов согласиться с таким определением. Для начала хотелось бы его установить. Нет ничего проще!
Создаем файл, скажем, myapp.rb и пишем в нем следующий код:
Запускаем
И открываем в броузере адрес localhost:4567. Вуаля! Видим Hello from Sinatra и радуемся.
Что же мы написали? В первых двух строчках мы подключаем Синатру. А далее начинается как раз тот самый DSL: приложение на Синатре состоит из набора блоков вида:
Где METHOD — это HTTP-метод (get, post, put, delete), PATTERN — шаблон URL'а, а OUTPUT — код, генерирующий ответ для браузера. При поступлении запроса от браузера Sinatra определяет первый подходящий блок, соответствующий запросу по HTTP-методу и шаблону URL'а, выполняет код из этого блока и возвращает полученную строку клиенту.
Остановимся чуть подробнее на PATTERN. Как я уже писал, это шаблон URL'а, и он может быть задан несколькими способами:
Вот пример использования различных паттернов:
Запустите приложение (ruby myapp.ru) и попробуйте следующие адреса:
Теперь пришла пора заняться планированием нашего файлообменника. Что хотелось бы по-максимуму:
В рамках этой статьи мы рассмотрим следующий упрощенный вариант:
Итак, начнем со структуры URL'ов.
Главная страница ('/') по методу GET выдает список файлов. Она же занимается обслуживанием upload'а файлов по POST'у. Страница '/:id' — отдает файл броузеру. Страница '/:id/delete' файл удаляет.
Давайте запишем это на Синатре:
Далее у нас возникает проблема — как мы храним файлы?
Сначала я хотел использовать следующее решение: хранить файлы в папке /public в директории с нашим приложением (Sinatra позволяет получать доступ к файлам из папки /public напрямую, без описания каких-либо PATTERN'ов; То есть файл photo.jpg, лежащий в директории public, находящейся в той же директории, что и наш myapp.rb всегда будет доступен по адресу /photo.jpg). В этом случае список файлов мы получаем просто вызывая 'ls ./public' и отдаем их по прямым ссылкам. Проблемы: прямые ссылки, без возможности их сокрытия, проблемы с русскими именами файлов и спецсимволами, отсутствие статистики и так далее. В общем, в этом случае мы фактически имеем аналог directory listing апача — а это не то, что мы хотим.
Глупо использовать для такой простой системы что-либо сложнее, чем sqlite. Ее и возьмем. Остается проблема работы с БД из ruby. Можно было, конечно, взять ActiveRecord и его ORM, но я решил, что это тоже довольно тяжелая штука, которая мне не понадобиться. Итак, я решил изучить не только Sinatra, но и DataMapper.
DataMapper — это небольшой легковесный фреймворк, позволяющий работать с записями БД, как с объектами Ruby.
Установка проста:
Подлючаем к приложению, дописывая после requre 'sinatra'
Указываем имя БД, с которой будем работать.
Затем нам надо описать структуру таблицы, в которой мы будем хранить наши данные.
Рассмотрим, что мы тут натворили.
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 обновлять структуру БД при изменениях нашего класса StoredFile.
Итак, с файлами мы будем работать следующим образом: при аплоаде, мы будем сохранять файл под именем ID.upload в папку files (ID — это id файла в БД) и записывать в БД оригинальное имя файла. При скачивании мы будем отдавать браузеру файл и в качестве имени передавать значение filename из БД. Таким образом в файловой системе у нас будет файл вида 242.upload, а броузер будет скачивать его под именем «Отчет по откатам за 2008 год.docx».
Пришла пора заполнить наши блоки кодом, который вы так ждали.
Надеюсь, комментарии и происходящее достаточно очевидны, за исключением строчки «erb :list». Обратите внимание, что upload/download и удаление происходят без вывода какой-либо информации пользователю (в конце его редиректит на главную страницу). На главной же мы хотим отображать список файлов. Конечно, это можно было сделать и сформировав ответ в виде строки прямо в коде, но к счастью, sinatra позволяет вам использовать несколько шаблонизаторов для генерации HTML страниц. Среди поддерживаемых шаблонизаторов есть: HAML, Erb, Builder, Sass. Cами шаблоны Синатра по умолчанию ищет в папке ./views. В нашем случае, я написал там довольно примитивный Erb шаблон, назвал его list.erb и поместил в папку views. Теперь я могу вызывать его командой erb :list. Также я мог написать, например list.haml и рендерить его командой haml :list.
Вот код шаблона:
Мы сделаем, как я и обещал, только простую HTTP-авторизацию. Делается она так:
Между use Rack::Auth::Basic do |username, password| и end может находится произвольный код, возвращающий true, когда доступ разрешен и false в противном случае. Я захардкодил логин и пароль, но ни чего не мешает брать их, к примеру, из БД.
Полный код приложения находится тут — http://pastie.org/368694
Если статья оказалась интересной, я продолжу рассказ про организацию домашнего файлообменника.
Спасибо за внимание!
Длительное вступление

Все эти варианты, сами понимаете, достаточно неудобны. Я решил, что файлы надо хранить на домашнем сервере, а скачивать каким-нибудь удобным способом. Я рассмотрел следующие варианты:
- Поднять 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 и пишем в нем следующий код:
- require 'rubygems'
- require 'sinatra'
-
- get '/' do
- 'Hello from Sinatra'
- 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
Немного проектирования
Теперь пришла пора заняться планированием нашего файлообменника. Что хотелось бы по-максимуму:
- Upload файлов
- Download файлов
- Счетчик скачиваний (для статистики)
- Удаление ненужных файлов
- Защита файла паролем
- Ограничение доступа
- Адреса, которые сложно подобрать (если ссылка имеет вид, например /download/4, то очевидно, что есть и /download/3, /download/5 и так далее; если же ссылка выглядит как /download/6a1941fb0cd47, то подобрать другие URL'ы гораздо сложнее)
- Возможность загружать файлы не только через веб-интерфейс, но и через 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
Если статья оказалась интересной, я продолжу рассказ про организацию домашнего файлообменника.
Спасибо за внимание!