saber 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. data/.gitignore +1 -0
  2. data/CHANGELOG.md +8 -1
  3. data/Gemfile +6 -1
  4. data/Gemfile.lock +58 -14
  5. data/README.md +40 -125
  6. data/bin/saber +4 -2
  7. data/bin/saber.bb +3 -0
  8. data/bin/saber.stp +3 -0
  9. data/lib/saber.rb +5 -0
  10. data/lib/saber/autofetcher/server.rb +1 -1
  11. data/lib/saber/book.rb +36 -0
  12. data/lib/saber/cli.rb +45 -19
  13. data/lib/saber/core_ext.rb +68 -0
  14. data/lib/saber/fetcher.rb +2 -2
  15. data/lib/saber/rc.rb +6 -0
  16. data/lib/saber/task.rb +3 -0
  17. data/lib/saber/task/base.rb +6 -1
  18. data/lib/saber/task/chd.rb +10 -2
  19. data/lib/saber/task/clean.rb +2 -2
  20. data/lib/saber/task/find_uploads.rb +53 -0
  21. data/lib/saber/task/generate.rb +37 -20
  22. data/lib/saber/task/make.rb +20 -8
  23. data/lib/saber/task/send.rb +1 -1
  24. data/lib/saber/task/upload.rb +29 -19
  25. data/lib/saber/tracker.rb +8 -4
  26. data/lib/saber/tracker/base.rb +36 -15
  27. data/lib/saber/tracker/bb.rb +4 -10
  28. data/lib/saber/tracker/bib.rb +27 -11
  29. data/lib/saber/tracker/chd.rb +4 -4
  30. data/lib/saber/tracker/gazelle.rb +7 -0
  31. data/lib/saber/tracker/ptp.rb +2 -2
  32. data/lib/saber/tracker/stp.rb +53 -0
  33. data/lib/saber/tracker/what.rb +2 -2
  34. data/lib/saber/tracker2.rb +31 -0
  35. data/lib/saber/tracker2/base.rb +126 -0
  36. data/lib/saber/tracker2/bb.rb +211 -0
  37. data/lib/saber/tracker2/bib.rb +162 -0
  38. data/lib/saber/tracker2/gazelle.rb +52 -0
  39. data/lib/saber/tracker2/stp.rb +136 -0
  40. data/lib/saber/tracker2/what.rb +51 -0
  41. data/lib/saber/version.rb +1 -1
  42. data/lib/saber/watir_ext.rb +95 -0
  43. data/saber.gemspec +6 -1
  44. data/spec/saber/autofetcher/server_spec.rb +1 -1
  45. data/spec/saber/task_spec.rb +1 -1
  46. data/systemd/saber-chd@.service +12 -0
  47. data/systemd/saber-client@.service +12 -0
  48. data/systemd/saber-server@.service +12 -0
  49. data/templates/_saberrc +29 -8
  50. data/templates/article.yml +18 -0
  51. data/templates/bib/application.yml +8 -8
  52. data/templates/bib/audiobook.yml +12 -14
  53. data/templates/comic.yml +25 -0
  54. data/templates/ebook.yml +35 -0
  55. data/templates/journal.yml +19 -0
  56. data/templates/magazine.yml +41 -0
  57. data/templates/manual.yml +6 -0
  58. data/templates/newspaper.yml +6 -0
  59. metadata +109 -13
  60. data/templates/bb/comic.yml +0 -7
  61. data/templates/bb/ebook.yml +0 -8
  62. data/templates/bb/magazine.yml +0 -6
  63. data/templates/bib/article.yml +0 -18
  64. data/templates/bib/comic.yml +0 -22
  65. data/templates/bib/ebook.yml +0 -20
  66. data/templates/bib/journal.yml +0 -18
  67. data/templates/bib/magazine.yml +0 -18
  68. data/templates/what/ebook.yml +0 -6
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ SABER_TRACKER=bb saber $*
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ SABER_TRACKER=stp saber $*
@@ -3,6 +3,8 @@ require "pa"
3
3
  require "optimism"
4
4
  require "active_support/core_ext/module/attribute_accessors"
5
5
  require "active_support/concern"
6
+ require "active_support/core_ext/numeric/bytes"
7
+ require "saber/core_ext"
6
8
 
7
9
  module Saber
8
10
  autoload :VERSION, "saber/version"
@@ -12,11 +14,14 @@ module Saber
12
14
  autoload :Fetcher, "saber/fetcher"
13
15
  autoload :AutoFetcher, "saber/autofetcher"
14
16
  autoload :Tracker, "saber/tracker"
17
+ autoload :Tracker2, "saber/tracker2"
18
+ autoload :Book, "saber/book"
15
19
 
16
20
  Error = Class.new Exception
17
21
  FatalError = Class.new Exception
18
22
  Rc = Optimism.require "saber/rc", "~/.saberrc"
19
23
 
24
+
20
25
  class << self
21
26
  attr_accessor :ui
22
27
 
@@ -65,7 +65,7 @@ module Saber
65
65
 
66
66
  # ["filea", "foo/filea", "foo/fileb"]
67
67
  def build_files(*names)
68
- Pa.ls2_r(*names, :base_dir => Rc.p.remote_download, :file => true, :include => true) { |p,abs| not Pa.directory?(abs) }
68
+ Pa.ls2_r(*names, :base_dir => Rc.fetch.remote_dir, :file => true, :include => true) { |p,abs| not Pa.directory?(abs) }
69
69
  end
70
70
  end
71
71
  end
@@ -0,0 +1,36 @@
1
+ require "faraday"
2
+ require "faraday_middleware"
3
+
4
+ module Saber
5
+ class Book
6
+ # delegate to #populate
7
+ def self.populate(*args)
8
+ new.populate(*args)
9
+ end
10
+
11
+ attr_reader :client
12
+
13
+ def initialize
14
+ @client = Faraday.new(url: Rc.api_url) {|c|
15
+ c.response :follow_redirects
16
+ c.response :json, :content_type => /\bjson$/
17
+
18
+ c.adapter Faraday.default_adapter
19
+ }
20
+ end
21
+
22
+ # @return [Hash] data
23
+ def populate(isbn, filename)
24
+ params = {}
25
+ rep = client.get("/books/#{isbn}", params)
26
+ data = rep.body
27
+
28
+ if data["status"] == 0
29
+ data["tracker"]
30
+ else
31
+ Saber.ui.error "Can't populate book -- #{isbn} #{filename}."
32
+ {}
33
+ end
34
+ end
35
+ end
36
+ end
@@ -7,28 +7,29 @@ module Saber
7
7
  class_option "no-color", banner: "Disable colorization in output", type: :boolean
8
8
  class_option "verbose", aliases: "-V", banner: "Enable verbose output mode", type: :boolean
9
9
  class_option "log", banner: "Log file", type: :string
10
- class_option "force", aliases: "-f", banner: "Fore writing even if file exists", type: :boolean
10
+ class_option "force", banner: "Fore writing even if file exists", type: :boolean
11
11
  class_option "tracker", aliases: "-t", banner: "tracker name", type: :string
12
-
13
- attr_reader :o
12
+ class_option "dry-run", aliases: "-n", banner: "dry run", type: :boolean
14
13
 
15
14
  def initialize(*)
16
15
  super
17
- o = @o = options.dup
16
+ self.options = self.options.dup
18
17
 
19
- Saber.ui = if o["log"] then
18
+ Saber.ui = if options["log"] then
20
19
  require "logger"
21
- UI::Logger.new(::Logger.new(o["log"]))
20
+ UI::Logger.new(::Logger.new(options["log"]))
22
21
  else
23
22
  the_shell = (options["no-color"] ? Thor::Shell::Basic.new : shell)
24
23
  UI::Shell.new(the_shell)
25
24
  end
26
25
 
27
- Saber.ui.debug! if o["verbose"]
26
+ Saber.ui.debug! if options["verbose"]
28
27
 
29
28
  # Initialize environment in first time
30
29
  unless Rc.p.home.exists?
31
- Pa.mkdir Rc.p.home
30
+ Pa.mkdir Rc.p.home
31
+ Pa.mkdir "#{Rc.p.home}/templates"
32
+ Pa.mkdir "#{Rc.p.home}/database"
32
33
  end
33
34
  end
34
35
 
@@ -36,7 +37,7 @@ module Saber
36
37
  def clean
37
38
  require "saber/task/clean"
38
39
 
39
- Task["clean"].invoke(:clean, [], o)
40
+ Task["clean"].invoke(:clean, [], options)
40
41
  end
41
42
 
42
43
  desc "server", "start saber-server daemon"
@@ -62,18 +63,25 @@ module Saber
62
63
  AutoFetcher::DRbClient.new.add(*names)
63
64
  end
64
65
 
65
- desc "upload [options] <torrent_file/file ...>", "[make a torrent file and] upoad a torrent file to the site"
66
- def upload(*files)
66
+ desc "upload [options] <format> <torrent_file/file ...>", "[make a torrent file and] upoad a torrent file to the site"
67
+ method_option "add", aliases: "-a", desc: "upload via add format.", type: :boolean
68
+ def upload(format0, *files)
67
69
  require "saber/task/upload"
70
+ format = format0.downcase
71
+ Saber.ui.error! "Don't support this format -- #{format0}" unless Rc.book_exts.include?(".#{format}")
68
72
 
69
- Task["upload"].invoke(:upload, [options["tracker"] || ENV["SABER_TRACKER"], *files], o)
73
+ files = files.map{|v| v.dup} # unfrozen string.
74
+ Task["upload"].invoke(:upload, [options["tracker"] || ENV["SABER_TRACKER"], format, *wrap_file(*files)], options)
70
75
  end
71
76
 
72
- desc "generate [options] <type> <filename> [arg ...]", %~generate a meta data file (alias: "g")~
73
- def generate(type, filename, *args)
77
+ desc "generate [options] <type> [filename:isbn ...]", %~generate a meta data file (alias: "g")~
78
+ method_option "file", aliases: "-f", desc: "read files from file list", type: :string
79
+ def generate(type, *files)
74
80
  require "saber/task/generate"
75
81
 
76
- Task["generate"].invoke(:generate, [options["tracker"] || ENV["SABER_TRACKER"], type, filename, *args], o)
82
+ files = File.read(options["file"]).split(/\n+/).map{|v| v.strip} if options["file"]
83
+ files = files.map{|v| name, isbn = v.split(":"); [*wrap_file(name), isbn]}
84
+ Task["generate"].invoke(:generate, [type, *files], options)
77
85
  end
78
86
  map "g" => "generate"
79
87
 
@@ -86,21 +94,39 @@ module Saber
86
94
  def send1(*names)
87
95
  require "saber/task/send"
88
96
 
89
- Task["send"].invoke(:send1, names, o)
97
+ Task["send"].invoke(:send1, names, options)
90
98
  end
91
99
 
92
100
  desc "make [options] <file ..>", "make a torent file and send it to local and/or remote watch directory"
101
+ method_option "option", aliases: "-o", desc: "extra options passed to mktorrent", type: :string
102
+ method_option "file", aliases: "-f", desc: "read files from file list", type: :string
93
103
  def make(*files)
94
104
  require "saber/task/make"
95
105
 
96
- Task["make"].invoke(:make, [options["tracker"] || ENV["SABER_TRACKER"], *files], o)
106
+ files = File.read(options["file"]).split(/\n+/).map{|v| v.strip.split(":")[0] } if options["file"]
107
+ Task["make"].invoke(:make, [options["tracker"] || ENV["SABER_TRACKER"], *files], options)
97
108
  end
98
109
 
99
- desc "chd", "chd"
110
+ desc "chd", "NOT WORKING"
100
111
  def chd
101
112
  require "saber/task/chd"
102
113
 
103
- Task["chd"].invoke(:chd, o)
114
+ Task["chd"].invoke(:chd, [], options)
115
+ end
116
+
117
+ desc "find_uploads", "NOT WORKING"
118
+ def find_uploads(page)
119
+ require "saber/task/find_uploads"
120
+
121
+ Task["find_uploads"].invoke(:find_uploads, [page], options)
122
+ end
123
+
124
+ private
125
+
126
+ # a.yml -> a
127
+ # a.epub.torrent -> a
128
+ def wrap_file(*names)
129
+ names.map{|v| Pa.delete_ext(v, *%w[.torrent .yml]).delete_ext2(*Rc.book_exts)}
104
130
  end
105
131
  end
106
132
  end
@@ -0,0 +1,68 @@
1
+ class Hash
2
+ # From Rails 4.0
3
+
4
+ # Return a new hash with all keys converted by the block operation.
5
+ # This includes the keys from the root hash and from all
6
+ # nested hashes.
7
+ #
8
+ # hash = { person: { name: 'Rob', age: '28' } }
9
+ #
10
+ # hash.deep_transform_keys{ |key| key.to_s.upcase }
11
+ # # => { "PERSON" => { "NAME" => "Rob", "AGE" => "28" } }
12
+ def deep_transform_keys(&block)
13
+ result = {}
14
+ each do |key, value|
15
+ result[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys(&block) : value
16
+ end
17
+ result
18
+ end
19
+
20
+ # Destructively convert all keys by using the block operation.
21
+ # This includes the keys from the root hash and from all
22
+ # nested hashes.
23
+ def deep_transform_keys!(&block)
24
+ keys.each do |key|
25
+ value = delete(key)
26
+ self[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys!(&block) : value
27
+ end
28
+ self
29
+ end
30
+
31
+ # Return a new hash with all keys converted to strings.
32
+ # This includes the keys from the root hash and from all
33
+ # nested hashes.
34
+ #
35
+ # hash = { person: { name: 'Rob', age: '28' } }
36
+ #
37
+ # hash.deep_stringify_keys
38
+ # # => { "person" => { "name" => "Rob", "age" => "28" } }
39
+ def deep_stringify_keys
40
+ deep_transform_keys{ |key| key.to_s }
41
+ end
42
+
43
+ # Destructively convert all keys to strings.
44
+ # This includes the keys from the root hash and from all
45
+ # nested hashes.
46
+ def deep_stringify_keys!
47
+ deep_transform_keys!{ |key| key.to_s }
48
+ end
49
+
50
+ # Return a new hash with all keys converted to symbols, as long as
51
+ # they respond to +to_sym+. This includes the keys from the root hash
52
+ # and from all nested hashes.
53
+ #
54
+ # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
55
+ #
56
+ # hash.deep_symbolize_keys
57
+ # # => { person: { name: "Rob", age: "28" } }
58
+ def deep_symbolize_keys
59
+ deep_transform_keys{ |key| key.to_sym rescue key }
60
+ end
61
+
62
+ # Destructively convert all keys to symbols, as long as they respond
63
+ # to +to_sym+. This includes the keys from the root hash and from all
64
+ # nested hashes.
65
+ def deep_symbolize_keys!
66
+ deep_transform_keys!{ |key| key.to_sym rescue key }
67
+ end
68
+ end
@@ -17,7 +17,7 @@ module Saber
17
17
  def add(*files)
18
18
  files.each { |file|
19
19
  uri = "#{Rc.server.ftp}/#{file}"
20
- gid = aria2_add([uri], :dir => Pa.dir2("#{Rc.p.fetcher_download}/#{file}"))
20
+ gid = aria2_add([uri], :dir => Pa.dir2("#{Rc.fetch.dir}/#{file}"))
21
21
  Saber.ui.debug "DOWNLOAD #{gid} #{uri}"
22
22
  }
23
23
  end
@@ -28,7 +28,7 @@ module Saber
28
28
  files = []
29
29
  Net::SSH.start(Rc.server.host, Rc.server.user) do |s|
30
30
  name = "'#{names.join("' '")}'"
31
- cmd = "cd #{Rc.p.remote_download} && find #{name} -type f"
31
+ cmd = "cd #{Rc.fetch.remote_dir} && find #{name} -type f"
32
32
 
33
33
  rst = s.exec!(cmd)
34
34
  if rst =~ /^find: `|^cd:cd:/
@@ -1,9 +1,15 @@
1
+ api_url = "http://saberapi.heroku.com"
1
2
  scgi_server = "http://localhost/RPC2"
3
+ browser = [:firefox]
4
+ book_exts = %w[.epub .mobi .pdf .txt .html .djvu .chm .cbr .cbz .azw3]
5
+ book_formats = %w[epub mobi pdf txt html djvu chm cbr cbz azw3]
2
6
 
3
7
  p:
4
8
  root = Pa.expand("../../..", __FILE__)
5
9
  home = Pa("~/.saber")
6
10
  homerc = Pa("~/.saberrc")
11
+ database = Pa("~/.saber/database")
12
+ template = Pa("~/.saber/templates")
7
13
 
8
14
  aria2:
9
15
  rpc = "http://localhost:6800/rpc"
@@ -18,6 +18,7 @@ module Saber
18
18
  autoload :Clean, "saber/task/clean"
19
19
  autoload :Make, "saber/task/make"
20
20
  autoload :Upload, "saber/task/upload"
21
+ autoload :FindUploads, "saber/task/find_uploads"
21
22
 
22
23
  @@tasks = {}
23
24
 
@@ -26,6 +27,8 @@ module Saber
26
27
 
27
28
  class << self
28
29
  def [](name)
30
+ require "saber/task/#{name}"
31
+
29
32
  tasks[name]
30
33
  end
31
34
  end
@@ -8,11 +8,16 @@ module Saber
8
8
  Task.tasks[child.name.demodulize.underscore] = child
9
9
  end
10
10
 
11
- # invoke a task
11
+ # delegate to #invoke
12
12
  def invoke(*args)
13
13
  new.invoke(*args)
14
14
  end
15
15
  end
16
+
17
+ def initialize(*)
18
+ super
19
+ self.options = self.options.dup
20
+ end
16
21
  end
17
22
  end
18
23
  end
@@ -1,10 +1,11 @@
1
+ require "sys/filesystem"
2
+
1
3
  module Saber
2
4
  module Task
3
5
  class CHD < Base
4
6
 
5
7
  desc "chd", "chd"
6
8
  def chd(o={})
7
- require "saber/tracker/chd"
8
9
  t = Tracker["chd"].new
9
10
 
10
11
  begin
@@ -14,7 +15,14 @@ module Saber
14
15
  t.update_cache(true)
15
16
 
16
17
  while true
17
- t.add_torrents
18
+ # check free diskspace
19
+ s = Sys::Filesystem.stat(Rc.chd.diskspace_dir.p)
20
+ if s.block_size * s.blocks_free < Rc.chd.diskspace_limit
21
+ Saber.ui.say "::SKIP:: Reach low diskspace."
22
+ else
23
+ t.add_torrents
24
+ end
25
+
18
26
  sleep Rc.chd.update_interval
19
27
  end
20
28
  rescue Errno::ETIMEDOUT, Mechanize::ResponseCodeError, SocketError
@@ -6,8 +6,8 @@ module Saber
6
6
  class Clean < Base
7
7
  desc "clean", "clean"
8
8
  def clean
9
- disk_files = Pa.ls2(Rc.p.download, absolute: true)
10
- bt_files = Retort::Torrent.all.map{|t| Retort::Torrent.action("name", t.info_hash) }.map{|n| Pa.join2(Rc.p.download, n)}
9
+ disk_files = Pa.ls2(Rc.clean.dir, absolute: true)
10
+ bt_files = Retort::Torrent.all.map{|t| Retort::Torrent.action("name", t.info_hash) }.map{|n| Pa.join2(Rc.clean.dir, n)}
11
11
 
12
12
  (disk_files - bt_files).each { |file|
13
13
  Pa.rm_r file, :verbose => true
@@ -0,0 +1,53 @@
1
+ require "tagen/core/io"
2
+ require "isbn"
3
+
4
+ module Saber
5
+ module Task
6
+ class FindUploads < Base
7
+ desc "find_uploads", "find_uploads"
8
+ def find_uploads(page="1")
9
+ bib = Tracker["bib"].new(options)
10
+ stp = Tracker["stp"].new(options)
11
+ dir = Rc._fetch("find_uploads.dir", ".")
12
+ bib.agent.pluggable_parser["application/x-bittorrent"] = Mechanize::DirectorySaver.save_to(dir.to_s)
13
+ bib.login
14
+ stp.login
15
+
16
+ bib.browse(page) {|torrent|
17
+ title, isbn, download_link, filenames, tags = torrent[:title], torrent[:isbn],
18
+ torrent[:download_link], torrent[:filenames], torrent[:tags]
19
+
20
+ generic_tags = convert_bibtags(tags)
21
+
22
+ begin
23
+ isbn = ISBN.thirteen(torrent[:isbn])
24
+ rescue ISBN::Invalid13DigitISBN # empty
25
+ next
26
+ end
27
+
28
+ if not stp.exists?(isbn: isbn)
29
+ bib.get(download_link)
30
+
31
+ Saber.ui.say "#{isbn} #{title}\n #{filenames.join("\n ")}"
32
+ File.append("list", "#{filenames[0]}:#{isbn}\n")
33
+
34
+ # local data
35
+ local_data = Pa.exists?("#{Rc.p.database}/#{isbn}.yml") ? YAML.load_file("#{Rc.p.database}/#{isbn}.yml") : {}
36
+ local_data.merge!({"tags" => generic_tags.join(", "), "bib.tags" => tags.join(", ")})
37
+ File.write "#{Rc.p.database}/#{isbn}.yml", YAML.dump(local_data)
38
+ end
39
+ }
40
+ end
41
+
42
+ private
43
+
44
+ def convert_bibtags(tags)
45
+ tags.map{|v|
46
+ v.gsub(/ \(programming\)/i, '')
47
+ }.sort_by {|v|
48
+ %w[fiction nonfiction].include?(v) ? -1 : tags.index(v)
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,3 +1,5 @@
1
+ require "isbn"
2
+
1
3
  module Saber
2
4
  module Task
3
5
  # Generate meta data file.
@@ -5,34 +7,49 @@ module Saber
5
7
  # Usage
6
8
  # -----
7
9
  #
8
- # Task["generate"].invoke(:generate, ["bib", "ebook", "Hello.epub", isbn])
10
+ # Task["generate"].invoke(:generate, ["ebook", ["harry_potter", isbn], ...])
9
11
  # > generate Hello.epub.yml data file.
10
12
  #
11
13
  class Generate < Base
12
14
  include Thor::Actions
13
15
 
16
+ def self.source_paths
17
+ ["#{Rc.p.home}/templates"]
18
+ end
19
+
14
20
  source_root "#{Rc.p.root}/templates"
15
21
 
16
22
  desc "generate", "generate"
17
- def generate(tracker_name, type, filename, *args)
18
- require "saber/tracker/#{tracker_name}" if !args.empty?
19
- template_file = find_in_source_paths("#{tracker_name}/#{type}.yml")
20
- dest = "#{filename}.yml"
21
-
22
- if !args.empty? and Tracker[tracker_name].can_populate?(type)
23
- require "yaml"
24
- populate = {}
25
- data = YAML.load_file(template_file)
26
-
27
- tracker = Tracker[tracker_name].new
28
- tracker.login
29
- populate = tracker.populate(type, *args)
30
- data.merge!(populate)
31
-
32
- create_file dest, YAML.dump(data)
33
- else
34
- copy_file template_file, dest
35
- end
23
+ def generate(type, *filenames)
24
+ template_file = find_in_source_paths("#{type}.yml")
25
+
26
+ filenames.each {|filename, isbn|
27
+ isbn = ISBN.thirteen(isbn) rescue nil
28
+ Saber.ui.say "Populating {#{isbn}} #{filename} ..."
29
+ dest = "#{filename}.yml"
30
+
31
+ if isbn
32
+ require "yaml"
33
+ populate = {}
34
+ data = YAML.load_file(template_file)
35
+ data.merge! Book.populate(isbn, filename)
36
+ data.merge! YAML.load_file("#{Rc.p.database}/#{isbn}.yml") if Pa.exists?("#{Rc.p.database}/#{isbn}.yml")
37
+
38
+ # tags
39
+ if data["tags"]
40
+ Tracker.trackers.each {|name, tracker_class|
41
+ if data["#{name}.tags"].nil? and tracker_class.method_defined?(:convert_tags)
42
+ tracker = tracker_class.new(options)
43
+ data["#{name}.tags"] = tracker.convert_tags(*data["tags"].split(/, */)).join(", ")
44
+ end
45
+ }
46
+ end
47
+
48
+ create_file dest, YAML.dump(data)
49
+ else
50
+ copy_file template_file, dest
51
+ end
52
+ }
36
53
  end
37
54
  end
38
55
  end