rubyzipについて

rubygemsでインストールできるZip圧縮ユーティリティ、rubyzipというものがある。ディレクトリごと圧縮したい場合はrubyzipを使うと簡単。ただ、rubyzipはGPLなので注意しないといけない。rubygems経由で使う分はGPLライセンスの影響は受けないと思うが、ソフトウェアにソースコードを組み込むと(gemディレクトリから抜き出してRailsのvendor以下に入れたり、libに入れたりとか)GPLライセンスが感染してしまうだろう。rubyzipはRubyと同じライセンスなので、vendor以下などに入れてもいいみたい。

#rubyzipのインストール
gem install rubyzip

ファイル・ディレクトリの圧縮はZipFileクラスのopenメソッドでzipファイルを開いて、addメソッドでファイルエントリを追加していく。
展開はZipInputStreamクラスのopenメソッドでzipファイルを開いてから、get_next_entryメソッドでエントリをひとつずつ取得して、readメソッドで中身を取得する。
注意しないといけないのは、zipファイルに登録するエントリのファイル名に使う文字コードである。Windowsの場合はShift_JISにしないと解凍したときにファイル名が化ける、LinuxだとUTF-8など環境に合わせてやる必要がある。

以下は簡単な例。ZipFileUtils.zipとZipFileUtils.unzipがそれぞれ圧縮と解凍。ディレクトリを圧縮することも可能。options引数は:fs_encodingでファイルシステムエンコードUTF-8Shift_JISEUC-JPから指定する。

require 'rubygems'
require 'kconv'
require 'zip/zipfilesystem'
require 'fileutils'

module ZipFileUtils
  
  # src  file or directory
  # dest  zip filename
  # options :fs_encoding=[UTF-8,Shift_JIS,EUC-JP]
  def self.zip(src, dest, options = {})
    src = File.expand_path(src)
    dest = File.expand_path(dest)
    File.unlink(dest) if File.exist?(dest)
    Zip::ZipFile.open(dest, Zip::ZipFile::CREATE) {|zf|
      if(File.file?(src))
        zf.add(encode_path(File.basename(src), options[:fs_encoding]), src)
        break
      else
        each_dir_for(src){ |path|
          if File.file?(path)
            zf.add(encode_path(relative(path, src), options[:fs_encoding]), path)
          elsif File.directory?(path)
            zf.mkdir(encode_path(relative(path, src), options[:fs_encoding]))
          end
        }
      end
    }
  end
  
  # src  zip filename
  # dest  destination directory
  # options :fs_encoding=[UTF-8,Shift_JIS,EUC-JP]
  def self.unzip(src, dest, options = {})
    FileUtils.makedirs(dest)
    Zip::ZipInputStream.open(src){ |is|
      loop do
        entry = is.get_next_entry()
        break if entry.nil?()
        dir = File.dirname(entry.name)
        FileUtils.makedirs(dest+ '/' + dir)
        path = encode_path(dest + '/' + entry.name, options[:fs_encoding])
        if(entry.file?())
          File.open(path,
                File::CREAT|File::WRONLY|File::BINARY) do |w|
           w.puts(is.read())
          end
        else
          FileUtils.makedirs(path)
        end
      end
    }
  end
  
  private
  def self.each_dir_for(dir_path, &block)
    dir = Dir.open(dir_path)
    each_file_for(dir_path){ |file_path|
      yield(file_path)
    }
  end
  
  def self.each_file_for(path, &block)
    if File.file?(path)
      yield(path)
      return true
    end
    dir = Dir.open(path)
    file_exist = false
    dir.each(){ |file|
      next if file == '.' || file == '..'
      file_exist = true if each_file_for(path + "/" + file, &block)
    }
    yield(path) unless file_exist
    return file_exist
  end
  
  def self.relative(path, base_dir)
    path[base_dir.length() + 1 .. path.length()] if path.index(base_dir) == 0
  end
  
  def self.encode_path(path, encode_s)
    return path if encode_s.nil?()
    case(encode_s)
    when('UTF-8')
      return path.toutf8()
    when('Shift_JIS')
      return path.tosjis()
    when('EUC-JP')
      return path.toeuc()
    else
      return path
    end
  end
end

使い方の例

#c:\rubyをruby.zipに圧縮
ZipFileUtils.zip('c:/ruby', 'c:/ruby.zip')

#ruby.zipをc:\rubyに展開
ZipFileUtils.unzip('c:/ruby.zip', 'c:/ruby')

#c:\rubyをruby.zipにファイル名のエンコードをShift_JISに指定して圧縮
ZipFileUtils.zip('c:/ruby', 'c:/ruby.zip', {:fs_encoding => 'Shift_JIS'})

#ruby.zipをc:\rubyにファイル名のエンコードをShift_JISに指定して展開
ZipFileUtils.unzip('c:/ruby.zip', 'c:/ruby', {:fs_encoding => 'Shift_JIS'})