diff --git a/gems/gitlab-backup-cli/lib/gitlab/backup/cli/backup_metadata.rb b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/backup_metadata.rb index e3ef81f8b419ef92ad7785638d82a211339bf07c..ad57ee68709fbbd017315c78427361a7f0d9bb22 100644 --- a/gems/gitlab-backup-cli/lib/gitlab/backup/cli/backup_metadata.rb +++ b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/backup_metadata.rb @@ -22,13 +22,25 @@ class BackupMetadata # List all of the current top level fields along with their expected data # type name to specify how to parse and serialize values - CURRENT_FIELD_DEFINITIONS = { + METADATA_SCHEMA = { + metadata_version: :integer, backup_id: :string, created_at: :time, - gitlab_version: :string, - metadata_version: :integer + gitlab_version: :string }.freeze + # Options used by the JSON parser + JSON_PARSE_OPTIONS = { + max_nesting: 1, + allow_nan: false, + symbolize_names: true + }.freeze + + # Integer representing the increment of metadata version + # this data was saved with + # @return [Integer] + attr_reader :metadata_version + # Unique ID for a backup # @return [String] attr_reader :backup_id @@ -41,29 +53,16 @@ class BackupMetadata # @return [String] attr_reader :gitlab_version - # Integer representing the increment of metadata version - # this data was saved with - # @return [Integer] - attr_reader :metadata_version - def initialize( created_at:, backup_id:, gitlab_version:, metadata_version: METADATA_VERSION ) - @created_at = created_at + @metadata_version = metadata_version @backup_id = backup_id + @created_at = created_at @gitlab_version = gitlab_version - @metadata_version = metadata_version - end - - # Returns the expected metadata file path inside directory +backup_directory+ - # - # @param [String|Pathname] backup_directory - # @return [Pathname] - def self.path_in_backup_directory(backup_directory) - Pathname(backup_directory).join(METADATA_FILENAME) end # Build a new BackupMetadata with defaults @@ -76,44 +75,41 @@ def self.build(gitlab_version:) new( backup_id: backup_id, created_at: created_at, - gitlab_version: gitlab_version, - metadata_version: METADATA_VERSION + gitlab_version: gitlab_version ) end # Load the metadata stored in the given JSON file path # - # @param [String] metadata_json_file + # @param [String] basepath # @return [Gitlab::Backup::Cli::BackupMetadata] - def self.read(metadata_json_file) - # should be a hash of string keys mapped to various JSON primitives - saved_data_hash = JSON.parse(File.read(metadata_json_file.to_path)) + def self.load!(basepath) + basepath = Pathname(basepath) unless basepath.is_a? Pathname + + json_file = basepath.join(METADATA_FILENAME) + json = JSON.parse(File.read(json_file), JSON_PARSE_OPTIONS) parsed_fields = {} - CURRENT_FIELD_DEFINITIONS.each do |key, data_type| - stored_value = saved_data_hash[key.to_s] + METADATA_SCHEMA.each do |key, data_type| + stored_value = json[key] parsed_value = parse_value(type: data_type, value: stored_value) parsed_fields[key] = parsed_value end new(**parsed_fields) - rescue IOError, Errno::ENOENT => error + rescue IOError, Errno::ENOENT => e Gitlab::Backup::Cli::Output.error( - "Failed to write backup information to: #{metadata_json_file} (#{error.message})" + "Failed to load backup information from: #{json_file} (#{e.message})" ) nil end - def self.read_from_backup_directory(backup_directory) - read(path_in_backup_directory(backup_directory)) - end - # Expose the information that will be part of the Metadata JSON file def to_hash - CURRENT_FIELD_DEFINITIONS.each_with_object({}) do |(key, type), hash| + METADATA_SCHEMA.each_with_object({}) do |(key, type), hash| # fetch attribute value dynamically - value = self.send(key) + value = public_send(key) serialized_value = serialize_value(type: type, value: value) hash[key] = serialized_value end @@ -122,7 +118,7 @@ def to_hash def write!(basepath) basepath = Pathname(basepath) unless basepath.is_a? Pathname - json_file = self.class.path_in_backup_directory(basepath) + json_file = basepath.join(METADATA_FILENAME) json = JSON.pretty_generate(to_hash) json_file.open(File::RDWR | File::CREAT, METADATA_FILE_MODE) do |file| diff --git a/gems/gitlab-backup-cli/spec/gitlab/backup/cli/backup_metadata_spec.rb b/gems/gitlab-backup-cli/spec/gitlab/backup/cli/backup_metadata_spec.rb index 371d8f503c9f024eac55d28bd82d5039f781685e..edde365c4b448e3565e51ea35df88c9cca8431f8 100644 --- a/gems/gitlab-backup-cli/spec/gitlab/backup/cli/backup_metadata_spec.rb +++ b/gems/gitlab-backup-cli/spec/gitlab/backup/cli/backup_metadata_spec.rb @@ -6,10 +6,10 @@ subject(:metadata) { build(:backup_metadata) } let(:expected_keys) { %i[metadata_version created_at backup_id gitlab_version] } - let(:tmp_dir) { Pathname(Dir.mktmpdir('backup-metadata', temp_path)) } + let(:basedir) { Pathname(Dir.mktmpdir('backup-metadata', temp_path)) } after do - tmp_dir.rmtree + basedir.rmtree end describe '#to_hash' do @@ -37,8 +37,8 @@ end end - describe '.read(json_metadata_path)' do - let(:json_file) { tmp_dir.join(described_class::METADATA_FILENAME) } + describe '.load!' do + let(:json_file) { basedir.join(described_class::METADATA_FILENAME) } before do json_file.write(<<~JSON) @@ -52,7 +52,7 @@ end it 'loads the JSON attributes into a metadata instance' do - metadata = described_class.read(json_file) + metadata = described_class.load!(basedir) timestamp = Time.parse("2024-05-05T00:00:00Z") expect(metadata.metadata_version).to eq(2) @@ -61,87 +61,35 @@ expect(metadata.gitlab_version).to eq("17.0.0-pre") end - context "when json_metadata_path does not exist", :silence_output do + context "when metadata file does not exist", :silence_output do before do # delete the json_file we setup in surrounding blocks json_file.unlink end it "does not raise an error and returns nil" do - metadata = described_class.read(json_file) - expect(metadata).to be_nil - end - - it "outputs an error message to stderr" do - expect { - described_class.read(json_file) - }.to output(/Failed to write backup information/).to_stderr - end - - end - end - - describe '.read_from_backup_directory(backup_directory)' do - let(:json_file) { tmp_dir.join(described_class::METADATA_FILENAME) } - - before do - json_file.write(<<~JSON) - { - "metadata_version": 2, - "backup_id": "1714868860_2024_05_05_17.0.0-pre", - "created_at": "2024-05-05T00:00:00Z", - "gitlab_version": "17.0.0-pre" - } - JSON - end - - it 'loads the JSON attributes into a metadata instance' do - metadata = described_class.read_from_backup_directory(tmp_dir) - timestamp = Time.parse("2024-05-05T00:00:00Z") - - expect(metadata.metadata_version).to eq(2) - expect(metadata.backup_id).to eq("1714868860_2024_05_05_17.0.0-pre") - expect(metadata.created_at).to eq(timestamp) - expect(metadata.gitlab_version).to eq("17.0.0-pre") - end + metadata = described_class.load!(basedir) - context "when json_metadata_path does not exist", :silence_output do - before do - # delete the json_file we setup in surrounding blocks - json_file.unlink - end - - it "does not raise an error and returns nil" do - metadata = described_class.read_from_backup_directory(tmp_dir) expect(metadata).to be_nil end it "outputs an error message to stderr" do - expect { - described_class.read_from_backup_directory(tmp_dir) - }.to output(/Failed to write backup information/).to_stderr + expect { described_class.load!(basedir) }.to output(/Failed to load backup information/).to_stderr end end end - describe '.path_in_backup_directory(backup_directory)' do - it 'returns the expected metadata file path inside the given directory' do - metadata_path = described_class.path_in_backup_directory(tmp_dir) - expect(metadata_path).to eq(tmp_dir.join('backup_information.json')) - end - end - describe '#write!' do - let(:json_file) { tmp_dir.join(described_class::METADATA_FILENAME) } + let(:json_file) { basedir.join(described_class::METADATA_FILENAME) } it 'creates a file with specific permissions' do - expect { metadata.write!(tmp_dir) }.to change { json_file.exist? }.from(false).to(true) + expect { metadata.write!(basedir) }.to change { json_file.exist? }.from(false).to(true) permissions_octal = json_file.lstat.mode % 0o1000 expect(permissions_octal).to eq(0o600) end it 'writes metadata information to a file' do - metadata.write!(tmp_dir) + metadata.write!(basedir) json_content = File.read(json_file) expect(json_content).not_to be_empty @@ -151,7 +99,7 @@ end context 'when basepath doesnt exist', :silence_output do - let(:nonexistentpath) { tmp_dir.join('nonexistentfolder') } + let(:nonexistentpath) { basedir.join('nonexistentfolder') } it 'doesnt raise an error' do expect { metadata.write!(nonexistentpath) }.not_to raise_error