staticdctl 0.0.1
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.
- checksums.yaml +7 -0
- data/bin/staticdctl +6 -0
- data/lib/staticd_utils/archive.rb +80 -0
- data/lib/staticd_utils/file_size.rb +26 -0
- data/lib/staticd_utils/gli_object.rb +14 -0
- data/lib/staticd_utils/memory_file.rb +36 -0
- data/lib/staticd_utils/sitemap.rb +100 -0
- data/lib/staticd_utils/tar.rb +43 -0
- data/lib/staticdctl.rb +3 -0
- data/lib/staticdctl/cli.rb +330 -0
- data/lib/staticdctl/rest_client.rb +87 -0
- data/lib/staticdctl/staticd_client.rb +119 -0
- data/lib/staticdctl/version.rb +3 -0
- metadata +113 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a564980b186b0e719ec4b76086f6d79866c8493a
|
4
|
+
data.tar.gz: a154b91cbf0b56568a7a63590ace9b4e5f1be233
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5e3b3d231e138732158caf657cf2b755709cc4fa310937cf214d5abf8636ac8c2cc80a511dea7426621ecf6a8b1b7a48745bbbba68cf772452e418f3bd466464
|
7
|
+
data.tar.gz: 60087771fcedfc6be518eb400289a3bfab1999a47ad393f2418046d18a12846ef24f010141b18b81590420551a427a41cfe44c3551a519130c0304879a94a2b2
|
data/bin/staticdctl
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require "zlib"
|
2
|
+
require "base64"
|
3
|
+
require "open-uri"
|
4
|
+
require "staticd_utils/memory_file"
|
5
|
+
require "staticd_utils/tar"
|
6
|
+
|
7
|
+
module StaticdUtils
|
8
|
+
|
9
|
+
# Manage Staticd archives.
|
10
|
+
#
|
11
|
+
# This class can manage the archives used as transport package to transfer
|
12
|
+
# files beetween Staticd client and Staticd API.
|
13
|
+
class Archive
|
14
|
+
attr_reader :stream
|
15
|
+
|
16
|
+
def self.open_file(url)
|
17
|
+
new(open(url))
|
18
|
+
end
|
19
|
+
|
20
|
+
# Create an archive from a folder.
|
21
|
+
#
|
22
|
+
# Can include a manifest as an array of files full path (from directory path
|
23
|
+
# as root).
|
24
|
+
#
|
25
|
+
# Example:
|
26
|
+
# StaticdUtils::Archive.create("/tmp/my_site", ["/index.html"])
|
27
|
+
# # Only the /tmp/my_site/index.html file will be included into
|
28
|
+
# the archive.
|
29
|
+
def self.create(directory_path, manifest=nil)
|
30
|
+
files =
|
31
|
+
if manifest
|
32
|
+
manifest.map { |entry| directory_path + entry }
|
33
|
+
else
|
34
|
+
Dir["#{directory_path}/**/*"].select { |f| File.file?(f) }
|
35
|
+
end
|
36
|
+
new(Tar.tar(files))
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(stream)
|
40
|
+
@stream = stream
|
41
|
+
end
|
42
|
+
|
43
|
+
def open
|
44
|
+
Dir.mktmpdir do |tmp|
|
45
|
+
Dir.chdir(tmp) do
|
46
|
+
extract(tmp)
|
47
|
+
yield tmp
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def close
|
53
|
+
@stream.close unless @stream.closed?
|
54
|
+
end
|
55
|
+
|
56
|
+
def extract(path)
|
57
|
+
return false if @stream.closed?
|
58
|
+
|
59
|
+
Tar.untar(@stream, path)
|
60
|
+
close
|
61
|
+
path
|
62
|
+
end
|
63
|
+
|
64
|
+
def size
|
65
|
+
@stream.size
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_file(path)
|
69
|
+
return false if @stream.closed?
|
70
|
+
|
71
|
+
File.open(path, 'w') { |file| file.write(@stream.read) }
|
72
|
+
self.close
|
73
|
+
path
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_memory_file
|
77
|
+
StaticdUtils::MemoryFile.new(@stream)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module StaticdUtils
|
2
|
+
|
3
|
+
# Class to convert file size in octect to human readable size.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
# Staticd::FileSize.new(1000).to_s
|
7
|
+
# # => "1KB"
|
8
|
+
class FileSize
|
9
|
+
|
10
|
+
def initialize(size)
|
11
|
+
@size = size
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
units = %w(B KB MB GB TB)
|
16
|
+
base = 1000
|
17
|
+
return "#{@size}#{units[0]}" if @size < base
|
18
|
+
|
19
|
+
exponent = (Math.log(@size) / Math.log(base)).to_i
|
20
|
+
exponent = units.size - 1 if exponent > units.size - 1
|
21
|
+
|
22
|
+
human_size = @size / base**exponent
|
23
|
+
"#{human_size}#{units[exponent]}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "gli"
|
2
|
+
|
3
|
+
# Move the GLI::App module into its own class.
|
4
|
+
#
|
5
|
+
# It's usefull to build a GLI::App like object.
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
# gli = GLIObject.new
|
9
|
+
# gli.program_desc("My Ultimate CLI")
|
10
|
+
# gli.version("1.0")
|
11
|
+
# gli.run(*args)
|
12
|
+
class GLIObject
|
13
|
+
include GLI::App
|
14
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module StaticdUtils
|
2
|
+
|
3
|
+
# Make an IO object behave like File objects.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
# io = StringIO.new("Content")
|
7
|
+
# file = MemoryFile.new(io)
|
8
|
+
# file.read
|
9
|
+
# # => "Content"
|
10
|
+
# file.path
|
11
|
+
# # => "memory_file"
|
12
|
+
# file.content_type
|
13
|
+
# # => "application/octet-stream"
|
14
|
+
class MemoryFile
|
15
|
+
|
16
|
+
def initialize(stream)
|
17
|
+
@stream = stream
|
18
|
+
end
|
19
|
+
|
20
|
+
def read(*args)
|
21
|
+
@stream.read(*args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def path
|
25
|
+
original_filename
|
26
|
+
end
|
27
|
+
|
28
|
+
def original_filename
|
29
|
+
"memory_file"
|
30
|
+
end
|
31
|
+
|
32
|
+
def content_type
|
33
|
+
"application/octet-stream"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require "digest/sha1"
|
2
|
+
require "yaml"
|
3
|
+
require "staticd_utils/memory_file"
|
4
|
+
|
5
|
+
module StaticdUtils
|
6
|
+
|
7
|
+
# Manifest for Staticd releases.
|
8
|
+
#
|
9
|
+
# A Sitemap consist of an associative array representing each resources of a
|
10
|
+
# site release. Each entry consist of the sha1 digest of the resource content
|
11
|
+
# and the complete HTTP path this resource must be available to.
|
12
|
+
#
|
13
|
+
# Example:
|
14
|
+
# sitemap = StaticdUtils::Sitemap.create("/tmp/my_website")
|
15
|
+
# sitemap.to_h
|
16
|
+
# # => {
|
17
|
+
# "058ec3fa8aab4c0ccac27d80fd24f30a8730d3f6"=>"/index.html",
|
18
|
+
# "92136ff551f50188f46486ab80db269eda4dfd4e"=>"/hello/world.html"
|
19
|
+
# }
|
20
|
+
class Sitemap
|
21
|
+
|
22
|
+
# Create a sitemap from a directory content.
|
23
|
+
#
|
24
|
+
# It register each files digest and path inside the sitemap.
|
25
|
+
def self.create(path)
|
26
|
+
map = {}
|
27
|
+
if File.directory?(path)
|
28
|
+
Dir.chdir(path) do
|
29
|
+
Dir["**/*"].each do |object|
|
30
|
+
if File.file?(object)
|
31
|
+
sha1 = Digest::SHA1.hexdigest(File.read(object))
|
32
|
+
map[sha1] = "/#{object}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
new(map)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create a sitemap from a YAML string.
|
41
|
+
#
|
42
|
+
# The YAML string must reflect the sitemap associative array structure.
|
43
|
+
#
|
44
|
+
# Example:
|
45
|
+
# yaml = "---\n058ec3fa8aab4c0ccac27d80fd24f30a8730d3f6: \"/hi.html\"\n"
|
46
|
+
# sitemap = StaticdUtils::Sitemap.open(yaml)
|
47
|
+
def self.open(yaml)
|
48
|
+
new(YAML.load(yaml))
|
49
|
+
end
|
50
|
+
|
51
|
+
# Create a sitemap from a YAML file.
|
52
|
+
#
|
53
|
+
# The YAML file must reflect the sitemap associative array structure.
|
54
|
+
def self.open_file(path)
|
55
|
+
open(File.read(path))
|
56
|
+
end
|
57
|
+
|
58
|
+
# Create a sitemap from an associative array.
|
59
|
+
#
|
60
|
+
# The associative array must have the folowing structure:
|
61
|
+
# * Key: the sha1 of the ressource
|
62
|
+
# * Value: the HTTP path of the resource
|
63
|
+
#
|
64
|
+
# Example:
|
65
|
+
# sitemap = Sitemap.new({
|
66
|
+
# 058ec3fa8aab4c0ccac27d80fd24f30a8730d3f6: "hi.html"
|
67
|
+
# })
|
68
|
+
def initialize(map)
|
69
|
+
@map = map
|
70
|
+
end
|
71
|
+
|
72
|
+
# View all HTTP path of the sitemap.
|
73
|
+
def routes
|
74
|
+
@map.map { |sha1, path| path }
|
75
|
+
end
|
76
|
+
|
77
|
+
# View all sha1 digest of the sitemap.
|
78
|
+
def digests
|
79
|
+
@map.map { |sha1, path| sha1 }
|
80
|
+
end
|
81
|
+
|
82
|
+
# Iterate over each resources of the sitemap.
|
83
|
+
def each_resources
|
84
|
+
@map.each { |sha1, path| yield sha1, path }
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_h
|
88
|
+
@map
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_yaml
|
92
|
+
@map.to_yaml
|
93
|
+
end
|
94
|
+
|
95
|
+
# Export the sitemap to a YAML file stored into memory.
|
96
|
+
def to_memory_file
|
97
|
+
StaticdUtils::MemoryFile.new(StringIO.new(to_yaml))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "rubygems/package"
|
2
|
+
require "digest/sha1"
|
3
|
+
|
4
|
+
module StaticdUtils
|
5
|
+
|
6
|
+
# Creation and Extraction of Tarball Stream.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
# tar = Tar.tar(["/tmp/hello"])
|
10
|
+
# Tar.untar(tar, "/tmp")
|
11
|
+
class Tar
|
12
|
+
|
13
|
+
def self.tar(files)
|
14
|
+
io = StringIO.new
|
15
|
+
tar = Gem::Package::TarWriter.new(io)
|
16
|
+
|
17
|
+
# Gem::Package::TarReader raise an exeption extracting an empty tarball,
|
18
|
+
# this add at least one useless file to extract.
|
19
|
+
tar.add_file("about", 0644) { |file| file.write("Hello.") }
|
20
|
+
|
21
|
+
files.each do |file|
|
22
|
+
content = File.read(file)
|
23
|
+
sha1 = Digest::SHA1.hexdigest(content)
|
24
|
+
tar.add_file(sha1, 0644) { |entry| entry.write(content) }
|
25
|
+
end
|
26
|
+
|
27
|
+
io.rewind
|
28
|
+
io
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.untar(io, path)
|
32
|
+
FileUtils.mkdir_p("#{path}")
|
33
|
+
tar = Gem::Package::TarReader.new(io)
|
34
|
+
tar.rewind
|
35
|
+
tar.each do |entry|
|
36
|
+
File.open("#{path}/#{entry.full_name}", "w+") do |file|
|
37
|
+
file.write(entry.read)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
tar.close
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/staticdctl.rb
ADDED
@@ -0,0 +1,330 @@
|
|
1
|
+
require "digest/sha1"
|
2
|
+
require "staticdctl"
|
3
|
+
require "yaml"
|
4
|
+
require "staticd_utils/gli_object"
|
5
|
+
require "staticd_utils/archive"
|
6
|
+
require "staticd_utils/sitemap"
|
7
|
+
require "staticd_utils/file_size"
|
8
|
+
|
9
|
+
module Staticdctl
|
10
|
+
class CLI
|
11
|
+
|
12
|
+
def initialize(options={})
|
13
|
+
@gli = GLIObject.new
|
14
|
+
@gli.program_desc("Staticd CLI client")
|
15
|
+
@gli.version(Staticdctl::VERSION)
|
16
|
+
|
17
|
+
enable_debugging if options[:debugging]
|
18
|
+
set_global_options
|
19
|
+
build_commands
|
20
|
+
end
|
21
|
+
|
22
|
+
def run(*args)
|
23
|
+
@gli.run(*args)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def enable_debugging
|
29
|
+
@gli.on_error { |exception| raise exception }
|
30
|
+
end
|
31
|
+
|
32
|
+
def load_global_config(config_file)
|
33
|
+
YAML.load_file(config_file)
|
34
|
+
rescue
|
35
|
+
{}
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_config(config_file, host)
|
39
|
+
config = load_global_config(config_file)
|
40
|
+
config && config.has_key?(host) ? config[host] : {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def staticd_client(options)
|
44
|
+
config = load_config(options[:config], options[:host])
|
45
|
+
client = Staticdctl::StaticdClient.new(
|
46
|
+
options[:host],
|
47
|
+
access_id: config["access_id"],
|
48
|
+
secret_key: config["secret_key"]
|
49
|
+
)
|
50
|
+
yield client
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_global_options
|
54
|
+
set_global_option_config
|
55
|
+
set_global_option_host
|
56
|
+
set_global_option_site
|
57
|
+
set_global_option_debug
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_global_option_config
|
61
|
+
@gli.desc("Staticd configuration file")
|
62
|
+
@gli.default_value("#{ENV['HOME']}/.staticdctl.yml")
|
63
|
+
@gli.arg_name("<Staticd configuration file>")
|
64
|
+
@gli.flag([:c, :config])
|
65
|
+
end
|
66
|
+
|
67
|
+
def set_global_option_host
|
68
|
+
@gli.desc("Staticd API endpoint")
|
69
|
+
@gli.default_value(
|
70
|
+
ENV["STATICDCTL_ENDPOINT"] || "http://localhost/api/v1"
|
71
|
+
)
|
72
|
+
@gli.arg_name("<Staticd API endpoint>")
|
73
|
+
@gli.flag([:h, :host])
|
74
|
+
end
|
75
|
+
|
76
|
+
def set_global_option_site
|
77
|
+
@gli.desc("Site name")
|
78
|
+
@gli.default_value(File.basename(Dir.pwd))
|
79
|
+
@gli.arg_name("<Site name>")
|
80
|
+
@gli.flag([:s, :site])
|
81
|
+
end
|
82
|
+
|
83
|
+
def set_global_option_debug
|
84
|
+
@gli.desc("Enable debugging (raise exception on error)")
|
85
|
+
@gli.default_value(false)
|
86
|
+
@gli.arg_name("debug")
|
87
|
+
@gli.switch([:d, :debug])
|
88
|
+
end
|
89
|
+
|
90
|
+
def build_commands
|
91
|
+
build_command_config
|
92
|
+
build_command_set_config
|
93
|
+
build_command_rm_config
|
94
|
+
build_command_sites
|
95
|
+
build_command_create_site
|
96
|
+
build_command_destroy_site
|
97
|
+
build_command_domains
|
98
|
+
build_command_attach_domain
|
99
|
+
build_command_detach_domain
|
100
|
+
build_command_releases
|
101
|
+
build_command_create_release
|
102
|
+
end
|
103
|
+
|
104
|
+
def build_command_config
|
105
|
+
@gli.desc("Display current configuration")
|
106
|
+
@gli.command(:config) do |c|
|
107
|
+
c.action do |global_options, options, args|
|
108
|
+
config = load_config(global_options[:config], global_options[:host])
|
109
|
+
puts "Current configuration for #{global_options[:host]}:"
|
110
|
+
puts "No config." unless config.any?
|
111
|
+
config.each do |key, value|
|
112
|
+
puts " * #{key}: #{value}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def build_command_set_config
|
119
|
+
@gli.desc("Set a configuration option")
|
120
|
+
@gli.arg_name("config_key")
|
121
|
+
@gli.command(:"config:set") do |c|
|
122
|
+
c.action do |global_options, options, args|
|
123
|
+
global_config = load_global_config(global_options[:config])
|
124
|
+
global_config[global_options[:host]] ||= {}
|
125
|
+
global_config[global_options[:host]][args[0]] = args[1]
|
126
|
+
File.open(global_options[:config], 'w+') do |file|
|
127
|
+
file.write(global_config.to_yaml)
|
128
|
+
puts "The #{args[0]} config key has been set to #{args[1]}."
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def build_command_rm_config
|
135
|
+
@gli.desc("Remove a configuration option")
|
136
|
+
@gli.arg_name("config_key")
|
137
|
+
@gli.command(:"config:rm") do |c|
|
138
|
+
c.action do |global_options, options, args|
|
139
|
+
global_config = load_global_config(global_options[:config])
|
140
|
+
|
141
|
+
unless (
|
142
|
+
global_config.has_key?(global_options[:host]) &&
|
143
|
+
global_config[global_options[:host]].has_key?(args.first)
|
144
|
+
) then
|
145
|
+
puts "The #{args.first} config key cannot be found."
|
146
|
+
end
|
147
|
+
|
148
|
+
global_config[global_options[:host]].delete(args.first)
|
149
|
+
File.open(global_options[:config], 'w+') do |file|
|
150
|
+
file.write(global_config.to_yaml)
|
151
|
+
puts "The #{args.first} config key has been removed."
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def build_command_sites
|
158
|
+
@gli.desc("List all sites")
|
159
|
+
@gli.command(:sites) do |c|
|
160
|
+
c.action do |global_options,options,args|
|
161
|
+
staticd_client(global_options) do |client|
|
162
|
+
client.sites do |sites|
|
163
|
+
puts "Sites hosted on #{global_options[:host]}:"
|
164
|
+
puts "No sites yet." unless sites.any?
|
165
|
+
sites.each do |site|
|
166
|
+
last_release = site.releases.last
|
167
|
+
last_release_string = last_release ? last_release.tag : "-"
|
168
|
+
domains =
|
169
|
+
site.domain_names.map{ |domain| domain.name }.join(", ")
|
170
|
+
domains_string = domains.empty? ? "no domains" : domains
|
171
|
+
puts " * #{site.name} (#{last_release_string}): #{site.url}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def build_command_create_site
|
180
|
+
@gli.desc("Create a new site")
|
181
|
+
@gli.command(:"sites:create") do |c|
|
182
|
+
c.action do |global_options,options,args|
|
183
|
+
staticd_client global_options do |client|
|
184
|
+
client.create_site(name: global_options[:site]) do |site|
|
185
|
+
puts "The #{site.name} site has been created."
|
186
|
+
puts site.url if site.url
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def build_command_destroy_site
|
194
|
+
@gli.desc("Destroy a site")
|
195
|
+
@gli.command(:"sites:destroy") do |c|
|
196
|
+
c.action do |global_options,options,args|
|
197
|
+
staticd_client(global_options) do |client|
|
198
|
+
client.destroy_site(global_options[:site]) do
|
199
|
+
puts "The #{global_options[:site]} site has been destroyed."
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def build_command_domains
|
207
|
+
@gli.desc("List all domain attached to the current site")
|
208
|
+
@gli.command :domains do |c|
|
209
|
+
c.action do |global_options,options,args|
|
210
|
+
staticd_client(global_options) do |client|
|
211
|
+
client.domains(global_options[:site]) do |domains|
|
212
|
+
puts "Domain names attached to #{global_options[:site]}:"
|
213
|
+
puts "No domain names attached." unless domains.any?
|
214
|
+
domains.each do |domain|
|
215
|
+
puts " * #{domain.name}"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def build_command_attach_domain
|
224
|
+
@gli.desc("Attach a domain name to a site")
|
225
|
+
@gli.arg_name("domain_name")
|
226
|
+
@gli.command(:"domains:attach") do |c|
|
227
|
+
c.action do |global_options,options,args|
|
228
|
+
staticd_client(global_options) do |client|
|
229
|
+
client.attach_domain(
|
230
|
+
global_options[:site],
|
231
|
+
name: args.first
|
232
|
+
) do |domain|
|
233
|
+
puts "The #{domain.name} domain has been attached to the " +
|
234
|
+
"#{domain.site_name} site."
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def build_command_detach_domain
|
242
|
+
@gli.desc("Detach a domain name from a site")
|
243
|
+
@gli.arg_name("domain_name")
|
244
|
+
@gli.command(:"domains:detach") do |c|
|
245
|
+
c.action do |global_options,options,args|
|
246
|
+
staticd_client(global_options) do |client|
|
247
|
+
client.detach_domain(global_options[:site], args.first) do |domain|
|
248
|
+
puts "The #{args.first} domain has been detached from the " +
|
249
|
+
"#{global_options[:site]} site."
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def build_command_releases
|
257
|
+
@gli.desc("List all releases of the current site")
|
258
|
+
@gli.command(:releases) do |c|
|
259
|
+
c.action do |global_options,options,args|
|
260
|
+
staticd_client(global_options) do |client|
|
261
|
+
client.releases(global_options[:site]) do |releases|
|
262
|
+
releases_string =
|
263
|
+
if releases.any?
|
264
|
+
releases.map { |release| release.tag }.join(", ")
|
265
|
+
else
|
266
|
+
"no release deployed yet"
|
267
|
+
end
|
268
|
+
puts "Releases of #{global_options[:site]}: #{releases_string}."
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def build_command_create_release
|
276
|
+
@gli.desc("Push a new release for the current app")
|
277
|
+
@gli.arg_name("[path]")
|
278
|
+
@gli.command(:push) do |c|
|
279
|
+
c.action do |global_options,options,args|
|
280
|
+
source_path = args.any? ? args.first : "."
|
281
|
+
|
282
|
+
print "Counting resources... "
|
283
|
+
sitemap = StaticdUtils::Sitemap.create(source_path)
|
284
|
+
if sitemap.routes.size < 1
|
285
|
+
puts "stop. No resources."
|
286
|
+
raise "No resources to send in '#{source_path}'"
|
287
|
+
end
|
288
|
+
puts "done (#{sitemap.routes.size} resources)."
|
289
|
+
|
290
|
+
print "Asking host to identify new resources... "
|
291
|
+
diff_sitemap = staticd_client(global_options) do |client|
|
292
|
+
client.cached_resources(sitemap.to_h) do |new_map|
|
293
|
+
StaticdUtils::Sitemap.new(new_map.to_h)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
puts "done (#{diff_sitemap.routes.size} new resources to upload)."
|
297
|
+
|
298
|
+
print "Building the archive... "
|
299
|
+
archive = StaticdUtils::Archive.create(
|
300
|
+
source_path,
|
301
|
+
diff_sitemap.routes
|
302
|
+
)
|
303
|
+
file_size = StaticdUtils::FileSize.new(archive.size)
|
304
|
+
puts "done (#{file_size})."
|
305
|
+
|
306
|
+
staticd_client(global_options) do |client|
|
307
|
+
|
308
|
+
print "Uploading the archive... "
|
309
|
+
timer_start = Time.now
|
310
|
+
client.create_release(
|
311
|
+
global_options[:site],
|
312
|
+
archive.to_memory_file,
|
313
|
+
sitemap.to_memory_file
|
314
|
+
) do |release|
|
315
|
+
time_spent = Time.now - timer_start
|
316
|
+
speed = archive.size / time_spent / 1000
|
317
|
+
puts "done (#{'%.2f' % time_spent}s / #{'%.2f' % speed}kbps)."
|
318
|
+
puts ""
|
319
|
+
puts "The #{release.site_name} release (#{release.tag}) has " +
|
320
|
+
"been created."
|
321
|
+
if release.site.url
|
322
|
+
puts release.site.url
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require "rest_client"
|
2
|
+
require "api-auth"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Staticdctl
|
6
|
+
|
7
|
+
# Simple REST client library with HMAC authentication.
|
8
|
+
#
|
9
|
+
# Support for remote call on JSON REST API.
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
# client = Staticdctl::RESTCLient.new(
|
13
|
+
# url: "http://api.domain.tld/v1",
|
14
|
+
# access_id: 1000,
|
15
|
+
# secret_key: "youshallnotpass"
|
16
|
+
# )
|
17
|
+
# client.call(:get, "/resources") { |response| puts response }
|
18
|
+
class RESTClient
|
19
|
+
|
20
|
+
def initialize(url, hmac={})
|
21
|
+
@url = url
|
22
|
+
@access_id = hmac[:access_id] || ""
|
23
|
+
@secret_key = hmac[:secret_key] || ""
|
24
|
+
end
|
25
|
+
|
26
|
+
# Call a remote REST API action.
|
27
|
+
#
|
28
|
+
# Example:
|
29
|
+
# client.call(:post, "/posts", {text: "hello_world"}) do |response|
|
30
|
+
# puts response
|
31
|
+
# end
|
32
|
+
def call(method, path, req_data=nil, &block)
|
33
|
+
headers = {
|
34
|
+
"Accept" => "application/json"
|
35
|
+
}
|
36
|
+
headers["Content-Type"] = "application/json" if req_data
|
37
|
+
payload = req_data ? req_data.to_json : nil
|
38
|
+
request = RestClient::Request.new(
|
39
|
+
url: @url + path,
|
40
|
+
method: method,
|
41
|
+
headers: headers,
|
42
|
+
payload: payload
|
43
|
+
)
|
44
|
+
send_request(request, block)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Send files using the HTTP multipart/form-data content-type.
|
48
|
+
#
|
49
|
+
# Example:
|
50
|
+
# client.send_files("/attachments", {first: file1, second: file2})
|
51
|
+
def send_files(path, files, &block)
|
52
|
+
headers = {
|
53
|
+
"Accept" => "application/json",
|
54
|
+
"Content-Type" => "multipart/form-data"
|
55
|
+
}
|
56
|
+
request = RestClient::Request.new(
|
57
|
+
url: @url + path,
|
58
|
+
method: :post,
|
59
|
+
headers: headers,
|
60
|
+
payload: files,
|
61
|
+
timeout: -1
|
62
|
+
)
|
63
|
+
send_request(request, block)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def send_request(request, block)
|
69
|
+
signed_request = ApiAuth.sign!(request, @access_id, @secret_key)
|
70
|
+
signed_request.execute do |response, request, result|
|
71
|
+
res_data = JSON.parse(response.to_s) unless response.to_s.empty?
|
72
|
+
case response.code
|
73
|
+
when 200
|
74
|
+
block.call(res_data)
|
75
|
+
when 204
|
76
|
+
block.call
|
77
|
+
when 403
|
78
|
+
raise res_data["error"]
|
79
|
+
when 401
|
80
|
+
raise res_data["error"]
|
81
|
+
else
|
82
|
+
raise "Server returned an '#{response.code}' status code."
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require "staticdctl/rest_client"
|
2
|
+
|
3
|
+
module Staticdctl
|
4
|
+
|
5
|
+
# Class to interact with the Staticd API.
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
# staticd_client = Staticdctl::StaticdClient.new(
|
9
|
+
# url: "http://staticd.domain.tld/api",
|
10
|
+
# access_id: ENV["STATICD_ACCESS_ID"],
|
11
|
+
# secret_key: ENV["STATICD_SECRET_KEY"]
|
12
|
+
# )
|
13
|
+
class StaticdClient
|
14
|
+
|
15
|
+
def initialize(url, hmac={})
|
16
|
+
url = url
|
17
|
+
access_id = hmac[:access_id] || ""
|
18
|
+
secret_key = hmac[:secret_key] || ""
|
19
|
+
@staticd_api = Staticdctl::RESTClient.new(
|
20
|
+
url,
|
21
|
+
access_id: access_id,
|
22
|
+
secret_key: secret_key
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def sites
|
27
|
+
@staticd_api.call(:get, "/sites") do |data|
|
28
|
+
yield build_response(data)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_site(site_params)
|
33
|
+
@staticd_api.call(:post, "/sites", site_params) do |data|
|
34
|
+
yield build_response(data)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def destroy_site(site_name)
|
39
|
+
@staticd_api.call(:delete, "/sites/#{site_name}") do
|
40
|
+
yield
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def domains(site_name)
|
45
|
+
@staticd_api.call(:get, "/sites/#{site_name}/domain_names") do |data|
|
46
|
+
yield build_response(data)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def attach_domain(site_name, domain_params)
|
51
|
+
@staticd_api.call(
|
52
|
+
:post,
|
53
|
+
"/sites/#{site_name}/domain_names",
|
54
|
+
domain_params
|
55
|
+
) do |data|
|
56
|
+
yield build_response(data)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def detach_domain(site_name, domain_name)
|
61
|
+
@staticd_api.call(
|
62
|
+
:delete,
|
63
|
+
"/sites/#{site_name}/domain_names/#{domain_name}"
|
64
|
+
) do
|
65
|
+
yield
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def releases(site_name)
|
70
|
+
@staticd_api.call :get, "/sites/#{site_name}/releases" do |data|
|
71
|
+
yield build_response(data)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def create_release(site_name, archive_file, sitemap_file)
|
76
|
+
@staticd_api.send_files(
|
77
|
+
"/sites/#{site_name}/releases",
|
78
|
+
{file: archive_file, sitemap: sitemap_file}
|
79
|
+
) do |data|
|
80
|
+
yield build_response(data)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Parse a sitemap of resources digest and return of sitemap purged of
|
85
|
+
# already know resources.
|
86
|
+
#
|
87
|
+
# Submit a list of resources sha1 digests with HTTP path (in the sitemap
|
88
|
+
# format) and get a list purged of already known resources (resources
|
89
|
+
# already stored in database).
|
90
|
+
def cached_resources(digests)
|
91
|
+
@staticd_api.call(:post, "/resources/get_cached", digests) do |data|
|
92
|
+
yield build_response(data)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def build_response(data)
|
99
|
+
data.kind_of?(Array) ? build_collection(data) : build_object(data)
|
100
|
+
end
|
101
|
+
|
102
|
+
def build_collection(data)
|
103
|
+
data.map { |element| build_object(element) }
|
104
|
+
end
|
105
|
+
|
106
|
+
def build_object(data)
|
107
|
+
struct = OpenStruct.new
|
108
|
+
data.each do |key, value|
|
109
|
+
struct[key] =
|
110
|
+
case
|
111
|
+
when value.kind_of?(Array) then build_collection(value)
|
112
|
+
when value.kind_of?(Hash) then build_object(value)
|
113
|
+
else value
|
114
|
+
end
|
115
|
+
end
|
116
|
+
struct
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: staticdctl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Etienne Garnier
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-01-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 10.3.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 10.3.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: gli
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.12.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.12.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rest_client
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.8.1
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.8.1
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: api-auth
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.2.6
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.2.6
|
69
|
+
description: CLI Client for the Staticd API service
|
70
|
+
email: garnier.etienne@gmail.com
|
71
|
+
executables:
|
72
|
+
- staticdctl
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- lib/staticdctl/cli.rb
|
77
|
+
- lib/staticdctl/rest_client.rb
|
78
|
+
- lib/staticdctl/staticd_client.rb
|
79
|
+
- lib/staticdctl/version.rb
|
80
|
+
- lib/staticd_utils/memory_file.rb
|
81
|
+
- lib/staticd_utils/file_size.rb
|
82
|
+
- lib/staticd_utils/sitemap.rb
|
83
|
+
- lib/staticd_utils/gli_object.rb
|
84
|
+
- lib/staticd_utils/archive.rb
|
85
|
+
- lib/staticd_utils/tar.rb
|
86
|
+
- lib/staticdctl.rb
|
87
|
+
- bin/staticdctl
|
88
|
+
homepage: http://staticd.eggnet.io
|
89
|
+
licenses:
|
90
|
+
- MIT
|
91
|
+
metadata: {}
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
requirements: []
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 2.0.14
|
110
|
+
signing_key:
|
111
|
+
specification_version: 4
|
112
|
+
summary: Staticd CLI Client
|
113
|
+
test_files: []
|