docs.thor 7.0 KB

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