app/server.rb in sqlui-0.1.71 vs app/server.rb in sqlui-0.1.72

- old
+ new

@@ -5,33 +5,42 @@ require 'digest/md5' require 'erb' require 'json' require 'prometheus/middleware/collector' require 'prometheus/middleware/exporter' +require 'securerandom' require 'sinatra/base' require 'uri' require 'webrick' require_relative 'database_metadata' require_relative 'github/cache' require_relative 'github/caching_client' require_relative 'github/client' +require_relative 'github/paths' require_relative 'github/tree' require_relative 'github/tree_client' require_relative 'mysql_types' +require_relative 'pigs' require_relative 'sql_parser' require_relative 'sqlui' require_relative 'version' # SQLUI Sinatra server. class Server < Sinatra::Base - def self.logger - @logger ||= WEBrick::Log.new + # An error message and status code. + class ClientError < StandardError + attr_reader :status + + def initialize(message, status: 400) + super(message) + @status = status + end end - def self.init_and_run(config, resources_dir, github_cache = Github::Cache.new({}, logger: Server.logger)) - logger.info("Starting SQLUI v#{Version::SQLUI}") - logger.info("Airbrake enabled: #{config.airbrake[:server]&.[](:enabled) || false}") + def self.init_and_run(config, resources_dir, github_cache) + Sqlui.logger.info("Starting SQLUI v#{Version::SQLUI}") + Sqlui.logger.info("Airbrake enabled: #{config.airbrake[:server]&.[](:enabled) || false}") WEBrick::HTTPRequest.const_set('MAX_URI_LENGTH', 2 * 1024 * 1024) if config.airbrake[:server]&.[](:enabled) require 'airbrake' @@ -108,37 +117,58 @@ get "#{config.base_url_path}/#{database.url_path}/?" do redirect "#{database.url_path}/query", 301 end post "#{config.base_url_path}/#{database.url_path}/metadata" do - tree = Github::Tree.new(files: [], truncated: false) + tree = nil if (saved_config = database.saved_config) - tree_client = Github::TreeClient.new(access_token: database.saved_config.token, cache: github_cache, - logger: Server.logger) - tree = tree_client.get_tree(owner: saved_config.owner, repo: saved_config.repo, branch: saved_config.branch, - regex: saved_config.regex) + tree_client = Github::TreeClient.new( + access_token: database.saved_config.token, + cache: github_cache, + logger: Sqlui.logger + ) + tree = tree_client.get_tree( + owner: saved_config.owner, + repo: saved_config.repo, + ref: saved_config.branch, + regex: saved_config.regex + ) + if params[:file] + owner, repo, ref, path = Github::Paths.parse_file_path(params[:file]) + raise ClientError, "invalid owner: #{owner}" unless tree.owner == owner + raise ClientError, "invalid repo: #{repo}" unless tree.repo == repo + + requested_tree = tree_client.get_tree( + owner: owner, + repo: repo, + ref: ref, + regex: /#{Regexp.escape(path)}/, + cache: ref == tree.ref + ) + raise ClientError.new("file not found: #{params[:file]}", status: 404) unless requested_tree[path] + + tree << requested_tree[path] + end end + metadata = database.with_client do |client| { server: "#{config.name} - #{database.display_name}", base_url_path: config.base_url_path, schemas: DatabaseMetadata.lookup(client, database), tables: database.tables, columns: database.columns, joins: database.joins, - saved: tree.to_h do |file| - comment_lines = file.content.split("\n").take_while do |l| - l.start_with?('--') - end - description = comment_lines.map { |l| l.sub(/^-- */, '') }.join("\n") + saved: (tree || {}).to_h do |file| [ - file.display_path, + file.full_path, { - filename: file.display_path, + filename: file.full_path, + path: file.path, github_url: file.github_url, - description: description, - contents: file.content + contents: file.content, + tree_sha: file.tree_sha } ] end } end @@ -149,11 +179,11 @@ post "#{config.base_url_path}/#{database.url_path}/query" do data = request.body.read request.body.rewind # since Airbrake will read the body on error params.merge!(JSON.parse(data, symbolize_names: true)) - break client_error('missing sql') unless params[:sql] + raise ClientError, 'ERROR: missing sql' unless params[:sql] variables = params[:variables] || {} queries = find_selected_queries(params[:sql], params[:selection]) status 200 @@ -230,11 +260,11 @@ end end end get "#{config.base_url_path}/#{database.url_path}/download_csv" do - break client_error('missing sql') unless params[:sql] + raise ClientError, 'missing sql' unless params[:sql] sql = Base64.decode64(params[:sql]).force_encoding('UTF-8') variables = params.map { |k, v| k[0] == '_' ? [k, v] : nil }.compact.to_h queries = find_selected_queries(sql, params[:selection]) @@ -276,40 +306,63 @@ airbrake_project_id: client_config[:project_id] || '', airbrake_project_key: client_config[:project_key] || '', resource_path_map: resource_path_map } end + + post("#{config.base_url_path}/#{database.url_path}/save-file") do + raise ClientError, 'saved files disabled' unless (saved_config = database.saved_config) + raise ClientError, 'missing base_sha' if (params[:base_sha] || '').strip.empty? + raise ClientError, 'missing path' if (params[:path] || '').strip.empty? + raise ClientError, 'missing content' if params[:path].nil? + + branch = "sqlui-#{Pigs.generate_phrase}" + tree_client = Github::TreeClient.new( + access_token: database.saved_config.token, + cache: github_cache, + logger: Sqlui.logger + ) + tree_client.create_commit_with_file( + owner: saved_config.owner, + repo: saved_config.repo, + base_sha: params[:base_sha], + branch: branch, + path: params[:path], + content: params[:content].gsub("\r\n", "\n"), + author_name: database.saved_config.author_name, + author_email: database.saved_config.author_email + ) + status 201 + erb :redirect, locals: { + resource_path_map: resource_path_map, + location: "https://github.com/#{saved_config.owner}/#{saved_config.repo}/compare/#{branch}" + } + end end - error 400..510 do - exception = env['sinatra.error'] - stacktrace = exception&.full_message(highlight: false) + error do |exception| + status exception.is_a?(ClientError) ? exception.status : 500 + stacktrace = exception.full_message(highlight: false) if request.env['HTTP_ACCEPT'] == 'application/json' headers 'Content-Type' => 'application/json; charset=utf-8' - message = "error: #{exception&.message&.lines&.first&.strip || 'unexpected error'}" + message = exception.message.to_s json = { error: message, stacktrace: stacktrace }.compact.to_json body json else - message = "#{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}" erb :error, locals: { resource_path_map: resource_path_map, title: "SQLUI #{message}", - message: message, + heading: "#{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}", + message: exception.message, stacktrace: stacktrace } end end run! end private - - def client_error(message, stacktrace: nil) - status(400) - headers 'Content-Type' => 'application/json; charset=utf-8' - body({ error: message, stacktrace: stacktrace }.compact.to_json) - end def find_selected_queries(full_sql, selection) if selection if selection.include?('-') # sort because the selection could be in either direction