Fix name-typo in NEWS
[coquelicot.git] / lib / coquelicot / jyraphe_migrator.rb
1 # -*- coding: UTF-8 -*-
2 # Coquelicot: "one-click" file sharing with a focus on users' privacy.
3 # Copyright © 2010-2013 potager.org <jardiniers@potager.org>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU Affero General Public License for more details.
14 #
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 module Coquelicot
19   class JyrapheMigrator
20     class << self
21       def run!(args)
22         parser.parse!(args)
23         usage_and_exit if args.empty?
24
25         jyraphe_var = args.shift
26
27         migrator = nil
28         begin
29           migrator = JyrapheMigrator.new(jyraphe_var)
30         rescue ArgumentError
31           usage_and_exit "#{jyraphe_var} is not a Jyraphe 'var' directory"
32         end
33
34         migrator.migrate!
35
36         $stdout.puts migrator.apache_rewrites(options[:rewrite_prefix])
37       end
38     private
39       def usage_and_exit(message = nil)
40         unless message.nil?
41           $stderr.puts message
42           $stderr.puts
43         end
44         $stderr.puts parser.banner
45         $stderr.puts "Run #{$0} --help for more details."
46         exit 1
47       end
48
49       def options
50         @options ||= {}
51       end
52
53       def parser
54         @parser ||= OptionParser.new do |opts|
55           opts.banner = "Usage: #{opts.program_name} [options] migrate-jyraphe [command options] JYRAPHE_VAR > REWRITE_RULES"
56
57           opts.separator ""
58           opts.separator "Command options:"
59
60           opts.on "-p", "--rewrite-prefix PREFIX", "prefix URL in rewrite rules" do |prefix|
61             options[:rewrite_prefix] = prefix
62           end
63           opts.on_tail("-h", "--help", "show this message") do
64             $stderr.puts opts.to_s
65             exit
66           end
67         end
68       end
69     end
70
71     attr_reader :files_path, :links_path, :migrated
72
73     def initialize(jyraphe_var, output = $stderr)
74       @files_path = File.expand_path('files', jyraphe_var)
75       @links_path = File.expand_path('links', jyraphe_var)
76       unless File.directory?(@files_path) && File.directory?(@links_path)
77         raise ArgumentError.new("#{jyraphe_var} is not a Jyraphe 'var' directory.")
78       end
79       @output = output
80     end
81
82     def warn(str)
83       @output.puts "W: #{str}"
84     end
85
86     def info(str)
87       @output.puts "I: #{str}"
88     end
89
90     def migrate!
91       max_expire_at = (Time.now + Coquelicot.settings.maximum_expire * 60).to_i
92       migrated = {}
93       get_links.each do |link|
94         begin
95           file = JyrapheFile.new(self, link)
96         rescue Errno::ENOENT
97           warn "#{link} refers to a non-existent file. Skipping."
98           next
99         rescue SizeMismatch
100           warn "#{link} refers to a file with mismatching size. Skipping."
101           next
102         end
103
104         pass = file.file_key || Coquelicot.gen_random_pass
105
106         if file.expire_at == -1 || file.expire_at > max_expire_at
107           expire_at = max_expire_at
108           warn "#{link} expiration time has been reduced."
109           info "#{link} will expire on #{Time.at(max_expire_at).strftime '%c'}."
110         elsif file.expire_at == 0
111           warn "#{link} has an unparseable expiration time. Skipping."
112           next
113         else
114           expire_at = file.expire_at
115         end
116
117         options = { 'Expire-at'     => expire_at,
118                     'One-time-only' => file.one_time_only,
119                     'Filename'      => file.filename,
120                     'Length'        => file.length,
121                     'Content-type'  => file.mime_type }
122
123         coquelicot_name = file.open do |f|
124           Coquelicot.depot.add_file(pass, options) do
125             f.eof ? nil : f.read
126           end
127         end
128
129         coquelicot_link = coquelicot_name
130         coquelicot_link << "-#{pass}" unless file.file_key
131         migrated[link] = coquelicot_link
132       end
133       @migrated = migrated
134     end
135
136    def apache_rewrites(prefix = '')
137      return '' if @migrated.empty?
138
139      rewrites = []
140      rewrites << 'RewriteEngine on'
141      migrated.each_pair do |jyraphe, coquelicot|
142        rewrites << "RewriteRule ^#{prefix}file-#{jyraphe}$ #{prefix}#{coquelicot} [L,R=301]"
143      end
144      rewrites.join "\n"
145    end
146
147    private
148
149     def get_links
150       Dir.entries(@links_path).select { |n| n =~ /^[RO][0-9a-z]{32}$/ }
151     end
152
153     class SizeMismatch < StandardError; end
154
155     class JyrapheFile
156       attr_reader :filename, :one_time_only, :mime_type, :length, :file_key, :expire_at
157
158       def initialize(migrator, link)
159         @migrator = migrator
160
161         @one_time_only = link[0] == ?O
162         File.open(File.expand_path(link, migrator.links_path)) do |f|
163           @filename = f.readline.strip
164           @mime_type = f.readline.strip
165           @length = f.readline.strip.to_i
166           if File.stat(file_path).size != length
167             raise SizeMismatch.new("#{filename} size does not match what is in #{link}.")
168           end
169           key = f.readline.strip
170           @file_key = key.empty? ? nil : key
171           @expire_at = f.readline.strip.to_i
172         end
173       end
174
175       def file_path
176         File.expand_path(filename, @migrator.files_path)
177       end
178
179       def open(*args, &block)
180         File.open(file_path, *args, &block)
181       end
182     end
183   end
184 end