move Coquelicot::Depot to its own file
[coquelicot.git] / lib / coquelicot / depot.rb
1 module Coquelicot
2   class Depot
3     attr_reader :path
4
5     def initialize(path)
6       @path = path
7     end
8
9     def add_file(src, pass, options)
10       dst = nil
11       lockfile.lock do
12         dst = gen_random_file_name
13         File.open(full_path(dst), 'w').close
14       end
15       begin
16         File.open(full_path(dst), 'w') do |dest|
17           StoredFile.create(src, pass, options) { |data| dest.write data }
18         end
19       rescue
20         File.unlink full_path(dst)
21         raise
22       end
23       link = gen_random_file_name
24       add_link(link, dst)
25       link
26     end
27
28     def get_file(link, pass=nil)
29       name = read_link(link)
30       return nil if name.nil?
31       StoredFile::open(full_path(name), pass)
32     end
33
34     def file_exists?(link)
35       name = read_link(link)
36       return !name.nil?
37     end
38
39     def gc!
40       files.each do |name|
41         path = full_path(name)
42         if File.lstat(path).size > 0
43           file = StoredFile::open path
44           file.empty! if file.expired?
45         elsif Time.now - File.lstat(path).mtime > (Coquelicot.settings.gone_period * 60)
46           remove_from_links { |l| l.strip.end_with? " #{name}" }
47           File.unlink path
48         end
49       end
50     end
51
52   private
53
54     LOCKFILE_OPTIONS = { :timeout => 60,
55                          :max_age => 8,
56                          :refresh => 2,
57                          :debug   => false }
58
59     def lockfile
60       Lockfile.new "#{@path}/.lock", LOCKFILE_OPTIONS
61     end
62
63     def links_path
64       "#{@path}/.links"
65     end
66
67     def add_link(src, dst)
68       lockfile.lock do
69         File.open(links_path, 'a') do |f|
70           f.write("#{src} #{dst}\n")
71         end
72       end
73     end
74
75     def remove_from_links(&block)
76       lockfile.lock do
77         links = []
78         File.open(links_path, 'r+') do |f|
79           f.readlines.each do |l|
80             links << l unless yield l
81           end
82           f.rewind
83           f.truncate(0)
84           f.write links.join
85         end
86       end
87     end
88
89     def remove_link(src)
90       remove_from_links { |l| l.start_with? "#{src} " }
91     end
92
93     def read_link(src)
94       dst = nil
95       lockfile.lock do
96         File.open(links_path) do |f|
97           begin
98             line = f.readline rescue break
99             if line.start_with? "#{src} " then
100               dst = line.split[1]
101               break
102             end
103           end until line.empty?
104         end if File.exists?(links_path)
105       end
106       dst
107     end
108
109     def files
110       lockfile.lock do
111         File.open(links_path) do |f|
112           f.readlines.collect { |l| l.split[1] }
113         end
114       end
115     end
116
117     def gen_random_file_name
118       begin
119         name = Coquelicot.gen_random_base32(Coquelicot.settings.filename_length)
120       end while File.exists?(full_path(name))
121       name
122     end
123
124     def full_path(name)
125       raise "Wrong name" unless name.each_char.collect { |c| Coquelicot::FILENAME_CHARS.include? c }.all?
126       "#{@path}/#{name}"
127     end
128   end
129
130   # Like RFC 4648 (Base32)
131   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)
132
133   class << self
134     def gen_random_base32(length)
135       name = ''
136       OpenSSL::Random::random_bytes(length).each_byte do |i|
137         name << FILENAME_CHARS[i % FILENAME_CHARS.length]
138       end
139       name
140     end
141     def gen_random_pass
142       gen_random_base32(settings.random_pass_length)
143     end
144     def remap_base32_extra_characters(str)
145       map = {}
146       FILENAME_CHARS.each { |c| map[c] = c; map[c.upcase] = c }
147       map.merge!({ '1' => 'l', '0' => 'o' })
148       result = ''
149       str.each_char { |c| result << map[c] if map[c] }
150       result
151     end
152   end
153 end