diff options
author | Michael Kozono <mkozono@gmail.com> | 2018-02-20 16:04:51 -0800 |
---|---|---|
committer | Michael Kozono <mkozono@gmail.com> | 2018-02-20 17:34:52 -0800 |
commit | 00e4311a78470d98838a134b3b3dc7a93ae37e5f (patch) | |
tree | ece390af15909594d5a65fd984b8b02a17a218ed | |
parent | 95de59c086118a492bcfcd29eea89107b1bdcbff (diff) | |
download | gitlab-ce-mk-test.tar.gz |
Revert "Test untracked uploads modifications"mk-test
This reverts commit 6a77c567425a250f0d886c84fd163c8f314d9250.
9 files changed, 638 insertions, 765 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 98325d1c6fe..7cad3c4a30d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -96,7 +96,7 @@ rspec-mike: script: - export CACHE_CLASSES=true - scripts/gitaly-test-spawn - - rspec --color --format documentation spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb + - rspec --color --format documentation spec/requests/api/users_spec.rb spec/factories_spec.rb spec/models/cycle_analytics/issue_spec.rb spec/features/issues/gfm_autocomplete_spec.rb spec/services/system_note_service_spec.rb spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb spec/helpers/markup_helper_spec.rb spec/requests/api/v3/triggers_spec.rb spec/services/create_deployment_service_spec.rb spec/features/explore/groups_spec.rb spec/services/members/approve_access_request_service_spec.rb spec/features/security/project/snippet/private_access_spec.rb spec/features/projects/user_replaces_files_spec.rb spec/services/issues/build_service_spec.rb spec/requests/api/project_hooks_spec.rb spec/controllers/projects/project_members_controller_spec.rb spec/services/merge_requests/conflicts/resolve_service_spec.rb spec/migrations/migrate_stages_statuses_spec.rb spec/models/concerns/mentionable_spec.rb spec/controllers/projects/variables_controller_spec.rb spec/features/merge_request/user_sees_pipelines_spec.rb spec/controllers/sent_notifications_controller_spec.rb spec/lib/gitlab/project_authorizations_spec.rb spec/features/groups/members/leave_group_spec.rb spec/features/projects/files/edit_file_soft_wrap_spec.rb spec/features/admin/admin_labels_spec.rb spec/features/search/user_searches_for_wiki_pages_spec.rb spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb spec/services/boards/issues/move_service_spec.rb spec/features/projects/settings/visibility_settings_spec.rb spec/finders/users_finder_spec.rb spec/features/merge_request/user_sees_mr_from_deleted_forked_project_spec.rb spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb spec/features/projects/members/group_members_spec.rb spec/workers/pipeline_metrics_worker_spec.rb spec/controllers/projects/clusters/user_controller_spec.rb spec/features/merge_request/user_sees_deleted_target_branch_spec.rb spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb artifacts: expire_in: 31d when: always diff --git a/lib/gitlab/background_migration/populate_untracked_uploads.rb b/lib/gitlab/background_migration/populate_untracked_uploads.rb index c5415bf0f9a..8a8e770940e 100644 --- a/lib/gitlab/background_migration/populate_untracked_uploads.rb +++ b/lib/gitlab/background_migration/populate_untracked_uploads.rb @@ -5,10 +5,157 @@ module Gitlab # This class processes a batch of rows in `untracked_files_for_uploads` by # adding each file to the `uploads` table if it does not exist. class PopulateUntrackedUploads # rubocop:disable Metrics/ClassLength + # This class is responsible for producing the attributes necessary to + # track an uploaded file in the `uploads` table. + class UntrackedFile < ActiveRecord::Base # rubocop:disable Metrics/ClassLength, Metrics/LineLength + self.table_name = 'untracked_files_for_uploads' + + # Ends with /:random_hex/:filename + FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z} + FULL_PATH_CAPTURE = /\A(.+)#{FILE_UPLOADER_PATH}/ + + # These regex patterns are tested against a relative path, relative to + # the upload directory. + # For convenience, if there exists a capture group in the pattern, then + # it indicates the model_id. + PATH_PATTERNS = [ + { + pattern: %r{\A-/system/appearance/logo/(\d+)/}, + uploader: 'AttachmentUploader', + model_type: 'Appearance' + }, + { + pattern: %r{\A-/system/appearance/header_logo/(\d+)/}, + uploader: 'AttachmentUploader', + model_type: 'Appearance' + }, + { + pattern: %r{\A-/system/note/attachment/(\d+)/}, + uploader: 'AttachmentUploader', + model_type: 'Note' + }, + { + pattern: %r{\A-/system/user/avatar/(\d+)/}, + uploader: 'AvatarUploader', + model_type: 'User' + }, + { + pattern: %r{\A-/system/group/avatar/(\d+)/}, + uploader: 'AvatarUploader', + model_type: 'Namespace' + }, + { + pattern: %r{\A-/system/project/avatar/(\d+)/}, + uploader: 'AvatarUploader', + model_type: 'Project' + }, + { + pattern: FILE_UPLOADER_PATH, + uploader: 'FileUploader', + model_type: 'Project' + } + ].freeze + + def to_h + @upload_hash ||= { + path: upload_path, + uploader: uploader, + model_type: model_type, + model_id: model_id, + size: file_size, + checksum: checksum + } + end + + def upload_path + # UntrackedFile#path is absolute, but Upload#path depends on uploader + @upload_path ||= + if uploader == 'FileUploader' + # Path relative to project directory in uploads + matchd = path_relative_to_upload_dir.match(FILE_UPLOADER_PATH) + matchd[0].sub(%r{\A/}, '') # remove leading slash + else + path + end + end + + def uploader + matching_pattern_map[:uploader] + end + + def model_type + matching_pattern_map[:model_type] + end + + def model_id + return @model_id if defined?(@model_id) + + pattern = matching_pattern_map[:pattern] + matchd = path_relative_to_upload_dir.match(pattern) + + # If something is captured (matchd[1] is not nil), it is a model_id + # Only the FileUploader pattern will not match an ID + @model_id = matchd[1] ? matchd[1].to_i : file_uploader_model_id + end + + def file_size + File.size(absolute_path) + end + + def checksum + Digest::SHA256.file(absolute_path).hexdigest + end + + private + + def matching_pattern_map + @matching_pattern_map ||= PATH_PATTERNS.find do |path_pattern_map| + path_relative_to_upload_dir.match(path_pattern_map[:pattern]) + end + + unless @matching_pattern_map + raise "Unknown upload path pattern \"#{path}\"" + end + + @matching_pattern_map + end + + def file_uploader_model_id + matchd = path_relative_to_upload_dir.match(FULL_PATH_CAPTURE) + not_found_msg = <<~MSG + Could not capture project full_path from a FileUploader path: + "#{path_relative_to_upload_dir}" + MSG + raise not_found_msg unless matchd + + full_path = matchd[1] + project = Project.find_by_full_path(full_path) + return nil unless project + + project.id + end + + # Not including a leading slash + def path_relative_to_upload_dir + upload_dir = Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR # rubocop:disable Metrics/LineLength + base = %r{\A#{Regexp.escape(upload_dir)}/} + @path_relative_to_upload_dir ||= path.sub(base, '') + end + + def absolute_path + File.join(Gitlab.config.uploads.storage_path, path) + end + end + + # This class is used to query the `uploads` table. + class Upload < ActiveRecord::Base + self.table_name = 'uploads' + end + def perform(start_id, end_id) return unless migrate? - files = Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.where(id: start_id..end_id) + files = UntrackedFile.where(id: start_id..end_id) processed_files = insert_uploads_if_needed(files) processed_files.delete_all @@ -18,8 +165,7 @@ module Gitlab private def migrate? - Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.table_exists? && - Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::Upload.table_exists? + UntrackedFile.table_exists? && Upload.table_exists? end def insert_uploads_if_needed(files) @@ -51,7 +197,7 @@ module Gitlab def filter_existing_uploads(files) paths = files.map(&:upload_path) - existing_paths = Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::Upload.where(path: paths).pluck(:path).to_set + existing_paths = Upload.where(path: paths).pluck(:path).to_set files.reject do |file| existing_paths.include?(file.upload_path) @@ -83,7 +229,7 @@ module Gitlab end ids.each do |model_type, model_ids| - model_class = Object.const_get("Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::#{model_type}") + model_class = Object.const_get(model_type) found_ids = model_class.where(id: model_ids.uniq).pluck(:id) deleted_ids = ids[model_type] - found_ids ids[model_type] = deleted_ids @@ -103,8 +249,8 @@ module Gitlab end def drop_temp_table_if_finished - if Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.all.empty? && !Rails.env.test? # Dropping a table intermittently breaks test cleanup - Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.connection.drop_table(:untracked_files_for_uploads, + if UntrackedFile.all.empty? + UntrackedFile.connection.drop_table(:untracked_files_for_uploads, if_exists: true) end end diff --git a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb deleted file mode 100644 index a2c5acbde71..00000000000 --- a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb +++ /dev/null @@ -1,201 +0,0 @@ -# frozen_string_literal: true -module Gitlab - module BackgroundMigration - module PopulateUntrackedUploadsDependencies - # This class is responsible for producing the attributes necessary to - # track an uploaded file in the `uploads` table. - class UntrackedFile < ActiveRecord::Base # rubocop:disable Metrics/ClassLength, Metrics/LineLength - self.table_name = 'untracked_files_for_uploads' - - # Ends with /:random_hex/:filename - FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z} - FULL_PATH_CAPTURE = /\A(.+)#{FILE_UPLOADER_PATH}/ - - # These regex patterns are tested against a relative path, relative to - # the upload directory. - # For convenience, if there exists a capture group in the pattern, then - # it indicates the model_id. - PATH_PATTERNS = [ - { - pattern: %r{\A-/system/appearance/logo/(\d+)/}, - uploader: 'AttachmentUploader', - model_type: 'Appearance' - }, - { - pattern: %r{\A-/system/appearance/header_logo/(\d+)/}, - uploader: 'AttachmentUploader', - model_type: 'Appearance' - }, - { - pattern: %r{\A-/system/note/attachment/(\d+)/}, - uploader: 'AttachmentUploader', - model_type: 'Note' - }, - { - pattern: %r{\A-/system/user/avatar/(\d+)/}, - uploader: 'AvatarUploader', - model_type: 'User' - }, - { - pattern: %r{\A-/system/group/avatar/(\d+)/}, - uploader: 'AvatarUploader', - model_type: 'Namespace' - }, - { - pattern: %r{\A-/system/project/avatar/(\d+)/}, - uploader: 'AvatarUploader', - model_type: 'Project' - }, - { - pattern: FILE_UPLOADER_PATH, - uploader: 'FileUploader', - model_type: 'Project' - } - ].freeze - - def to_h - @upload_hash ||= { - path: upload_path, - uploader: uploader, - model_type: model_type, - model_id: model_id, - size: file_size, - checksum: checksum - } - end - - def upload_path - # UntrackedFile#path is absolute, but Upload#path depends on uploader - @upload_path ||= - if uploader == 'FileUploader' - # Path relative to project directory in uploads - matchd = path_relative_to_upload_dir.match(FILE_UPLOADER_PATH) - matchd[0].sub(%r{\A/}, '') # remove leading slash - else - path - end - end - - def uploader - matching_pattern_map[:uploader] - end - - def model_type - matching_pattern_map[:model_type] - end - - def model_id - return @model_id if defined?(@model_id) - - pattern = matching_pattern_map[:pattern] - matchd = path_relative_to_upload_dir.match(pattern) - - # If something is captured (matchd[1] is not nil), it is a model_id - # Only the FileUploader pattern will not match an ID - @model_id = matchd[1] ? matchd[1].to_i : file_uploader_model_id - end - - def file_size - File.size(absolute_path) - end - - def checksum - Digest::SHA256.file(absolute_path).hexdigest - end - - private - - def matching_pattern_map - @matching_pattern_map ||= PATH_PATTERNS.find do |path_pattern_map| - path_relative_to_upload_dir.match(path_pattern_map[:pattern]) - end - - unless @matching_pattern_map - raise "Unknown upload path pattern \"#{path}\"" - end - - @matching_pattern_map - end - - def file_uploader_model_id - matchd = path_relative_to_upload_dir.match(FULL_PATH_CAPTURE) - not_found_msg = <<~MSG - Could not capture project full_path from a FileUploader path: - "#{path_relative_to_upload_dir}" - MSG - raise not_found_msg unless matchd - - full_path = matchd[1] - project = Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::Project.find_by_full_path(full_path) - return nil unless project - - project.id - end - - # Not including a leading slash - def path_relative_to_upload_dir - upload_dir = Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR # rubocop:disable Metrics/LineLength - base = %r{\A#{Regexp.escape(upload_dir)}/} - @path_relative_to_upload_dir ||= path.sub(base, '') - end - - def absolute_path - File.join(Gitlab.config.uploads.storage_path, path) - end - end - - # Avoid using application code - class Upload < ActiveRecord::Base - self.table_name = 'uploads' - end - - # Avoid using application code - class Appearance < ActiveRecord::Base - self.table_name = 'appearances' - end - - # Avoid using application code - class Namespace < ActiveRecord::Base - self.table_name = 'namespaces' - end - - # Avoid using application code - class Note < ActiveRecord::Base - self.table_name = 'notes' - end - - # Avoid using application code - class User < ActiveRecord::Base - self.table_name = 'users' - end - - # Since project Markdown upload paths don't contain the project ID, we have to find the - # project by its full_path. Due to MySQL/PostgreSQL differences, and historical reasons, - # the logic is somewhat complex, so I've mostly copied it in here. - class Project < ActiveRecord::Base - self.table_name = 'projects' - - def self.find_by_full_path(path) - binary = Gitlab::Database.mysql? ? 'BINARY' : '' - order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)" - where_full_path_in(path).reorder(order_sql).take - end - - def self.where_full_path_in(path) - cast_lower = Gitlab::Database.postgresql? - - path = connection.quote(path) - - where = - if cast_lower - "(LOWER(routes.path) = LOWER(#{path}))" - else - "(routes.path = #{path})" - end - - joins("INNER JOIN routes ON routes.source_id = projects.id AND routes.source_type = 'Project'").where(where) - end - end - end - end -end diff --git a/lib/gitlab/background_migration/prepare_untracked_uploads.rb b/lib/gitlab/background_migration/prepare_untracked_uploads.rb index 914a9e48a2f..a7a1bbe1752 100644 --- a/lib/gitlab/background_migration/prepare_untracked_uploads.rb +++ b/lib/gitlab/background_migration/prepare_untracked_uploads.rb @@ -43,11 +43,7 @@ module Gitlab store_untracked_file_paths - if UntrackedFile.all.empty? - drop_temp_table - else - schedule_populate_untracked_uploads_jobs - end + schedule_populate_untracked_uploads_jobs end private @@ -96,7 +92,7 @@ module Gitlab end end - yield(paths) if paths.any? + yield(paths) end def build_find_command(search_dir) @@ -169,13 +165,6 @@ module Gitlab bulk_queue_background_migration_jobs_by_range( UntrackedFile, FOLLOW_UP_MIGRATION) end - - def drop_temp_table - unless Rails.env.test? # Dropping a table intermittently breaks test cleanup - UntrackedFile.connection.drop_table(:untracked_files_for_uploads, - if_exists: true) - end - end end end end diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_dependencies/untracked_file_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_dependencies/untracked_file_spec.rb deleted file mode 100644 index b5c76fa6b18..00000000000 --- a/spec/lib/gitlab/background_migration/populate_untracked_uploads_dependencies/untracked_file_spec.rb +++ /dev/null @@ -1,262 +0,0 @@ -require 'spec_helper' - -# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. -describe Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile, :migration, schema: 20180206200543 do - include MigrationsHelpers::TrackUntrackedUploadsHelpers - - let!(:appearances) { table(:appearances) } - let!(:namespaces) { table(:namespaces) } - let!(:projects) { table(:projects) } - let!(:routes) { table(:routes) } - let!(:uploads) { table(:uploads) } - - before(:all) do - ensure_temporary_tracking_table_exists - end - - describe '#upload_path' do - def assert_upload_path(file_path, expected_upload_path) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.upload_path).to eq(expected_upload_path) - end - - context 'for an appearance logo file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/appearance/logo/1/some_logo.jpg', 'uploads/-/system/appearance/logo/1/some_logo.jpg') - end - end - - context 'for an appearance header_logo file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/appearance/header_logo/1/some_logo.jpg', 'uploads/-/system/appearance/header_logo/1/some_logo.jpg') - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/note/attachment/1234/some_attachment.pdf', 'uploads/-/system/note/attachment/1234/some_attachment.pdf') - end - end - - context 'for a user avatar file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/user/avatar/1234/avatar.jpg', 'uploads/-/system/user/avatar/1234/avatar.jpg') - end - end - - context 'for a group avatar file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/group/avatar/1234/avatar.jpg', 'uploads/-/system/group/avatar/1234/avatar.jpg') - end - end - - context 'for a project avatar file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/project/avatar/1234/avatar.jpg', 'uploads/-/system/project/avatar/1234/avatar.jpg') - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns the file path relative to the project directory in uploads' do - project = create_project - random_hex = SecureRandom.hex - - assert_upload_path("/#{get_full_path(project)}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg") - end - end - end - - describe '#uploader' do - def assert_uploader(file_path, expected_uploader) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.uploader).to eq(expected_uploader) - end - - context 'for an appearance logo file path' do - it 'returns AttachmentUploader as a string' do - assert_uploader('/-/system/appearance/logo/1/some_logo.jpg', 'AttachmentUploader') - end - end - - context 'for an appearance header_logo file path' do - it 'returns AttachmentUploader as a string' do - assert_uploader('/-/system/appearance/header_logo/1/some_logo.jpg', 'AttachmentUploader') - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns AttachmentUploader as a string' do - assert_uploader('/-/system/note/attachment/1234/some_attachment.pdf', 'AttachmentUploader') - end - end - - context 'for a user avatar file path' do - it 'returns AvatarUploader as a string' do - assert_uploader('/-/system/user/avatar/1234/avatar.jpg', 'AvatarUploader') - end - end - - context 'for a group avatar file path' do - it 'returns AvatarUploader as a string' do - assert_uploader('/-/system/group/avatar/1234/avatar.jpg', 'AvatarUploader') - end - end - - context 'for a project avatar file path' do - it 'returns AvatarUploader as a string' do - assert_uploader('/-/system/project/avatar/1234/avatar.jpg', 'AvatarUploader') - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns FileUploader as a string' do - project = create_project - - assert_uploader("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader') - end - end - end - - describe '#model_type' do - def assert_model_type(file_path, expected_model_type) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.model_type).to eq(expected_model_type) - end - - context 'for an appearance logo file path' do - it 'returns Appearance as a string' do - assert_model_type('/-/system/appearance/logo/1/some_logo.jpg', 'Appearance') - end - end - - context 'for an appearance header_logo file path' do - it 'returns Appearance as a string' do - assert_model_type('/-/system/appearance/header_logo/1/some_logo.jpg', 'Appearance') - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns Note as a string' do - assert_model_type('/-/system/note/attachment/1234/some_attachment.pdf', 'Note') - end - end - - context 'for a user avatar file path' do - it 'returns User as a string' do - assert_model_type('/-/system/user/avatar/1234/avatar.jpg', 'User') - end - end - - context 'for a group avatar file path' do - it 'returns Namespace as a string' do - assert_model_type('/-/system/group/avatar/1234/avatar.jpg', 'Namespace') - end - end - - context 'for a project avatar file path' do - it 'returns Project as a string' do - assert_model_type('/-/system/project/avatar/1234/avatar.jpg', 'Project') - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns Project as a string' do - project = create_project - - assert_model_type("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'Project') - end - end - end - - describe '#model_id' do - def assert_model_id(file_path, expected_model_id) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.model_id).to eq(expected_model_id) - end - - context 'for an appearance logo file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/appearance/logo/1/some_logo.jpg', 1) - end - end - - context 'for an appearance header_logo file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/appearance/header_logo/1/some_logo.jpg', 1) - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/note/attachment/1234/some_attachment.pdf', 1234) - end - end - - context 'for a user avatar file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/user/avatar/1234/avatar.jpg', 1234) - end - end - - context 'for a group avatar file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/group/avatar/1234/avatar.jpg', 1234) - end - end - - context 'for a project avatar file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/project/avatar/1234/avatar.jpg', 1234) - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns the ID as a string' do - project = create_project - - assert_model_id("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", project.id) - end - end - end - - describe '#file_size' do - context 'for an appearance logo file path' do - let(:appearance) { create_or_update_appearance(logo: true) } - let(:untracked_file) { described_class.create!(path: get_uploads(appearance, 'Appearance').first.path) } - - it 'returns the file size' do - expect(untracked_file.file_size).to eq(1062) - end - end - - context 'for a project avatar file path' do - let(:project) { create_project(avatar: true) } - let(:untracked_file) { described_class.create!(path: get_uploads(project, 'Project').first.path) } - - it 'returns the file size' do - expect(untracked_file.file_size).to eq(1062) - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - let(:project) { create_project } - let(:untracked_file) { create_untracked_file("/#{get_full_path(project)}/#{get_uploads(project, 'Project').first.path}") } - - before do - add_markdown_attachment(project) - end - - it 'returns the file size' do - expect(untracked_file.file_size).to eq(1062) - end - end - end - - def create_untracked_file(path_relative_to_upload_dir) - described_class.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}#{path_relative_to_upload_dir}") - end -end diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb index 6f11684fc48..be45c00dfe6 100644 --- a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb @@ -1,60 +1,59 @@ require 'spec_helper' -# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. -describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migration, schema: 20180206200543 do - include MigrationsHelpers::TrackUntrackedUploadsHelpers +describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq do + include TrackUntrackedUploadsHelpers subject { described_class.new } - let!(:appearances) { table(:appearances) } - let!(:namespaces) { table(:namespaces) } - let!(:notes) { table(:notes) } - let!(:projects) { table(:projects) } - let!(:routes) { table(:routes) } - let!(:untracked_files_for_uploads) { table(:untracked_files_for_uploads) } - let!(:uploads) { table(:uploads) } - let!(:users) { table(:users) } + let!(:untracked_files_for_uploads) { described_class::UntrackedFile } + let!(:uploads) { described_class::Upload } before do + DatabaseCleaner.clean + drop_temp_table_if_exists ensure_temporary_tracking_table_exists uploads.delete_all end + after(:all) do + drop_temp_table_if_exists + end + context 'with untracked files and tracked files in untracked_files_for_uploads' do - let!(:appearance) { create_or_update_appearance(logo: true, header_logo: true) } - let!(:user1) { create_user(avatar: true) } - let!(:user2) { create_user(avatar: true) } - let!(:project1) { create_project(avatar: true) } - let!(:project2) { create_project(avatar: true) } + let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } + let!(:user1) { create(:user, :with_avatar) } + let!(:user2) { create(:user, :with_avatar) } + let!(:project1) { create(:project, :with_avatar) } + let!(:project2) { create(:project, :with_avatar) } before do - add_markdown_attachment(project1) - add_markdown_attachment(project2) + UploadService.new(project1, uploaded_file, FileUploader).execute # Markdown upload + UploadService.new(project2, uploaded_file, FileUploader).execute # Markdown upload # File records created by PrepareUntrackedUploads - untracked_files_for_uploads.create!(path: get_uploads(appearance, 'Appearance').first.path) - untracked_files_for_uploads.create!(path: get_uploads(appearance, 'Appearance').last.path) - untracked_files_for_uploads.create!(path: get_uploads(user1, 'User').first.path) - untracked_files_for_uploads.create!(path: get_uploads(user2, 'User').first.path) - untracked_files_for_uploads.create!(path: get_uploads(project1, 'Project').first.path) - untracked_files_for_uploads.create!(path: get_uploads(project2, 'Project').first.path) - untracked_files_for_uploads.create!(path: "#{legacy_project_uploads_dir(project1).sub("#{MigrationsHelpers::TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project1, 'Project').last.path}") - untracked_files_for_uploads.create!(path: "#{legacy_project_uploads_dir(project2).sub("#{MigrationsHelpers::TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project2, 'Project').last.path}") + untracked_files_for_uploads.create!(path: appearance.uploads.first.path) + untracked_files_for_uploads.create!(path: appearance.uploads.last.path) + untracked_files_for_uploads.create!(path: user1.uploads.first.path) + untracked_files_for_uploads.create!(path: user2.uploads.first.path) + untracked_files_for_uploads.create!(path: project1.uploads.first.path) + untracked_files_for_uploads.create!(path: project2.uploads.first.path) + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project1.full_path}/#{project1.uploads.last.path}") + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/#{project2.uploads.last.path}") # Untrack 4 files - get_uploads(user2, 'User').delete_all - get_uploads(project2, 'Project').delete_all # 2 files: avatar and a Markdown upload - get_uploads(appearance, 'Appearance').where("path like '%header_logo%'").delete_all + user2.uploads.delete_all + project2.uploads.delete_all # 2 files: avatar and a Markdown upload + appearance.uploads.where("path like '%header_logo%'").delete_all end it 'adds untracked files to the uploads table' do expect do - subject.perform(1, untracked_files_for_uploads.reorder(:id).last.id) + subject.perform(1, untracked_files_for_uploads.last.id) end.to change { uploads.count }.from(4).to(8) - expect(get_uploads(user2, 'User').count).to eq(1) - expect(get_uploads(project2, 'Project').count).to eq(2) - expect(get_uploads(appearance, 'Appearance').count).to eq(2) + expect(user2.uploads.count).to eq(1) + expect(project2.uploads.count).to eq(2) + expect(appearance.uploads.count).to eq(2) end it 'deletes rows after processing them' do @@ -68,9 +67,9 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra it 'does not create duplicate uploads of already tracked files' do subject.perform(1, untracked_files_for_uploads.last.id) - expect(get_uploads(user1, 'User').count).to eq(1) - expect(get_uploads(project1, 'Project').count).to eq(2) - expect(get_uploads(appearance, 'Appearance').count).to eq(2) + expect(user1.uploads.count).to eq(1) + expect(project1.uploads.count).to eq(2) + expect(appearance.uploads.count).to eq(2) end it 'uses the start and end batch ids [only 1st half]' do @@ -82,11 +81,11 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra subject.perform(start_id, end_id) end.to change { uploads.count }.from(4).to(6) - expect(get_uploads(user1, 'User').count).to eq(1) - expect(get_uploads(user2, 'User').count).to eq(1) - expect(get_uploads(appearance, 'Appearance').count).to eq(2) - expect(get_uploads(project1, 'Project').count).to eq(2) - expect(get_uploads(project2, 'Project').count).to eq(0) + expect(user1.uploads.count).to eq(1) + expect(user2.uploads.count).to eq(1) + expect(appearance.uploads.count).to eq(2) + expect(project1.uploads.count).to eq(2) + expect(project2.uploads.count).to eq(0) # Only 4 have been either confirmed or added to uploads expect(untracked_files_for_uploads.count).to eq(4) @@ -101,11 +100,11 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra subject.perform(start_id, end_id) end.to change { uploads.count }.from(4).to(6) - expect(get_uploads(user1, 'User').count).to eq(1) - expect(get_uploads(user2, 'User').count).to eq(0) - expect(get_uploads(appearance, 'Appearance').count).to eq(1) - expect(get_uploads(project1, 'Project').count).to eq(2) - expect(get_uploads(project2, 'Project').count).to eq(2) + expect(user1.uploads.count).to eq(1) + expect(user2.uploads.count).to eq(0) + expect(appearance.uploads.count).to eq(1) + expect(project1.uploads.count).to eq(2) + expect(project2.uploads.count).to eq(2) # Only 4 have been either confirmed or added to uploads expect(untracked_files_for_uploads.count).to eq(4) @@ -118,13 +117,13 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra end it 'drops the temporary tracking table after processing the batch, if there are no untracked rows left' do - expect(subject).to receive(:drop_temp_table_if_finished) - subject.perform(1, untracked_files_for_uploads.last.id) + + expect(ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads)).to be_falsey end it 'does not block a whole batch because of one bad path' do - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(project2)}/._7d37bf4c747916390e596744117d5d1a") + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a") expect(untracked_files_for_uploads.count).to eq(9) expect(uploads.count).to eq(4) @@ -135,7 +134,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra end it 'an unparseable path is shown in error output' do - bad_path = "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(project2)}/._7d37bf4c747916390e596744117d5d1a" + bad_path = "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a" untracked_files_for_uploads.create!(path: bad_path) expect(Rails.logger).to receive(:error).with(/Error parsing path "#{bad_path}":/) @@ -154,100 +153,367 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra describe 'upload outcomes for each path pattern' do shared_examples_for 'non_markdown_file' do - let!(:expected_upload_attrs) { model_uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') } + let!(:expected_upload_attrs) { model.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') } let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) } before do - model_uploads.delete_all + model.uploads.delete_all end it 'creates an Upload record' do expect do subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { model_uploads.count }.from(0).to(1) + end.to change { model.reload.uploads.count }.from(0).to(1) - expect(model_uploads.first.attributes).to include(expected_upload_attrs) + expect(model.uploads.first.attributes).to include(expected_upload_attrs) end end context 'for an appearance logo file path' do - let(:model) { create_or_update_appearance(logo: true) } - let(:model_uploads) { get_uploads(model, 'Appearance') } + let(:model) { create_or_update_appearance(logo: uploaded_file) } it_behaves_like 'non_markdown_file' end context 'for an appearance header_logo file path' do - let(:model) { create_or_update_appearance(header_logo: true) } - let(:model_uploads) { get_uploads(model, 'Appearance') } + let(:model) { create_or_update_appearance(header_logo: uploaded_file) } it_behaves_like 'non_markdown_file' end context 'for a pre-Markdown Note attachment file path' do - let(:model) { create_note(attachment: true) } - let!(:expected_upload_attrs) { get_uploads(model, 'Note').first.attributes.slice('path', 'uploader', 'size', 'checksum') } + let(:model) { create(:note, :with_attachment) } + let!(:expected_upload_attrs) { Upload.where(model_type: 'Note', model_id: model.id).first.attributes.slice('path', 'uploader', 'size', 'checksum') } let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) } before do - get_uploads(model, 'Note').delete_all + Upload.where(model_type: 'Note', model_id: model.id).delete_all end # Can't use the shared example because Note doesn't have an `uploads` association it 'creates an Upload record' do expect do subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { get_uploads(model, 'Note').count }.from(0).to(1) + end.to change { Upload.where(model_type: 'Note', model_id: model.id).count }.from(0).to(1) - expect(get_uploads(model, 'Note').first.attributes).to include(expected_upload_attrs) + expect(Upload.where(model_type: 'Note', model_id: model.id).first.attributes).to include(expected_upload_attrs) end end context 'for a user avatar file path' do - let(:model) { create_user(avatar: true) } - let(:model_uploads) { get_uploads(model, 'User') } + let(:model) { create(:user, :with_avatar) } it_behaves_like 'non_markdown_file' end context 'for a group avatar file path' do - let(:model) { create_group(avatar: true) } - let(:model_uploads) { get_uploads(model, 'Namespace') } + let(:model) { create(:group, :with_avatar) } it_behaves_like 'non_markdown_file' end context 'for a project avatar file path' do - let(:model) { create_project(avatar: true) } - let(:model_uploads) { get_uploads(model, 'Project') } + let(:model) { create(:project, :with_avatar) } it_behaves_like 'non_markdown_file' end context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - let(:model) { create_project } + let(:model) { create(:project) } before do # Upload the file - add_markdown_attachment(model) + UploadService.new(model, uploaded_file, FileUploader).execute # Create the untracked_files_for_uploads record - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(model)}/#{get_uploads(model, 'Project').first.path}") + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{model.full_path}/#{model.uploads.first.path}") # Save the expected upload attributes - @expected_upload_attrs = get_uploads(model, 'Project').first.attributes.slice('path', 'uploader', 'size', 'checksum') + @expected_upload_attrs = model.reload.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') # Untrack the file - get_uploads(model, 'Project').delete_all + model.reload.uploads.delete_all end it 'creates an Upload record' do expect do subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { get_uploads(model, 'Project').count }.from(0).to(1) + end.to change { model.reload.uploads.count }.from(0).to(1) + + expect(model.uploads.first.attributes).to include(@expected_upload_attrs) + end + end + end +end + +describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do + include TrackUntrackedUploadsHelpers + + let(:upload_class) { Gitlab::BackgroundMigration::PopulateUntrackedUploads::Upload } + + before(:all) do + ensure_temporary_tracking_table_exists + end + + after(:all) do + drop_temp_table_if_exists + end + + describe '#upload_path' do + def assert_upload_path(file_path, expected_upload_path) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.upload_path).to eq(expected_upload_path) + end + + context 'for an appearance logo file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/appearance/logo/1/some_logo.jpg', 'uploads/-/system/appearance/logo/1/some_logo.jpg') + end + end + + context 'for an appearance header_logo file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/appearance/header_logo/1/some_logo.jpg', 'uploads/-/system/appearance/header_logo/1/some_logo.jpg') + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/note/attachment/1234/some_attachment.pdf', 'uploads/-/system/note/attachment/1234/some_attachment.pdf') + end + end + + context 'for a user avatar file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/user/avatar/1234/avatar.jpg', 'uploads/-/system/user/avatar/1234/avatar.jpg') + end + end + + context 'for a group avatar file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/group/avatar/1234/avatar.jpg', 'uploads/-/system/group/avatar/1234/avatar.jpg') + end + end + + context 'for a project avatar file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/project/avatar/1234/avatar.jpg', 'uploads/-/system/project/avatar/1234/avatar.jpg') + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns the file path relative to the project directory in uploads' do + project = create(:project) + random_hex = SecureRandom.hex + + assert_upload_path("/#{project.full_path}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg") + end + end + end + + describe '#uploader' do + def assert_uploader(file_path, expected_uploader) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.uploader).to eq(expected_uploader) + end + + context 'for an appearance logo file path' do + it 'returns AttachmentUploader as a string' do + assert_uploader('/-/system/appearance/logo/1/some_logo.jpg', 'AttachmentUploader') + end + end + + context 'for an appearance header_logo file path' do + it 'returns AttachmentUploader as a string' do + assert_uploader('/-/system/appearance/header_logo/1/some_logo.jpg', 'AttachmentUploader') + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns AttachmentUploader as a string' do + assert_uploader('/-/system/note/attachment/1234/some_attachment.pdf', 'AttachmentUploader') + end + end + + context 'for a user avatar file path' do + it 'returns AvatarUploader as a string' do + assert_uploader('/-/system/user/avatar/1234/avatar.jpg', 'AvatarUploader') + end + end + + context 'for a group avatar file path' do + it 'returns AvatarUploader as a string' do + assert_uploader('/-/system/group/avatar/1234/avatar.jpg', 'AvatarUploader') + end + end + + context 'for a project avatar file path' do + it 'returns AvatarUploader as a string' do + assert_uploader('/-/system/project/avatar/1234/avatar.jpg', 'AvatarUploader') + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns FileUploader as a string' do + project = create(:project) + + assert_uploader("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader') + end + end + end + + describe '#model_type' do + def assert_model_type(file_path, expected_model_type) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.model_type).to eq(expected_model_type) + end - expect(get_uploads(model, 'Project').first.attributes).to include(@expected_upload_attrs) + context 'for an appearance logo file path' do + it 'returns Appearance as a string' do + assert_model_type('/-/system/appearance/logo/1/some_logo.jpg', 'Appearance') + end + end + + context 'for an appearance header_logo file path' do + it 'returns Appearance as a string' do + assert_model_type('/-/system/appearance/header_logo/1/some_logo.jpg', 'Appearance') + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns Note as a string' do + assert_model_type('/-/system/note/attachment/1234/some_attachment.pdf', 'Note') + end + end + + context 'for a user avatar file path' do + it 'returns User as a string' do + assert_model_type('/-/system/user/avatar/1234/avatar.jpg', 'User') + end + end + + context 'for a group avatar file path' do + it 'returns Namespace as a string' do + assert_model_type('/-/system/group/avatar/1234/avatar.jpg', 'Namespace') + end + end + + context 'for a project avatar file path' do + it 'returns Project as a string' do + assert_model_type('/-/system/project/avatar/1234/avatar.jpg', 'Project') + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns Project as a string' do + project = create(:project) + + assert_model_type("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'Project') + end + end + end + + describe '#model_id' do + def assert_model_id(file_path, expected_model_id) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.model_id).to eq(expected_model_id) + end + + context 'for an appearance logo file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/appearance/logo/1/some_logo.jpg', 1) + end + end + + context 'for an appearance header_logo file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/appearance/header_logo/1/some_logo.jpg', 1) + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/note/attachment/1234/some_attachment.pdf', 1234) + end + end + + context 'for a user avatar file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/user/avatar/1234/avatar.jpg', 1234) + end + end + + context 'for a group avatar file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/group/avatar/1234/avatar.jpg', 1234) + end + end + + context 'for a project avatar file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/project/avatar/1234/avatar.jpg', 1234) end end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns the ID as a string' do + project = create(:project) + + assert_model_id("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", project.id) + end + end + end + + describe '#file_size' do + context 'for an appearance logo file path' do + let(:appearance) { create_or_update_appearance(logo: uploaded_file) } + let(:untracked_file) { described_class.create!(path: appearance.uploads.first.path) } + + it 'returns the file size' do + expect(untracked_file.file_size).to eq(35255) + end + + it 'returns the same thing that CarrierWave would return' do + expect(untracked_file.file_size).to eq(appearance.logo.size) + end + end + + context 'for a project avatar file path' do + let(:project) { create(:project, avatar: uploaded_file) } + let(:untracked_file) { described_class.create!(path: project.uploads.first.path) } + + it 'returns the file size' do + expect(untracked_file.file_size).to eq(35255) + end + + it 'returns the same thing that CarrierWave would return' do + expect(untracked_file.file_size).to eq(project.avatar.size) + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + let(:project) { create(:project) } + let(:untracked_file) { create_untracked_file("/#{project.full_path}/#{project.uploads.first.path}") } + + before do + UploadService.new(project, uploaded_file, FileUploader).execute + end + + it 'returns the file size' do + expect(untracked_file.file_size).to eq(35255) + end + + it 'returns the same thing that CarrierWave would return' do + expect(untracked_file.file_size).to eq(project.uploads.first.size) + end + end + end + + def create_untracked_file(path_relative_to_upload_dir) + described_class.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}#{path_relative_to_upload_dir}") end end diff --git a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb index 688e0e5f54d..370c2490b97 100644 --- a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb @@ -1,16 +1,20 @@ require 'spec_helper' -# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. -describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migration, schema: 20180206200543 do - include MigrationsHelpers::TrackUntrackedUploadsHelpers - - let!(:untracked_files_for_uploads) { table(:untracked_files_for_uploads) } - let!(:appearances) { table(:appearances) } - let!(:namespaces) { table(:namespaces) } - let!(:projects) { table(:projects) } - let!(:routes) { table(:routes) } - let!(:uploads) { table(:uploads) } - let!(:users) { table(:users) } +describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do + include TrackUntrackedUploadsHelpers + include MigrationsHelpers + + let!(:untracked_files_for_uploads) { described_class::UntrackedFile } + + before do + DatabaseCleaner.clean + + drop_temp_table_if_exists + end + + after do + drop_temp_table_if_exists + end around do |example| # Especially important so the follow-up migration does not get run @@ -19,34 +23,71 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migrat end end - shared_examples 'prepares the untracked_files_for_uploads table' do - context 'when files were uploaded before and after hashed storage was enabled' do - let!(:appearance) { create_or_update_appearance(logo: true, header_logo: true) } - let!(:user) { create_user(avatar: true) } - let!(:project1) { create_project(avatar: true) } - let(:project2) { create_project } # instantiate after enabling hashed_storage + # E.g. The installation is in use at the time of migration, and someone has + # just uploaded a file + shared_examples 'does not add files in /uploads/tmp' do + let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') } - before do - # Markdown upload before enabling hashed_storage - add_markdown_attachment(project1) + before do + FileUtils.mkdir(File.dirname(tmp_file)) + FileUtils.touch(tmp_file) + end - # Markdown upload after enabling hashed_storage - add_markdown_attachment(project2, hashed_storage: true) + after do + FileUtils.rm(tmp_file) + end + + it 'does not add files from /uploads/tmp' do + described_class.new.perform + + expect(untracked_files_for_uploads.count).to eq(5) + end + end + + it 'ensures the untracked_files_for_uploads table exists' do + expect do + described_class.new.perform + end.to change { ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads) }.from(false).to(true) + end + + it 'has a path field long enough for really long paths' do + described_class.new.perform + + component = 'a' * 255 + + long_path = [ + 'uploads', + component, # project.full_path + component # filename + ].flatten.join('/') + + record = untracked_files_for_uploads.create!(path: long_path) + expect(record.reload.path.size).to eq(519) + end + + context "test bulk insert with ON CONFLICT DO NOTHING or IGNORE" do + around do |example| + # If this is CI, we use Postgres 9.2 so this whole context should be + # skipped since we're unable to use ON CONFLICT DO NOTHING or IGNORE. + if described_class.new.send(:can_bulk_insert_and_ignore_duplicates?) + example.run end + end - it 'has a path field long enough for really long paths' do - described_class.new.perform + context 'when files were uploaded before and after hashed storage was enabled' do + let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } + let!(:user) { create(:user, :with_avatar) } + let!(:project1) { create(:project, :with_avatar) } + let(:project2) { create(:project) } # instantiate after enabling hashed_storage - component = 'a' * 255 + before do + # Markdown upload before enabling hashed_storage + UploadService.new(project1, uploaded_file, FileUploader).execute - long_path = [ - 'uploads', - component, # project.full_path - component # filename - ].flatten.join('/') + stub_application_setting(hashed_storage_enabled: true) - record = untracked_files_for_uploads.create!(path: long_path) - expect(record.reload.path.size).to eq(519) + # Markdown upload after enabling hashed_storage + UploadService.new(project2, uploaded_file, FileUploader).execute end it 'adds unhashed files to the untracked_files_for_uploads table' do @@ -65,15 +106,14 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migrat it 'does not add hashed files to the untracked_files_for_uploads table' do described_class.new.perform - hashed_file_path = get_uploads(project2, 'Project').where(uploader: 'FileUploader').first.path + hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey end it 'correctly schedules the follow-up background migration jobs' do described_class.new.perform - ids = described_class::UntrackedFile.all.order(:id).pluck(:id) - expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(ids.first, ids.last) + expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(1, 5) expect(BackgroundMigrationWorker.jobs.size).to eq(1) end @@ -90,68 +130,91 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migrat end end - # E.g. The installation is in use at the time of migration, and someone has - # just uploaded a file context 'when there are files in /uploads/tmp' do - let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') } + it_behaves_like 'does not add files in /uploads/tmp' + end + end + end - before do - FileUtils.mkdir(File.dirname(tmp_file)) - FileUtils.touch(tmp_file) - end + context 'test bulk insert without ON CONFLICT DO NOTHING or IGNORE' do + before do + # If this is CI, we use Postgres 9.2 so this stub has no effect. + # + # If this is being run on Postgres 9.5+ or MySQL, then this stub allows us + # to test the bulk insert functionality without ON CONFLICT DO NOTHING or + # IGNORE. + allow_any_instance_of(described_class).to receive(:postgresql_pre_9_5?).and_return(true) + end - after do - FileUtils.rm(tmp_file) - end + context 'when files were uploaded before and after hashed storage was enabled' do + let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } + let!(:user) { create(:user, :with_avatar) } + let!(:project1) { create(:project, :with_avatar) } + let(:project2) { create(:project) } # instantiate after enabling hashed_storage - it 'does not add files from /uploads/tmp' do - described_class.new.perform + before do + # Markdown upload before enabling hashed_storage + UploadService.new(project1, uploaded_file, FileUploader).execute - expect(untracked_files_for_uploads.count).to eq(5) + stub_application_setting(hashed_storage_enabled: true) + + # Markdown upload after enabling hashed_storage + UploadService.new(project2, uploaded_file, FileUploader).execute + end + + it 'adds unhashed files to the untracked_files_for_uploads table' do + described_class.new.perform + + expect(untracked_files_for_uploads.count).to eq(5) + end + + it 'adds files with paths relative to CarrierWave.root' do + described_class.new.perform + untracked_files_for_uploads.all.each do |file| + expect(file.path.start_with?('uploads/')).to be_truthy end end - context 'when the last batch size exactly matches the max batch size' do - it 'does not raise error' do - stub_const("#{described_class}::FIND_BATCH_SIZE", 5) + it 'does not add hashed files to the untracked_files_for_uploads table' do + described_class.new.perform + hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path + expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey + end + + it 'correctly schedules the follow-up background migration jobs' do + described_class.new.perform + + expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(1, 5) + expect(BackgroundMigrationWorker.jobs.size).to eq(1) + end + + # E.g. from a previous failed run of this background migration + context 'when there is existing data in untracked_files_for_uploads' do + before do + described_class.new.perform + end + + it 'does not error or produce duplicates of existing data' do expect do described_class.new.perform - end.not_to raise_error - - expect(untracked_files_for_uploads.count).to eq(5) + end.not_to change { untracked_files_for_uploads.count }.from(5) end end - end - end - - # If running on Postgres 9.2 (like on CI), this whole context is skipped - # since we're unable to use ON CONFLICT DO NOTHING or IGNORE. - context "test bulk insert with ON CONFLICT DO NOTHING or IGNORE", if: described_class.new.send(:can_bulk_insert_and_ignore_duplicates?) do - it_behaves_like 'prepares the untracked_files_for_uploads table' - end - # If running on Postgres 9.2 (like on CI), the stubbed method has no effect. - # - # If running on Postgres 9.5+ or MySQL, then this context effectively tests - # the bulk insert functionality without ON CONFLICT DO NOTHING or IGNORE. - context 'test bulk insert without ON CONFLICT DO NOTHING or IGNORE' do - before do - allow_any_instance_of(described_class).to receive(:postgresql_pre_9_5?).and_return(true) + context 'when there are files in /uploads/tmp' do + it_behaves_like 'does not add files in /uploads/tmp' + end end - - it_behaves_like 'prepares the untracked_files_for_uploads table' end # Very new or lightly-used installations that are running this migration # may not have an upload directory because they have no uploads. context 'when no files were ever uploaded' do - it 'deletes the `untracked_files_for_uploads` table (and does not raise error)' do - background_migration = described_class.new - - expect(background_migration).to receive(:drop_temp_table) + it 'does not add to the untracked_files_for_uploads table (and does not raise error)' do + described_class.new.perform - background_migration.perform + expect(untracked_files_for_uploads.count).to eq(0) end end end diff --git a/spec/migrations/track_untracked_uploads_spec.rb b/spec/migrations/track_untracked_uploads_spec.rb index 2fccfb3f12c..fe4d5b8a279 100644 --- a/spec/migrations/track_untracked_uploads_spec.rb +++ b/spec/migrations/track_untracked_uploads_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20171103140253_track_untracked_uploads') describe TrackUntrackedUploads, :migration, :sidekiq do - include MigrationsHelpers::TrackUntrackedUploadsHelpers + include TrackUntrackedUploadsHelpers it 'correctly schedules the follow-up background migration' do Sidekiq::Testing.fake! do diff --git a/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb b/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb deleted file mode 100644 index 016bcfa9b1b..00000000000 --- a/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb +++ /dev/null @@ -1,128 +0,0 @@ -module MigrationsHelpers - module TrackUntrackedUploadsHelpers - PUBLIC_DIR = File.join(Rails.root, 'tmp', 'tests', 'public') - UPLOADS_DIR = File.join(PUBLIC_DIR, 'uploads') - SYSTEM_DIR = File.join(UPLOADS_DIR, '-', 'system') - UPLOAD_FILENAME = 'image.png'.freeze - FIXTURE_FILE_PATH = File.join(Rails.root, 'spec', 'fixtures', 'dk.png') - FIXTURE_CHECKSUM = 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75'.freeze - - def create_or_update_appearance(logo: false, header_logo: false) - appearance = appearances.first_or_create(title: 'foo', description: 'bar', logo: (UPLOAD_FILENAME if logo), header_logo: (UPLOAD_FILENAME if header_logo)) - - add_upload(appearance, 'Appearance', 'logo', 'AttachmentUploader') if logo - add_upload(appearance, 'Appearance', 'header_logo', 'AttachmentUploader') if header_logo - - appearance - end - - def create_group(avatar: false) - index = unique_index(:group) - group = namespaces.create(name: "group#{index}", path: "group#{index}", avatar: (UPLOAD_FILENAME if avatar)) - - add_upload(group, 'Group', 'avatar', 'AvatarUploader') if avatar - - group - end - - def create_note(attachment: false) - note = notes.create(attachment: (UPLOAD_FILENAME if attachment)) - - add_upload(note, 'Note', 'attachment', 'AttachmentUploader') if attachment - - note - end - - def create_project(avatar: false) - group = create_group - project = projects.create(namespace_id: group.id, path: "project#{unique_index(:project)}", avatar: (UPLOAD_FILENAME if avatar)) - routes.create(path: "#{group.path}/#{project.path}", source_id: project.id, source_type: 'Project') # so Project.find_by_full_path works - - add_upload(project, 'Project', 'avatar', 'AvatarUploader') if avatar - - project - end - - def create_user(avatar: false) - user = users.create(email: "foo#{unique_index(:user)}@bar.com", avatar: (UPLOAD_FILENAME if avatar), projects_limit: 100) - - add_upload(user, 'User', 'avatar', 'AvatarUploader') if avatar - - user - end - - def unique_index(name = :unnamed) - @unique_index ||= {} - @unique_index[name] ||= 0 - @unique_index[name] += 1 - end - - def add_upload(model, model_type, attachment_type, uploader) - file_path = upload_file_path(model, model_type, attachment_type) - path_relative_to_public = file_path.sub("#{PUBLIC_DIR}/", '') - create_file(file_path) - - uploads.create!( - size: 1062, - path: path_relative_to_public, - model_id: model.id, - model_type: model_type == 'Group' ? 'Namespace' : model_type, - uploader: uploader, - checksum: FIXTURE_CHECKSUM - ) - end - - def add_markdown_attachment(project, hashed_storage: false) - project_dir = hashed_storage ? hashed_project_uploads_dir(project) : legacy_project_uploads_dir(project) - attachment_dir = File.join(project_dir, SecureRandom.hex) - attachment_file_path = File.join(attachment_dir, UPLOAD_FILENAME) - project_attachment_path_relative_to_project = attachment_file_path.sub("#{project_dir}/", '') - create_file(attachment_file_path) - - uploads.create!( - size: 1062, - path: project_attachment_path_relative_to_project, - model_id: project.id, - model_type: 'Project', - uploader: 'FileUploader', - checksum: FIXTURE_CHECKSUM - ) - end - - def legacy_project_uploads_dir(project) - namespace = namespaces.find_by(id: project.namespace_id) - File.join(UPLOADS_DIR, namespace.path, project.path) - end - - def hashed_project_uploads_dir(project) - File.join(UPLOADS_DIR, '@hashed', 'aa', 'aaaaaaaaaaaa') - end - - def upload_file_path(model, model_type, attachment_type) - dir = File.join(upload_dir(model_type.downcase, attachment_type.to_s), model.id.to_s) - File.join(dir, UPLOAD_FILENAME) - end - - def upload_dir(model_type, attachment_type) - File.join(SYSTEM_DIR, model_type, attachment_type) - end - - def create_file(path) - File.delete(path) if File.exist?(path) - FileUtils.mkdir_p(File.dirname(path)) - FileUtils.cp(FIXTURE_FILE_PATH, path) - end - - def get_uploads(model, model_type) - uploads.where(model_type: model_type, model_id: model.id) - end - - def get_full_path(project) - routes.find_by(source_id: project.id, source_type: 'Project').path - end - - def ensure_temporary_tracking_table_exists - Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists) - end - end -end |