upstatic 0.1.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9d8c0dafd826bfe88476003a538e4a1d224d3aa7
4
+ data.tar.gz: 1b1c06c646502e866dfc2a8efd64d30b273a187d
5
+ SHA512:
6
+ metadata.gz: 8f8082e3e79931cf3089570dea57ead37aa21d4507b4d95ed372c84a4d09861dc2634c8aacc2811d4111bc73f3e6fde66706069f986ef92ff32af657e200c8ca
7
+ data.tar.gz: 99d79cba6f0c0bf7a60acd594802d1bb38e5b0fd0934f833ec897f0787250ee2da19f04ca064bf94b55c33e05d5b0d0174179d229d9729067d3fd41eee418efa
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ vendor/
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source 'https://rubygems.org'
2
+
3
+ group :development do
4
+ gem "rake"
5
+ end
6
+
7
+ group :test do
8
+ gem "minitest", "~> 5.0.0"
9
+ gem "simplecov"
10
+ gem "webmock"
11
+ end
12
+
13
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Joao Carlos
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,106 @@
1
+ # Upstatic
2
+
3
+ Deploy your static sites to S3. Supports Cloudront object invalidation,
4
+ file gzipping, and configuration of cache control headers.
5
+
6
+
7
+ ## Installation
8
+
9
+ Until upstatic is released on rubygems, please clone the repository and
10
+ install with:
11
+
12
+ rake install
13
+
14
+ ## Usage
15
+
16
+ Create a file named _Upstatic_ in the directory you want to deploy and
17
+ specify at least the S3 bucket name:
18
+
19
+ ```ruby
20
+ bucket "upstatic-site"
21
+ ```
22
+
23
+ Upstatic tries to read your AWS credentials from the following
24
+ environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.
25
+
26
+ You can also specify them in the _Upstatic_ file as:
27
+
28
+ ```ruby
29
+ access_key_id "MY_AWS_KEY_ID"
30
+ secret_access_key "MY_AWS_SECRET_ACCESS_KEY"
31
+ ```
32
+
33
+ Once the bucket and credentials are setup, you can deploy by running:
34
+
35
+ upstatic deploy
36
+
37
+ For more configuration options, keep reading.
38
+
39
+ ## Configuration
40
+
41
+ ### Gzip
42
+
43
+ If you want to gzip certain files, just specify which file extensions
44
+ you'd like gzipped. Note that gzipping files means that they'll always
45
+ be served compressed. S3 does not support serving different files based
46
+ on the Accept-Encoding header.
47
+
48
+ ```ruby
49
+ gzip_extensions ".html", ".js", ".css"
50
+ ```
51
+
52
+ By default no files are gzipped.
53
+
54
+ ### Cache-Control headers
55
+
56
+ You can also specify Cache-Control headers for specific file extensions.
57
+ For example, you might want Cloudfront (and proxies) to cache HTML files
58
+ for 600 seconds, but tell the browser not to cache them:
59
+
60
+ ```ruby
61
+ cache_control ".html", "public, s-maxage=600, max-age=0"
62
+ ```
63
+
64
+ You can also specify the default for all other files:
65
+
66
+ ```ruby
67
+ default_cache_control "public, max-age=691200"
68
+ ```
69
+
70
+ The default is to serve every file with `Cache-Control: public,
71
+ max-age=691200`.
72
+
73
+ ### Cloudfront
74
+
75
+ If you are serving some or all your files from Cloudfront, you probably
76
+ want to invalidate the newly updated objects. Just specify your
77
+ Cloudfront distribution ID and it will be taken care of when you deploy.
78
+
79
+ ```ruby
80
+ distribution_id "E2ADVMBKHEFZJ3"
81
+ ```
82
+
83
+ ## Notes
84
+
85
+ This extension keeps track of every deployed file in a `.sha1sums` at
86
+ the root of the bucket. This is a private file that contains a SHA-1 sum
87
+ of the uploaded file, followed by a SHA-1 sum of its upload options and
88
+ finally the filename. This file is used to know if a file needs to be
89
+ uploaded again.
90
+
91
+ ## Testing
92
+
93
+ Everything is tested by actually deploying to S3 and requesting the
94
+ deployed files.
95
+
96
+ ## Contributing
97
+
98
+ 1. Fork it
99
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
100
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
101
+ 4. Push to the branch (`git push origin my-new-feature`)
102
+ 5. Create new Pull Request
103
+
104
+ ## License
105
+
106
+ MIT License.
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.test_files = Dir.glob("test/**/test_*.rb")
7
+ end
8
+
9
+ task :default => :test
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require "upstatic/cli"
3
+ Upstatic::CLI.start
@@ -0,0 +1,15 @@
1
+ require "upstatic/compat"
2
+ require "upstatic/configuration"
3
+ require "upstatic/deployer"
4
+ require "upstatic/manager"
5
+ require "upstatic/shell"
6
+
7
+ module Upstatic
8
+ def self.shell
9
+ @@shell ||= Upstatic::Shell.new
10
+ end
11
+
12
+ def self.shell=(shell)
13
+ @@shell = shell
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ require "upstatic"
2
+ require "thor"
3
+
4
+ module Upstatic
5
+ class CLI < Thor
6
+ include Thor::Actions
7
+
8
+ desc "deploy", "Deploy to S3"
9
+ def deploy
10
+ Deployer.new(configuration).deploy!
11
+ end
12
+
13
+ private
14
+
15
+ def configuration
16
+ @configuration ||= Configuration.read("./Upstatic")
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ require "securerandom"
2
+
3
+ unless SecureRandom.respond_to?(:uuid)
4
+ module SecureRandom
5
+ def self.uuid
6
+ ary = self.random_bytes(16).unpack("NnnnnN")
7
+ ary[2] = (ary[2] & 0x0fff) | 0x4000
8
+ ary[3] = (ary[3] & 0x3fff) | 0x8000
9
+ "%08x-%04x-%04x-%04x-%04x%08x" % ary
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,45 @@
1
+ require "upstatic/options"
2
+
3
+ module Upstatic
4
+ class Configuration
5
+ include Options
6
+
7
+ option :bucket
8
+ option :region
9
+ option :access_key_id
10
+ option :secret_access_key
11
+ option :distribution_id
12
+ option :default_cache_control, :default => "public, max-age=691200"
13
+ option :upload_dir, :default => "."
14
+ option :force_deploy, :default => false
15
+
16
+ def gzip_extensions(*extensions)
17
+ if extensions.count > 0
18
+ @gzip_extensions = extensions.map { |ext| sanitize_extension(ext) }
19
+ else
20
+ @gzip_extensions ||= []
21
+ end
22
+ end
23
+
24
+ def cache_controls
25
+ @cache_controls ||= {}
26
+ end
27
+
28
+ def cache_control(extension, cache_control)
29
+ cache_controls[sanitize_extension(extension)] = cache_control
30
+ end
31
+
32
+ def self.read(path)
33
+ config = new
34
+ config.instance_eval(File.read(path), path, 1)
35
+ config
36
+ end
37
+
38
+ private
39
+
40
+ def sanitize_extension(extension)
41
+ extension.to_s.sub(/\A([^.])/, '.\1')
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,145 @@
1
+ require "securerandom"
2
+ require "mime/types"
3
+ require "zlib"
4
+ require "stringio"
5
+ require "upstatic/tool"
6
+ require "upstatic/manager"
7
+ require "upstatic/verifier"
8
+
9
+ module Upstatic
10
+ class Deployer < Tool
11
+ FORBIDDEN_FILENAMES = ["Upstatic"]
12
+
13
+ def deploy!
14
+ manager.create_bucket
15
+ manager.configure_bucket
16
+
17
+ Dir.chdir config.upload_dir do
18
+ Dir["**/*"].each do |filename|
19
+ upload(filename)
20
+ end
21
+ end
22
+
23
+ perform_invalidation
24
+ verifier.write_checksum_file
25
+
26
+ @verifier = nil
27
+ @invalidations = nil
28
+ end
29
+
30
+ private
31
+
32
+ def manager
33
+ @manager ||= Manager.new(config)
34
+ end
35
+
36
+ def verifier
37
+ @verifier ||= Verifier.new(config)
38
+ end
39
+
40
+ def invalidations
41
+ @invalidations ||= Set.new
42
+ end
43
+
44
+ def mark_to_invalidate(filename)
45
+ if config.distribution_id
46
+ invalidate_path("/#{filename}")
47
+
48
+ if File.basename(filename) == "index.html"
49
+ dirname = File.dirname(filename)
50
+
51
+ if dirname == "."
52
+ invalidate_path("/")
53
+ else
54
+ invalidate_path("/#{dirname}")
55
+ invalidate_path("/#{dirname}/")
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ def invalidate_path(path)
62
+ if invalidations.add?(path)
63
+ shell.say_status("invalidate", path, :cyan)
64
+ end
65
+ end
66
+
67
+ def options_for_file(filename)
68
+ extension = File.extname(filename)
69
+ mime_type = MIME::Types.type_for(filename).first
70
+ content_type = mime_type ? mime_type.content_type : nil
71
+
72
+ file_options = {
73
+ :file => filename,
74
+ :acl => :public_read,
75
+ :content_type => content_type,
76
+ :cache_control => config.default_cache_control
77
+ }
78
+
79
+ if cache_control = config.cache_controls[extension]
80
+ file_options[:cache_control] = cache_control
81
+ end
82
+
83
+ if gzip_file?(filename)
84
+ file_options.delete(:file)
85
+ file_options[:content_encoding] = "gzip"
86
+ end
87
+
88
+ file_options
89
+ end
90
+
91
+ def gzip_file?(filename)
92
+ extension = File.extname(filename)
93
+ config.gzip_extensions.include?(extension)
94
+ end
95
+
96
+ def gzipped_file(filename)
97
+ shell.say_status("gzip", filename, :yellow)
98
+
99
+ io = StringIO.new("w")
100
+ gz = Zlib::GzipWriter.new(io)
101
+ gz.write(File.read(filename))
102
+ gz.close
103
+
104
+ io.string
105
+ end
106
+
107
+ def upload(filename)
108
+ return unless File.file?(filename)
109
+ return if FORBIDDEN_FILENAMES.include?(filename)
110
+
111
+ options = options_for_file(filename)
112
+
113
+ if verifier.stale?(filename, options)
114
+ params = [options_for_file(filename)]
115
+
116
+ if gzip_file?(filename)
117
+ data = gzipped_file(filename)
118
+ params.unshift(data)
119
+ end
120
+
121
+ shell.say_status("upload", filename, :green)
122
+ bucket.objects[filename].write(*params)
123
+ mark_to_invalidate(filename)
124
+ else
125
+ shell.say_status("identical", filename, :blue)
126
+ end
127
+ end
128
+
129
+ def perform_invalidation
130
+ return unless config.distribution_id
131
+ return if invalidations.empty?
132
+
133
+ cf.client.create_invalidation(
134
+ :distribution_id => config.distribution_id,
135
+ :invalidation_batch => {
136
+ :paths => {
137
+ :quantity => invalidations.count,
138
+ :items => invalidations.to_a
139
+ },
140
+ :caller_reference => SecureRandom.uuid
141
+ }
142
+ )
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,33 @@
1
+ require "upstatic/tool"
2
+
3
+ module Upstatic
4
+ class Manager < Tool
5
+
6
+ def create_bucket
7
+ unless bucket.exists?
8
+ shell.say_status "bucket", "creating '#{config.bucket}'"
9
+ s3.buckets.create(config.bucket)
10
+ end
11
+ end
12
+
13
+ def configure_bucket
14
+ unless bucket.website_configuration
15
+ shell.say_status "bucket", "configuring for website hosting"
16
+ bucket.configure_website
17
+ end
18
+ rescue AWS::S3::Errors::NoSuchBucket
19
+ # Can't find the newly created bucket yet. Let's wait a bit and retry.
20
+ sleep(2)
21
+ retry
22
+ rescue AWS::S3::Errors::AccessDenied
23
+ abort "We could not access the bucket '#{config.bucket}'.\n\n" <<
24
+ "One of two things happened here:\n\n" <<
25
+ "- You're trying to access a bucket that another user already\n" <<
26
+ " owns. Please note that the bucket namespace is shared by\n" <<
27
+ " all users of the system.\n" <<
28
+ "- You actually own this bucket on some other account. If that\n" <<
29
+ " is the case, please check your credentials."
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,44 @@
1
+ module Upstatic
2
+
3
+ # The +Options+ module can be mixed-in with any other class to provide some
4
+ # nice helpers for adding options with default values.
5
+ #
6
+ # This is used internally in +Configuration+:
7
+ #
8
+ # class Configuration
9
+ # include Options
10
+ # option :bucket
11
+ # end
12
+ module Options
13
+
14
+ def self.included(base)
15
+ base.extend(ClassMethods)
16
+ end
17
+
18
+ module ClassMethods
19
+
20
+ # Allows the definition of options in a similar way to +attr_accessor+,
21
+ # except that the same method acts as getter and setter, depending on
22
+ # whether an argument is passed (setter) or not (getter).
23
+ #
24
+ # An optional hash can be passed as a second argument, to specify
25
+ # additional options such as a default value using +:default+.
26
+ def option(name, options={})
27
+ define_method(name) do |*args|
28
+ if args.size > 0
29
+ instance_variable_set("@#{name}", args.first)
30
+ else
31
+ if instance_variable_defined?("@#{name}")
32
+ instance_variable_get("@#{name}")
33
+ elsif options.keys.include?(:default)
34
+ options[:default]
35
+ else
36
+ nil
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,7 @@
1
+ module Upstatic
2
+ class QuietShell
3
+ def say_status(*args)
4
+ # Shh... Let's be quiet.
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ require "thor/shell"
2
+
3
+ module Upstatic
4
+ class Shell
5
+ def initialize
6
+ @shell = Thor::Base.shell.new
7
+ end
8
+
9
+ def say_status(status, message, log_status=nil)
10
+ @shell.say_status(status, message, log_status)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,40 @@
1
+ require "aws-sdk"
2
+ require "upstatic/configuration"
3
+
4
+ module Upstatic
5
+ class Tool
6
+
7
+ def initialize(configuration)
8
+ @configuration = configuration
9
+ end
10
+
11
+ private
12
+
13
+ def config
14
+ @configuration
15
+ end
16
+
17
+ def s3
18
+ @s3 ||= AWS::S3.new(
19
+ :access_key_id => config.access_key_id,
20
+ :secret_access_key => config.secret_access_key,
21
+ :region => config.region
22
+ )
23
+ end
24
+
25
+ def bucket
26
+ @bucket ||= s3.buckets[config.bucket]
27
+ end
28
+
29
+ def cf
30
+ @cf ||= AWS::CloudFront.new(
31
+ :access_key_id => config.access_key_id,
32
+ :secret_access_key => config.secret_access_key
33
+ )
34
+ end
35
+
36
+ def shell
37
+ Upstatic.shell
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,59 @@
1
+ require "digest/sha1"
2
+ require "upstatic/tool"
3
+
4
+ module Upstatic
5
+ class Verifier < Tool
6
+
7
+ def initialize(configuration)
8
+ super
9
+
10
+ @files = {}
11
+ @file_options = {}
12
+
13
+ @new_files = {}
14
+ @new_file_options = {}
15
+
16
+ bucket.objects[".sha1sums"].read.lines.each do |line|
17
+ file_checksum, file_options_checksum, filename = line.split(" ", 3)
18
+ filename.chomp!
19
+ @files[filename] = file_checksum
20
+ @file_options[filename] = file_options_checksum
21
+ end
22
+ rescue AWS::S3::Errors::NoSuchKey
23
+ # Checksum file doesn't exist.
24
+ # Both @files and @file_options will be empty, and that's ok.
25
+ @has_no_data = true
26
+ end
27
+
28
+ def stale?(filename, write_options)
29
+ return true if config.force_deploy
30
+
31
+ file_options_checksum = Digest::SHA1.hexdigest(Marshal.dump(write_options))
32
+ file_checksum = Digest::SHA1.file(filename).hexdigest
33
+
34
+ @new_files[filename] = file_checksum
35
+ @new_file_options[filename] = file_options_checksum
36
+
37
+ if @files[filename] != file_checksum
38
+ return true
39
+ end
40
+
41
+ if @file_options[filename] != file_options_checksum
42
+ return true
43
+ end
44
+
45
+ return false
46
+ end
47
+
48
+ def write_checksum_file
49
+ lines = []
50
+ @new_files.each do |filename, checksum|
51
+ file_option_checksum = @new_file_options[filename]
52
+ lines << "#{checksum} #{file_option_checksum} #{filename}"
53
+ end
54
+
55
+ shell.say_status("upload", ".sha1sums", :green)
56
+ bucket.objects[".sha1sums"].write(lines.join("\n"))
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module Upstatic
2
+ VERSION = "0.1.0.pre"
3
+ end
@@ -0,0 +1,2 @@
1
+ require "minitest_helper"
2
+ require "cases/test_case"
@@ -0,0 +1,39 @@
1
+ require "cases/helper"
2
+
3
+ class TestCacheControl < Upstatic::TestCase
4
+
5
+ def self.config
6
+ return @config if @config
7
+
8
+ @config = Upstatic::Configuration.new
9
+ @config.default_cache_control "public, no-cache"
10
+ @config.cache_control ".css", "public, max-age=3600"
11
+ @config.cache_control ".js", "public, max-age=0"
12
+ @config
13
+ end
14
+
15
+ def config
16
+ self.class.config
17
+ end
18
+
19
+ def setup
20
+ super
21
+ deploy :cache_control, config
22
+ end
23
+
24
+ def test_sets_cache_control_for_css_file
25
+ response = request("styles.css")
26
+ assert_equal "public, max-age=3600", response["Cache-Control"]
27
+ end
28
+
29
+ def test_sets_cache_control_for_css_file
30
+ response = request("scripts.js")
31
+ assert_equal "public, max-age=0", response["Cache-Control"]
32
+ end
33
+
34
+ def test_sets_default_cache_control_for_other_files
35
+ response = request("index.html")
36
+ assert_equal "public, no-cache", response["Cache-Control"]
37
+ end
38
+
39
+ end
@@ -0,0 +1,76 @@
1
+ require "minitest_helper"
2
+
3
+ module Upstatic
4
+ class TestCase < Minitest::Test
5
+ REGION = "eu-west-1"
6
+ ORIG_UPLOAD_DIR = File.expand_path("../../upload_dir", __FILE__)
7
+ TEST_UPLOAD_DIR = File.expand_path("../../upload_dir_test", __FILE__)
8
+
9
+ def setup
10
+ FileUtils.rm_rf(TEST_UPLOAD_DIR)
11
+ FileUtils.cp_r(ORIG_UPLOAD_DIR, TEST_UPLOAD_DIR)
12
+ end
13
+
14
+ def teardown
15
+ FileUtils.rm_rf(TEST_UPLOAD_DIR)
16
+ end
17
+
18
+ def request(path, headers={})
19
+ uri = URI("http://#{@@last_deployment}.s3-website-#{REGION}.amazonaws.com/#{path}")
20
+ req = Net::HTTP::Get.new(uri.path)
21
+ headers.each { |k,v| req[k] = v }
22
+ Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
23
+ end
24
+
25
+ def deployments
26
+ @@deployments ||= {}
27
+ end
28
+
29
+ def deploy(identifier, configuration, force=false)
30
+ deployment = deployments[identifier]
31
+
32
+ if !deployment || force
33
+ unless configuration.bucket
34
+ configuration.bucket "upstatic-test-#{SecureRandom.uuid}"
35
+ end
36
+
37
+ configuration.upload_dir TEST_UPLOAD_DIR
38
+ configuration.region REGION
39
+
40
+ Upstatic::Deployer.new(configuration).deploy!
41
+ @@last_deployment = configuration.bucket
42
+ deployments[identifier] = configuration.bucket
43
+ else
44
+ deployment
45
+ end
46
+ end
47
+
48
+ def write_file(path, content)
49
+ File.open(path, "w") { |f| f.write(content) }
50
+ end
51
+
52
+ def assert_gzipped(string)
53
+ msg = "Expected string to be gzipped"
54
+ assert string.bytes.first(2) == [0x1f, 0x8b], msg
55
+ end
56
+
57
+ def refute_gzipped(string)
58
+ msg = "Expected string to not be gzipped"
59
+ assert string.bytes.first(2) != [0x1f, 0x8b], msg
60
+ end
61
+
62
+ def assert_request_count(method, expected_count, &block)
63
+ request_count = 0
64
+
65
+ WebMock.after_request do |request_signature, response|
66
+ request_count += 1 if request_signature.method == method
67
+ end
68
+
69
+ block.call
70
+
71
+ WebMock.reset_callbacks
72
+
73
+ assert_equal expected_count, request_count
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,61 @@
1
+ require "cases/helper"
2
+
3
+ class TestDeployment < Upstatic::TestCase
4
+
5
+ def self.config
6
+ return @config if @config
7
+ @config = Upstatic::Configuration.new
8
+ end
9
+
10
+ def config
11
+ self.class.config
12
+ end
13
+
14
+ def setup
15
+ super
16
+ deploy :default, config
17
+ end
18
+
19
+ def test_deploys_html_file
20
+ response = request("index.html")
21
+ assert_equal "200", response.code
22
+ assert_equal "UPSTATIC_HTML", response.body.strip
23
+ assert_equal "text/html", response["Content-Type"]
24
+ assert_equal "public, max-age=691200", response["Cache-Control"]
25
+ end
26
+
27
+ def test_deploys_css_file
28
+ response = request("styles.css")
29
+ assert_equal "200", response.code
30
+ assert_equal "UPSTATIC_CSS", response.body.strip
31
+ assert_equal "text/css", response["Content-Type"]
32
+ assert_equal "public, max-age=691200", response["Cache-Control"]
33
+ end
34
+
35
+ def test_deploys_javascript_file
36
+ response = request("scripts.js")
37
+ assert_equal "200", response.code
38
+ assert_equal "UPSTATIC_JS", response.body.strip
39
+ assert_equal "application/javascript", response["Content-Type"]
40
+ assert_equal "public, max-age=691200", response["Cache-Control"]
41
+ end
42
+
43
+ def test_deploys_folder
44
+ response = request("folder/index.html")
45
+ assert_equal "200", response.code
46
+ assert_equal "UPSTATIC_FOLDER", response.body.strip
47
+ assert_equal "text/html", response["Content-Type"]
48
+ assert_equal "public, max-age=691200", response["Cache-Control"]
49
+ end
50
+
51
+ def test_does_not_make_checksum_file_public
52
+ response = request(".sha1sums")
53
+ assert_equal "403", response.code
54
+ end
55
+
56
+ def test_does_not_deploy_config_file
57
+ response = request("Upstatic")
58
+ assert_equal "403", response.code
59
+ end
60
+
61
+ end
@@ -0,0 +1,56 @@
1
+ require "cases/helper"
2
+
3
+ class TestGzip < Upstatic::TestCase
4
+
5
+ def self.config
6
+ return @config if @config
7
+
8
+ @config = Upstatic::Configuration.new
9
+ @config.gzip_extensions ".html", ".css"
10
+ @config
11
+ end
12
+
13
+ def config
14
+ self.class.config
15
+ end
16
+
17
+ def request(uri, headers={})
18
+ headers.merge!("Accept-Encoding" => "identity")
19
+ super
20
+ end
21
+
22
+ def setup
23
+ super
24
+ deploy :gzip, config
25
+ end
26
+
27
+ def test_gzips_specified_files
28
+ response = request("index.html")
29
+ assert_gzipped response.body
30
+ assert_equal "gzip", response["Content-Encoding"]
31
+
32
+ response = request("folder/index.html")
33
+ assert_gzipped response.body
34
+ assert_equal "gzip", response["Content-Encoding"]
35
+
36
+ response = request("styles.css")
37
+ assert_gzipped response.body
38
+ assert_equal "gzip", response["Content-Encoding"]
39
+ end
40
+
41
+ def test_does_not_gzip_other_files
42
+ response = request("scripts.js")
43
+ refute_gzipped response.body
44
+ assert_nil response["Content-Encoding"]
45
+ end
46
+
47
+ def test_compresses_content_correctly
48
+ response = request("index.html")
49
+
50
+ io = StringIO.new(response.body)
51
+ gz = Zlib::GzipReader.new(io)
52
+
53
+ assert_equal "UPSTATIC_HTML", gz.read.strip
54
+ end
55
+
56
+ end
@@ -0,0 +1,64 @@
1
+ require "cases/helper"
2
+
3
+ class TestInvalidations < Upstatic::TestCase
4
+
5
+ def self.config
6
+ return @config if @config
7
+ @config = Upstatic::Configuration.new
8
+ @config.distribution_id "EX7C5EKLRKVC7"
9
+ @config
10
+ end
11
+
12
+ def config
13
+ self.class.config
14
+ end
15
+
16
+ def last_invalidation_id
17
+ AWS::CloudFront.new.client.list_invalidations(
18
+ :distribution_id => config.distribution_id,
19
+ :max_items => 1
20
+ )[:items][0][:id]
21
+ end
22
+
23
+ def last_invalidation
24
+ AWS::CloudFront.new.client.get_invalidation(
25
+ :distribution_id => config.distribution_id,
26
+ :id => last_invalidation_id
27
+ )
28
+ end
29
+
30
+ def last_invalidated_items
31
+ last_invalidation[:invalidation_batch][:paths][:items]
32
+ end
33
+
34
+ def test_creates_invalidation_rules
35
+ deploy :invalidations, config
36
+
37
+ paths = last_invalidated_items
38
+
39
+ assert_equal 7, paths.count
40
+
41
+ assert_includes paths, "/"
42
+ assert_includes paths, "/index.html"
43
+ assert_includes paths, "/scripts.js"
44
+ assert_includes paths, "/styles.css"
45
+ assert_includes paths, "/folder"
46
+ assert_includes paths, "/folder/"
47
+ assert_includes paths, "/folder/index.html"
48
+
49
+ changed_file = File.join(TEST_UPLOAD_DIR, "scripts.js")
50
+ write_file(changed_file, "UPSTATIC_CHANGED")
51
+
52
+ new_config = config.dup
53
+ new_config.cache_control ".css", "private, max-age=0"
54
+
55
+ deploy :invalidations, new_config, true
56
+
57
+ paths = last_invalidated_items
58
+
59
+ assert_equal 2, paths.count
60
+ assert_includes paths, "/scripts.js"
61
+ assert_includes paths, "/styles.css"
62
+ end
63
+
64
+ end
@@ -0,0 +1,74 @@
1
+ require "cases/helper"
2
+
3
+ class TestMultipleDeployments < Upstatic::TestCase
4
+
5
+ def self.config
6
+ return @config if @config
7
+ @config = Upstatic::Configuration.new
8
+ end
9
+
10
+ def config
11
+ self.class.config
12
+ end
13
+
14
+ def setup
15
+ super
16
+ config.bucket nil
17
+ end
18
+
19
+ def test_do_not_upload_unchanged_files
20
+ deploy :multiple_unchanged, config
21
+
22
+ assert_request_count(:put, 1) do
23
+ deploy :multiple_unchanged, config, true
24
+ end
25
+ end
26
+
27
+ def test_upload_changed_files
28
+ deploy :multiple_changed, config
29
+
30
+ changed_file = File.join(TEST_UPLOAD_DIR, "folder/index.html")
31
+ write_file(changed_file, "UPSTATIC_CHANGED")
32
+
33
+ assert_request_count(:put, 2) do
34
+ deploy :multiple_changed, config, true
35
+ end
36
+
37
+ response = request("folder/index.html")
38
+ assert_equal "UPSTATIC_CHANGED", response.body.strip
39
+ end
40
+
41
+ def test_upload_added_files
42
+ deploy :multiple_added, config
43
+
44
+ new_file = File.join(TEST_UPLOAD_DIR, "new.html")
45
+ write_file(new_file, "UPSTATIC_NEW")
46
+
47
+ assert_request_count(:put, 2) do
48
+ deploy :multiple_added, config, true
49
+ end
50
+
51
+ response = request("new.html")
52
+ assert_equal "UPSTATIC_NEW", response.body.strip
53
+ end
54
+
55
+ def test_upload_modified_configuration_files
56
+ deploy :muliple_config, config
57
+
58
+ new_config = config.dup
59
+ new_config.gzip_extensions ".html"
60
+
61
+ assert_request_count(:put, 3) do
62
+ deploy :multiple_config, new_config, true
63
+ end
64
+
65
+ response = request("index.html", "Accept-Encoding" => "identity")
66
+ assert_gzipped response.body
67
+ assert_equal "gzip", response["Content-Encoding"]
68
+
69
+ response = request("folder/index.html", "Accept-Encoding" => "identity")
70
+ assert_gzipped response.body
71
+ assert_equal "gzip", response["Content-Encoding"]
72
+ end
73
+
74
+ end
@@ -0,0 +1,20 @@
1
+ require "simplecov"
2
+ SimpleCov.start do
3
+ add_filter "/test/"
4
+ add_filter "/lib/upstatic/compat.rb"
5
+ end
6
+
7
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
8
+ require "upstatic"
9
+
10
+ require "minitest/autorun"
11
+ require "webmock/minitest"
12
+ require "upstatic/quiet_shell"
13
+
14
+ Minitest.after_run do
15
+ AWS::S3.new.buckets.each { |b| b.delete! if b.name =~ /^upstatic-test-/ }
16
+ end
17
+
18
+ WebMock.allow_net_connect!
19
+
20
+ Upstatic.shell = Upstatic::QuietShell.new
@@ -0,0 +1,33 @@
1
+ require "minitest_helper"
2
+
3
+ class TestConfiguration < Minitest::Test
4
+
5
+ def setup
6
+ super
7
+ @config = Upstatic::Configuration.new
8
+ end
9
+
10
+ def test_reads_from_file
11
+ path = File.expand_path("../upload_dir/Upstatic", __FILE__)
12
+ config = Upstatic::Configuration.read(path)
13
+ assert_equal "upstatic", config.bucket
14
+ end
15
+
16
+ def test_cache_control_sanitizes_extensions
17
+ @config.cache_control "html", "public"
18
+ assert_equal "public", @config.cache_controls[".html"]
19
+ end
20
+
21
+ def test_cache_control_accepts_symbols_as_extensions
22
+ @config.cache_control :css, "public"
23
+ assert_equal "public", @config.cache_controls[".css"]
24
+ end
25
+
26
+ def test_gzip_extensions_sanitizes_extensions
27
+ @config.gzip_extensions :html, "css", ".js"
28
+ assert @config.gzip_extensions.include?(".html")
29
+ assert @config.gzip_extensions.include?(".css")
30
+ assert @config.gzip_extensions.include?(".js")
31
+ end
32
+
33
+ end
@@ -0,0 +1 @@
1
+ bucket "upstatic"
@@ -0,0 +1 @@
1
+ UPSTATIC_FOLDER
@@ -0,0 +1 @@
1
+ UPSTATIC_HTML
@@ -0,0 +1 @@
1
+ UPSTATIC_JS
@@ -0,0 +1 @@
1
+ UPSTATIC_CSS
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'upstatic/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "upstatic"
8
+ spec.version = Upstatic::VERSION
9
+ spec.authors = ["Joao Carlos"]
10
+ spec.email = ["mail@joao-carlos.com"]
11
+ spec.description = %q{Upstatic lets you deploy your static sites on AWS}
12
+ spec.summary = %q{Deploy static sites}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+
23
+ spec.add_dependency "aws-sdk"
24
+ spec.add_dependency "thor"
25
+ spec.add_dependency "mime-types"
26
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: upstatic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre
5
+ platform: ruby
6
+ authors:
7
+ - Joao Carlos
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: thor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mime-types
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Upstatic lets you deploy your static sites on AWS
70
+ email:
71
+ - mail@joao-carlos.com
72
+ executables:
73
+ - upstatic
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - bin/upstatic
83
+ - lib/upstatic.rb
84
+ - lib/upstatic/cli.rb
85
+ - lib/upstatic/compat.rb
86
+ - lib/upstatic/configuration.rb
87
+ - lib/upstatic/deployer.rb
88
+ - lib/upstatic/manager.rb
89
+ - lib/upstatic/options.rb
90
+ - lib/upstatic/quiet_shell.rb
91
+ - lib/upstatic/shell.rb
92
+ - lib/upstatic/tool.rb
93
+ - lib/upstatic/verifier.rb
94
+ - lib/upstatic/version.rb
95
+ - test/cases/helper.rb
96
+ - test/cases/test_cache_control.rb
97
+ - test/cases/test_case.rb
98
+ - test/cases/test_deployment.rb
99
+ - test/cases/test_gzip.rb
100
+ - test/cases/test_invalidations.rb
101
+ - test/cases/test_multiple_deployments.rb
102
+ - test/minitest_helper.rb
103
+ - test/test_configuration.rb
104
+ - test/upload_dir/Upstatic
105
+ - test/upload_dir/folder/index.html
106
+ - test/upload_dir/index.html
107
+ - test/upload_dir/scripts.js
108
+ - test/upload_dir/styles.css
109
+ - upstatic.gemspec
110
+ homepage: ''
111
+ licenses:
112
+ - MIT
113
+ metadata: {}
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">"
126
+ - !ruby/object:Gem::Version
127
+ version: 1.3.1
128
+ requirements: []
129
+ rubyforge_project:
130
+ rubygems_version: 2.2.2
131
+ signing_key:
132
+ specification_version: 4
133
+ summary: Deploy static sites
134
+ test_files:
135
+ - test/cases/helper.rb
136
+ - test/cases/test_cache_control.rb
137
+ - test/cases/test_case.rb
138
+ - test/cases/test_deployment.rb
139
+ - test/cases/test_gzip.rb
140
+ - test/cases/test_invalidations.rb
141
+ - test/cases/test_multiple_deployments.rb
142
+ - test/minitest_helper.rb
143
+ - test/test_configuration.rb
144
+ - test/upload_dir/Upstatic
145
+ - test/upload_dir/folder/index.html
146
+ - test/upload_dir/index.html
147
+ - test/upload_dir/scripts.js
148
+ - test/upload_dir/styles.css
149
+ has_rdoc: