scrap home made config system to use Sinatra::ConfigFile
[coquelicot.git] / lib / coquelicot / app.rb
1 require 'lockfile'
2 require 'sinatra/base'
3 require 'sinatra/config_file'
4 require 'haml'
5 require 'sass'
6 require 'digest/sha1'
7 require 'gettext'
8 require 'haml_gettext'
9
10 module Coquelicot
11   class << self
12     def settings
13       (class << self; Application; end)
14     end
15     def depot
16       @depot = Depot.new(settings.depot_path) if @depot.nil? || settings.depot_path != @depot.path
17       @depot
18     end
19   end
20
21   class Application < Sinatra::Base
22     register Sinatra::ConfigFile
23     register Coquelicot::Auth::Extension
24
25     set :root, Proc.new { app_file && File.expand_path('../../..', app_file) }
26     set :depot_path, Proc.new { File.join(root, 'files') }
27     set :default_expire, 60
28     set :maximum_expire, 60 * 24 * 30 # 1 month
29     set :gone_period, 60 * 24 * 7 # 1 week
30     set :filename_length, 20
31     set :random_pass_length, 16
32     set :about_text, ''
33     set :additional_css, ''
34     set :authentication_method, :name => :simplepass,
35                                 :upload_password => 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
36
37     config_file File.expand_path('../../../conf/settings.yml', __FILE__)
38
39     GetText::bindtextdomain('coquelicot')
40     before do
41       GetText::set_current_locale(params[:lang] || request.env['HTTP_ACCEPT_LANGUAGE'] || 'en')
42     end
43
44     not_found do
45       'Not found'
46     end
47
48     get '/style.css' do
49       content_type 'text/css', :charset => 'utf-8'
50       sass :style
51     end
52
53     get '/' do
54       haml :index
55     end
56
57     get '/random_pass' do
58       "#{Coquelicot.gen_random_pass}"
59     end
60
61     get '/ready/:link' do |link|
62       not_found if link.nil?
63
64       link, pass = link.split '-' if link.include? '-'
65       begin
66         file = Coquelicot.depot.get_file(link, nil)
67       rescue Errno::ENOENT => ex
68         not_found
69       end
70       @expire_at = file.expire_at
71       @base = request.url.gsub(/\/ready\/[^\/]*$/, '')
72       @name = "#{link}"
73       unless pass.nil?
74         @name << "-#{pass}"
75         @unprotected = true
76       end
77       @url = "#{@base}/#{@name}"
78       haml :ready
79     end
80
81     post '/authenticate' do
82       pass unless request.xhr?
83       unless authenticate(params) then
84         error 403, "Forbidden"
85       end
86       'OK'
87     end
88
89     post '/upload' do
90       # if JS is disabled upload_token might be nil
91       params['upload_token'] = JSON.parse(params['upload_token']) unless params['upload_token'].nil?
92       unless authenticate(params) then
93         error 403, "Forbidden"
94       end
95       if params[:file] then
96         tmpfile = params[:file][:tempfile]
97         name = params[:file][:filename]
98       end
99       if tmpfile.nil? || name.nil? then
100         @error = "No file selected"
101         return haml(:index)
102       end
103       if tmpfile.lstat.size == 0 then
104         @error = "#{name} is empty"
105         return haml(:index)
106       end
107       if params[:expire].nil? or params[:expire].to_i == 0 then
108         params[:expire] = settings.default_expire
109       elsif params[:expire].to_i > settings.maximum_expire then
110         error 403
111       end
112       expire_at = Time.now + 60 * params[:expire].to_i
113       one_time_only = params[:one_time] and params[:one_time] == 'true'
114       if params[:file_key].nil? or params[:file_key].empty?then
115         pass = Coquelicot.gen_random_pass
116       else
117         pass = params[:file_key]
118       end
119       src = params[:file][:tempfile]
120       link = Coquelicot.depot.add_file(
121          src, pass,
122          { "Expire-at" => expire_at.to_i,
123            "One-time-only" => one_time_only,
124            "Filename" => params[:file][:filename],
125            "Length" => src.stat.size,
126            "Content-Type" => params[:file][:type],
127          })
128       redirect "ready/#{link}-#{pass}" if params[:file_key].nil? or params[:file_key].empty?
129       redirect "ready/#{link}"
130     end
131
132     def expired
133       throw :halt, [410, haml(:expired)]
134     end
135
136     def send_stored_file(file)
137       last_modified file.created_at.httpdate
138       attachment file.meta['Filename']
139       response['Content-Length'] = "#{file.meta['Length']}"
140       response['Content-Type'] = file.meta['Content-Type'] || 'application/octet-stream'
141       throw :halt, [200, file]
142     end
143
144     def send_link(link, pass)
145       file = Coquelicot.depot.get_file(link, pass)
146       return false if file.nil?
147       return expired if file.expired?
148
149       if file.one_time_only?
150         begin
151           # unlocking done in file.close
152           file.lockfile.lock
153         rescue Lockfile::TimeoutLockError
154           error 409, "Download currently in progress"
155         end
156       end
157       send_stored_file(file)
158     end
159
160     get '/:link-:pass' do |link, pass|
161       not_found if link.nil? || pass.nil?
162
163       link = Coquelicot.remap_base32_extra_characters(link)
164       pass = Coquelicot.remap_base32_extra_characters(pass)
165       not_found unless send_link(link, pass)
166     end
167
168     get '/:link' do |link|
169       not_found if link.nil?
170
171       link = Coquelicot.remap_base32_extra_characters(link)
172       not_found unless Coquelicot.depot.file_exists? link
173       @link = link
174       haml :enter_file_key
175     end
176
177     post '/:link' do |link|
178       pass = params[:file_key]
179       return 403 if pass.nil? or pass.empty?
180       begin
181         # send Forbidden even if file is not found
182         return 403 unless send_link(link, pass)
183       rescue Coquelicot::BadKey => ex
184         403
185       end
186     end
187
188     helpers do
189       def base_href
190         url = request.scheme + "://"
191         url << request.host
192         if request.scheme == "https" && request.port != 443 ||
193             request.scheme == "http" && request.port != 80
194           url << ":#{request.port}"
195         end
196         url << request.script_name
197         "#{url}/"
198       end
199
200       def clone_url
201         settings.respond_to?(:clone_url) ? settings.clone_url : "#{base_href}coquelicot.git"
202       end
203
204       def authenticate(params)
205         Coquelicot.settings.authenticator.authenticate(params)
206       end
207
208       def auth_method
209         Coquelicot.settings.authenticator.class.name.gsub(/Coquelicot::Auth::([A-z0-9]+)Authenticator$/, '\1').downcase
210       end
211     end
212   end
213 end
214
215 Coquelicot::Application.run! if __FILE__ == $0