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