gallery-remote 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|