docs.thor 8.0 KB

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