gallery-remote 0.0.1
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.
- data/LICENSE.md +1 -0
- data/README +3 -0
- data/Rakefile +30 -0
- data/lib/cookie_jar.rb +59 -0
- data/lib/gallery.rb +8 -0
- data/lib/gallery/album.rb +64 -0
- data/lib/gallery/gallery.rb +31 -0
- data/lib/gallery/image.rb +29 -0
- data/lib/gallery/remote.rb +233 -0
- metadata +64 -0
data/LICENSE.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
[License](http://mwalker.info/license.html)
|
data/README
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
|
5
|
+
$:.unshift File.join(File.dirname(__FILE__), '/lib')
|
6
|
+
require 'gallery'
|
7
|
+
|
8
|
+
PKG_NAME = 'gallery-remote'
|
9
|
+
PKG_VERSION = Gallery::VERSION
|
10
|
+
|
11
|
+
LIB_FILES = Dir.glob('lib/**/*')
|
12
|
+
RELEASE_FILES = [ 'Rakefile', 'README', 'LICENSE.md' ] + LIB_FILES
|
13
|
+
|
14
|
+
task :default => [ :package ]
|
15
|
+
|
16
|
+
spec = Gem::Specification.new do |spec|
|
17
|
+
spec.name = PKG_NAME
|
18
|
+
spec.version = PKG_VERSION
|
19
|
+
spec.summary = 'A Ruby client for the Gallery2 photo gallery system'
|
20
|
+
spec.description = 'gallery-remote is an implementation of the Gallery Remote protocol in Ruby.'
|
21
|
+
spec.authors = ['Carl Leiby', 'Matt Walker']
|
22
|
+
spec.email = 'matt.r.walker@gmail.com'
|
23
|
+
spec.homepage = 'http://github.com/mrwalker/gallery-remote'
|
24
|
+
spec.files = RELEASE_FILES
|
25
|
+
spec.rubyforge_project = PKG_NAME
|
26
|
+
end
|
27
|
+
|
28
|
+
Rake::GemPackageTask.new(spec) do |package|
|
29
|
+
package.need_tar = true
|
30
|
+
end
|
data/lib/cookie_jar.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
#
|
2
|
+
# cookie_jar.rb
|
3
|
+
#
|
4
|
+
# Created on Sep 19, 2007, 7:39:33 PM
|
5
|
+
require 'date'
|
6
|
+
|
7
|
+
class CookieJar
|
8
|
+
def initialize
|
9
|
+
@cookies = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(cookie_list)
|
13
|
+
return if ! cookie_list
|
14
|
+
cookie_list.each do |cookie_string|
|
15
|
+
cookie = Cookie.new(cookie_string)
|
16
|
+
if ! cookie.expired
|
17
|
+
@cookies = @cookies.reject {|c| c.name==cookie.name}
|
18
|
+
@cookies << cookie
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def cookies
|
24
|
+
return if @cookies.length == 0
|
25
|
+
@cookies.map { |cookie| cookie.to_s }.join("; ")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Cookie
|
30
|
+
attr_accessor :name, :value, :expiration
|
31
|
+
|
32
|
+
def initialize(cookie_str)
|
33
|
+
cookie_str.split(/;\s?/).each_with_index do |c,i|
|
34
|
+
if i==0
|
35
|
+
@name, *values = c.split("=")
|
36
|
+
@value = values.join("=")
|
37
|
+
else
|
38
|
+
attr_name, *attr_values = c.split("=")
|
39
|
+
attr_value = attr_values.join("=")
|
40
|
+
if attr_name =~ /expires/i
|
41
|
+
@expiration = attr_value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
"#{@name}=#{@value}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def expired
|
52
|
+
if @expiration
|
53
|
+
@expiration.sub!(/^([a-zA-Z]+,)(\d)/) { |s| "#{$1} #{$2}" }
|
54
|
+
expiration_date = DateTime.parse( @expiration, true )
|
55
|
+
return expiration_date < DateTime.now
|
56
|
+
end
|
57
|
+
false
|
58
|
+
end
|
59
|
+
end
|
data/lib/gallery.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
module Gallery
|
2
|
+
class Album
|
3
|
+
attr_accessor :remote, :params
|
4
|
+
|
5
|
+
def initialize(remote, params)
|
6
|
+
@remote, @params = remote, params
|
7
|
+
end
|
8
|
+
|
9
|
+
def properties(params = {})
|
10
|
+
@remote.album_properties(name, params)
|
11
|
+
end
|
12
|
+
|
13
|
+
def images(params = {})
|
14
|
+
response = @remote.fetch_album_images(name, params)
|
15
|
+
image_params = response.keys.inject([]) do |image_params, key|
|
16
|
+
next image_params unless key =~ /image\.(.*)\.(\d+)/
|
17
|
+
_, param, index = key.match(/image\.(.*)\.(\d+)/).to_a
|
18
|
+
index = index.to_i
|
19
|
+
image_params[index] ||= {}
|
20
|
+
image_params[index][param] = response[key]
|
21
|
+
image_params
|
22
|
+
end.compact # Keys are 1-based; remove first element
|
23
|
+
image_params.map do |params|
|
24
|
+
image = Image.new(@remote, params)
|
25
|
+
yield image if block_given?
|
26
|
+
image
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_item(file_name, params = {})
|
31
|
+
@remote.add_item(name, file_name, params)
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_album(title, params = {})
|
35
|
+
@remote.new_album(name, { :newAlbumName => title_to_name(title), :newAlbumTitle => title }.merge(params))
|
36
|
+
end
|
37
|
+
|
38
|
+
def name
|
39
|
+
@params['name']
|
40
|
+
end
|
41
|
+
|
42
|
+
def title
|
43
|
+
@params['title']
|
44
|
+
end
|
45
|
+
|
46
|
+
def parent
|
47
|
+
@params['parent']
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_s
|
51
|
+
"Album #{name}: #{title}"
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def title_to_name(title)
|
57
|
+
name = title.dup
|
58
|
+
name.downcase!
|
59
|
+
name.gsub!(/[,'\-:]/, '') # Remove illegal characters
|
60
|
+
name.gsub!(/\s+/, '_') # Join words with underscores
|
61
|
+
name
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Gallery
|
2
|
+
class Gallery
|
3
|
+
attr_accessor :remote
|
4
|
+
|
5
|
+
def initialize(url, &block)
|
6
|
+
@remote = Remote.new(url)
|
7
|
+
instance_eval(&block) if block_given?
|
8
|
+
end
|
9
|
+
|
10
|
+
def login(user, pass)
|
11
|
+
@remote.login(user, pass)
|
12
|
+
end
|
13
|
+
|
14
|
+
def albums(params = {})
|
15
|
+
response = @remote.fetch_albums_prune
|
16
|
+
album_params = response.keys.inject([]) do |album_params, key|
|
17
|
+
next album_params unless key =~ /album\.(.*)\.(\d+)/
|
18
|
+
_, param, index = key.match(/album\.(.*)\.(\d+)/).to_a
|
19
|
+
index = index.to_i
|
20
|
+
album_params[index] ||= {}
|
21
|
+
album_params[index][param] = response[key]
|
22
|
+
album_params
|
23
|
+
end.compact # Keys are 1-based; remove first element
|
24
|
+
album_params.map do |params|
|
25
|
+
album = Album.new(@remote, params)
|
26
|
+
yield album if block_given?
|
27
|
+
album
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Gallery
|
2
|
+
class Image
|
3
|
+
attr_accessor :remote, :params
|
4
|
+
|
5
|
+
def initialize(remote, params = {})
|
6
|
+
@remote, @params = remote, params
|
7
|
+
end
|
8
|
+
|
9
|
+
def properties(params = {})
|
10
|
+
@remote.image_properites(name, params)
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
@params['name']
|
15
|
+
end
|
16
|
+
|
17
|
+
def title
|
18
|
+
@params['title']
|
19
|
+
end
|
20
|
+
|
21
|
+
def caption
|
22
|
+
@params['caption']
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
"Image #{name}: #{title}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'net/http'
|
3
|
+
require 'cookie_jar'
|
4
|
+
|
5
|
+
module Gallery
|
6
|
+
class Remote
|
7
|
+
attr_accessor :last_response, :status, :status_text
|
8
|
+
|
9
|
+
GR_STAT_SUCCESS = 0
|
10
|
+
PROTO_MAJ_VER_INVAL = 101
|
11
|
+
PROTO_MIN_VER_INVAL = 102
|
12
|
+
PROTO_VER_FMT_INVAL = 103
|
13
|
+
PROTO_VER_MISSING = 104
|
14
|
+
PASSWD_WRONG = 201
|
15
|
+
LOGIN_MISSING = 202
|
16
|
+
UNKNOWN_CMD = 301
|
17
|
+
NO_ADD_PERMISSION = 401
|
18
|
+
NO_FILENAME = 402
|
19
|
+
UPLOAD_PHOTO_FAIL = 403
|
20
|
+
NO_WRITE_PERMISSION = 404
|
21
|
+
NO_VIEW_PERMISSIO = 405
|
22
|
+
NO_CREATE_ALBUM_PERMISSION = 501
|
23
|
+
CREATE_ALBUM_FAILED = 502
|
24
|
+
MOVE_ALBUM_FAILED = 503
|
25
|
+
ROTATE_IMAGE_FAILED = 504
|
26
|
+
STATUS_DESCRIPTIONS = {
|
27
|
+
GR_STAT_SUCCESS => 'The command the client sent in the request completed successfully. The data (if any) in the response should be considered valid.',
|
28
|
+
PROTO_MAJ_VER_INVAL => 'The protocol major version the client is using is not supported.',
|
29
|
+
PROTO_MIN_VER_INVAL => 'The protocol minor version the client is using is not supported.',
|
30
|
+
PROTO_VER_FMT_INVAL => 'The format of the protocol version string the client sent in the request is invalid.',
|
31
|
+
PROTO_VER_MISSING => 'The request did not contain the required protocol_version key.',
|
32
|
+
PASSWD_WRONG => 'The password and/or username the client send in the request is invalid.',
|
33
|
+
LOGIN_MISSING => 'The client used the login command in the request but failed to include either the username or password (or both) in the request.',
|
34
|
+
UNKNOWN_CMD => 'The value of the cmd key is not valid.',
|
35
|
+
NO_ADD_PERMISSION => 'The user does not have permission to add an item to the gallery.',
|
36
|
+
NO_FILENAME => 'No filename was specified.',
|
37
|
+
UPLOAD_PHOTO_FAIL => 'The file was received, but could not be processed or added to the album.',
|
38
|
+
NO_WRITE_PERMISSION => 'No write permission to destination album.',
|
39
|
+
NO_VIEW_PERMISSIO => 'No view permission for this image.',
|
40
|
+
NO_CREATE_ALBUM_PERMISSION => 'A new album could not be created because the user does not have permission to do so.',
|
41
|
+
CREATE_ALBUM_FAILED => 'A new album could not be created, for a different reason (name conflict).',
|
42
|
+
MOVE_ALBUM_FAILED => 'The album could not be moved.',
|
43
|
+
ROTATE_IMAGE_FAILED => 'The image could not be rotated'
|
44
|
+
}
|
45
|
+
|
46
|
+
@@supported_types = {
|
47
|
+
'.avi' => 'video/x-msvideo',
|
48
|
+
'.bmp' => 'image/bmp',
|
49
|
+
'.gif' => 'image/gif',
|
50
|
+
'.jpe' => 'image/jpeg',
|
51
|
+
'.jpg' => 'image/jpeg',
|
52
|
+
'.jpeg' => 'image/jpeg',
|
53
|
+
'.mov' => 'video/quicktime',
|
54
|
+
'.qt' => 'video/quicktime',
|
55
|
+
'.mp4' => 'video/mp4',
|
56
|
+
'.tif' => 'image/tiff',
|
57
|
+
'.tiff' => 'image/tiff'
|
58
|
+
}
|
59
|
+
|
60
|
+
def self.supported_type?(extension)
|
61
|
+
@@supported_types.has_key?(extension)
|
62
|
+
end
|
63
|
+
|
64
|
+
def initialize(url)
|
65
|
+
@uri = URI.parse(url)
|
66
|
+
@base_params = {
|
67
|
+
'g2_controller' => 'remote:GalleryRemote',
|
68
|
+
'g2_form[protocol_version]' => '2.9'
|
69
|
+
}
|
70
|
+
@cookie_jar = CookieJar.new
|
71
|
+
@boundary = '7d21f123d00c4'
|
72
|
+
end
|
73
|
+
|
74
|
+
# cmd=login
|
75
|
+
# protocol_version=2.0
|
76
|
+
# uname=gallery-user-name
|
77
|
+
# password=cleartext-password
|
78
|
+
def login(uname, password, params = {})
|
79
|
+
params = { :cmd => 'login', :uname => uname, :password => password }.merge(params)
|
80
|
+
send_request(params)
|
81
|
+
end
|
82
|
+
|
83
|
+
# cmd=fetch-albums-prune
|
84
|
+
# protocol_version=2.2
|
85
|
+
# no_perms=yes/no [optional, G2 since 2.9]
|
86
|
+
def fetch_albums_prune(params = {})
|
87
|
+
params = { :cmd => 'fetch-albums-prune', :no_perms => 'y' }.merge(params)
|
88
|
+
send_request(params)
|
89
|
+
end
|
90
|
+
|
91
|
+
# cmd=add-item
|
92
|
+
# protocol_version=2.0
|
93
|
+
# set_albumName=album name
|
94
|
+
# userfile=user-file
|
95
|
+
# userfile_name=file-name
|
96
|
+
# caption=caption [optional]
|
97
|
+
# force_filename=force-filename [optional]
|
98
|
+
# auto_rotate=yes/no [optional, since 2.5]
|
99
|
+
# extrafield.fieldname=fieldvalue [optional, since 2.3]
|
100
|
+
def add_item(set_albumName, userfile_name, params = {})
|
101
|
+
params = { :cmd => 'add-item', :set_albumName => set_albumName, :userfile_name => userfile_name }.merge(params)
|
102
|
+
send_request(params)
|
103
|
+
end
|
104
|
+
|
105
|
+
# cmd=album-properties
|
106
|
+
# protocol_version=2.0
|
107
|
+
# set_albumName=album-name
|
108
|
+
def album_properties(set_albumName, params = {})
|
109
|
+
params = { :cmd => 'album-properties', :set_albumName => set_albumName }.merge(params)
|
110
|
+
send_request(params)
|
111
|
+
end
|
112
|
+
|
113
|
+
# cmd=new-album
|
114
|
+
# protocol_version=2.1
|
115
|
+
# set_albumName=parent-album-name
|
116
|
+
# newAlbumName=album-name [optional]
|
117
|
+
# newAlbumTitle=album-title [optional]
|
118
|
+
# newAlbumDesc=album-description [optional]
|
119
|
+
def new_album(set_albumName, params = {})
|
120
|
+
params = { :cmd => 'new-album', :set_albumName => set_albumName }.merge(params)
|
121
|
+
send_request(params)
|
122
|
+
end
|
123
|
+
|
124
|
+
# cmd=fetch-album-images
|
125
|
+
# protocol_version=2.4
|
126
|
+
# set_albumName=album-name
|
127
|
+
# albums_too=yes/no [optional, since 2.13]
|
128
|
+
# random=yes/no [optional, G2 since ***]
|
129
|
+
# limit=number-of-images [optional, G2 since ***]
|
130
|
+
# extrafields=yes/no [optional, G2 since 2.12]
|
131
|
+
# all_sizes=yes/no [optional, G2 since 2.14]
|
132
|
+
def fetch_album_images(set_albumName, params = {})
|
133
|
+
params = { :cmd => 'fetch-album-images', :set_albumName => set_albumName }.merge(params)
|
134
|
+
send_request(params)
|
135
|
+
end
|
136
|
+
|
137
|
+
# cmd=image-properties
|
138
|
+
# protocol_version=***
|
139
|
+
# id=item-id
|
140
|
+
def image_properties(id, params = {})
|
141
|
+
params = { :cmd => 'image-properties', :id => id }.merge(params)
|
142
|
+
send_request(params)
|
143
|
+
end
|
144
|
+
|
145
|
+
def status_msg
|
146
|
+
"#{@status} - (#{@status_text})"
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def build_multipart_query(params, userfile_name)
|
152
|
+
params['g2_userfile_name'] = userfile_name
|
153
|
+
request = params.map{ |k, v| "Content-Disposition: form-data; name=\"#{k}\"\r\n\r\n#{v}\r\n" }
|
154
|
+
content = File.open(userfile_name, 'r'){ |f| f.read }
|
155
|
+
request << "Content-Disposition: form-data; name=\"g2_userfile\"; filename=\"#{userfile_name}\"\r\n" +
|
156
|
+
"Content-Transfer-Encoding: binary\r\n" +
|
157
|
+
"Content-Type: #{@@supported_types[File.extname(userfile_name).downcase]}\r\n\r\n" +
|
158
|
+
content + "\r\n"
|
159
|
+
request.collect { |p| "--#{@boundary}\r\n#{p}" }.join("") + "--#{@boundary}--"
|
160
|
+
end
|
161
|
+
|
162
|
+
def build_query(params)
|
163
|
+
params.map{ |k, v| "#{k}=#{v}" }.join('&')
|
164
|
+
end
|
165
|
+
|
166
|
+
def send_request(params)
|
167
|
+
userfile_name = params.delete(:userfile_name)
|
168
|
+
post_parameters = prep_params(params)
|
169
|
+
headers = {}
|
170
|
+
headers['Cookie'] = @cookie_jar.cookies if @cookie_jar.cookies
|
171
|
+
if userfile_name && File.file?(userfile_name)
|
172
|
+
query = build_multipart_query(post_parameters, userfile_name)
|
173
|
+
headers['Content-type'] = "multipart/form-data, boundary=#{@boundary}" if @boundary
|
174
|
+
else
|
175
|
+
query = build_query(post_parameters)
|
176
|
+
end
|
177
|
+
res = post(query, headers)
|
178
|
+
case res
|
179
|
+
when Net::HTTPSuccess, Net::HTTPRedirection
|
180
|
+
handle_response(res)
|
181
|
+
else
|
182
|
+
res.error!
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def post(query, headers = {}, retries = 3)
|
187
|
+
begin
|
188
|
+
Net::HTTP.start(@uri.host, @uri.port) do |h|
|
189
|
+
h.post(@uri.path, query, headers)
|
190
|
+
end
|
191
|
+
rescue Exception => e
|
192
|
+
puts "Error during POST (#{retries} retries remain): #{e}"
|
193
|
+
throw e if retries == 0
|
194
|
+
post(query, headers, retries - 1)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def prep_params(params)
|
199
|
+
result = {}
|
200
|
+
result = result.merge(@base_params)
|
201
|
+
params.each_pair do |name, value|
|
202
|
+
result["g2_form[#{name}]"] = value
|
203
|
+
end
|
204
|
+
result['g2_authToken'] = @auth_token if @auth_token
|
205
|
+
result
|
206
|
+
end
|
207
|
+
|
208
|
+
def handle_response(res)
|
209
|
+
@cookie_jar.add(res.header.get_fields('set-cookie'))
|
210
|
+
@last_response = {}
|
211
|
+
begin
|
212
|
+
header = false
|
213
|
+
res.body.each do |line|
|
214
|
+
header = true if line.chomp == '#__GR2PROTO__'
|
215
|
+
next unless header # Ignore debug output
|
216
|
+
next if line =~ /^#/ # Ignore comments
|
217
|
+
name, *values = line.strip.split(/\s*=\s*/)
|
218
|
+
@last_response[name.strip] = values.join('=')
|
219
|
+
end
|
220
|
+
rescue Exception => e
|
221
|
+
puts "Error parsing response:\n#{res.body}"
|
222
|
+
throw e
|
223
|
+
end
|
224
|
+
@auth_token ||= @last_response['auth_token']
|
225
|
+
puts 'WARN: no auth token in response (using last)' unless @last_response['auth_token']
|
226
|
+
@status = @last_response['status'].to_i
|
227
|
+
@status_text = @last_response['status_text']
|
228
|
+
puts status_msg
|
229
|
+
puts "WARN: #{STATUS_DESCRIPTIONS[@status]}" unless @status == GR_STAT_SUCCESS
|
230
|
+
@last_response
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gallery-remote
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Carl Leiby
|
8
|
+
- Matt Walker
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-04-18 00:00:00 -05:00
|
14
|
+
default_executable:
|
15
|
+
dependencies: []
|
16
|
+
|
17
|
+
description: gallery-remote is an implementation of the Gallery Remote protocol in Ruby.
|
18
|
+
email: matt.r.walker@gmail.com
|
19
|
+
executables: []
|
20
|
+
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files: []
|
24
|
+
|
25
|
+
files:
|
26
|
+
- Rakefile
|
27
|
+
- README
|
28
|
+
- LICENSE.md
|
29
|
+
- lib/cookie_jar.rb
|
30
|
+
- lib/gallery/album.rb
|
31
|
+
- lib/gallery/gallery.rb
|
32
|
+
- lib/gallery/image.rb
|
33
|
+
- lib/gallery/remote.rb
|
34
|
+
- lib/gallery.rb
|
35
|
+
has_rdoc: true
|
36
|
+
homepage: http://github.com/mrwalker/gallery-remote
|
37
|
+
licenses: []
|
38
|
+
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options: []
|
41
|
+
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: "0"
|
49
|
+
version:
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "0"
|
55
|
+
version:
|
56
|
+
requirements: []
|
57
|
+
|
58
|
+
rubyforge_project: gallery-remote
|
59
|
+
rubygems_version: 1.3.5
|
60
|
+
signing_key:
|
61
|
+
specification_version: 3
|
62
|
+
summary: A Ruby client for the Gallery2 photo gallery system
|
63
|
+
test_files: []
|
64
|
+
|