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