Extract a class that represents artifacts file path
This commit is contained in:
		
							parent
							
								
									dfb8fcbb65
								
							
						
					
					
						commit
						c53f319f88
					
				| 
						 | 
					@ -1,128 +1,128 @@
 | 
				
			||||||
module Gitlab
 | 
					module Gitlab
 | 
				
			||||||
  module Ci::Build::Artifacts
 | 
					  module Ci
 | 
				
			||||||
    class Metadata
 | 
					    module Build
 | 
				
			||||||
      ##
 | 
					      module Artifacts
 | 
				
			||||||
      # Class that represents an entry (path and metadata) to a file or
 | 
					        class Metadata
 | 
				
			||||||
      # directory in GitLab CI Build Artifacts binary file / archive
 | 
					          ##
 | 
				
			||||||
      #
 | 
					          # Class that represents an entry (path and metadata) to a file or
 | 
				
			||||||
      # This is IO-operations safe class, that does similar job to
 | 
					          # directory in GitLab CI Build Artifacts binary file / archive
 | 
				
			||||||
      # Ruby's Pathname but without the risk of accessing filesystem.
 | 
					          #
 | 
				
			||||||
      #
 | 
					          # This is IO-operations safe class, that does similar job to
 | 
				
			||||||
      # This class is working only with UTF-8 encoded paths.
 | 
					          # Ruby's Pathname but without the risk of accessing filesystem.
 | 
				
			||||||
      #
 | 
					          #
 | 
				
			||||||
      class Entry
 | 
					          # This class is working only with UTF-8 encoded paths.
 | 
				
			||||||
        attr_reader :path, :entries
 | 
					          #
 | 
				
			||||||
        attr_accessor :name
 | 
					          class Entry
 | 
				
			||||||
 | 
					            attr_reader :entries
 | 
				
			||||||
 | 
					            attr_accessor :name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        def initialize(path, entries)
 | 
					            def initialize(path, entries)
 | 
				
			||||||
          @path = path.dup.force_encoding('UTF-8')
 | 
					              @entries = entries
 | 
				
			||||||
          @entries = entries
 | 
					              @path = Artifacts::Path.new(path)
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if path.include?("\0")
 | 
					            delegate :empty?, to: :children
 | 
				
			||||||
            raise ArgumentError, 'Path contains zero byte character!'
 | 
					
 | 
				
			||||||
 | 
					            def directory?
 | 
				
			||||||
 | 
					              blank_node? || @path.directory?
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def file?
 | 
				
			||||||
 | 
					              !directory?
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def blob
 | 
				
			||||||
 | 
					              return unless file?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              @blob ||= Blob.decorate(::Ci::ArtifactBlob.new(self), nil)
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def has_parent?
 | 
				
			||||||
 | 
					              nodes > 0
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def parent
 | 
				
			||||||
 | 
					              return nil unless has_parent?
 | 
				
			||||||
 | 
					              self.class.new(@path.to_s.chomp(basename), @entries)
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def basename
 | 
				
			||||||
 | 
					              (directory? && !blank_node?) ? name + '/' : name
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def name
 | 
				
			||||||
 | 
					              @name || @path.name
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def children
 | 
				
			||||||
 | 
					              return [] unless directory?
 | 
				
			||||||
 | 
					              return @children if @children
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              child_pattern = %r{^#{Regexp.escape(@path.to_s)}[^/]+/?$}
 | 
				
			||||||
 | 
					              @children = select_entries { |path| path =~ child_pattern }
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def directories(opts = {})
 | 
				
			||||||
 | 
					              return [] unless directory?
 | 
				
			||||||
 | 
					              dirs = children.select(&:directory?)
 | 
				
			||||||
 | 
					              return dirs unless has_parent? && opts[:parent]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              dotted_parent = parent
 | 
				
			||||||
 | 
					              dotted_parent.name = '..'
 | 
				
			||||||
 | 
					              dirs.prepend(dotted_parent)
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def files
 | 
				
			||||||
 | 
					              return [] unless directory?
 | 
				
			||||||
 | 
					              children.select(&:file?)
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def metadata
 | 
				
			||||||
 | 
					              @entries[@path.to_s] || {}
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def nodes
 | 
				
			||||||
 | 
					              @path.nodes + (file? ? 1 : 0)
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def blank_node?
 | 
				
			||||||
 | 
					              @path.to_s.empty? # "" is considered to be './'
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def exists?
 | 
				
			||||||
 | 
					              blank_node? || @entries.include?(@path.to_s)
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def total_size
 | 
				
			||||||
 | 
					              descendant_pattern = %r{^#{Regexp.escape(@path.to_s)}}
 | 
				
			||||||
 | 
					              entries.sum do |path, entry|
 | 
				
			||||||
 | 
					                (entry[:size] if path =~ descendant_pattern).to_i
 | 
				
			||||||
 | 
					              end
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def path
 | 
				
			||||||
 | 
					              @path.to_s
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def to_s
 | 
				
			||||||
 | 
					              @path.to_s
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def ==(other)
 | 
				
			||||||
 | 
					              path == other.path && @entries == other.entries
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def inspect
 | 
				
			||||||
 | 
					              "#{self.class.name}: #{self.to_s}"
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def select_entries
 | 
				
			||||||
 | 
					              selected = @entries.select { |path, _metadata| yield path }
 | 
				
			||||||
 | 
					              selected.map { |path, _metadata| self.class.new(path, @entries) }
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
 | 
					 | 
				
			||||||
          unless path.valid_encoding?
 | 
					 | 
				
			||||||
            raise ArgumentError, 'Path contains non-UTF-8 byte sequence!'
 | 
					 | 
				
			||||||
          end
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        delegate :empty?, to: :children
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def directory?
 | 
					 | 
				
			||||||
          blank_node? || @path.end_with?('/')
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def file?
 | 
					 | 
				
			||||||
          !directory?
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def blob
 | 
					 | 
				
			||||||
          return unless file?
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          @blob ||= Blob.decorate(::Ci::ArtifactBlob.new(self), nil)
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def has_parent?
 | 
					 | 
				
			||||||
          nodes > 0
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def parent
 | 
					 | 
				
			||||||
          return nil unless has_parent?
 | 
					 | 
				
			||||||
          self.class.new(@path.chomp(basename), @entries)
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def basename
 | 
					 | 
				
			||||||
          (directory? && !blank_node?) ? name + '/' : name
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def name
 | 
					 | 
				
			||||||
          @name || @path.split('/').last.to_s
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def children
 | 
					 | 
				
			||||||
          return [] unless directory?
 | 
					 | 
				
			||||||
          return @children if @children
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          child_pattern = %r{^#{Regexp.escape(@path)}[^/]+/?$}
 | 
					 | 
				
			||||||
          @children = select_entries { |path| path =~ child_pattern }
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def directories(opts = {})
 | 
					 | 
				
			||||||
          return [] unless directory?
 | 
					 | 
				
			||||||
          dirs = children.select(&:directory?)
 | 
					 | 
				
			||||||
          return dirs unless has_parent? && opts[:parent]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          dotted_parent = parent
 | 
					 | 
				
			||||||
          dotted_parent.name = '..'
 | 
					 | 
				
			||||||
          dirs.prepend(dotted_parent)
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def files
 | 
					 | 
				
			||||||
          return [] unless directory?
 | 
					 | 
				
			||||||
          children.select(&:file?)
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def metadata
 | 
					 | 
				
			||||||
          @entries[@path] || {}
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def nodes
 | 
					 | 
				
			||||||
          @path.count('/') + (file? ? 1 : 0)
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def blank_node?
 | 
					 | 
				
			||||||
          @path.empty? # "" is considered to be './'
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def exists?
 | 
					 | 
				
			||||||
          blank_node? || @entries.include?(@path)
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def total_size
 | 
					 | 
				
			||||||
          descendant_pattern = %r{^#{Regexp.escape(@path)}}
 | 
					 | 
				
			||||||
          entries.sum do |path, entry|
 | 
					 | 
				
			||||||
            (entry[:size] if path =~ descendant_pattern).to_i
 | 
					 | 
				
			||||||
          end
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def to_s
 | 
					 | 
				
			||||||
          @path
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def ==(other)
 | 
					 | 
				
			||||||
          @path == other.path && @entries == other.entries
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def inspect
 | 
					 | 
				
			||||||
          "#{self.class.name}: #{@path}"
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        def select_entries
 | 
					 | 
				
			||||||
          selected = @entries.select { |path, _metadata| yield path }
 | 
					 | 
				
			||||||
          selected.map { |path, _metadata| self.class.new(path, @entries) }
 | 
					 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,51 @@
 | 
				
			||||||
 | 
					module Gitlab
 | 
				
			||||||
 | 
					  module Ci
 | 
				
			||||||
 | 
					    module Build
 | 
				
			||||||
 | 
					      module Artifacts
 | 
				
			||||||
 | 
					        class Path
 | 
				
			||||||
 | 
					          def initialize(path)
 | 
				
			||||||
 | 
					            @path = path.dup.force_encoding('UTF-8')
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          def valid?
 | 
				
			||||||
 | 
					            nonzero? && utf8?
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          def directory?
 | 
				
			||||||
 | 
					            @path.end_with?('/')
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          def name
 | 
				
			||||||
 | 
					            @path.split('/').last.to_s
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          def nodes
 | 
				
			||||||
 | 
					            @path.count('/')
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          def to_s
 | 
				
			||||||
 | 
					            @path.tap do |path|
 | 
				
			||||||
 | 
					              unless nonzero?
 | 
				
			||||||
 | 
					                raise ArgumentError, 'Path contains zero byte character!'
 | 
				
			||||||
 | 
					              end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              unless utf8?
 | 
				
			||||||
 | 
					                raise ArgumentError, 'Path contains non-UTF-8 byte sequence!'
 | 
				
			||||||
 | 
					              end
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          def nonzero?
 | 
				
			||||||
 | 
					            @path.exclude?("\0")
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          def utf8?
 | 
				
			||||||
 | 
					            @path.valid_encoding?
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,64 @@
 | 
				
			||||||
 | 
					require 'spec_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe Gitlab::Ci::Build::Artifacts::Path do
 | 
				
			||||||
 | 
					  describe '#valid?' do
 | 
				
			||||||
 | 
					    context 'when path contains a zero character' do
 | 
				
			||||||
 | 
					      it 'is not valid' do
 | 
				
			||||||
 | 
					        expect(described_class.new("something/\255")).not_to be_valid
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when path is not utf8 string' do
 | 
				
			||||||
 | 
					      it 'is not valid' do
 | 
				
			||||||
 | 
					        expect(described_class.new("something/\0")).not_to be_valid
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when path is valid' do
 | 
				
			||||||
 | 
					      it 'is valid' do
 | 
				
			||||||
 | 
					        expect(described_class.new("some/file/path")).to be_valid
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#directory?' do
 | 
				
			||||||
 | 
					    context 'when path ends with a directory indicator' do
 | 
				
			||||||
 | 
					      it 'is a directory' do
 | 
				
			||||||
 | 
					        expect(described_class.new("some/file/dir/")).to be_directory
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when path does not end with a directory indicator' do
 | 
				
			||||||
 | 
					      it 'is not a directory' do
 | 
				
			||||||
 | 
					        expect(described_class.new("some/file")).not_to be_directory
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#name' do
 | 
				
			||||||
 | 
					    it 'returns a base name' do
 | 
				
			||||||
 | 
					      expect(described_class.new("some/file").name).to eq 'file'
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#nodes' do
 | 
				
			||||||
 | 
					    it 'returns number of path nodes' do
 | 
				
			||||||
 | 
					      expect(described_class.new("some/dir/file").nodes).to eq 2
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#to_s' do
 | 
				
			||||||
 | 
					    context 'when path is valid' do
 | 
				
			||||||
 | 
					      it 'returns a string representation of a path' do
 | 
				
			||||||
 | 
					        expect(described_class.new('some/path').to_s).to eq 'some/path'
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when path is invalid' do
 | 
				
			||||||
 | 
					      it 'raises an error' do
 | 
				
			||||||
 | 
					        expect { described_class.new("invalid/\0").to_s }
 | 
				
			||||||
 | 
					          .to raise_error ArgumentError
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
		Loading…
	
		Reference in New Issue