ぎょーぼのぶろぐ

IT系の話を書いていくブログです。今はRubyの勉強中。

【ツール作成】ファイルコピーを実際に行うモジュール

前置き

ファイルのコピーや、ディレクトリの作成を行うモジュール群です。

実装コード

全体のモジュール名は、BackupUtils としています。また、ファイルをコピーする際に、FileUtils.copyを使用するので、fileutils をrequireしています。

module BackupUtils

    require 'fileutils'
self.get_dir_info メソッド

コピー前の確認用に、バックアップ元ディレクトリ、バックアップ先ディレクトリの情報を画面に表示するメソッドです。設定の中で、有効ではないディレクトリは、事前にチェックして外しているので、ここではディレクトリのハッシュ配列を受け取って、表示するだけです。

    #
    # self.get_dir_info
    #   ディレクトリ情報を受け取り、読みやすい形式で画面出力する。
    #   引数
    #       dirs    : バックアップディレクトリ情報
    #
    def self.show_dir_info(dirs)
        tmp = ""

        # バックアップ元ディレクトリを展開し、文言化する。
        dirs.src_dir.each { |key, value|
            tmp += Message.t(:info09, key: key, value: value)
        } 

        # 表示
        Message.show_and_log(:info, :info02, desc: tmp)
        Message.show_and_log(:info, :info03, desc: dirs.dest_dir)
    end
self.show_confirmation メソッド

実行確認を行うメソッドです。

  • gets メソッドで入力を取得。
  • 前後の空白改行をstripで取り除き、大文字小文字の区別をなくすためにupcaseで大文字に統一します。
  • 入力が Y だったら、バックアップ処理を継続。Nだったら、処理を中止し、他の値の場合は再度、入力を促します。
  • 空の入力の場合は、デフォルトで処理を継続します。

一応、例外処理は入れています。Ctrl-c等で強制終了したりすると、例外になります。個別に rescue Interrupt で捕捉することはできますが、ここでは意味がないので、その他の例外と合わせてメッセージを表示します。

    #
    # self.show_confirmation
    #   実行確認を行う。
    #
    def self.show_confirmation
        # 処理の実行確認メッセージ
        loop do 
            Message.show(:confirm01, "")
            answer = gets

            case answer.strip.upcase
            when "Y", ""
                Message.show(:confirm02, "")
                break
            when "N"
                Message.show(:confirm03, "")
                exit(true)
            else
                Message.show(:confirm04, "") 
            end
        end
    
    rescue => ex
        Message.exception(__method__, ex)
        exit(false)
    end
self.execute_copy メソッド

バックアップを実行するメソッドです。
最初のバックアップ先ディレクトリの作成で、"[prefix名][yyyymmddhhmmss]" の名前でディレクトリを生成するのですが、ディレクトリ名の重複チェックをしていないので、そこで例外が発生する可能性があります。しかし、年月日時分秒でディレクトリを生成しているので、狙って仕込まないとディレクトリ名が重複する可能性は皆無、と判断して、チェックは入れていません。

バックアップ元ディレクトリ毎に、ProressBarオブジェクトを生成し、ファイルコピーを実行、最後に結果を表示します。

    #
    # self.execute_copy
    #   バックアップ先へ、ディレクトリ作成、ファイルコピーを行う。
    #   引数
    #       dirs    :  バックアップ元、バックアップ先ディレクトリ情報
    #
    def self.execute_copy(dirs)

        # バックアップディレクトリの生成
        dest_dir = File.join(dirs.dest_dir, dirs.dest_prefix+Time.new.strftime("%Y%m%d%H%M%S"))
        Dir.mkdir(dest_dir) 

        # 実行開始
        dirs.src_dir.each { |key, value| 

            # プログレスバーの生成(ソースディレクトリ名とディレクトリの容量を渡す。)
            bar = ProgressBar.new(File.basename(value), get_dir_info(value)[:dir_size])

            # ディレクトリ内のファイルコピーを実行する。
            copy_files(value, dest_dir, bar)

            # 実行完了したら、改行しておく。
            Message.show(:info04)
        }

        # 結果の表示
        tmp = get_dir_info(dest_dir)
        Message.show_and_log(:info, :info05, path: dest_dir)
        Message.show_and_log(:info, :info06, size: tmp[:dir_size], fcount: tmp[:file_count], dcount: tmp[:dir_count])

    rescue => ex
        Message.exception(__method__, ex)
        exit(false)
    end
self.get_dir_info メソッド(プライベート)

ディレクトリの総サイズ、ファイル数、ディレクトリ数を収集するメソッドです。

File.join(directory, "**", "*") の部分は、directory/**/* というパスを生成しています。引数で渡されたディレクトリの「配下、サブディレクトリ内も含めて(/*/)」「全て()」という意味になります。

つづいての File::FNM_DOTMATCH オプションで、ピリオド付きの名前のディレクトリも含め、最後の rejectメソッドで、「ピリオドで終わる名前」を除外しています。これは、カレントディレクトリ(.)と、親ディレクトリ(..)を除外するためです。

ディレクトリ内のオブジェクトが取得できたら、後はサイズを合算、ファイルかディレクトリかを判断してカウントするだけです。

    private

    #
    # self.get_dir_info
    #   引数のディレクトリの情報を取得する。
    #   引数
    #       directory   :   情報を取得するディレクトリパス
    #   戻り値
    #       ハッシュ配列(ディレクトリの総サイズ、ファイル数、ディレクトリ数)
    #
    def self.get_dir_info(directory)
        dir_size = 0      # 総サイズ計算用
        file_count = 0    # 総ファイル数計算用
        dir_count = 0     # 総ディレクトリ数計算用
    
        # オブジェクト一覧を取得
        # `.`付きのフォルダーも取得するが、`.`で終わる特殊フォルダーは除外する。
        list = Dir.glob(File.join(directory, "**", "*"), File::FNM_DOTMATCH).reject{|x| x =~ /\.$/}

        list.each { |f| 
            # ディレクトリではない場合のみ、サイズを加算する。
            if File::ftype(f) != "directory" then
                dir_size += File.size(f)    # ファイルサイズのカウント
                file_count += 1             # ファイル数のカウント
            else
                dir_count += 1              # ディレクトリ数のカウント
            end
        }

        # 総サイズを返す。
        return Hash[dir_size: dir_size, file_count: file_count, dir_count: dir_count]

    rescue => ex
        Message.exception(__method__, ex)
        exit(false)
    end
self.copy_files メソッド(プライベート)

実際にディレクトリの作成、ファイルのコピーを行うメソッドです。

ディレクトリは、中身を含めてまとめてコピーはできますが、ディレクトリの皮(?)のみをコピーすることはできません。ですので、mkdir メソッドを使って、新規作成していきます。

ここでは、ディレクトリの重複チェックを行っています。今回は、バックアップ元ディレクトリを複数選択できるのですが、バックアップ元ディレクトリの名前が同じだった場合、バックアップ先で重複してしまうためです。重複したら、_を都度つけることで対応しています。

手順は以下の通りです。

例外は基本的には出ないハズですが、一応、想定外のエラーのためのrescueはしています。

    #
    # self.copy_files
    #   ディレクトリの作成、ファイルのコピーを行う。
    #   再帰呼び出しを行うことで、配下のファイル、ディレクトリ全てを作成、コピーする。
    #   引数
    #       src     :   コピー元のディレクトリ(単体。ハッシュではないので注意)
    #       dest    :   コピー先のディレクトリ
    #       bar     :   プログレスバーオブジェクト
    #
    def self.copy_files(src, dest, bar)
        # バックアップ元のディレクトリ名を取得し、バックアップ先にその名前のディレクトリを生成する。
        dest_dir = File.join(dest, File.basename(src))

        # バックアップ先に同じ名前のディレクトリがすでにある場合は、"_"を付与。
        # ディレクトリの重複が無くなるまで、"_"を付与し続ける。
        while Dir.exists?(dest_dir)
            dest_dir = dest_dir + "_"
        end

        Dir.mkdir(dest_dir)
        Message.log(:info, :info07, dir: dest_dir.to_s)

        # バックアップ元のディレクトリ内のディレクトリ/ファイルの一覧取得(`.`, `..`は取得しない)
        list = Dir.glob(File.join(src, "*"), File::FNM_DOTMATCH).reject{|x| x =~ /\.$/}

        list.each { |f| 
            if File::ftype(f) == "directory"
                # ディレクトリの場合、そのディレクトリをソースディレクトリとして、再帰呼び出しする。
                copy_files(f, dest_dir, bar)
            else
                # ファイルの場合、プログレスバーを更新して、ファイルコピー実行
                bar.show(File.basename(f), File.size(f))
                FileUtils.copy(f, dest_dir)
                Message.log(:info, :info08, file: f.to_s)
            end
        }
    rescue => ex
        Message.exception(__method__, ex)
        exit(false)
    end
end

補足

Message モジュールについては、一つ前の記事で説明しているので、そちらを見てください。