imgsrc 0.8.5

Sign up to get free protection for your applications and to get access to all the features.
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: []