docs.thor 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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. unless options[:force]
  57. puts <<-TEXT.strip_heredoc
  58. Note: this command will scrape the documentation from the source.
  59. Some scrapers require a local setup. Others will send thousands of
  60. HTTP requests, potentially slowing down the source site.
  61. Please don't use it unless you are modifying the code.
  62. To download the latest tested version of a documentation, use:
  63. thor docs:download #{name}\n
  64. TEXT
  65. return unless yes? 'Proceed? (y/n)'
  66. end
  67. require 'unix_utils' if options[:package]
  68. doc = Docs.find(name, options[:version])
  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. private
  138. def find_docs(names)
  139. names.map do |name|
  140. name, version = name.split(/@|~/)
  141. Docs.find(name, version)
  142. end
  143. end
  144. def assert_docs(docs)
  145. if docs.empty?
  146. puts 'ERROR: called with no arguments.'
  147. puts 'Run "thor list" for usage patterns.'
  148. exit
  149. end
  150. end
  151. def handle_doc_not_found_error(error)
  152. puts %(ERROR: #{error}.)
  153. puts 'Run "thor docs:list" to see the list of docs and versions.'
  154. end
  155. def generate_doc(doc, package: nil)
  156. if Docs.generate(doc)
  157. package_doc(doc) if package
  158. puts 'Done'
  159. true
  160. else
  161. puts "Failed!#{' (try running with --debug for more information)' unless options[:debug]}"
  162. false
  163. end
  164. end
  165. def download_docs(docs)
  166. # Don't allow downloaded files to be created as StringIO
  167. require 'open-uri'
  168. OpenURI::Buffer.send :remove_const, 'StringMax' if OpenURI::Buffer.const_defined?('StringMax')
  169. OpenURI::Buffer.const_set 'StringMax', 0
  170. require 'thread'
  171. length = docs.length
  172. mutex = Mutex.new
  173. i = 0
  174. (1..4).map do
  175. Thread.new do
  176. while doc = docs.shift
  177. status = begin
  178. download_doc(doc)
  179. 'OK'
  180. rescue => e
  181. "FAILED (#{e.class}: #{e.message})"
  182. end
  183. mutex.synchronize { puts "(#{i += 1}/#{length}) #{doc.name}#{ " #{doc.version}" if doc.version} #{status}" }
  184. end
  185. end
  186. end.map(&:join)
  187. end
  188. def download_doc(doc)
  189. target = File.join(Docs.store_path, "#{doc.path}.tar.gz")
  190. open "http://dl.devdocs.io/#{doc.path}.tar.gz" do |file|
  191. FileUtils.mkpath(Docs.store_path)
  192. FileUtils.mv(file, target)
  193. unpackage_doc(doc)
  194. end
  195. end
  196. def unpackage_doc(doc)
  197. path = File.join(Docs.store_path, doc.path)
  198. FileUtils.mkpath(path)
  199. tar = UnixUtils.gunzip("#{path}.tar.gz")
  200. dir = UnixUtils.untar(tar)
  201. FileUtils.rm_rf(path)
  202. FileUtils.mv(dir, path)
  203. FileUtils.rm(tar)
  204. FileUtils.rm("#{path}.tar.gz")
  205. end
  206. def package_doc(doc)
  207. path = File.join Docs.store_path, doc.path
  208. if File.exist?(path)
  209. tar = UnixUtils.tar(path)
  210. gzip = UnixUtils.gzip(tar)
  211. FileUtils.mv(gzip, "#{path}.tar.gz")
  212. FileUtils.rm(tar)
  213. else
  214. puts %(ERROR: can't find "#{doc.name}" documentation files.)
  215. end
  216. end
  217. def generate_manifest
  218. Docs.generate_manifest
  219. end
  220. end