github-downloads 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +11 -0
- data/Gemfile.lock +55 -0
- data/README.md +25 -0
- data/Rakefile +102 -0
- data/bin/github-downloads +5 -0
- data/lib/github.rb +2 -0
- data/lib/github/client.rb +142 -0
- data/lib/github/downloads.rb +66 -0
- data/lib/github/downloads_controller.rb +109 -0
- data/lib/github/s3_uploader.rb +24 -0
- data/spec/fixtures/textfile.txt +0 -0
- data/spec/github_client_spec.rb +211 -0
- data/spec/github_download_spec.rb +87 -0
- data/spec/spec_helper.rb +26 -0
- metadata +229 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
github-downloads (0.1.0)
|
5
|
+
highline (~> 1.6.2)
|
6
|
+
hirb (~> 0.4.5)
|
7
|
+
json (~> 1.5.3)
|
8
|
+
mime-types (~> 1.16.0)
|
9
|
+
rest-client (~> 1.6.3)
|
10
|
+
simpleconsole (~> 0.1.1)
|
11
|
+
|
12
|
+
GEM
|
13
|
+
remote: http://rubygems.org/
|
14
|
+
specs:
|
15
|
+
awesome_print (0.3.1)
|
16
|
+
diff-lcs (1.1.2)
|
17
|
+
highline (1.6.2)
|
18
|
+
hirb (0.4.5)
|
19
|
+
json (1.5.3)
|
20
|
+
mime-types (1.16)
|
21
|
+
mimic (0.4.1)
|
22
|
+
json
|
23
|
+
plist
|
24
|
+
rack
|
25
|
+
sinatra
|
26
|
+
mocha (0.9.10)
|
27
|
+
rake
|
28
|
+
plist (3.1.0)
|
29
|
+
rack (1.2.1)
|
30
|
+
rake (0.9.2)
|
31
|
+
rest-client (1.6.3)
|
32
|
+
mime-types (>= 1.16)
|
33
|
+
rspec (2.5.0)
|
34
|
+
rspec-core (~> 2.5.0)
|
35
|
+
rspec-expectations (~> 2.5.0)
|
36
|
+
rspec-mocks (~> 2.5.0)
|
37
|
+
rspec-core (2.5.2)
|
38
|
+
rspec-expectations (2.5.0)
|
39
|
+
diff-lcs (~> 1.1.2)
|
40
|
+
rspec-mocks (2.5.0)
|
41
|
+
simpleconsole (0.1.1)
|
42
|
+
sinatra (1.1.2)
|
43
|
+
rack (~> 1.1)
|
44
|
+
tilt (~> 1.2)
|
45
|
+
tilt (1.2.2)
|
46
|
+
|
47
|
+
PLATFORMS
|
48
|
+
ruby
|
49
|
+
|
50
|
+
DEPENDENCIES
|
51
|
+
awesome_print
|
52
|
+
github-downloads!
|
53
|
+
mimic
|
54
|
+
mocha
|
55
|
+
rspec
|
data/README.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# github-downloads - a gem for managing your downloads, on Github
|
2
|
+
|
3
|
+
This is a simple gem that does exactly what it says on the tin: manages your Github project downloads.
|
4
|
+
|
5
|
+
$ gem install github-downloads
|
6
|
+
|
7
|
+
It can be used to list the downloads for your project:
|
8
|
+
|
9
|
+
$ github-downloads list -u lukeredpath -r simpleconfig
|
10
|
+
|
11
|
+
It can also be used to create a new download:
|
12
|
+
|
13
|
+
$ github-downloads create -u lukeredpath -r simpleconfig -f ~/myproject/somefile.zip -d "This is an important file"
|
14
|
+
|
15
|
+
### Github::Client, a generic Github API client
|
16
|
+
|
17
|
+
The gem is built on top of [version 3 of the Github API](http://developer.github.com/v3/); included in the source is a class, Github::Client, a simple wrapper around the Github API that uses RestClient and simplifies communication with the Github API.
|
18
|
+
|
19
|
+
It provides a simple interface to the Github REST API. It handles errors appropriately and returns parsed response data as well as additional metadata such as API rate limits.
|
20
|
+
|
21
|
+
It doesn't aim to be a full-blown Github API library (completely with a local domain model) but a very thin utility ckass that can be used to write other simple scripts and gems, like this one. Feel free to use this in your own project if you are working with the Github API.
|
22
|
+
|
23
|
+
### License
|
24
|
+
|
25
|
+
This code is provided under the terms of the MIT license.
|
data/Rakefile
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rdoc/task'
|
3
|
+
require "bundler/setup"
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
desc "Run all examples"
|
7
|
+
RSpec::Core::RakeTask.new(:specs)
|
8
|
+
|
9
|
+
task :default => :specs
|
10
|
+
|
11
|
+
require "rubygems"
|
12
|
+
require "rubygems/package_task"
|
13
|
+
|
14
|
+
GEM_VERSION = "0.1.0"
|
15
|
+
|
16
|
+
# This builds the actual gem. For details of what all these options
|
17
|
+
# mean, and other ones you can add, check the documentation here:
|
18
|
+
#
|
19
|
+
# http://rubygems.org/read/chapter/20
|
20
|
+
#
|
21
|
+
spec = Gem::Specification.new do |s|
|
22
|
+
|
23
|
+
# Change these as appropriate
|
24
|
+
s.name = "github-downloads"
|
25
|
+
s.version = GEM_VERSION
|
26
|
+
s.summary = "Manages downloads for your Github projects"
|
27
|
+
s.author = "Luke Redpath"
|
28
|
+
s.email = "luke@lukeredpath.co.uk"
|
29
|
+
s.homepage = "http://lukeredpath.co.uk"
|
30
|
+
|
31
|
+
s.has_rdoc = false
|
32
|
+
# s.extra_rdoc_files = %w(README)
|
33
|
+
# s.rdoc_options = %w(--main README)
|
34
|
+
|
35
|
+
# Add any extra files to include in the gem
|
36
|
+
s.files = %w(Gemfile Gemfile.lock Rakefile README.md) + Dir.glob("{bin,spec,lib}/**/*")
|
37
|
+
s.executables = FileList["bin/**"].map { |f| File.basename(f) }
|
38
|
+
s.require_paths = ["lib"]
|
39
|
+
|
40
|
+
# If you want to depend on other gems, add them here, along with any
|
41
|
+
# relevant versions
|
42
|
+
gem "rest-client"
|
43
|
+
gem "json"
|
44
|
+
gem "simpleconsole"
|
45
|
+
gem "hirb"
|
46
|
+
gem "mime-types"
|
47
|
+
gem "highline"
|
48
|
+
|
49
|
+
s.add_dependency("rest-client", "~> 1.6.3")
|
50
|
+
s.add_dependency("json", "~> 1.5.3")
|
51
|
+
s.add_dependency("simpleconsole", "~> 0.1.1")
|
52
|
+
s.add_dependency("hirb", "~> 0.4.5")
|
53
|
+
s.add_dependency("mime-types", "~> 1.16.0")
|
54
|
+
s.add_dependency("highline", "~> 1.6.2")
|
55
|
+
|
56
|
+
# If your tests use any gems, include them here
|
57
|
+
s.add_development_dependency("rspec")
|
58
|
+
s.add_development_dependency("mocha")
|
59
|
+
s.add_development_dependency("mimic")
|
60
|
+
s.add_development_dependency("awesome_print")
|
61
|
+
end
|
62
|
+
|
63
|
+
# This task actually builds the gem. We also regenerate a static
|
64
|
+
# .gemspec file, which is useful if something (i.e. GitHub) will
|
65
|
+
# be automatically building a gem for this project. If you're not
|
66
|
+
# using GitHub, edit as appropriate.
|
67
|
+
#
|
68
|
+
# To publish your gem online, install the 'gemcutter' gem; Read more
|
69
|
+
# about that here: http://gemcutter.org/pages/gem_docs
|
70
|
+
Gem::PackageTask.new(spec) do |pkg|
|
71
|
+
pkg.gem_spec = spec
|
72
|
+
end
|
73
|
+
|
74
|
+
desc "Build the gemspec file #{spec.name}.gemspec"
|
75
|
+
task :gemspec do
|
76
|
+
file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
|
77
|
+
File.open(file, "w") {|f| f << spec.to_ruby }
|
78
|
+
end
|
79
|
+
|
80
|
+
# If you don't want to generate the .gemspec file, just remove this line. Reasons
|
81
|
+
# why you might want to generate a gemspec:
|
82
|
+
# - using bundler with a git source
|
83
|
+
# - building the gem without rake (i.e. gem build blah.gemspec)
|
84
|
+
# - maybe others?
|
85
|
+
task :package => :gemspec
|
86
|
+
|
87
|
+
# Generate documentation
|
88
|
+
RDoc::Task.new do |rd|
|
89
|
+
rd.main = "README"
|
90
|
+
rd.rdoc_files.include("README", "lib/**/*.rb")
|
91
|
+
rd.rdoc_dir = "rdoc"
|
92
|
+
end
|
93
|
+
|
94
|
+
desc 'Clear out RDoc and generated packages'
|
95
|
+
task :clean => [:clobber_rdoc, :clobber_package] do
|
96
|
+
rm "#{spec.name}.gemspec"
|
97
|
+
end
|
98
|
+
|
99
|
+
desc 'Publish gem'
|
100
|
+
task :publish => :gem do
|
101
|
+
system "gem push pkg/#{spec.file_name}"
|
102
|
+
end
|
data/lib/github.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'restclient'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Github
|
6
|
+
class Client
|
7
|
+
class << self
|
8
|
+
def connect(base_url, user = nil, pass = nil)
|
9
|
+
new RestClient::Resource.new(base_url, user, pass)
|
10
|
+
end
|
11
|
+
|
12
|
+
def proxy=(proxy_host)
|
13
|
+
RestClient.proxy = proxy_host
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(resource)
|
18
|
+
@resource = resource
|
19
|
+
end
|
20
|
+
|
21
|
+
def get(path)
|
22
|
+
request :get, path
|
23
|
+
end
|
24
|
+
|
25
|
+
def post(path, data)
|
26
|
+
request :post, path, data
|
27
|
+
end
|
28
|
+
|
29
|
+
def put(path, data)
|
30
|
+
request :put, path, data
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete(path)
|
34
|
+
request :delete, path
|
35
|
+
end
|
36
|
+
|
37
|
+
def head(path)
|
38
|
+
request :head, path
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def request(method, path, data = nil)
|
44
|
+
if data
|
45
|
+
Response.new(@resource[path].send(method, data.to_json))
|
46
|
+
else
|
47
|
+
Response.new(@resource[path].send(method))
|
48
|
+
end
|
49
|
+
rescue RestClient::Exception => e
|
50
|
+
if Response.can_handle?(e.response)
|
51
|
+
Response.new(e.response)
|
52
|
+
else
|
53
|
+
raise e
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Response
|
58
|
+
SUCCESS_CODES = [200, 201]
|
59
|
+
FAILURE_CODES = [400, 401, 422]
|
60
|
+
|
61
|
+
def initialize(response)
|
62
|
+
@response = response
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.can_handle?(response)
|
66
|
+
(SUCCESS_CODES + FAILURE_CODES).include?(response.code)
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s
|
70
|
+
"#{@response.description.strip} | Rate limit: #{rate_limit_remaining} / #{rate_limit_remaining}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def success?
|
74
|
+
when_statuses_are *SUCCESS_CODES
|
75
|
+
end
|
76
|
+
|
77
|
+
def error?
|
78
|
+
when_statuses_are *FAILURE_CODES
|
79
|
+
end
|
80
|
+
|
81
|
+
def error_message
|
82
|
+
parsed_response["message"]
|
83
|
+
end
|
84
|
+
|
85
|
+
def errors
|
86
|
+
parsed_response["errors"].map { |hash| EntityError.new(hash) }
|
87
|
+
end
|
88
|
+
|
89
|
+
def rate_limit
|
90
|
+
@response.headers[:x_ratelimit_limit].to_i
|
91
|
+
end
|
92
|
+
|
93
|
+
def rate_limit_remaining
|
94
|
+
@response.headers[:x_ratelimit_remaining].to_i
|
95
|
+
end
|
96
|
+
|
97
|
+
def location
|
98
|
+
@response.headers[:location]
|
99
|
+
end
|
100
|
+
|
101
|
+
def data
|
102
|
+
parsed_response
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def parsed_response
|
108
|
+
@parsed_response ||= JSON.parse(@response)
|
109
|
+
end
|
110
|
+
|
111
|
+
def when_statuses_are(*codes)
|
112
|
+
codes.include?(@response.code)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class EntityError < OpenStruct
|
117
|
+
MISSING = "missing"
|
118
|
+
MISSING_FIELD = "missing_field"
|
119
|
+
INVALID = "invalid"
|
120
|
+
ALREADY_EXISTS = "already_exists"
|
121
|
+
|
122
|
+
def to_s
|
123
|
+
"<Github::Client::EntityError: #{description}>"
|
124
|
+
end
|
125
|
+
|
126
|
+
def description
|
127
|
+
case self.code
|
128
|
+
when MISSING
|
129
|
+
"#{self.resource} is missing"
|
130
|
+
when MISSING_FIELD
|
131
|
+
"#{self.resource}##{self.field} is missing"
|
132
|
+
when INVALID
|
133
|
+
"#{self.resource}##{self.field} is invalid (refer to documentation)"
|
134
|
+
when ALREADY_EXISTS
|
135
|
+
"#{self.resource}##{self.field} value is already taken (must be unique)"
|
136
|
+
else
|
137
|
+
"#{self.resource}##{self.field} error: #{self.code}"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'mime/types'
|
3
|
+
require 'github/client'
|
4
|
+
|
5
|
+
module Github
|
6
|
+
class Downloads
|
7
|
+
attr_accessor :uploader
|
8
|
+
|
9
|
+
GITHUB_BASE_URL = "https://api.github.com"
|
10
|
+
|
11
|
+
def initialize(client, user, repos)
|
12
|
+
@client = client
|
13
|
+
@user = user
|
14
|
+
@repos = repos
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.connect(user, password, repos)
|
18
|
+
client = Client.connect(GITHUB_BASE_URL, user, password)
|
19
|
+
new(client, user, repos)
|
20
|
+
end
|
21
|
+
|
22
|
+
class UnexpectedResponse < StandardError
|
23
|
+
attr_reader :response
|
24
|
+
|
25
|
+
def initialize(response)
|
26
|
+
@response = response
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def list
|
31
|
+
response = @client.get(downloads_resource_path)
|
32
|
+
|
33
|
+
if response.success?
|
34
|
+
from_response_data(response.data)
|
35
|
+
else
|
36
|
+
raise UnexpectedResponse, response
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def create(file_path, description = "")
|
41
|
+
response = @client.post(downloads_resource_path, {
|
42
|
+
:name => File.basename(file_path),
|
43
|
+
:description => description,
|
44
|
+
:content_type => MIME::Types.type_for(file_path)[0] || MIME::Types["application/octet-stream"][0],
|
45
|
+
:size => File.size?(file_path)
|
46
|
+
})
|
47
|
+
|
48
|
+
if response.success?
|
49
|
+
uploader.upload(File.expand_path(file_path), response.data)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def downloads_resource_path
|
56
|
+
"/repos/#{@user}/#{@repos}/downloads"
|
57
|
+
end
|
58
|
+
|
59
|
+
def from_response_data(data)
|
60
|
+
data.map { |hash| Download.new(hash) }
|
61
|
+
end
|
62
|
+
|
63
|
+
class Download < OpenStruct
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'simpleconsole'
|
2
|
+
require 'hirb'
|
3
|
+
require 'highline/import'
|
4
|
+
require 'osx_keychain'
|
5
|
+
|
6
|
+
require 'github/downloads'
|
7
|
+
require 'github/s3_uploader'
|
8
|
+
|
9
|
+
module Github
|
10
|
+
class DownloadsController < SimpleConsole::Controller
|
11
|
+
include Hirb::Console
|
12
|
+
|
13
|
+
params :string => {
|
14
|
+
:u => :user,
|
15
|
+
:f => :file,
|
16
|
+
:n => :name,
|
17
|
+
:r => :repos,
|
18
|
+
:d => :description,
|
19
|
+
:p => :proxy
|
20
|
+
},
|
21
|
+
:bool => {
|
22
|
+
:o => :overwrite
|
23
|
+
}
|
24
|
+
|
25
|
+
before_filter :set_proxy
|
26
|
+
|
27
|
+
def default
|
28
|
+
puts "Valid actions: list, upload, delete"
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
|
32
|
+
def list
|
33
|
+
if (downloads = github.list)
|
34
|
+
table downloads, {:fields => [:name, :description, :download_count]}
|
35
|
+
else
|
36
|
+
puts "Couldn't fetch downloads!"
|
37
|
+
end
|
38
|
+
rescue Github::Downloads::UnexpectedResponse => e
|
39
|
+
fail! "Unexpected response (#{e.response})."
|
40
|
+
end
|
41
|
+
|
42
|
+
def create
|
43
|
+
if params[:file].nil?
|
44
|
+
fail! "* A file must be specified (-f or --file)"
|
45
|
+
end
|
46
|
+
|
47
|
+
if (url_for_upload = github(:authenticated).create(params[:file], params[:description]))
|
48
|
+
puts "Upload successful! (#{url_for_upload})"
|
49
|
+
else
|
50
|
+
fail! "Upload failed!"
|
51
|
+
end
|
52
|
+
rescue Github::Downloads::UnexpectedResponse => e
|
53
|
+
fail! "Unexpected response (#{e.response})."
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def fail!(message, status = 1)
|
59
|
+
puts(message)
|
60
|
+
exit(status)
|
61
|
+
end
|
62
|
+
|
63
|
+
def fetch_downloads(repo)
|
64
|
+
Hpricot(open("https://github.com/#{repo}/downloads"))
|
65
|
+
end
|
66
|
+
|
67
|
+
def set_proxy
|
68
|
+
Github::Client.proxy = params[:proxy]
|
69
|
+
end
|
70
|
+
|
71
|
+
def github(authentication_required = false)
|
72
|
+
if params[:repos].nil? || params[:user].nil?
|
73
|
+
fail! "* Please specify a user (-u or --user) and repository (-r or --repos)"
|
74
|
+
end
|
75
|
+
|
76
|
+
if authentication_required
|
77
|
+
password = lookup_or_prompt_for_password(params[:user])
|
78
|
+
end
|
79
|
+
|
80
|
+
@github ||= Github::Downloads.connect(params[:user], password, params[:repos]).tap do |gh|
|
81
|
+
gh.uploader = Github::S3Uploader.new
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def lookup_or_prompt_for_password(user)
|
86
|
+
fetch_password_from_keychain(user) || prompt_for_password(user)
|
87
|
+
end
|
88
|
+
|
89
|
+
def prompt_for_password(user)
|
90
|
+
ask("Enter Github password:") {|q| q.echo = false }.tap do |password|
|
91
|
+
store_password_in_keychain(user, password)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def mac?
|
96
|
+
RUBY_PLATFORM.match(/darwin/)
|
97
|
+
end
|
98
|
+
|
99
|
+
KEYCHAIN_SERVICE = "github-uploads"
|
100
|
+
|
101
|
+
def fetch_password_from_keychain(user)
|
102
|
+
OSXKeychain.new[KEYCHAIN_SERVICE, user] if mac?
|
103
|
+
end
|
104
|
+
|
105
|
+
def store_password_in_keychain(user, password)
|
106
|
+
OSXKeychain.new[KEYCHAIN_SERVICE, user] = password if mac?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'restclient'
|
2
|
+
|
3
|
+
module Github
|
4
|
+
class S3Uploader
|
5
|
+
def upload(path, metadata)
|
6
|
+
response = RestClient.post(metadata["s3_url"], [
|
7
|
+
["key", "#{metadata["prefix"]}#{metadata["name"]}"],
|
8
|
+
["acl", metadata["acl"]],
|
9
|
+
["success_action_status", 201],
|
10
|
+
["Filename", metadata["name"]],
|
11
|
+
["AWSAccessKeyId", metadata["accesskeyid"]],
|
12
|
+
["Policy", metadata["policy"]],
|
13
|
+
["Signature", metadata["signature"]],
|
14
|
+
["Content-Type", metadata["mime_type"]],
|
15
|
+
["file", File.open(path)]
|
16
|
+
])
|
17
|
+
if response.code == 201
|
18
|
+
metadata["url"]
|
19
|
+
else
|
20
|
+
false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
File without changes
|
@@ -0,0 +1,211 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'github/client'
|
3
|
+
|
4
|
+
class FakeResource # fake RestClient::Resource
|
5
|
+
attr_reader :path
|
6
|
+
|
7
|
+
def initialize(path = "/")
|
8
|
+
@path = Mocha::Mockery.instance.new_state_machine("location").starts_as(path)
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](path)
|
12
|
+
tap { @path.become(path) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "Github::Client" do
|
17
|
+
|
18
|
+
before :each do
|
19
|
+
@resource = FakeResource.new
|
20
|
+
@client = Github::Client.new(@resource)
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#get" do
|
24
|
+
it "performs a GET request to /some/path on the resource" do
|
25
|
+
@resource.expects(:get).when @resource.path.is("/some/path")
|
26
|
+
@client.get("/some/path")
|
27
|
+
end
|
28
|
+
|
29
|
+
it "returns a Github::Client::Response" do
|
30
|
+
@resource.stubs(:get)
|
31
|
+
@client.get("/some/path").should be_instance_of(Github::Client::Response)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#post" do
|
36
|
+
it "performs a POST request with the supplied data in JSON format to /some/path on the resource" do
|
37
|
+
@resource.expects(:post).with({"foo" => "bar"}.to_json).when @resource.path.is("/some/path")
|
38
|
+
@client.post("/some/path", {"foo" => "bar"})
|
39
|
+
end
|
40
|
+
|
41
|
+
it "returns a Github::Client::Response" do
|
42
|
+
@resource.stubs(:post)
|
43
|
+
@client.post("/some/path", nil).should be_instance_of(Github::Client::Response)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#put" do
|
48
|
+
it "performs a PUT request with the supplied data in JSON format to /some/path on the resource" do
|
49
|
+
@resource.expects(:put).with({"foo" => "bar"}.to_json).when @resource.path.is("/some/path")
|
50
|
+
@client.put("/some/path", {"foo" => "bar"})
|
51
|
+
end
|
52
|
+
|
53
|
+
it "returns a Github::Client::Response" do
|
54
|
+
@resource.stubs(:put)
|
55
|
+
@client.put("/some/path", nil).should be_instance_of(Github::Client::Response)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#delete" do
|
60
|
+
it "performs a DELETE request to /some/path on the resource" do
|
61
|
+
@resource.expects(:delete).when @resource.path.is("/some/path")
|
62
|
+
@client.delete("/some/path")
|
63
|
+
end
|
64
|
+
|
65
|
+
it "returns a Github::Client::Response" do
|
66
|
+
@resource.stubs(:delete)
|
67
|
+
@client.delete("/some/path").should be_instance_of(Github::Client::Response)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "#head" do
|
72
|
+
it "performs a HEAD request to /some/path on the resource" do
|
73
|
+
@resource.expects(:head).when @resource.path.is("/some/path")
|
74
|
+
@client.head("/some/path")
|
75
|
+
end
|
76
|
+
|
77
|
+
it "returns a Github::Client::Response" do
|
78
|
+
@resource.stubs(:head)
|
79
|
+
@client.head("/some/path").should be_instance_of(Github::Client::Response)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "Github::Client::Response" do
|
86
|
+
|
87
|
+
before :each do
|
88
|
+
@client = Github::Client.connect("http://localhost:#{mimic_port}")
|
89
|
+
end
|
90
|
+
|
91
|
+
def response
|
92
|
+
@client.get("/some/path")
|
93
|
+
end
|
94
|
+
|
95
|
+
def post_response(data = "")
|
96
|
+
@client.post("/some/path", data)
|
97
|
+
end
|
98
|
+
|
99
|
+
context "for a standard non-paginated 200 response" do
|
100
|
+
before :each do
|
101
|
+
Mimic.mimic.get("/some/path").returning({"result" => "ok"}.to_json, 200, {"X-RateLimit-Limit" => "5000", "X-RateLimit-Remaining" => "4966"})
|
102
|
+
end
|
103
|
+
|
104
|
+
it "indicates success" do
|
105
|
+
response.should be_success
|
106
|
+
end
|
107
|
+
|
108
|
+
it "stores the current rate limit" do
|
109
|
+
response.rate_limit.should == 5000
|
110
|
+
end
|
111
|
+
|
112
|
+
it "stores the number of rate-limited requests remaining" do
|
113
|
+
response.rate_limit_remaining.should == 4966
|
114
|
+
end
|
115
|
+
|
116
|
+
it "returns the parsed JSON response" do
|
117
|
+
response.data.should == {"result" => "ok"}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context "for 201 created responses" do
|
122
|
+
before :each do
|
123
|
+
Mimic.mimic.post("/some/path").returning("", 201, {"Location" => "http://example.com/resource"})
|
124
|
+
end
|
125
|
+
|
126
|
+
it "indicates success" do
|
127
|
+
post_response.should be_success
|
128
|
+
end
|
129
|
+
|
130
|
+
it "returns the location of the created resource" do
|
131
|
+
post_response.location.should == "http://example.com/resource"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context "for a bad request" do
|
136
|
+
before :each do
|
137
|
+
Mimic.mimic.get("/some/path").returning({"message" => "There was an error"}.to_json, 400, {})
|
138
|
+
end
|
139
|
+
|
140
|
+
it "indicates an error" do
|
141
|
+
response.should be_error
|
142
|
+
response.should_not be_success
|
143
|
+
end
|
144
|
+
|
145
|
+
it "returns the error message" do
|
146
|
+
response.error_message.should == "There was an error"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
context "for requests with invalid data" do
|
151
|
+
before :each do
|
152
|
+
Mimic.mimic.get("/some/path").returning(
|
153
|
+
{"message" => "Validation failed",
|
154
|
+
"errors" => [
|
155
|
+
{"resource" => "Issue", "field" => "title", "code" => "missing_field"}
|
156
|
+
]}.to_json, 422, {})
|
157
|
+
end
|
158
|
+
|
159
|
+
it "indicates an error" do
|
160
|
+
response.should be_error
|
161
|
+
response.should_not be_success
|
162
|
+
end
|
163
|
+
|
164
|
+
it "returns the error message" do
|
165
|
+
response.error_message.should == "Validation failed"
|
166
|
+
end
|
167
|
+
|
168
|
+
it "returns the errors" do
|
169
|
+
response.should have(1).errors
|
170
|
+
response.errors[0].resource.should == "Issue"
|
171
|
+
response.errors[0].field.should == "title"
|
172
|
+
response.errors[0].code.should == Github::Client::EntityError::MISSING_FIELD
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
context "for a server error" do
|
177
|
+
before :each do
|
178
|
+
Mimic.mimic.get("/some/path").returning("", 500)
|
179
|
+
end
|
180
|
+
|
181
|
+
it "raises an exception" do
|
182
|
+
proc{ @client.get("/some/path") }.should raise_error
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe "Github::Client authentication" do
|
188
|
+
|
189
|
+
context "with basic auth" do
|
190
|
+
before :each do
|
191
|
+
Mimic.mimic do
|
192
|
+
use Rack::Auth::Basic do |user, pass|
|
193
|
+
user == "joebloggs" and pass == "letmein"
|
194
|
+
end
|
195
|
+
|
196
|
+
get("/some/path") { [200, {}, ""] }
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
it "should succeed when authenticated correctly" do
|
201
|
+
@client = Github::Client.connect("http://localhost:#{mimic_port}", "joebloggs", "letmein")
|
202
|
+
@client.get("/some/path").should be_success
|
203
|
+
end
|
204
|
+
|
205
|
+
it "should fail when not authenticated correctly" do
|
206
|
+
@client = Github::Client.connect("http://localhost:#{mimic_port}", "joebloggs", "wrongpass")
|
207
|
+
@client.get("/some/path").should be_error
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'github/downloads'
|
3
|
+
|
4
|
+
describe "Github::Downloads" do
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
@client = mock("client")
|
8
|
+
@downloads = Github::Downloads.new(@client, "username", "somerepo")
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#list" do
|
12
|
+
it "returns an empty array when there is no downloads" do
|
13
|
+
@client.stubs(:get).with("/repos/username/somerepo/downloads").returns successful_response_with([])
|
14
|
+
@downloads.list.should be_empty
|
15
|
+
end
|
16
|
+
|
17
|
+
it "returns an array of downloads parsed from the response when there are downloads" do
|
18
|
+
@client.stubs(:get).with("/repos/username/somerepo/downloads").returns successful_response_with([
|
19
|
+
{
|
20
|
+
"url" => "https://api.github.com/repos/octocat/Hello-World/downloads/1",
|
21
|
+
"html_url" => "https://github.com/repos/octocat/Hello-World/downloads/filename",
|
22
|
+
"id" => 1,
|
23
|
+
"name" => "file.zip",
|
24
|
+
"description" => "The latest release",
|
25
|
+
"size" => 1024,
|
26
|
+
"download_count" => 40
|
27
|
+
}
|
28
|
+
])
|
29
|
+
@downloads.list.size.should == 1
|
30
|
+
@downloads.list.first.description.should == "The latest release"
|
31
|
+
end
|
32
|
+
|
33
|
+
it "raises if the response is not successful" do
|
34
|
+
@client.stubs(:get).with("/repos/username/somerepo/downloads").returns unsuccessful_response
|
35
|
+
proc { @downloads.list }.should raise_error(Github::Downloads::UnexpectedResponse)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#upload" do
|
40
|
+
|
41
|
+
before :each do
|
42
|
+
@uploader = mock("uploader")
|
43
|
+
@downloads.uploader = @uploader
|
44
|
+
end
|
45
|
+
|
46
|
+
it "posts the file metadata to the server" do
|
47
|
+
@client.expects(:post).with("/repos/username/somerepo/downloads", {
|
48
|
+
:name => "textfile.txt",
|
49
|
+
:description => "an example file",
|
50
|
+
:content_type => "text/plain",
|
51
|
+
:size => File.size?("fixtures/textfile.txt")
|
52
|
+
}).returns(unsuccessful_response)
|
53
|
+
|
54
|
+
@downloads.create("fixtures/textfile.txt", "an example file")
|
55
|
+
end
|
56
|
+
|
57
|
+
it "passes the upload response data to the uploader if successful" do
|
58
|
+
@client.stubs(:post).returns successful_response_with("upload data")
|
59
|
+
@uploader.expects(:upload).with(File.expand_path("fixtures/textfile.txt"), "upload data")
|
60
|
+
@downloads.create("fixtures/textfile.txt", "an example file")
|
61
|
+
end
|
62
|
+
|
63
|
+
it "returns the URL returned by the uploader if successful" do
|
64
|
+
@client.stubs(:post).returns successful_response_with("upload data")
|
65
|
+
@uploader.stubs(:upload).returns("http://www.example.com/download")
|
66
|
+
@downloads.create("fixtures/textfile.txt", "an example file").should == "http://www.example.com/download"
|
67
|
+
end
|
68
|
+
|
69
|
+
it "returns false without uploading anything when upload post fails" do
|
70
|
+
@client.stubs(:post).returns unsuccessful_response
|
71
|
+
@uploader.expects(:upload).never
|
72
|
+
@downloads.create("fixtures/textfile.txt", "an example file").should be_false
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def unsuccessful_response
|
80
|
+
stub("response", :success? => false)
|
81
|
+
end
|
82
|
+
|
83
|
+
def successful_response_with(data)
|
84
|
+
stub("response", :success? => true, :data => data)
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "bundler/setup"
|
3
|
+
require 'rspec'
|
4
|
+
require 'mocha'
|
5
|
+
require 'mimic'
|
6
|
+
require 'ap'
|
7
|
+
|
8
|
+
$:.unshift(File.join(File.dirname(__FILE__), *%w[.. lib]))
|
9
|
+
|
10
|
+
Rspec.configure do |config|
|
11
|
+
config.color_enabled = true
|
12
|
+
config.mock_with :mocha
|
13
|
+
config.before(:each) do
|
14
|
+
Mimic.cleanup!
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
USE_CHARLES_PROXY = false
|
19
|
+
|
20
|
+
def mimic_port
|
21
|
+
if USE_CHARLES_PROXY
|
22
|
+
11989
|
23
|
+
else
|
24
|
+
Mimic::MIMIC_DEFAULT_PORT
|
25
|
+
end
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: github-downloads
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Luke Redpath
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-06-27 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rest-client
|
22
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ~>
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
hash: 9
|
28
|
+
segments:
|
29
|
+
- 1
|
30
|
+
- 6
|
31
|
+
- 3
|
32
|
+
version: 1.6.3
|
33
|
+
prerelease: false
|
34
|
+
type: :runtime
|
35
|
+
requirement: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: json
|
38
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 5
|
44
|
+
segments:
|
45
|
+
- 1
|
46
|
+
- 5
|
47
|
+
- 3
|
48
|
+
version: 1.5.3
|
49
|
+
prerelease: false
|
50
|
+
type: :runtime
|
51
|
+
requirement: *id002
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: simpleconsole
|
54
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ~>
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 25
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
- 1
|
63
|
+
- 1
|
64
|
+
version: 0.1.1
|
65
|
+
prerelease: false
|
66
|
+
type: :runtime
|
67
|
+
requirement: *id003
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: hirb
|
70
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
hash: 5
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
- 4
|
79
|
+
- 5
|
80
|
+
version: 0.4.5
|
81
|
+
prerelease: false
|
82
|
+
type: :runtime
|
83
|
+
requirement: *id004
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: mime-types
|
86
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ~>
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
hash: 87
|
92
|
+
segments:
|
93
|
+
- 1
|
94
|
+
- 16
|
95
|
+
- 0
|
96
|
+
version: 1.16.0
|
97
|
+
prerelease: false
|
98
|
+
type: :runtime
|
99
|
+
requirement: *id005
|
100
|
+
- !ruby/object:Gem::Dependency
|
101
|
+
name: highline
|
102
|
+
version_requirements: &id006 !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ~>
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
hash: 11
|
108
|
+
segments:
|
109
|
+
- 1
|
110
|
+
- 6
|
111
|
+
- 2
|
112
|
+
version: 1.6.2
|
113
|
+
prerelease: false
|
114
|
+
type: :runtime
|
115
|
+
requirement: *id006
|
116
|
+
- !ruby/object:Gem::Dependency
|
117
|
+
name: rspec
|
118
|
+
version_requirements: &id007 !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
hash: 3
|
124
|
+
segments:
|
125
|
+
- 0
|
126
|
+
version: "0"
|
127
|
+
prerelease: false
|
128
|
+
type: :development
|
129
|
+
requirement: *id007
|
130
|
+
- !ruby/object:Gem::Dependency
|
131
|
+
name: mocha
|
132
|
+
version_requirements: &id008 !ruby/object:Gem::Requirement
|
133
|
+
none: false
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
hash: 3
|
138
|
+
segments:
|
139
|
+
- 0
|
140
|
+
version: "0"
|
141
|
+
prerelease: false
|
142
|
+
type: :development
|
143
|
+
requirement: *id008
|
144
|
+
- !ruby/object:Gem::Dependency
|
145
|
+
name: mimic
|
146
|
+
version_requirements: &id009 !ruby/object:Gem::Requirement
|
147
|
+
none: false
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
hash: 3
|
152
|
+
segments:
|
153
|
+
- 0
|
154
|
+
version: "0"
|
155
|
+
prerelease: false
|
156
|
+
type: :development
|
157
|
+
requirement: *id009
|
158
|
+
- !ruby/object:Gem::Dependency
|
159
|
+
name: awesome_print
|
160
|
+
version_requirements: &id010 !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
hash: 3
|
166
|
+
segments:
|
167
|
+
- 0
|
168
|
+
version: "0"
|
169
|
+
prerelease: false
|
170
|
+
type: :development
|
171
|
+
requirement: *id010
|
172
|
+
description:
|
173
|
+
email: luke@lukeredpath.co.uk
|
174
|
+
executables:
|
175
|
+
- github-downloads
|
176
|
+
extensions: []
|
177
|
+
|
178
|
+
extra_rdoc_files: []
|
179
|
+
|
180
|
+
files:
|
181
|
+
- Gemfile
|
182
|
+
- Gemfile.lock
|
183
|
+
- Rakefile
|
184
|
+
- README.md
|
185
|
+
- bin/github-downloads
|
186
|
+
- spec/fixtures/textfile.txt
|
187
|
+
- spec/github_client_spec.rb
|
188
|
+
- spec/github_download_spec.rb
|
189
|
+
- spec/spec_helper.rb
|
190
|
+
- lib/github/client.rb
|
191
|
+
- lib/github/downloads.rb
|
192
|
+
- lib/github/downloads_controller.rb
|
193
|
+
- lib/github/s3_uploader.rb
|
194
|
+
- lib/github.rb
|
195
|
+
homepage: http://lukeredpath.co.uk
|
196
|
+
licenses: []
|
197
|
+
|
198
|
+
post_install_message:
|
199
|
+
rdoc_options: []
|
200
|
+
|
201
|
+
require_paths:
|
202
|
+
- lib
|
203
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
204
|
+
none: false
|
205
|
+
requirements:
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
hash: 3
|
209
|
+
segments:
|
210
|
+
- 0
|
211
|
+
version: "0"
|
212
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
213
|
+
none: false
|
214
|
+
requirements:
|
215
|
+
- - ">="
|
216
|
+
- !ruby/object:Gem::Version
|
217
|
+
hash: 3
|
218
|
+
segments:
|
219
|
+
- 0
|
220
|
+
version: "0"
|
221
|
+
requirements: []
|
222
|
+
|
223
|
+
rubyforge_project:
|
224
|
+
rubygems_version: 1.8.5
|
225
|
+
signing_key:
|
226
|
+
specification_version: 3
|
227
|
+
summary: Manages downloads for your Github projects
|
228
|
+
test_files: []
|
229
|
+
|