The previous two articles (
one and
two ) turned out to be much more popular than I could have expected. And now it's time for the third and final article about the file sharing service based on Sinatra and DataMapper.
This time we will look at:
- Problem with the same file names
- Waiting page before loading
- Creating CSS with SASS
- Authentication
- Run from under thin
- Testing with RSpec
- Benchmarks
')
Same file names
The last time
Kane noticed an important error in the application: the key to download the file is a digest on its behalf - but what happens if we load two files with the same name? Unfortunately, since their digests are the same, in the current version we cannot even tell which of the files will be given to the user - the first or the second. But, fortunately, this error is easy to correct: we simply add the id of the file we want to download to the download link. So two identical files will have different links of the form:
/ DIGEST / ID1
/ DIGEST / ID2
But users will still not be able to identify links for downloading other files (the only thing that can be done is to iterate over the ID for the same digest hoping to download the same file). For this fix, we will have to change quite a bit: the code in init.rb and the template list.haml.
init.rb :
get '/: sha /: id' do
@file = StoredFile.first: sha => params [: sha],: id => params [: id]
Next, no change
get '/: sha /: id / delete' do
@file = StoredFile.first: sha => params [: sha],: id => params [: id]
Next, no change
list.haml :
% a {: href => "/#{file.sha}/##file.id}",: title => file.filename} = file.filename
Now we are not afraid of the namesake files!
Page before loading
Imagine a situation: your friend asks you to send him a photo made by you. You drop it into your file sharing service and send it a link to the file sharing service, it starts downloading it. And now let's complicate the task: the file size is 20 megabytes, and a friend sits on GPRS. Naturally, if he knew the file size in advance, he would not download it to save expensive traffic. Solution: create a page that will be displayed before downloading and place information on the file name and size on it.
Let's start with init.rb:
get '/: sha /: id' do
@file = StoredFile.first: sha => params [: sha],: id => params [: id]
unless params [: nowait] == ​​'true'
haml: download
else
@ file.downloads + = 1
@ file.save
send_file "./files/#{@file.id}.upload",: filename => @ file.filename,: type => 'Application / octet-stream'
end
end
So: if in the link parameters it is transferred “nowait = true”, then the download starts instantly, otherwise we just show the download.haml template.
And here he is:
download.haml :
% script {: type => "text / javascript"}
nowait = '? nowait = true';
var timeout = true;
setTimeout ('if (timeout) {window.location = window.location + nowait;}', 10,000);
% h1 file download
.info
You are about to download the file.
% span.filename> = "#{@file.filename}"
by size
% span.filesize
= @ file.filesize / 1024
kilobyte
The download will start for
% span # seconds 10
seconds Press on
% a {: href => "/ # {params [: sha]}? nowait = true",: onclick => 'timeout = false;' } this link
if you don't want to wait
At the beginning there is a simple JavaScript that waits 10 seconds and redirects us to the same link, but with the parameter “nowait = true”, and then the text itself, indicating the file name and its size.
It remains only to expand the file list template so that it contains two links - for immediate download (we will use it ourselves) and for downloading with a delay (we will send this link to ICQ). Looks like that:
list.haml :
% td.filename
% a {: href => "/#{file.sha}/#{file.id}?nowait=true",: title => file.filename} = file.filename
= "(# {file.filesize / 1024} Kb)"
% a {: href => "/#{file.sha}/##file.id}"} For shipping
Put a tick and proceed to the next item.
SASS
SASS is the part of the Haml package that is responsible for creating CSS files. In terms of syntax, SASS is located between CSS and Haml: it uses a scheme with selectors and attributes (CSS), but it uses indents (Haml) as a delimiter, rather than braces.
SASS file consists of a set of rules:
SELECTOR (S)
: PROPERTY1 VALUE1
: PROPERTY2 VALUE2
...
: PROPERTY_N VALUE_N
Where SELECTOR (S) is one or more ordinary CSS selectors (class, id, tag name), and PROPRETY_X / VALUE_X are the names and values ​​of CSS properties. Very similar to CSS, but there are some differences:
- Instead of curly braces, an indent of 2 spaces is used.
- You can use nested rules (damn convenient in the case of a complex set of CSS rules):
#main p
: color # 00ff00
: width 97%
.redbox
: background-color # ff0000
: color # 000000
compiled into #main p {
color: # 00ff00;
width: 97%; }
#main p .redbox {
background-color: # ff0000;
color: # 000000; }
- Nested "namespaces":
.funky
font
: family fantasy
: size 30em
: weight bold
Compiles to .funky {
font-family: fantasy;
font-size: 30em;
font-weight: bold; }
- Using constants and arithmetic operations:
! main_width = 10
! unit1 = em
! unit2 = px
! bg_color = # a5f39e
#main
: background-color =! bg_color
p
: background-color =! bg_color + # 202020
: width =! main_width +! unit1
img.thumb
: width = (! main_width + 15) +! unit2
- SASS comments - they are present in the SASS template, but they are not in the final CSS
- Several options for formatting the final CSS (starting from the most readable expanded and ending with minimal compressed)
In general, a huge amount of advantages - perhaps, many designers and web developers would be greatly helped by the knowledge of SASS in creating serious projects.
But back to our sheep: SASS file can be used in two ways - you can get a CSS file from it and connect it to the application, or you can use the SASS template engine built into Sinatra to generate CSS on the fly. We will use the second method in spite of its senselessness :)
init.rb :
get '/style.css' do
response ['Content-Type'] = 'text / css; charset = utf-8 '# Set the response header
sass: style
end
layout.haml :
% link {: href => "/style.css",: media => "screen",: rel => "stylesheet",: type => "text / css"}
Well, and my file style.sass you can look at
this link .
Now our application has some kind of design.
Authentication
It's time to attach a normal authentication mechanism to our application. We want everyone to be able to download files via direct links, but uploading and deleting files, as well as viewing the general list should be available only after entering the password (for simplicity, we will install one hard-coded password).
I solved the problem as follows: I took the HTTP authentication module (code by
reference ), put it in the lib folder and made the following changes to init.rb:
require 'lib / authorization'
helpers do
include Sinatra :: Authorization
end
get '/' do
require_administrative_privileges
# Get no further
end
post '/' do
require_administrative_privileges
# Further post without changes
end
get '/: sha /: id / delete' do
require_administrative_privileges
# Delete without changes
end
In short, the “helpers do ... end” block is executed in the context of all our blocks - URL handlers, that is, we make the Sinatra: Authorization module accessible within the application. In the same block, you can define methods that can be used in templates and the main application (the so-called helpers are auxiliary methods that allow you to avoid repetitions of the same code in the templates).
Run from under thin
So, our application has reached industrial heights and is ready for deployment on a production server. Let me remind you that now we run it with the command “ruby init.rb” and it works while the console is open with ruby ​​- naturally, this is not serious - the web application should be launched by the web server. I choose
thin as a web server - a compact and extremely fast server for Ruby applications. Installation is simple:
sudo gem install thin
Now it's time to create several folders in the directory of our application.
mkdir config
mkdir tmp
mkdir log
In the config folder, move the config.rb file from the lib folder (at the same time adjusting the path to it in init.rb). To configure thin, we need a file that we call thin.yml - create it in the config folder and write the following:
---
environment: production
chdir: APPLICATION CATALOG
pid: APPLICATION CATALOG / tmp / thin.pid
rackup: APPLICATION CATALOG / config / config.ru
log: APPENDIX / log / thin.log
max_conns: 1024
timeout: 30
max_persistent_conns: 512
daemonize: true
We tell thin that we need to work in the production environment, make chdir in the root directory of the application, place the PID file in the tmp folder, take the Rackup file (see below) in the config folder, keep the log in log / thin.log, support up to 1024 simultaneous connections with a timeout of 30 seconds, hold up to 512 persistent connections and work as a daemon (that is, regardless of the presence of a logged in user in the system).
Now about the rackup file: in fact, this is the configuration file for the Rack - the interface between Ruby and the web server (in our case, thin). This file contains only two lines:
require 'init'
Rack :: Handler :: Thin.run Sinatra :: Application,: Port => 3000,: Host => "0.0.0.0"
The first line connects init.rb (that is, our application), the second tells Rack that you need to run thin on port 3000 and give it a Sinatra application.
It is done! Now the application is launched by this command.
thin start -C config / thin.yml
We simply transfer the thin configuration file.
Stop is a team
thin stop -C config / thin.yml
Testing with RSpec
I specifically left this section in the end because I understand that very few people will test applications for Sinatra. I will not go into details and tell you what
RSpec is , just show you what the specs look like.
require 'sinatra'
require 'sinatra / test / rspec'
require 'init'
describe 'TrashFiles app' do
it 'should render template with delay' do
@file = StoredFile.first
get "/#{@file.sha}/#{@file.id}"
@response ['Content-Type']. should == "text / html"
end
it 'should give file if? nowait = true is supplied' do
@file = StoredFile.first
get "/#{@file.sha}/#{@file.id}?nowait=true"
@response ['Content-Type']. should == "Application / octet-stream"
@response ['Content-Disposition']. should == "attachment; filename = \" # {@ file.filename} \ ""
end
end
No tricks - the same describe / it / should, as in Rails, for example. The main thing is not to forget to connect sinatra / test / rspec.
Benchmarks
In one of the comments I was asked to measure the performance of the resulting application - no problem.
First is the benchmark of the main page (file list).
ab -n 1000 -c 1 -A admin: secret http://127.0.0.1.1000000/ Concurrency Level: 1
Time taken for tests: 24.109 seconds
Total transferred: 3739000 bytes
HTML transferred: 3604000 bytes
Requests per second: 41.48 [# / sec] (mean)
Time per request: 24.109 [ms] (mean)
Time per request: 24.109 [ms] (mean, across all concurrent requests)
Transfer rate: 151.45 [Kbytes / sec] received
ab -n 1000 -c 10 -A admin: secret http://127.0.0.1.1000000/ Concurrency Level: 10
Time taken for tests: 24.381 seconds
Total transferred: 3739000 bytes
HTML transferred: 3604000 bytes
Requests per second: 41.02 [# / sec] (mean)
Time per request: 243.811 [ms] (mean)
Time per request: 24.381 [ms] (mean, across all concurrent requests)
Transfer rate: 149.76 [Kbytes / sec] received
ab -n 1000 -c 100 -A admin: secret http://127.0.0.1.1000000/ Concurrency Level: 100
Time taken for tests: 23.798 seconds
Total transferred: 3739000 bytes
HTML transferred: 3604000 bytes
Requests per second: 42.02 [# / sec] (mean)
Time per request: 2379.816 [ms] (mean)
Time per request: 23.798 [ms] (mean, across all concurrent requests)
Transfer rate: 153.43 [Kbytes / sec] received
Upload files to server (file size 1.5 Kb)
ab -n 1000 -c 1 -A admin: secret -T 'application / x-www-form-urlencoded' -p post.data http://127.0.0.1.73000/ Concurrency Level: 1
Time taken for tests: 16.305 seconds
Total transferred: 160000 bytes
Total POSTed: 242000
Requests per second: 61.33 [# / sec] (mean)
Time per request: 16.305 [ms] (mean)
Time per request: 16.305 [ms] (mean, across all concurrent requests)
Transfer rate: 9.58 [Kbytes / sec] received
14.49 kb / s sent
24.08 kb / s total
ab -n 1000 -c 10 -A admin: secret -T 'application / x-www-form-urlencoded' -p post.data http://127.0.0.1.73000/ Concurrency Level: 10
Time taken for tests: 18.463 seconds
Total transferred: 161280 bytes
Total POSTed: 243936
HTML transferred: 0 bytes
Requests per second: 54.16 [# / sec] (mean)
Time per request: 184.631 [ms] (mean)
Time per request: 18.463 [ms] (mean, across all concurrent requests)
Transfer rate: 8.53 [Kbytes / sec] received
12.90 kb / s sent
21.43 kb / s total
ab -n 1000 -c 100 -A admin: secret -T 'application / x-www-form-urlencoded' -p post.data http://127.0.0.1天000/ Concurrency Level: 100
Time taken for tests: 16.029 seconds
Total transferred: 160160 bytes
Total POSTed: 242242
HTML transferred: 0 bytes
Requests per second: 62.39 [# / sec] (mean)
Time per request: 1602.899 [ms] (mean)
Time per request: 16.029 [ms] (mean, across all concurrent requests)
Transfer rate: 9.76 [Kbytes / sec] received
14.76 kb / s sent
24.52 kb / s total
Note that the performance practically does not change with an increase in the number of simultaneous requests 100 (one hundred!) Times. Testing was done on Mac Book Core 2 Duo 2.4 Ghz, 2 GB ram with several applications running in the background.
The end
It is time to complete my randomly begun epic. I hope you were interested and I was able to encourage at least some to study non-mainstream technologies (Sinatra, DataMapper, thin, haml, sass). The application in its latest version is posted on
github . Thanks to everyone who took the time to read these non-brief articles.