cdnget 0.3.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +40 -0
- data/MIT-LICENSE +1 -1
- data/README.md +33 -11
- data/Rakefile +2 -2
- data/bin/cdnget +471 -142
- data/cdnget.gemspec +4 -5
- data/lib/cdnget.rb +471 -142
- data/test/cdnget_test.rb +1061 -216
- metadata +7 -21
data/lib/cdnget.rb
CHANGED
@@ -1,31 +1,109 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# -*- coding: utf-8 -*-
|
3
|
+
# frozen_string_literal: true
|
3
4
|
|
4
5
|
##
|
5
|
-
## Download files from CDN
|
6
|
+
## Download JS/CSS files from public CDN.
|
6
7
|
##
|
7
|
-
##
|
8
|
-
##
|
9
|
-
##
|
8
|
+
## * CDNJS (https://cdnjs.com/)
|
9
|
+
## * jsDelivr (https://www.jsdelivr.com/)
|
10
|
+
## * UNPKG (https://unpkg.com/)
|
11
|
+
## * Google (https://developers.google.com/speed/libraries/)
|
10
12
|
##
|
11
13
|
## Example:
|
12
14
|
## $ cdnget # list public CDN
|
13
|
-
## $ cdnget [-q] cdnjs # list libraries
|
15
|
+
## $ cdnget [-q] cdnjs # list libraries (except jsdelivr/unpkg)
|
16
|
+
## $ cdnget [-q] cdnjs '*jquery*' # search libraries
|
14
17
|
## $ cdnget [-q] cdnjs jquery # list versions
|
15
|
-
## $ cdnget [-q] cdnjs jquery
|
16
|
-
## $ cdnget [-q] cdnjs jquery
|
18
|
+
## $ cdnget [-q] cdnjs jquery latest # detect latest version
|
19
|
+
## $ cdnget [-q] cdnjs jquery 3.6.0 # list files
|
20
|
+
## $ mkdir -p static/lib # create a directory
|
21
|
+
## $ cdnget [-q] cdnjs jquery 3.6.0 static/lib # download files
|
17
22
|
##
|
18
23
|
|
19
24
|
require 'open-uri'
|
20
25
|
require 'uri'
|
26
|
+
require 'net/http'
|
27
|
+
require 'openssl'
|
21
28
|
require 'json'
|
22
29
|
require 'fileutils'
|
30
|
+
require 'pp'
|
23
31
|
|
24
32
|
|
25
33
|
module CDNGet
|
26
34
|
|
27
35
|
|
28
|
-
RELEASE = '$Release:
|
36
|
+
RELEASE = '$Release: 1.1.0 $'.split()[1]
|
37
|
+
|
38
|
+
|
39
|
+
class HttpConnection
|
40
|
+
|
41
|
+
def initialize(uri, headers=nil)
|
42
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
43
|
+
if uri.scheme == 'https'
|
44
|
+
http.use_ssl = true
|
45
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
46
|
+
end
|
47
|
+
http.start()
|
48
|
+
@http = http
|
49
|
+
@headers = headers
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.open(uri, headers=nil)
|
53
|
+
http = self.new(uri, headers)
|
54
|
+
return http unless block_given?()
|
55
|
+
begin
|
56
|
+
return yield http
|
57
|
+
ensure
|
58
|
+
http.close()
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def get(uri)
|
63
|
+
resp = request('GET', uri.path, uri.query)
|
64
|
+
return _get_resp_body(resp)
|
65
|
+
end
|
66
|
+
|
67
|
+
def post(uri, payload)
|
68
|
+
resp = request('POST', uri.path, uri.query, payload: payload)
|
69
|
+
return _get_resp_body(resp)
|
70
|
+
end
|
71
|
+
|
72
|
+
def request(meth, path, query=nil, payload: nil, headers: nil)
|
73
|
+
path += "?" + query if query
|
74
|
+
if @headers
|
75
|
+
headers ||= {}
|
76
|
+
headers.update(@headers)
|
77
|
+
end
|
78
|
+
resp = @http.send_request(meth, path, payload, headers)
|
79
|
+
return resp
|
80
|
+
end
|
81
|
+
|
82
|
+
def _get_resp_body(resp)
|
83
|
+
case resp
|
84
|
+
when Net::HTTPSuccess
|
85
|
+
return resp.body
|
86
|
+
else
|
87
|
+
raise HttpError.new(resp.code.to_i, resp.message)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def close()
|
92
|
+
@http.finish()
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
class HttpError < StandardError
|
99
|
+
def initialize(code, msgtext)
|
100
|
+
super("#{code} #{msgtext}")
|
101
|
+
@code = code
|
102
|
+
@msgtext = msgtext
|
103
|
+
end
|
104
|
+
attr_reader :code, :msgtext
|
105
|
+
end
|
106
|
+
|
29
107
|
|
30
108
|
CLASSES = []
|
31
109
|
|
@@ -36,10 +114,22 @@ module CDNGet
|
|
36
114
|
CLASSES << klass
|
37
115
|
end
|
38
116
|
|
117
|
+
def initialize(debug_mode: false)
|
118
|
+
@debug_mode = debug_mode
|
119
|
+
end
|
120
|
+
attr_reader :debug_mode
|
121
|
+
|
39
122
|
def list()
|
40
123
|
raise NotImplementedError.new("#{self.class.name}#list(): not implemented yet.")
|
41
124
|
end
|
42
125
|
|
126
|
+
def search(pattern)
|
127
|
+
return list().select {|d| File.fnmatch(pattern, d[:name], File::FNM_CASEFOLD) }
|
128
|
+
#rexp_str = pattern.split('*', -1).collect {|x| Regexp.escape(x) }.join('.*')
|
129
|
+
#rexp = Regexp.compile("\\A#{rexp_str}\\z", Regexp::IGNORECASE)
|
130
|
+
#return list().select {|d| d[:name] =~ rexp }
|
131
|
+
end
|
132
|
+
|
43
133
|
def find(library)
|
44
134
|
raise NotImplementedError.new("#{self.class.name}#find(): not implemented yet.")
|
45
135
|
end
|
@@ -54,14 +144,36 @@ module CDNGet
|
|
54
144
|
raise CommandError.new("#{basedir}: not exist.")
|
55
145
|
File.directory?(basedir) or
|
56
146
|
raise CommandError.new("#{basedir}: not a directory.")
|
57
|
-
target_dir = File.join(basedir, library, version)
|
58
147
|
d = get(library, version)
|
148
|
+
target_dir = d[:destdir] ? File.join(basedir, d[:destdir]) \
|
149
|
+
: File.join(basedir, library, version)
|
150
|
+
http = nil
|
151
|
+
skipfile = d[:skipfile] # ex: /\.DS_Store\z/
|
59
152
|
d[:files].each do |file|
|
60
153
|
filepath = File.join(target_dir, file)
|
154
|
+
#
|
155
|
+
if skipfile && file =~ skipfile
|
156
|
+
puts "#{filepath} ... Skipped" # for example, skip '.DS_Store' files
|
157
|
+
next
|
158
|
+
end
|
159
|
+
#
|
160
|
+
if filepath.end_with?('/')
|
161
|
+
if File.exist?(filepath)
|
162
|
+
puts "#{filepath} ... Done (Already exists)" unless quiet
|
163
|
+
else
|
164
|
+
print "#{filepath} ..." unless quiet
|
165
|
+
FileUtils.mkdir_p(filepath)
|
166
|
+
puts " Done (Created)" unless quiet
|
167
|
+
end
|
168
|
+
next
|
169
|
+
end
|
170
|
+
#
|
61
171
|
dirpath = File.dirname(filepath)
|
62
172
|
print "#{filepath} ..." unless quiet
|
63
173
|
url = File.join(d[:baseurl], file) # not use URI.join!
|
64
|
-
|
174
|
+
uri = URI.parse(url)
|
175
|
+
http ||= HttpConnection.new(uri)
|
176
|
+
content = http.get(uri)
|
65
177
|
content = content.force_encoding('ascii-8bit')
|
66
178
|
print " Done (#{format_integer(content.bytesize)} byte)" unless quiet
|
67
179
|
FileUtils.mkdir_p(dirpath) unless File.exist?(dirpath)
|
@@ -73,56 +185,83 @@ module CDNGet
|
|
73
185
|
end
|
74
186
|
puts() unless quiet
|
75
187
|
end
|
188
|
+
http.close() if http
|
76
189
|
nil
|
77
190
|
end
|
78
191
|
|
192
|
+
def latest_version(library)
|
193
|
+
validate(library, nil)
|
194
|
+
d = self.find(library)
|
195
|
+
return d[:versions].first
|
196
|
+
end
|
197
|
+
|
79
198
|
protected
|
80
199
|
|
200
|
+
def http_get(url)
|
201
|
+
## * `open()` on Ruby 3.X can't open http url
|
202
|
+
## * `URI.open()` on Ruby <= 2.4 raises NoMethodError (private method `open' called)
|
203
|
+
## * `URI.__send__(:open)` is a hack to work on both Ruby 2.X and 3.X
|
204
|
+
return URI.__send__(:open, url, 'rb') {|f| f.read() }
|
205
|
+
end
|
206
|
+
|
81
207
|
def fetch(url, library=nil)
|
82
208
|
begin
|
83
|
-
html =
|
209
|
+
html = http_get(url)
|
84
210
|
return html
|
85
|
-
rescue OpenURI::HTTPError =>
|
86
|
-
|
211
|
+
rescue OpenURI::HTTPError => exc
|
212
|
+
if ! (exc.message == "404 Not Found" && library)
|
213
|
+
raise CommandError.new("GET #{url} : #{exc.message}")
|
214
|
+
elsif ! library.end_with?('js')
|
215
|
+
raise CommandError.new("#{library}: Library not found.")
|
216
|
+
else
|
217
|
+
maybe = library.end_with?('.js') ? library.sub('.js', 'js') : library.sub(/js$/, '.js')
|
218
|
+
raise CommandError.new("#{library}: Library not found (maybe '#{maybe}'?).")
|
219
|
+
end
|
87
220
|
end
|
88
221
|
end
|
89
222
|
|
223
|
+
LIBRARY_REXP = /\A[-.\w]+\z/
|
224
|
+
VERSION_REXP = /\A\d+(\.\d+)+([-.\w]+)?/
|
225
|
+
|
90
226
|
def validate(library, version)
|
91
227
|
if library
|
92
|
-
|
93
|
-
|
228
|
+
rexp = self.class.const_get(:LIBRARY_REXP)
|
229
|
+
library =~ self.class.const_get(:LIBRARY_REXP) or
|
230
|
+
raise CommandError.new("#{library}: Invalid library name.")
|
94
231
|
end
|
95
232
|
if version
|
96
|
-
version =~
|
97
|
-
raise
|
233
|
+
version =~ self.class.const_get(:VERSION_REXP) or
|
234
|
+
raise CommandError.new("#{version}: Invalid version number.")
|
98
235
|
end
|
99
236
|
end
|
100
237
|
|
238
|
+
def npmpkg_url(library, version)
|
239
|
+
pkg = library.sub(/^@[-\w]+/, '')
|
240
|
+
path = "/#{library.gsub('/', '%2f')}/-/#{pkg}-#{version}.tgz"
|
241
|
+
return "https://registry.npmjs.org#{path}"
|
242
|
+
end
|
243
|
+
|
101
244
|
def format_integer(value)
|
102
245
|
return value.to_s.reverse.scan(/..?.?/).collect {|s| s.reverse }.reverse.join(',')
|
103
246
|
end
|
104
247
|
|
105
|
-
|
106
|
-
|
248
|
+
def _debug_print(x)
|
249
|
+
if @debug_mode
|
250
|
+
$stderr.puts "\e[0;35m*** #{PP.pp(x,String.new)}\e[0m"
|
251
|
+
end
|
252
|
+
end
|
107
253
|
|
108
|
-
class HttpError < StandardError
|
109
254
|
end
|
110
255
|
|
111
256
|
|
112
|
-
class CDNJS < Base
|
257
|
+
class CDNJS < Base
|
113
258
|
CODE = "cdnjs"
|
114
|
-
SITE_URL =
|
259
|
+
SITE_URL = "https://cdnjs.com/"
|
260
|
+
API_URL = "https://api.cdnjs.com/libraries"
|
261
|
+
CDN_URL = "https://cdnjs.cloudflare.com/ajax/libs"
|
115
262
|
|
116
263
|
def fetch(url, library=nil)
|
117
|
-
|
118
|
-
json_str = URI.open(url, 'rb') {|f| f.read() }
|
119
|
-
rescue OpenURI::HTTPError => exc
|
120
|
-
if exc.message == "404 Not Found"
|
121
|
-
json_str = "{}"
|
122
|
-
else
|
123
|
-
raise HttpError.new("GET #{url} : #{ex.message}")
|
124
|
-
end
|
125
|
-
end
|
264
|
+
json_str = super
|
126
265
|
if json_str == "{}" && library
|
127
266
|
if library.end_with?('js')
|
128
267
|
maybe = library.end_with?('.js') \
|
@@ -137,43 +276,51 @@ module CDNGet
|
|
137
276
|
end
|
138
277
|
protected :fetch
|
139
278
|
|
140
|
-
def list
|
141
|
-
|
142
|
-
jstr = fetch("https://api.cdnjs.com/libraries?fields=name,description")
|
279
|
+
def list()
|
280
|
+
jstr = fetch("#{API_URL}?fields=name,description")
|
143
281
|
jdata = JSON.parse(jstr)
|
282
|
+
_debug_print(jdata)
|
144
283
|
libs = jdata['results'].collect {|d| {name: d['name'], desc: d['description']} }
|
145
284
|
return libs.sort_by {|d| d[:name] }.uniq
|
146
285
|
end
|
147
286
|
|
148
287
|
def find(library)
|
149
288
|
validate(library, nil)
|
150
|
-
jstr = fetch("
|
289
|
+
jstr = fetch("#{API_URL}/#{library}", library)
|
151
290
|
jdata = JSON.parse(jstr)
|
291
|
+
_debug_print(jdata)
|
152
292
|
versions = jdata['assets'].collect {|d| d['version'] }\
|
153
293
|
.sort_by {|v| v.split(/[-.]/).map(&:to_i) }
|
154
294
|
return {
|
155
295
|
name: library,
|
156
296
|
desc: jdata['description'],
|
157
297
|
tags: (jdata['keywords'] || []).join(", "),
|
298
|
+
site: jdata['homepage'],
|
299
|
+
info: File.join(SITE_URL, "/libraries/#{library}"),
|
300
|
+
license: jdata['license'],
|
158
301
|
versions: versions.reverse(),
|
159
302
|
}
|
160
303
|
end
|
161
304
|
|
162
305
|
def get(library, version)
|
163
306
|
validate(library, version)
|
164
|
-
jstr = fetch("
|
307
|
+
jstr = fetch("#{API_URL}/#{library}", library)
|
165
308
|
jdata = JSON.parse(jstr)
|
309
|
+
_debug_print(jdata)
|
166
310
|
d = jdata['assets'].find {|d| d['version'] == version } or
|
167
311
|
raise CommandError.new("#{library}/#{version}: Library or version not found.")
|
168
|
-
baseurl = "
|
312
|
+
baseurl = "#{CDN_URL}/#{library}/#{version}/"
|
169
313
|
return {
|
170
314
|
name: library,
|
315
|
+
version: version,
|
171
316
|
desc: jdata['description'],
|
172
317
|
tags: (jdata['keywords'] || []).join(", "),
|
173
|
-
|
318
|
+
site: jdata['homepage'],
|
319
|
+
info: File.join(SITE_URL, "/libraries/#{library}/#{version}"),
|
174
320
|
urls: d['files'].collect {|s| baseurl + s },
|
175
321
|
files: d['files'],
|
176
322
|
baseurl: baseurl,
|
323
|
+
license: jdata['license'],
|
177
324
|
}
|
178
325
|
end
|
179
326
|
|
@@ -183,59 +330,237 @@ module CDNGet
|
|
183
330
|
class JSDelivr < Base
|
184
331
|
CODE = "jsdelivr"
|
185
332
|
SITE_URL = "https://www.jsdelivr.com/"
|
186
|
-
API_URL = "https://api.jsdelivr.com/v1/jsdelivr/libraries"
|
333
|
+
#API_URL = "https://api.jsdelivr.com/v1/jsdelivr/libraries"
|
334
|
+
API_URL = "https://data.jsdelivr.com/v1"
|
335
|
+
CDN_URL = "https://cdn.jsdelivr.net/npm"
|
336
|
+
HEADERS = {
|
337
|
+
"x-algo""lia-app""lication-id"=>"OFCNC""OG2CU",
|
338
|
+
"x-algo""lia-api""-key"=>"f54e21fa3a2""a0160595bb05""8179bfb1e",
|
339
|
+
}
|
340
|
+
LIBRARY_REXP = /\A([-.\w]+|\@[-\w]+\/[-.\w]+)\z/
|
341
|
+
|
342
|
+
def list()
|
343
|
+
return nil # nil means that this CDN can't list libraries without pattern
|
344
|
+
end
|
187
345
|
|
188
|
-
def
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
346
|
+
def search(pattern)
|
347
|
+
form_data = {
|
348
|
+
query: pattern,
|
349
|
+
page: "0",
|
350
|
+
hitsPerPage: "1000",
|
351
|
+
attributesToHighlight: '[]',
|
352
|
+
attributesToRetrieve: '["name","description","version"]'
|
353
|
+
}
|
354
|
+
payload = JSON.dump({"params"=>URI.encode_www_form(form_data)})
|
355
|
+
url = "https://ofcncog2cu-3.algolianet.com/1/indexes/npm-search/query"
|
356
|
+
uri = URI.parse(url)
|
357
|
+
json = HttpConnection.open(uri, HEADERS) {|http| http.post(uri, payload) }
|
358
|
+
jdata = JSON.load(json)
|
359
|
+
_debug_print(jdata)
|
360
|
+
return jdata["hits"].select {|d|
|
361
|
+
File.fnmatch(pattern, d["name"], File::FNM_CASEFOLD)
|
362
|
+
}.collect {|d|
|
363
|
+
{name: d["name"], desc: d["description"], version: d["version"]}
|
193
364
|
}
|
194
365
|
end
|
195
366
|
|
196
367
|
def find(library)
|
197
368
|
validate(library, nil)
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
369
|
+
url = "https://ofcncog2cu-dsn.algolia.net/1/indexes/npm-search/#{library.sub('/', '%2f')}"
|
370
|
+
uri = URI.parse(url)
|
371
|
+
begin
|
372
|
+
json = HttpConnection.open(uri, HEADERS) {|http| http.get(uri) }
|
373
|
+
rescue HttpError
|
374
|
+
raise CommandError, "#{library}: Library not found."
|
375
|
+
end
|
376
|
+
dict1 = JSON.load(json)
|
377
|
+
_debug_print(dict1)
|
378
|
+
#
|
379
|
+
json = fetch("#{API_URL}/package/npm/#{library}")
|
380
|
+
dict2 = JSON.load(json)
|
381
|
+
_debug_print(dict2)
|
382
|
+
#
|
383
|
+
d = dict1
|
202
384
|
return {
|
203
|
-
name:
|
204
|
-
desc:
|
205
|
-
|
206
|
-
versions:
|
385
|
+
name: d['name'],
|
386
|
+
desc: d['description'],
|
387
|
+
#versions: d['versions'].collect {|k,v| k },
|
388
|
+
versions: dict2['versions'],
|
389
|
+
tags: (d['keywords'] || []).join(", "),
|
390
|
+
site: d['homepage'],
|
391
|
+
info: File.join(SITE_URL, "/package/npm/#{library}"),
|
392
|
+
license: d['license'],
|
207
393
|
}
|
208
394
|
end
|
209
395
|
|
210
396
|
def get(library, version)
|
211
397
|
validate(library, version)
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
398
|
+
url = File.join(API_URL, "/package/npm/#{library}@#{version}/flat")
|
399
|
+
begin
|
400
|
+
json = fetch(url, library)
|
401
|
+
rescue CommandError
|
402
|
+
raise CommandError.new("#{library}@#{version}: Library or version not found.")
|
403
|
+
end
|
404
|
+
jdata = JSON.load(json)
|
405
|
+
files = jdata["files"].collect {|d| d["name"] }
|
406
|
+
baseurl = "#{CDN_URL}/#{library}@#{version}"
|
407
|
+
_debug_print(jdata)
|
408
|
+
#
|
409
|
+
dict = find(library)
|
410
|
+
dict.delete(:versions)
|
411
|
+
dict.update({
|
412
|
+
version: version,
|
413
|
+
info: File.join(SITE_URL, "/package/npm/#{library}?version=#{version}"),
|
414
|
+
npmpkg: npmpkg_url(library, version),
|
415
|
+
urls: files.collect {|x| baseurl + x },
|
416
|
+
files: files,
|
417
|
+
baseurl: baseurl,
|
418
|
+
default: jdata["default"],
|
419
|
+
destdir: "#{library}@#{version}",
|
420
|
+
})
|
421
|
+
return dict
|
422
|
+
end
|
423
|
+
|
424
|
+
def latest_version(library)
|
425
|
+
validate(library, nil)
|
426
|
+
json = fetch("#{API_URL}/package/npm/#{library}")
|
427
|
+
jdict = JSON.load(json)
|
428
|
+
return jdict["tags"]["latest"]
|
429
|
+
end
|
430
|
+
|
431
|
+
end
|
432
|
+
|
433
|
+
|
434
|
+
class Unpkg < Base
|
435
|
+
CODE = "unpkg"
|
436
|
+
SITE_URL = "https://unpkg.com/"
|
437
|
+
#API_URL = "https://www.npmjs.com"
|
438
|
+
API_URL = "https://api.npms.io/v2"
|
439
|
+
LIBRARY_REXP = /\A([-.\w]+|\@[-\w]+\/[-.\w]+)\z/
|
440
|
+
|
441
|
+
protected
|
442
|
+
|
443
|
+
def http_get(url)
|
444
|
+
return URI.__send__(:open, url, 'rb', {"x-spiferack"=>"1"}) {|f| f.read() }
|
445
|
+
end
|
446
|
+
|
447
|
+
public
|
448
|
+
|
449
|
+
def list()
|
450
|
+
return nil # nil means that this CDN can't list libraries without pattern
|
451
|
+
end
|
452
|
+
|
453
|
+
def search(pattern)
|
454
|
+
#json = fetch("#{API_URL}/search?q=#{pattern}")
|
455
|
+
json = fetch("#{API_URL}/search?q=#{pattern}&size=250")
|
456
|
+
jdata = JSON.load(json)
|
457
|
+
_debug_print(jdata)
|
458
|
+
#arr = jdata["objects"] # www.npmjs.com
|
459
|
+
arr = jdata["results"] # api.npms.io
|
460
|
+
return arr.select {|dict|
|
461
|
+
File.fnmatch(pattern, dict["package"]["name"], File::FNM_CASEFOLD)
|
462
|
+
}.collect {|dict|
|
463
|
+
d = dict["package"]
|
464
|
+
{name: d["name"], desc: d["description"], version: d["version"]}
|
465
|
+
}
|
466
|
+
end
|
467
|
+
|
468
|
+
def find(library)
|
469
|
+
validate(library, nil)
|
470
|
+
json = fetch("#{API_URL}/package/#{library.sub('/', '%2f')}", library)
|
471
|
+
jdata = JSON.load(json)
|
472
|
+
_debug_print(jdata)
|
473
|
+
dict = jdata["collected"]["metadata"]
|
474
|
+
versions = [dict["version"]]
|
475
|
+
#
|
476
|
+
url = File.join(SITE_URL, "/browse/#{library}/")
|
477
|
+
html = fetch(url, library)
|
478
|
+
_debug_print(html)
|
479
|
+
if html =~ /<script>window.__DATA__\s*=\s*(.*?)<\/script>/m
|
480
|
+
jdata2 = JSON.load($1)
|
481
|
+
versions = jdata2["availableVersions"].reverse()
|
482
|
+
end
|
483
|
+
#
|
219
484
|
return {
|
485
|
+
name: dict["name"],
|
486
|
+
desc: dict["description"],
|
487
|
+
tags: (dict["keywords"] || []).join(", "),
|
488
|
+
site: dict["links"] ? dict["links"]["homepage"] : dict["links"]["npm"],
|
489
|
+
info: File.join(SITE_URL, "/browse/#{library}/"),
|
490
|
+
versions: versions,
|
491
|
+
license: dict["license"],
|
492
|
+
}
|
493
|
+
end
|
494
|
+
|
495
|
+
def get(library, version)
|
496
|
+
validate(library, version)
|
497
|
+
dict = find(library)
|
498
|
+
dict.delete(:versions)
|
499
|
+
#
|
500
|
+
url = "#{SITE_URL}#{library}@#{version}/?meta"
|
501
|
+
begin
|
502
|
+
json = fetch(url, library)
|
503
|
+
rescue CommandError
|
504
|
+
raise CommandError.new("#{library}@#{version}: Version not found.")
|
505
|
+
end
|
506
|
+
jdata = JSON.load(json)
|
507
|
+
_debug_print(jdata)
|
508
|
+
pr = proc do |jdata, files|
|
509
|
+
jdata['files'].each do |d|
|
510
|
+
d['type'] == "directory" ? pr.call(d, files) \
|
511
|
+
: (files << d['path'])
|
512
|
+
end if jdata['files']
|
513
|
+
files
|
514
|
+
end
|
515
|
+
files = pr.call(jdata, [])
|
516
|
+
#files = files.sort_by {|s| s.downcase }
|
517
|
+
baseurl = File.join(SITE_URL, "/#{library}@#{version}")
|
518
|
+
#
|
519
|
+
dict.update({
|
220
520
|
name: library,
|
221
521
|
version: version,
|
222
|
-
|
522
|
+
info: File.join(SITE_URL, "/browse/#{library}@#{version}/"),
|
523
|
+
npmpkg: npmpkg_url(library, version),
|
524
|
+
urls: files.collect {|x| baseurl+x },
|
223
525
|
files: files,
|
224
526
|
baseurl: baseurl,
|
225
|
-
|
527
|
+
#default: jdata["default"],
|
528
|
+
destdir: "#{library}@#{version}",
|
529
|
+
skipfile: /\.DS_Store\z/, # downloading '.DS_Store' from UNPKG results in 403
|
530
|
+
})
|
531
|
+
return dict
|
532
|
+
end
|
533
|
+
|
534
|
+
def latest_version(library)
|
535
|
+
validate(library, nil)
|
536
|
+
version = nil
|
537
|
+
url = File.join(SITE_URL, "/browse/#{library}/")
|
538
|
+
uri = URI.parse(url)
|
539
|
+
HttpConnection.open(URI.parse(SITE_URL)) do |http|
|
540
|
+
resp = http.request('HEAD', "/browse/#{library}/")
|
541
|
+
if resp.code == "302" # 302 Found
|
542
|
+
location = resp.header['Location']
|
543
|
+
location =~ /@([^@\/]+)\/\z/
|
544
|
+
version = $1
|
545
|
+
end
|
546
|
+
end
|
547
|
+
version ||= super(library)
|
548
|
+
return version
|
226
549
|
end
|
227
550
|
|
228
551
|
end
|
229
552
|
|
230
553
|
|
231
|
-
class GoogleCDN < Base
|
554
|
+
class GoogleCDN < Base
|
232
555
|
CODE = "google"
|
233
|
-
SITE_URL =
|
556
|
+
SITE_URL = "https://developers.google.com/speed/libraries/"
|
557
|
+
CDN_URL = "https://ajax\.googleapis\.com/ajax/libs"
|
234
558
|
|
235
|
-
def list
|
559
|
+
def list()
|
560
|
+
html = fetch(SITE_URL)
|
561
|
+
_debug_print(html)
|
562
|
+
rexp = %r`"#{Regexp.escape(CDN_URL)}/([^/]+)/([^/]+)/([^"]+)"`
|
236
563
|
libs = []
|
237
|
-
html = fetch("https://developers.google.com/speed/libraries/")
|
238
|
-
rexp = %r`"https://ajax\.googleapis\.com/ajax/libs/([^/]+)/([^/]+)/([^"]+)"`
|
239
564
|
html.scan(rexp) do |lib, ver, file|
|
240
565
|
libs << {name: lib, desc: "latest version: #{ver}" }
|
241
566
|
end
|
@@ -244,8 +569,9 @@ module CDNGet
|
|
244
569
|
|
245
570
|
def find(library)
|
246
571
|
validate(library, nil)
|
247
|
-
html = fetch(
|
248
|
-
|
572
|
+
html = fetch(SITE_URL)
|
573
|
+
_debug_print(html)
|
574
|
+
rexp = %r`"#{Regexp.escape(CDN_URL)}/#{library}/`
|
249
575
|
site_url = nil
|
250
576
|
versions = []
|
251
577
|
urls = []
|
@@ -255,9 +581,7 @@ module CDNGet
|
|
255
581
|
found = true
|
256
582
|
if text =~ /<dt>.*?snippet:<\/dt>\s*<dd>(.*?)<\/dd>/m
|
257
583
|
s = $1
|
258
|
-
s.scan(/\b(?:src|href)="([^"]*?)"/)
|
259
|
-
urls << href
|
260
|
-
end
|
584
|
+
s.scan(/\b(?:src|href)="([^"]*?)"/) {|href,| urls << href }
|
261
585
|
end
|
262
586
|
if text =~ /<dt>site:<\/dt>\s*<dd>(.*?)<\/dd>/m
|
263
587
|
s = $1
|
@@ -278,6 +602,7 @@ module CDNGet
|
|
278
602
|
return {
|
279
603
|
name: library,
|
280
604
|
site: site_url,
|
605
|
+
info: "#{SITE_URL}\##{library}",
|
281
606
|
urls: urls,
|
282
607
|
versions: versions,
|
283
608
|
}
|
@@ -286,18 +611,19 @@ module CDNGet
|
|
286
611
|
def get(library, version)
|
287
612
|
validate(library, version)
|
288
613
|
d = find(library)
|
289
|
-
d[:versions].find
|
290
|
-
raise CommandError.new("#{version}:
|
614
|
+
d[:versions].find {|s| s == version } or
|
615
|
+
raise CommandError.new("#{library} #{version}: Version not found.")
|
291
616
|
urls = d[:urls]
|
292
617
|
if urls
|
293
618
|
rexp = /(\/libs\/#{library})\/[^\/]+/
|
294
619
|
urls = urls.collect {|x| x.gsub(rexp, "\\1/#{version}") }
|
295
620
|
end
|
296
|
-
baseurl = "
|
621
|
+
baseurl = "#{CDN_URL}/#{library}/#{version}"
|
297
622
|
files = urls ? urls.collect {|x| x[baseurl.length..-1] } : nil
|
298
623
|
return {
|
299
624
|
name: d[:name],
|
300
625
|
site: d[:site],
|
626
|
+
info: "#{SITE_URL}\##{library}",
|
301
627
|
urls: urls,
|
302
628
|
files: files,
|
303
629
|
baseurl: baseurl,
|
@@ -330,24 +656,34 @@ module CDNGet
|
|
330
656
|
@script = script || File.basename($0)
|
331
657
|
end
|
332
658
|
|
333
|
-
def help_message
|
659
|
+
def help_message()
|
334
660
|
script = @script
|
335
661
|
return <<END
|
336
|
-
#{script} -- download files from public CDN
|
662
|
+
#{script} -- download files from public CDN (cdnjs/jsdelivr/unpkg/google)
|
337
663
|
|
338
|
-
Usage: #{script} [options] [CDN
|
664
|
+
Usage: #{script} [<options>] [<CDN> [<library> [<version> [<directory>]]]]
|
339
665
|
|
340
666
|
Options:
|
341
667
|
-h, --help : help
|
342
668
|
-v, --version : version
|
343
669
|
-q, --quiet : minimal output
|
670
|
+
--debug : (debug mode)
|
344
671
|
|
345
672
|
Example:
|
346
|
-
$ #{script}
|
347
|
-
$ #{script} [-q] cdnjs
|
348
|
-
$ #{script} [-q] cdnjs jquery
|
349
|
-
$ #{script} [-q] cdnjs jquery
|
350
|
-
$ #{script} [-q] cdnjs jquery
|
673
|
+
$ #{script} # list public CDN names
|
674
|
+
$ #{script} [-q] cdnjs # list libraries (except jsdelivr/unpkg)
|
675
|
+
$ #{script} [-q] cdnjs 'jquery*' # search libraries
|
676
|
+
$ #{script} [-q] cdnjs jquery # list versions
|
677
|
+
$ #{script} [-q] cdnjs jquery latest # show latest version
|
678
|
+
$ #{script} [-q] cdnjs jquery 2.2.0 # list files
|
679
|
+
$ mkdir -p static/lib # create a directory
|
680
|
+
$ #{script} [-q] cdnjs jquery 2.2.0 static/lib # download files
|
681
|
+
static/lib/jquery/2.2.0/jquery.js ... Done (258,388 byte)
|
682
|
+
static/lib/jquery/2.2.0/jquery.min.js ... Done (85,589 byte)
|
683
|
+
static/lib/jquery/2.2.0/jquery.min.map ... Done (129,544 byte)
|
684
|
+
$ ls static/lib/jquery/2.2.0
|
685
|
+
jquery.js jquery.min.js jquery.min.map
|
686
|
+
|
351
687
|
END
|
352
688
|
end
|
353
689
|
|
@@ -362,13 +698,11 @@ END
|
|
362
698
|
end
|
363
699
|
|
364
700
|
def run(*args)
|
365
|
-
cmdopts = parse_cmdopts(args, "hvq", ["help", "version", "quiet"])
|
701
|
+
cmdopts = parse_cmdopts(args, "hvq", ["help", "version", "quiet", "debug"])
|
366
702
|
return help_message() if cmdopts['h'] || cmdopts['help']
|
367
703
|
return RELEASE + "\n" if cmdopts['v'] || cmdopts['version']
|
368
704
|
@quiet = cmdopts['quiet'] || cmdopts['q']
|
369
|
-
|
370
|
-
validate(args[1], args[2])
|
371
|
-
#
|
705
|
+
@debug_mode = cmdopts['debug']
|
372
706
|
case args.length
|
373
707
|
when 0
|
374
708
|
return do_list_cdns()
|
@@ -377,11 +711,9 @@ END
|
|
377
711
|
return do_list_libraries(cdn_code)
|
378
712
|
when 2
|
379
713
|
cdn_code, library = args
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
return do_find_library(cdn_code, library)
|
384
|
-
end
|
714
|
+
return library.include?('*') \
|
715
|
+
? do_search_libraries(cdn_code, library) \
|
716
|
+
: do_find_library(cdn_code, library)
|
385
717
|
when 3
|
386
718
|
cdn_code, library, version = args
|
387
719
|
return do_get_library(cdn_code, library, version)
|
@@ -394,17 +726,6 @@ END
|
|
394
726
|
end
|
395
727
|
end
|
396
728
|
|
397
|
-
def validate(library, version)
|
398
|
-
if library && ! library.include?('*')
|
399
|
-
library =~ /\A[-.\w]+\z/ or
|
400
|
-
raise CommandError.new("#{library}: Unexpected library name.")
|
401
|
-
end
|
402
|
-
if version
|
403
|
-
version =~ /\A[-.\w]+\z/ or
|
404
|
-
raise CommandError.new("#{version}: Unexpected version number.")
|
405
|
-
end
|
406
|
-
end
|
407
|
-
|
408
729
|
def parse_cmdopts(cmdargs, short_opts, long_opts)
|
409
730
|
cmdopts = {}
|
410
731
|
while cmdargs[0] && cmdargs[0].start_with?('-')
|
@@ -435,88 +756,96 @@ END
|
|
435
756
|
def find_cdn(cdn_code)
|
436
757
|
klass = CLASSES.find {|c| c::CODE == cdn_code } or
|
437
758
|
raise CommandError.new("#{cdn_code}: no such CDN.")
|
438
|
-
return klass.new
|
759
|
+
return klass.new(debug_mode: @debug_mode)
|
439
760
|
end
|
440
761
|
|
441
762
|
def render_list(list)
|
442
|
-
if @quiet
|
443
|
-
|
444
|
-
else
|
445
|
-
return list.collect {|d| "%-20s # %s\n" % [d[:name], d[:desc]] }.join()
|
446
|
-
end
|
763
|
+
return list.collect {|d| "#{d[:name]}\n" }.join() if @quiet
|
764
|
+
return list.collect {|d| "%-20s # %s\n" % [d[:name], d[:desc]] }.join()
|
447
765
|
end
|
448
766
|
|
449
|
-
def do_list_cdns
|
450
|
-
if @quiet
|
451
|
-
|
452
|
-
else
|
453
|
-
return CLASSES.map {|c| "%-10s # %s\n" % [c::CODE, c::SITE_URL] }.join()
|
454
|
-
end
|
767
|
+
def do_list_cdns()
|
768
|
+
return CLASSES.map {|c| "#{c::CODE}\n" }.join() if @quiet
|
769
|
+
return CLASSES.map {|c| "%-10s # %s\n" % [c::CODE, c::SITE_URL] }.join()
|
455
770
|
end
|
456
771
|
|
457
772
|
def do_list_libraries(cdn_code)
|
458
773
|
cdn = find_cdn(cdn_code)
|
459
|
-
|
774
|
+
list = cdn.list() or
|
775
|
+
raise CommandError.new("#{cdn_code}: cannot list libraries; please specify pattern such as 'jquery*'.")
|
776
|
+
return render_list(list)
|
460
777
|
end
|
461
778
|
|
462
779
|
def do_search_libraries(cdn_code, pattern)
|
463
780
|
cdn = find_cdn(cdn_code)
|
464
|
-
|
465
|
-
rexp = Regexp.compile("\\A#{rexp_str}\\z", Regexp::IGNORECASE)
|
466
|
-
return render_list(cdn.list.select {|a| a[:name] =~ rexp })
|
781
|
+
return render_list(cdn.search(pattern))
|
467
782
|
end
|
468
783
|
|
469
784
|
def do_find_library(cdn_code, library)
|
470
785
|
cdn = find_cdn(cdn_code)
|
471
786
|
d = cdn.find(library)
|
472
|
-
|
787
|
+
buf = []
|
473
788
|
if @quiet
|
474
789
|
d[:versions].each do |ver|
|
475
|
-
|
476
|
-
end
|
790
|
+
buf << "#{ver}\n"
|
791
|
+
end unless empty?(d[:versions])
|
477
792
|
else
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
793
|
+
buf << "name: #{d[:name]}\n"
|
794
|
+
buf << "desc: #{d[:desc]}\n" unless empty?(d[:desc])
|
795
|
+
buf << "tags: #{d[:tags]}\n" unless empty?(d[:tags])
|
796
|
+
buf << "site: #{d[:site]}\n" unless empty?(d[:site])
|
797
|
+
buf << "info: #{d[:info]}\n" unless empty?(d[:info])
|
798
|
+
buf << "license: #{d[:license]}\n" unless empty?(d[:license])
|
799
|
+
buf << "snippet: |\n" << d[:snippet].gsub(/^/, ' ') unless empty?(d[:snippet])
|
800
|
+
buf << "versions:\n"
|
484
801
|
d[:versions].each do |ver|
|
485
|
-
|
486
|
-
end
|
802
|
+
buf << " - #{ver}\n"
|
803
|
+
end unless empty?(d[:versions])
|
487
804
|
end
|
488
|
-
return
|
805
|
+
return buf.join()
|
489
806
|
end
|
490
807
|
|
491
808
|
def do_get_library(cdn_code, library, version)
|
492
809
|
cdn = find_cdn(cdn_code)
|
810
|
+
version = cdn.latest_version(library) if version == 'latest'
|
493
811
|
d = cdn.get(library, version)
|
494
|
-
|
812
|
+
buf = []
|
495
813
|
if @quiet
|
496
814
|
d[:urls].each do |url|
|
497
|
-
|
815
|
+
buf << "#{url}\n"
|
498
816
|
end if d[:urls]
|
499
817
|
else
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
818
|
+
buf << "name: #{d[:name]}\n"
|
819
|
+
buf << "version: #{d[:version]}\n"
|
820
|
+
buf << "desc: #{d[:desc]}\n" unless empty?(d[:desc])
|
821
|
+
buf << "tags: #{d[:tags]}\n" unless empty?(d[:tags])
|
822
|
+
buf << "site: #{d[:site]}\n" unless empty?(d[:site])
|
823
|
+
buf << "info: #{d[:info]}\n" unless empty?(d[:info])
|
824
|
+
buf << "npmpkg: #{d[:npmpkg]}\n" unless empty?(d[:npmpkg])
|
825
|
+
buf << "default: #{d[:default]}\n" unless empty?(d[:default])
|
826
|
+
buf << "license: #{d[:license]}\n" unless empty?(d[:license])
|
827
|
+
buf << "snippet: |\n" << d[:snippet].gsub(/^/, ' ') unless empty?(d[:snippet])
|
828
|
+
buf << "urls:\n" unless empty?(d[:urls])
|
507
829
|
d[:urls].each do |url|
|
508
|
-
|
509
|
-
end
|
830
|
+
buf << " - #{url}\n"
|
831
|
+
end unless empty?(d[:urls])
|
510
832
|
end
|
511
|
-
return
|
833
|
+
return buf.join()
|
512
834
|
end
|
513
835
|
|
514
836
|
def do_download_library(cdn_code, library, version, basedir)
|
515
837
|
cdn = find_cdn(cdn_code)
|
838
|
+
version = cdn.latest_version(library) if version == 'latest'
|
516
839
|
cdn.download(library, version, basedir, quiet: @quiet)
|
517
840
|
return nil
|
518
841
|
end
|
519
842
|
|
843
|
+
private
|
844
|
+
|
845
|
+
def empty?(x)
|
846
|
+
return x.nil? || x.empty?
|
847
|
+
end
|
848
|
+
|
520
849
|
end
|
521
850
|
|
522
851
|
|