cdnget 0.3.1 → 1.1.0
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 +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/bin/cdnget
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
|
|