summaryrefslogtreecommitdiff
path: root/app/services/pages/zip_directory_service.rb
blob: a27ad5fda460a46126abbecf2ae9307dda5d7dc8 (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
# frozen_string_literal: true

module Pages
  class ZipDirectoryService
    InvalidArchiveError = Class.new(RuntimeError)
    InvalidEntryError = Class.new(RuntimeError)

    PUBLIC_DIR = 'public'

    def initialize(input_dir)
      @input_dir = File.realpath(input_dir)
      @output_file = File.join(@input_dir, "@migrated.zip") # '@' to avoid any name collision with groups or projects
    end

    def execute
      FileUtils.rm_f(@output_file)

      count = 0
      ::Zip::File.open(@output_file, ::Zip::File::CREATE) do |zipfile|
        write_entry(zipfile, PUBLIC_DIR)
        count = zipfile.entries.count
      end

      [@output_file, count]
    end

    private

    def write_entry(zipfile, zipfile_path)
      disk_file_path = File.join(@input_dir, zipfile_path)

      unless valid_path?(disk_file_path)
        # archive without public directory is completelly unusable
        raise InvalidArchiveError if zipfile_path == PUBLIC_DIR

        # archive with invalid entry will just have this entry missing
        raise InvalidEntryError
      end

      case File.lstat(disk_file_path).ftype
      when 'directory'
        recursively_zip_directory(zipfile, disk_file_path, zipfile_path)
      when 'file', 'link'
        zipfile.add(zipfile_path, disk_file_path)
      else
        raise InvalidEntryError
      end
    rescue InvalidEntryError => e
      Gitlab::ErrorTracking.track_exception(e, input_dir: @input_dir, disk_file_path: disk_file_path)
    end

    def recursively_zip_directory(zipfile, disk_file_path, zipfile_path)
      zipfile.mkdir(zipfile_path)

      entries = Dir.entries(disk_file_path) - %w[. ..]
      entries = entries.map { |entry| File.join(zipfile_path, entry) }

      write_entries(zipfile, entries)
    end

    def write_entries(zipfile, entries)
      entries.each do |zipfile_path|
        write_entry(zipfile, zipfile_path)
      end
    end

    # that should never happen, but we want to be safer
    # in theory without this we would allow to use symlinks
    # to pack any directory on disk
    # it isn't possible because SafeZip doesn't extract such archives
    def valid_path?(disk_file_path)
      realpath = File.realpath(disk_file_path)

      realpath == File.join(@input_dir, PUBLIC_DIR) ||
        realpath.start_with?(File.join(@input_dir, PUBLIC_DIR + "/"))
    # happens if target of symlink isn't there
    rescue => e
      Gitlab::ErrorTracking.track_exception(e, input_dir: @input_dir, disk_file_path: disk_file_path)

      false
    end
  end
end