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