cdnget 0.2.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/cdnget.rb CHANGED
@@ -2,29 +2,97 @@
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  ##
5
- ## Download files from CDN (CDNJS, Google, jsDelivr).
5
+ ## Download files from CDN (CDNJS, jsDelivr, UNPKG, Google).
6
6
  ##
7
7
  ## - CDNJS (https://cdnjs.com/)
8
- ## - Google (https://developers.google.com/speed/libraries/)
9
8
  ## - jsDelivr (https://www.jsdelivr.com/)
9
+ ## - UNPKG (https://unpkg.com/)
10
+ ## - Google (https://developers.google.com/speed/libraries/)
10
11
  ##
11
12
  ## Example:
12
13
  ## $ cdnget # list public CDN
13
14
  ## $ cdnget [-q] cdnjs # list libraries
14
15
  ## $ cdnget [-q] cdnjs jquery # list versions
15
- ## $ cdnget [-q] cdnjs jquery 2.2.0 # list files
16
- ## $ cdnget [-q] cdnjs jquery 2.2.0 /tmp # download files
16
+ ## $ cdnget [-q] cdnjs jquery latest # detect latest version
17
+ ## $ cdnget [-q] cdnjs jquery 3.6.0 # list files
18
+ ## $ cdnget [-q] cdnjs jquery 3.6.0 /tmp # download files
17
19
  ##
18
20
 
19
21
  require 'open-uri'
22
+ require 'uri'
23
+ require 'net/http'
24
+ require 'openssl'
20
25
  require 'json'
21
26
  require 'fileutils'
27
+ require 'pp'
22
28
 
23
29
 
24
30
  module CDNGet
25
31
 
26
32
 
27
- RELEASE = '$Release: 0.2.0 $'.split()[1]
33
+ RELEASE = '$Release: 1.0.1 $'.split()[1]
34
+
35
+
36
+ class HttpConnection
37
+
38
+ def initialize(uri, headers=nil)
39
+ http = Net::HTTP.new(uri.host, uri.port)
40
+ if uri.scheme == 'https'
41
+ http.use_ssl = true
42
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
43
+ end
44
+ http.start()
45
+ @http = http
46
+ @headers = headers
47
+ end
48
+
49
+ def self.open(uri, headers=nil)
50
+ http = self.new(uri, headers)
51
+ return http unless block_given?()
52
+ begin
53
+ return yield http
54
+ ensure
55
+ http.close()
56
+ end
57
+ end
58
+
59
+ def get(uri)
60
+ resp = @http.send_request('GET', uri.path, nil, @headers)
61
+ case resp
62
+ when Net::HTTPSuccess
63
+ return resp.body
64
+ #when HTTPInformation, Net::HTTPRedirection, HTTPClientError, HTTPServerError
65
+ else
66
+ raise HttpError.new(resp.code.to_i, resp.message)
67
+ end
68
+ end
69
+
70
+ def post(uri, payload)
71
+ path = uri.path
72
+ path += "?"+uri.query if uri.query && !uri.query.empty?
73
+ resp = @http.send_request('POST', path, payload, @headers)
74
+ case resp
75
+ when Net::HTTPSuccess ; return resp.body
76
+ else ; raise HttpError.new(resp.code.to_i, resp.message)
77
+ end
78
+ end
79
+
80
+ def close()
81
+ @http.finish()
82
+ end
83
+
84
+ end
85
+
86
+
87
+ class HttpError < StandardError
88
+ def initialize(code, msgtext)
89
+ super("#{code} #{msgtext}")
90
+ @code = code
91
+ @msgtext = msgtext
92
+ end
93
+ attr_reader :code, :msgtext
94
+ end
95
+
28
96
 
29
97
  CLASSES = []
30
98
 
@@ -35,10 +103,22 @@ module CDNGet
35
103
  CLASSES << klass
36
104
  end
37
105
 
106
+ def initialize(debug_mode: false)
107
+ @debug_mode = debug_mode
108
+ end
109
+ attr_reader :debug_mode
110
+
38
111
  def list()
39
112
  raise NotImplementedError.new("#{self.class.name}#list(): not implemented yet.")
40
113
  end
41
114
 
115
+ def search(pattern)
116
+ return list().select {|d| File.fnmatch(pattern, d[:name], File::FNM_CASEFOLD) }
117
+ #rexp_str = pattern.split('*', -1).collect {|x| Regexp.escape(x) }.join('.*')
118
+ #rexp = Regexp.compile("\\A#{rexp_str}\\z", Regexp::IGNORECASE)
119
+ #return list().select {|d| d[:name] =~ rexp }
120
+ end
121
+
42
122
  def find(library)
43
123
  raise NotImplementedError.new("#{self.class.name}#find(): not implemented yet.")
44
124
  end
@@ -53,14 +133,36 @@ module CDNGet
53
133
  raise CommandError.new("#{basedir}: not exist.")
54
134
  File.directory?(basedir) or
55
135
  raise CommandError.new("#{basedir}: not a directory.")
56
- target_dir = File.join(basedir, library, version)
57
136
  d = get(library, version)
137
+ target_dir = d[:destdir] ? File.join(basedir, d[:destdir]) \
138
+ : File.join(basedir, library, version)
139
+ http = nil
140
+ skipfile = d[:skipfile] # ex: /\.DS_Store\z/
58
141
  d[:files].each do |file|
59
142
  filepath = File.join(target_dir, file)
143
+ #
144
+ if skipfile && file =~ skipfile
145
+ puts "#{filepath} ... Skipped" # for example, skip '.DS_Store' files
146
+ next
147
+ end
148
+ #
149
+ if filepath.end_with?('/')
150
+ if File.exist?(filepath)
151
+ puts "#{filepath} ... Done (Already exists)" unless quiet
152
+ else
153
+ print "#{filepath} ..." unless quiet
154
+ FileUtils.mkdir_p(filepath)
155
+ puts " Done (Created)" unless quiet
156
+ end
157
+ next
158
+ end
159
+ #
60
160
  dirpath = File.dirname(filepath)
61
161
  print "#{filepath} ..." unless quiet
62
162
  url = File.join(d[:baseurl], file) # not use URI.join!
63
- content = fetch(url)
163
+ uri = URI.parse(url)
164
+ http ||= HttpConnection.new(uri)
165
+ content = http.get(uri)
64
166
  content = content.force_encoding('ascii-8bit')
65
167
  print " Done (#{format_integer(content.bytesize)} byte)" unless quiet
66
168
  FileUtils.mkdir_p(dirpath) unless File.exist?(dirpath)
@@ -72,17 +174,32 @@ module CDNGet
72
174
  end
73
175
  puts() unless quiet
74
176
  end
177
+ http.close() if http
75
178
  nil
76
179
  end
77
180
 
78
181
  protected
79
182
 
183
+ def http_get(url)
184
+ ## * `open()` on Ruby 3.X can't open http url
185
+ ## * `URI.open()` on Ruby <= 2.4 raises NoMethodError (private method `open' called)
186
+ ## * `URI.__send__(:open)` is a hack to work on both Ruby 2.X and 3.X
187
+ return URI.__send__(:open, url, 'rb') {|f| f.read() }
188
+ end
189
+
80
190
  def fetch(url, library=nil)
81
191
  begin
82
- html = open(url, 'rb') {|f| f.read() }
192
+ html = http_get(url)
83
193
  return html
84
- rescue OpenURI::HTTPError => ex
85
- raise CommandError.new("GET #{url} : #{ex.message}")
194
+ rescue OpenURI::HTTPError => exc
195
+ if ! (exc.message == "404 Not Found" && library)
196
+ raise CommandError.new("GET #{url} : #{exc.message}")
197
+ elsif ! library.end_with?('js')
198
+ raise CommandError.new("#{library}: Library not found.")
199
+ else
200
+ maybe = library.end_with?('.js') ? library.sub('.js', 'js') : library.sub(/js$/, '.js')
201
+ raise CommandError.new("#{library}: Library not found (maybe '#{maybe}'?).")
202
+ end
86
203
  end
87
204
  end
88
205
 
@@ -101,83 +218,80 @@ module CDNGet
101
218
  return value.to_s.reverse.scan(/..?.?/).collect {|s| s.reverse }.reverse.join(',')
102
219
  end
103
220
 
104
- end
105
-
221
+ def _debug_print(x)
222
+ if @debug_mode
223
+ $stderr.puts "\e[0;35m*** #{PP.pp(x,'')}\e[0m"
224
+ end
225
+ end
106
226
 
107
- class HttpError < StandardError
108
227
  end
109
228
 
110
229
 
111
- class CDNJS < Base # TODO: use jsdelivr api
230
+ class CDNJS < Base
112
231
  CODE = "cdnjs"
113
232
  SITE_URL = 'https://cdnjs.com/'
114
233
 
115
234
  def fetch(url, library=nil)
116
- begin
117
- html = open(url, 'rb') {|f| f.read() }
118
- if library
119
- html =~ /<h1\b.*?>(.*?)<\/h1>/ or
120
- raise CommandError.new("#{library}: Library not found.")
121
- library == $1.strip() or
122
- raise CommandError.new("#{library}: Library not found (maybe '#{$1.strip()}'?).")
235
+ json_str = super
236
+ if json_str == "{}" && library
237
+ if library.end_with?('js')
238
+ maybe = library.end_with?('.js') \
239
+ ? library.sub('.js', 'js') \
240
+ : library.sub(/js$/, '.js')
241
+ raise CommandError.new("#{library}: Library not found (maybe '#{maybe}'?).")
242
+ else
243
+ raise CommandError.new("#{library}: Library not found.")
123
244
  end
124
- return html
125
- rescue OpenURI::HTTPError => ex
126
- raise HttpError.new("GET #{url} : #{ex.message}")
127
245
  end
246
+ return json_str
128
247
  end
129
248
  protected :fetch
130
249
 
131
- def list
132
- libs = []
133
- html = fetch("https://cdnjs.com/libraries")
134
- html.scan(/<td\b(.*?)<\/td>/m) do |str,|
135
- name = desc = nil
136
- name = $1 if str =~ /href="\/libraries\/([-.\w]+)">\s*\1\s*<\/a>/
137
- desc = $1.strip if str =~ /<div style="display: none;">(.*?)<\/div>/m
138
- libs << {name: name, desc: desc} if name
139
- end
250
+ def list()
251
+ jstr = fetch("https://api.cdnjs.com/libraries?fields=name,description")
252
+ jdata = JSON.parse(jstr)
253
+ _debug_print(jdata)
254
+ libs = jdata['results'].collect {|d| {name: d['name'], desc: d['description']} }
140
255
  return libs.sort_by {|d| d[:name] }.uniq
141
256
  end
142
257
 
143
258
  def find(library)
144
259
  validate(library, nil)
145
- html = fetch("https://cdnjs.com/libraries/#{library}", library)
146
- flagment = html.split(/<select class=".*?version-selector.*?"/, 2).last
147
- flagment = flagment.split(/<\/select>/, 2).first
148
- versions = []
149
- flagment.scan(/<option value="([^"]+)" *(?:selected)?>/) do |ver,|
150
- versions << ver
151
- end
152
- desc = tags = nil
153
- if html =~ /<\/p>\s*<p>(.*?)<\/p>\s*<em>(.*?)<\/em>/
154
- desc = $1
155
- tags = $2
156
- end
260
+ jstr = fetch("https://api.cdnjs.com/libraries/#{library}", library)
261
+ jdata = JSON.parse(jstr)
262
+ _debug_print(jdata)
263
+ versions = jdata['assets'].collect {|d| d['version'] }\
264
+ .sort_by {|v| v.split(/[-.]/).map(&:to_i) }
157
265
  return {
158
266
  name: library,
159
- desc: desc,
160
- tags: tags,
161
- versions: versions,
267
+ desc: jdata['description'],
268
+ tags: (jdata['keywords'] || []).join(", "),
269
+ site: jdata['homepage'],
270
+ info: File.join(SITE_URL, "/libraries/#{library}"),
271
+ license: jdata['license'],
272
+ versions: versions.reverse(),
162
273
  }
163
274
  end
164
275
 
165
276
  def get(library, version)
166
277
  validate(library, version)
167
- html = fetch("https://cdnjs.com/libraries/#{library}/#{version}", library)
278
+ jstr = fetch("https://api.cdnjs.com/libraries/#{library}", library)
279
+ jdata = JSON.parse(jstr)
280
+ _debug_print(jdata)
281
+ d = jdata['assets'].find {|d| d['version'] == version } or
282
+ raise CommandError.new("#{library}/#{version}: Library or version not found.")
168
283
  baseurl = "https://cdnjs.cloudflare.com/ajax/libs/#{library}/#{version}/"
169
- basepat = Regexp.escape("#{library}/#{version}")
170
- files = []
171
- html.scan(%r`>#{basepat}/([^<]+)<\/p>`) do |file,|
172
- files << file.gsub(/&#x2F;/, '/')
173
- end
174
- urls = files.collect {|s| baseurl + s }
175
284
  return {
176
285
  name: library,
177
286
  version: version,
178
- urls: urls,
179
- files: files,
287
+ desc: jdata['description'],
288
+ tags: (jdata['keywords'] || []).join(", "),
289
+ site: jdata['homepage'],
290
+ info: File.join(SITE_URL, "/libraries/#{library}/#{version}"),
291
+ urls: d['files'].collect {|s| baseurl + s },
292
+ files: d['files'],
180
293
  baseurl: baseurl,
294
+ license: jdata['license'],
181
295
  }
182
296
  end
183
297
 
@@ -187,59 +301,201 @@ module CDNGet
187
301
  class JSDelivr < Base
188
302
  CODE = "jsdelivr"
189
303
  SITE_URL = "https://www.jsdelivr.com/"
190
- API_URL = "https://api.jsdelivr.com/v1/jsdelivr/libraries"
304
+ #API_URL = "https://api.jsdelivr.com/v1/jsdelivr/libraries"
305
+ API_URL = "https://data.jsdelivr.com/v1"
306
+ HEADERS = {
307
+ "x-algo""lia-app""lication-id"=>"OFCNC""OG2CU",
308
+ "x-algo""lia-api""-key"=>"f54e21fa3a2""a0160595bb05""8179bfb1e",
309
+ }
191
310
 
192
- def list
193
- json = fetch("#{API_URL}?fields=name,description,homepage")
194
- arr = JSON.load(json)
195
- return arr.collect {|d|
196
- {name: d["name"], desc: d["description"], site: d["homepage"] }
311
+ def list()
312
+ return nil # nil means that this CDN can't list libraries without pattern
313
+ end
314
+
315
+ def search(pattern)
316
+ form_data = {
317
+ query: pattern,
318
+ page: "0",
319
+ hitsPerPage: "1000",
320
+ attributesToHighlight: '[]',
321
+ attributesToRetrieve: '["name","description","version"]'
322
+ }
323
+ payload = JSON.dump({"params"=>URI.encode_www_form(form_data)})
324
+ url = "https://ofcncog2cu-3.algolianet.com/1/indexes/npm-search/query"
325
+ uri = URI.parse(url)
326
+ json = HttpConnection.open(uri, HEADERS) {|http| http.post(uri, payload) }
327
+ jdata = JSON.load(json)
328
+ _debug_print(jdata)
329
+ return jdata["hits"].select {|d|
330
+ File.fnmatch(pattern, d["name"], File::FNM_CASEFOLD)
331
+ }.collect {|d|
332
+ {name: d["name"], desc: d["description"], version: d["version"]}
197
333
  }
198
334
  end
199
335
 
200
336
  def find(library)
201
337
  validate(library, nil)
202
- json = fetch("#{API_URL}?name=#{library}&fields=name,description,homepage,versions")
203
- arr = JSON.load(json)
204
- d = arr.first or
205
- raise CommandError.new("#{library}: Library not found.")
338
+ url = "https://ofcncog2cu-dsn.algolia.net/1/indexes/npm-search/#{library}"
339
+ uri = URI.parse(url)
340
+ begin
341
+ json = HttpConnection.open(uri, HEADERS) {|http| http.get(uri) }
342
+ rescue HttpError
343
+ raise CommandError, "#{library}: Library not found."
344
+ end
345
+ dict1 = JSON.load(json)
346
+ _debug_print(dict1)
347
+ #
348
+ json = fetch("#{API_URL}/package/npm/#{library}")
349
+ dict2 = JSON.load(json)
350
+ _debug_print(dict2)
351
+ #
352
+ d = dict1
206
353
  return {
207
- name: d['name'],
208
- desc: d['description'],
209
- site: d['homepage'],
210
- versions: d['versions'],
354
+ name: d['name'],
355
+ desc: d['description'],
356
+ #versions: d['versions'].collect {|k,v| k },
357
+ versions: dict2['versions'],
358
+ tags: (d['keywords'] || []).join(", "),
359
+ site: d['homepage'],
360
+ info: File.join(SITE_URL, "/package/npm/#{library}"),
361
+ license: d['license'],
211
362
  }
212
363
  end
213
364
 
214
365
  def get(library, version)
215
366
  validate(library, version)
216
- baseurl = "https://cdn.jsdelivr.net/#{library}/#{version}"
217
- url = "#{API_URL}/#{library}/#{version}"
218
- json = fetch("#{API_URL}/#{library}/#{version}")
219
- files = JSON.load(json)
220
- ! files.empty? or
221
- raise CommandError.new("#{library}: Library not found.")
222
- urls = files.collect {|x| "#{baseurl}/#{x}" }
367
+ url = File.join(API_URL, "/package/npm/#{library}@#{version}/flat")
368
+ begin
369
+ json = fetch(url, library)
370
+ rescue CommandError
371
+ raise CommandError.new("#{library}@#{version}: Library or version not found.")
372
+ end
373
+ jdata = JSON.load(json)
374
+ files = jdata["files"].collect {|d| d["name"] }
375
+ baseurl = "https://cdn.jsdelivr.net/npm/#{library}@#{version}"
376
+ _debug_print(jdata)
377
+ #
378
+ dict = find(library)
379
+ dict.delete(:versions)
380
+ dict.update({
381
+ version: version,
382
+ info: File.join(SITE_URL, "/package/npm/#{library}?version=#{version}"),
383
+ npmpkg: "https://registry.npmjs.org/#{library}/-/#{library}-#{version}.tgz",
384
+ urls: files.collect {|x| baseurl + x },
385
+ files: files,
386
+ baseurl: baseurl,
387
+ default: jdata["default"],
388
+ destdir: "#{library}@#{version}",
389
+ })
390
+ return dict
391
+ end
392
+
393
+ end
394
+
395
+
396
+ class Unpkg < Base
397
+ CODE = "unpkg"
398
+ SITE_URL = "https://unpkg.com/"
399
+ #API_URL = "https://www.npmjs.com"
400
+ API_URL = "https://api.npms.io/v2"
401
+
402
+ protected
403
+
404
+ def http_get(url)
405
+ return URI.__send__(:open, url, 'rb', {"x-spiferack"=>"1"}) {|f| f.read() }
406
+ end
407
+
408
+ public
409
+
410
+ def list()
411
+ return nil # nil means that this CDN can't list libraries without pattern
412
+ end
413
+
414
+ def search(pattern)
415
+ #json = fetch("#{API_URL}/search?q=#{pattern}")
416
+ json = fetch("#{API_URL}/search?q=#{pattern}&size=250")
417
+ jdata = JSON.load(json)
418
+ _debug_print(jdata)
419
+ #arr = jdata["objects"] # www.npmjs.com
420
+ arr = jdata["results"] # api.npms.io
421
+ return arr.select {|dict|
422
+ File.fnmatch(pattern, dict["package"]["name"], File::FNM_CASEFOLD)
423
+ }.collect {|dict|
424
+ d = dict["package"]
425
+ {name: d["name"], desc: d["description"], version: d["version"]}
426
+ }
427
+ end
428
+
429
+ def find(library)
430
+ validate(library, nil)
431
+ json = fetch("#{API_URL}/package/#{library}", library)
432
+ jdata = JSON.load(json)
433
+ _debug_print(jdata)
434
+ dict = jdata["collected"]["metadata"]
435
+ versions = [dict["version"]]
436
+ #
437
+ url = File.join(SITE_URL, "/browse/#{library}/")
438
+ html = fetch(url, library)
439
+ _debug_print(html)
440
+ if html =~ /<script>window.__DATA__\s*=\s*(.*?)<\/script>/m
441
+ jdata2 = JSON.load($1)
442
+ versions = jdata2["availableVersions"].reverse()
443
+ end
444
+ #
223
445
  return {
446
+ name: dict["name"],
447
+ desc: dict["description"],
448
+ tags: (dict["keywords"] || []).join(", "),
449
+ site: dict["links"] ? dict["links"]["homepage"] : dict["links"]["npm"],
450
+ info: File.join(SITE_URL, "/browse/#{library}/"),
451
+ versions: versions,
452
+ license: dict["license"],
453
+ }
454
+ end
455
+
456
+ def get(library, version)
457
+ validate(library, version)
458
+ dict = find(library)
459
+ dict.delete(:versions)
460
+ #
461
+ url = "https://data.jsdelivr.com/v1/package/npm/#{library}@#{version}/flat"
462
+ begin
463
+ json = fetch(url, library)
464
+ rescue CommandError
465
+ raise CommandError.new("#{library}@#{version}: Library or version not found.")
466
+ end
467
+ jdata = JSON.load(json)
468
+ files = jdata["files"].collect {|d| d["name"] }
469
+ baseurl = File.join(SITE_URL, "/#{library}@#{version}")
470
+ _debug_print(jdata)
471
+ #
472
+ dict.update({
224
473
  name: library,
225
474
  version: version,
226
- urls: urls,
475
+ info: File.join(SITE_URL, "/browse/#{library}@#{version}/"),
476
+ npmpkg: "https://registry.npmjs.org/#{library}/-/#{library}-#{version}.tgz",
477
+ urls: files.collect {|x| baseurl+x },
227
478
  files: files,
228
479
  baseurl: baseurl,
229
- }
480
+ default: jdata["default"],
481
+ destdir: "#{library}@#{version}",
482
+ skipfile: /\.DS_Store\z/, # downloading '.DS_Store' from UNPKG results in 403
483
+ })
484
+ return dict
230
485
  end
231
486
 
232
487
  end
233
488
 
234
489
 
235
- class GoogleCDN < Base # TODO: use jsdelivr api
490
+ class GoogleCDN < Base
236
491
  CODE = "google"
237
492
  SITE_URL = 'https://developers.google.com/speed/libraries/'
238
493
 
239
- def list
240
- libs = []
494
+ def list()
241
495
  html = fetch("https://developers.google.com/speed/libraries/")
496
+ _debug_print(html)
242
497
  rexp = %r`"https://ajax\.googleapis\.com/ajax/libs/([^/]+)/([^/]+)/([^"]+)"`
498
+ libs = []
243
499
  html.scan(rexp) do |lib, ver, file|
244
500
  libs << {name: lib, desc: "latest version: #{ver}" }
245
501
  end
@@ -249,6 +505,7 @@ module CDNGet
249
505
  def find(library)
250
506
  validate(library, nil)
251
507
  html = fetch("https://developers.google.com/speed/libraries/")
508
+ _debug_print(html)
252
509
  rexp = %r`"https://ajax\.googleapis\.com/ajax/libs/#{library}/`
253
510
  site_url = nil
254
511
  versions = []
@@ -259,9 +516,7 @@ module CDNGet
259
516
  found = true
260
517
  if text =~ /<dt>.*?snippet:<\/dt>\s*<dd>(.*?)<\/dd>/m
261
518
  s = $1
262
- s.scan(/\b(?:src|href)="([^"]*?)"/) do |href,|
263
- urls << href
264
- end
519
+ s.scan(/\b(?:src|href)="([^"]*?)"/) {|href,| urls << href }
265
520
  end
266
521
  if text =~ /<dt>site:<\/dt>\s*<dd>(.*?)<\/dd>/m
267
522
  s = $1
@@ -282,6 +537,7 @@ module CDNGet
282
537
  return {
283
538
  name: library,
284
539
  site: site_url,
540
+ info: "#{SITE_URL}\##{library}",
285
541
  urls: urls,
286
542
  versions: versions,
287
543
  }
@@ -302,6 +558,7 @@ module CDNGet
302
558
  return {
303
559
  name: d[:name],
304
560
  site: d[:site],
561
+ info: "#{SITE_URL}\##{library}",
305
562
  urls: urls,
306
563
  files: files,
307
564
  baseurl: baseurl,
@@ -334,24 +591,28 @@ module CDNGet
334
591
  @script = script || File.basename($0)
335
592
  end
336
593
 
337
- def help_message
594
+ def help_message()
338
595
  script = @script
339
596
  return <<END
340
- #{script} -- download files from public CDN
597
+ #{script} -- download files from public CDN (cdnjs/jsdelivr/unpkg/google)
341
598
 
342
- Usage: #{script} [options] [CDN] [library] [version] [directory]
599
+ Usage: #{script} [<options>] [<CDN> [<library> [<version> [<directory>]]]]
343
600
 
344
601
  Options:
345
602
  -h, --help : help
346
603
  -v, --version : version
347
604
  -q, --quiet : minimal output
605
+ --debug : (debug mode)
348
606
 
349
607
  Example:
350
- $ #{script} # list public CDN
608
+ $ #{script} # list public CDN names
351
609
  $ #{script} [-q] cdnjs # list libraries
610
+ $ #{script} [-q] cdnjs 'jquery*' # search libraries
352
611
  $ #{script} [-q] cdnjs jquery # list versions
353
- $ #{script} [-q] cdnjs jquery 2.2.0 # list files
354
- $ #{script} [-q] cdnjs jquery 2.2.0 /tmp # download files
612
+ $ #{script} [-q] cdnjs jquery latest # show latest version
613
+ $ #{script} [-q] cdnjs jquery 3.6.0 # list files
614
+ $ #{script} [-q] cdnjs jquery 3.6.0 /tmp # download files into directory
615
+
355
616
  END
356
617
  end
357
618
 
@@ -366,10 +627,11 @@ END
366
627
  end
367
628
 
368
629
  def run(*args)
369
- cmdopts = parse_cmdopts(args, "hvq", ["help", "version", "quiet"])
630
+ cmdopts = parse_cmdopts(args, "hvq", ["help", "version", "quiet", "debug"])
370
631
  return help_message() if cmdopts['h'] || cmdopts['help']
371
632
  return RELEASE + "\n" if cmdopts['v'] || cmdopts['version']
372
633
  @quiet = cmdopts['quiet'] || cmdopts['q']
634
+ @debug_mode = cmdopts['debug']
373
635
  #
374
636
  validate(args[1], args[2])
375
637
  #
@@ -381,11 +643,9 @@ END
381
643
  return do_list_libraries(cdn_code)
382
644
  when 2
383
645
  cdn_code, library = args
384
- if library.include?('*')
385
- return do_search_libraries(cdn_code, library)
386
- else
387
- return do_find_library(cdn_code, library)
388
- end
646
+ return library.include?('*') \
647
+ ? do_search_libraries(cdn_code, library) \
648
+ : do_find_library(cdn_code, library)
389
649
  when 3
390
650
  cdn_code, library, version = args
391
651
  return do_get_library(cdn_code, library, version)
@@ -439,35 +699,29 @@ END
439
699
  def find_cdn(cdn_code)
440
700
  klass = CLASSES.find {|c| c::CODE == cdn_code } or
441
701
  raise CommandError.new("#{cdn_code}: no such CDN.")
442
- return klass.new
702
+ return klass.new(debug_mode: @debug_mode)
443
703
  end
444
704
 
445
705
  def render_list(list)
446
- if @quiet
447
- return list.collect {|d| "#{d[:name]}\n" }.join()
448
- else
449
- return list.collect {|d| "%-20s # %s\n" % [d[:name], d[:desc]] }.join()
450
- end
706
+ return list.collect {|d| "#{d[:name]}\n" }.join() if @quiet
707
+ return list.collect {|d| "%-20s # %s\n" % [d[:name], d[:desc]] }.join()
451
708
  end
452
709
 
453
- def do_list_cdns
454
- if @quiet
455
- return CLASSES.map {|c| "#{c::CODE}\n" }.join()
456
- else
457
- return CLASSES.map {|c| "%-10s # %s\n" % [c::CODE, c::SITE_URL] }.join()
458
- end
710
+ def do_list_cdns()
711
+ return CLASSES.map {|c| "#{c::CODE}\n" }.join() if @quiet
712
+ return CLASSES.map {|c| "%-10s # %s\n" % [c::CODE, c::SITE_URL] }.join()
459
713
  end
460
714
 
461
715
  def do_list_libraries(cdn_code)
462
716
  cdn = find_cdn(cdn_code)
463
- return render_list(cdn.list)
717
+ list = cdn.list() or
718
+ raise CommandError.new("#{cdn_code}: cannot list libraries; please specify pattern such as 'jquery*'.")
719
+ return render_list(list)
464
720
  end
465
721
 
466
722
  def do_search_libraries(cdn_code, pattern)
467
723
  cdn = find_cdn(cdn_code)
468
- rexp_str = pattern.split('*', -1).collect {|x| Regexp.escape(x) }.join('.*')
469
- rexp = Regexp.compile("\\A#{rexp_str}\\z", Regexp::IGNORECASE)
470
- return render_list(cdn.list.select {|a| a[:name] =~ rexp })
724
+ return render_list(cdn.search(pattern))
471
725
  end
472
726
 
473
727
  def do_find_library(cdn_code, library)
@@ -479,10 +733,12 @@ END
479
733
  s << "#{ver}\n"
480
734
  end if d[:versions]
481
735
  else
482
- s << "name: #{d[:name]}\n"
483
- s << "desc: #{d[:desc]}\n" if d[:desc]
484
- s << "tags: #{d[:tags]}\n" if d[:tags]
485
- s << "site: #{d[:site]}\n" if d[:site]
736
+ s << "name: #{d[:name]}\n"
737
+ s << "desc: #{d[:desc]}\n" if d[:desc]
738
+ s << "tags: #{d[:tags]}\n" if d[:tags]
739
+ s << "site: #{d[:site]}\n" if d[:site]
740
+ s << "info: #{d[:info]}\n" if d[:info]
741
+ s << "license: #{d[:license]}\n" if d[:license]
486
742
  s << "snippet: |\n" << d[:snippet].gsub(/^/, ' ') if d[:snippet]
487
743
  s << "versions:\n"
488
744
  d[:versions].each do |ver|
@@ -494,6 +750,7 @@ END
494
750
 
495
751
  def do_get_library(cdn_code, library, version)
496
752
  cdn = find_cdn(cdn_code)
753
+ version = _latest_version(cdn, library) if version == 'latest'
497
754
  d = cdn.get(library, version)
498
755
  s = ""
499
756
  if @quiet
@@ -503,8 +760,13 @@ END
503
760
  else
504
761
  s << "name: #{d[:name]}\n"
505
762
  s << "version: #{d[:version]}\n"
506
- s << "tags: #{d[:tags]}\n" if d[:tags]
507
- s << "site: #{d[:site]}\n" if d[:site]
763
+ s << "desc: #{d[:desc]}\n" if d[:desc]
764
+ s << "tags: #{d[:tags]}\n" if d[:tags]
765
+ s << "site: #{d[:site]}\n" if d[:site]
766
+ s << "info: #{d[:info]}\n" if d[:info]
767
+ s << "npmpkg: #{d[:npmpkg]}\n" if d[:npmpkg]
768
+ s << "default: #{d[:default]}\n" if d[:default]
769
+ s << "license: #{d[:license]}\n" if d[:license]
508
770
  s << "snippet: |\n" << d[:snippet].gsub(/^/, ' ') if d[:snippet]
509
771
  s << "urls:\n" if d[:urls]
510
772
  d[:urls].each do |url|
@@ -516,10 +778,18 @@ END
516
778
 
517
779
  def do_download_library(cdn_code, library, version, basedir)
518
780
  cdn = find_cdn(cdn_code)
781
+ version = _latest_version(cdn, library) if version == 'latest'
519
782
  cdn.download(library, version, basedir, quiet: @quiet)
520
783
  return nil
521
784
  end
522
785
 
786
+ private
787
+
788
+ def _latest_version(cdn, library)
789
+ d = cdn.find(library)
790
+ return d[:versions].first
791
+ end
792
+
523
793
  end
524
794
 
525
795