Extract a class that represents artifacts file path
This commit is contained in:
parent
dfb8fcbb65
commit
c53f319f88
3 changed files with 236 additions and 121 deletions
|
@ -1,128 +1,128 @@
|
|||
module Gitlab
|
||||
module Ci::Build::Artifacts
|
||||
class Metadata
|
||||
##
|
||||
# Class that represents an entry (path and metadata) to a file or
|
||||
# directory in GitLab CI Build Artifacts binary file / archive
|
||||
#
|
||||
# This is IO-operations safe class, that does similar job to
|
||||
# Ruby's Pathname but without the risk of accessing filesystem.
|
||||
#
|
||||
# This class is working only with UTF-8 encoded paths.
|
||||
#
|
||||
class Entry
|
||||
attr_reader :path, :entries
|
||||
attr_accessor :name
|
||||
module Ci
|
||||
module Build
|
||||
module Artifacts
|
||||
class Metadata
|
||||
##
|
||||
# Class that represents an entry (path and metadata) to a file or
|
||||
# directory in GitLab CI Build Artifacts binary file / archive
|
||||
#
|
||||
# This is IO-operations safe class, that does similar job to
|
||||
# Ruby's Pathname but without the risk of accessing filesystem.
|
||||
#
|
||||
# This class is working only with UTF-8 encoded paths.
|
||||
#
|
||||
class Entry
|
||||
attr_reader :entries
|
||||
attr_accessor :name
|
||||
|
||||
def initialize(path, entries)
|
||||
@path = path.dup.force_encoding('UTF-8')
|
||||
@entries = entries
|
||||
def initialize(path, entries)
|
||||
@entries = entries
|
||||
@path = Artifacts::Path.new(path)
|
||||
end
|
||||
|
||||
if path.include?("\0")
|
||||
raise ArgumentError, 'Path contains zero byte character!'
|
||||
delegate :empty?, to: :children
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
|
51
lib/gitlab/ci/build/artifacts/path.rb
Normal file
51
lib/gitlab/ci/build/artifacts/path.rb
Normal file
|
@ -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
|
64
spec/lib/gitlab/ci/build/artifacts/path_spec.rb
Normal file
64
spec/lib/gitlab/ci/build/artifacts/path_spec.rb
Normal file
|
@ -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 a new issue