kenai_tools 0.0.7
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/.gitignore +8 -0
- data/.idea/.name +1 -0
- data/.idea/.rakeTasks +7 -0
- data/.idea/encodings.xml +5 -0
- data/.idea/kenai_tools.iml +40 -0
- data/.idea/misc.xml +8 -0
- data/.idea/modules.xml +9 -0
- data/.idea/vcs.xml +7 -0
- data/Gemfile +4 -0
- data/README.md +28 -0
- data/Rakefile +2 -0
- data/bin/dlutil +139 -0
- data/kenai_tools.gemspec +43 -0
- data/lib/kenai_tools/downloads_client.rb +241 -0
- data/lib/kenai_tools/kenai_client.rb +168 -0
- data/lib/kenai_tools/version.rb +3 -0
- data/lib/kenai_tools.rb +7 -0
- data/spec/downloads_client_spec.rb +302 -0
- data/spec/fixtures/data/irs_docs/irs-form-1040.pdf +0 -0
- data/spec/fixtures/data/irs_docs/irs-p555.pdf +0 -0
- data/spec/fixtures/data/sax.tgz +0 -0
- data/spec/fixtures/data/sax2/.cvsignore +9 -0
- data/spec/fixtures/data/sax2/CHANGES +245 -0
- data/spec/fixtures/data/sax2/COPYING +12 -0
- data/spec/fixtures/data/sax2/ChangeLog +666 -0
- data/spec/fixtures/data/sax2/Makefile +77 -0
- data/spec/fixtures/data/sax2/README +62 -0
- data/spec/fixtures/data/sax2/build.xml +68 -0
- data/spec/fixtures/data/sax2/src/SAXDump.java +238 -0
- data/spec/fixtures/data/sax2/src/SAXTest.java +351 -0
- data/spec/fixtures/data/sax2/src/org/xml/sax/Attributes.java +257 -0
- data/spec/fixtures/data/sax2/src/org/xml/sax/package.html +297 -0
- data/spec/fixtures/data/sax2r2.jar +0 -0
- data/spec/fixtures/data/text1.txt +4 -0
- data/spec/spec_helper.rb +9 -0
- metadata +222 -0
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require "bundler/setup"
|
3
|
+
|
4
|
+
require 'rest_client'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module KenaiTools
|
8
|
+
class KenaiClient
|
9
|
+
DEFAULT_HOST = 'https://kenai.com/'
|
10
|
+
|
11
|
+
RestClient.proxy = ENV['http_proxy'] if ENV['http_proxy']
|
12
|
+
|
13
|
+
attr_reader :host, :user, :password
|
14
|
+
|
15
|
+
def initialize(host = nil, opts = {})
|
16
|
+
@host = host || DEFAULT_HOST
|
17
|
+
@opts = opts
|
18
|
+
end
|
19
|
+
|
20
|
+
# check credentials using the login/authenticate method; if successful,
|
21
|
+
# cache the credentials for future calls
|
22
|
+
def authenticate(user, password)
|
23
|
+
@user = user
|
24
|
+
@password = password
|
25
|
+
begin
|
26
|
+
client = self['login/authenticate']
|
27
|
+
client["?username=#{@user}&password=#{@password}"].get
|
28
|
+
@auth = true
|
29
|
+
rescue RestClient::Unauthorized, RestClient::RequestFailed
|
30
|
+
@auth = false
|
31
|
+
@user = @password = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
return @auth
|
35
|
+
end
|
36
|
+
|
37
|
+
def authenticated?
|
38
|
+
@auth
|
39
|
+
end
|
40
|
+
|
41
|
+
def project(proj_name)
|
42
|
+
begin
|
43
|
+
JSON.parse(self["projects/#{proj_name}"].get)
|
44
|
+
rescue RestClient::ResourceNotFound
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# collect all project hashes (scope may be :all, or all projects, or
|
50
|
+
# :mine, for projects in which the current user has some role)
|
51
|
+
def projects(scope=:all)
|
52
|
+
fetch_all('projects', 'projects')
|
53
|
+
end
|
54
|
+
|
55
|
+
def my_projects
|
56
|
+
fetch_all('projects/mine', 'projects')
|
57
|
+
end
|
58
|
+
|
59
|
+
# get wiki images for a project
|
60
|
+
def wiki_images(project, on_page = nil)
|
61
|
+
fetch_all("projects/#{project}/features/wiki/images", 'images', on_page)
|
62
|
+
end
|
63
|
+
|
64
|
+
# get the wiki raw image data for an image
|
65
|
+
def wiki_image_data(image)
|
66
|
+
RestClient.get(image['image_url'], :accept => image['image_content_type'])
|
67
|
+
end
|
68
|
+
|
69
|
+
# hash has the following keys
|
70
|
+
# +:uploaded_data+ = raw image data, required only if creating a new image
|
71
|
+
# +:comments+ = optional comments for the image
|
72
|
+
# throws IOError unless create or update was successful
|
73
|
+
def create_or_update_wiki_image(proj_name, image_filename, hash)
|
74
|
+
req_params = {}
|
75
|
+
if data = hash[:uploaded_data]
|
76
|
+
upload_io = UploadIO.new(StringIO.new(data), "image/png", image_filename)
|
77
|
+
req_params["image[uploaded_data]"] = upload_io
|
78
|
+
end
|
79
|
+
if comments = hash[:comments]
|
80
|
+
req_params["image[comments]"] = comments
|
81
|
+
end
|
82
|
+
return false if req_params.empty?
|
83
|
+
end
|
84
|
+
|
85
|
+
# get wiki pages for a project
|
86
|
+
def wiki_pages(project, on_page = nil)
|
87
|
+
fetch_all("projects/#{project}/features/wiki/pages", 'pages', on_page)
|
88
|
+
end
|
89
|
+
|
90
|
+
def wiki_page(proj_name, page_name)
|
91
|
+
page = wiki_page_client(proj_name, page_name)
|
92
|
+
JSON.parse(page.get)
|
93
|
+
end
|
94
|
+
|
95
|
+
# edit a single wiki page -- yields the current page contents, and
|
96
|
+
# saves them back if the result of the block is different
|
97
|
+
def edit_wiki_page(proj_name, page_name)
|
98
|
+
# fetch current page contents
|
99
|
+
page = wiki_page_client(proj_name, page_name)
|
100
|
+
begin
|
101
|
+
page_data = JSON.parse(page.get)
|
102
|
+
current_src = page_data['text']
|
103
|
+
rescue RestClient::ResourceNotFound
|
104
|
+
page_data = {}
|
105
|
+
current_src = ''
|
106
|
+
end
|
107
|
+
|
108
|
+
new_src = yield(current_src)
|
109
|
+
|
110
|
+
changed = !(new_src.nil? || new_src == current_src)
|
111
|
+
|
112
|
+
if changed
|
113
|
+
new_data = {
|
114
|
+
'page' => {
|
115
|
+
'text' => new_src,
|
116
|
+
'description' => 'edited with kenai-client',
|
117
|
+
'number' => page_data['number']
|
118
|
+
}
|
119
|
+
}
|
120
|
+
page.put(JSON.dump(new_data), :content_type => 'application/json')
|
121
|
+
end
|
122
|
+
|
123
|
+
return changed
|
124
|
+
end
|
125
|
+
|
126
|
+
def api_client(fragment='')
|
127
|
+
params = {:headers => {:accept => 'application/json'}}
|
128
|
+
if @auth
|
129
|
+
params[:user] = @user
|
130
|
+
params[:password] = @password
|
131
|
+
end
|
132
|
+
params.merge!(@opts)
|
133
|
+
|
134
|
+
if fragment =~ %r{^https://}
|
135
|
+
RestClient::Resource.new(fragment, params)
|
136
|
+
else
|
137
|
+
RestClient::Resource.new(@host, params)['api'][fragment]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
alias :[] :api_client
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
# +on_page+ means only on that particular page or all pages if nil
|
146
|
+
def fetch_all(initial_url, item_key, on_page = nil)
|
147
|
+
unless on_page
|
148
|
+
next_page = initial_url
|
149
|
+
results = []
|
150
|
+
|
151
|
+
begin
|
152
|
+
curr_page = JSON.parse(self[next_page].get)
|
153
|
+
results += curr_page[item_key]
|
154
|
+
next_page = curr_page['next']
|
155
|
+
end until next_page.nil?
|
156
|
+
|
157
|
+
results
|
158
|
+
else
|
159
|
+
url = on_page ? initial_url + "?page=#{on_page}" : initial_url
|
160
|
+
JSON.parse(self[url].get)[item_key]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def wiki_page_client(project, page)
|
165
|
+
self["projects/#{project}/features/wiki/pages/#{page}"]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
data/lib/kenai_tools.rb
ADDED
@@ -0,0 +1,302 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require "open-uri"
|
3
|
+
|
4
|
+
# This rspec test assumes that a development kenai/junction2 server is running
|
5
|
+
SITE = "http://localhost:3000"
|
6
|
+
begin
|
7
|
+
RestClient::Resource.new(SITE).get
|
8
|
+
rescue
|
9
|
+
fail("Check that a Rails kenai/junction2 development mode server is running at #{SITE}")
|
10
|
+
end
|
11
|
+
|
12
|
+
describe KenaiTools::DownloadsClient do
|
13
|
+
before :all do
|
14
|
+
# Init downloads feature for test project oasis
|
15
|
+
dlclient = KenaiTools::DownloadsClient.new(SITE, "oasis", :downloads_name => "downloads")
|
16
|
+
dlclient.authenticate("mehdi", "mehdi") unless dlclient.authenticated?
|
17
|
+
dlclient.delete_feature('yes') if dlclient.ping
|
18
|
+
dlclient.get_or_create
|
19
|
+
end
|
20
|
+
|
21
|
+
# Larger timeout used here to debug server-side code or handling a large amount of data
|
22
|
+
let(:dlclient) { KenaiTools::DownloadsClient.new(SITE, "oasis", :timeout => 36000) }
|
23
|
+
let(:data) { Pathname.new(File.dirname(__FILE__) + '/fixtures/data') }
|
24
|
+
let(:file1) { data + "text1.txt" }
|
25
|
+
|
26
|
+
def ensure_write_permission
|
27
|
+
dlclient.authenticate("mehdi", "mehdi") unless dlclient.authenticated?
|
28
|
+
end
|
29
|
+
|
30
|
+
def ensure_remote_dir(remote_dir)
|
31
|
+
unless dlclient.entry_type(remote_dir) == 'directory'
|
32
|
+
ensure_write_permission
|
33
|
+
dlclient.rm_r(remote_dir) if dlclient.exist?(remote_dir)
|
34
|
+
dlclient.mkdir(remote_dir)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def ensure_remote_sample_data(rel_path)
|
39
|
+
ensure_write_permission
|
40
|
+
pn = data + rel_path
|
41
|
+
dlclient.push(pn) unless dlclient.exist?(rel_path)
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "authentication" do
|
45
|
+
it "should authenticate with valid credentials" do
|
46
|
+
dlclient.authenticate("mehdi", "mehdi").should be_true
|
47
|
+
dlclient.authenticated?.should be_true
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should fail to authenticate with invalid credentials" do
|
51
|
+
dlclient.authenticate("mehdi", "xmehdi").should be_false
|
52
|
+
dlclient.authenticated?.should be_false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "bootstrap" do
|
57
|
+
context "basic" do
|
58
|
+
# Note: this test depend upon sample downloads data in the development DB
|
59
|
+
it "should detect existence of a file" do
|
60
|
+
dlclient = KenaiTools::DownloadsClient.new(SITE, "glassfish")
|
61
|
+
dlclient.exist?("glassfishv4solaris.zip").should be_true
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should detect non-existence of a file" do
|
65
|
+
dlclient = KenaiTools::DownloadsClient.new(SITE, "glassfish")
|
66
|
+
dlclient.exist?("non-existent-download.zip").should be_false
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should return the entry_type of an entry" do
|
70
|
+
dlclient = KenaiTools::DownloadsClient.new(SITE, "glassfish")
|
71
|
+
dlclient.entry_type("glassfishv4solaris.zip").should == 'file'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context "authenticated" do
|
76
|
+
before :each do
|
77
|
+
dlclient.authenticate("mehdi", "mehdi")
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should upload a single file to the top level" do
|
81
|
+
dlclient.rm_r(file1.basename) if dlclient.exist?(file1.basename)
|
82
|
+
|
83
|
+
dlclient.push(file1)
|
84
|
+
dlclient.exist?(file1.basename).should be_true
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should destroy a single file at the top level" do
|
88
|
+
ensure_remote_sample_data(file1.basename)
|
89
|
+
|
90
|
+
dlclient.rm(file1.basename).should be_true
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should make a directory" do
|
94
|
+
dirname = "x11r5"
|
95
|
+
dlclient.rm_r(dirname) if dlclient.exist?(dirname)
|
96
|
+
|
97
|
+
dlclient.mkdir(dirname)
|
98
|
+
dlclient.entry_type(dirname).should == 'directory'
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should delete a directory" do
|
102
|
+
dirname = "x11r5"
|
103
|
+
dlclient.rm_r(dirname) if dlclient.exist?(dirname)
|
104
|
+
dlclient.mkdir(dirname)
|
105
|
+
|
106
|
+
dlclient.exist?(dirname).should be_true
|
107
|
+
dlclient.rm_r(dirname)
|
108
|
+
dlclient.exist?(dirname).should be_false
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "listing" do
|
114
|
+
before :each do
|
115
|
+
dlclient.authenticate("mehdi", "mehdi")
|
116
|
+
@dir19 = 'version-1.9'
|
117
|
+
ensure_remote_dir(@dir19)
|
118
|
+
ensure_remote_sample_data(file1.basename)
|
119
|
+
dlclient.authenticate("mehdi", "wrong-password").should be_false
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should list the top level downloads of a project as a directory named '/'" do
|
123
|
+
dlclient.ls.keys.should =~ ['href', 'display_name', 'entry_type', 'description', 'tags', 'children',
|
124
|
+
'created_at', 'updated_at', 'content_type']
|
125
|
+
dlclient.ls['entry_type'].should == 'directory'
|
126
|
+
dlclient.ls['display_name'].should == '/'
|
127
|
+
dlclient.ls['children'].map { |ch| ch['display_name'] }.should include(file1.basename.to_s, @dir19)
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should list a file" do
|
131
|
+
entry = dlclient.ls(file1.basename)
|
132
|
+
entry['entry_type'].should == 'file'
|
133
|
+
entry['entry_content_type'].should == 'text/plain'
|
134
|
+
open(entry['content_url']).read == file1.open.read
|
135
|
+
entry.keys.should =~ ['href', 'display_name', 'entry_type', 'description', 'tags', 'size',
|
136
|
+
'created_at', 'updated_at', 'content_url', 'entry_content_type', 'content_type']
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should list a subdirectory" do
|
140
|
+
entry = dlclient.ls(@dir19)
|
141
|
+
entry['entry_type'].should == 'directory'
|
142
|
+
entry.keys.should =~ ['href', 'display_name', 'entry_type', 'description', 'tags', 'children',
|
143
|
+
'created_at', 'updated_at', 'content_type']
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe "push" do
|
148
|
+
before :each do
|
149
|
+
dlclient.authenticate("mehdi", "mehdi")
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should upload a single file to a remote directory specified with a relative path" do
|
153
|
+
target_dir = "version-1.9"
|
154
|
+
ensure_remote_dir(target_dir)
|
155
|
+
|
156
|
+
dlclient.push(file1, target_dir)
|
157
|
+
target_file = File.join(target_dir, file1.basename)
|
158
|
+
dlclient.exist?(target_file).should be_true
|
159
|
+
end
|
160
|
+
|
161
|
+
it "should upload a single file to a remote directory specified with an absolute path" do
|
162
|
+
target_dir = "/version-1.9"
|
163
|
+
ensure_remote_dir(target_dir)
|
164
|
+
|
165
|
+
dlclient.push(file1, target_dir)
|
166
|
+
target_file = File.join(target_dir, file1.basename)
|
167
|
+
dlclient.exist?(target_file).should be_true
|
168
|
+
end
|
169
|
+
|
170
|
+
it "should recursively upload a directory into a new target directory" do
|
171
|
+
target_dir = "tax_year_2010"
|
172
|
+
ensure_remote_dir(target_dir)
|
173
|
+
src_dir = data + "irs_docs"
|
174
|
+
|
175
|
+
dlclient.push(src_dir, target_dir)
|
176
|
+
target_subdir = File.join(target_dir, src_dir.basename)
|
177
|
+
dlclient.entry_type(target_subdir).should == 'directory'
|
178
|
+
expected_names = src_dir.children.map { |ch| ch.basename.to_s }
|
179
|
+
actual_names = dlclient.ls(target_subdir)['children'].map { |ch| ch['display_name'] }
|
180
|
+
actual_names.should =~ expected_names
|
181
|
+
end
|
182
|
+
|
183
|
+
it "should recursively upload source directory contents if the source argument ends with a '/'" do
|
184
|
+
target_dir = "sax2r2"
|
185
|
+
ensure_remote_dir(target_dir)
|
186
|
+
src_dir = data + "sax2/"
|
187
|
+
|
188
|
+
dlclient.push(src_dir, target_dir)
|
189
|
+
expected_names = src_dir.children.map { |ch| ch.basename.to_s }
|
190
|
+
actual_names = dlclient.ls(target_dir)['children'].map { |ch| ch['display_name'] }
|
191
|
+
actual_names.should =~ expected_names
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe "remove files" do
|
196
|
+
before :each do
|
197
|
+
dlclient.authenticate("mehdi", "mehdi")
|
198
|
+
end
|
199
|
+
|
200
|
+
it "should remove a directory and its contents" do
|
201
|
+
dir = "irs_docs"
|
202
|
+
ensure_remote_sample_data(dir)
|
203
|
+
|
204
|
+
dlclient.rm_r(dir)
|
205
|
+
dlclient.exist?(dir).should be_false
|
206
|
+
end
|
207
|
+
|
208
|
+
it "should not remove a directory and its contents for rmdir" do
|
209
|
+
dir = "irs_docs"
|
210
|
+
ensure_remote_sample_data(dir)
|
211
|
+
|
212
|
+
lambda { dlclient.rmdir(dir) }.should raise_error(/not empty/)
|
213
|
+
dlclient.exist?(dir).should be_true
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
describe "miscellaneous" do
|
218
|
+
it "should ping a working service" do
|
219
|
+
dlclient.ping.should be_true
|
220
|
+
end
|
221
|
+
|
222
|
+
it "should fail to ping a non-working service" do
|
223
|
+
down_dlclient = KenaiTools::DownloadsClient.new(SITE, "bad-project")
|
224
|
+
down_dlclient.ping.should be_false
|
225
|
+
end
|
226
|
+
|
227
|
+
it "should discover the name of a downloads feature if a project only has one" do
|
228
|
+
dlclient2 = KenaiTools::DownloadsClient.new(SITE, "oasis")
|
229
|
+
dlclient2.downloads_name.should == 'downloads'
|
230
|
+
end
|
231
|
+
|
232
|
+
it "should discover the downloads feature if a project only has one" do
|
233
|
+
dlclient2 = KenaiTools::DownloadsClient.new(SITE, "oasis")
|
234
|
+
dlclient2.downloads_feature['type'].should == 'downloads'
|
235
|
+
end
|
236
|
+
|
237
|
+
it "should delete a downloads feature" do
|
238
|
+
dlclient2 = KenaiTools::DownloadsClient.new(SITE, "openjdk")
|
239
|
+
dlclient2.authenticate("craigmcc", "craigmcc") unless dlclient2.authenticated?
|
240
|
+
dlclient2.get_or_create
|
241
|
+
|
242
|
+
dlclient2.ping.should be_true
|
243
|
+
lambda { dlclient2.delete_feature }.should raise_error(/[Cc]onfirm/)
|
244
|
+
dlclient2.delete_feature('yes').should be_true
|
245
|
+
dlclient2.ping.should be_false
|
246
|
+
end
|
247
|
+
|
248
|
+
it "should create a downloads feature" do
|
249
|
+
dlclient2 = KenaiTools::DownloadsClient.new(SITE, "openjdk")
|
250
|
+
dlclient2.authenticate("craigmcc", "craigmcc") unless dlclient2.authenticated?
|
251
|
+
dlclient2.delete_feature('yes') if dlclient2.ping
|
252
|
+
|
253
|
+
dlclient2.ping.should be_false
|
254
|
+
dlclient2.get_or_create.should == 'downloads'
|
255
|
+
dlclient2.ping.should be_true
|
256
|
+
end
|
257
|
+
|
258
|
+
it "should make a directory with a name that needs to be encoded" do
|
259
|
+
dirname = "Web 2.0"
|
260
|
+
ensure_write_permission
|
261
|
+
dlclient.rm_r(dirname) if dlclient.exist?(dirname)
|
262
|
+
|
263
|
+
dlclient.mkdir(dirname)
|
264
|
+
dlclient.entry_type(dirname).should == 'directory'
|
265
|
+
|
266
|
+
dlclient.push(file1, dirname).should be_true
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
describe "pull" do
|
271
|
+
it "should download a single file to a local directory" do
|
272
|
+
ensure_remote_sample_data(file1.basename)
|
273
|
+
|
274
|
+
Dir.mktmpdir do |dir|
|
275
|
+
dlclient.pull(file1.basename, dir)
|
276
|
+
(Pathname(dir) + file1.basename).read.should == file1.read
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
it "should recursively download a remote subdirectory to a local directory" do
|
281
|
+
sample_dir = "irs_docs"
|
282
|
+
ensure_remote_sample_data(sample_dir)
|
283
|
+
|
284
|
+
Dir.mktmpdir do |dir|
|
285
|
+
dlclient.pull(sample_dir, dir)
|
286
|
+
dest_dir = Pathname(dir) + sample_dir
|
287
|
+
system("diff -r #{data + sample_dir} #{dest_dir}").should be_true
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
it "should recursively download all entries to a local directory" do
|
292
|
+
sample_dir = "irs_docs"
|
293
|
+
ensure_remote_sample_data(sample_dir)
|
294
|
+
|
295
|
+
Dir.mktmpdir do |dir|
|
296
|
+
dlclient.pull('/', dir)
|
297
|
+
dest_dir = Pathname(dir) + sample_dir
|
298
|
+
dest_dir.should be_exist
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
Binary file
|
Binary file
|
Binary file
|