cdnget 0.3.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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 (CDNJS, Google, jsDelivr).
6
+ ## Download JS/CSS files from public CDN.
6
7
  ##
7
- ## - CDNJS (https://cdnjs.com/)
8
- ## - Google (https://developers.google.com/speed/libraries/)
9
- ## - jsDelivr (https://www.jsdelivr.com/)
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 2.2.0 # list files
16
- ## $ cdnget [-q] cdnjs jquery 2.2.0 /tmp # download files
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: 0.3.1 $'.split()[1]
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
- content = fetch(url)
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 = URI.open(url, 'rb') {|f| f.read() }
209
+ html = http_get(url)
84
210
  return html
85
- rescue OpenURI::HTTPError => ex
86
- raise CommandError.new("GET #{url} : #{ex.message}")
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
- library =~ /\A[-.\w]+\z/ or
93
- raise ArgumentError.new("#{library.inspect}: Unexpected library name.")
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 =~ /\A\d+(\.\d+)+([-.\w]+)?/ or
97
- raise ArgumentError.new("#{version.inspect}: Unexpected version number.")
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
- end
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 # TODO: use jsdelivr api
257
+ class CDNJS < Base
113
258
  CODE = "cdnjs"
114
- SITE_URL = 'https://cdnjs.com/'
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
- begin
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
- libs = []
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("https://api.cdnjs.com/libraries/#{library}", library)
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("https://api.cdnjs.com/libraries/#{library}", library)
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 = "https://cdnjs.cloudflare.com/ajax/libs/#{library}/#{version}/"
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
- version: version,
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 list
189
- json = fetch("#{API_URL}?fields=name,description,homepage")
190
- arr = JSON.load(json)
191
- return arr.collect {|d|
192
- {name: d["name"], desc: d["description"], site: d["homepage"] }
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
- json = fetch("#{API_URL}?name=#{library}&fields=name,description,homepage,versions")
199
- arr = JSON.load(json)
200
- d = arr.first or
201
- raise CommandError.new("#{library}: Library not found.")
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: d['name'],
204
- desc: d['description'],
205
- site: d['homepage'],
206
- versions: d['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
- baseurl = "https://cdn.jsdelivr.net/#{library}/#{version}"
213
- url = "#{API_URL}/#{library}/#{version}"
214
- json = fetch("#{API_URL}/#{library}/#{version}")
215
- files = JSON.load(json)
216
- ! files.empty? or
217
- raise CommandError.new("#{library}: Library not found.")
218
- urls = files.collect {|x| "#{baseurl}/#{x}" }
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
- urls: urls,
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 # TODO: use jsdelivr api
554
+ class GoogleCDN < Base
232
555
  CODE = "google"
233
- SITE_URL = 'https://developers.google.com/speed/libraries/'
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("https://developers.google.com/speed/libraries/")
248
- rexp = %r`"https://ajax\.googleapis\.com/ajax/libs/#{library}/`
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)="([^"]*?)"/) do |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(version) or
290
- raise CommandError.new("#{version}: No such version of #{library}.")
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 = "https://ajax.googleapis.com/ajax/libs/#{library}/#{version}"
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] [library] [version] [directory]
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} # list public CDN
347
- $ #{script} [-q] cdnjs # list libraries
348
- $ #{script} [-q] cdnjs jquery # list versions
349
- $ #{script} [-q] cdnjs jquery 2.2.0 # list files
350
- $ #{script} [-q] cdnjs jquery 2.2.0 /tmp # download files
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
- if library.include?('*')
381
- return do_search_libraries(cdn_code, library)
382
- else
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
- return list.collect {|d| "#{d[:name]}\n" }.join()
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
- return CLASSES.map {|c| "#{c::CODE}\n" }.join()
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
- return render_list(cdn.list)
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
- rexp_str = pattern.split('*', -1).collect {|x| Regexp.escape(x) }.join('.*')
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
- s = ""
787
+ buf = []
473
788
  if @quiet
474
789
  d[:versions].each do |ver|
475
- s << "#{ver}\n"
476
- end if d[:versions]
790
+ buf << "#{ver}\n"
791
+ end unless empty?(d[:versions])
477
792
  else
478
- s << "name: #{d[:name]}\n"
479
- s << "desc: #{d[:desc]}\n" if d[:desc]
480
- s << "tags: #{d[:tags]}\n" if d[:tags]
481
- s << "site: #{d[:site]}\n" if d[:site]
482
- s << "snippet: |\n" << d[:snippet].gsub(/^/, ' ') if d[:snippet]
483
- s << "versions:\n"
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
- s << " - #{ver}\n"
486
- end if d[:versions]
802
+ buf << " - #{ver}\n"
803
+ end unless empty?(d[:versions])
487
804
  end
488
- return s
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
- s = ""
812
+ buf = []
495
813
  if @quiet
496
814
  d[:urls].each do |url|
497
- s << "#{url}\n"
815
+ buf << "#{url}\n"
498
816
  end if d[:urls]
499
817
  else
500
- s << "name: #{d[:name]}\n"
501
- s << "version: #{d[:version]}\n"
502
- s << "desc: #{d[:desc]}\n" if d[:desc]
503
- s << "tags: #{d[:tags]}\n" if d[:tags]
504
- s << "site: #{d[:site]}\n" if d[:site]
505
- s << "snippet: |\n" << d[:snippet].gsub(/^/, ' ') if d[:snippet]
506
- s << "urls:\n" if d[:urls]
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
- s << " - #{url}\n"
509
- end if d[:urls]
830
+ buf << " - #{url}\n"
831
+ end unless empty?(d[:urls])
510
832
  end
511
- return s
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