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