imgsrc 0.8.5

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.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/bin/imgsrc +84 -0
  3. data/bin/imgsrc_login +17 -0
  4. data/lib/imgsrc.rb +244 -0
  5. metadata +90 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: acb20a9bc77dbc2416e7c5bd23e47dc75968f69f
4
+ data.tar.gz: 90d0e92ff2c1c813c45134199c8d3b6411b085e9
5
+ SHA512:
6
+ metadata.gz: 0d8c0f9240bd3c2716ae1f51092a3407365687cbe6c99f8bb2e64a3e496471902d3ebae0dc524f5cd21b5fa5914ca27575d05f439aa87c57d5b65b9a2845d34b
7
+ data.tar.gz: 2f5263ebf15e1052b46a38069e82eae71374c614289aee6d850e07364da553a6711babb6172d1a905644b12e103ca2118226ddd75e8e432a1247ba2ea567c068
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ #############################################################################
5
+ # imgsrc - iMGSRC.RU client #
6
+ # (C) 2011, Vasiliy Yeremeyev <vayerx@gmail.com> #
7
+ # #
8
+ # This program is free software: you can redistribute it and/or modify #
9
+ # it under the terms of the GNU General Public License as published by #
10
+ # the Free Software Foundation, either version 3 of the License, or #
11
+ # (at your option) any later version. #
12
+ # #
13
+ # This program is distributed in the hope that it will be useful, #
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of #
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
16
+ # GNU General Public License for more details. #
17
+ # #
18
+ # You should have received a copy of the GNU General Public License #
19
+ # along with this program. If not, see <http://www.gnu.org/licenses/>. #
20
+ #############################################################################
21
+
22
+ require 'imgsrc'
23
+ require 'parseconfig'
24
+ require 'optiflag'
25
+ #require 'progressbar'
26
+
27
+
28
+ def load_dir(path)
29
+ return [path] unless File.directory?(path)
30
+ files = []
31
+ Dir.entries(path).each do |f|
32
+ file = "#{path}/#{f}"
33
+ files << file if /.+\.jpe?g$/i =~ file && File.file?(file) && File.readable?(file)
34
+ end rescue nil
35
+ files.sort!
36
+ end
37
+
38
+ module ImgSrcOptions extend OptiFlagSet
39
+ # TODO postional options parser
40
+ optional_flag 'user', { :description => 'Username', :alternate_forms => ['u', 'username'] }
41
+ optional_flag 'passwd', { :description => 'User password, MD5', :alternate_forms => ['p', 'password', 'user_password'] }
42
+ optional_flag 'album', { :description => 'Album name', :alternate_forms => 'a' }
43
+ optional_flag 'album_pass', { :description => 'Album password', :alternate_forms => ['album_password', 'album_passwd'] }
44
+ optional_flag 'category', { :description => 'Category ID (TODO name)', :alternate_forms => 'c' }
45
+
46
+ and_process!
47
+ end
48
+
49
+
50
+ def get_any_directory(paths)
51
+ native_dir = paths.index { |f| File.directory?(f) }
52
+ return paths[native_dir] if native_dir
53
+ file_dir = paths.index { |f| !File.dirname(f).empty? }
54
+ file_dir ? File.dirname(paths[file_dir]) : nil
55
+ end
56
+
57
+ FILTER = /d80|a5|nikon|canon|vayerx|paola|small|upload|show|flickr|gallery|best/i
58
+ def guess_album_name(paths)
59
+ dir = get_any_directory(paths)
60
+ return nil unless dir
61
+ dir.split(File::SEPARATOR).delete_if { |part| FILTER =~ part }.last
62
+ end
63
+
64
+ # TODO strict positional opts
65
+ full_args = ARGV.map { |file| file !~ /^-/ && File.exists?(file) ? File.expand_path(file) : nil }.compact
66
+ files = []
67
+ full_args.each { |arg| files += load_dir(arg) }
68
+ if files.empty?
69
+ puts "no files"
70
+ exit
71
+ end
72
+
73
+ config = ParseConfig.new(RUBY_PLATFORM =~ /linux|bsd|darwin|cygwin/i ? File.expand_path('~/.imgsrc.conf') : 'imgsrc.ini')
74
+ user_name = ARGV.flags.user || config['user']
75
+ user_pass = ARGV.flags.passwd || config['passwd']
76
+ album_name = ARGV.flags.album || guess_album_name(full_args) || 'новый альбом'
77
+ album_opts = {}
78
+ album_opts[:passwd] = ARGV.flags.album_pass if ARGV.flags.album_pass
79
+ album_opts[:category] = ARGV.flags.category if ARGV.flags.category
80
+
81
+ client = IMGSrc::API.new(user_name, user_pass)
82
+ client.login
83
+ client.get_or_create_album(album_name, album_opts)
84
+ client.upload(album_name, files)
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'io/console'
4
+ require 'digest/md5'
5
+ require 'fileutils'
6
+
7
+ print 'Username: '; username = gets
8
+ print 'Password: '; password = STDIN.noecho(&:gets).chomp
9
+ puts
10
+
11
+ filename = File.expand_path('~/.imgsrc.conf')
12
+ FileUtils.mv(filename, "#{filename}.bak")
13
+ File.open(filename, 'w') do |conf|
14
+ conf.puts "user=#{username}"
15
+ conf.puts "passwd=#{Digest::MD5.hexdigest(password)}"
16
+ end
17
+
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #############################################################################
4
+ # imgsrc - iMGSRC.RU client #
5
+ # (C) 2011, Vasiliy Yeremeyev <vayerx@gmail.com> #
6
+ # #
7
+ # This program is free software: you can redistribute it and/or modify #
8
+ # it under the terms of the GNU General Public License as published by #
9
+ # the Free Software Foundation, either version 3 of the License, or #
10
+ # (at your option) any later version. #
11
+ # #
12
+ # This program is distributed in the hope that it will be useful, #
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of #
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15
+ # GNU General Public License for more details. #
16
+ # #
17
+ # You should have received a copy of the GNU General Public License #
18
+ # along with this program. If not, see <http://www.gnu.org/licenses/>. #
19
+ #############################################################################
20
+
21
+ require 'net/http'
22
+ require 'libxml'
23
+ require 'base64'
24
+ require 'pp'
25
+
26
+
27
+ module IMGSrc
28
+
29
+ class InfoError < RuntimeError; end
30
+ class LoginError < RuntimeError; end
31
+ class CreateError < RuntimeError; end
32
+ class UploadError < RuntimeError; end
33
+
34
+ Album = Struct.new( 'Album', :id, :name, :size, :date, :password, :photos )
35
+ Photo = Struct.new( 'Photo', :id, :page, :small, :big )
36
+ Category = Struct.new( 'Category', :name, :parent_id )
37
+
38
+ class API
39
+ ROOT_HOST = 'imgsrc.ru'
40
+ PROTO_VER = '0.8'
41
+ CACHE_DIR = '.imgsrc'
42
+
43
+ class << self
44
+ include LibXML
45
+
46
+ @@http_conn = Net::HTTP.new(ROOT_HOST) # this method does not open the TCP connection
47
+ @@categories = nil
48
+
49
+ # Get photo categories hash (id=>category)
50
+ def categories
51
+ @@categories || load_categories
52
+ end
53
+
54
+ def call_get(method, params = {}, cachable = :nocache)
55
+ uri = "/#{method}#{params.empty? ? '' : '?'}#{params.map{ |key, value| "#{key}=#{value}" }.join('&')}"
56
+
57
+ params_string = params.map { |k, v| /passw/ =~ k ? nil : "_#{k}-#{ k == 'create' ? v.encode('UTF-8') : v }" }
58
+ cached_file = "#{CACHE_DIR}/#{method.gsub(/\//, '-')}#{params_string.compact.join}.xml"
59
+ if cachable == :cache && File::exists?( cached_file )
60
+ puts "using cached #{cached_file}"
61
+ return File.read( cached_file )
62
+ else
63
+ # puts "fetching #{uri} to #{cached_file}"
64
+ result = @@http_conn.get( uri ).body
65
+ File.new( cached_file, "w" ).write( result ) rescue nil
66
+ return result
67
+ end
68
+ rescue Exception => x
69
+ raise RuntimeError, "can not load #{method.sub(/.*\./, "")}: #{x.message}"
70
+ end
71
+
72
+ def extract_info(response)
73
+ info = XML::Parser.string(response).parse.find_first '/info'
74
+ raise RuntimeError, "Invalid xml:\r\n#{response}" unless info
75
+ raise InfoError, "Unsupported protocol version #{info['proto']}" unless info['proto'] == PROTO_VER
76
+ info
77
+ end
78
+
79
+ private
80
+ # Fetch categories list
81
+ def load_categories
82
+ info = extract_info(call_get('cli/cats.php', {}, :nocache))
83
+ @@categories = {}
84
+ info.find('categories/category').each do |node|
85
+ next unless id = node['id']
86
+ name = node.find_first('name').content rescue nil
87
+ parent_id = node.find_first('parent_id').content rescue nil
88
+ @@categories[id] = Category.new(name, parent_id)
89
+ end
90
+ @@categories
91
+ end
92
+
93
+ end
94
+
95
+ attr_reader :username, :albums
96
+
97
+ def initialize(user, passwd_md5)
98
+ @username = user
99
+ @Login = { :login => @username, :passwd => passwd_md5 }
100
+ @storage = nil # hostname of imgsrc storge server
101
+ @stor_conn = nil # http-connection to storage host
102
+ @albums = []
103
+
104
+ Dir.mkdir( CACHE_DIR ) unless File.directory?( CACHE_DIR )
105
+ end
106
+
107
+ # Login and fetch user info
108
+ def login
109
+ raise LoginError, 'already logined' if @storage
110
+ begin
111
+ parse_info(self.class.call_get('cli/info.php', @Login, :nocache))
112
+ rescue InfoError => x
113
+ raise LoginError, x.message
114
+ end
115
+ self
116
+ end
117
+
118
+ # Create new album. Optional arguments: category, passwd
119
+ def create_album(name, args = {})
120
+ album = get_album(name) rescue nil
121
+ raise CreateError, "Album #{name} already exists: #{album.size} photos, modified #{album.date}" if album
122
+
123
+ params = { :create => name.encode('CP1251') }
124
+ params[:create_category] = args[:category] if args[:category]
125
+ params[:create_passwd] = args[:passwd] if args[:passwd]
126
+ parse_info(self.class.call_get('cli/info.php', @Login.merge(params)))
127
+ end
128
+
129
+ # Get existing album by name
130
+ def get_album(name)
131
+ @albums.fetch( @albums.index { |album| album.name == name } ) rescue raise RuntimeError, "no album #{name}"
132
+ end
133
+
134
+ # Shortcut for getting exising album or creating new one
135
+ def get_or_create_album(name, args = {})
136
+ album = get_album(name) rescue nil
137
+ return album if album
138
+ create_album(name, args)
139
+ get_album(name) # create_album receives full list - additional get_album() call required
140
+ end
141
+
142
+ # Upload files to album
143
+ def upload(name, files)
144
+ raise RuntimeError, 'user is not logined (no storage host)' unless @stor_conn
145
+ album = get_album(name)
146
+ uri = "/cli/post.php?#{@Login.map{ |k, v| "#{k}=#{v}" }.join('&')}&album_id=#{album.id}"
147
+ photos = nil
148
+ files.each do |file| # imgsrc.ru badly handles multi-file uploads (unstable work, unrecoverable errors)
149
+ puts "uploading #{file}..."
150
+ photos, retries = nil, 3
151
+ begin
152
+ photos = do_upload(uri, [file], :nobase64)
153
+ rescue Exception => x
154
+ puts "#{File.basename(file)}: upload failed (#{x.message}), #{retries - 1} retries left"
155
+ retry if (retries -= 1) > 0
156
+ raise UploadError, x.message
157
+ end
158
+ end
159
+ album.photos, album.size = photos, photos.size if photos # imgsrc returns whole album in response
160
+ pp album
161
+ end
162
+
163
+ private
164
+ # Parse info.php response
165
+ def parse_info(response)
166
+ info = self.class.extract_info(response)
167
+ status, error = info.find_first('status'), info.find_first('error')
168
+ raise RuntimeError, "No status in response info:\r\n#{info}" unless status
169
+ raise InfoError, error ? error.content : 'unknown' unless status.content == 'OK'
170
+
171
+ raise RuntimeError, 'No storage ID in server response' unless storage = info.find_first('store')
172
+ host = "e#{storage.content}.#{ROOT_HOST}"
173
+ @stor_conn, @storage = Net::HTTP.new(host), host unless @storage == host
174
+
175
+ @albums = []
176
+ info.find('albums/album').each do |node|
177
+ album = Album.new(node['id'])
178
+ album.name = node.find_first('name').content rescue nil
179
+ album.size = node.find_first('photos').content.to_i rescue 0
180
+ album.date = node.find_first('modified').content rescue nil
181
+ album.password = node.find_first('password').content rescue nil
182
+ album.photos = []
183
+ @albums << album
184
+ end
185
+ end
186
+
187
+ BOUNDARY = 'x----------------------------Rai8cheth7thi6ee'
188
+
189
+ # Upload files to hosting
190
+ # TODO fix base64
191
+ def do_upload(uri, files, encoding = :binary)
192
+ raise LoginError, 'no files' if files.empty?
193
+
194
+ post_body = []
195
+
196
+ files.each_index do |index|
197
+ file = files[index]
198
+ filename = File.basename(file)
199
+ data = file.empty? ? '' : File.read(file)
200
+
201
+ headers = []
202
+ headers << "--#{BOUNDARY}\r\n"
203
+ headers << "Content-Disposition: form-data; name=\"u#{index+1}\"; filename=\"#{filename}\"\r\n"
204
+ headers << "Content-Type: #{data.empty? ? 'application/octet-stream' : 'image/jpeg'}\r\n"
205
+ headers << "Content-Length: #{data.size}\r\n" unless data.empty? || encoding == :base64
206
+ headers << "Content-Transfer-Encoding: base64\r\n" if encoding == :base64
207
+ headers << "\r\n"
208
+
209
+ post_body << headers.join
210
+ post_body << (encoding == :base64 ? Base64.encode64(data) : data)
211
+ post_body << "\r\n"
212
+ end
213
+ post_body << "--#{BOUNDARY}--\r\n"
214
+
215
+ request = Net::HTTP::Post.new(uri)
216
+ request.body = post_body.join
217
+
218
+ # request["Connection"] = 'keep-alive'
219
+ request["Content-Type"] = "multipart/form-data, boundary=#{BOUNDARY}"
220
+
221
+ response = @stor_conn.request(request)
222
+ raise UploadError, "Code #{response.code}: #{response.body}" unless response.kind_of?(Net::HTTPSuccess)
223
+ parse_upload(self.class.extract_info(response.body))
224
+ end
225
+
226
+ # Parse file uploading response
227
+ def parse_upload(info)
228
+ status, error = info.find_first('status'), info.find_first('error')
229
+ raise RuntimeError, "no status in upload response:\n#{info}" unless status
230
+ raise UploadError, error ? error.content : 'unknown' unless status.content == 'OK'
231
+
232
+ raise RuntimeError, "no uploads in\r\n#{info}" unless uploads = info.find_first('uploads')
233
+ photos = []
234
+ uploads.find('photo').each do |node|
235
+ photo = Photo.new(node['id'])
236
+ photo.page = node.find_first('page').content rescue nil
237
+ photo.small = node.find_first('small').content rescue nil
238
+ photo.big = node.find_first('big').content rescue nil
239
+ photos << photo
240
+ end
241
+ photos
242
+ end
243
+ end
244
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: imgsrc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.5
5
+ platform: ruby
6
+ authors:
7
+ - Vasiliy Yeremeyev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-03-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: libxml-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 2.2.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 2.2.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: parseconfig
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.4
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 1.0.4
41
+ - !ruby/object:Gem::Dependency
42
+ name: optiflag
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0.7'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0.7'
55
+ description: Simple client for imgsrc.ru photo-hosting
56
+ email: vayerx@gmail.com
57
+ executables:
58
+ - imgsrc
59
+ - imgsrc_login
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - lib/imgsrc.rb
64
+ - bin/imgsrc
65
+ - bin/imgsrc_login
66
+ homepage: http://github.com/vayerx/imgsrc
67
+ licenses:
68
+ - GPL-3
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - '>='
77
+ - !ruby/object:Gem::Version
78
+ version: 1.9.2
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 2.0.14
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: iMGSRC photo-hosting client.
90
+ test_files: []