worochi 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +5 -0
- data/lib/worochi/agent/dropbox.rb +82 -0
- data/lib/worochi/agent/github.rb +199 -0
- data/lib/worochi/agent/sample.rb +64 -0
- data/lib/worochi/agent.rb +143 -0
- data/lib/worochi/config.rb +23 -0
- data/lib/worochi/error.rb +2 -0
- data/lib/worochi/helper/github.rb +99 -0
- data/lib/worochi/helper.rb +34 -0
- data/lib/worochi/item.rb +139 -0
- data/lib/worochi/log.rb +41 -0
- data/lib/worochi.rb +114 -0
- data/worochi.gemspec +14 -0
- metadata +59 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2ca726002689f7fe66fe07d7ff2af93a5fc6b2e4
|
4
|
+
data.tar.gz: 27f7cfdcd912eeb33d9103969c59b192a79d47ab
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 57fbb09d74618b9544fe5e2c8651637b1393cd97a76dec396474286393c6552ed8bfc6a568a57f60986f38665f1498352d87e0cb893e7a341d2e1034b3f39bf0
|
7
|
+
data.tar.gz: c0519b4338dbeabc4365776293935c6a6e6156c4ded75ed86756ab4d4d8c9c17807a27c013b640c33a0a4de359d44dc9c8d579871fd52e181b99518d54514639
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'dropbox_sdk'
|
2
|
+
|
3
|
+
class Worochi
|
4
|
+
# The {Agent} for Dropbox API. This wraps around the `dropbox-sdk` gem.
|
5
|
+
# @see https://www.dropbox.com/developers/core/start/ruby
|
6
|
+
class Agent::Dropbox < Agent
|
7
|
+
# @return [Hash] default options for Dropbox
|
8
|
+
def default_options
|
9
|
+
{
|
10
|
+
chunk_size: 2*1024*1024,
|
11
|
+
overwrite: true,
|
12
|
+
dir: '/'
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Initializes Dropbox SDK client. Refer to
|
17
|
+
# {https://www.dropbox.com/developers/core/start/ruby
|
18
|
+
# official Dropbox documentation}.
|
19
|
+
#
|
20
|
+
# @return [DropboxClient]
|
21
|
+
def init_client
|
22
|
+
@client = DropboxClient.new(options[:token])
|
23
|
+
end
|
24
|
+
|
25
|
+
# Push a single {Item} to Dropbox.
|
26
|
+
#
|
27
|
+
# @param [Item]
|
28
|
+
# @return [nil]
|
29
|
+
def push_item(item)
|
30
|
+
Worochi::Log.debug "Uploading #{item.path} (#{item.size} bytes) to Dropbox..."
|
31
|
+
if item.size > options[:chunk_size]
|
32
|
+
push_item_chunked(item)
|
33
|
+
else
|
34
|
+
@client.put_file(full_path(item), item.content, options[:overwrite])
|
35
|
+
end
|
36
|
+
Worochi::Log.debug "Uploaded"
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns a list of files and subdirectories at the remote path specified
|
41
|
+
# by `options[:dir]`.
|
42
|
+
#
|
43
|
+
# @return [Array<Hash>] list of files and subdirectories
|
44
|
+
def list
|
45
|
+
remote_path = options[:dir]
|
46
|
+
begin
|
47
|
+
response = @client.metadata(remote_path)
|
48
|
+
rescue DropboxError
|
49
|
+
raise Error, 'Invalid Dropbox folder specified'
|
50
|
+
end
|
51
|
+
|
52
|
+
response['contents'].map do |elem|
|
53
|
+
{
|
54
|
+
name: elem['path'].split('/').last,
|
55
|
+
path: elem['path'],
|
56
|
+
type: elem['is_dir'] ? 'folder' : 'file'
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Uses the `/chunked_upload` endpoint to push large files to Dropbox.
|
62
|
+
# Refer to {https://www.dropbox.com/developers/core/docs#chunked-upload
|
63
|
+
# API documentation}.
|
64
|
+
#
|
65
|
+
# @param [Item]
|
66
|
+
# @return [nil]
|
67
|
+
def push_item_chunked(item)
|
68
|
+
Worochi::Log.debug "Using chunk uploader..."
|
69
|
+
uploader = @client.get_chunked_uploader(item.content, item.size)
|
70
|
+
while uploader.offset < uploader.total_size
|
71
|
+
begin
|
72
|
+
uploader.upload(options[:chunk_size])
|
73
|
+
rescue DropboxError
|
74
|
+
raise Error, 'Dropbox chunk upload failed'
|
75
|
+
end
|
76
|
+
Worochi::Log.debug "Uploaded #{uploader.offset} bytes"
|
77
|
+
end
|
78
|
+
uploader.finish(full_path(item), options[:overwrite])
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'octokit'
|
2
|
+
require 'base64'
|
3
|
+
require 'worochi/helper/github'
|
4
|
+
|
5
|
+
class Worochi
|
6
|
+
# The {Agent} for GitHub API. This wraps around the `octokit` gem.
|
7
|
+
# @see https://github.com/octokit/octokit.rb
|
8
|
+
class Agent::Github < Agent
|
9
|
+
|
10
|
+
# @return [Hash] default options for GitHub
|
11
|
+
def default_options
|
12
|
+
{
|
13
|
+
source: 'master',
|
14
|
+
target: 'worochi',
|
15
|
+
repo: 'darkmirage/test',
|
16
|
+
block_size: Worochi::Helper::Github::BLOCK_SIZE,
|
17
|
+
commit_msg: 'Empty commit message',
|
18
|
+
dir: '/'
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Initializes Octokit client. Refer to
|
23
|
+
# {https://github.com/octokit/octokit.rb
|
24
|
+
# octokit.rb documentation}.
|
25
|
+
#
|
26
|
+
# @return [Octokit::Client]
|
27
|
+
def init_client
|
28
|
+
@client = Octokit::Client.new(login: 'me', oauth_token: options[:token])
|
29
|
+
end
|
30
|
+
|
31
|
+
# Pushes a list of {Item} to GitHub.
|
32
|
+
#
|
33
|
+
# @param [Array<Item>]
|
34
|
+
# @return [nil]
|
35
|
+
# @see Agent#push_items
|
36
|
+
def push_all(items)
|
37
|
+
source_sha = source_branch
|
38
|
+
items.each { |item| source_sha = tree_append(source_sha, item) }
|
39
|
+
commit = @client.create_commit(repo, options[:commit_msg], source_sha,
|
40
|
+
target_branch)
|
41
|
+
@client.update_ref(repo, "heads/#{options[:target]}", commit.sha)
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
# Pushes a single {Item} to GitHub. This means making a new commit for each
|
46
|
+
# file. Not recommended and should just use {#push_all} instead.
|
47
|
+
#
|
48
|
+
# @param [Item]
|
49
|
+
# @return [nil]
|
50
|
+
def push_item(item)
|
51
|
+
Worochi::Log.warn 'push_item should not be used for GitHub'
|
52
|
+
push_all([item])
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns a list of files and subdirectories at the remote path specified
|
56
|
+
# by `options[:dir]`.
|
57
|
+
#
|
58
|
+
# @return [Array<Hash>] list of files and subdirectories
|
59
|
+
def list
|
60
|
+
remote_path = options[:dir].sub(/^\//, '').sub(/\/$/, '')
|
61
|
+
|
62
|
+
result = @client.tree(repo, source_branch, recursive: true).tree
|
63
|
+
result.sort! do |x, y|
|
64
|
+
x.path.split('/').size <=> y.path.split('/').size
|
65
|
+
end
|
66
|
+
|
67
|
+
# Checks that folders are at the requested path and not at a lower or
|
68
|
+
# higher level
|
69
|
+
result.reject! do |elem|
|
70
|
+
!elem.path.match(remote_path + '($|\/.+)') ||
|
71
|
+
(File.join(remote_path,
|
72
|
+
elem.path.split('/').last).sub(/^\//, '') != elem.path)
|
73
|
+
end
|
74
|
+
|
75
|
+
result.map do |elem|
|
76
|
+
{
|
77
|
+
name: elem.path.split('/').last,
|
78
|
+
path: elem.path,
|
79
|
+
type: elem.type == 'tree' ? 'folder' : 'file',
|
80
|
+
sha: elem.sha
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns a list of repositories for the remote branch specified by
|
86
|
+
# `options[:source]`. If `opts[:push]` is `true`, then only repos with
|
87
|
+
# push access are returned. If `opts[:details]` is `true`, returns hashes
|
88
|
+
# containing more information about each repo.
|
89
|
+
#
|
90
|
+
# @param opts [Hash]
|
91
|
+
# @return [Array<String>, Array<Hash>] a list of repositories
|
92
|
+
def repos(opts={ push: false, details: false, orgs: true })
|
93
|
+
repos = @client.repositories.map {|repo| parse_repo repo}
|
94
|
+
@client.organizations.each do |org|
|
95
|
+
repos += @client.organization_repositories(org.login).map {|repo| parse_repo repo}
|
96
|
+
end
|
97
|
+
repos.reject! {|repo| !repo[:push]} if opts[:push]
|
98
|
+
repos.map { |repo| repo[:full_name] } unless opts[:details]
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
# Appends an item to the existing tree.
|
103
|
+
#
|
104
|
+
# @param tree_sha [String] SHA1 checksum of the root tree
|
105
|
+
# @param item [Item] the item to append
|
106
|
+
# @return [String] SHA1 checksum of the resulting tree
|
107
|
+
def tree_append(tree_sha, item)
|
108
|
+
child = {
|
109
|
+
path: full_path(item).gsub(/^\//, ''),
|
110
|
+
sha: push_blob(item),
|
111
|
+
type: 'blob',
|
112
|
+
mode: '100644'
|
113
|
+
}
|
114
|
+
new_tree = @client.create_tree(repo, [child], base_tree: tree_sha)
|
115
|
+
new_tree.sha
|
116
|
+
end
|
117
|
+
|
118
|
+
# Pushes a single item to GitHub and returns the blob SHA1 checksum.
|
119
|
+
#
|
120
|
+
# @param item [Item]
|
121
|
+
# @return [String] SHA1 checksum of the created blob
|
122
|
+
def push_blob(item)
|
123
|
+
Worochi::Log.debug "Uploading #{item.path} (#{item.size} bytes) to GitHub..."
|
124
|
+
if item.size > options[:block_size]
|
125
|
+
sha = stream_blob(item)
|
126
|
+
else
|
127
|
+
sha = @client.create_blob(repo, Base64.strict_encode64(item.read), 'base64')
|
128
|
+
end
|
129
|
+
Worochi::Log.debug "Uploaded [#{sha}]"
|
130
|
+
sha
|
131
|
+
end
|
132
|
+
|
133
|
+
# Pushes a single item to GitHub using JSON streaming and returns the SHA1
|
134
|
+
# checksum
|
135
|
+
#
|
136
|
+
# @param item [Item]
|
137
|
+
# @return [String] SHA1 checksum of the created blob
|
138
|
+
def stream_blob(item)
|
139
|
+
Worochi::Log.debug "Using JSON streaming..."
|
140
|
+
post_stream = Worochi::Helper::Github::StreamIO.new(item)
|
141
|
+
|
142
|
+
uri = URI("https://api.github.com/repos/#{repo}/git/blobs")
|
143
|
+
request = Net::HTTP::Post.new(uri.path)
|
144
|
+
request.content_length = post_stream.size
|
145
|
+
request.content_type = 'application/x-www-form-urlencoded'
|
146
|
+
request.add_field('Authorization', "token #{options[:token]}")
|
147
|
+
request.body_stream = post_stream
|
148
|
+
|
149
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
150
|
+
http.use_ssl = (uri.scheme == 'https')
|
151
|
+
http.request request do |response|
|
152
|
+
return JSON.parse(response.body)['sha']
|
153
|
+
end
|
154
|
+
|
155
|
+
raise Error, 'Failed to upload file to GitHub'
|
156
|
+
end
|
157
|
+
|
158
|
+
# @return [Hash] repo information
|
159
|
+
def parse_repo(repo)
|
160
|
+
{
|
161
|
+
name: repo.name,
|
162
|
+
full_name: repo.full_name,
|
163
|
+
owner: repo.owner.login,
|
164
|
+
description: repo.description,
|
165
|
+
url: repo.url,
|
166
|
+
push: repo.permissions.push,
|
167
|
+
pull: repo.permissions.pull
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
# Returns the SHA1 checksum of the source branch.
|
172
|
+
#
|
173
|
+
# @return [String] SHA1 checksum
|
174
|
+
def source_branch
|
175
|
+
@client.branch(repo, options[:source]).commit.sha
|
176
|
+
end
|
177
|
+
|
178
|
+
# Returns the SHA1 checksum of the target branch. Clones source branch if
|
179
|
+
# target branch does not exist
|
180
|
+
#
|
181
|
+
# @return [String] SHA1 checksum
|
182
|
+
def target_branch
|
183
|
+
begin
|
184
|
+
sha = @client.branch(repo, options[:target]).commit.sha
|
185
|
+
rescue
|
186
|
+
ref = @client.create_ref(repo, "heads/#{options[:target]}", source_branch)
|
187
|
+
sha = ref.object.sha
|
188
|
+
end
|
189
|
+
sha
|
190
|
+
end
|
191
|
+
|
192
|
+
# An alias for `options[:repo]`
|
193
|
+
#
|
194
|
+
# @return [String] full repo name
|
195
|
+
def repo
|
196
|
+
options[:repo]
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class Worochi
|
2
|
+
# The {Agent} for a sample API.
|
3
|
+
# This is a sample of methods that should be implemented by a service agent.
|
4
|
+
class Agent::Sample < Agent
|
5
|
+
|
6
|
+
# @return [Hash] default options for sample API
|
7
|
+
# @see Agent#set_options
|
8
|
+
def default_options
|
9
|
+
{
|
10
|
+
dir: '/'
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
# This is where the service-specific API client should be initialized.
|
15
|
+
#
|
16
|
+
# @return [ApiClient]
|
17
|
+
# @see Agent#initialize
|
18
|
+
def init_client
|
19
|
+
@client = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
# Defined for services that need to push all the items together as a
|
23
|
+
# batch. For example, GitHub needs to make one commit for all the files.
|
24
|
+
# Usually a service-specific implementation should only need one of either
|
25
|
+
# {#push_item} or {#push_all}.
|
26
|
+
#
|
27
|
+
# @return [nil]
|
28
|
+
# @see Agent#push_items
|
29
|
+
def push_all(items)
|
30
|
+
items.each { |item| @client.push(item.content, item.path) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Pushes an individual item to the service. Usually a service-specific
|
34
|
+
# implementation should only need one of either {#push_item} or
|
35
|
+
# {#push_all}.
|
36
|
+
#
|
37
|
+
# @return [nil]
|
38
|
+
# @see Agent#push_items
|
39
|
+
def push_item(item)
|
40
|
+
@client.push(item.content, item.path)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns a list of files and subdirectories at the remote path specified
|
44
|
+
# by `options[:dir]`. Each file entry is a hash that should contain at
|
45
|
+
# least the keys `:name`, `:path`, and `:type`, but can also contain other
|
46
|
+
# service-specific meta information.
|
47
|
+
#
|
48
|
+
# @return [Array<Hash>] list of files and subdirectories
|
49
|
+
# @see Agent#folders
|
50
|
+
# @see Agent#files
|
51
|
+
def list
|
52
|
+
result = @client.get_file_list
|
53
|
+
result.map do |elem|
|
54
|
+
{
|
55
|
+
name: elem.name,
|
56
|
+
path: elem.path,
|
57
|
+
type: elem.type
|
58
|
+
}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Service specific methods
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
class Worochi
|
2
|
+
# The parent class for all service agents.
|
3
|
+
class Agent
|
4
|
+
# Service name.
|
5
|
+
# @return [Symbol]
|
6
|
+
attr_reader :type
|
7
|
+
# Service options.
|
8
|
+
# @return [Hash]
|
9
|
+
attr_accessor :options
|
10
|
+
|
11
|
+
# @param opts [Hash] service options
|
12
|
+
def initialize(opts={})
|
13
|
+
set_options(opts)
|
14
|
+
@type = options[:service]
|
15
|
+
init_client
|
16
|
+
end
|
17
|
+
|
18
|
+
# Push list of files to the service. Refer to {Item.open} for how to
|
19
|
+
# format the file list. An optional `opts` hash can be used to update the
|
20
|
+
# agent options before pushing.
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# agent = Worochi.create(:github, 'sfsFj41na89cx')
|
24
|
+
# agent.push({ source: 'http://a.com/file.jpg', path: 'folder/file.jpg' })
|
25
|
+
#
|
26
|
+
# @param origin [Array<Hash>, Array<String>, Hash, String]
|
27
|
+
# @param opts [Hash] update agent options before pushing
|
28
|
+
# @return [nil]
|
29
|
+
# @see Item.open
|
30
|
+
def push(origin, opts=nil)
|
31
|
+
set_options(opts) unless opts.nil?
|
32
|
+
items = Item.open(origin)
|
33
|
+
push_items(items)
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
# Push a list of {Item} to the service. Usually called by {#push}.
|
38
|
+
#
|
39
|
+
# @param items [Array<Item>]
|
40
|
+
# @return [nil]
|
41
|
+
def push_items(items)
|
42
|
+
items.each { |item| item.content.rewind }
|
43
|
+
Worochi::Log.info "Pushing #{items.size} items to #{type}"
|
44
|
+
if respond_to?(:push_all)
|
45
|
+
push_all(items)
|
46
|
+
else
|
47
|
+
items.each { |item| push_item(item) }
|
48
|
+
end
|
49
|
+
Worochi::Log.info "Push to #{type} completed"
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
# Remove the agent from the list of active agents responding to calls to
|
54
|
+
# {Worochi.push}.
|
55
|
+
#
|
56
|
+
# @return [nil]
|
57
|
+
def remove
|
58
|
+
Worochi.remove(self)
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns a list of subdirectories at the remote path specified by
|
63
|
+
# `options[:dir]`. Relies on the service-specific implementation of
|
64
|
+
# `#list`.
|
65
|
+
#
|
66
|
+
# @return [Array<Hash>] list of subdirectories
|
67
|
+
def folders(details=false)
|
68
|
+
result = list.reject { |elem| elem[:type] != 'folder' }
|
69
|
+
result.map { |elem| elem[:name] } unless details
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns a list of files at the remote path specified by `options[:dir]`.
|
73
|
+
# Relies on the service-specific implementation of `#list`.
|
74
|
+
#
|
75
|
+
# @return [Array<Hash>] list of files
|
76
|
+
def files(details=false)
|
77
|
+
result = list.reject { |elem| elem[:type] != 'file' }
|
78
|
+
result.map { |elem| elem[:name] } unless details
|
79
|
+
end
|
80
|
+
|
81
|
+
# Updates {.options} using `opts`.
|
82
|
+
#
|
83
|
+
# @param opts [Hash] new options
|
84
|
+
# @return [Hash] the updated options
|
85
|
+
def set_options(opts={})
|
86
|
+
self.options ||= default_options
|
87
|
+
options.merge!(opts)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Sets the remote target directory path. This is the same as modifying
|
91
|
+
# `options[:dir]`.
|
92
|
+
#
|
93
|
+
# @param path [String] the new path
|
94
|
+
# @return [Hash] the updated options
|
95
|
+
def set_dir(path)
|
96
|
+
options[:dir] = path
|
97
|
+
options
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
# @return [String] full path combining remote directory and item path
|
102
|
+
def full_path(item)
|
103
|
+
File.join(options[:dir], item.path)
|
104
|
+
end
|
105
|
+
|
106
|
+
# @return [Hash] default options for agents that do not override this
|
107
|
+
def default_options
|
108
|
+
{ dir: '/' }
|
109
|
+
end
|
110
|
+
|
111
|
+
class << self
|
112
|
+
public
|
113
|
+
# Creates a new service-specific {Agent} based on `:service`.
|
114
|
+
#
|
115
|
+
# @example
|
116
|
+
# Worochi::Agent.new({ service: :github, token:'6st46setsybhd64' })
|
117
|
+
#
|
118
|
+
# @param opts [Hash] service options; must contain `:service` key.
|
119
|
+
# @return [Agent]
|
120
|
+
def new(opts={})
|
121
|
+
service = opts[:service]
|
122
|
+
if self.name == 'Worochi::Agent'
|
123
|
+
raise Error, 'Invalid service' unless Config.services.include?(service)
|
124
|
+
Agent.const_get(class_name(service)).new(opts)
|
125
|
+
else
|
126
|
+
super
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
# Returns the class name for the {Agent} given a service name
|
132
|
+
#
|
133
|
+
# @return [String]
|
134
|
+
def class_name(service)
|
135
|
+
service.to_s.split('_').map{|e| e.capitalize}.join
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
Worochi::Config.services.each do |service|
|
142
|
+
require "worochi/agent/#{service}"
|
143
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Worochi
|
2
|
+
# Configurations for Worochi.
|
3
|
+
module Config
|
4
|
+
@services = [:github, :dropbox]
|
5
|
+
@s3_bucket = 'data-pixelapse'
|
6
|
+
@s3_prefix = 's3'
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# Array of service names.
|
10
|
+
# @return [Array<Symbol>]
|
11
|
+
attr_reader :services
|
12
|
+
|
13
|
+
# Name of S3 bucket.
|
14
|
+
# @return [String]
|
15
|
+
attr_reader :s3_bucket
|
16
|
+
|
17
|
+
# Prefix for S3 resource paths.
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :s3_prefix
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
class Worochi
|
4
|
+
# Helper classes for GitHub JSON streaming.
|
5
|
+
module Helper::Github
|
6
|
+
|
7
|
+
BLOCK_SIZE = 12288
|
8
|
+
|
9
|
+
# This is a wrapper that produces a JSON stream that works with
|
10
|
+
# {Net::HTTP::Post#body_stream}.
|
11
|
+
class StreamIO
|
12
|
+
|
13
|
+
def initialize(item)
|
14
|
+
item.content.rewind
|
15
|
+
@parts = [
|
16
|
+
StringIO.new('{"content":"'),
|
17
|
+
Base64IO.new(item.content),
|
18
|
+
StringIO.new('","encoding":"base64"}')
|
19
|
+
]
|
20
|
+
@part_no = 0
|
21
|
+
@size = @parts.inject(0) {|sum, p| sum + p.size}
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [Integer] size of the JSON
|
25
|
+
def size
|
26
|
+
@size
|
27
|
+
end
|
28
|
+
|
29
|
+
# Rewind each component of the stream.
|
30
|
+
#
|
31
|
+
# @return [nil]
|
32
|
+
def rewind
|
33
|
+
@parts.each { |part| part.rewind }
|
34
|
+
@part_no = 0
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param length [Integer]
|
39
|
+
# @param outbuf [IO]
|
40
|
+
# @return [String]
|
41
|
+
def read(length=nil, outbuf=nil)
|
42
|
+
return length.nil? ? '' : nil if @part_no >= @parts.size
|
43
|
+
|
44
|
+
length = length || size
|
45
|
+
output = @parts[@part_no].read(length).to_s
|
46
|
+
|
47
|
+
if output.nil?
|
48
|
+
return nil if @part_no >= @parts.size
|
49
|
+
output = ''
|
50
|
+
end
|
51
|
+
|
52
|
+
while output.length < length
|
53
|
+
@part_no += 1
|
54
|
+
break if @part_no == @parts.size
|
55
|
+
output += @parts[@part_no].read(length - output.length).to_s
|
56
|
+
end
|
57
|
+
if not outbuf.nil?
|
58
|
+
outbuf.clear
|
59
|
+
outbuf.insert(0, output)
|
60
|
+
end
|
61
|
+
|
62
|
+
output
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# This is a wrapper around the file content that streams the file as a
|
67
|
+
# Base64 encoded string.
|
68
|
+
class Base64IO
|
69
|
+
def initialize(file)
|
70
|
+
file.rewind
|
71
|
+
@file = file
|
72
|
+
@encoded_size = (@file.size / 3.0).ceil * 4
|
73
|
+
@buffer = ''
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [Integer] size of the JSON
|
77
|
+
def size
|
78
|
+
@encoded_size
|
79
|
+
end
|
80
|
+
|
81
|
+
# Rewind the stream.
|
82
|
+
def rewind
|
83
|
+
@file.rewind
|
84
|
+
@buffer = ''
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
|
88
|
+
# @param length [Integer]
|
89
|
+
# @param outbuf [IO]
|
90
|
+
# @return [String]
|
91
|
+
def read(length=size, outbuf=nil)
|
92
|
+
while @buffer.length < length and not @file.eof?
|
93
|
+
@buffer += Base64.strict_encode64 @file.read(BLOCK_SIZE)
|
94
|
+
end
|
95
|
+
@buffer.empty? ? nil : @buffer.slice!(0, length)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'aws-sdk' unless Worochi::Config.s3_bucket.nil?
|
2
|
+
|
3
|
+
class Worochi
|
4
|
+
module Helper
|
5
|
+
class << self
|
6
|
+
# A regex generated from {Config.s3_prefix} for determining if a given
|
7
|
+
# String is an S3 path.
|
8
|
+
#
|
9
|
+
# @return [Regexp]
|
10
|
+
def s3_prefix_re
|
11
|
+
/^#{Config.s3_prefix}\:/
|
12
|
+
end
|
13
|
+
|
14
|
+
# Given an S3 path, return the full URL for the corresponding object
|
15
|
+
# determined using the AWS SDK.
|
16
|
+
#
|
17
|
+
# @param path [String]
|
18
|
+
# @return [URI::HTTP]
|
19
|
+
def s3_url(path)
|
20
|
+
raise Error, 'S3 bucket name is not defined' if Config.s3_bucket.nil?
|
21
|
+
path = path.sub(s3_prefix_re, '')
|
22
|
+
AWS::S3.new.buckets[Config.s3_bucket].objects[path].url_for(:read)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Check if a given path is an S3 path.
|
26
|
+
#
|
27
|
+
# @param path [String]
|
28
|
+
# @return [Boolean]
|
29
|
+
def is_s3_path?(path)
|
30
|
+
!s3_prefix_re.match(path).nil?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/worochi/item.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
class Worochi
|
4
|
+
# This represents a single file that is being pushed. The {#content}
|
5
|
+
# attribute holds an IO object that is read by the push agent and the
|
6
|
+
# {#path} attribute is the relative path to the remote target directory that
|
7
|
+
# the file will be pushed to.
|
8
|
+
class Item
|
9
|
+
# The relative path of the object from the target root directory. If
|
10
|
+
# the {Item} was initialized without a `:path` option, this attribute
|
11
|
+
# defaults to the file name.
|
12
|
+
# @return [String]
|
13
|
+
attr_accessor :path
|
14
|
+
|
15
|
+
# An IO object containing the content of the file being pushed.
|
16
|
+
# @return [IO]
|
17
|
+
attr_accessor :content
|
18
|
+
def initialize(opts={})
|
19
|
+
@path = opts[:path]
|
20
|
+
raise Error, 'Missing Item content' if !opts[:content]
|
21
|
+
@content = opts[:content]
|
22
|
+
@content.rewind
|
23
|
+
end
|
24
|
+
|
25
|
+
# The total size of the content in bytes.
|
26
|
+
#
|
27
|
+
# @return [Integer]
|
28
|
+
def size
|
29
|
+
content.size
|
30
|
+
end
|
31
|
+
|
32
|
+
# Read from the content. This is just a wrapper for the `#read` method on
|
33
|
+
# {#content} and any arguments will be passed on to the IO object.
|
34
|
+
#
|
35
|
+
# @return [String]
|
36
|
+
def read(*args)
|
37
|
+
content.read(args)
|
38
|
+
end
|
39
|
+
|
40
|
+
class << self
|
41
|
+
# Takes in either a single file entry or a list of file entries and
|
42
|
+
# parses them into a list of {Item} objects. Each entry can be either a
|
43
|
+
# String specifying the source location of the file, or a Hash
|
44
|
+
# specifying both the `:source` location and the remote `:path` to push
|
45
|
+
# the file to.
|
46
|
+
#
|
47
|
+
# @example Single file
|
48
|
+
# Item.open('folder/file.txt')
|
49
|
+
# @example Multiple files
|
50
|
+
# Item.open(['folder/file1.txt', 'folder/file2.txt'])
|
51
|
+
# @example Remote path
|
52
|
+
# Item.open({
|
53
|
+
# source: 'http://a.com/file.jpg',
|
54
|
+
# path: 'folder/file.jpg'
|
55
|
+
# })
|
56
|
+
# @example Remote path with mixed origins
|
57
|
+
# a = { source: 'http://a.com/file.jpg', path: 'folder/file1.jpg' }
|
58
|
+
# b = { source: 'folder/file.jpg', path: 'folder/file2.jpg' }
|
59
|
+
# c = { source: 's3:folder/file.jpg', path: 'folder/file3.jpg' }
|
60
|
+
# Item.open([a, b, c])
|
61
|
+
# # c is an example of retrieving files using an AWS S3 path
|
62
|
+
#
|
63
|
+
# @param origin [Array<Hash>, Array<String>, Hash, String]
|
64
|
+
# @return [Array<Item>]
|
65
|
+
def open(origin)
|
66
|
+
file_list = origin.kind_of?(Array) ? origin : [origin]
|
67
|
+
file_list.map { |entry| open_single(entry) }
|
68
|
+
end
|
69
|
+
|
70
|
+
# Takes in a single entry from {.open} and creates an {Item}.
|
71
|
+
#
|
72
|
+
# @param entry [Hash, String] the file metadata
|
73
|
+
# @return [Item] the file item
|
74
|
+
def open_single(entry)
|
75
|
+
if entry.kind_of?(Hash)
|
76
|
+
source = entry[:source]
|
77
|
+
path = entry[:path]
|
78
|
+
else
|
79
|
+
source = entry
|
80
|
+
end
|
81
|
+
|
82
|
+
Item.new({
|
83
|
+
path: path || File.basename(source),
|
84
|
+
content: retrieve(source)
|
85
|
+
})
|
86
|
+
end
|
87
|
+
|
88
|
+
# Retrieves the file content from `source`.
|
89
|
+
#
|
90
|
+
# @param source [String] local or remote location of the file content
|
91
|
+
# @return [File]
|
92
|
+
def retrieve(source)
|
93
|
+
if File.file?(source)
|
94
|
+
retrieve_local(source)
|
95
|
+
else
|
96
|
+
url = Helper.is_s3_path?(source) ? Helper.s3_url(source) : source
|
97
|
+
retrieve_remote(url)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Retrieves the local file.
|
102
|
+
#
|
103
|
+
# @param local_path [String] local path to the file
|
104
|
+
# @return [File]
|
105
|
+
def retrieve_local(local_path)
|
106
|
+
Worochi::Log.debug 'OPEN: ' + local_path
|
107
|
+
file = File.open(local_path)
|
108
|
+
Worochi::Log.debug "#{file.size} bytes"
|
109
|
+
file
|
110
|
+
end
|
111
|
+
|
112
|
+
# Downloads a remote file using {HTTP::Get}.
|
113
|
+
#
|
114
|
+
# @param file_url [String, URI] the URL of the file
|
115
|
+
# @return [Tempfile] the downloaded file
|
116
|
+
def retrieve_remote(file_url)
|
117
|
+
Worochi::Log.debug file_url.class
|
118
|
+
uri = URI(file_url)
|
119
|
+
Worochi::Log.debug 'GET: ' + uri.to_s
|
120
|
+
|
121
|
+
file = Tempfile.new('worochi_tmp_')
|
122
|
+
file.binmode
|
123
|
+
|
124
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
125
|
+
http.use_ssl = (uri.scheme == 'https')
|
126
|
+
request = Net::HTTP::Get.new uri
|
127
|
+
|
128
|
+
http.request request do |response|
|
129
|
+
response.read_body do |segment|
|
130
|
+
file.write segment
|
131
|
+
end
|
132
|
+
end
|
133
|
+
Worochi::Log.debug "Downloaded #{file.size} bytes"
|
134
|
+
file.rewind
|
135
|
+
file
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/lib/worochi/log.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
class Worochi
|
4
|
+
class Log
|
5
|
+
SEVERITY_COLOR = {
|
6
|
+
'DEBUG' => 37,
|
7
|
+
'INFO' => 32,
|
8
|
+
'WARN' => 33,
|
9
|
+
'ERROR' => 31,
|
10
|
+
'FATAL' => 31
|
11
|
+
}
|
12
|
+
class << self
|
13
|
+
def init_log
|
14
|
+
@logger = Logger.new(STDOUT)
|
15
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
16
|
+
"[\033[#{SEVERITY_COLOR[severity]}m#{severity}\033[0m]: #{msg}\n"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def debug(message)
|
21
|
+
init_log if @logger.nil?
|
22
|
+
@logger.debug message
|
23
|
+
end
|
24
|
+
|
25
|
+
def warn(message)
|
26
|
+
init_log if @logger.nil?
|
27
|
+
@logger.warn message
|
28
|
+
end
|
29
|
+
|
30
|
+
def info(message)
|
31
|
+
init_log if @logger.nil?
|
32
|
+
@logger.info message
|
33
|
+
end
|
34
|
+
|
35
|
+
def error(message)
|
36
|
+
init_log if @logger.nil?
|
37
|
+
@logger.error message
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/worochi.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'worochi/config'
|
2
|
+
require 'worochi/error'
|
3
|
+
require 'worochi/log'
|
4
|
+
require 'worochi/helper'
|
5
|
+
require 'worochi/item'
|
6
|
+
require 'worochi/agent'
|
7
|
+
|
8
|
+
class Worochi
|
9
|
+
@agents = []
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# List of {Worochi::Agent} waiting for {Worochi.push}.
|
13
|
+
#
|
14
|
+
# @return [Array]
|
15
|
+
attr_reader :agents
|
16
|
+
|
17
|
+
# Creates a new {Worochi::Agent} and adds it to the list of agents
|
18
|
+
# listening to {Worochi.push} requests.
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# Worochi.create(:dropbox, 'as89h38nFBUSHFfuh99f', { dir: '/folder' })
|
22
|
+
# @example
|
23
|
+
# opts = {
|
24
|
+
# repo: 'darkmirage/worochi',
|
25
|
+
# source: 'master',
|
26
|
+
# target: 'temp',
|
27
|
+
# commit_msg: 'Hello'
|
28
|
+
# }
|
29
|
+
# Worochi.create(:github, '6st46setsytgbhd64', opts)
|
30
|
+
#
|
31
|
+
# @param service [Symbol] service name as defined in {Config.services}
|
32
|
+
# @param token [String] authorization token for the service API
|
33
|
+
# @param opts [Hash] additional service-specific options
|
34
|
+
# @return [Worochi::Agent]
|
35
|
+
# @see Agent.new
|
36
|
+
def create(service, token, opts={})
|
37
|
+
opts[:service] = service
|
38
|
+
opts[:token] = token
|
39
|
+
agent = Agent.new(opts)
|
40
|
+
@agents << agent
|
41
|
+
agent
|
42
|
+
end
|
43
|
+
|
44
|
+
# Adds an exist {Worochi::Agent} to the list of agents listening to
|
45
|
+
# {Worochi.push} requests.
|
46
|
+
#
|
47
|
+
# @param agent [Worochi::Agent]
|
48
|
+
# @return [nil]
|
49
|
+
def add(agent)
|
50
|
+
@agents << agent
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
|
54
|
+
# Remove a specific {Worochi::Agent} from the list of agents listening to
|
55
|
+
# {Worochi.push} requests.
|
56
|
+
#
|
57
|
+
# @param agent [Worochi::Agent]
|
58
|
+
# @return [nil]
|
59
|
+
def remove(agent)
|
60
|
+
@agents.delete(agent)
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
# Remove all agents belonging to a given service from the list. Removes
|
65
|
+
# all agents if service is not specified. (See {.reset}).
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# Worochi.remove(:dropbox)
|
69
|
+
#
|
70
|
+
# @param service [Symbol] service name as defined in {Config.services}
|
71
|
+
# @return [nil]
|
72
|
+
def remove_service(service=nil)
|
73
|
+
if service.nil?
|
74
|
+
reset
|
75
|
+
else
|
76
|
+
@agents.reject! { |a| a.type == service }
|
77
|
+
end
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
|
81
|
+
# Removes all agents from the list
|
82
|
+
#
|
83
|
+
# @return [nil]
|
84
|
+
def reset
|
85
|
+
@agents.clear
|
86
|
+
end
|
87
|
+
|
88
|
+
# List the active agents.
|
89
|
+
#
|
90
|
+
# @return [Array<String>]
|
91
|
+
def list
|
92
|
+
@agents.map { |a| a.to_s }
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Integer] number of active agents.
|
96
|
+
def size
|
97
|
+
@agents.size
|
98
|
+
end
|
99
|
+
|
100
|
+
# Push list of files using the active agents in {.agents}. Refer to
|
101
|
+
# {Item.open} for how to format the file list.
|
102
|
+
#
|
103
|
+
# @param origin [Array<Hash>, Array<String>, Hash, String]
|
104
|
+
# @param opts [Hash] update agent options before pushing
|
105
|
+
# @return [Boolean] success
|
106
|
+
# @see Item.open
|
107
|
+
# @see Agent#push
|
108
|
+
def push(origin, opts={})
|
109
|
+
Log.warn 'No push targets specified' and return false if @agents.empty?
|
110
|
+
@agents.each { |agent| agent.push(origin, opts) }
|
111
|
+
true
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/worochi.gemspec
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'worochi'
|
3
|
+
s.version = '0.0.0'
|
4
|
+
s.date = '2013-08-02'
|
5
|
+
s.summary = 'Worochi'
|
6
|
+
s.description = 'Provides a standard way to interface with Ruby API wrappers provided by various cloud storage services such as Dropbox and Google Drive.'
|
7
|
+
s.authors = ['Raven Jiang']
|
8
|
+
s.email = ['raven@cs.stanford.edu']
|
9
|
+
s.files = %w(README.md worochi.gemspec)
|
10
|
+
s.files += Dir.glob('lib/**/*.rb')
|
11
|
+
s.require_paths = ["lib"]
|
12
|
+
s.homepage = 'http://rubygems.org/gems/worochi'
|
13
|
+
s.license = 'MIT'
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: worochi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Raven Jiang
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-08-02 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Provides a standard way to interface with Ruby API wrappers provided
|
14
|
+
by various cloud storage services such as Dropbox and Google Drive.
|
15
|
+
email:
|
16
|
+
- raven@cs.stanford.edu
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- README.md
|
22
|
+
- worochi.gemspec
|
23
|
+
- lib/worochi/error.rb
|
24
|
+
- lib/worochi/log.rb
|
25
|
+
- lib/worochi/agent.rb
|
26
|
+
- lib/worochi/helper/github.rb
|
27
|
+
- lib/worochi/config.rb
|
28
|
+
- lib/worochi/agent/sample.rb
|
29
|
+
- lib/worochi/agent/github.rb
|
30
|
+
- lib/worochi/agent/dropbox.rb
|
31
|
+
- lib/worochi/helper.rb
|
32
|
+
- lib/worochi/item.rb
|
33
|
+
- lib/worochi.rb
|
34
|
+
homepage: http://rubygems.org/gems/worochi
|
35
|
+
licenses:
|
36
|
+
- MIT
|
37
|
+
metadata: {}
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
requirements: []
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 2.0.6
|
55
|
+
signing_key:
|
56
|
+
specification_version: 4
|
57
|
+
summary: Worochi
|
58
|
+
test_files: []
|
59
|
+
has_rdoc:
|