Skip to content
Snippets Groups Projects
Commit 14032d8e authored by Marin Jankovski's avatar Marin Jankovski
Browse files

Add support for git lfs.

parent 9179fcec
No related branches found
No related tags found
No related merge requests found
Showing
with 556 additions and 13 deletions
Loading
Loading
@@ -43,3 +43,4 @@ rails_best_practices_output.html
tmp/
vendor/bundle/*
builds/*
shared/*
Loading
Loading
@@ -72,8 +72,7 @@ class ProjectsController < ApplicationController
def remove_fork
return access_denied! unless can?(current_user, :remove_fork_project, @project)
 
if @project.forked?
@project.forked_project_link.destroy
if @project.unlink_fork
flash[:notice] = 'The fork relationship has been removed.'
end
end
Loading
Loading
@@ -243,7 +242,7 @@ class ProjectsController < ApplicationController
project.repository_exists? && !project.empty_repo?
end
 
# Override get_id from ExtractsPath, which returns the branch and file path
# Override get_id from ExtractsPath, which returns the branch and file path
# for the blob/tree, which in this case is just the root of the default branch.
def get_id
project.repository.root_ref
Loading
Loading
class LfsObject < ActiveRecord::Base
has_many :lfs_objects_projects, dependent: :destroy
has_many :projects, through: :lfs_objects_projects
validates :oid, presence: true, uniqueness: true
mount_uploader :file, LfsObjectUploader
end
class LfsObjectsProject < ActiveRecord::Base
belongs_to :project
belongs_to :lfs_object
validates :lfs_object_id, presence: true
validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
validates :project_id, presence: true
end
Loading
Loading
@@ -124,6 +124,8 @@ class Project < ActiveRecord::Base
has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id
has_many :ci_builds, through: :ci_commits, source: :builds, dependent: :destroy, class_name: 'Ci::Build'
has_many :releases, dependent: :destroy
has_many :lfs_objects_projects, dependent: :destroy
has_many :lfs_objects, through: :lfs_objects_projects
 
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :gitlab_ci_project, dependent: :destroy, class_name: "Ci::Project", foreign_key: :gitlab_id
Loading
Loading
@@ -798,4 +800,14 @@ class Project < ActiveRecord::Base
def enable_ci
self.builds_enabled = true
end
def unlink_fork
if forked?
forked_from_project.lfs_objects.find_each do |lfs_object|
lfs_object.projects << self
end
forked_project_link.destroy
end
end
end
# encoding: utf-8
class LfsObjectUploader < CarrierWave::Uploader::Base
storage :file
def store_dir
"#{Gitlab.config.lfs.storage_path}/#{model.oid[0,2]}/#{model.oid[2,2]}"
end
def cache_dir
"#{Gitlab.config.lfs.storage_path}/tmp/cache"
end
def move_to_cache
true
end
def move_to_store
true
end
def exists?
file.try(:exists?)
end
def filename
model.oid[4..-1]
end
end
Loading
Loading
@@ -124,6 +124,12 @@ production: &base
# The mailbox where incoming mail will end up. Usually "inbox".
mailbox: "inbox"
 
## Git LFS
lfs:
enabled: false
# The location where LFS objects are stored (default: shared/lfs-objects).
# storage_path: shared/lfs-objects
## Gravatar
## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html
gravatar:
Loading
Loading
@@ -317,8 +323,6 @@ production: &base
# path: /mnt/gitlab # Default: shared
 
 
#
# 4. Advanced settings
# ==========================
Loading
Loading
@@ -419,6 +423,8 @@ test:
<<: *base
gravatar:
enabled: true
lfs:
enabled: false
gitlab:
host: localhost
port: 80
Loading
Loading
Loading
Loading
@@ -199,6 +199,13 @@ Settings.incoming_email['ssl'] = false if Settings.incoming_email['ssl'].
Settings.incoming_email['start_tls'] = false if Settings.incoming_email['start_tls'].nil?
Settings.incoming_email['mailbox'] = "inbox" if Settings.incoming_email['mailbox'].nil?
 
#
# Git LFS
#
Settings['lfs'] ||= Settingslogic.new({})
Settings.lfs['enabled'] = false if Settings.lfs['enabled'].nil?
Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"), Rails.root)
#
# Gravatar
#
Loading
Loading
Loading
Loading
@@ -93,7 +93,7 @@ Gitlab::Application.routes.draw do
end
 
# Enable Grack support
mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post]
mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put]
 
# Help
get 'help' => 'help#index'
Loading
Loading
class CreateLfsObjects < ActiveRecord::Migration
def change
create_table :lfs_objects do |t|
t.string :oid, null: false, unique: true
t.integer :size, null: false
t.timestamps
end
end
end
class CreateLfsObjectsProjects < ActiveRecord::Migration
def change
create_table :lfs_objects_projects do |t|
t.integer :lfs_object_id, null: false
t.integer :project_id, null: false
t.timestamps
end
add_index :lfs_objects_projects, :project_id
end
end
class AddFileToLfsObjects < ActiveRecord::Migration
def change
add_column :lfs_objects, :file, :string
end
end
class AddIndexForLfsOidAndSize < ActiveRecord::Migration
def change
add_index :lfs_objects, :oid
add_index :lfs_objects, [:oid, :size]
end
end
Loading
Loading
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
 
ActiveRecord::Schema.define(version: 20151109100728) do
ActiveRecord::Schema.define(version: 20151114113410) do
 
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Loading
Loading
@@ -422,6 +422,26 @@ ActiveRecord::Schema.define(version: 20151109100728) do
 
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
 
create_table "lfs_objects", force: true do |t|
t.string "oid", null: false, unique: true
t.integer "size", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.string "file"
end
add_index "lfs_objects", ["oid", "size"], name: "index_lfs_objects_on_oid_and_size", using: :btree
add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", using: :btree
create_table "lfs_objects_projects", force: true do |t|
t.integer "lfs_object_id", null: false
t.integer "project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree
create_table "members", force: true do |t|
t.integer "access_level", null: false
t.integer "source_id", null: false
Loading
Loading
Loading
Loading
@@ -33,6 +33,9 @@ module Grack
 
auth!
 
lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call
return lfs_response unless lfs_response.nil?
if project && authorized_request?
# Tell gitlab-workhorse the request is OK, and what the GL_ID is
render_grack_auth_ok
Loading
Loading
@@ -72,7 +75,7 @@ module Grack
matched_login = /(?<s>^[a-zA-Z]*-ci)-token$/.match(login)
 
if project && matched_login.present? && git_cmd == 'git-upload-pack'
underscored_service = matched_login['s'].underscore
underscored_service = matched_login['s'].underscore
 
if Service.available_services_names.include?(underscored_service)
service_method = "#{underscored_service}_service"
Loading
Loading
Loading
Loading
@@ -13,7 +13,7 @@ module Gitlab
def user
return @user if defined?(@user)
 
@user =
@user =
case actor
when User
actor
Loading
Loading
@@ -125,7 +125,7 @@ module Gitlab
def change_access_check(change)
oldrev, newrev, ref = change.split(' ')
 
action =
action =
if project.protected_branch?(branch_name(ref))
protected_branch_action(oldrev, newrev, branch_name(ref))
elsif protected_tag?(tag_name(ref))
Loading
Loading
@@ -148,7 +148,7 @@ module Gitlab
build_status_object(false, "You are not allowed to change existing tags on this project.")
else # :push_code
build_status_object(false, "You are not allowed to push code to this project.")
end
end
return status
end
 
Loading
Loading
module Gitlab
module Lfs
class Response
def initialize(project, user, request)
@origin_project = project
@project = storage_project(project)
@user = user
@env = request.env
@request = request
end
# Return a response for a download request
# Can be a response to:
# Request from a user to get the file
# Request from gitlab-workhorse which file to serve to the user
def render_download_hypermedia_response(oid)
render_response_to_download do
if check_download_accept_header?
render_lfs_download_hypermedia(oid)
else
render_not_found
end
end
end
def render_download_object_response(oid)
render_response_to_download do
if check_download_sendfile_header? && check_download_accept_header?
render_lfs_sendfile(oid)
else
render_not_found
end
end
end
def render_lfs_api_auth
render_response_to_push do
request_body = JSON.parse(@request.body.read)
return render_not_found if request_body.empty? || request_body['objects'].empty?
response = build_response(request_body['objects'])
[
200,
{
"Content-Type" => "application/json; charset=utf-8",
"Cache-Control" => "private",
},
[JSON.dump(response)]
]
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)
render_response_to_push do
render_lfs_upload_ok(oid, size, tmp_file_name)
end
end
private
def render_not_enabled
[
501,
{
"Content-Type" => "application/vnd.git-lfs+json",
},
[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_lfs_download_hypermedia(oid)
return render_not_found unless oid.present?
lfs_object = object_for_download(oid)
if lfs_object
[
200,
{ "Content-Type" => "application/vnd.git-lfs+json" },
[JSON.dump(download_hypermedia(oid))]
]
else
render_not_found
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
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 check_download_accept_header?
@env['HTTP_ACCEPT'].to_s == "application/vnd.git-lfs+json; charset=utf-8"
end
def user_can_fetch?
# Check user access against the project they used to initiate the pull
@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?
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)
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_response(objects)
selected_objects = select_existing_objects(objects)
upload_hypermedia(objects, selected_objects)
end
def download_hypermedia(oid)
{
'_links' => {
'download' =>
{
'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{oid}",
'header' => {
'Accept' => "application/vnd.git-lfs+json; charset=utf-8",
'Authorization' => @env['HTTP_AUTHORIZATION']
}.compact
}
}
}
end
def upload_hypermedia(all_objects, existing_objects)
all_objects.each do |object|
object['_links'] = hypermedia_links(object) unless existing_objects.include?(object['oid'])
end
{ 'objects' => all_objects }
end
def hypermedia_links(object)
{
"upload" => {
'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}",
'header' => { 'Authorization' => @env['HTTP_AUTHORIZATION'] }
}.compact
}
end
end
end
end
module Gitlab
module Lfs
class Router
def initialize(project, user, request)
@project = project
@user = user
@env = request.env
@request = request
end
def try_call
return unless @request && @request.path.present?
case @request.request_method
when 'GET'
get_response
when 'POST'
post_response
when 'PUT'
put_response
else
nil
end
end
private
def get_response
path_match = @request.path.match(/\/(info\/lfs|gitlab-lfs)\/objects\/([0-9a-f]{64})$/)
return nil unless path_match
oid = path_match[2]
return nil unless oid
case path_match[1]
when "info/lfs"
lfs.render_download_hypermedia_response(oid)
when "gitlab-lfs"
lfs.render_download_object_response(oid)
else
nil
end
end
def post_response
post_path = @request.path.match(/\/info\/lfs\/objects(\/batch)?$/)
return nil unless post_path
# Check for Batch API
if post_path[0].ends_with?("/info/lfs/objects/batch")
lfs.render_lfs_api_auth
else
nil
end
end
def put_response
object_match = @request.path.match(/\/gitlab-lfs\/objects\/([0-9a-f]{64})\/([0-9]+)(|\/authorize){1}$/)
return nil if object_match.nil?
oid = object_match[1]
size = object_match[2].try(:to_i)
return nil if oid.nil? || size.nil?
# GitLab-workhorse requests
# 1. Try to authorize the request
# 2. send a request with a header containing the name of the temporary file
if object_match[3] && object_match[3] == '/authorize'
lfs.render_storage_upload_authorize_response(oid, size)
else
tmp_file_name = sanitize_tmp_filename(@request.env['HTTP_X_GITLAB_LFS_TMP'])
return nil unless tmp_file_name
lfs.render_storage_upload_store_response(oid, size, tmp_file_name)
end
end
def lfs
return unless @project
Gitlab::Lfs::Response.new(@project, @user, @request)
end
def sanitize_tmp_filename(name)
if name.present?
name.gsub!(/^.*(\\|\/)/, '')
name = name.match(/[0-9a-f]{73}/)
name[0] if name
else
nil
end
end
end
end
end
Loading
Loading
@@ -44,7 +44,7 @@ upstream gitlab-workhorse {
 
## Normal HTTP host
server {
## Either remove "default_server" from the listen line below,
## Either remove "default_server" from the listen line below,
## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab
## to be served if you visit any address that your server responds to, eg.
## the ip address of the server (http://x.x.x.x/)n 0.0.0.0:80 default_server;
Loading
Loading
@@ -113,6 +113,13 @@ server {
proxy_pass http://gitlab;
}
 
location ~ ^/[\w\.-]+/[\w\.-]+/gitlab-lfs/objects {
client_max_body_size 0;
# 'Error' 418 is a hack to re-use the @gitlab-workhorse block
error_page 418 = @gitlab-workhorse;
return 418;
}
location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ {
# 'Error' 418 is a hack to re-use the @gitlab-workhorse block
error_page 418 = @gitlab-workhorse;
Loading
Loading
Loading
Loading
@@ -48,7 +48,7 @@ upstream gitlab-workhorse {
 
## Redirects all HTTP traffic to the HTTPS host
server {
## Either remove "default_server" from the listen line below,
## Either remove "default_server" from the listen line below,
## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab
## to be served if you visit any address that your server responds to, eg.
## the ip address of the server (http://x.x.x.x/)
Loading
Loading
@@ -160,6 +160,13 @@ server {
proxy_pass http://gitlab;
}
 
location ~ ^/[\w\.-]+/[\w\.-]+/gitlab-lfs/objects {
client_max_body_size 0;
# 'Error' 418 is a hack to re-use the @gitlab-workhorse block
error_page 418 = @gitlab-workhorse;
return 418;
}
location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ {
# 'Error' 418 is a hack to re-use the @gitlab-workhorse block
error_page 418 = @gitlab-workhorse;
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment