Extract a class that represents artifacts file path

This commit is contained in:
Grzegorz Bizon 2017-09-05 12:17:56 +02:00
parent dfb8fcbb65
commit c53f319f88
3 changed files with 236 additions and 121 deletions

View File

@ -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

View 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

View 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