docs.thor 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. class DocsCLI < Thor
  2. include Thor::Actions
  3. def self.to_s
  4. 'Docs'
  5. end
  6. def initialize(*args)
  7. require 'docs'
  8. trap('INT') { puts; exit! } # hide backtrace on ^C
  9. super
  10. end
  11. desc 'list', 'List available documentations'
  12. def list
  13. max_length = 0
  14. Docs.all.
  15. map { |doc| [doc.to_s.demodulize.underscore, doc] }.
  16. each { |pair| max_length = pair.first.length if pair.first.length > max_length }.
  17. each { |pair| puts "#{pair.first.rjust max_length + 1}: #{pair.second.base_url.remove %r{\Ahttps?://}}" }
  18. end
  19. desc 'page <doc> [path] [--verbose] [--debug]', 'Generate a page (no indexing)'
  20. option :verbose, type: :boolean
  21. option :debug, type: :boolean
  22. def page(name, path = '')
  23. unless path.empty? || path.start_with?('/')
  24. return puts 'ERROR: [path] must be an absolute path.'
  25. end
  26. Docs.install_report :store if options[:verbose]
  27. if options[:debug]
  28. GC.disable
  29. Docs.install_report :filter, :request
  30. end
  31. if Docs.generate_page(name, path)
  32. puts 'Done'
  33. else
  34. puts "Failed!#{' (try running with --debug for more information)' unless options[:debug]}"
  35. end
  36. rescue Docs::DocNotFound
  37. invalid_doc(name)
  38. end
  39. desc 'generate <doc> [--verbose] [--debug] [--force]', 'Generate a documentation'
  40. option :verbose, type: :boolean
  41. option :debug, type: :boolean
  42. option :force, type: :boolean
  43. def generate(name)
  44. Docs.install_report :store if options[:verbose]
  45. Docs.install_report :scraper if options[:debug]
  46. Docs.install_report :progress_bar, :doc if $stdout.tty?
  47. unless options[:force]
  48. puts <<-TEXT.strip_heredoc
  49. Note: this command will scrape the documentation from the source.
  50. Some scrapers require a local setup. Others will send thousands of
  51. HTTP requests, potentially slowing down the source site.
  52. Please don't use it unless you are modifying the code.
  53. To download the latest tested version of a documentation, use:
  54. thor docs:download #{name}\n
  55. TEXT
  56. return unless yes? 'Proceed? (y/n)'
  57. end
  58. if Docs.generate(name)
  59. generate_manifest
  60. puts 'Done'
  61. else
  62. puts "Failed!#{' (try running with --debug for more information)' unless options[:debug]}"
  63. end
  64. rescue Docs::DocNotFound
  65. invalid_doc(name)
  66. end
  67. desc 'manifest', 'Create the manifest'
  68. def manifest
  69. generate_manifest
  70. puts 'Done'
  71. end
  72. desc 'download (<doc> <doc>... | --all)', 'Download documentations'
  73. option :all, type: :boolean
  74. def download(*names)
  75. require 'unix_utils'
  76. docs = options[:all] ? Docs.all : find_docs(names)
  77. assert_docs(docs)
  78. download_docs(docs)
  79. generate_manifest
  80. puts 'Done'
  81. rescue Docs::DocNotFound => error
  82. invalid_doc(error.name)
  83. end
  84. desc 'package (<doc> <doc>... | --all)', 'Package documentations'
  85. option :all, type: :boolean
  86. def package(*names)
  87. require 'unix_utils'
  88. docs = options[:all] ? Docs.all : find_docs(names)
  89. assert_docs(docs)
  90. docs.each(&method(:package_doc))
  91. puts 'Done'
  92. rescue Docs::DocNotFound => error
  93. invalid_doc(error.name)
  94. end
  95. desc 'clean', 'Delete documentation packages'
  96. def clean
  97. File.delete(*Dir[File.join Docs.store_path, '*.tar.gz'])
  98. puts 'Done'
  99. end
  100. private
  101. def find_docs(names)
  102. names.map do |name|
  103. Docs.find(name)
  104. end
  105. end
  106. def assert_docs(docs)
  107. if docs.empty?
  108. puts 'ERROR: called with no arguments.'
  109. puts 'Run "thor docs:list" for usage patterns.'
  110. exit
  111. end
  112. end
  113. def invalid_doc(name)
  114. puts %(ERROR: invalid doc "#{name}".)
  115. puts 'Run "thor docs:list" to see the list of docs.'
  116. end
  117. def download_docs(docs)
  118. # Don't allow downloaded files to be created as StringIO
  119. require 'open-uri'
  120. OpenURI::Buffer.send :remove_const, 'StringMax' if OpenURI::Buffer.const_defined?('StringMax')
  121. OpenURI::Buffer.const_set 'StringMax', 0
  122. require 'thread'
  123. length = docs.length
  124. i = 0
  125. (1..4).map do
  126. Thread.new do
  127. while doc = docs.shift
  128. status = begin
  129. download_doc(doc)
  130. 'OK'
  131. rescue => e
  132. "FAILED (#{e.class}: #{e.message})"
  133. end
  134. puts "(#{i += 1}/#{length}) #{doc.name} #{status}"
  135. end
  136. end
  137. end.map(&:join)
  138. end
  139. def download_doc(doc)
  140. target = File.join(Docs.store_path, "#{doc.path}.tar.gz")
  141. open "http://dl.devdocs.io/#{doc.path}.tar.gz" do |file|
  142. FileUtils.mkpath(Docs.store_path)
  143. FileUtils.mv(file, target)
  144. unpackage_doc(doc)
  145. end
  146. end
  147. def unpackage_doc(doc)
  148. path = File.join(Docs.store_path, doc.path)
  149. FileUtils.mkpath(path)
  150. tar = UnixUtils.gunzip("#{path}.tar.gz")
  151. dir = UnixUtils.untar(tar)
  152. FileUtils.rm_rf(path)
  153. FileUtils.mv(dir, path)
  154. FileUtils.rm(tar)
  155. FileUtils.rm("#{path}.tar.gz")
  156. end
  157. def package_doc(doc)
  158. path = File.join Docs.store_path, doc.path
  159. if File.exist?(path)
  160. tar = UnixUtils.tar(path)
  161. gzip = UnixUtils.gzip(tar)
  162. FileUtils.mv(gzip, "#{path}.tar.gz")
  163. FileUtils.rm(tar)
  164. else
  165. puts %(ERROR: can't find "#{doc.name}" documentation files.)
  166. end
  167. end
  168. def generate_manifest
  169. Docs.generate_manifest
  170. end
  171. end