summaryrefslogtreecommitdiff
path: root/lib/pgxn_utils/cli.rb
blob: 58061d4524dcf003d0b38b9d016c87c3b7dcd5e3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
module PgxnUtils
  class CLI < Thor
    attr_accessor :extension_name, :target, :maintainer #, :maintainer_mail
    attr_accessor :abstract, :description, :version, :tags
    attr_accessor :license, :release_status, :generated_by
    attr_accessor :pgxn_username, :pgxn_password

    include Thor::Actions
    include PgxnUtils::Constants

    desc "skeleton extension_name", "Creates an extension skeleton in current directory."

    method_option :target,            :aliases => "-p", :default => ".",  :desc => "Define the target directory"

    # META required fields
    method_option :maintainer,        :aliases => "-m", :type => :string, :desc => "Maintainer's name <maintainer@email>"
    method_option :abstract,          :aliases => "-a", :type => :string, :desc => "Defines a short description to abstract"
    method_option :license,           :aliases => "-l", :type => :string, :desc => "The extension license."
    method_option :version,           :aliases => "-v", :type => :string, :desc => "Initial version"

    # META optional fields
    method_option :description,       :aliases => "-d", :type => :string, :desc => "A long text that contains more information about extension"
    method_option :generated_by,      :aliases => "-b", :type => :string, :desc => "Name of extension's generator"
    method_option :tags,              :aliases => "-t", :type => :array,  :desc => "Defines extension's tags"
    method_option :release_status,    :aliases => "-r", :type => :string, :desc => "Initial extension's release status"

    def skeleton(extension_name,target=nil)
      self.target = options[:target] || target || "."

      if is_extension?("#{self.target}/#{extension_name}")
        say "'#{extension_name}' already exists. Please, use 'change' instead 'skeleton'.", :red
      elsif is_extension?(".")
        say "You are inside a extension directory, already. Consider use 'change' instead.", :red
      elsif is_dir?("#{self.target}/#{extension_name}")
        say "Can't create an extension overwriting an existing directory.", :red
      else
        self.set_accessors extension_name

        directory "root", extension_name
      end
    end

    desc "change [extension_name]", "Change META's attributes in current extension."

    method_option :target,            :aliases => "-p", :type => :string, :default => ".", :desc => "Define the target directory"

    # META required fields
    method_option :maintainer,        :aliases => "-m", :type => :string, :desc => "Maintainer's name <maintainer@email>"
    method_option :abstract,          :aliases => "-a", :type => :string, :desc => "Defines a short description to abstract"
    method_option :license,           :aliases => "-l", :type => :string, :desc => "The extension license."
    method_option :version,           :aliases => "-v", :type => :string, :desc => "Initial version"

    # META optional fields
    method_option :description,       :aliases => "-d", :type => :string, :desc => "A long text that contains more information about extension"
    method_option :generated_by,      :aliases => "-b", :type => :string, :desc => "Name of extension's generator"
    method_option :tags,              :aliases => "-t", :type => :array,  :desc => "Defines extension's tags"
    method_option :release_status,    :aliases => "-r", :type => :string, :desc => "Initial extension's release status"

    def change(extension_name=".")
      extension_path, extension_name = resolve_extension_path_and_name(extension_name)

      self.target = extension_path
      self.extension_name = extension_name

      set_accessors(extension_name)

      if is_extension?(extension_path)
        template "root/META.json.tt", "#{extension_path}/META.json"
        template "root/%extension_name%.control.tt", "#{extension_path}/%extension_name%.control"
      else
        say "'#{extension_name}' doesn't appears to be an extension. Please, supply the extension's name", :red
      end
    end

    desc "bundle [extension_name]", "Bundles an extension."

    def bundle(extension_name=".")
      unless is_extension?(extension_name)
        say "'#{extension_name}' doesn't appears to be an extension. Please, supply the extension's name", :red
      else
        path = File.expand_path(extension_name)
        extension_name = File.basename(path)

        self.target = path
        archive_name = "#{path}-#{config_options['version']}"
        ext = "zip"
        archive = "#{archive_name}.#{ext}"

        if can_zip?(archive)
          make_dist_clean(path)

          Zippy.create(archive) do |zip|
            Dir["#{path}/**/**"].each do |file|
              zip["#{extension_name}-#{config_options['version']}/#{file.sub(path+'/','')}"] = File.open(file) unless File.directory?(file)
            end
          end
          say_status :create, archive, :green
        end
      end
    end

    desc "release filename", "Release a extension"

    def release(filename)
      send_file_to_pgxn(filename)
    end

    no_tasks do
      def make_dist_clean(path)
        inside path do
          run 'make distclean', :capture => true
        end
      end

      def ask_for_pgxn_credential
        self.pgxn_username = ENV["PGXN_USER"] || HighLine.ask("Enter your PGXN username: ") { |q| q.validate = /^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$/ }
        self.pgxn_password = ENV["PGXN_PASS"] || HighLine.ask("Enter your PGXN password: ") { |q| q.echo =  '*' }
      end

      def check_response(response)
        case response
        when Net::HTTPUnauthorized then
          say "oops!", :red
          say "It seems that you entered a wrong username or password.", :red
        when Net::HTTPConflict then
          say "conflict!", :yellow
          say "Distribution already exists! Please, check your META.json.", :yellow
        when Net::HTTPSeeOther then
          say "released successfully!", :green
          say "Visit: #{URI.parse(response['Location'])}", :green
        else
          say "Unknown error. (#{response})"
        end
      end

      def prepare_multipart_post_for(filename)
        file_basename = File.basename(filename)
        zip_file = File.open(filename)
        Net::HTTP::Post::Multipart.new(
          UPLOAD_URL.path,
          "archive" => UploadIO.new(zip_file, "application/zip", file_basename),
          "Expect" => ""
        )
      end

      def try_send_file(request, filename)
        begin
          Net::HTTP.start(UPLOAD_URL.host, UPLOAD_URL.port) do |http|
            say "Trying to release #{File.basename(filename)} ... "
            http.request(request)
          end
        rescue SocketError
          say "Please, check your connection.", :red
          exit(1)
        end
      end

      def send_file_to_pgxn(filename)
        request = prepare_multipart_post_for(filename)
        ask_for_pgxn_credential

        request.basic_auth pgxn_username, pgxn_password
        response = try_send_file(request, filename)
        check_response(response)
      end

      def resolve_extension_path_and_name(extension_name)
        target = options[:target]
        extension_path = "."

        if target != "." && extension_name == "."
          raise ArgumentError, "Please, supply a extension name"
        elsif target == "."
          extension_path = File.expand_path(extension_name)
          extension_name = File.basename(extension_path)
        else
          extension_path = "#{target}/#{extension_name}"
        end
        [ extension_path, extension_name ]
      end

      def can_zip?(archive)
        can_zip = false

        if File.exists?(archive)
          say_status :conflict, archive, :red
          if yes? "Overwrite #{archive}? [yN]"
            can_zip = true
          else
            can_zip = false
          end
        else
          can_zip = true
        end
      end

      def is_extension?(dir=".")
        File.directory?(dir) && File.exists?("#{dir}/META.json")
      end

      def is_dir?(dir)
        File.directory?(dir)
      end

      def config_options
        file = File.join(target, "META.json")

        if File.exist?(file)
          @@config_options ||= JSON.load(File.read(file))
        else
          {}
        end
      end

      def set_accessors(extension_name="your_extension_name")
        self.extension_name = extension_name

        self.maintainer      = options[:maintainer]      || config_options["maintainer"]      || "The maintainer's name"
        self.abstract        = options[:abstract]        || config_options["abstract"]        || "A short description"
        self.license         = options[:license]         || config_options["license"]         || "postgresql"
        self.version         = options[:version]         || config_options["version"]         || "0.0.1"

        self.description     = options[:description]     || config_options["description"]     || "A long description"
        self.generated_by    = options[:generated_by]    || config_options["generated_by"]    || maintainer
        self.tags            = options[:tags]            || config_options["tags"]
        self.release_status  = options[:release_status]  || config_options["release_status"]  || "unstable"

        self.destination_root = target
      end
    end

    def self.source_root
      @_source_root ||= File.expand_path('../templates', __FILE__)
    end
  end
end