Skip to content
This repository
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 318 lines (267 sloc) 11.555 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
require 'capistrano/recipes/deploy/strategy/base'
require 'fileutils'
require 'tempfile' # Dir.tmpdir

module Capistrano
  module Deploy
    module Strategy

      # This class implements the strategy for deployments which work
      # by preparing the source code locally, compressing it, copying the
      # file to each target host, and uncompressing it to the deployment
      # directory.
      #
      # By default, the SCM checkout command is used to obtain the local copy
      # of the source code. If you would rather use the export operation,
      # you can set the :copy_strategy variable to :export.
      #
      # set :copy_strategy, :export
      #
      # For even faster deployments, you can set the :copy_cache variable to
      # true. This will cause deployments to do a new checkout of your
      # repository to a new directory, and then copy that checkout. Subsequent
      # deploys will just resync that copy, rather than doing an entirely new
      # checkout. Additionally, you can specify file patterns to exclude from
      # the copy when using :copy_cache; just set the :copy_exclude variable
      # to a file glob (or an array of globs).
      #
      # set :copy_cache, true
      # set :copy_exclude, ".git/*"
      #
      # Note that :copy_strategy is ignored when :copy_cache is set. Also, if
      # you want the copy cache put somewhere specific, you can set the variable
      # to the path you want, instead of merely 'true':
      #
      # set :copy_cache, "/tmp/caches/myapp"
      #
      # This deployment strategy also supports a special variable,
      # :copy_compression, which must be one of :gzip, :bz2, or
      # :zip, and which specifies how the source should be compressed for
      # transmission to each host.
      #
      # There is a possibility to pass a build command that will get
      # executed if your code needs to be compiled or something needs to be
      # done before the code is ready to run.
      #
      # set :build_script, "make all"
      #
      # Note that if you use :copy_cache, the :build_script is used on the
      # cache and thus you get faster compilation if your script does not
      # recompile everything.
      class Copy < Base
        # Obtains a copy of the source code locally (via the #command method),
        # compresses it to a single file, copies that file to all target
        # servers, and uncompresses it on each of them into the deployment
        # directory.
        def deploy!
          if copy_cache
            copy_repository_to_local_cache
            build(copy_cache)

            copy_cache_to_staging_area
          else
            copy_repository_to_server
            build(destination)

            remove_excluded_files if copy_exclude.any?
          end

          create_revision_file
          compress_repository
          distribute!
        ensure
          rollback_changes
        end

        def build(directory)
          return unless configuration[:build_script]

          execute "running build script on #{directory}" do
            Dir.chdir(directory) { self.system(configuration[:build_script]) }
          end
        end

        def check!
          super.check do |d|
            d.local.command(source.local.command) if source.local.command
            d.local.command(compress(nil, nil).first)
            d.remote.command(decompress(nil).first)
          end
        end

        # Returns the location of the local copy cache, if the strategy should
        # use a local cache + copy instead of a new checkout/export every
        # time. Returns +nil+ unless :copy_cache has been set. If :copy_cache
        # is +true+, a default cache location will be returned.
        def copy_cache
          @copy_cache ||= configuration[:copy_cache] == true ?
            File.expand_path(configuration[:application], Dir.tmpdir) :
            File.expand_path(configuration[:copy_cache], Dir.pwd) rescue nil
        end

        private

          def execute description, &block
            logger.debug description
            handle_system_errors &block
          end

          def handle_system_errors &block
            block.call
            raise_command_failed if last_command_failed?
          end

          def refresh_local_cache
            execute "refreshing local cache to revision #{revision} at #{copy_cache}" do
              system(source.sync(revision, copy_cache))
            end
          end

          def create_local_cache
            execute "preparing local cache at #{copy_cache}" do
              system(source.checkout(revision, copy_cache))
            end
          end

          def raise_command_failed
            raise Capistrano::Error, "shell command failed with return code #{$?}"
          end

          def last_command_failed?
            $? != 0
          end

          def copy_cache_to_staging_area
            execute "copying cache to deployment staging area #{destination}" do
              create_destination

              Dir.chdir(copy_cache) do
                copy_files(queue_files)
              end
            end
          end

          def create_destination
            FileUtils.mkdir_p(destination)
          end

          def copy_files files
            files.each { |name| process_file(name) }
          end

          def process_file name
            return copy_symlink(name) if File.symlink? name
            return copy_directory(name) if File.directory? name
            copy_hardlink name
          end

          def copy_symlink name
            FileUtils.ln_s(File.readlink(item), File.join(destination, name))
          end

          def copy_directory name
            FileUtils.mkdir(File.join(destination, name))
            copy_files(queue_files(name))
          end

          def copy_hardlink name
            FileUtils.ln(name, File.join(destination, name))
          end

          def queue_files directory=nil
            Dir.glob(pattern_for(directory), File::FNM_DOTMATCH).reject! { |file| excluded_files_contain? file }
          end

          def pattern_for directory
            !directory.nil? ? "#{directory}/*" : "*"
          end

          def excluded_files_contain? file
            copy_exclude.any? { |p| File.fnmatch(p, file) } or [ ".", ".."].include? File.basename(file)
          end

          def copy_repository_to_server
            execute "getting (via #{copy_strategy}) revision #{revision} to #{destination}" do
              system(command)
            end
          end

          def remove_excluded_files
            logger.debug "processing exclusions..."

            copy_exclude.each do |pattern|
              delete_list = Dir.glob(File.join(destination, pattern), File::FNM_DOTMATCH)
              # avoid the /.. trap that deletes the parent directories
              delete_list.delete_if { |dir| dir =~ /\/\.\.$/ }
              FileUtils.rm_rf(delete_list.compact)
            end
          end

          def create_revision_file
            File.open(File.join(destination, "REVISION"), "w") { |f| f.puts(revision) }
          end

          def compress_repository
            execute "Compressing #{destination} to #{filename}" do
              Dir.chdir(copy_dir) { system(compress(File.basename(destination), File.basename(filename)).join(" ")) }
            end
          end

          def rollback_changes
            FileUtils.rm filename rescue nil
            FileUtils.rm_rf destination rescue nil
          end

          def copy_repository_to_local_cache
            return refresh_local_cache if File.exists?(copy_cache)
            create_local_cache
          end

          # Specify patterns to exclude from the copy. This is only valid
          # when using a local cache.
          def copy_exclude
            @copy_exclude ||= Array(configuration.fetch(:copy_exclude, []))
          end

          # Returns the basename of the release_path, which will be used to
          # name the local copy and archive file.
          def destination
            @destination ||= File.join(copy_dir, File.basename(configuration[:release_path]))
          end

          # Returns the value of the :copy_strategy variable, defaulting to
          # :checkout if it has not been set.
          def copy_strategy
            @copy_strategy ||= configuration.fetch(:copy_strategy, :checkout)
          end

          # Should return the command(s) necessary to obtain the source code
          # locally.
          def command
            @command ||= case copy_strategy
            when :checkout
              source.checkout(revision, destination)
            when :export
              source.export(revision, destination)
            end
          end

          # Returns the name of the file that the source code will be
          # compressed to.
          def filename
            @filename ||= File.join(copy_dir, "#{File.basename(destination)}.#{compression.extension}")
          end

          # The directory to which the copy should be checked out
          def copy_dir
            @copy_dir ||= File.expand_path(configuration[:copy_dir] || Dir.tmpdir, Dir.pwd)
          end

          # The directory on the remote server to which the archive should be
          # copied
          def remote_dir
            @remote_dir ||= configuration[:copy_remote_dir] || "/tmp"
          end

          # The location on the remote server where the file should be
          # temporarily stored.
          def remote_filename
            @remote_filename ||= File.join(remote_dir, File.basename(filename))
          end

          # A struct for representing the specifics of a compression type.
          # Commands are arrays, where the first element is the utility to be
          # used to perform the compression or decompression.
          Compression = Struct.new(:extension, :compress_command, :decompress_command)

          # The compression method to use, defaults to :gzip.
          def compression
            remote_tar = configuration[:copy_remote_tar] || 'tar'
            local_tar = configuration[:copy_local_tar] || 'tar'

            type = configuration[:copy_compression] || :gzip
            case type
            when :gzip, :gz then Compression.new("tar.gz", [local_tar, 'czf'], [remote_tar, 'xzf'])
            when :bzip2, :bz2 then Compression.new("tar.bz2", [local_tar, 'cjf'], [remote_tar, 'xjf'])
            when :zip then Compression.new("zip", %w(zip -qyr), %w(unzip -q))
            else raise ArgumentError, "invalid compression type #{type.inspect}"
            end
          end

          # Returns the command necessary to compress the given directory
          # into the given file.
          def compress(directory, file)
            compression.compress_command + [file, directory]
          end

          # Returns the command necessary to decompress the given file,
          # relative to the current working directory. It must also
          # preserve the directory structure in the file.
          def decompress(file)
            compression.decompress_command + [file]
          end

          def decompress_remote_file
            run "cd #{configuration[:releases_path]} && #{decompress(remote_filename).join(" ")} && rm #{remote_filename}"
          end

          # Distributes the file to the remote servers
          def distribute!
            upload(filename, remote_filename)
            decompress_remote_file
          end
      end

    end
  end
end
Something went wrong with that request. Please try again.