docs.thor 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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. output = Docs.all.flat_map do |doc|
  14. name = doc.to_s.demodulize.underscore
  15. if doc.versioned?
  16. doc.versions.map { |_doc| "#{name}@#{_doc.version}" }
  17. else
  18. name
  19. end
  20. end.join("\n")
  21. require 'tty-pager'
  22. TTY::Pager.new.page(output)
  23. end
  24. desc 'page <doc> [path] [--version] [--verbose] [--debug]', 'Generate a page (no indexing)'
  25. option :version, type: :string
  26. option :verbose, type: :boolean
  27. option :debug, type: :boolean
  28. def page(name, path = '')
  29. unless path.empty? || path.start_with?('/')
  30. return puts 'ERROR: [path] must be an absolute path.'
  31. end
  32. Docs.install_report :store if options[:verbose]
  33. if options[:debug]
  34. GC.disable
  35. Docs.install_report :filter, :request
  36. end
  37. if Docs.generate_page(name, options[:version], path)
  38. puts 'Done'
  39. else
  40. puts "Failed!#{' (try running with --debug for more information)' unless options[:debug]}"
  41. end
  42. rescue Docs::DocNotFound => error
  43. handle_doc_not_found_error(error)
  44. end
  45. desc 'generate <doc> [--version] [--verbose] [--debug] [--force] [--package]', 'Generate a documentation'
  46. option :version, type: :string
  47. option :all, type: :boolean
  48. option :verbose, type: :boolean
  49. option :debug, type: :boolean
  50. option :force, type: :boolean
  51. option :package, type: :boolean
  52. def generate(name)
  53. Docs.rescue_errors = true
  54. Docs.install_report :store if options[:verbose]
  55. Docs.install_report :scraper if options[:debug]
  56. Docs.install_report :progress_bar, :doc if $stdout.tty?
  57. require 'unix_utils' if options[:package]
  58. doc = Docs.find(name, options[:version])
  59. if doc < Docs::UrlScraper && !options[:force]
  60. puts <<-TEXT.strip_heredoc
  61. /!\\ WARNING /!\\
  62. Some scrapers send thousands of HTTP requests in a short period of time,
  63. which can slow down the source site and trouble its maintainers.
  64. Please scrape responsibly. Don't do it unless you're modifying the code.
  65. To download the latest tested version of this documentation, run:
  66. thor docs:download #{name}\n
  67. TEXT
  68. return unless yes? 'Proceed? (y/n)'
  69. end
  70. result = if doc.version && options[:all]
  71. doc.superclass.versions.all? do |_doc|
  72. puts "==> #{_doc.version}"
  73. generate_doc(_doc, package: options[:package]).tap { puts "\n" }
  74. end
  75. else
  76. generate_doc(doc, package: options[:package])
  77. end
  78. generate_manifest if result
  79. rescue Docs::DocNotFound => error
  80. handle_doc_not_found_error(error)
  81. ensure
  82. Docs.rescue_errors = false
  83. end
  84. desc 'manifest', 'Create the manifest'
  85. def manifest
  86. generate_manifest
  87. puts 'Done'
  88. end
  89. desc 'download (<doc> <doc@version>... | --default | --installed)', 'Download documentations'
  90. option :default, type: :boolean
  91. option :installed, type: :boolean
  92. option :all, type: :boolean
  93. def download(*names)
  94. require 'unix_utils'
  95. docs = if options[:default]
  96. Docs.defaults
  97. elsif options[:installed]
  98. Docs.installed
  99. elsif options[:all]
  100. Docs.all_versions
  101. else
  102. find_docs(names)
  103. end
  104. assert_docs(docs)
  105. download_docs(docs)
  106. generate_manifest
  107. puts 'Done'
  108. rescue Docs::DocNotFound => error
  109. handle_doc_not_found_error(error)
  110. end
  111. desc 'package <doc> <doc@version>...', 'Package documentations'
  112. def package(*names)
  113. require 'unix_utils'
  114. docs = find_docs(names)
  115. assert_docs(docs)
  116. docs.each(&method(:package_doc))
  117. puts 'Done'
  118. rescue Docs::DocNotFound => error
  119. handle_doc_not_found_error(error)
  120. end
  121. desc 'clean', 'Delete documentation packages'
  122. def clean
  123. File.delete(*Dir[File.join Docs.store_path, '*.tar.gz'])
  124. puts 'Done'
  125. end
  126. desc 'upload', '[private]'
  127. option :dryrun, type: :boolean
  128. option :packaged, type: :boolean
  129. def upload(*names)
  130. names = Dir[File.join(Docs.store_path, '*.tar.gz')].map { |f| File.basename(f, '.tar.gz') } if options[:packaged]
  131. docs = find_docs(names)
  132. assert_docs(docs)
  133. docs.each do |doc|
  134. puts "Syncing #{doc.path}..."
  135. cmd = "aws s3 sync #{File.join(Docs.store_path, doc.path)} s3://docs.devdocs.io/#{doc.path} --delete"
  136. cmd << ' --dryrun' if options[:dryrun]
  137. system(cmd)
  138. end
  139. end
  140. desc 'commit', '[private]'
  141. option :message, type: :string
  142. option :amend, type: :boolean
  143. def commit(name)
  144. doc = Docs.find(name, false)
  145. message = options[:message] || "Update #{doc.name} documentation (#{doc.versions.map(&:release).join(', ')})"
  146. amend = " --amend" if options[:amend]
  147. system("git add assets/ *#{name}*") && system("git commit -m '#{message}'#{amend}")
  148. rescue Docs::DocNotFound => error
  149. handle_doc_not_found_error(error)
  150. end
  151. private
  152. def find_docs(names)
  153. names.map do |name|
  154. name, version = name.split(/@|~/)
  155. Docs.find(name, version)
  156. end
  157. end
  158. def assert_docs(docs)
  159. if docs.empty?
  160. puts 'ERROR: called with no arguments.'
  161. puts 'Run "thor list" for usage patterns.'
  162. exit
  163. end
  164. end
  165. def handle_doc_not_found_error(error)
  166. puts %(ERROR: #{error}.)
  167. puts 'Run "thor docs:list" to see the list of docs and versions.'
  168. end
  169. def generate_doc(doc, package: nil)
  170. if Docs.generate(doc)
  171. package_doc(doc) if package
  172. puts 'Done'
  173. true
  174. else
  175. puts "Failed!#{' (try running with --debug for more information)' unless options[:debug]}"
  176. false
  177. end
  178. end
  179. def download_docs(docs)
  180. # Don't allow downloaded files to be created as StringIO
  181. require 'open-uri'
  182. OpenURI::Buffer.send :remove_const, 'StringMax' if OpenURI::Buffer.const_defined?('StringMax')
  183. OpenURI::Buffer.const_set 'StringMax', 0
  184. require 'thread'
  185. length = docs.length
  186. mutex = Mutex.new
  187. i = 0
  188. (1..4).map do
  189. Thread.new do
  190. while doc = docs.shift
  191. status = begin
  192. download_doc(doc)
  193. 'OK'
  194. rescue => e
  195. "FAILED (#{e.class}: #{e.message})"
  196. end
  197. mutex.synchronize { puts "(#{i += 1}/#{length}) #{doc.name}#{ " #{doc.version}" if doc.version} #{status}" }
  198. end
  199. end
  200. end.map(&:join)
  201. end
  202. def download_doc(doc)
  203. target_path = File.join(Docs.store_path, doc.path)
  204. open "http://dl.devdocs.io/#{doc.path}.tar.gz" do |file|
  205. FileUtils.mkpath(target_path)
  206. file.close
  207. tar = UnixUtils.gunzip(file.path)
  208. dir = UnixUtils.untar(tar)
  209. FileUtils.rm_rf(target_path)
  210. FileUtils.mv(dir, target_path)
  211. FileUtils.rm(file.path)
  212. end
  213. end
  214. def package_doc(doc)
  215. path = File.join Docs.store_path, doc.path
  216. if File.exist?(path)
  217. tar = UnixUtils.tar(path)
  218. gzip = UnixUtils.gzip(tar)
  219. FileUtils.mv(gzip, "#{path}.tar.gz")
  220. FileUtils.rm(tar)
  221. else
  222. puts %(ERROR: can't find "#{doc.name}" documentation files.)
  223. end
  224. end
  225. def generate_manifest
  226. Docs.generate_manifest
  227. end
  228. end