Module: Xolo::Server::Mixins::Changelog
Overview
This is mixed in to Xolo::Server::Title and Xolo::Server::Version, for simplified access to a title’s changelog
Each title has a changelog file that records changes to the title and its versions.
The changelog file is a ‘jsonlines’ file, which is a JSON file containing a single JSON object per line. See jsonlines.org/ for more info. The reason for using jsonlines is that it is easy to append to the file, rather than having to read the whole file into memory, parse it, add a new entry, and write it back.
In this case, each line is a JSON object (ruby Hash) representing a change or an action.
The keys in the hash are:
:time - the time the change was made
:admin - the admin who made the change
:host - the hostname or IP address of the admin
:version - the version number, or nil if the change is to the title
:attrib - the attribute name, or nil if the change is an action
:old - the original value, or nil if the change is an action
:new - the new value, or nil if the change is an action
:action - a description of the action, or nil if the change is to an attribute
The changelog file is stored in the title directory in a file named ‘changelog.json’. The file exists for as long as the title exists. It is backed up when before every event logged to it, in the backup directory in the server’s BACKUPS_DIR.
When a title is deleted, its changelog file is moved to a backup directory before the title directory is deleted, and will remain there until manually removed.
Constant Summary collapse
- TITLE_CHANGELOG_FILENAME =
The change log filename
'changelog.jsonl'
Class Method Summary collapse
-
.backup_file_dir ⇒ Object
When a title is deleted, its changelog is moved to this directory and renamed to ‘<title>_changelog.json’ This is so that the changelog can be accessed after the title is deleted.
-
.changelog_locks ⇒ Concurrent::Hash
A hash of the read-write locks for each title’s changelog file The key is the title name, the value is the Concurrent::ReentrantReadWriteLock instance for that title.
-
.included(includer) ⇒ Object
when this module is included.
Instance Method Summary collapse
-
#backup_changelog ⇒ void
Copy the changelog file to the backup directory.
-
#changelog ⇒ Array<Hash>
the change log for a title.
-
#changelog_backup_file ⇒ Pathname
The path to the backup file for this title’s changelog.
-
#changelog_file ⇒ Pathname
the change log file for a title.
-
#changelog_lock ⇒ Concurrent::ReentrantReadWriteLock
the read-write lock for a title’s changelog file.
-
#delete_changelog ⇒ void
when a title is deleted, make a final entry, then move its changelog to the backup directory.
-
#hostname_from_ip(ip) ⇒ String
get a hostname from an IP address if possible.
-
#log_change(attrib: nil, old_val: nil, new_val: nil, msg: nil) ⇒ void
Log a change by adding an entry to the changelog file for a title or one of its versions.
-
#log_update_changes ⇒ void
Record all changes during an update of a title or version.
-
#note_changes_for_update_and_log ⇒ Hash
At the start of an update, populate the hash for the @changes_for_update attribute with the changes being made.
Class Method Details
.backup_file_dir ⇒ Object
When a title is deleted, its changelog is moved to this directory and renamed to ‘<title>_changelog.json’ This is so that the changelog can be accessed after the title is deleted.
69 70 71 |
# File 'lib/xolo/server/mixins/changelog.rb', line 69 def self.backup_file_dir @backup_file_dir ||= Xolo::Server::BACKUPS_DIR + 'changelogs' end |
.changelog_locks ⇒ Concurrent::Hash
A hash of the read-write locks for each title’s changelog file The key is the title name, the value is the Concurrent::ReentrantReadWriteLock instance for that title.
Titles and versions use these locks to ensure that only one thread at a time can write to a title’s changelog file.
81 82 83 |
# File 'lib/xolo/server/mixins/changelog.rb', line 81 def self.changelog_locks @changelog_locks ||= Concurrent::Hash.new end |
.included(includer) ⇒ Object
when this module is included
61 62 63 |
# File 'lib/xolo/server/mixins/changelog.rb', line 61 def self.included(includer) Xolo.verbose_include includer, self end |
Instance Method Details
#backup_changelog ⇒ void
This method returns an undefined value.
Copy the changelog file to the backup directory
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/xolo/server/mixins/changelog.rb', line 143 def backup_changelog return unless changelog_file.exist? unless changelog_backup_file.dirname.exist? log_debug 'Creating backup directory for changelogs' changelog_backup_file.dirname.mkpath end log_debug "Backing up changelog for #{title}" if changelog_backup_file.exist? # if deleting the whole title # move aside any previously existing one, appending a timestamp if self.class == Xolo::Server::Title && deleting? changelog_backup_file.rename "#{changelog_backup_file.basename}.#{changelog_backup_file.mtime.strftime('%Y%m%d%H%M%S')}" # otherwise, overwrite the current backup else changelog_backup_file.delete end end changelog_file.pix_cp changelog_backup_file end |
#changelog ⇒ Array<Hash>
the change log for a title
125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/xolo/server/mixins/changelog.rb', line 125 def changelog log_debug "Reading changelog for #{title}" changelog_data = [] if changelog_file.exist? changelog_lock.with_read_lock do changelog_file.read.lines.each { |l| changelog_data << JSON.parse(l, symbolize_names: true) } end end # reverse the order so the most recent change is first changelog_data.reverse end |
#changelog_backup_file ⇒ Pathname
Returns the path to the backup file for this title’s changelog.
101 102 103 |
# File 'lib/xolo/server/mixins/changelog.rb', line 101 def changelog_backup_file @changelog_backup_file ||= Xolo::Server::Mixins::Changelog.backup_file_dir + "#{title}-#{TITLE_CHANGELOG_FILENAME}" end |
#changelog_file ⇒ Pathname
the change log file for a title
95 96 97 |
# File 'lib/xolo/server/mixins/changelog.rb', line 95 def changelog_file @changelog_file ||= Xolo::Server::Title.title_dir(title) + TITLE_CHANGELOG_FILENAME end |
#changelog_lock ⇒ Concurrent::ReentrantReadWriteLock
the read-write lock for a title’s changelog file
111 112 113 114 115 116 117 118 119 |
# File 'lib/xolo/server/mixins/changelog.rb', line 111 def changelog_lock @changelog_lock ||= if Xolo::Server::Mixins::Changelog.changelog_locks[title] Xolo::Server::Mixins::Changelog.changelog_locks[title] else log_debug "Creating changelog lock for #{title}" Xolo::Server::Mixins::Changelog.changelog_locks[title] = Concurrent::ReentrantReadWriteLock.new end end |
#delete_changelog ⇒ void
This method returns an undefined value.
when a title is deleted, make a final entry, then move its changelog to the backup directory
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 |
# File 'lib/xolo/server/mixins/changelog.rb', line 288 def delete_changelog change = { time: Time.now, admin: session[:admin], host: hostname_from_ip(server_app_instance.request.ip), version: nil, action: 'Title Deleted', attrib: nil, old: nil, new: nil } changelog_lock.with_write_lock do changelog_file.pix_append "#{change.to_json}\n" # final backup changelog_backup_file.delete if changelog_backup_file.exist? changelog_file.rename changelog_backup_file end end |
#hostname_from_ip(ip) ⇒ String
get a hostname from an IP address if possible
218 219 220 221 222 223 224 225 226 227 |
# File 'lib/xolo/server/mixins/changelog.rb', line 218 def hostname_from_ip(ip) # gethostbbaddr is deprecated, so use Resolv instead # host = Socket.gethostbyaddr(ip.split('.').map(&:to_i).pack('CCCC')).first host = Resolv.getname(ip) host.pix_empty? ? ip : host rescue Resolv::ResolvError ip end |
#log_change(attrib: nil, old_val: nil, new_val: nil, msg: nil) ⇒ void
This method returns an undefined value.
Log a change by adding an entry to the changelog file for a title or one of its versions.
The entry may be for an message, such as ‘Title Created’, or for a change to the value of an attribute.
Provide either a message to log with :msg, or the name of an attribute being changed, with :attrib, and either :old_val, :new_val, or both. (either can be omitted or set to nil, when adding or removing the attribute)
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/xolo/server/mixins/changelog.rb', line 186 def log_change(attrib: nil, old_val: nil, new_val: nil, msg: nil) raise ArgumentError, 'Must provide attrib: or action:' if !msg && !attrib raise ArgumentError, 'Must provide old: or new: or both with attrib:' if attrib && !old_val && !new_val # if action, attrib, old, and new are ignored attrib, old_val, new_val = nil if msg change = { time: Time.now, admin: session[:admin], host: hostname_from_ip(server_app_instance.request.ip), version: respond_to?(:version) ? version : nil, msg: msg, attrib: attrib, old: old_val, new: new_val } log_debug "Writing to changelog for #{title}" changelog_lock.with_write_lock do backup_changelog changelog_file.pix_append "#{change.to_json}\n" end end |
#log_update_changes ⇒ void
This method returns an undefined value.
Record all changes during an update of a title or version
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/xolo/server/mixins/changelog.rb', line 262 def log_update_changes return unless changes_for_update # self.class::ATTRIBUTES.each do |attr, deets| # next unless deets[:changelog] # new_val = deets[:type] == :time ? Time.parse(new_data_for_update[attr]) : new_data_for_update[attr] # old_val = send attr # new_val = "'#{new_val.sort.join("', '")}'" if new_val.is_a? Array # old_val = "'#{old_val.sort.join("', '")}'" if old_val.is_a? Array # next if new_val == old_val # log_change attrib: attr, old_val: old_val, new_val: new_val # end changes_for_update.each do |attr, vals| log_change attrib: attr, old_val: vals[:old], new_val: vals[:new] end end |
#note_changes_for_update_and_log ⇒ Hash
At the start of an update, populate the hash for the @changes_for_update attribute with the changes being made.
This is run at the start of the update process, and
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/xolo/server/mixins/changelog.rb', line 235 def note_changes_for_update_and_log return unless new_data_for_update changes = {} self.class::ATTRIBUTES.each do |attr, deets| next unless deets[:changelog] new_val = deets[:type] == :time ? Time.parse(new_data_for_update[attr]) : new_data_for_update[attr] old_val = send attr # Don't change arrays to strings! # just sort them to compare new_val_to_compare = new_val.is_a?(Array) ? new_val.sort : new_val old_val_to_compare = old_val.is_a?(Array) ? old_val.sort : old_val next if new_val_to_compare == old_val_to_compare changes[attr] = { old: old_val, new: new_val } end changes end |