cosmetic rewrite of Depot#read_link
[coquelicot.git] / lib / coquelicot / depot.rb
1 # Coquelicot: "one-click" file sharing with a focus on users' privacy.
2 # Copyright © 2010-2012 potager.org <jardiniers@potager.org>
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as
6 # published by the Free Software Foundation, either version 3 of the
7 # License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 require 'lockfile'
18 require 'openssl'
19
20 module Coquelicot
21   class Depot
22     attr_reader :path
23
24     def initialize(path)
25       @path = path
26     end
27
28     def add_file(pass, options, &block)
29       dst = nil
30
31       # Ensure that the generated name is not already used
32       loop do
33         dst = gen_random_file_name
34         begin
35           StoredFile.create(full_path(dst), pass, options, &block)
36           break
37         rescue Errno::EEXIST => e
38           raise unless e.message =~ /(?:^|\s)#{Regexp.escape(full_path(dst))}(?:\s|$)/
39           next # let's try again
40         end
41       end
42
43       # retry to add the link until a free name is generated
44       loop do
45         link = gen_random_file_name
46         return link if add_link(link, dst)
47       end
48     end
49
50     def get_file(link, pass=nil)
51       name = nil
52       lockfile.lock do
53         name = read_link(link)
54       end
55       return nil if name.nil?
56       begin
57         StoredFile::open(full_path(name), pass)
58       rescue Errno::ENOENT
59         nil
60       end
61     end
62
63     def file_exists?(link)
64       lockfile.lock do
65         name = read_link(link)
66         return name && File.exists?(full_path(name))
67       end
68     end
69
70     def gc!
71       files.each do |name|
72         path = full_path(name)
73         if File.lstat(path).size > 0
74           file = StoredFile::open path
75           file.empty! if file.expired?
76         end
77         if Time.now - File.lstat(path).mtime > (Coquelicot.settings.gone_period * 60)
78           remove_from_links { |l| l.strip.end_with? " #{name}" }
79           File.unlink path
80         end
81       end
82     end
83
84     def size
85       files.count
86     end
87
88   private
89
90     LOCKFILE_OPTIONS = { :timeout => 60,
91                          :max_age => 8,
92                          :refresh => 2,
93                          :debug   => false }
94
95     def lockfile
96       Lockfile.new "#{@path}/.lock", LOCKFILE_OPTIONS
97     end
98
99     def links_path
100       "#{@path}/.links"
101     end
102
103     def add_link(src, dst)
104       lockfile.lock do
105         return false unless read_link(src).nil?
106
107         File.open(links_path, 'a') do |f|
108           f.write("#{src} #{dst}\n")
109         end
110       end
111       true
112     end
113
114     def remove_from_links(&block)
115       lockfile.lock do
116         links = []
117         File.open(links_path, 'r+') do |f|
118           f.readlines.each do |l|
119             links << l unless yield l
120           end
121           f.rewind
122           f.truncate(0)
123           f.write links.join
124         end
125       end
126     end
127
128     def remove_link(src)
129       remove_from_links { |l| l.start_with? "#{src} " }
130     end
131
132     def read_link(src)
133       File.open(links_path) do |f|
134         until f.eof?
135           return $1 if f.readline =~ /^#{Regexp.escape(src)}\s+(.+)$/
136         end
137       end
138       nil
139     rescue Errno::ENOENT
140       nil
141     end
142
143     def files
144       lockfile.lock do
145         begin
146           File.open(links_path) do |f|
147             f.readlines.collect { |l| l.split[1] }
148           end
149         rescue Errno::ENOENT # if links file has not been created yet
150           []
151         end
152       end
153     end
154
155     def gen_random_file_name
156       begin
157         name = Coquelicot.gen_random_base32(Coquelicot.settings.filename_length)
158       end while File.exists?(full_path(name))
159       name
160     end
161
162     def full_path(name)
163       raise "Wrong name" unless name.each_char.collect { |c| Coquelicot::FILENAME_CHARS.include? c }.all?
164       "#{@path}/#{name}"
165     end
166   end
167
168   # Like RFC 4648 (Base32)
169   FILENAME_CHARS = %w(a b c d e f g h i j k l m n o p q r s t u v w x y z 2 3 4 5 6 7)
170
171   class << self
172     def gen_random_base32(length)
173       name = ''
174       OpenSSL::Random::random_bytes(length).each_byte do |i|
175         name << FILENAME_CHARS[i % FILENAME_CHARS.length]
176       end
177       name
178     end
179     def gen_random_pass
180       gen_random_base32(settings.random_pass_length)
181     end
182     def remap_base32_extra_characters(str)
183       map = {}
184       FILENAME_CHARS.each { |c| map[c] = c; map[c.upcase] = c }
185       map.merge!({ '1' => 'l', '0' => 'o' })
186       result = ''
187       str.each_char { |c| result << map[c] if map[c] }
188       result
189     end
190   end
191 end