cp2s3.rb 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. #!/bin/env ruby
  2. # This file is released under the MIT license.
  3. # Copyright (c) Famundo LLC, 2007. http://www.famundo.com
  4. # Author: Guy Naor - http://devblog.famundo.com
  5. require 'optparse'
  6. # Parse the options
  7. @buckets = []
  8. @compress = []
  9. @verbose = 0
  10. opts = OptionParser.new do |opts|
  11. opts.banner = "Usage: cp2s3.rb [options] FILE_SPEC"
  12. opts.separator "Copy files and directories from the local machine into Amazon's S3. Keep the directory structure intact."
  13. opts.separator "Empty directories will be skipped."
  14. opts.separator ""
  15. opts.separator "FILE_SPEC List of files/directories. Accepts wildcards."
  16. opts.separator " If given the -g option, interpret FILE_SPEC as a Ruby Dir::Glob style regular expressions."
  17. opts.separator " With -g option, '' needed around the pattern to protect it from shell parsing."
  18. opts.separator ""
  19. opts.separator "Required:"
  20. opts.on("-k", "--key ACCESS_KEY" , "Your S3 access key. You can also set the environment variable AWS_ACCESS_KEY_ID instead") { |o| @access_key = o }
  21. opts.on("-s", "--secret SECRET_KEY" , "Your S3 secret key. You can also set the environment variable AWS_SECRET_ACCESS_KEY instead") { |o| @secret_key = o }
  22. opts.on("-b", "--bucket BUCKET_NAME", "The S3 bucket you want the files to go into. Repeat for multiple buckets.") { |o| @buckets << o }
  23. opts.separator ""
  24. opts.separator "Optional:"
  25. opts.on("-x", "--remote-prefix PREFIX", "A prefix to add to each file as it's uploaded") { |o| @prefix = o }
  26. opts.on("-v", "--verbose", "Print the file names as they are being copied. Repeat for more details") { |o| @verbose += 1 }
  27. opts.on("-p", "--public-read", "Set the copied files permission to be public readable.") { |o| @public = true }
  28. opts.on("-c", "--compress EXT", "Compress files with given EXT before uploading (ususally css and js),", "setting the HTTP headers for delivery accordingly. Repeat for multiple extensions") { |o| @compress << ".#{o}" }
  29. opts.on("-d", "--digest", "Save the sha1 digest of the file, to the S3 metadata. Require sha1sum to be installed") { |o| @save_hash = true }
  30. opts.on("-t", "--time", "Save modified time of the file, to the S3 metadata") { |o| @save_time = true }
  31. opts.on("-z", "--size", "Save size of the file, to the S3 metadata ") { |o| @save_size = true }
  32. opts.on("-r", "--recursive", "If using file system based FILE_SPEC, recurse into sub-directories") { |o| @fs_recurse = true }
  33. opts.on("-g", "--glob-ruby", "Interpret FILE_SPEC as a Ruby Dir::Glob. Make sure to put it in ''") { |o| @ruby_glob = true }
  34. opts.on("-m", "--modified-only", "Only upload files that were modified must have need uploaded with the digest option.", "Will force digest, size and time modes on") { |o| @modified_only = @save_hash = @save_time = @save_size = true; }
  35. opts.on("-y", "--dry-run", "Simulate only - do not upload any file to S3") { |o| @dry_run = true }
  36. opts.on("-h", "--help", "Show this instructions") { |o| @help_exit = true }
  37. opts.separator ""
  38. opts.banner = "Copyright(c) Famundo LLC, 2007 (www.famundo.com). Released under the MIT license."
  39. end
  40. @file_spec = opts.parse!(ARGV)
  41. @access_key ||= ENV['AWS_ACCESS_KEY_ID']
  42. @secret_key ||= ENV['AWS_SECRET_ACCESS_KEY']
  43. @prefix ||= ''
  44. if @help_exit || !@access_key || !@secret_key || @buckets.empty? || !@file_spec || @file_spec.empty?
  45. puts opts.to_s
  46. exit
  47. end
  48. # Now we start working for real
  49. require 'rubygems'
  50. require 'aws/s3'
  51. include AWS::S3
  52. require 'fileutils'
  53. require 'stringio'
  54. require 'zlib'
  55. # Log to stderr according to verbosity
  56. def log message, for_level
  57. puts(message) if @verbose >= for_level
  58. end
  59. # Connect to s3
  60. log "Connecting to S3", 3
  61. AWS::S3::Base.establish_connection!(:access_key_id => @access_key, :secret_access_key => @secret_key)
  62. log "Connected!", 3
  63. # Copy one file to amazon, compressing and setting metadata as needed
  64. def copy_one_file file, fstat
  65. compressed = nil
  66. content_encoding = nil
  67. log_prefix = ''
  68. # Store it!
  69. options = {}
  70. options[:access] = :public_read if @public
  71. options["x-amz-meta-sha1_hash"] = `sha1sum #{file}`.split[0] if @save_hash
  72. options["x-amz-meta-mtime"] = fstat.mtime.getutc.to_i if @save_time
  73. options["x-amz-meta-size"] = fstat.size if @save_size
  74. sent_it = !@modified_only
  75. @buckets.each do |b|
  76. # Check if it was modified
  77. if @modified_only
  78. begin
  79. if S3Object.find("#{@prefix}#{file}", b).metadata["x-amz-meta-sha1_hash"] == options["x-amz-meta-sha1_hash"]
  80. # No change - go on
  81. log("Skipping: #{file} in #{b}", 3)
  82. next
  83. end
  84. rescue AWS::S3::NoSuchKey => ex
  85. # This file isn't there yet, so we need to send it
  86. end
  87. end
  88. # We compress only if we need to compredd and we didn't compress yet
  89. if !@compress.empty? && compressed.nil?
  90. if @compress.include?(File.extname(file))
  91. # Compress it
  92. log "Compressing #{file}", 3
  93. strio = StringIO.open('', 'w')
  94. gz = Zlib::GzipWriter.new(strio)
  95. gz.write(open(file).read)
  96. gz.close
  97. compressed = strio.string
  98. options["Content-Encoding"] = 'gzip'
  99. log_prefix = '[c] ' if @verbose == 2 # Mark as compressed
  100. elsif @verbose == 2
  101. log_prefix = '[-] ' # So the file names align...
  102. end
  103. end
  104. log("Sending #{file} to #{b}...", 3)
  105. S3Object.store("#{@prefix}#{file}", compressed.nil? ? open(file) : compressed, b, options) unless @dry_run
  106. sent_it = true
  107. end
  108. log("#{log_prefix}#{file}", 1) if sent_it
  109. end
  110. # Copy one file/dir from the system, recurssing if needed. Used for non-Ruby style globs
  111. def copy_one_file_or_dir name, base_dir
  112. return if name[0,1] == '.'
  113. file_name = "#{base_dir}#{name}"
  114. fstat = File.stat(file_name)
  115. copy_one_file(file_name, fstat) if fstat.file? || fstat.symlink?
  116. # See if we need to recurse...
  117. if @fs_recurse && fstat.directory?
  118. my_base = file_name + '/'
  119. Dir.foreach(my_base) { |e| copy_one_file_or_dir(e, my_base) }
  120. end
  121. end
  122. # Glob all the dirs for the files to upload - we expect a ruby like glob format or file system list from the command line
  123. @file_spec.each do |spec|
  124. if @ruby_glob
  125. # Ruby style
  126. Dir.glob(spec) do |file|
  127. fstat = File.stat(file)
  128. copy_one_file(file, fstat) if fstat.file? || fstat.symlink?
  129. end
  130. else
  131. # File system style
  132. copy_one_file_or_dir(spec, '')
  133. end
  134. end