📜 ⬆️ ⬇️

Home file hosting based on Sinatra and DataMapper. Part 1 - The Begining

Article continuation

Long entry


Sinatra Me and my wife often have to transfer files from point A to point B, where point A is one of the home computers, and point B is either a computer of one of my friends, or, for example, a computer at work (where flash drives are not allowed ). In addition, to go for a flash drive (which can lie in any part of the apartment) and copy the file to it is also rather lazy :) In general, I usually use services like webfile.ru and box.net for this purpose, and my wife stupidly sends the file myself (or correspondent) by mail.


All these options, you understand, are rather inconvenient. I decided that the files should be stored on the home server and downloaded in some convenient way. I considered the following options:


It was possible, of course, to simply configure Apache or Lighttpd to display the contents of directories and fasten authorization, but this looks ugly outwardly and creates inconvenience when searching for a file if there are a lot of them loaded. In addition, it is difficult to restrict access to a specific file (and, if you remember, you wanted to transfer some files to your friends, not just download them to yourself).
')
In general, I decided that I am still a programmer, not horseradish from the mountain, and I am quite able to write a simple file sharing service for my needs.

Platform selection


In my life I have been able to write a lot on ASP.NET, but I got tired of it; I don't know PHP well enough and for some reason I dislike it; I am currently writing on Ruby on Rails, but it is somewhat monstrous for such a task. However, I really like Ruby itself, so I decided to take some other Ruby framework - at the same time there will be an occasion to expand my horizons a bit. At first my choice fell on Rack , but it quickly turned out that they were too austere for my requirements. Two small frameworks stand a step higher: Sinatra and Camping . For reasons unknown to me, I chose Sinatra.

Meet Sinatra


The official website says that Sinatra is not even a framework, but a DSL for creating web applications, and I am ready to agree with this definition. First I would like to install it. Nothing is easier!

sudo gem install sinatra

Now let's write the simplest web application.


Create a file, say, myapp.rb and write the following code in it:

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


Run

ruby myapp.rb

And open the browser address localhost: 4567. Voila! We see Hello from Sinatra and rejoice.

What did we write? In the first two lines we connect Sinatra. And then that DSL begins: an application on Sinatra consists of a set of blocks of the form:

METHOD PATTERN do
OUTPUT
end


Where METHOD is an HTTP method (get, post, put, delete), PATTERN is a URL pattern, and OUTPUT is a code that generates a response for the browser. When a request is received from the browser, Sinatra determines the first suitable block that matches the request by the HTTP method and the URL pattern, executes the code from this block and returns the received string to the client.

Let us dwell a little more on PATTERN. As I already wrote, this is a URL pattern, and it can be specified in several ways:


Here is an example of using different patterns:

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 .


Run the application (ruby myapp.ru) and try the following addresses:.

Little design


Now it's time to start planning our file sharing service. What I would like to the maximum:
  1. Upload files
  2. Download files
  3. Download counter (for statistics)
  4. Delete unnecessary files
  5. File password protection
  6. Access limitation
  7. Addresses that are difficult to find (if the link looks like, for example / download / 4, then it is obvious that there are / download / 3, / download / 5, and so on; if the link looks like / download / 6a1941fb0cd47, then pick up other URLs It's much harder)
  8. Ability to upload files not only through the web interface, but also through Samba


In this article, we will consider the following simplified version:


So let's start with the URL structure.
The main page ('/') using the GET method gives a list of files. She is also engaged in the service of uploading files via POST. Page '/: id' - gives the file to the browser. The '/: id / delete' page deletes the file.

Let's write it down on Sinatra:

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


Next we have a problem - how do we store the files?

First, I wanted to use the following solution: store files in the / public folder in the directory with our application (Sinatra allows you to access files from the / public folder directly, without describing any PATTERNs; That is, the file photo.jpg, which is in the directory public, located in the same directory as our myapp.rb will always be available at /photo.jpg). In this case, the list of files we get by simply calling 'ls ./public' and giving them through direct links. Problems: direct links, without the possibility of their concealment, problems with Russian file names and special characters, lack of statistics, and so on. In general, in this case, we actually have an analogue of the directory listing of Apache - and this is not what we want.

Hence the conclusion: we need a database


It is foolish to use something more complicated for such a simple system than sqlite. And take it. There remains the problem of working with the database from ruby. It was possible, of course, to take ActiveRecord and its ORM, but I decided that it was also a pretty hard thing that I did not need. So, I decided to study not only Sinatra, but also DataMapper .

DataMapper is a small, lightweight framework that allows you to work with database records as with Ruby objects.

Install and configure DataMapper


Installation is simple:

sudo gem install data_mapper

We connect to the application, appending after requre 'sinatra'

require 'dm-core' #
require 'dm-validations' # (not_null )
require 'dm-timestamps' #


Specify the name of the database with which we will work.

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

Then we need to describe the structure of the table in which we will store our data.

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



Consider what we have done here.
class StoredFile - the class whose instances represent our files.
include DataMapper :: Resource - we declare that the StoredFile class is a DataMapper resource (that is, it describes records in the database). This means that the stored_files table will be created in the database and that we get the methods for working with it. Note that, unlike ActiveRecord, we are not inherited from some class, but we plug in a module, which is much more convenient from the point of view of OOP.
Next, we describe three properties (= columns in the database) of our files - id, file name, creation date. : serial => true means that the column will auto_increment (automatically receive values),: nullable => false, obviously means that the file name cannot be NULL, the created_at column will automatically be filled with the creation time of the record (for this we connected dm- timestamps).
Finally, default_scope (: default) .update (: order => [: created_at.desc]) tells us that the default entries should be retrieved in order of descending creation time.

After describing StoredFile we write

DataMapper.auto_upgrade!

This line will force DataMapper to update the database structure when changes are made to our StoredFile class.

We work with data



So, we will work with the files as follows: with upload, we will save the file as ID.upload to the files folder (ID is the file id in the database) and write the original file name to the database. When downloading, we will give the browser a file and transfer the filename from the database as the name. Thus, in the file system we will have a file of the type 242.upload, and the browser will download it under the name "Report on kickbacks for 2008.docx".

It's time to fill our blocks with the code you were waiting for.

#
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


I hope the comments and what is happening are fairly obvious, except for the line "erb: list". Please note that upload / download and deletion occur without outputting any information to the user (at the end of it redirects to the main page). On the main page, we want to display a list of files. Of course, this could be done by creating the answer as a string directly in the code, but fortunately, sinatra allows you to use several template engines to generate HTML pages. Among the supported template engines are: HAML , Erb , Builder , Sass . Themselves, Sinatra's default templates look in the ./views folder. In our case, I wrote a fairly primitive Erb template there, called it list.erb and placed it in the views folder. Now I can call it with the erb: list command. I could also write, for example, list.haml and render it with the haml: list command.

Here is the template code:

<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>


Finally: password protection


We will do, as I promised, only a simple HTTP authorization. It is done like this:

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


Between use Rack :: Auth :: Basic do | username, password | and end can be arbitrary code that returns true when access is allowed and false otherwise. I zahardkodil login and password, but that does not interfere with taking them, for example, from the database.

Conclusion


The full application code is here - http://pastie.org/368694

If the article was interesting, I will continue the story about the organization of the home file sharing.

Thanks for attention!

Source: https://habr.com/ru/post/50031/


All Articles