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.
- 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: []
|