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 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: