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.
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