backupgem 0.0.7 → 0.0.8
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.
- data/CHANGELOG +8 -0
- data/Rakefile +25 -2
- data/bin/backup +1 -1
- data/doc/Backup of backupgem.key/Contents/PkgInfo +1 -0
- data/doc/Backup of backupgem.key/droppedImage-2.pdf +0 -0
- data/doc/Backup of backupgem.key/droppedImage-3.pdf +0 -0
- data/doc/Backup of backupgem.key/droppedImage-5.pdf +0 -0
- data/doc/Backup of backupgem.key/droppedImage-6.pdf +0 -0
- data/doc/Backup of backupgem.key/index.apxl.gz +0 -0
- data/doc/Backup of backupgem.key/thumbs/st0.tiff +0 -0
- data/doc/Backup of backupgem.key/thumbs/st1.tiff +0 -0
- data/doc/Backup of backupgem.key/thumbs/st15.tiff +0 -0
- data/doc/Backup of backupgem.key/thumbs/st2.tiff +0 -0
- data/doc/Backup of backupgem.key/thumbs/st3-1.tiff +0 -0
- data/doc/Backup of backupgem.key/thumbs/st3-2.tiff +0 -0
- data/doc/Backup of backupgem.key/thumbs/st3-3.tiff +0 -0
- data/doc/Backup of backupgem.key/thumbs/st3-4.tiff +0 -0
- data/doc/Backup of backupgem.key/thumbs/st3-5.tiff +0 -0
- data/doc/Backup of backupgem.key/thumbs/st3-6.tiff +0 -0
- data/doc/Backup of backupgem.key/thumbs/st3-7.tiff +0 -0
- data/doc/Backup of backupgem.key/thumbs/st3.tiff +0 -0
- data/doc/Backup of backupgem.key/thumbs/st4.tiff +0 -0
- data/doc/Backup of backupgem.key/thumbs/st5.tiff +0 -0
- data/doc/Backup of backupgem.key/thumbs/st6-1.tiff +0 -0
- data/doc/Backup of backupgem.key/thumbs/st6.tiff +0 -0
- data/doc/backup_flow.graffle/Icon/r +0 -0
- data/doc/backup_flow.graffle/data.plist +320 -0
- data/doc/backup_flow.graffle/image1.ai +602 -2
- data/doc/backup_flow.graffle/image2.pict +0 -0
- data/doc/backupgem.key/Contents/PkgInfo +1 -0
- data/doc/backupgem.key/droppedImage-2.pdf +0 -0
- data/doc/backupgem.key/droppedImage-3.pdf +0 -0
- data/doc/backupgem.key/droppedImage-5.pdf +0 -0
- data/doc/backupgem.key/droppedImage-6.pdf +0 -0
- data/doc/backupgem.key/index.apxl.gz +0 -0
- data/doc/backupgem.key/thumbs/st0.tiff +0 -0
- data/doc/backupgem.key/thumbs/st1.tiff +0 -0
- data/doc/backupgem.key/thumbs/st15.tiff +0 -0
- data/doc/backupgem.key/thumbs/st2.tiff +0 -0
- data/doc/backupgem.key/thumbs/st3-1.tiff +0 -0
- data/doc/backupgem.key/thumbs/st3-2.tiff +0 -0
- data/doc/backupgem.key/thumbs/st3-3.tiff +0 -0
- data/doc/backupgem.key/thumbs/st3-4.tiff +0 -0
- data/doc/backupgem.key/thumbs/st3-5.tiff +0 -0
- data/doc/backupgem.key/thumbs/st3-6.tiff +0 -0
- data/doc/backupgem.key/thumbs/st3-7.tiff +0 -0
- data/doc/backupgem.key/thumbs/st3.tiff +0 -0
- data/doc/backupgem.key/thumbs/st4.tiff +0 -0
- data/doc/backupgem.key/thumbs/st5.tiff +0 -0
- data/doc/backupgem.key/thumbs/st6-1.tiff +0 -0
- data/doc/backupgem.key/thumbs/st6.tiff +0 -0
- data/examples/mediawiki_numeric.rb +19 -0
- data/lib/backup.rb +8 -0
- data/lib/backup/actor.rb +17 -6
- data/lib/backup/actor.rb.orig +200 -0
- data/lib/backup/cli.rb +45 -6
- data/lib/backup/recipes/standard.rb +16 -2
- data/lib/backup/rotator.rb +62 -10
- data/lib/backup/state_recorder.rb +21 -0
- data/tests/s3_test.rb +40 -0
- metadata +80 -11
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
????????
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#------------------------------------------------------------------------------
|
2
|
+
# Mediawiki Backup script
|
3
|
+
# Uses numeric rotation instead of temporal
|
4
|
+
# @author: Nate Murray <nate@natemurray.com>
|
5
|
+
#------------------------------------------------------------------------------
|
6
|
+
action(:content) do
|
7
|
+
dump = c[:tmp_dir] + "/test.sql"
|
8
|
+
sh "mysqldump -uroot test > #{dump}"
|
9
|
+
dump
|
10
|
+
end
|
11
|
+
|
12
|
+
set :rotation_mode, :numeric # base our rotation on numbers not dates
|
13
|
+
set :sons_promoted_after, 3 # upgrade to father after 3 sons
|
14
|
+
set :fathers_promoted_after, 1
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
data/lib/backup.rb
CHANGED
@@ -5,6 +5,7 @@ require 'backup/configuration'
|
|
5
5
|
require 'backup/extensions'
|
6
6
|
require 'backup/ssh_helpers'
|
7
7
|
require 'backup/date_parser'
|
8
|
+
require 'backup/state_recorder'
|
8
9
|
|
9
10
|
begin
|
10
11
|
require 'aws/s3'
|
@@ -14,3 +15,10 @@ rescue LoadError
|
|
14
15
|
# wont have access to s3 methods. It's worth noting
|
15
16
|
# at least version 1.8.4 of ruby is required for s3.
|
16
17
|
end
|
18
|
+
|
19
|
+
begin
|
20
|
+
require 'madeleine'
|
21
|
+
rescue LoadError
|
22
|
+
# If you don't have madeleine then you just cant use numeric rotation mode
|
23
|
+
::NO_NUMERIC_ROTATION = true
|
24
|
+
end
|
data/lib/backup/actor.rb
CHANGED
@@ -155,15 +155,26 @@ module Backup
|
|
155
155
|
orig = opts[:is_contents_of]
|
156
156
|
tmpdir = c[:tmp_dir] + "/tmp_" + Time.now.strftime("%Y%m%d%H%M%S") +"_#{rand}"
|
157
157
|
new_orig = tmpdir + "/" + File.basename(orig)
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
158
|
+
mkdir_p tmpdir
|
159
|
+
mkdir_p new_orig
|
160
|
+
if opts[:copy]
|
161
|
+
cp_r orig + '/.', new_orig
|
162
|
+
else
|
163
|
+
mv orig + '/.', new_orig
|
164
|
+
end
|
165
|
+
dirty_file new_orig
|
166
|
+
return new_orig
|
167
|
+
end
|
168
|
+
if opts[:is_hg_repository]
|
169
|
+
orig = opts[:is_hg_repository]
|
170
|
+
name = opts[:as] || File.basename(orig)
|
171
|
+
new_orig = c[:tmp_dir] + '/' + name
|
172
|
+
sh "hg clone #{orig} #{new_orig}"
|
162
173
|
dirty_file new_orig
|
163
174
|
return new_orig
|
164
175
|
end
|
165
176
|
raise "Unknown option in :content. Try :is_file, :is_folder " +
|
166
|
-
"or :
|
177
|
+
":is_contents_of or :is_hg_repository"
|
167
178
|
end
|
168
179
|
|
169
180
|
# Given name of a file in +string+ adds that file to @dirty_files. These
|
@@ -175,7 +186,7 @@ module Backup
|
|
175
186
|
# +cleanup+ takes every element from @dirty_files and performs an +rm -rf+ on the value
|
176
187
|
def cleanup(opts={})
|
177
188
|
dirty_files.each do |f|
|
178
|
-
|
189
|
+
rm_rf f
|
179
190
|
end
|
180
191
|
end
|
181
192
|
|
@@ -0,0 +1,200 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
module Backup
|
4
|
+
include FileUtils
|
5
|
+
# An Actor is the entity that actually does the work of determining which
|
6
|
+
# servers should be the target of a particular task, and of executing the
|
7
|
+
# task on each of them in parallel. An Actor is never instantiated
|
8
|
+
# directly--rather, you create a new Configuration instance, and access the
|
9
|
+
# new actor via Configuration#actor.
|
10
|
+
class Actor
|
11
|
+
# The configuration instance associated with this actor.
|
12
|
+
attr_reader :configuration
|
13
|
+
|
14
|
+
# Alias for #configuration
|
15
|
+
alias_method :c, :configuration
|
16
|
+
|
17
|
+
# A hash of the tasks known to this actor, keyed by name. The values are
|
18
|
+
# instances of Actor::Action.
|
19
|
+
attr_reader :action
|
20
|
+
|
21
|
+
# A stack of the results of the actions called
|
22
|
+
attr_reader :result_history
|
23
|
+
|
24
|
+
# the rotator instance
|
25
|
+
attr_reader :rotator
|
26
|
+
|
27
|
+
# Returns an Array of Strings specifying dirty files. These files will be
|
28
|
+
# +rm -rf+ when the +cleanup+ task is called.
|
29
|
+
attr_reader :dirty_files
|
30
|
+
|
31
|
+
class Action #:nodoc:
|
32
|
+
attr_reader :name, :actor, :options
|
33
|
+
|
34
|
+
def initialize(name, actor, options)
|
35
|
+
@name, @actor, @options = name, actor, options
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(config) #:nodoc:
|
40
|
+
@configuration = config
|
41
|
+
@action = {}
|
42
|
+
@result_history = []
|
43
|
+
@dirty_files = []
|
44
|
+
@rotator = Backup::Rotator.new(self)
|
45
|
+
end
|
46
|
+
|
47
|
+
# each action in the action_order is part of the chain. so you start by
|
48
|
+
# setting the output as 'nil' then you try to call before_ action, then
|
49
|
+
# store the output, then cal action with the args if action takes the args
|
50
|
+
# you are sending. if it doesnt give an intelligent error message. do this
|
51
|
+
# for all actions. then call after_action with the output if it exists.
|
52
|
+
# each time out are calilng the method with the arguemtns f the method
|
53
|
+
# exists and the method takes the arguments.
|
54
|
+
def start_process!
|
55
|
+
configuration[:action_order].each do |a|
|
56
|
+
self.send_and_store("before_" + a)
|
57
|
+
self.send_and_store(a)
|
58
|
+
self.send_and_store("after_" + a)
|
59
|
+
end
|
60
|
+
last_result
|
61
|
+
end
|
62
|
+
|
63
|
+
def send_and_store(name)
|
64
|
+
store_result self.send(name) if self.respond_to? name
|
65
|
+
end
|
66
|
+
|
67
|
+
# Define a new task for this actor. The block will be invoked when this
|
68
|
+
# task is called.
|
69
|
+
# todo, this might be more complex if the before and after tasks are going
|
70
|
+
# to be part of the input and output chain
|
71
|
+
def define_action(name, options={}, &block)
|
72
|
+
@action[name] = (options[:action_class] || Action).new(name, self, options)
|
73
|
+
|
74
|
+
if self.respond_to?(name) && !( block_given? || options[:method] )
|
75
|
+
# if it was already defined and we aren't trying to re-define it then
|
76
|
+
# what we are trying to do is define it the same way it is defined now
|
77
|
+
# only with options being sent to it.
|
78
|
+
metaclass.send(:alias_method, "old_#{name}".intern, name)
|
79
|
+
#self.class.send(:alias_method, "old_#{name}".intern, name)
|
80
|
+
#define_method("#{name.to_s}_new".intern) do
|
81
|
+
define_method(name) do
|
82
|
+
begin
|
83
|
+
result = self.send("old_#{name}", options)
|
84
|
+
end
|
85
|
+
result
|
86
|
+
end
|
87
|
+
return
|
88
|
+
end
|
89
|
+
|
90
|
+
define_method(name) do
|
91
|
+
#logger.debug "executing task #{name}"
|
92
|
+
begin
|
93
|
+
if block_given?
|
94
|
+
result = instance_eval( &block )
|
95
|
+
elsif options[:method]
|
96
|
+
#result = self.send(options[:method], options[:args])
|
97
|
+
result = self.send(options[:method])
|
98
|
+
# here we need to have a thing where we can send the arguments
|
99
|
+
# define the method 'content' so that would take the other options
|
100
|
+
# if there are options (any hash) just send along that hash. this needs more work
|
101
|
+
end
|
102
|
+
end
|
103
|
+
result
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
def metaclass
|
109
|
+
class << self; self; end
|
110
|
+
end
|
111
|
+
|
112
|
+
# rotate Actions
|
113
|
+
def via_mv; rotator.rotate_via_mv(last_result); end
|
114
|
+
def via_ssh; rotator.rotate_via_ssh(last_result); end
|
115
|
+
def via_ftp; rotator.rotate_via_ftp(last_result); end
|
116
|
+
def via_s3; rotator.rotate_via_s3(last_result); end
|
117
|
+
|
118
|
+
# By default, +:content+ can perform one of three actions
|
119
|
+
# * +:is_file+
|
120
|
+
# * +:is_folder+
|
121
|
+
# + +:is_contents_of+
|
122
|
+
#
|
123
|
+
# Examples:
|
124
|
+
# action :content, :is_file => "/path/to/file" # content is a single file
|
125
|
+
# action :content, :is_folder => "/path/to/folder" # content is the folder itself
|
126
|
+
# action :content, :is_contents_of => "/path/to/other/folder" # files in folder/
|
127
|
+
#
|
128
|
+
# +:is_file+ and +:is_folder+ are basically the same thing in that they
|
129
|
+
# backup the whole file/folder whereas +:is_contents_of+ backs up the
|
130
|
+
# <em>contents</em> of the given folder.
|
131
|
+
#
|
132
|
+
# Note that +:is_contents_of+ performs a very spcific action:
|
133
|
+
# * a temporary directory is created
|
134
|
+
# * all of the files are moved (including subdirectories) into the temporary directory
|
135
|
+
# * the archive is created from the temporary directory
|
136
|
+
#
|
137
|
+
# If you wish to copy the files out of the original directory instead of
|
138
|
+
# moving them. Then you may specify the +copy+ option passing a +true+
|
139
|
+
# value like so:
|
140
|
+
#
|
141
|
+
# action :content, :is_contents_of => "/path/to/other/folder",
|
142
|
+
# :copy => true
|
143
|
+
#
|
144
|
+
# This will copy recursively.
|
145
|
+
#
|
146
|
+
# If this is not your desired behavior then you can easily write your own.
|
147
|
+
# Also, these options only work for local files. If you are getting the
|
148
|
+
# files from a foreign server you will have to write a custom +:content+
|
149
|
+
# method.
|
150
|
+
#
|
151
|
+
def content(opts={})
|
152
|
+
return opts[:is_file] if opts[:is_file]
|
153
|
+
return opts[:is_folder] if opts[:is_folder]
|
154
|
+
if opts[:is_contents_of]
|
155
|
+
orig = opts[:is_contents_of]
|
156
|
+
tmpdir = c[:tmp_dir] + "/tmp_" + Time.now.strftime("%Y%m%d%H%M%S") +"_#{rand}"
|
157
|
+
new_orig = tmpdir + "/" + File.basename(orig)
|
158
|
+
mkdir_p tmpdir
|
159
|
+
mkdir_p new_orig
|
160
|
+
if opts[:copy]
|
161
|
+
cp_r orig + '/.', new_orig
|
162
|
+
else
|
163
|
+
mv orig + '/.', new_orig
|
164
|
+
end
|
165
|
+
dirty_file new_orig
|
166
|
+
return new_orig
|
167
|
+
end
|
168
|
+
raise "Unknown option in :content. Try :is_file, :is_folder " +
|
169
|
+
"or :is_contents_of"
|
170
|
+
end
|
171
|
+
|
172
|
+
# Given name of a file in +string+ adds that file to @dirty_files. These
|
173
|
+
# files will be removed when the +cleanup+ task is called.
|
174
|
+
def dirty_file(string)
|
175
|
+
@dirty_files << string
|
176
|
+
end
|
177
|
+
|
178
|
+
# +cleanup+ takes every element from @dirty_files and performs an +rm -rf+ on the value
|
179
|
+
def cleanup(opts={})
|
180
|
+
dirty_files.each do |f|
|
181
|
+
rm_rf f
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
def define_method(name, &block)
|
187
|
+
metaclass.send(:define_method, name, &block)
|
188
|
+
end
|
189
|
+
|
190
|
+
def store_result(result)
|
191
|
+
@result_history.push result
|
192
|
+
end
|
193
|
+
|
194
|
+
def last_result
|
195
|
+
@result_history.last
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
data/lib/backup/cli.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'optparse'
|
2
2
|
require 'backup'
|
3
|
+
require 'yaml'
|
3
4
|
|
4
5
|
module Backup
|
5
6
|
# The CLI class encapsulates the behavior of backup when it is invoked
|
@@ -24,15 +25,14 @@ module Backup
|
|
24
25
|
:global => nil }
|
25
26
|
|
26
27
|
OptionParser.new do |opts|
|
27
|
-
opts.banner = "Usage: #{$0} [options]
|
28
|
+
opts.banner = "Usage: #{$0} [options]"
|
28
29
|
|
29
30
|
opts.separator ""
|
30
31
|
opts.separator "Recipe Options -----------------------"
|
31
32
|
opts.separator ""
|
32
33
|
|
33
|
-
opts.on("-r", "--recipe RECIPE",
|
34
|
-
"A recipe file to load. Multiple recipes
|
35
|
-
"be specified, and are loaded in the given order."
|
34
|
+
opts.on("-r", "--recipe RECIPE ",
|
35
|
+
"A recipe file to load. Multiple recipes is DEPRECATED and not fully functional."
|
36
36
|
) { |value| @options[:recipes] << value }
|
37
37
|
|
38
38
|
opts.on("-s", "--set NAME=VALUE",
|
@@ -49,6 +49,10 @@ module Backup
|
|
49
49
|
"file +global.rb+ in the same directory."
|
50
50
|
) { |value| @options[:recipes] << value }
|
51
51
|
|
52
|
+
opts.on("-q", "--quiet",
|
53
|
+
"suppresses much of the output of backup, except",
|
54
|
+
"for error messages") { verbose(false) }
|
55
|
+
|
52
56
|
if args.empty?
|
53
57
|
puts opts
|
54
58
|
exit
|
@@ -74,7 +78,7 @@ module Backup
|
|
74
78
|
|
75
79
|
private
|
76
80
|
def check_options!
|
77
|
-
#
|
81
|
+
# perform a sanity check
|
78
82
|
end
|
79
83
|
|
80
84
|
# Load the recipes specified by the options, and execute the actions
|
@@ -91,7 +95,14 @@ module Backup
|
|
91
95
|
global = options[:global] || File.dirname(recipe) + "/global.rb"
|
92
96
|
config.load global if File.exists? global # cache this?
|
93
97
|
end
|
94
|
-
|
98
|
+
|
99
|
+
options[:recipes].each_with_index do |recipe,i|
|
100
|
+
config.load(recipe)
|
101
|
+
$state = setup_saved_state(recipe, config)
|
102
|
+
warn "DEPRICATED: Using multiple recipes with one command is deprecated for the time being. Just run a different command if you want to do two recipes at the same time" if i > 0
|
103
|
+
|
104
|
+
end
|
105
|
+
|
95
106
|
#options[:vars].each { |name, value| config.set(name, value) }
|
96
107
|
|
97
108
|
actor = config.actor
|
@@ -100,6 +111,34 @@ module Backup
|
|
100
111
|
#options[:actions].each { |action| actor.send action }
|
101
112
|
end
|
102
113
|
|
114
|
+
|
115
|
+
# Setup the persistant state using madeline
|
116
|
+
def setup_saved_state(recipe, config)
|
117
|
+
if defined? ::NO_NUMERIC_ROTATION
|
118
|
+
if :numeric == config[:rotation_mode]
|
119
|
+
puts "Missing Gem: :numeric :rotation mode is not valid unless you have madeleine installed. try: 'gem install madeleine'"
|
120
|
+
exit 1
|
121
|
+
end
|
122
|
+
return nil
|
123
|
+
end
|
124
|
+
|
125
|
+
saved_state_folder = config[:saved_state_folder] || File.join(config[:backup_path], ".backupgem_#{File.basename(recipe)}_state")
|
126
|
+
|
127
|
+
state = SnapshotMadeleine.new(saved_state_folder, YAML) do
|
128
|
+
StateRecorder.new
|
129
|
+
end
|
130
|
+
state.system.saved_state_folder = saved_state_folder
|
131
|
+
state
|
132
|
+
end
|
133
|
+
|
103
134
|
end
|
104
135
|
end
|
105
136
|
|
137
|
+
at_exit {
|
138
|
+
|
139
|
+
if !defined?(::NO_NUMERIC_ROTATION) && defined?($state)
|
140
|
+
$state.take_snapshot
|
141
|
+
$state.system.cleanup_snapshots
|
142
|
+
end
|
143
|
+
}
|
144
|
+
|
@@ -38,6 +38,13 @@ set :gpg_encrypt_options, ""
|
|
38
38
|
# Read more about that below
|
39
39
|
set :rotation_method, :gfs
|
40
40
|
|
41
|
+
# rotation mode - temporal or numeric. For instance
|
42
|
+
# temporal mode would continue to be the default and work with
|
43
|
+
# :son_promoted_on. The promotions are based on days. This works well for 1 backup per day.
|
44
|
+
# numeric works by promoting after every number of creations. This is better for multiple backups per day.
|
45
|
+
# numeric mode uses :sons_promoted_after
|
46
|
+
set :rotation_mode, :temporal
|
47
|
+
|
41
48
|
# :mon-sun
|
42
49
|
# :last_day_of_the_month # whatever son_promoted on son was, but the last of the month
|
43
50
|
# everything else you can define with a Runt object
|
@@ -57,13 +64,19 @@ set :sons_to_keep, 14
|
|
57
64
|
set :fathers_to_keep, 6
|
58
65
|
set :grandfathers_to_keep, 6 # 6 months, by default
|
59
66
|
|
67
|
+
# These options are only used if :rotation_mode is :numeric.
|
68
|
+
# This is better if you are doing multiple backups per day.
|
69
|
+
# This setting says that every 14th son will be promoted to a father.
|
70
|
+
set :sons_promoted_after, 14
|
71
|
+
set :fathers_promoted_after, 6
|
60
72
|
|
61
73
|
# -------------------------
|
62
74
|
# Standard Actions
|
63
75
|
# -------------------------
|
64
76
|
action(:tar_bz2) do
|
65
77
|
name = c[:tmp_dir] + "/" + File.basename(last_result) + ".tar.bz2"
|
66
|
-
|
78
|
+
v = "v" if verbose
|
79
|
+
sh "tar -c#{v}jf #{name} #{last_result}"
|
67
80
|
name
|
68
81
|
end
|
69
82
|
|
@@ -78,7 +91,8 @@ action(:scp) do
|
|
78
91
|
end
|
79
92
|
|
80
93
|
action(:mv) do
|
81
|
-
|
94
|
+
move last_result, c[:backup_path] # has to be move (not mv) to avoid infinite
|
95
|
+
# recursion
|
82
96
|
c[:backup_path] + "/" + File.basename(last_result)
|
83
97
|
end
|
84
98
|
|