summaryrefslogtreecommitdiff
path: root/lib/gitlab/ci/build/artifacts/metadata/entry.rb
blob: 2e073334abca9799da22cca831cb5f7c2b5971db (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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

        def initialize(path, entries)
          @path = path.dup.force_encoding('UTF-8')
          @entries = entries

          if path.include?("\0")
            raise ArgumentError, 'Path contains zero byte character!'
          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