worochi 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,5 @@
1
+ Worochi
2
+ ===============================================================================
3
+
4
+ Worochi provides a standard way to interface with Ruby API wrappers provided
5
+ by various cloud storage services such as Dropbox and Google Drive.
@@ -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,2 @@
1
+ class Worochi::Error < StandardError
2
+ end
@@ -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
@@ -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
@@ -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: