add Gemfile for bundler
[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 << self
42     def settings
43       (class << self; Application; end)
44     end
45     def depot
46       @depot = Depot.new(settings.depot_path) if @depot.nil? || settings.depot_path != @depot.path
47       @depot
48     end
49   end
50
51   class Application < Sinatra::Base
52     set :app_file, __FILE__
53     set :upload_password, '0e5f7d398e6f9cd1f6bac5cc823e363aec636495'
54     set :default_expire, 60
55     set :maximum_expire, 60 * 24 * 30 # 1 month
56     set :gone_period, 10080
57     set :filename_length, 20
58     set :random_pass_length, 16
59     set :depot_path, Proc.new { File.join(root, 'files') }
60
61     def password_match?(password)
62       return TRUE if settings.upload_password.nil?
63       (not password.nil?) && Digest::SHA1.hexdigest(password) == settings.upload_password
64     end
65
66     GetText::bindtextdomain('coquelicot')
67     before do
68       GetText::set_current_locale(params[:lang] || request.env['HTTP_ACCEPT_LANGUAGE'] || 'en')
69     end
70
71     get '/style.css' do
72       content_type 'text/css', :charset => 'utf-8'
73       sass :style
74     end
75
76     get '/' do
77       haml :index
78     end
79
80     get '/random_pass' do
81       "#{Coquelicot.gen_random_pass}"
82     end
83
84     get '/ready/:link' do |link|
85       link, pass = link.split '-' if link.include? '-'
86       begin
87         file = Coquelicot.depot.get_file(link, nil)
88       rescue Errno::ENOENT => ex
89         not_found
90       end
91       @expire_at = file.expire_at
92       @base = request.url.gsub(/\/ready\/[^\/]*$/, '')
93       @name = "#{link}"
94       unless pass.nil?
95         @name << "-#{pass}"
96         @unprotected = true
97       end
98       @url = "#{@base}/#{@name}"
99       haml :ready
100     end
101
102     post '/authenticate' do
103       pass unless request.xhr?
104       unless password_match? params[:upload_password] then
105         error 403, "Forbidden"
106       end
107       'OK'
108     end
109
110     post '/upload' do
111       unless password_match? params[:upload_password] then
112         error 403
113       end
114       if params[:file] then
115         tmpfile = params[:file][:tempfile]
116         name = params[:file][:filename]
117       end
118       if tmpfile.nil? || name.nil? then
119         @error = "No file selected"
120         return haml(:index)
121       end
122       if tmpfile.lstat.size == 0 then
123         @error = "#{name} is empty"
124         return haml(:index)
125       end
126       if params[:expire].nil? or params[:expire].to_i == 0 then
127         params[:expire] = settings.default_expire
128       elsif params[:expire].to_i > settings.maximum_expire then
129         error 403
130       end
131       expire_at = Time.now + 60 * params[:expire].to_i
132       one_time_only = params[:one_time] and params[:one_time] == 'true'
133       if params[:file_key].nil? or params[:file_key].empty?then
134         pass = Coquelicot.gen_random_pass
135       else
136         pass = params[:file_key]
137       end
138       src = params[:file][:tempfile]
139       link = Coquelicot.depot.add_file(
140          src, pass,
141          { "Expire-at" => expire_at.to_i,
142            "One-time-only" => one_time_only,
143            "Filename" => params[:file][:filename],
144            "Length" => src.stat.size,
145            "Content-Type" => params[:file][:type],
146          })
147       redirect "ready/#{link}-#{pass}" if params[:file_key].nil? or params[:file_key].empty?
148       redirect "ready/#{link}"
149     end
150
151     def expired
152       throw :halt, [410, haml(:expired)]
153     end
154
155     def send_stored_file(file)
156       last_modified file.created_at.httpdate
157       attachment file.meta['Filename']
158       response['Content-Length'] = "#{file.meta['Length']}"
159       response['Content-Type'] = file.meta['Content-Type'] || 'application/octet-stream'
160       throw :halt, [200, file]
161     end
162
163     def send_link(link, pass)
164       file = Coquelicot.depot.get_file(link, pass)
165       return false if file.nil?
166       return expired if file.expired?
167
168       if file.one_time_only?
169         begin
170           # unlocking done in file.close
171           file.lockfile.lock
172         rescue Lockfile::TimeoutLockError
173           error 409, "Download currently in progress"
174         end
175       end
176       send_stored_file(file)
177     end
178
179     get '/:link-:pass' do |link, pass|
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       link = Coquelicot.remap_base32_extra_characters(link)
187       not_found unless Coquelicot.depot.file_exists? link
188       @link = link
189       haml :enter_file_key
190     end
191
192     post '/:link' do |link|
193       pass = params[:file_key]
194       return 403 if pass.nil? or pass.empty?
195       begin
196         # send Forbidden even if file is not found
197         return 403 unless send_link(link, pass)
198       rescue Coquelicot::BadKey => ex
199         403
200       end
201     end
202
203     helpers do
204       def base_href
205         url = request.scheme + "://"
206         url << request.host
207         if request.scheme == "https" && request.port != 443 ||
208             request.scheme == "http" && request.port != 80
209           url << ":#{request.port}"
210         end
211         url << request.script_name
212         "#{url}/"
213       end
214     end
215   end
216 end
217
218 Coquelicot::Application.run! if __FILE__ == $0