beam_up 0.5.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.
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "beam_up/providers/s3_compatible"
4
+
5
+ module BeamUp
6
+ module Providers
7
+ class Hetzner < S3Compatible
8
+ class Config
9
+ def self.config_keys = %w[access_key secret_key region bucket]
10
+
11
+ attr_accessor :access_key, :secret_key, :region, :bucket
12
+
13
+ def with(options)
14
+ self.access_key = options[:access_key]
15
+ self.secret_key = options[:secret_key]
16
+ self.region = options[:region] || "fsn1"
17
+ self.bucket = options[:bucket]
18
+ self
19
+ end
20
+
21
+ def validate!
22
+ raise ConfigurationError, "Access key must be set" unless access_key
23
+ raise ConfigurationError, "Secret key must be set" unless secret_key
24
+ raise ConfigurationError, "Bucket must be set" unless bucket
25
+ raise ConfigurationError, "Invalid region: #{region}. Valid regions: fsn1, nbg1, hel1, ash, hil, sin" unless VALID_REGIONS.include?(region)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ VALID_REGIONS = %w[ash fsn1 hel1 hil nbg1 sin]
32
+
33
+ def bucket_name = @configuration.bucket
34
+
35
+ def endpoint = "https://#{@configuration.region}.your-objectstorage.com"
36
+
37
+ def public_url = "https://#{bucket_name}.#{@configuration.region}.your-objectstorage.com"
38
+
39
+ def provider_name = "Hetzner"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+
6
+ module BeamUp
7
+ module Providers
8
+ class Neocities < Base
9
+ API_HOST = "https://neocities.org"
10
+
11
+ class Config
12
+ def self.config_keys = %w[api_key site_name]
13
+
14
+ attr_accessor :api_key, :site_name
15
+
16
+ def with(options)
17
+ self.api_key = options[:api_key]
18
+ self.site_name = options[:site_name]
19
+ self
20
+ end
21
+
22
+ def validate!
23
+ raise ConfigurationError, "API key must be set" unless api_key
24
+ raise ConfigurationError, "Site name must be set" unless site_name
25
+ end
26
+ end
27
+
28
+ def deploy!(path)
29
+ @path = path
30
+
31
+ upload_files
32
+
33
+ Result.new(
34
+ provider: "Neocities",
35
+ deploy_id: Time.now.to_i.to_s,
36
+ url: "https://#{@configuration.site_name}.neocities.org"
37
+ )
38
+ rescue => error
39
+ Result.new(provider: "Neocities", error: error.message)
40
+ end
41
+
42
+ private
43
+
44
+ def upload_files
45
+ uri = URI("#{API_HOST}/api/upload")
46
+
47
+ request = Net::HTTP::Post.new(uri)
48
+ request.basic_auth(@configuration.site_name, @configuration.api_key)
49
+
50
+ form_data = files_to_deploy.map do |file|
51
+ relative_path = file.delete_prefix("#{@path}/")
52
+
53
+ [relative_path, File.read(file)]
54
+ end
55
+
56
+ request.set_form(form_data, "multipart/form-data")
57
+
58
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
59
+ http.request(request)
60
+ end
61
+
62
+ case response.code.to_i
63
+ when 200..299
64
+ json_response = JSON.parse(response.body)
65
+ raise DeploymentError, json_response["message"] if json_response["result"] == "error"
66
+ else
67
+ raise DeploymentError, "Neocities API error: #{response.code} #{response.body}"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "cgi/escape"
5
+ require "net/http"
6
+ require "json"
7
+
8
+ module BeamUp
9
+ module Providers
10
+ class Netlify < Base
11
+ class Config
12
+ def self.config_keys = %w[api_token project_id]
13
+
14
+ attr_accessor :api_token, :project_id
15
+
16
+ def with(options)
17
+ self.api_token = options[:api_token]
18
+ self.project_id = options[:project_id]
19
+ self
20
+ end
21
+
22
+ def validate!
23
+ raise ConfigurationError, "API token must be set" unless api_token
24
+ end
25
+ end
26
+
27
+ def deploy!(path)
28
+ @path = path
29
+ digested_files = digested files_to_deploy
30
+ response = post "/sites/#{project_id}/deploys", files: digested_files
31
+
32
+ upload(files_to_deploy, response)
33
+
34
+ Result.new(
35
+ provider: "Netlify",
36
+ deploy_id: response["id"],
37
+ url: response["deploy_ssl_url"] || response["ssl_url"]
38
+ )
39
+ rescue => error
40
+ Result.new(provider: "Netlify", error: error.message)
41
+ end
42
+
43
+ private
44
+
45
+ def digested(files)
46
+ files.to_h do |file|
47
+ relative_path = "/#{file.delete_prefix("#{@path}/")}"
48
+
49
+ [relative_path, Digest::SHA1.hexdigest(File.read(file))]
50
+ end
51
+ end
52
+
53
+ def project_id
54
+ return @configuration.project_id if @configuration.project_id.to_s != ""
55
+
56
+ site_name = "#{File.basename(Dir.pwd)}-#{Time.now.to_i}"
57
+ site = post("/sites", {name: [site_name, SecureRandom.hex(4)].join("-")})
58
+ @created_project_id = site["id"]
59
+ end
60
+
61
+ def upload(files, response)
62
+ required_shas = response["required"] || []
63
+ return if required_shas.empty?
64
+
65
+ required_shas.each.with_index(1) do |sha, index|
66
+ file_path = file_map_from(files)[sha]
67
+ next if file_path.nil? || file_path.empty?
68
+
69
+ relative_path = "/" + file_path.delete_prefix("#{@path}/")
70
+ escaped_path = CGI.escape(relative_path.delete_prefix("/"))
71
+
72
+ put("/deploys/#{response["id"]}/files/#{escaped_path}", File.read(file_path))
73
+ end
74
+ end
75
+
76
+ def message
77
+ if @configuration.project_id.to_s.empty?
78
+ <<~MSG
79
+ Successfully deployed to Netlify.
80
+
81
+ New site created with ID: #{@created_project_id}
82
+ Add this to your .beam_up.yml to skip site creation in future deploys:
83
+ project_id: #{@created_project_id}
84
+ MSG
85
+ else
86
+ "Successfully deployed to Netlify"
87
+ end
88
+ end
89
+
90
+ def file_map_from(files)
91
+ files.to_h { [Digest::SHA1.hexdigest(File.read(it)), it] }
92
+ end
93
+
94
+ def post(path, data)
95
+ request(:post, path, data.to_json, "application/json")
96
+ end
97
+
98
+ def put(path, data)
99
+ request(:put, path, data, "application/octet-stream")
100
+ end
101
+
102
+ def request(method, path, body = nil, content_type = nil)
103
+ uri = URI("https://api.netlify.com/api/v1#{path}")
104
+
105
+ request = case method
106
+ when :post then Net::HTTP::Post.new(uri)
107
+ when :put then Net::HTTP::Put.new(uri)
108
+ end
109
+
110
+ request["Authorization"] = "Bearer #{@configuration.api_token}"
111
+ request["Content-Type"] = content_type if content_type
112
+ request.body = body if body
113
+
114
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
115
+
116
+ case response.code.to_i
117
+ when 200..299
118
+ response.body.empty? ? {} : JSON.parse(response.body)
119
+ else
120
+ raise DeploymentError, "Netlify API error: #{response.code} #{response.body}"
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aws-sdk-s3"
4
+
5
+ module BeamUp
6
+ module Providers
7
+ class S3Compatible < Base
8
+ def deploy!(path)
9
+ @path = path
10
+
11
+ files_to_deploy.each do |file|
12
+ upload file.delete_prefix("#{@path}/"), file
13
+ end
14
+
15
+ Result.new(
16
+ provider: provider_name,
17
+ deploy_id: Time.now.to_i.to_s,
18
+ url: public_url
19
+ )
20
+ rescue => error
21
+ Result.new(provider: provider_name, error: error.message)
22
+ end
23
+
24
+ private
25
+
26
+ def upload(key, file)
27
+ File.open(file, "rb") do |opened_file|
28
+ s3_client.put_object(
29
+ bucket: bucket_name,
30
+ key: key,
31
+ body: opened_file,
32
+ acl: "public-read"
33
+ )
34
+ end
35
+ end
36
+
37
+ def s3_client
38
+ @s3_client ||= Aws::S3::Client.new(
39
+ access_key_id: @configuration.access_key,
40
+ secret_access_key: @configuration.secret_key,
41
+ region: @configuration.region,
42
+ endpoint: endpoint
43
+ )
44
+ end
45
+
46
+ def bucket_name
47
+ raise NotImplementedError, "Subclasses must implement #bucket_name"
48
+ end
49
+
50
+ def endpoint
51
+ raise NotImplementedError, "Subclasses must implement #endpoint"
52
+ end
53
+
54
+ def public_url
55
+ raise NotImplementedError, "Subclasses must implement #public_url"
56
+ end
57
+
58
+ def provider_name
59
+ raise NotImplementedError, "Subclasses must implement #provider_name"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BeamUp
4
+ module Providers
5
+ class SFTP < Base
6
+ class Config
7
+ def self.config_keys = %w[host port username password key remote_path]
8
+
9
+ attr_accessor :host, :port, :username, :password, :key, :remote_path
10
+
11
+ def with(options)
12
+ self.host = options[:host]
13
+ self.port = options[:port] || 22
14
+ self.username = options[:username]
15
+ self.password = options[:password]
16
+ self.key = options[:key]
17
+ self.remote_path = options[:remote_path]
18
+ self
19
+ end
20
+
21
+ def validate!
22
+ raise ConfigurationError, "Host must be set" unless host
23
+ raise ConfigurationError, "Username must be set" unless username
24
+ raise ConfigurationError, "Remote path must be set" unless remote_path
25
+ raise ConfigurationError, "Password or key must be set" unless password || key
26
+ end
27
+ end
28
+
29
+ def deploy!(path)
30
+ @path = path
31
+
32
+ require "net/ssh"
33
+ require "net/sftp"
34
+
35
+ options = {
36
+ password: @configuration.password,
37
+ encryption: ["aes256-gcm@openssh.com", "aes256-ctr", "aes192-ctr", "aes128-ctr", "twofish256-ctr", "twofish192-ctr", "twofish128-ctr"]
38
+ }
39
+ options[:keys] = [@configuration.key] if @configuration.key
40
+
41
+ Net::SFTP.start(@configuration.host, @configuration.username, options) do |sftp|
42
+ files_to_deploy.each do |file|
43
+ upload sftp, file
44
+ end
45
+ end
46
+
47
+ Result.new(
48
+ provider: "SFTP",
49
+ deploy_id: Time.now.to_i.to_s,
50
+ url: "sftp://#{@configuration.host}#{@configuration.remote_path}"
51
+ )
52
+ rescue LoadError
53
+ raise ConfigurationError, "SFTP requires net-sftp gem. Install with: gem install net-sftp (or add to Gemfile)"
54
+ rescue => error
55
+ Result.new(provider: "SFTP", error: error.message)
56
+ end
57
+
58
+ private
59
+
60
+ def upload(sftp, file)
61
+ remote_path = File.join(@configuration.remote_path, file.delete_prefix("#{@path}/"))
62
+
63
+ return if unchanged?(sftp, file, remote_path)
64
+
65
+ sftp.upload(file, remote_path)
66
+ end
67
+
68
+ def unchanged?(sftp, file, remote_path)
69
+ size = File.size(file)
70
+
71
+ begin
72
+ remote_stat = sftp.stat!(remote_path)
73
+
74
+ remote_stat.size == size
75
+ rescue Net::SFTP::StatusException
76
+ false
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "zip"
5
+ require "tempfile"
6
+
7
+ module BeamUp
8
+ module Providers
9
+ class Statichost < Base
10
+ BUILDER_HOST = "https://builder.statichost.eu"
11
+
12
+ class Config
13
+ def self.config_keys = %w[api_key site_name builder_host]
14
+
15
+ attr_accessor :api_key, :site_name, :builder_host
16
+
17
+ def with(options)
18
+ self.api_key = options[:api_key]
19
+ self.site_name = options[:site_name]
20
+ self
21
+ end
22
+
23
+ def validate!
24
+ raise ConfigurationError, "API key must be set" unless api_key
25
+ raise ConfigurationError, "Site name must be set" unless site_name
26
+ end
27
+ end
28
+
29
+ def deploy!(path)
30
+ @path = path
31
+
32
+ zipped_file = create_zip path
33
+ response = upload zipped_file
34
+
35
+ Result.new(
36
+ provider: "Statichost",
37
+ deploy_id: response["id"] || Time.now.to_i.to_s,
38
+ url: "https://#{@configuration.site_name}.statichost.eu"
39
+ )
40
+ rescue => error
41
+ Result.new(provider: "Statichost", error: error.message)
42
+ ensure
43
+ zipped_file&.close!
44
+ end
45
+
46
+ private
47
+
48
+ def create_zip(path)
49
+ temp = Tempfile.new(["statichost", ".zip"], binmode: true)
50
+
51
+ Zip::OutputStream.open(temp) do |zip|
52
+ Dir.glob("#{path}/**/*").each do |file|
53
+ next unless File.file?(file)
54
+
55
+ relative_path = file.delete_prefix("#{path}/")
56
+ zip.put_next_entry(relative_path)
57
+ zip.write(File.read(file))
58
+ end
59
+ end
60
+
61
+ temp.rewind
62
+ temp
63
+ end
64
+
65
+ def upload(zipped_file)
66
+ uri = URI("https://builder.statichost.eu/#{@configuration.site_name}/drop")
67
+
68
+ request = Net::HTTP::Post.new(uri)
69
+ request["Authorization"] = "Bearer #{@configuration.api_key}"
70
+ request["Content-Type"] = "application/zip"
71
+ request["Accept"] = "application/json"
72
+ request.body = zipped_file.read
73
+
74
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
75
+ http.request(request)
76
+ end
77
+
78
+ case response.code.to_i
79
+ when 200..299
80
+ response.body.empty? ? {} : JSON.parse(response.body)
81
+ else
82
+ raise DeploymentError, "Statichost API error: #{response.code} #{response.body}"
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module BeamUp
6
+ module Providers
7
+ class Transporter < Base
8
+ class Config
9
+ def self.config_keys = %w[target_directory]
10
+
11
+ attr_accessor :target_directory
12
+
13
+ def with(options)
14
+ self.target_directory = options[:target_directory]
15
+ self
16
+ end
17
+
18
+ def validate!
19
+ raise ConfigurationError, "Target directory must be set" unless target_directory
20
+ end
21
+ end
22
+
23
+ def deploy!(path)
24
+ @path = path
25
+ files = files_to_deploy
26
+
27
+ puts "Energizing… 🚀"
28
+ puts "Matter stream detected: #{files.length} files"
29
+
30
+ FileUtils.mkdir_p(@configuration.target_directory)
31
+
32
+ files.each do |file|
33
+ relative_path = file.sub("#{@path}/", "")
34
+ target_path = File.join(@configuration.target_directory, relative_path)
35
+
36
+ FileUtils.mkdir_p(File.dirname(target_path))
37
+ FileUtils.cp(file, target_path)
38
+
39
+ puts " Beaming: #{relative_path}"
40
+ end
41
+
42
+ puts "Transport complete. Files materialized at: #{@configuration.target_directory}"
43
+
44
+ Result.new(
45
+ provider: "Transporter",
46
+ deploy_id: files.length.to_s,
47
+ url: @configuration.target_directory
48
+ )
49
+ rescue => error
50
+ Result.new(provider: "Transporter", error: error.message)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "beam_up/providers/base"
4
+ require "beam_up/providers/aws_s3"
5
+ require "beam_up/providers/bunny"
6
+ require "beam_up/providers/digital_ocean_spaces"
7
+ require "beam_up/providers/hetzner"
8
+ require "beam_up/providers/neocities"
9
+ require "beam_up/providers/netlify"
10
+ require "beam_up/providers/sftp"
11
+ require "beam_up/providers/statichost"
12
+ require "beam_up/providers/transporter"
13
+
14
+ module BeamUp
15
+ module Providers
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BeamUp
4
+ class Result
5
+ attr_reader :deploy_id, :error, :provider
6
+
7
+ def initialize(provider:, deploy_id: nil, url: nil, error: nil)
8
+ @provider = provider
9
+ @deploy_id = deploy_id
10
+ @url = url
11
+ @error = error
12
+ end
13
+
14
+ def success? = @error.nil?
15
+
16
+ def failure? = !success?
17
+
18
+ def message
19
+ if success?
20
+ "Successfully deployed to #{@provider}#{" at #{@url}" if @url}"
21
+ else
22
+ "Deployment to #{@provider} failed: #{@error}"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module BeamUp
2
+ VERSION = "0.5.0"
3
+ end
data/lib/beam_up.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "beam_up/version"
4
+ require "beam_up/errors"
5
+ require "beam_up/providers"
6
+ require "beam_up/configuration"
7
+ require "beam_up/result"
8
+ require "beam_up/core"
9
+ require "beam_up/cli"
10
+
11
+ module BeamUp
12
+ PROVIDERS = {
13
+ "aws_s3" => Providers::AwsS3,
14
+ "bunny" => Providers::Bunny,
15
+ "digital_ocean_spaces" => Providers::DigitalOceanSpaces,
16
+ "hetzner" => Providers::Hetzner,
17
+ "neocities" => Providers::Neocities,
18
+ "netlify" => Providers::Netlify,
19
+ "sftp" => Providers::SFTP,
20
+ "statichost" => Providers::Statichost,
21
+ "transporter" => Providers::Transporter
22
+ }
23
+
24
+ class << self
25
+ def configure(&block) = Core.configure(&block)
26
+
27
+ def configuration = Core.configuration
28
+
29
+ def deploy!(path = nil, provider: nil, to: nil) = Core.deploy!(path, provider: (to || provider)&.to_s)
30
+ end
31
+ end