upstatic 0.1.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: