imgsrc 0.8.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/imgsrc +84 -0
- data/bin/imgsrc_login +17 -0
- data/lib/imgsrc.rb +244 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -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
|
data/bin/imgsrc
ADDED
@@ -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)
|
data/bin/imgsrc_login
ADDED
@@ -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
|
+
|
data/lib/imgsrc.rb
ADDED
@@ -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: []
|