├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── netlify.rb └── netlify │ ├── access_token.rb │ ├── access_tokens.rb │ ├── client.rb │ ├── collection_proxy.rb │ ├── deploy.rb │ ├── deploy_key.rb │ ├── deploy_keys.rb │ ├── deploys.rb │ ├── dns_record.rb │ ├── dns_records.rb │ ├── dns_zone.rb │ ├── dns_zones.rb │ ├── file.rb │ ├── files.rb │ ├── form.rb │ ├── forms.rb │ ├── model.rb │ ├── multipass.rb │ ├── site.rb │ ├── sites.rb │ ├── snippet.rb │ ├── snippets.rb │ ├── submission.rb │ ├── submissions.rb │ ├── user.rb │ ├── users.rb │ └── version.rb ├── netlify.gemspec └── test ├── client_test.rb ├── files ├── site-dir.zip └── site-dir │ └── index.html ├── multipass_test.rb ├── sites_test.rb └── test_helper.rb /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in Netlify.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | netlify (0.2.2) 5 | github_api 6 | highline 7 | oauth2 (>= 0.9.2) 8 | slop 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | addressable (2.3.3) 14 | crack (0.3.2) 15 | descendants_tracker (0.0.4) 16 | thread_safe (~> 0.3, >= 0.3.1) 17 | faraday (0.9.2) 18 | multipart-post (>= 1.2, < 3) 19 | github_api (0.12.2) 20 | addressable (~> 2.3) 21 | descendants_tracker (~> 0.0.4) 22 | faraday (~> 0.8, < 0.10) 23 | hashie (>= 3.3) 24 | multi_json (>= 1.7.5, < 2.0) 25 | nokogiri (~> 1.6.3) 26 | oauth2 27 | hashie (3.4.3) 28 | highline (1.7.8) 29 | jwt (1.5.2) 30 | mini_portile2 (2.1.0) 31 | minitest (5.0.8) 32 | multi_json (1.12.1) 33 | multi_xml (0.5.5) 34 | multipart-post (2.0.0) 35 | nokogiri (1.6.8) 36 | mini_portile2 (~> 2.1.0) 37 | pkg-config (~> 1.1.7) 38 | oauth2 (1.0.0) 39 | faraday (>= 0.8, < 0.10) 40 | jwt (~> 1.0) 41 | multi_json (~> 1.3) 42 | multi_xml (~> 0.5) 43 | rack (~> 1.2) 44 | pkg-config (1.1.7) 45 | rack (1.6.4) 46 | slop (4.2.0) 47 | thread_safe (0.3.5) 48 | webmock (1.11.0) 49 | addressable (>= 2.2.7) 50 | crack (>= 0.3.2) 51 | 52 | PLATFORMS 53 | ruby 54 | 55 | DEPENDENCIES 56 | minitest 57 | netlify! 58 | webmock 59 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Mathias Biilmann Christensen 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This code is deprecated. Our current API interface can be found here: https://github.com/netlify/open-api/ 2 | 3 | ## Netlify Ruby Client 4 | ====================== 5 | 6 | Netlify is a hosting service for the programmable web. It understands your documents, processes forms and lets you do deploys, manage forms submissions, inject javascript snippets into sites and do intelligent updates of HTML documents through it's API. 7 | 8 | The basic flow to using the ruby client is: 9 | 10 | 1. Authenticate (via credentials or a previously aquired access token) 11 | 2. Get site (via id) 12 | 3. Deploy 13 | * If site has not been deployed to yet, then the above step will throw a `not found` exception, and you'll need to use `Netlify.sites.create` to create the site and do the initial deploy. 14 | * If the site has already been deployed and the above step was successful, you can simply use `site.update` to re-deploy. 15 | 16 | If you'd rather, there's also a command line utility to handle most of these steps: `Netlify deploy`. 17 | 18 | ## Installation 19 | ============ 20 | 21 | Install the gem by running 22 | 23 | gem install netlify 24 | 25 | or put it in a Gemfile and run `bundle install` 26 | 27 | gem netlify 28 | 29 | 30 | ## Authenticating 31 | ============== 32 | 33 | Register a new application at https://app.netlify.com/applications to get your Oauth2 secret and key. 34 | 35 | Once you have your credentials you can instantiate a Netlify client. 36 | 37 | ```ruby 38 | Netlify = Netlify::Client.new(:client_id => "YOUR_API_KEY", :client_secret => "YOUR_API_SECRET") 39 | ``` 40 | 41 | Before you can make any requests to the API, you'll need to authenticate with OAuth2. The Netlify client supports two OAuth2 flows. 42 | 43 | If you're authenticating on behalf of a user, you'll need to get a valid access token for that user. Use the Netlify client to request an authentication URL: 44 | 45 | ```ruby 46 | url = Netlify.authorize_url(:redirect_uri => "http://www.example.com/callback") 47 | ``` 48 | 49 | The user then visits that URL and will be prompted to authorize your application to access his Netlify sites. If she grants permission, she'll be redirected back to the `redirect_uri` provided in the `authorize_url` call. This URL must match the redirect url configured for your Netlify application. Once the user comes back to your app, you'll be able to access a `code` query parameter that gives you an authorization code. Use this to finish the OAuth2 flow: 50 | 51 | ```ruby 52 | Netlify.authorize!(token, :redirect_uri => "http://www.example.com/callback") 53 | ``` 54 | 55 | If you're not authenticating on behalf of a user you can authorize directly with the API credentials. Just call: 56 | 57 | ```ruby 58 | Netlify.authorize_from_credentials! 59 | ``` 60 | 61 | If you already have an OAuth2 `access_token` you can instantiate the client like this: 62 | 63 | ```ruby 64 | Netlify = Netlify::Client.new(:access_token => access_token) 65 | ``` 66 | 67 | And the client will be ready to do requests without having to use `authorize_from_credentials`. This means that once you've gotten a token via `authorize_from_credentials!` you can store it and reuse it for later sessions. 68 | 69 | If you're authenticating via the `access_token` and you'd like to test if you have a valid `access_token`, you can attempt to make a request with the Netlify client and if the token is invalid, a `Netlify::Client::AuthenticationError` will be raised. See Miles Matthias' [Netlify Rakefile](https://github.com/milesmatthias/Netlify-rakefile) for an example. 70 | 71 | 72 | ## Sites 73 | ===== 74 | 75 | Getting a list of all sites you have access to: 76 | 77 | ```ruby 78 | Netlify.sites.each do |site| 79 | puts site.id 80 | puts site.url 81 | end 82 | ``` 83 | 84 | Each site has a unique, system generated id. Getting a specific site by id: 85 | 86 | ```ruby 87 | site = Netlify.sites.get(id) 88 | ``` 89 | 90 | Creating a site from a directory: _(note the path given is a system path)_ 91 | 92 | ```ruby 93 | site = Netlify.sites.create(:dir => "my-site") 94 | puts site.id 95 | ``` 96 | 97 | You'll want to then save that site id for future reference. Note that a site can also be looked up by its `url`. 98 | 99 | Creating a site from a zip file: 100 | 101 | ```ruby 102 | site = Netlify.sites.create(:zip => "/tmp/my-site.zip") 103 | ``` 104 | 105 | Both methods will create the site and upload the files to a new deploy. 106 | 107 | Creating a site with a dir or a zip is actually a shortcut for this: 108 | 109 | ```ruby 110 | site = Netlify.sites.create(:name => "unique-site-subdomain", :custom_domain => "www.example.com") 111 | deploy = site.deploys.create(:dir => "path/to/my-site") 112 | ``` 113 | 114 | Use `wait_for_ready` to wait until a site has finished processing. 115 | 116 | ```ruby 117 | site = Netlify.sites.create(:dir => "/tmp/my-site") 118 | site.wait_for_ready 119 | site.state == "ready" 120 | ``` 121 | 122 | This also works on a specific deploy, and you can pass in a block to execute after each polling action: 123 | 124 | ```ruby 125 | deploy = site.deploys.create(:dir => "/tmp/my-site") 126 | deploy.wait_for_ready do |deploy| 127 | puts "Current state: #{deploy.state}" 128 | end 129 | ``` 130 | 131 | Redeploy a site from a dir: 132 | 133 | ```ruby 134 | site = Netlify.sites.get(site_id) 135 | deploy = site.deploys.create(:dir => "/tmp/my-site") 136 | deploy.wait_for_ready 137 | ``` 138 | 139 | Redeploy a site from a zip file: 140 | 141 | ```ruby 142 | site = Netlify.sites.get(site_id) 143 | deploy = site.deploys.create(:zip => "/tmp/my-site.zip") 144 | deploy.wait_for_ready 145 | ``` 146 | 147 | Update the name of the site (its subdomain), the custom domain and the notification email for form submissions: 148 | 149 | ```ruby 150 | site.update(:name => "my-site", :custom_domain => "www.example.com", :notification_email => "me@example.com", :password => "secret-password") 151 | ``` 152 | 153 | Deleting a site: 154 | 155 | ```ruby 156 | site.destroy! 157 | ``` 158 | 159 | ## Deploys 160 | ======= 161 | 162 | Access all deploys for a site 163 | 164 | ```ruby 165 | site = Netlify.sites.get(site_id) 166 | site.deploys.all 167 | ``` 168 | 169 | Access a specific deploy 170 | 171 | ```ruby 172 | site = Netlify.sites.get(site_id) 173 | deploy = site.deploys.get(id) 174 | ``` 175 | 176 | Publish a deploy (makes it the current live version of the site) 177 | 178 | ```ruby 179 | site.deploys.get(id).publish 180 | ``` 181 | 182 | Create a new deploy 183 | 184 | ```ruby 185 | deploy = site.deploys.create(:dir => "/tmp/my-site") 186 | ``` 187 | 188 | Create a draft deploy 189 | 190 | ```ruby 191 | deploy = site.deploys.draft(:dir => "/tmp/my-site") 192 | ``` 193 | 194 | Or 195 | 196 | ```ruby 197 | deploy = site.deploys.create(:dir => "/tmp/my-site", :draft => true) 198 | ``` 199 | 200 | This will upload and process a deploy. You can view the deploy at `deploy.deploy_url` and make it the live version of the site with `deploy.publish`. 201 | 202 | ## Continuous Deployment 203 | ===================== 204 | 205 | You can also configure continuous deployment for a new or existing site from the Ruby client. 206 | 207 | You'll need a Github access token (this will never get sent to Netlify) in addition to your Netlify token. Make sure this access token have the permission to add a deploy key and a web hook to your repository. 208 | 209 | Create a new site with continuous deployment: 210 | 211 | ```ruby 212 | client.sites.create(:github => { 213 | :repo => "netlify/example", 214 | :dir => "build", 215 | :cmd => "middleman build", 216 | :access_token => GITHUB_ACCESS_TOKEN 217 | }) 218 | ``` 219 | 220 | Configure continuous deployment for an existing site: 221 | 222 | ```ruby 223 | site.configure_github!( 224 | :repo => "netlify/example", 225 | :dir => "build", 226 | :cmd => "middleman build", 227 | :access_token => GITHUB_ACCESS_TOKEN 228 | ) 229 | ``` 230 | 231 | Forms 232 | ===== 233 | 234 | Access all forms you have access to: 235 | 236 | ```ruby 237 | Netlify.forms.all 238 | ``` 239 | 240 | Access forms for a specific site: 241 | 242 | ```ruby 243 | site = Netlify.sites.get(id) 244 | site.forms.all 245 | ``` 246 | 247 | Access a specific form: 248 | 249 | ```ruby 250 | form = Netlify.forms.get(id) 251 | ``` 252 | 253 | Access a list of all form submissions you have access to: 254 | 255 | ```ruby 256 | Netlify.submissions.all 257 | ``` 258 | 259 | Access submissions from a specific site 260 | 261 | ```ruby 262 | site = Netlify.sites.get(id) 263 | site.submissions.all 264 | ``` 265 | 266 | Access submissions from a specific form 267 | 268 | ```ruby 269 | form = Netlify.forms.get(id) 270 | form.submissions.all 271 | ``` 272 | 273 | Get a specific submission 274 | 275 | ```ruby 276 | Netlify.submissions.get(id) 277 | ``` 278 | 279 | Files 280 | ===== 281 | 282 | Access all files in a site: 283 | 284 | ```ruby 285 | site = Netlify.sites.get(id) 286 | site.files.all 287 | ``` 288 | 289 | Get a specific file: 290 | 291 | ```ruby 292 | file = site.files.get(path) # Example paths: "/css/main.css", "/index.html" 293 | ``` 294 | 295 | Reading a file: 296 | 297 | ```ruby 298 | file.read 299 | ``` 300 | 301 | ## Snippets 302 | ======== 303 | 304 | Snippets are small code snippets injected into all HTML pages of a site right before the closing head or body tag. To get all snippets for a site: 305 | 306 | ```ruby 307 | site = Netlify.sites.get(id) 308 | site.snippets.all 309 | ``` 310 | 311 | Get a specific snippet 312 | 313 | ```ruby 314 | site.snippets.get(0) 315 | ``` 316 | 317 | Add a snippet to a site. 318 | 319 | You can specify a `general` snippet that will be inserted into all pages, and a `goal` snippet that will be injected into a page following a successful form submission. Each snippet must have a title. You can optionally set the position of both the general and the goal snippet to `head` or `footer` to determine if it gets injected into the head tag or at the end of the page. 320 | 321 | ```ruby 322 | site.snippets.create( 323 | :general => general_snippet, 324 | :general_position => "footer", 325 | :goal => goal_snippet, 326 | :goal_position => "head", 327 | :title => "My Snippet" 328 | ) 329 | ``` 330 | 331 | Update a snippet 332 | 333 | ```ruby 334 | site.snippets.get(id).update( 335 | :general => general_snippet, 336 | :general_position => "footer", 337 | :goal => goal_snippet, 338 | :goal_position => "head", 339 | :title => "My Snippet" 340 | ) 341 | ``` 342 | 343 | Remove a snippet 344 | 345 | ```ruby 346 | site.snippet.get(id).destroy 347 | end 348 | ``` 349 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rake/testtask' 3 | 4 | Rake::TestTask.new do |t| 5 | t.libs << 'test' 6 | t.pattern = "test/*_test.rb" 7 | end -------------------------------------------------------------------------------- /lib/netlify.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "netlify/version" 3 | require "netlify/client" 4 | require "netlify/collection_proxy" 5 | require "netlify/model" 6 | require "netlify/sites" 7 | require "netlify/forms" 8 | require "netlify/submissions" 9 | require "netlify/files" 10 | require "netlify/snippets" 11 | require "netlify/users" 12 | require "netlify/deploys" 13 | require "netlify/deploy_keys" 14 | require "netlify/dns_zones" 15 | require "netlify/access_tokens" 16 | require "netlify/multipass" 17 | 18 | module Netlify; end 19 | -------------------------------------------------------------------------------- /lib/netlify/access_token.rb: -------------------------------------------------------------------------------- 1 | module Netlify 2 | class AccessToken < Model 3 | fields :id, :access_token, :user_id, :created_at 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/netlify/access_tokens.rb: -------------------------------------------------------------------------------- 1 | require "netlify/access_token" 2 | 3 | module Netlify 4 | class AccessTokens < CollectionProxy 5 | path "/access_tokens" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/netlify/client.rb: -------------------------------------------------------------------------------- 1 | require 'oauth2' 2 | 3 | module Netlify 4 | class Client 5 | ENDPOINT = ENV['OAUTH_CLIENT_API_URL'] || 'https://api.netlify.com' 6 | API_VERSION = "v1" 7 | RETRIES = 3 8 | 9 | class NetlifyError < StandardError; end 10 | class NotFoundError < NetlifyError; end 11 | class ConnectionError < NetlifyError; end 12 | class InternalServerError < NetlifyError; end 13 | class AuthenticationError < NetlifyError; end 14 | 15 | attr_accessor :client_id, :client_secret, :oauth, :access_token, :endpoint 16 | 17 | def initialize(options) 18 | self.client_id = options[:client_id] 19 | self.client_secret = options[:client_secret] 20 | self.access_token = options[:access_token] 21 | self.endpoint = options[:endpoint] || ENDPOINT 22 | self.oauth = OAuth2::Client.new(client_id, client_secret, :site => endpoint, :connection_build => lambda {|f| 23 | f.request :multipart 24 | f.request :url_encoded 25 | f.adapter :net_http 26 | }) 27 | end 28 | 29 | def authorize_url(options) 30 | oauth.auth_code.authorize_url(options) 31 | end 32 | 33 | def authorize_from_code!(authorization_code, options) 34 | @oauth_token = oauth.auth_code.get_token(authorization_code, options) 35 | self.access_token = oauth_token.token 36 | end 37 | 38 | def authorize_from_credentials! 39 | @oauth_token = oauth.client_credentials.get_token 40 | self.access_token = oauth_token.token 41 | end 42 | 43 | def sites 44 | Sites.new(self) 45 | end 46 | 47 | def forms 48 | Forms.new(self) 49 | end 50 | 51 | def submissions 52 | Submissions.new(self) 53 | end 54 | 55 | def deploy_keys 56 | DeployKeys.new(self) 57 | end 58 | 59 | def users 60 | Users.new(self) 61 | end 62 | 63 | def dns_zones 64 | DnsZones.new(self) 65 | end 66 | 67 | def access_tokens 68 | AccessTokens.new(self) 69 | end 70 | 71 | def request(verb, path, opts={}, &block) 72 | retries = 0 73 | begin 74 | raise AuthenticationError, "Authorize with Netlify before making requests" unless oauth_token 75 | 76 | ssl_options = {:version => :TLSv1_2} 77 | oauth_token.request(verb, ::File.join("/api", API_VERSION, path), ssl_options.merge(opts), &block) 78 | rescue OAuth2::Error => e 79 | case e.response.status 80 | when 401 81 | raise AuthenticationError, message_for(e, "Authentication Error") 82 | when 404 83 | raise NotFoundError, message_for(e, "Not Found") 84 | when 500 85 | if retry_request?(verb, e.response.status, retries) 86 | retries += 1 87 | retry 88 | else 89 | raise InternalServerError, message_for(e, "Internal Server Error") 90 | end 91 | else 92 | raise NetlifyError, message_for(e, "OAuth2 Error") 93 | end 94 | rescue Faraday::Error::ConnectionFailed, Faraday::Error::TimeoutError => e 95 | if retry_request?(verb, e.response && e.response.status, retries) 96 | retries += 1 97 | retry 98 | else 99 | raise ConnectionError, message_for(e, "Connection Error") 100 | end 101 | end 102 | end 103 | 104 | private 105 | def retry_request?(http_verb, status_code, retries) 106 | return false unless [:get, :put, :delete, :head].include?(http_verb.to_s.downcase.to_sym) 107 | return retries < 3 unless status_code && status_code == 422 108 | end 109 | 110 | def oauth_token 111 | @oauth_token ||= access_token && OAuth2::AccessToken.new(oauth, access_token) 112 | end 113 | 114 | def message_for(error, default) 115 | error.message.strip == "" ? default : error.message 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/netlify/collection_proxy.rb: -------------------------------------------------------------------------------- 1 | module Netlify 2 | class CollectionProxy 3 | include Enumerable 4 | 5 | attr_accessor :client, :prefix 6 | 7 | def self.path(value = nil) 8 | return @path unless value 9 | @path = value 10 | end 11 | 12 | def self.model(value = nil) 13 | @model ||= Netlify.const_get(to_s.split("::").last.sub(/s$/, '')) 14 | end 15 | 16 | def initialize(client, prefix = nil) 17 | self.client = client 18 | self.prefix = prefix 19 | end 20 | 21 | def all(options = {}) 22 | response = client.request(:get, path, {:params => options}) 23 | response.parsed.map {|attributes| model.new(client, attributes.merge(:prefix => prefix)) } if response.parsed 24 | end 25 | 26 | def each(&block) 27 | all.each(&block) 28 | end 29 | 30 | def get(id) 31 | response = client.request(:get, ::File.join(path, id)) 32 | model.new(client, response.parsed.merge(:prefix => prefix)) if response.parsed 33 | end 34 | 35 | def create(attributes) 36 | response = client.request(:post, path, :body => attributes) 37 | model.new(client, response.parsed.merge(:prefix => prefix)) if response.parsed 38 | end 39 | 40 | def model 41 | self.class.model 42 | end 43 | 44 | def path 45 | [prefix, self.class.path].compact.join("/") 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/netlify/deploy.rb: -------------------------------------------------------------------------------- 1 | module Netlify 2 | class Deploy < Model 3 | fields :id, :state, :premium, :claimed, :name, :custom_domain, :url, 4 | :admin_url, :deploy_url, :screenshot_url, :created_at, :updated_at, 5 | :user_id, :required 6 | 7 | def upload_dir(dir) 8 | return unless state == "uploading" 9 | 10 | shas = Hash.new { [] } 11 | glob = ::File.join(dir, "**", "*") 12 | 13 | Dir.glob(glob) do |file| 14 | next unless ::File.file?(file) 15 | pathname = ::File.join("/", file[dir.length..-1]) 16 | next if pathname.match(/(^\/?__MACOSX\/|\/\.)/) 17 | sha = Digest::SHA1.hexdigest(::File.read(file)) 18 | shas[sha] = shas[sha] + [pathname] 19 | end 20 | 21 | (required || []).each do |sha| 22 | shas[sha].each do |pathname| 23 | client.request(:put, ::File.join(path, "files", URI.encode(pathname)), :body => ::File.read(::File.join(dir, pathname)), :headers => {"Content-Type" => "application/octet-stream"}) 24 | end 25 | end 26 | 27 | refresh 28 | end 29 | 30 | def wait_for_ready(timeout = 900) 31 | start = Time.now 32 | while !(ready?) 33 | sleep 5 34 | refresh 35 | raise "Error processing site: #{error_message}" if error? 36 | yield(self) if block_given? 37 | raise "Timeout while waiting for ready" if Time.now - start > timeout 38 | end 39 | self 40 | end 41 | 42 | def ready? 43 | state == "ready" 44 | end 45 | 46 | def error? 47 | state == "error" 48 | end 49 | 50 | def publish 51 | response = client.request(:post, ::File.join(path, "restore")) 52 | process(response.parsed) 53 | self 54 | end 55 | alias :restore :publish 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/netlify/deploy_key.rb: -------------------------------------------------------------------------------- 1 | require "netlify/dns_records" 2 | 3 | module Netlify 4 | class DeployKey < Model 5 | fields :id, :public_key, :created_at 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/netlify/deploy_keys.rb: -------------------------------------------------------------------------------- 1 | require "netlify/deploy_key" 2 | 3 | module Netlify 4 | class DeployKeys < CollectionProxy 5 | path "/deploy_keys" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/netlify/deploys.rb: -------------------------------------------------------------------------------- 1 | require "netlify/deploy" 2 | 3 | module Netlify 4 | class Deploys < CollectionProxy 5 | path "/deploys" 6 | 7 | def create(attributes) 8 | if attributes[:dir] 9 | response = client.request(:post, path, 10 | :body => JSON.generate({:files => inventory(attributes[:dir]), :draft => attributes[:draft] || false}), 11 | :headers => {"Content-Type" => "application/json"} 12 | ) 13 | Deploy.new(client, response.parsed).tap do |deploy| 14 | deploy.upload_dir(attributes[:dir]) 15 | end 16 | elsif attributes[:zip] 17 | request_path = attributes[:draft] ? "#{path}?draft=true" : path 18 | response = client.request(:post, request_path, 19 | :body => ::File.read(attributes[:zip]), 20 | :headers => {"Content-Type" => "application/zip"} 21 | ) 22 | Deploy.new(client, response.parsed) 23 | elsif attributes[:tar] 24 | request_path = attributes[:draft] ? "#{path}?draft=true" : path 25 | response = client.request(:post, request_path, 26 | :body => ::File.read(attributes[:tar]), 27 | :headers => {"Content-Type" => "application/x-gzip"} 28 | ) 29 | Deploy.new(client, response.parsed) 30 | else 31 | raise "Need dir or zip to create a deploy" 32 | end 33 | end 34 | 35 | def draft(attributes) 36 | create(attributes.merge(:draft => true)) 37 | end 38 | 39 | private 40 | def inventory(dir) 41 | files = {} 42 | Dir[::File.join(dir, "**", "*")].each do |file| 43 | next unless ::File.file?(file) 44 | path = ::File.join("/", file[dir.length..-1]) 45 | files[path] = Digest::SHA1.hexdigest(::File.read(file)) 46 | end 47 | files 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/netlify/dns_record.rb: -------------------------------------------------------------------------------- 1 | module Netlify 2 | class DnsRecord < Model 3 | fields :id, :hostname, :type, :value, :ttl, :domain_id, :managed 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/netlify/dns_records.rb: -------------------------------------------------------------------------------- 1 | require "netlify/dns_record" 2 | 3 | module Netlify 4 | class DnsRecords < CollectionProxy 5 | path "/dns_records" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/netlify/dns_zone.rb: -------------------------------------------------------------------------------- 1 | require "netlify/dns_records" 2 | 3 | module Netlify 4 | class DnsZone < Model 5 | fields :id, :name, :user_id, :created_at, :updated_at 6 | 7 | def dns_records 8 | DnsRecords.new(client, path) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/netlify/dns_zones.rb: -------------------------------------------------------------------------------- 1 | require "netlify/dns_zone" 2 | 3 | module Netlify 4 | class DnsZones < CollectionProxy 5 | path "/dns_zones" 6 | 7 | def dns_records 8 | DnsRecords.new(client, path) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/netlify/file.rb: -------------------------------------------------------------------------------- 1 | module Netlify 2 | class File < Model 3 | fields :id, :path, :sha, :mime_type, :size 4 | end 5 | end -------------------------------------------------------------------------------- /lib/netlify/files.rb: -------------------------------------------------------------------------------- 1 | require "netlify/file" 2 | 3 | module Netlify 4 | class Files < CollectionProxy 5 | path "/files" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/netlify/form.rb: -------------------------------------------------------------------------------- 1 | module Netlify 2 | class Form < Model 3 | fields :id, :site_id, :name, :paths, :submission_count, :fields, :created_at 4 | 5 | def submissions 6 | Submissions.new(client, path) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/netlify/forms.rb: -------------------------------------------------------------------------------- 1 | require "netlify/form" 2 | 3 | module Netlify 4 | class Forms < CollectionProxy 5 | path "/forms" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/netlify/model.rb: -------------------------------------------------------------------------------- 1 | module Netlify 2 | class Model 3 | attr_reader :client, :attributes, :prefix 4 | 5 | def self.fields(*names) 6 | return @fields if names.empty? 7 | 8 | @fields ||= [] 9 | 10 | names.each do |name| 11 | define_method name do 12 | @attributes[name.to_sym] 13 | end 14 | 15 | define_method "#{name}=" do |value| 16 | @attributes[name.to_sym] = value 17 | end 18 | 19 | @fields.push(name.to_sym) 20 | end 21 | end 22 | 23 | def self.collection(value = nil) 24 | @collection ||= Netlify.const_get(to_s.split("::").last + "s") 25 | end 26 | 27 | def initialize(client, attributes) 28 | @client = client 29 | @attributes = {} 30 | @prefix = attributes.delete(:prefix) 31 | process(attributes) 32 | end 33 | 34 | def process(attributes) 35 | self.class.fields.each do |field| 36 | if attributes.has_key?(field) || attributes.has_key?(field.to_s) 37 | @attributes[field] = attributes[field] || attributes[field.to_s] 38 | end 39 | end 40 | self 41 | end 42 | 43 | def update(attributes) 44 | response = client.request(:put, path, :body => attributes) 45 | process(response.parsed) if response.parsed 46 | end 47 | 48 | def destroy 49 | client.request(:delete, path) 50 | end 51 | 52 | def refresh 53 | response = client.request(:get, path) 54 | process(response.parsed) 55 | end 56 | 57 | def collection 58 | self.class.collection 59 | end 60 | 61 | def path 62 | ::File.join(*[prefix, collection.path, id.to_s].compact) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/netlify/multipass.rb: -------------------------------------------------------------------------------- 1 | # Multipass implementation used for single-sign-on for resellers 2 | require "openssl" 3 | require "base64" 4 | require "time" 5 | require "json" 6 | 7 | module Netlify 8 | class Multipass 9 | def initialize(multipass_secret) 10 | ### Use the Multipass secret to derive two cryptographic keys, 11 | ### one for encryption, one for signing 12 | key_material = OpenSSL::Digest.new("sha256").digest(multipass_secret) 13 | @encryption_key = key_material[ 0,16] 14 | @signature_key = key_material[16,16] 15 | end 16 | 17 | def generate_token(customer_data_hash) 18 | ### Store the current time in ISO8601 format. 19 | ### The token will only be valid for a small timeframe around this timestamp. 20 | customer_data_hash["created_at"] = Time.now.iso8601 21 | 22 | ### Serialize the customer data to JSON and encrypt it 23 | ciphertext = encrypt(customer_data_hash.to_json) 24 | 25 | ### Create a signature (message authentication code) of the ciphertext 26 | ### and encode everything using URL-safe Base64 (RFC 4648) 27 | sig = sign(ciphertext) 28 | 29 | Base64.urlsafe_encode64(ciphertext + sign(ciphertext)) 30 | end 31 | 32 | def decode_token(token) 33 | decoded_token = Base64.urlsafe_decode64(token) 34 | ciphertext, signature = [decoded_token[0..-33], decoded_token[-32..-1]] 35 | 36 | sig = sign(ciphertext) 37 | 38 | raise "Bad signature" unless sign(ciphertext) == signature 39 | 40 | JSON.parse(decrypt(ciphertext)) 41 | end 42 | 43 | private 44 | def encrypt(plaintext) 45 | cipher = OpenSSL::Cipher::Cipher.new("aes-128-cbc") 46 | cipher.encrypt 47 | cipher.key = @encryption_key 48 | 49 | ### Use a random IV 50 | cipher.iv = iv = cipher.random_iv 51 | 52 | ### Use IV as first block of ciphertext 53 | iv + cipher.update(plaintext) + cipher.final 54 | end 55 | 56 | def decrypt(ciphertext) 57 | decipher = OpenSSL::Cipher::Cipher.new("aes-128-cbc") 58 | decipher.decrypt 59 | decipher.key = @encryption_key 60 | 61 | decipher.iv, encrypted = [ciphertext[0..15], ciphertext[16..-1]] 62 | 63 | decipher.update(encrypted) + decipher.final 64 | end 65 | 66 | def sign(data) 67 | OpenSSL::HMAC.digest("sha256", @signature_key, data) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/netlify/site.rb: -------------------------------------------------------------------------------- 1 | require 'digest/sha1' 2 | require 'uri' 3 | 4 | module Netlify 5 | class Site < Model 6 | fields :id, :state, :premium, :claimed, :name, :custom_domain, :url, 7 | :admin_url, :deploy_id, :build_id, :deploy_url, :screenshot_url, :created_at, :updated_at, 8 | :password, :notification_email, :user_id, :error_message, :required, :deploy_hook 9 | 10 | def ready? 11 | state == "current" 12 | end 13 | 14 | def error? 15 | state == "error" 16 | end 17 | 18 | def wait_for_ready(timeout = 900, &block) 19 | deploy = deploys.get(deploy_id) 20 | raise "Error fetching deploy #{deploy_id}" unless deploy 21 | deploy.wait_for_ready(timeout, &block) 22 | self 23 | end 24 | 25 | def update(attributes) 26 | response = client.request(:put, path, :body => mutable_attributes(attributes)) 27 | process(response.parsed) 28 | if attributes[:zip] || attributes[:tar] || attributes[:dir] 29 | deploy = deploys.create(attributes) 30 | self.deploy_id = deploy.id 31 | end 32 | self 33 | end 34 | 35 | def configure_github!(options) 36 | raise Client::NetlifyError, "You must specify a Github access_token" unless options[:access_token] 37 | raise Client::NetlifyError, "You must specify a Github repo" unless options[:repo] 38 | require "github_api" 39 | 40 | _, user, repo = *options[:repo].match(/^([^\/]+)\/([^\/]+)$/) 41 | unless user && repo 42 | raise Client::NetlifyError, "Invalid github repo #{options[:repo]}" 43 | end 44 | 45 | github = Github.new(:oauth_token => options[:access_token]) 46 | deploy_key = client.deploy_keys.create({}) 47 | github.repos.keys.create(user, repo, title: "Netlify", key: deploy_key.public_key) 48 | response = client.request(:put, path, :body => { 49 | :github => { 50 | :repo => options[:repo], 51 | :deploy_key_id => deploy_key.id, 52 | :dir => options[:dir], 53 | :cmd => options[:cmd], 54 | :branch => options[:branch], 55 | :env => options[:env] 56 | } 57 | }) 58 | process(response.parsed) 59 | github.repos.hooks.create(user, repo, name: "web", active: true, events: ["push"], config: { 60 | url: deploy_hook, 61 | content_type: 'json' 62 | }) 63 | self 64 | end 65 | 66 | def destroy! 67 | client.request(:delete, path) 68 | true 69 | end 70 | 71 | def forms 72 | Forms.new(client, path) 73 | end 74 | 75 | def submissions 76 | Submissions.new(client, path) 77 | end 78 | 79 | def files 80 | Files.new(client, path) 81 | end 82 | 83 | def snippets 84 | Snippets.new(client, path) 85 | end 86 | 87 | def deploys 88 | Deploys.new(client, path) 89 | end 90 | 91 | private 92 | def mutable_attributes(attributes) 93 | Hash[*[:name, :custom_domain, :password, :notification_email].map {|key| 94 | if attributes.has_key?(key) || attributes.has_key?(key.to_s) 95 | [key, attributes[key] || attributes[key.to_s]] 96 | end 97 | }.compact.flatten] 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/netlify/sites.rb: -------------------------------------------------------------------------------- 1 | require "netlify/site" 2 | require "digest/sha1" 3 | 4 | module Netlify 5 | class Sites < CollectionProxy 6 | path "/sites" 7 | 8 | def create(attributes = {}) 9 | response = client.request(:post, path, :body => Site.new(client, {}).send(:mutable_attributes, attributes)) 10 | Site.new(client, response.parsed).tap do |site| 11 | if attributes[:zip] || attributes[:tar] || attributes[:dir] 12 | deploy = site.deploys.create(attributes) 13 | site.deploy_id = deploy.id 14 | elsif attributes[:github] 15 | site.configure_github!(attributes[:github]) 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/netlify/snippet.rb: -------------------------------------------------------------------------------- 1 | module Netlify 2 | class Snippet < Model 3 | fields :id, :title, :general, :general_position, :goal, :goal_position 4 | end 5 | end -------------------------------------------------------------------------------- /lib/netlify/snippets.rb: -------------------------------------------------------------------------------- 1 | require "netlify/snippet" 2 | 3 | module Netlify 4 | class Snippets < CollectionProxy 5 | path "/snippets" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/netlify/submission.rb: -------------------------------------------------------------------------------- 1 | module Netlify 2 | class Submission < Model 3 | fields :id, :number, :title, :email, :name, :first_name, :last_name, 4 | :company, :summary, :body, :data, :created_at, :site_url 5 | end 6 | end -------------------------------------------------------------------------------- /lib/netlify/submissions.rb: -------------------------------------------------------------------------------- 1 | require "netlify/submission" 2 | 3 | module Netlify 4 | class Submissions < CollectionProxy 5 | path "/submissions" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/netlify/user.rb: -------------------------------------------------------------------------------- 1 | module Netlify 2 | class User < Model 3 | fields :id, :uid, :email, :affiliate_id, :site_count, :created_at, :last_login 4 | 5 | def sites 6 | Sites.new(client, path) 7 | end 8 | 9 | def submissions 10 | Submissions.new(client, path) 11 | end 12 | 13 | def forms 14 | Forms.new(client, path) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/netlify/users.rb: -------------------------------------------------------------------------------- 1 | require "netlify/user" 2 | 3 | module Netlify 4 | class Users < CollectionProxy 5 | path "/users" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/netlify/version.rb: -------------------------------------------------------------------------------- 1 | module Netlify 2 | VERSION = "0.2.2" 3 | end 4 | -------------------------------------------------------------------------------- /netlify.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | d = File.read(File.expand_path("../lib/netlify/version.rb", __FILE__)) 4 | if d =~ /VERSION = "(\d+\.\d+\.\d+)"/ 5 | version = $1 6 | else 7 | version = "0.0.1" 8 | end 9 | 10 | Gem::Specification.new do |gem| 11 | gem.name = "netlify" 12 | gem.version = version 13 | gem.authors = ["Mathias Biilmann Christensen"] 14 | gem.email = ["mathias@Netlify.com"] 15 | gem.description = %q{API Client for Netlify} 16 | gem.summary = %q{API Client for Netlify} 17 | gem.homepage = "https://www.netlify.com" 18 | 19 | gem.files = `git ls-files`.split($/) 20 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 21 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 22 | gem.require_paths = ["lib"] 23 | 24 | gem.add_dependency "oauth2", ">= 0.9.2" 25 | gem.add_dependency "slop" 26 | gem.add_dependency "highline" 27 | gem.add_dependency "github_api" 28 | gem.add_development_dependency "minitest" 29 | gem.add_development_dependency "webmock" 30 | end 31 | -------------------------------------------------------------------------------- /test/client_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ClientTest < MiniTest::Unit::TestCase 4 | attr_reader :client 5 | 6 | def setup 7 | @client = Netlify::Client.new(:client_id => "client_id", :client_secret => "client_secret") 8 | end 9 | 10 | def test_authorize_url 11 | expected = URI.parse("https://api.netlify.com/oauth/authorize?response_type=code&client_id=client_id&redirect_uri=http%3A%2F%2Fexample.com%2Fcallback") 12 | actual = URI.parse(client.authorize_url(:redirect_uri => "http://example.com/callback")) 13 | assert_equal expected.scheme, actual.scheme 14 | assert_equal expected.host, actual.host 15 | assert_equal expected.port, actual.port 16 | assert_equal expected.query.split("&").sort, actual.query.split("&").sort 17 | end 18 | 19 | def test_authorize_from_code 20 | stub_request(:post, "https://api.netlify.com/oauth/token").to_return( 21 | :headers => {'Content-Type' => 'application/json'}, 22 | :body => { 23 | "access_token" => "2YotnFZFEjr1zCsicMWpAA" 24 | }) 25 | client.authorize_from_code!("authorization_code", :redirect_uri => "http://example.com/callback") 26 | assert_equal "2YotnFZFEjr1zCsicMWpAA", client.access_token 27 | end 28 | 29 | def test_authorize_from_credentials 30 | stub_request(:post, "https://client_id:client_secret@api.netlify.com/oauth/token").to_return( 31 | :headers => {'Content-Type' => 'application/json'}, 32 | :body => { 33 | "access_token" => "2YotnFZFEjr1zCsicMWpAA" 34 | }) 35 | 36 | client.authorize_from_credentials! 37 | assert_equal "2YotnFZFEjr1zCsicMWpAA", client.access_token 38 | end 39 | 40 | def test_simple_get_request 41 | stub_request(:get, "https://api.netlify.com/api/v1/sites") 42 | .with(:headers => {'Authorization' => "Bearer access_token"}) 43 | .to_return( 44 | :headers => {'Content-Type' => 'application/json'}, 45 | :body => [] 46 | ) 47 | 48 | client.access_token = "access_token" 49 | response = client.request(:get, "/sites") 50 | assert_equal [], response.parsed 51 | end 52 | 53 | def test_sites 54 | stub_request(:get, "https://api.netlify.com/api/v1/sites") 55 | .with(:headers => {'Authorization' => "Bearer access_token"}) 56 | .to_return( 57 | :headers => {'Content-Type' => 'application/json'}, 58 | :body => JSON.generate([{:url => "http://www.example.com"}]) 59 | ) 60 | client.access_token = "access_token" 61 | sites = client.sites.all 62 | assert_equal "http://www.example.com", sites.first.url 63 | end 64 | 65 | def test_get_site 66 | stub_request(:get, "https://api.netlify.com/api/v1/sites/1234") 67 | .with(:headers => {'Authorization' => "Bearer access_token"}) 68 | .to_return( 69 | :headers => {'Content-Type' => 'application/json'}, 70 | :body => {:url => "http://www.example.com"} 71 | ) 72 | 73 | client.access_token = "access_token" 74 | site = client.sites.get("1234") 75 | assert_equal "http://www.example.com", site.url 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/files/site-dir.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify/ruby-client/fcd18d282b3539cc67b70206a3f4a1b6e0df3562/test/files/site-dir.zip -------------------------------------------------------------------------------- /test/files/site-dir/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Test Document 4 |

Test

5 | -------------------------------------------------------------------------------- /test/multipass_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'digest/sha1' 3 | 4 | class MultipassTest < MiniTest::Unit::TestCase 5 | def setup 6 | @mp = Netlify::Multipass.new("secret") 7 | end 8 | 9 | def test_generate_and_decode_token 10 | data = {"email" => "test@example.com", "uid" => "1234"} 11 | token = @mp.generate_token(data) 12 | assert_equal data, @mp.decode_token(token), "Data should be the same after generating and decoding" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/sites_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'digest/sha1' 3 | 4 | class SitesTest < MiniTest::Unit::TestCase 5 | attr_reader :client 6 | attr_accessor :_assertions 7 | 8 | def setup 9 | @client = Netlify::Client.new(:client_id => "client_id", :client_secret => "client_secret") 10 | @client.access_token = "access_token" 11 | self._assertions = 0 12 | end 13 | 14 | def test_create_from_dir 15 | body = nil 16 | dir = ::File.expand_path("../files/site-dir", __FILE__) 17 | index_sha = Digest::SHA1.hexdigest(::File.read(::File.join(dir, "index.html"))) 18 | 19 | stub_request(:post, "https://api.netlify.com/api/v1/sites") 20 | .to_return {|request| 21 | { 22 | :headers => {'Content-Type' => 'application/json'}, 23 | :body => JSON.generate({:id => "1234"}) 24 | } 25 | } 26 | stub_request(:post, "https://api.netlify.com/api/v1/sites/1234/deploys") 27 | .to_return {|request| 28 | body = JSON.parse(request.body) 29 | { 30 | :headers => {'Content-Type' => 'application/json'}, 31 | :body => JSON.generate({:id => "2345", :state => "uploading", :required => [index_sha]}) 32 | } 33 | } 34 | stub_request(:put, "https://api.netlify.com/api/v1/deploys/2345/files/index.html") 35 | stub_request(:get, "https://api.netlify.com/api/v1/deploys/2345") 36 | .to_return(:headers => {'Content-Type' => 'application/json'}, :body => {:id => "2345", :state => "processing"}) 37 | 38 | site = client.sites.create(:dir => dir) 39 | 40 | assert_equal index_sha, body['files']['/index.html'] 41 | 42 | assert_requested :put, "https://api.netlify.com/api/v1/deploys/2345/files/index.html", 43 | :body => ::File.read(::File.join(dir, "index.html")), :times => 1 # ===> Success 44 | end 45 | 46 | def test_create_from_zip 47 | stub_request(:post, "https://api.netlify.com/api/v1/sites") 48 | .to_return({ 49 | :headers => {'Content-Type' => 'application/json'}, 50 | :body => JSON.generate({:id => "1234"}) 51 | }) 52 | 53 | stub_request(:post, "https://api.netlify.com/api/v1/sites/1234/deploys") 54 | .to_return {|request| 55 | { 56 | :headers => {'Content-Type' => 'application/json'}, 57 | :body => JSON.generate({:id => "2345", :state => "uploading", :required => []}) 58 | } 59 | } 60 | zip = ::File.expand_path("../files/site-dir.zip", __FILE__) 61 | site = client.sites.create(:zip => zip) 62 | 63 | assert_requested :post, "https://api.netlify.com/api/v1/sites/1234/deploys", 64 | :body => ::File.read(zip), :times => 1 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'Netlify' 2 | require 'json' 3 | require 'minitest' 4 | require 'minitest/autorun' 5 | # require 'minitest/pride' 6 | require 'webmock/minitest' --------------------------------------------------------------------------------