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