summaryrefslogtreecommitdiff
path: root/app/controllers/projects/repositories_controller.rb
blob: a9ff5d8a3bfae41ea0888239b40600acb78ef9ab (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
131
132
133
134
135
136
137
138
139
140
# frozen_string_literal: true

class Projects::RepositoriesController < Projects::ApplicationController
  include ExtractsPath
  include StaticObjectExternalStorage
  include HotlinkInterceptor
  include Gitlab::RepositoryArchiveRateLimiter

  prepend_before_action(only: [:archive]) { authenticate_sessionless_user!(:archive) }

  skip_before_action :default_cache_headers, only: :archive

  # Authorize
  before_action :check_archive_rate_limiting!, only: :archive
  before_action :require_non_empty_project, except: :create
  before_action :intercept_hotlinking!, only: :archive
  before_action :assign_archive_vars, only: :archive
  before_action :assign_append_sha, only: :archive
  before_action :authorize_download_code!
  before_action :authorize_admin_project!, only: :create
  before_action :redirect_to_external_storage, only: :archive, if: :static_objects_external_storage_enabled?

  feature_category :source_code_management

  def create
    @project.create_repository unless @project.repository_exists?

    redirect_to project_path(@project)
  end

  def archive
    return render_404 if html_request?

    set_cache_headers
    return if archive_not_modified?

    send_git_archive @repository, **repo_params
  rescue StandardError => e
    logger.error("#{self.class.name}: #{e}")
    git_not_found!
  end

  private

  def repo_params
    @repo_params ||= { ref: @ref, path: params[:path], format: params[:format], append_sha: @append_sha }
  end

  def set_cache_headers
    commit_id = archive_metadata['CommitId']

    if Feature.enabled?(:improve_blobs_cache_headers)
      expires_in(cache_max_age(commit_id),
                 public: Guest.can?(:download_code, project), must_revalidate: true, stale_if_error: 5.minutes,
                 stale_while_revalidate: 1.minute, 's-maxage': 1.minute)

      fresh_when(strong_etag: [commit_id, archive_metadata['ArchivePath']])
    else
      expires_in cache_max_age(commit_id), public: Guest.can?(:download_code, project)
      fresh_when(etag: archive_metadata['ArchivePath'])
    end
  end

  def archive_not_modified?
    # Check response freshness (Last-Modified and ETag)
    # against request If-Modified-Since and If-None-Match conditions.
    request.fresh?(response)
  end

  def archive_metadata
    @archive_metadata ||= @repository.archive_metadata(
      @ref,
      '', # Where archives are stored isn't really important for ETag purposes
      repo_params[:format],
      path: repo_params[:path],
      append_sha: @append_sha
    )
  end

  def cache_max_age(commit_id)
    if @ref == commit_id
      # This is a link to an archive by a commit SHA. That means that the archive
      # is immutable. The only reason to invalidate the cache is if the commit
      # was deleted or if the user lost access to the repository.
      Repository::ARCHIVE_CACHE_TIME_IMMUTABLE
    else
      # A branch or tag points at this archive. That means that the expected archive
      # content may change over time.
      Repository::ARCHIVE_CACHE_TIME
    end
  end

  def assign_append_sha
    @append_sha = params[:append_sha]

    if @ref
      shortname = "#{@project.path}-#{@ref.tr('/', '-')}"
      @append_sha = false if @filename == shortname
    end
  end

  def assign_archive_vars
    if params[:id]
      @ref, @filename = extract_ref_and_filename(params[:id])
    else
      @ref = params[:ref]
      @filename = nil
    end
  rescue InvalidPathError
    render_404
  end

  # path can be of the form:
  # master
  # master/first.zip
  # master/first/second.tar.gz
  # master/first/second/third.zip
  #
  # In the archive case, we know that the last value is always the filename, so we
  # do a greedy match to extract the ref. This avoid having to pull all ref names
  # from Redis.
  def extract_ref_and_filename(id)
    path = id.strip
    data = path.match(%r{(.*)/(.*)})

    if data
      [data[1], data[2]]
    else
      [path, nil]
    end
  end

  def check_archive_rate_limiting!
    check_archive_rate_limit!(current_user, @project) do
      render(plain: _('This archive has been requested too many times. Try again later.'), status: :too_many_requests)
    end
  end
end

Projects::RepositoriesController.prepend_mod_with('Projects::RepositoriesController')