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