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