worochi 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|