rewrite Jyraphe migration system
authorLunar <lunar@anargeek.net>
Sun, 4 Mar 2012 10:01:08 +0000 (11:01 +0100)
committerLunar <lunar@anargeek.net>
Thu, 14 Mar 2013 09:12:09 +0000 (10:12 +0100)
Instead of the old quickly hacked script, we now have a properly integated,
tested migration system to migrate from Jyraphe installations.

README
bin/coquelicot-migrate-jyraphe [new file with mode: 0755]
lib/coquelicot/app.rb
lib/coquelicot/jyraphe_migrator.rb [new file with mode: 0644]
spec/coquelicot/app_spec.rb
spec/coquelicot/jyraphe_migrator_spec.rb [new file with mode: 0644]
tools/migrate_jyraphe.rb [deleted file]

diff --git a/README b/README
index 2f0c13c..f39495d 100644 (file)
--- a/README
+++ b/README
@@ -164,7 +164,20 @@ every 5 minutes (or so).
 
 [Jyraphe] is another free software web file sharing application.
 Coquelicot provides a migration script to import Jyraphe 0.5
-repositories in `tools/migrate_jyraphe.rb`.
+repositories as `bin/coquelicot-migrate-jyraphe`:
+
+    Usage: coquelicot-migrate-jyraphe [options] jyraphe-var > rewrite-rules
+
+    Options:
+        -c, --config FILE            read settings from FILE
+        -p, --rewrite-prefix PREFIX  prefix URL in rewrite rules
+        -h, --help                   show this message
+
+The last argument must be a path to the `var` directory of the Jyraphe
+installation. After migrating the files to Coquelicot, directives for
+Apache mod_rewrite will be printed on stdout which ought to be
+redirected to a file. The `-p` option can be used to add a specific
+paths in the redirected URLs.
 
 [Jyraphe]: http://home.gna.org/jyraphe/
 
diff --git a/bin/coquelicot-migrate-jyraphe b/bin/coquelicot-migrate-jyraphe
new file mode 100755 (executable)
index 0000000..e0c1b71
--- /dev/null
@@ -0,0 +1,24 @@
+#!/usr/bin/env ruby1.8
+# Coquelicot: "one-click" file sharing with a focus on users' privacy.
+# Copyright © 2010-2012 potager.org <jardiniers@potager.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+require 'rubygems'
+require 'bundler'
+Bundler.setup
+
+require 'coquelicot'
+
+Coquelicot.migrate_jyraphe!(ARGV)
index e0b55c7..1353276 100644 (file)
@@ -56,6 +56,11 @@ module Coquelicot
       parser.parse!(args)
       depot.gc!
     end
+    # Called by +coquelicot-migrate-jyraphe+ script.
+    def migrate_jyraphe!(args = [])
+      require 'coquelicot/jyraphe_migrator'
+      Coquelicot::JyrapheMigrator.run! args
+    end
   end
 
   class Application < Sinatra::Base
diff --git a/lib/coquelicot/jyraphe_migrator.rb b/lib/coquelicot/jyraphe_migrator.rb
new file mode 100644 (file)
index 0000000..2f5849f
--- /dev/null
@@ -0,0 +1,186 @@
+# Coquelicot: "one-click" file sharing with a focus on users' privacy.
+# Copyright © 2010-2012 potager.org <jardiniers@potager.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+module Coquelicot
+  class JyrapheMigrator
+    class << self
+      def run!(args)
+        parser.parse!(args)
+        usage_and_exit if args.empty?
+
+        jyraphe_var = args.shift
+
+        migrator = nil
+        begin
+          migrator = JyrapheMigrator.new(jyraphe_var)
+        rescue ArgumentError
+          usage_and_exit "#{jyraphe_var} is not a Jyraphe 'var' directory"
+        end
+
+        migrator.migrate!
+
+        $stdout.puts migrator.apache_rewrites(options[:rewrite_prefix])
+      end
+    private
+      def usage_and_exit(message = nil)
+        unless message.nil?
+          $stderr.puts message
+          $stderr.puts
+        end
+        $stderr.puts parser.banner
+        $stderr.puts "Run #{$0} --help for more details."
+        exit 1
+      end
+
+      def options
+        @options ||= {}
+      end
+
+      def parser
+        @parser ||= OptionParser.new do |opts|
+          opts.banner = "Usage: #{$0} [options] jyraphe-var > rewrite-rules"
+
+          opts.separator ""
+          opts.separator "Options:"
+
+          opts.on "-c", "--config FILE", "read settings from FILE" do |file|
+            Coquelicot.settings.config_file file
+          end
+          opts.on "-p", "--rewrite-prefix PREFIX", "prefix URL in rewrite rules" do |prefix|
+            options[:rewrite_prefix] = prefix
+          end
+          opts.on_tail("-h", "--help", "show this message") do
+            $stderr.puts opts.to_s
+            exit
+          end
+        end
+      end
+    end
+
+    attr_reader :files_path, :links_path, :migrated
+
+    def initialize(jyraphe_var, output = $stderr)
+      @files_path = File.expand_path('files', jyraphe_var)
+      @links_path = File.expand_path('links', jyraphe_var)
+      unless File.directory?(@files_path) && File.directory?(@links_path)
+        raise ArgumentError.new("#{jyraphe_var} is not a Jyraphe 'var' directory.")
+      end
+      @output = output
+    end
+
+    def warn(str)
+      @output.puts "W: #{str}"
+    end
+
+    def info(str)
+      @output.puts "I: #{str}"
+    end
+
+    def migrate!
+      max_expire_at = (Time.now + Coquelicot.settings.maximum_expire * 60).to_i
+      migrated = {}
+      get_links.each do |link|
+        begin
+          file = JyrapheFile.new(self, link)
+        rescue Errno::ENOENT
+          warn "#{link} refers to a non-existent file. Skipping."
+          next
+        rescue SizeMismatch
+          warn "#{link} refers to a file with mismatching size. Skipping."
+          next
+        end
+
+        pass = file.file_key || Coquelicot.gen_random_pass
+
+        if file.expire_at == -1 || file.expire_at > max_expire_at
+          expire_at = max_expire_at
+          warn "#{link} expiration time has been reduced."
+          info "#{link} will expire on #{Time.at(max_expire_at).strftime '%c'}."
+        elsif file.expire_at == 0
+          warn "#{link} has an unparseable expiration time. Skipping."
+          next
+        else
+          expire_at = file.expire_at
+        end
+
+        options = { 'Expire-at'     => expire_at,
+                    'One-time-only' => file.one_time_only,
+                    'Filename'      => file.filename,
+                    'Length'        => file.length,
+                    'Content-type'  => file.mime_type }
+
+        coquelicot_name = file.open do |f|
+          Coquelicot.depot.add_file(pass, options) do
+            f.eof ? nil : f.read
+          end
+        end
+
+        coquelicot_link = coquelicot_name
+        coquelicot_link << "-#{pass}" unless file.file_key
+        migrated[link] = coquelicot_link
+      end
+      @migrated = migrated
+    end
+
+   def apache_rewrites(prefix = '')
+     return '' if @migrated.empty?
+
+     rewrites = []
+     rewrites << 'RewriteEngine on'
+     migrated.each_pair do |jyraphe, coquelicot|
+       rewrites << "RewriteRule ^#{prefix}file-#{jyraphe}$ #{prefix}#{coquelicot} [L,R=301]"
+     end
+     rewrites.join "\n"
+   end
+
+   private
+
+    def get_links
+      Dir.entries(@links_path).select { |n| n =~ /^[RO][0-9a-z]{32}$/ }
+    end
+
+    class SizeMismatch < StandardError; end
+
+    class JyrapheFile
+      attr_reader :filename, :one_time_only, :mime_type, :length, :file_key, :expire_at
+
+      def initialize(migrator, link)
+        @migrator = migrator
+
+        @one_time_only = link[0] == ?O
+        File.open(File.expand_path(link, migrator.links_path)) do |f|
+          @filename = f.readline.strip
+          @mime_type = f.readline.strip
+          @length = f.readline.strip.to_i
+          if File.stat(file_path).size != length
+            raise SizeMismatch.new("#{filename} size does not match what is in #{link}.")
+          end
+          key = f.readline.strip
+          @file_key = key.empty? ? nil : key
+          @expire_at = f.readline.strip.to_i
+        end
+      end
+
+      def file_path
+        File.expand_path(filename, @migrator.files_path)
+      end
+
+      def open(*args, &block)
+        File.open(file_path, *args, &block)
+      end
+    end
+  end
+end
index 9429897..bf63f8c 100644 (file)
@@ -15,6 +15,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 require 'spec_helper'
+require 'coquelicot/jyraphe_migrator'
 require 'capybara/dsl'
 require 'tempfile'
 
@@ -118,3 +119,11 @@ describe Coquelicot, '.collect_garbage!' do
     end
   end
 end
+
+describe Coquelicot, '.migrate_jyraphe!' do
+  it 'should call the migrator' do
+    args = ['whatever']
+    Coquelicot::JyrapheMigrator.should_receive(:run!).with(args)
+    Coquelicot.migrate_jyraphe! args
+  end
+end
diff --git a/spec/coquelicot/jyraphe_migrator_spec.rb b/spec/coquelicot/jyraphe_migrator_spec.rb
new file mode 100644 (file)
index 0000000..3b495be
--- /dev/null
@@ -0,0 +1,424 @@
+# Coquelicot: "one-click" file sharing with a focus on users' privacy.
+# Copyright © 2010-2012 potager.org <jardiniers@potager.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+require 'spec_helper'
+require 'coquelicot/jyraphe_migrator'
+require 'tmpdir'
+require 'digest/md5'
+require 'timecop'
+
+module Coquelicot
+  describe JyrapheMigrator do
+    include_context 'with Coquelicot::Application'
+
+    around do |example|
+      @jyraphe_var_path = Dir.mktmpdir('coquelicot')
+      begin
+        Dir.mkdir(File.expand_path('files', @jyraphe_var_path))
+        Dir.mkdir(File.expand_path('links', @jyraphe_var_path))
+        example.run
+      ensure
+        FileUtils.remove_entry_secure @jyraphe_var_path
+      end
+    end
+
+    def add_file_to_jyraphe(file, options = {})
+      options = { :mime_type => 'text/plain',
+                  :expire_at => (Time.now + 3600).to_i }.merge(options)
+      md5 = Digest::MD5.hexdigest(File.read(file))
+      FileUtils.cp file, File.expand_path('files', @jyraphe_var_path)
+      prefix = options[:one_time_only] ? 'O' : 'R'
+      File.open(File.expand_path("links/#{prefix}#{md5}", @jyraphe_var_path), 'w') do |f|
+        f.write("#{File.basename(file)}\n")
+        f.write("#{options[:mime_type]}\n")
+        f.write("#{File.stat(file).size}\n")
+        f.write("#{options[:file_key]}\n")
+        f.write("#{options[:expire_at]}\n")
+      end
+    end
+
+    def get_first_migrated_file(pass = nil)
+      old, new = migrator.migrated.to_a[0]
+      if pass.nil?
+        file, pass = new.split('-')
+      else
+        file = new
+      end
+      Coquelicot.depot.get_file(file, pass)
+    end
+
+    describe '#new' do
+      context 'when the given directory is not a Jyraphe "var" directory' do
+        it 'should raise an error' do
+          expect {
+            JyrapheMigrator.new(Coquelicot.settings.depot_path)
+          }.to raise_error(ArgumentError)
+        end
+      end
+    end
+
+    describe '#migrate!' do
+      let(:output) { double.as_null_object }
+      let(:migrator) { JyrapheMigrator.new(@jyraphe_var_path, output) }
+      context 'when there is a file in Jyraphe' do
+        before(:each) do
+          add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby')
+        end
+        it 'should add a new file to Coquelicot' do
+          expect {
+            migrator.migrate!
+          }.to change { Coquelicot.depot.size }.by(1)
+        end
+        context 'when I read the file in Coquelicot' do
+          before(:each) { migrator.migrate! }
+          subject { get_first_migrated_file }
+          it 'should have the same length' do
+            subject.meta['Length'].should == File.stat(__FILE__).size
+          end
+          it 'should have the same mime type' do
+            subject.meta['Content-type'].should == 'application/x-ruby'
+          end
+        end
+      end
+
+      context 'when there is two files in Jyraphe' do
+        before(:each) do
+          add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby')
+          add_file_to_jyraphe(File.expand_path('../../../README', __FILE__),
+                              :mime_type => 'text/plain')
+        end
+        it 'should add two files to Coquelicot' do
+          expect {
+            migrator.migrate!
+          }.to change { Coquelicot.depot.size }.by(2)
+        end
+      end
+
+      context 'when there is a "one-time only" file in Jyraphe' do
+        before(:each) do
+          add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby',
+                                        :one_time_only => true)
+        end
+        it 'should add a file to Coquelicot' do
+          expect {
+            migrator.migrate!
+          }.to change { Coquelicot.depot.size }.by(1)
+        end
+        context 'when I read the file in Coquelicot' do
+          before(:each) { migrator.migrate! }
+          subject { get_first_migrated_file }
+          it 'should be labeled as "one-time only"' do
+            subject.meta['One-time-only'].should be_true
+          end
+        end
+      end
+
+      context 'when there is a password protected file in Jyraphe' do
+        let(:pass) { 'secret' }
+        before(:each) do
+          add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby',
+                                        :file_key => pass)
+        end
+        it 'should add a file to Coquelicot' do
+          expect {
+            migrator.migrate!
+          }.to change { Coquelicot.depot.size }.by(1)
+        end
+        context 'when I read the file in Coquelicot' do
+          before(:each) { migrator.migrate! }
+          it 'should need a pass' do
+            stored_file = get_first_migrated_file
+            stored_file.meta.should_not include('Content-type')
+          end
+          it 'should be readable with a wrong pass' do
+            expect {
+              get_first_migrated_file('wrong')
+            }.to raise_error(BadKey)
+          end
+          it 'should be readable with the same pass' do
+            stored_file = get_first_migrated_file(pass)
+            stored_file.meta.should include('Content-type')
+          end
+        end
+      end
+
+      context 'when there is a never expiring file in Jyraphe' do
+        before(:each) do
+          add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby',
+                                        :expire_at => -1)
+        end
+        it 'should issue a warning' do
+          output.should_receive(:puts).
+              with(/^W: R[0-9a-z]{32} expiration time has been reduced/)
+          migrator.migrate!
+        end
+        it 'should add a file to Coquelicot' do
+          expect {
+            migrator.migrate!
+          }.to change { Coquelicot.depot.size }.by(1)
+        end
+        context 'when I read the file in Coquelicot' do
+          it 'should have the maximum expiration time' do
+            Timecop.freeze(Time.now) do
+              migrator.migrate!
+              stored_file = get_first_migrated_file
+              stored_file.meta['Expire-at'].should ==
+                  (Time.now + Coquelicot.settings.maximum_expire * 60).to_i
+            end
+          end
+        end
+      end
+
+      context 'when there is a file in Jyraphe which expires after the maximum allowed time' do
+        before(:each) do
+          add_file_to_jyraphe(
+             __FILE__,
+             :mime_type => 'application/x-ruby',
+             :expire_at => (Time.now + Coquelicot.settings.maximum_expire * 60 + 5).to_i)
+        end
+        it 'should issue a warning' do
+          output.should_receive(:puts).
+              with(/^W: R[0-9a-z]{32} expiration time has been reduced/)
+          migrator.migrate!
+        end
+        it 'should add a file to Coquelicot' do
+          expect {
+            migrator.migrate!
+          }.to change { Coquelicot.depot.size }.by(1)
+        end
+        context 'when I read the file in Coquelicot' do
+          it 'should have the maximum expiration time' do
+            Timecop.freeze(Time.now) do
+              migrator.migrate!
+              stored_file = get_first_migrated_file
+              stored_file.meta['Expire-at'].should ==
+                  (Time.now + Coquelicot.settings.maximum_expire * 60).to_i
+            end
+          end
+        end
+      end
+
+      context 'when there is a file in Jyraphe which has a bad expiration time' do
+        before(:each) do
+          add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby',
+                                        :expire_at => 'unparseable')
+        end
+        it 'should issue a warning' do
+          output.should_receive(:puts).
+              with(/^W: R[0-9a-z]{32} has an unparseable expiration time\. Skipping\./)
+          migrator.migrate!
+        end
+        it 'should not add a file to Coquelicot' do
+          expect {
+            migrator.migrate!
+          }.to_not change { Coquelicot.depot.size }
+        end
+      end
+
+      context 'when the file associated with a link is missing' do
+        before(:each) do
+          add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby')
+          FileUtils.rm(File.expand_path(File.basename(__FILE__), "#{@jyraphe_var_path}/files"))
+        end
+        it 'should issue a warning' do
+          output.should_receive(:puts).
+              with(/^W: R[0-9a-z]{32} refers to a non-existent file\. Skipping\./)
+          migrator.migrate!
+        end
+        it 'should not add a file to Coquelicot' do
+          expect {
+            migrator.migrate!
+          }.to_not change { Coquelicot.depot.size }
+        end
+      end
+
+      context 'when a file size does not match the link size' do
+        before(:each) do
+          add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby')
+          File.truncate(File.expand_path(File.basename(__FILE__), "#{@jyraphe_var_path}/files"), 0)
+        end
+        it 'should issue a warning' do
+          output.should_receive(:puts).
+              with(/^W: R[0-9a-z]{32} refers to a file with mismatching size\. Skipping\./)
+          migrator.migrate!
+        end
+        it 'should not add a file to Coquelicot' do
+          expect {
+            migrator.migrate!
+          }.to_not change { Coquelicot.depot.size }
+        end
+      end
+    end
+
+    describe '#apache_rewrites' do
+      let(:output) { double.as_null_object }
+      let(:migrator) { JyrapheMigrator.new(@jyraphe_var_path, output) }
+      context 'when there was nothing to migrate' do
+        before(:each) { migrator.migrate! }
+        subject { migrator.apache_rewrites }
+        it { should == '' }
+      end
+      context 'when there was a file migrated' do
+        before(:each) do
+          add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby')
+          migrator.migrate!
+        end
+        it 'should begin with "RewriteEngine on"' do
+          migrator.apache_rewrites.should satisfy do |s|
+            s.start_with?('RewriteEngine on')
+          end
+        end
+        context 'when given no prefix' do
+          it 'should contain a rule appropriate for an .htaccess' do
+            jyraphe, coquelicot = migrator.migrated.to_a[0]
+            migrator.apache_rewrites.split("\n").should include(
+                "RewriteRule ^file-#{jyraphe}$ #{coquelicot} [L,R=301]")
+          end
+        end
+        context 'when given a prefix' do
+          it 'should contain rules with the prefix' do
+            jyraphe, coquelicot = migrator.migrated.to_a[0]
+            migrator.apache_rewrites('/dl/').split("\n").should include(
+                "RewriteRule ^/dl/file-#{jyraphe}$ /dl/#{coquelicot} [L,R=301]")
+          end
+        end
+      end
+      context 'when there was two files migrated' do
+        before(:each) do
+          add_file_to_jyraphe(File.expand_path('../../../README', __FILE__),
+                              :mime_type => 'text/plain',
+                              :one_time_only => true)
+          add_file_to_jyraphe(__FILE__, :mime_type => 'application/x-ruby')
+          migrator.migrate!
+        end
+        context 'when given no prefix' do
+          it 'should contain two rule appropriate for an .htaccess' do
+            jyraphe, coquelicot = migrator.migrated.to_a[0]
+            migrator.apache_rewrites.split("\n").should include(
+                "RewriteRule ^file-#{jyraphe}$ #{coquelicot} [L,R=301]")
+            jyraphe, coquelicot = migrator.migrated.to_a[1]
+            migrator.apache_rewrites.split("\n").should include(
+                "RewriteRule ^file-#{jyraphe}$ #{coquelicot} [L,R=301]")
+          end
+        end
+      end
+    end
+
+    describe '.run!' do
+      context 'when given no option' do
+        before(:each) do
+          JyrapheMigrator.stub(:new).and_return(double.as_null_object)
+        end
+        it 'should display usage and exit with an error' do
+          stderr = capture(:stderr) do
+            expect {
+              JyrapheMigrator.run! []
+            }.to raise_error(SystemExit)
+          end
+          stderr.should =~ /Usage:/
+          stderr.should =~ /--help for more details/
+        end
+      end
+      context 'when given a path to a random directory' do
+        it 'should display an error' do
+          path = File.expand_path('files', @jyraphe_var_path)
+          stderr = capture(:stderr) do
+            expect {
+              JyrapheMigrator.run! [path]
+            }.to raise_error(SystemExit)
+          end
+          stderr.should =~ /is not a Jyraphe/
+        end
+      end
+      context 'when given a path to a Jyraphe var directory' do
+        it 'should use the default depot path' do
+          JyrapheMigrator.stub(:new).and_return(double.as_null_object)
+          capture(:stdout) do
+            JyrapheMigrator.run! [@jyraphe_var_path]
+          end
+          Coquelicot.settings.depot_path.should == @depot_path
+        end
+        it 'should migrate using the given Jyraphe var directory' do
+          migrator = double('JyrapheMigrator').as_null_object
+          migrator.should_receive(:migrate!)
+          JyrapheMigrator.should_receive(:new).with(@jyraphe_var_path).
+              and_return(migrator)
+          capture(:stdout) do
+            JyrapheMigrator.run! [@jyraphe_var_path]
+          end
+        end
+        it 'should print rewrite rules after migrating' do
+          migrator = double('JyrapheMigrator').as_null_object
+          migrator.should_receive(:migrate!).ordered
+          migrator.should_receive(:apache_rewrites).ordered.and_return('rules')
+          JyrapheMigrator.stub(:new).and_return(migrator)
+          stdout = capture(:stdout) do
+            JyrapheMigrator.run! [@jyraphe_var_path]
+          end
+          stdout.strip.should == 'rules'
+        end
+      end
+      context 'when using "-c <settings.yml>"' do
+        around(:each) do |example|
+          settings = Tempfile.new('coquelicot')
+          begin
+            settings.write(YAML.dump({ 'depot_path' => '/nonexistent' }))
+            settings.close
+            @settings_path = settings.path
+            example.run
+          ensure
+            settings.unlink
+          end
+        end
+        it 'should use the depot path defined in the given settings' do
+          JyrapheMigrator.stub(:new).and_return(double.as_null_object)
+          capture(:stdout) do
+            JyrapheMigrator.run! ['-c', @settings_path, @jyraphe_var_path]
+          end
+          Coquelicot.settings.depot_path.should == '/nonexistent'
+        end
+        it 'should migrate' do
+          migrator = double('JyrapheMigrator').as_null_object
+          migrator.should_receive(:migrate!)
+          JyrapheMigrator.stub(:new).and_return(migrator)
+          capture(:stdout) do
+            JyrapheMigrator.run! ['-c', @settings_path, @jyraphe_var_path]
+          end
+        end
+      end
+      context 'when using "-p"' do
+        it 'should print rewrite rules using the given prefix' do
+          migrator = double('JyrapheMigrator').as_null_object
+          migrator.should_receive(:apache_rewrites).with('/prefix/')
+          JyrapheMigrator.stub(:new).and_return(migrator)
+          capture(:stdout) do
+            JyrapheMigrator.run! ['-p', '/prefix/', @jyraphe_var_path]
+          end
+        end
+      end
+      context 'when using "-h"' do
+        it 'should display help and exit' do
+          stderr = capture(:stderr) do
+            expect {
+              JyrapheMigrator.run! ['-h']
+            }.to raise_error(SystemExit)
+          end
+          stderr.should =~ /Usage:/
+        end
+      end
+    end
+  end
+end
diff --git a/tools/migrate_jyraphe.rb b/tools/migrate_jyraphe.rb
deleted file mode 100755 (executable)
index b7aafb1..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-#!/usr/bin/env ruby1.8
-# Coquelicot: "one-click" file sharing with a focus on users' privacy.
-# Copyright © 2010 potager.org <jardiniers@potager.org>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-$:.unshift File.join(File.dirname(__FILE__), '../lib')
-
-require 'coquelicot'
-
-class JyrapheMigrator
-  def initialize(jyraphe_var)
-    @var = jyraphe_var
-    @redirects = {}
-  end
-
-  def process
-    process_links
-    puts "RewriteEngine on"
-    @redirects.each_pair do |jyraphe, coquelicot|
-      puts "RewriteRule ^file-#{jyraphe}$ #{coquelicot} [L,R=301]"
-    end
-  end
-
-  def process_links
-    Dir.glob("#{@var}/links/*").each do |link_path|
-      link_name = File.basename(link_path)
-      one_time_only = link_name.slice(0, 1) == 'O'
-      File.open(link_path) do |link_file|
-        filename = link_file.readline.strip
-        mime_type = link_file.readline.strip
-        length = link_file.readline.strip.to_i
-        next if length > 10 * 1024 * 1024
-        file_key = link_file.readline.strip
-        if file_key.empty? then
-          random_pass = Coquelicot::gen_random_pass
-        end
-        expire_at = link_file.readline.strip.to_i
-        expire_at = [Time.now + Coquelicot.settings.maximum_expire,
-                     expire_at].min if expire_at <= 0
-        begin
-          coquelicot_link = File.open("#{@var}/files/#{filename}") do |src|
-            Coquelicot::depot.add_file(
-              file_key || random_pass,
-              { "Expire-at" => expire_at,
-                "One-time-only" => one_time_only,
-                "Filename" => filename,
-                "Content-Type" => mime_type
-              }) { src.eof? ? nil : src.read }
-          end
-          @redirects[link_name] = "#{coquelicot_link}"
-          @redirects[link_name] << "-#{random_pass}" if file_key.empty?
-        rescue Errno::ENOENT => ex
-          STDERR.puts "#{ex}"
-        end
-      end
-    end
-  end
-end
-
-def usage
-  STDERR.puts "Usage: #{$0} </path/to/jyraphe/var> </path/to/coquelicot/depot>"
-  exit 1
-end
-
-def main
-  usage unless ARGV.length == 2
-  jyraphe_var = ARGV[0]
-  coquelicot_depot = ARGV[1]
-
-  unless File.directory? "#{jyraphe_var}/files" and
-         File.directory? "#{jyraphe_var}/links" then
-    STDERR.puts "#{jyraphe_var} is not a Jyraphe 'var' directory."
-    exit 1
-  end
-  unless File.exists? "#{coquelicot_depot}/.links" then
-    STDERR.puts "#{coquelicot_depot} is not a Coquelicot depot."
-    exit 1
-  end
-
-  Coquelicot::Application.set :depot_path, coquelicot_depot
-  JyrapheMigrator.new(jyraphe_var).process
-end
-
-main