redmine-installer 1.0.7 → 2.0.0.rc1
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 +4 -4
- data/.gitignore +4 -0
- data/.travis.yml +7 -0
- data/Gemfile +3 -15
- data/README.md +49 -177
- data/bin/redmine +4 -5
- data/lib/redmine-installer/backup.rb +13 -40
- data/lib/redmine-installer/cli.rb +92 -61
- data/lib/redmine-installer/command.rb +63 -117
- data/lib/redmine-installer/configuration.rb +148 -0
- data/lib/redmine-installer/database.rb +204 -0
- data/lib/redmine-installer/environment.rb +21 -0
- data/lib/redmine-installer/errors.rb +7 -0
- data/lib/redmine-installer/install.rb +37 -42
- data/lib/redmine-installer/logger.rb +83 -0
- data/lib/redmine-installer/package.rb +180 -0
- data/lib/redmine-installer/patches/ruby.rb +35 -0
- data/lib/redmine-installer/patches/tty.rb +16 -0
- data/lib/redmine-installer/profile.rb +24 -55
- data/lib/redmine-installer/redmine.rb +551 -0
- data/lib/redmine-installer/spec/spec.rb +81 -0
- data/lib/redmine-installer/task.rb +18 -77
- data/lib/redmine-installer/task_module.rb +12 -0
- data/lib/redmine-installer/upgrade.rb +51 -59
- data/lib/redmine-installer/utils.rb +46 -233
- data/lib/redmine-installer/version.rb +2 -4
- data/lib/redmine-installer.rb +69 -56
- data/redmine-installer.gemspec +20 -19
- data/spec/custom_matchers.rb +21 -0
- data/spec/installer_helper.rb +107 -0
- data/spec/installer_process.rb +82 -0
- data/spec/lib/backup_restore_spec.rb +81 -0
- data/spec/lib/install_spec.rb +125 -36
- data/spec/lib/upgrade_spec.rb +73 -52
- data/spec/packages/redmine-3.1.0.zip +0 -0
- data/spec/packages/redmine-3.2.0.zip +0 -0
- data/spec/packages/redmine-3.3.0-bad-migration.zip +0 -0
- data/spec/packages/redmine-3.3.0.zip +0 -0
- data/spec/packages/something-else.zip +0 -0
- data/spec/packages_helper.rb +19 -0
- data/spec/shared_contexts.rb +13 -0
- data/spec/spec_helper.rb +34 -18
- metadata +62 -63
- data/lib/redmine-installer/config_param.rb +0 -100
- data/lib/redmine-installer/error.rb +0 -5
- data/lib/redmine-installer/exec.rb +0 -158
- data/lib/redmine-installer/ext/module.rb +0 -7
- data/lib/redmine-installer/ext/string.rb +0 -15
- data/lib/redmine-installer/git.rb +0 -51
- data/lib/redmine-installer/helper.rb +0 -5
- data/lib/redmine-installer/helpers/generate_config.rb +0 -29
- data/lib/redmine-installer/locales/cs.yml +0 -147
- data/lib/redmine-installer/locales/en.yml +0 -154
- data/lib/redmine-installer/plugin.rb +0 -9
- data/lib/redmine-installer/plugins/base.rb +0 -24
- data/lib/redmine-installer/plugins/database.rb +0 -180
- data/lib/redmine-installer/plugins/email_sending.rb +0 -82
- data/lib/redmine-installer/plugins/redmine_plugin.rb +0 -82
- data/lib/redmine-installer/plugins/web_server.rb +0 -26
- data/lib/redmine-installer/step.rb +0 -16
- data/lib/redmine-installer/steps/backup.rb +0 -125
- data/lib/redmine-installer/steps/base.rb +0 -79
- data/lib/redmine-installer/steps/database_config.rb +0 -15
- data/lib/redmine-installer/steps/email_config.rb +0 -11
- data/lib/redmine-installer/steps/env_check.rb +0 -20
- data/lib/redmine-installer/steps/install.rb +0 -23
- data/lib/redmine-installer/steps/load_package.rb +0 -226
- data/lib/redmine-installer/steps/move_redmine.rb +0 -22
- data/lib/redmine-installer/steps/redmine_root.rb +0 -52
- data/lib/redmine-installer/steps/upgrade.rb +0 -57
- data/lib/redmine-installer/steps/validation.rb +0 -38
- data/lib/redmine-installer/steps/webserver_config.rb +0 -22
- data/spec/load_redmine.rb +0 -24
@@ -0,0 +1,21 @@
|
|
1
|
+
module RedmineInstaller
|
2
|
+
class Environment < TaskModule
|
3
|
+
|
4
|
+
def check
|
5
|
+
if user_is_root? && !task.options.enable_user_root
|
6
|
+
error 'You cannot install redmine under root. Please change user.'
|
7
|
+
end
|
8
|
+
|
9
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new(RedmineInstaller::MIN_SUPPORTED_RUBY)
|
10
|
+
error "You are using unsupported ruby. Please install at least #{RedmineInstaller::MIN_SUPPORTED_RUBY}."
|
11
|
+
end
|
12
|
+
|
13
|
+
logger.info 'Environemnt checked'
|
14
|
+
end
|
15
|
+
|
16
|
+
def user_is_root?
|
17
|
+
Process.euid == 0
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -1,50 +1,45 @@
|
|
1
|
-
|
2
|
-
# Install redmine
|
3
|
-
#
|
4
|
-
# You can instal redmine package from archive or git.
|
5
|
-
#
|
6
|
-
# == Steps:
|
7
|
-
# 1. Redmine root - where should be new redmine located
|
8
|
-
# 2. Load package - extract package
|
9
|
-
# 3. Database configuration - you can choose type of DB which you want to use
|
10
|
-
# 4. Email sending configuration - email sending configuration
|
11
|
-
# 5. Install - install commands are executed
|
12
|
-
# 6. Moving redmine - redmine is moved from temporarily folder to given redmine_root
|
13
|
-
# 7. Webserve configuration - generating webserver configuration
|
14
|
-
#
|
15
|
-
# == Usage:
|
16
|
-
#
|
17
|
-
# From archive::
|
18
|
-
# Supported archives are .zip and .tar.gz.
|
19
|
-
#
|
20
|
-
# # minimal
|
21
|
-
# redmine install PATH_TO_PACKAGE
|
22
|
-
# redmine install REDMINE_VERSION
|
23
|
-
#
|
24
|
-
# # full
|
25
|
-
# redmine install PATH_TO_PACKAGE --env ENV1,ENV2,ENV3
|
26
|
-
#
|
27
|
-
module Redmine::Installer
|
1
|
+
module RedmineInstaller
|
28
2
|
class Install < Task
|
29
3
|
|
30
|
-
|
31
|
-
|
32
|
-
step::RedmineRoot,
|
33
|
-
step::LoadPackage,
|
34
|
-
step::DatabaseConfig,
|
35
|
-
step::EmailConfig,
|
36
|
-
step::Install,
|
37
|
-
step::MoveRedmine,
|
38
|
-
step::WebserverConfig
|
39
|
-
]
|
4
|
+
def initialize(package, redmine_root, **options)
|
5
|
+
super(**options)
|
40
6
|
|
41
|
-
|
7
|
+
@environment = Environment.new(self)
|
8
|
+
@package = Package.new(self, package)
|
9
|
+
@target_redmine = Redmine.new(self, redmine_root)
|
10
|
+
@temp_redmine = Redmine.new(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
def up
|
14
|
+
@temp_redmine.valid_options
|
15
|
+
@environment.check
|
16
|
+
@target_redmine.ensure_and_valid_root
|
17
|
+
@package.ensure_and_valid_package
|
18
|
+
@package.extract
|
19
|
+
|
20
|
+
@temp_redmine.root = @package.redmine_root
|
21
|
+
|
22
|
+
@temp_redmine.create_database_yml
|
23
|
+
@temp_redmine.create_configuration_yml
|
24
|
+
@temp_redmine.install
|
25
|
+
|
26
|
+
print_title('Finishing installation')
|
27
|
+
ok('Cleaning root'){ @target_redmine.delete_root }
|
28
|
+
ok('Moving redmine to target directory'){ @target_redmine.move_from(@temp_redmine) }
|
29
|
+
ok('Cleanning up'){ @package.clean_up }
|
30
|
+
ok('Moving installer log'){ logger.move_to(@target_redmine, suffix: 'install') }
|
31
|
+
|
32
|
+
puts
|
33
|
+
puts pastel.bold('Redmine was installed')
|
34
|
+
logger.info('Redmine was installed')
|
35
|
+
end
|
42
36
|
|
43
|
-
def
|
44
|
-
|
45
|
-
|
37
|
+
def down
|
38
|
+
@temp_redmine.clean_up
|
39
|
+
@package.clean_up
|
46
40
|
|
47
|
-
|
41
|
+
puts
|
42
|
+
puts "(Log is located on #{pastel.bold(logger.path)})"
|
48
43
|
end
|
49
44
|
|
50
45
|
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module RedmineInstaller
|
4
|
+
class Logger
|
5
|
+
|
6
|
+
def self.verify(log_file)
|
7
|
+
log_file = log_file.to_s
|
8
|
+
|
9
|
+
unless File.exist?(log_file)
|
10
|
+
puts "File '#{log_file}' does not exist."
|
11
|
+
exit(false)
|
12
|
+
end
|
13
|
+
|
14
|
+
content = File.open(log_file, &:to_a)
|
15
|
+
digest1 = content.pop
|
16
|
+
digest2 = Digest::SHA256.hexdigest(content.join)
|
17
|
+
|
18
|
+
if digest1 == digest2
|
19
|
+
puts RedmineInstaller.pastel.green("Logfile is OK. Digest verified.")
|
20
|
+
else
|
21
|
+
puts RedmineInstaller.pastel.red("Logfile is not OK. Digest wasn't verified.")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
if ENV['REDMINE_INSTALLER_LOGFILE']
|
27
|
+
@output = File.open(ENV['REDMINE_INSTALLER_LOGFILE'], 'w')
|
28
|
+
else
|
29
|
+
@output = Tempfile.create('redmine_installer.log')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def path
|
34
|
+
@output.path
|
35
|
+
end
|
36
|
+
|
37
|
+
def finish
|
38
|
+
close
|
39
|
+
digest = Digest::SHA256.file(path).hexdigest
|
40
|
+
File.open(path, 'a') { |f| f.write(digest) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def close
|
44
|
+
@output.flush
|
45
|
+
@output.close
|
46
|
+
end
|
47
|
+
|
48
|
+
def move_to(redmine, suffix: '%d%m%Y_%H%M%S')
|
49
|
+
close
|
50
|
+
|
51
|
+
new_path = File.join(redmine.log_path, Time.now.strftime("redmine_installer_#{suffix}.log"))
|
52
|
+
|
53
|
+
FileUtils.mkdir_p(redmine.log_path)
|
54
|
+
FileUtils.mv(path, new_path)
|
55
|
+
@output = File.open(new_path, 'a+')
|
56
|
+
end
|
57
|
+
|
58
|
+
def info(*messages)
|
59
|
+
log(' INFO', *messages)
|
60
|
+
end
|
61
|
+
|
62
|
+
def error(*messages)
|
63
|
+
log('ERROR', *messages)
|
64
|
+
end
|
65
|
+
|
66
|
+
def warn(*messages)
|
67
|
+
log(' WARN', *messages)
|
68
|
+
end
|
69
|
+
|
70
|
+
def std(*messages)
|
71
|
+
log(' STD', *messages)
|
72
|
+
end
|
73
|
+
|
74
|
+
def log(severity, *messages)
|
75
|
+
messages.each do |message|
|
76
|
+
@output.puts("#{severity}: #{message}")
|
77
|
+
end
|
78
|
+
|
79
|
+
@output.flush
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'rubygems/package'
|
2
|
+
require 'net/http'
|
3
|
+
require 'zlib'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module RedmineInstaller
|
7
|
+
class Package < TaskModule
|
8
|
+
|
9
|
+
SUPPORTED_ARCHIVE_FORMATS = ['.zip', '.gz', '.tgz']
|
10
|
+
TAR_LONGLINK = '././@LongLink'
|
11
|
+
|
12
|
+
attr_reader :package
|
13
|
+
|
14
|
+
def initialize(task, package)
|
15
|
+
super(task)
|
16
|
+
@package = package.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def ensure_and_valid_package
|
20
|
+
if package.empty?
|
21
|
+
@package = prompt.ask('Path to package:', required: true)
|
22
|
+
end
|
23
|
+
|
24
|
+
if !File.exist?(@package)
|
25
|
+
if @package =~ /\Av?(\d\.\d\.\d)\Z/
|
26
|
+
@package = download_redmine($1)
|
27
|
+
else
|
28
|
+
error "File doesn't exist #{@package}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
@type = File.extname(@package)
|
33
|
+
unless SUPPORTED_ARCHIVE_FORMATS.include?(@type)
|
34
|
+
error "File #{@package} must have format: #{SUPPORTED_ARCHIVE_FORMATS.join(', ')}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def extract
|
39
|
+
print_title('Extracting redmine package')
|
40
|
+
|
41
|
+
@temp_dir = Dir.mktmpdir
|
42
|
+
|
43
|
+
case @type
|
44
|
+
when '.zip'
|
45
|
+
extract_zip
|
46
|
+
when '.gz', '.tgz'
|
47
|
+
extract_tar_gz
|
48
|
+
end
|
49
|
+
|
50
|
+
logger.info("Package was loaded into #{@temp_dir}.")
|
51
|
+
end
|
52
|
+
|
53
|
+
# Move files from temp dir to target. First check if folder contains redmine
|
54
|
+
# or contains folder which contains redmine.
|
55
|
+
#
|
56
|
+
# Package can have format:
|
57
|
+
# |-- redmine-2
|
58
|
+
# |-- app
|
59
|
+
# `-- config
|
60
|
+
# ...
|
61
|
+
#
|
62
|
+
def redmine_root
|
63
|
+
root = @temp_dir
|
64
|
+
|
65
|
+
loop {
|
66
|
+
ls = Dir.glob(File.join(root, '*'))
|
67
|
+
|
68
|
+
if ls.size == 1
|
69
|
+
root = ls.first
|
70
|
+
else
|
71
|
+
break
|
72
|
+
end
|
73
|
+
}
|
74
|
+
|
75
|
+
root
|
76
|
+
end
|
77
|
+
|
78
|
+
def clean_up
|
79
|
+
@temp_dir && FileUtils.remove_entry_secure(@temp_dir)
|
80
|
+
@temp_file && FileUtils.remove_entry_secure(@temp_file)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def download_redmine(version)
|
86
|
+
@temp_file = Tempfile.new(['redmine', '.zip'])
|
87
|
+
@temp_file.binmode
|
88
|
+
|
89
|
+
uri = URI("http://www.redmine.org/releases/redmine-#{version}.zip")
|
90
|
+
|
91
|
+
Net::HTTP.start(uri.host, uri.port) do |http|
|
92
|
+
head = http.request_head(uri)
|
93
|
+
|
94
|
+
unless head.is_a?(Net::HTTPSuccess)
|
95
|
+
error "Cannot download redmine #{version}"
|
96
|
+
end
|
97
|
+
|
98
|
+
print_title("Downloading redmine #{version}")
|
99
|
+
progressbar = TTY::ProgressBar.new(PROGRESSBAR_FORMAT, total: head['content-length'].to_i, frequency: 2, clear: true)
|
100
|
+
|
101
|
+
http.get(uri) do |data|
|
102
|
+
@temp_file.write(data)
|
103
|
+
progressbar.advance(data.size)
|
104
|
+
end
|
105
|
+
|
106
|
+
progressbar.finish
|
107
|
+
end
|
108
|
+
|
109
|
+
logger.info("Redmine #{version} downloaded")
|
110
|
+
|
111
|
+
@temp_file.close
|
112
|
+
@temp_file.path
|
113
|
+
end
|
114
|
+
|
115
|
+
def extract_zip
|
116
|
+
Zip::File.open(@package) do |zip_file|
|
117
|
+
# Progressbar
|
118
|
+
progressbar = TTY::ProgressBar.new(PROGRESSBAR_FORMAT, total: zip_file.size, frequency: 2, clear: true)
|
119
|
+
|
120
|
+
zip_file.each do |entry|
|
121
|
+
dest_file = File.join(@temp_dir, entry.name)
|
122
|
+
FileUtils.mkdir_p(File.dirname(dest_file))
|
123
|
+
|
124
|
+
entry.extract(dest_file)
|
125
|
+
progressbar.advance(1)
|
126
|
+
end
|
127
|
+
|
128
|
+
progressbar.finish
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
# Extract .tar.gz archive
|
134
|
+
# based on http://dracoater.blogspot.cz/2013/10/extracting-files-from-targz-with-ruby.html
|
135
|
+
#
|
136
|
+
# Originally tar did not support paths longer than 100 chars. GNU tar is better and they
|
137
|
+
# implemented support for longer paths, but it was made through a hack called ././@LongLink.
|
138
|
+
# Shortly speaking, if you stumble upon an entry in tar archive which path equals to above
|
139
|
+
# mentioned ././@LongLink, that means that the following entry path is longer than 100 chars and
|
140
|
+
# is truncated. The full path of the following entry is actually the value of the current entry.
|
141
|
+
#
|
142
|
+
def extract_tar_gz
|
143
|
+
Gem::Package::TarReader.new(Zlib::GzipReader.open(@package)) do |tar|
|
144
|
+
|
145
|
+
# Progressbar
|
146
|
+
progressbar = TTY::ProgressBar.new(PROGRESSBAR_FORMAT, total: tar.count, frequency: 2, clear: true)
|
147
|
+
|
148
|
+
# tar.count move position pointer to end
|
149
|
+
tar.rewind
|
150
|
+
|
151
|
+
dest_file = nil
|
152
|
+
tar.each do |entry|
|
153
|
+
if entry.full_name == TAR_LONGLINK
|
154
|
+
dest_file = File.join(@temp_dir, entry.read.strip)
|
155
|
+
next
|
156
|
+
end
|
157
|
+
dest_file ||= File.join(@temp_dir, entry.full_name)
|
158
|
+
if entry.directory?
|
159
|
+
FileUtils.rm_rf(dest_file) unless File.directory?(dest_file)
|
160
|
+
FileUtils.mkdir_p(dest_file, mode: entry.header.mode, verbose: false)
|
161
|
+
elsif entry.file?
|
162
|
+
FileUtils.rm_rf(dest_file) unless File.file?(dest_file)
|
163
|
+
File.open(dest_file, 'wb') do |f|
|
164
|
+
f.write(entry.read)
|
165
|
+
end
|
166
|
+
FileUtils.chmod(entry.header.mode, dest_file, verbose: false)
|
167
|
+
elsif entry.header.typeflag == '2' # symlink
|
168
|
+
File.symlink(entry.header.linkname, dest_file)
|
169
|
+
end
|
170
|
+
|
171
|
+
dest_file = nil
|
172
|
+
progressbar.advance(1)
|
173
|
+
end
|
174
|
+
|
175
|
+
progressbar.finish
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Object
|
2
|
+
|
3
|
+
def blank?
|
4
|
+
false
|
5
|
+
end
|
6
|
+
|
7
|
+
def present?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class String
|
14
|
+
|
15
|
+
def blank?
|
16
|
+
empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
def present?
|
20
|
+
!blank?
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
class NilClass
|
26
|
+
|
27
|
+
def blank?
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
def present?
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -1,73 +1,42 @@
|
|
1
|
-
require '
|
1
|
+
require 'ostruct'
|
2
2
|
require 'yaml'
|
3
3
|
|
4
|
-
module
|
5
|
-
class Profile
|
4
|
+
module RedmineInstaller
|
5
|
+
class Profile < OpenStruct
|
6
|
+
PROFILES_FILE = File.join(Dir.home, '.redmine-installer-profiles.yml')
|
6
7
|
|
7
|
-
|
8
|
+
def self.get!(profile_id)
|
9
|
+
data = YAML.load_file(PROFILES_FILE) rescue nil
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
profile = Profile.new(task)
|
16
|
-
profile.save
|
17
|
-
|
18
|
-
say t(:your_profile_can_be_used_as, id: profile.id), 2
|
19
|
-
end
|
20
|
-
|
21
|
-
def self.load(task, id)
|
22
|
-
profile = Profile.new(task)
|
23
|
-
profile.load(id)
|
24
|
-
end
|
25
|
-
|
26
|
-
def self.check_writable
|
27
|
-
FileUtils.touch(CONFIG_FILE)
|
28
|
-
File.writable?(CONFIG_FILE)
|
11
|
+
if data.is_a?(Hash) && data.has_key?(profile_id)
|
12
|
+
Profile.new(profile_id, data[profile_id])
|
13
|
+
else
|
14
|
+
raise RedmineInstaller::ProfileError, "Profile ID=#{profile_id} does not exist"
|
15
|
+
end
|
29
16
|
end
|
30
17
|
|
31
|
-
|
32
|
-
|
33
|
-
def initialize(task)
|
34
|
-
self.task = task
|
18
|
+
attr_reader :id
|
35
19
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
# Make empty Hash if there is no profiles
|
40
|
-
@data = {} unless @data.is_a?(Hash)
|
41
|
-
end
|
42
|
-
|
43
|
-
def id
|
44
|
-
@id ||= @data.keys.map(&:to_i).max.to_i + 1
|
20
|
+
def initialize(id=nil, data={})
|
21
|
+
super(data)
|
22
|
+
@id = id
|
45
23
|
end
|
46
24
|
|
47
25
|
def save
|
48
|
-
|
49
|
-
configuration = {}
|
50
|
-
task.steps.each do |_, step|
|
51
|
-
step.save(configuration)
|
52
|
-
end
|
26
|
+
FileUtils.touch(PROFILES_FILE)
|
53
27
|
|
54
|
-
|
28
|
+
all_data = YAML.load_file(PROFILES_FILE)
|
29
|
+
all_data = {} unless all_data.is_a?(Hash)
|
55
30
|
|
56
|
-
|
57
|
-
end
|
58
|
-
|
59
|
-
def load(id)
|
60
|
-
@id = id.to_i
|
31
|
+
@id ||= all_data.keys.last.to_i + 1
|
61
32
|
|
62
|
-
|
33
|
+
all_data[@id] = to_h
|
63
34
|
|
64
|
-
|
65
|
-
|
66
|
-
task.steps.each do |_, step|
|
67
|
-
step.load(configuration)
|
68
|
-
end
|
35
|
+
File.write(PROFILES_FILE, YAML.dump(all_data))
|
69
36
|
|
70
|
-
|
37
|
+
puts "Profile was saved under ID=#{@id}"
|
38
|
+
rescue => e
|
39
|
+
puts "Profile could not be save due to #{e.message}"
|
71
40
|
end
|
72
41
|
|
73
42
|
end
|