329 lines
8.8 KiB
Ruby
329 lines
8.8 KiB
Ruby
module Gitlab
|
|
module Lfs
|
|
class Response
|
|
def initialize(project, user, ci, request)
|
|
@origin_project = project
|
|
@project = storage_project(project)
|
|
@user = user
|
|
@ci = ci
|
|
@env = request.env
|
|
@request = request
|
|
end
|
|
|
|
def render_download_object_response(oid)
|
|
render_response_to_download do
|
|
if check_download_sendfile_header?
|
|
render_lfs_sendfile(oid)
|
|
else
|
|
render_not_found
|
|
end
|
|
end
|
|
end
|
|
|
|
def render_batch_operation_response
|
|
request_body = JSON.parse(@request.body.read)
|
|
case request_body["operation"]
|
|
when "download"
|
|
render_batch_download(request_body)
|
|
when "upload"
|
|
render_batch_upload(request_body)
|
|
else
|
|
render_not_found
|
|
end
|
|
end
|
|
|
|
def render_storage_upload_authorize_response(oid, size)
|
|
render_response_to_push do
|
|
[
|
|
200,
|
|
{ "Content-Type" => "application/json; charset=utf-8" },
|
|
[JSON.dump({
|
|
'StoreLFSPath' => "#{Gitlab.config.lfs.storage_path}/tmp/upload",
|
|
'LfsOid' => oid,
|
|
'LfsSize' => size
|
|
})]
|
|
]
|
|
end
|
|
end
|
|
|
|
def render_storage_upload_store_response(oid, size, tmp_file_name)
|
|
return render_forbidden unless tmp_file_name
|
|
|
|
render_response_to_push do
|
|
render_lfs_upload_ok(oid, size, tmp_file_name)
|
|
end
|
|
end
|
|
|
|
def render_unsupported_deprecated_api
|
|
[
|
|
501,
|
|
{ "Content-Type" => "application/json; charset=utf-8" },
|
|
[JSON.dump({
|
|
'message' => 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.',
|
|
'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
|
|
})]
|
|
]
|
|
end
|
|
|
|
private
|
|
|
|
def render_not_enabled
|
|
[
|
|
501,
|
|
{
|
|
"Content-Type" => "application/json; charset=utf-8",
|
|
},
|
|
[JSON.dump({
|
|
'message' => 'Git LFS is not enabled on this GitLab server, contact your admin.',
|
|
'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
|
|
})]
|
|
]
|
|
end
|
|
|
|
def render_unauthorized
|
|
[
|
|
401,
|
|
{
|
|
'Content-Type' => 'text/plain'
|
|
},
|
|
['Unauthorized']
|
|
]
|
|
end
|
|
|
|
def render_not_found
|
|
[
|
|
404,
|
|
{
|
|
"Content-Type" => "application/vnd.git-lfs+json"
|
|
},
|
|
[JSON.dump({
|
|
'message' => 'Not found.',
|
|
'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
|
|
})]
|
|
]
|
|
end
|
|
|
|
def render_forbidden
|
|
[
|
|
403,
|
|
{
|
|
"Content-Type" => "application/vnd.git-lfs+json"
|
|
},
|
|
[JSON.dump({
|
|
'message' => 'Access forbidden. Check your access level.',
|
|
'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
|
|
})]
|
|
]
|
|
end
|
|
|
|
def render_lfs_sendfile(oid)
|
|
return render_not_found unless oid.present?
|
|
|
|
lfs_object = object_for_download(oid)
|
|
|
|
if lfs_object && lfs_object.file.exists?
|
|
[
|
|
200,
|
|
{
|
|
# GitLab-workhorse will forward Content-Type header
|
|
"Content-Type" => "application/octet-stream",
|
|
"X-Sendfile" => lfs_object.file.path
|
|
},
|
|
[]
|
|
]
|
|
else
|
|
render_not_found
|
|
end
|
|
end
|
|
|
|
def render_batch_upload(body)
|
|
return render_not_found if body.empty? || body['objects'].nil?
|
|
|
|
render_response_to_push do
|
|
response = build_upload_batch_response(body['objects'])
|
|
[
|
|
200,
|
|
{
|
|
"Content-Type" => "application/json; charset=utf-8",
|
|
"Cache-Control" => "private",
|
|
},
|
|
[JSON.dump(response)]
|
|
]
|
|
end
|
|
end
|
|
|
|
def render_batch_download(body)
|
|
return render_not_found if body.empty? || body['objects'].nil?
|
|
|
|
render_response_to_download do
|
|
response = build_download_batch_response(body['objects'])
|
|
[
|
|
200,
|
|
{
|
|
"Content-Type" => "application/json; charset=utf-8",
|
|
"Cache-Control" => "private",
|
|
},
|
|
[JSON.dump(response)]
|
|
]
|
|
end
|
|
end
|
|
|
|
def render_lfs_upload_ok(oid, size, tmp_file)
|
|
if store_file(oid, size, tmp_file)
|
|
[
|
|
200,
|
|
{
|
|
'Content-Type' => 'text/plain',
|
|
'Content-Length' => 0
|
|
},
|
|
[]
|
|
]
|
|
else
|
|
[
|
|
422,
|
|
{ 'Content-Type' => 'text/plain' },
|
|
["Unprocessable entity"]
|
|
]
|
|
end
|
|
end
|
|
|
|
def render_response_to_download
|
|
return render_not_enabled unless Gitlab.config.lfs.enabled
|
|
|
|
unless @project.public?
|
|
return render_unauthorized unless @user || @ci
|
|
return render_forbidden unless user_can_fetch?
|
|
end
|
|
|
|
yield
|
|
end
|
|
|
|
def render_response_to_push
|
|
return render_not_enabled unless Gitlab.config.lfs.enabled
|
|
return render_unauthorized unless @user
|
|
return render_forbidden unless user_can_push?
|
|
|
|
yield
|
|
end
|
|
|
|
def check_download_sendfile_header?
|
|
@env['HTTP_X_SENDFILE_TYPE'].to_s == "X-Sendfile"
|
|
end
|
|
|
|
def user_can_fetch?
|
|
# Check user access against the project they used to initiate the pull
|
|
@ci || @user.can?(:download_code, @origin_project)
|
|
end
|
|
|
|
def user_can_push?
|
|
# Check user access against the project they used to initiate the push
|
|
@user.can?(:push_code, @origin_project)
|
|
end
|
|
|
|
def storage_project(project)
|
|
if project.forked?
|
|
storage_project(project.forked_from_project)
|
|
else
|
|
project
|
|
end
|
|
end
|
|
|
|
def store_file(oid, size, tmp_file)
|
|
tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file)
|
|
|
|
object = LfsObject.find_or_create_by(oid: oid, size: size)
|
|
if object.file.exists?
|
|
success = true
|
|
else
|
|
success = move_tmp_file_to_storage(object, tmp_file_path)
|
|
end
|
|
|
|
if success
|
|
success = link_to_project(object)
|
|
end
|
|
|
|
success
|
|
ensure
|
|
# Ensure that the tmp file is removed
|
|
FileUtils.rm_f(tmp_file_path)
|
|
end
|
|
|
|
def object_for_download(oid)
|
|
@project.lfs_objects.find_by(oid: oid)
|
|
end
|
|
|
|
def move_tmp_file_to_storage(object, path)
|
|
File.open(path) do |f|
|
|
object.file = f
|
|
end
|
|
|
|
object.file.store!
|
|
object.save
|
|
end
|
|
|
|
def link_to_project(object)
|
|
if object && !object.projects.exists?(@project.id)
|
|
object.projects << @project
|
|
object.save
|
|
end
|
|
end
|
|
|
|
def select_existing_objects(objects)
|
|
objects_oids = objects.map { |o| o['oid'] }
|
|
@project.lfs_objects.where(oid: objects_oids).pluck(:oid).to_set
|
|
end
|
|
|
|
def build_upload_batch_response(objects)
|
|
selected_objects = select_existing_objects(objects)
|
|
|
|
upload_hypermedia_links(objects, selected_objects)
|
|
end
|
|
|
|
def build_download_batch_response(objects)
|
|
selected_objects = select_existing_objects(objects)
|
|
|
|
download_hypermedia_links(objects, selected_objects)
|
|
end
|
|
|
|
def download_hypermedia_links(all_objects, existing_objects)
|
|
all_objects.each do |object|
|
|
if existing_objects.include?(object['oid'])
|
|
object['actions'] = {
|
|
'download' => {
|
|
'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}",
|
|
'header' => {
|
|
'Authorization' => @env['HTTP_AUTHORIZATION']
|
|
}.compact
|
|
}
|
|
}
|
|
else
|
|
object['error'] = {
|
|
'code' => 404,
|
|
'message' => "Object does not exist on the server or you don't have permissions to access it",
|
|
}
|
|
end
|
|
end
|
|
|
|
{ 'objects' => all_objects }
|
|
end
|
|
|
|
def upload_hypermedia_links(all_objects, existing_objects)
|
|
all_objects.each do |object|
|
|
# generate actions only for non-existing objects
|
|
next if existing_objects.include?(object['oid'])
|
|
|
|
object['actions'] = {
|
|
'upload' => {
|
|
'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}",
|
|
'header' => {
|
|
'Authorization' => @env['HTTP_AUTHORIZATION']
|
|
}.compact
|
|
}
|
|
}
|
|
end
|
|
|
|
{ 'objects' => all_objects }
|
|
end
|
|
end
|
|
end
|
|
end
|