artifactory-gem_import 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +3 -0
  4. data/CODE_OF_CONDUCT.md +74 -0
  5. data/Gemfile +9 -0
  6. data/README.md +57 -0
  7. data/Rakefile +6 -0
  8. data/artifactory-gem_import.gemspec +31 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/exe/artifactory-gem-import +13 -0
  12. data/lib/artifactory/gem_import.rb +44 -0
  13. data/lib/artifactory/gem_import/bookkeeper.rb +24 -0
  14. data/lib/artifactory/gem_import/bookkeeper/base.rb +27 -0
  15. data/lib/artifactory/gem_import/bookkeeper/counter.rb +29 -0
  16. data/lib/artifactory/gem_import/bookkeeper/publisher.rb +32 -0
  17. data/lib/artifactory/gem_import/bookkeeper/reviewer.rb +27 -0
  18. data/lib/artifactory/gem_import/cli.rb +79 -0
  19. data/lib/artifactory/gem_import/gem.rb +54 -0
  20. data/lib/artifactory/gem_import/gem/errors.rb +35 -0
  21. data/lib/artifactory/gem_import/gem_specs.rb +28 -0
  22. data/lib/artifactory/gem_import/gem_specs/downloader.rb +31 -0
  23. data/lib/artifactory/gem_import/gem_specs/parser.rb +28 -0
  24. data/lib/artifactory/gem_import/gem_specs/specs.rb +26 -0
  25. data/lib/artifactory/gem_import/gems.rb +28 -0
  26. data/lib/artifactory/gem_import/gems/cleaner.rb +21 -0
  27. data/lib/artifactory/gem_import/gems/downloader.rb +29 -0
  28. data/lib/artifactory/gem_import/gems/uploader.rb +28 -0
  29. data/lib/artifactory/gem_import/gems/verifier.rb +23 -0
  30. data/lib/artifactory/gem_import/repo.rb +20 -0
  31. data/lib/artifactory/gem_import/version.rb +5 -0
  32. data/lib/artifactory/gem_import/worker.rb +12 -0
  33. data/lib/artifactory/gem_import/worker/base.rb +29 -0
  34. data/lib/artifactory/gem_import/worker/importer.rb +152 -0
  35. data/lib/artifactory/gem_import/worker/missing_detector.rb +29 -0
  36. data/lib/artifactory/gem_import/worker/remover.rb +76 -0
  37. metadata +111 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a78f4d3790cd4ba1a1432a4e4e29aec1f5ca5fad92e8be32e9f8608d87e8a180
4
+ data.tar.gz: a0d17f0967a594ff6de0a0a55fd26bf52db56aa29456b84938a8e0f0b1459179
5
+ SHA512:
6
+ metadata.gz: 82121eacfe3848c110594e09cdc58e74ba9036bec2154a8f7e2c1c85bf68854c44f07ca0c658229c4a4537d391e96657ffb5669435fca98c75ea9d06e64f8588
7
+ data.tar.gz: 2ab395d19f10a0e9cd41d3971c1c920742c45abeec120602223c07991198643171a232482c3bb5dfa9c0f59d1446de0e581a051f8d7be6a78c4a30948797ea41
@@ -0,0 +1,17 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ .vscode
14
+ .idea
15
+ .byebug_history
16
+ Gemfile.lock
17
+ .env.local
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at thomas.scholz@swisscom.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in artifactory-gem_import.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "rspec", "~> 3.0"
8
+ gem "byebug", "~> 11.1"
9
+ gem "webmock", "~> 3.8"
@@ -0,0 +1,57 @@
1
+ # Artifactory::GemImport
2
+
3
+ A command line tool for importing gems into a JFrog Artifactory gem repository server.
4
+
5
+ If you just want to copy gems from your existing GemServer to the Artifactory
6
+ you might want to have a look at the `gem mirror` command and the `jfrog` command
7
+ line utility that is able to upload your files too.
8
+
9
+ If you want to migrate you local gem repository over time and if you want to keep
10
+ your old GemServer kind of "mirrored" to the Artifactory, this tool might be useful
11
+ to you e.g. as part of the CI/CD pipeline when releasing gems.
12
+
13
+ ## How it works
14
+ - it diffs the specs.4.8.gz of both repositories
15
+ - it down- and uploads the missing gems only
16
+ - it verifies the MD5 hash of the uploaded gem file with the cached one
17
+ - it deletes the uploaded gem if the checksums do not match
18
+
19
+ ## Installation
20
+
21
+ ```ruby
22
+ gem 'artifactory-gem_import'
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ Show gems not already present in the Artifactory.
28
+ ```shell
29
+ $ artifactory-gem-import show-missing --source-repo https://your-repo.local/private --target-repo https://your-artifactory.local/gems-local --target-repo-api-key <api-key> [--only "+."]
30
+ ```
31
+
32
+ Import gems into the Artifactory.
33
+ ```shell
34
+ $ artifactory-gem-import import --source-repo https://your-repo.local/private --target-repo https://your-artifactory.local/gems-local --target-repo-api-key <api-key> [--only "+."]
35
+ ```
36
+
37
+ Delete gems from the Artifactory. It will NOT remove the `rubygems-update` gem as it seems to be needed to keep the gem repository working.
38
+ ```shell
39
+ $ artifactory-gem-import delete --target-repo https://your-artifactory.local/gems-local --target-repo-api-key <api-key> [--only "+."]
40
+ ```
41
+
42
+ Keep in mind that Artifactory needs some time to apply your changes.
43
+
44
+ ## Development
45
+
46
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
47
+
48
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
49
+
50
+ ## Contributing
51
+
52
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/artifactory-gem_import. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/artifactory-gem_import/blob/master/CODE_OF_CONDUCT.md).
53
+
54
+
55
+ ## Code of Conduct
56
+
57
+ Everyone interacting in the Artifactory::GemImport project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/artifactory-gem_import/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,31 @@
1
+ require_relative "lib/artifactory/gem_import/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "artifactory-gem_import"
5
+ spec.version = Artifactory::GemImport::VERSION
6
+ spec.authors = ["Thomas Scholz"]
7
+ spec.email = ["thomas.scholz@rubyapps.ch"]
8
+
9
+ spec.summary = %q{Artifactory Gem Import.}
10
+ spec.description = %q{Artifactory Gem Import for importing gems from existing Gem Repo into a new Artifactory Gem Repo.}
11
+ spec.homepage = "https://github.com/tscholz/artifactory-gem_import"
12
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
13
+
14
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_dependency "httparty", "~> 0.18"
30
+ spec.add_dependency "thor", "~> 1.0"
31
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "artifactory/gem_import"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ Signal.trap("INT") { exit 1 }
5
+
6
+ require_relative "../lib/artifactory/gem_import"
7
+ require_relative "../lib/artifactory/gem_import/cli"
8
+
9
+ begin
10
+ Artifactory::GemImport::Cli.start(ARGV)
11
+ rescue Artifactory::GemImport::Error => err
12
+ abort "ERROR: #{err}"
13
+ end
@@ -0,0 +1,44 @@
1
+ require_relative "gem_import/bookkeeper"
2
+ require_relative "gem_import/gem"
3
+ require_relative "gem_import/gems"
4
+ require_relative "gem_import/gem_specs"
5
+ require_relative "gem_import/repo"
6
+ require_relative "gem_import/version"
7
+ require_relative "gem_import/worker"
8
+
9
+ module Artifactory
10
+ module GemImport
11
+ Error = Class.new StandardError
12
+ ClientError = Class.new Error
13
+
14
+ module_function
15
+
16
+ def import!(source_repo:, target_repo:, only: /.+/)
17
+ Worker::Importer
18
+ .new(source_repo: source_repo, target_repo: target_repo, only: only)
19
+ .import!
20
+ end
21
+
22
+ def show_missing(source_repo:, target_repo:, only: /.+/)
23
+ Worker::MissingDetector
24
+ .new(source_repo: source_repo, target_repo: target_repo, only: only)
25
+ .detect!
26
+ end
27
+
28
+ def delete!(repo:, only: /.+/)
29
+ Worker::Remover
30
+ .new(target_repo: repo, only: only)
31
+ .remove!
32
+ end
33
+
34
+ def source_repo(url:)
35
+ Repo.new url: url,
36
+ headers: {}
37
+ end
38
+
39
+ def target_repo(url:, api_key:)
40
+ Repo.new url: url,
41
+ headers: { "X-JFrog-Art-Api" => api_key }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,24 @@
1
+ require_relative "bookkeeper/base"
2
+ require_relative "bookkeeper/counter"
3
+ require_relative "bookkeeper/publisher"
4
+ require_relative "bookkeeper/reviewer"
5
+
6
+ module Artifactory
7
+ module GemImport
8
+ module Bookkeeper
9
+ module_function
10
+
11
+ def counter
12
+ Counter.new
13
+ end
14
+
15
+ def publisher
16
+ Publisher.new
17
+ end
18
+
19
+ def reviewer
20
+ Reviewer.new
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ module Artifactory
2
+ module GemImport
3
+ module Bookkeeper
4
+ class Base
5
+ def initialize
6
+ init_store
7
+ end
8
+
9
+ def tell(message)
10
+ on_message message
11
+ end
12
+
13
+ alias_method :ask!, :tell
14
+
15
+ private
16
+
17
+ def init_store
18
+ raise NotImplementedError
19
+ end
20
+
21
+ def on_message(message)
22
+ raise NotImplementedError
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ module Artifactory
2
+ module GemImport
3
+ module Bookkeeper
4
+ class Counter < Base
5
+ private
6
+
7
+ def init_store
8
+ @store = Hash.new do |store, action|
9
+ store[action] = 0
10
+ end
11
+ end
12
+
13
+ def on_message(message)
14
+ subject, action, count = message
15
+
16
+ case subject
17
+ when :summary
18
+ @store.dup
19
+ when :reset
20
+ init_store
21
+ else
22
+ @store[action] += count
23
+ self
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ module Artifactory
2
+ module GemImport
3
+ module Bookkeeper
4
+ class Publisher < Base
5
+ private
6
+
7
+ # Maintain the contract
8
+ def init_store
9
+ end
10
+
11
+ def on_message(message)
12
+ subject, action, msg = message
13
+
14
+ case action
15
+ when :processing
16
+ puts "Processing #{subject.name} (#{subject.version})"
17
+ when :status
18
+ if msg == :ok
19
+ puts msg
20
+ else
21
+ puts [msg, subject.errors.full_messages].join("\t")
22
+ end
23
+ when :count
24
+ puts "#{subject}: #{msg} gems"
25
+ else
26
+ print " --> #{action.to_s.capitalize}".ljust(20)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ module Artifactory
2
+ module GemImport
3
+ module Bookkeeper
4
+ class Reviewer < Base
5
+ private
6
+
7
+ def init_store
8
+ @store = []
9
+ end
10
+
11
+ def on_message(message)
12
+ subject, _action, _msg = message
13
+
14
+ case subject
15
+ when :summary
16
+ @store
17
+ .map(&:filename)
18
+ .uniq
19
+ .sort
20
+ else
21
+ @store << subject
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,79 @@
1
+ require "thor"
2
+
3
+ module Artifactory
4
+ module GemImport
5
+ class Cli < Thor
6
+ check_unknown_options!
7
+
8
+ def self.exit_on_failure?
9
+ true
10
+ end
11
+
12
+ option :source_repo, required: true, type: :string
13
+
14
+ option :target_repo, required: true, type: :string
15
+
16
+ option :target_repo_api_key, required: true, type: :string
17
+
18
+ option :only, type: :string, default: ".+"
19
+
20
+ desc "import", "Copy gems from the source repo into the target repo."
21
+
22
+ def import
23
+ say GemImport.import! source_repo: source_repo,
24
+ target_repo: target_repo,
25
+ only: options[:only]
26
+ end
27
+
28
+ option :target_repo, required: true, type: :string
29
+
30
+ option :target_repo_api_key, required: true, type: :string
31
+
32
+ option :only, type: :string, default: ".+"
33
+
34
+ desc "delete", "Delete gems from the target repo."
35
+
36
+ def delete
37
+ say GemImport.delete! repo: target_repo,
38
+ only: options[:only]
39
+ end
40
+
41
+ option :source_repo, required: true, type: :string
42
+
43
+ option :target_repo, required: true, type: :string
44
+
45
+ option :target_repo_api_key, required: true, type: :string
46
+
47
+ option :only, type: :string, default: ".+"
48
+
49
+ desc "show-missing", "Show gems not already in the target repo."
50
+
51
+ def show_missing
52
+ gems = GemImport.show_missing source_repo: source_repo,
53
+ target_repo: target_repo,
54
+ only: options[:only]
55
+
56
+ gems.each { |gem| say gem }
57
+
58
+ say gems.count
59
+ end
60
+
61
+ desc "version", "Show version information"
62
+
63
+ def version
64
+ say Artifactory::GemImport::VERSION
65
+ end
66
+
67
+ private
68
+
69
+ def source_repo
70
+ GemImport.source_repo url: options[:source_repo]
71
+ end
72
+
73
+ def target_repo
74
+ GemImport.target_repo url: options[:target_repo],
75
+ api_key: options[:target_repo_api_key]
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,54 @@
1
+ require_relative "gem/errors"
2
+
3
+ module Artifactory
4
+ module GemImport
5
+ class Gem
6
+ attr_reader :name, :version
7
+ attr_reader :source_repo, :target_repo
8
+ attr_reader :cache_dir
9
+ attr_reader :errors
10
+
11
+ attr_accessor :foreign_representation
12
+
13
+ def initialize(spec:, source_repo:, target_repo:, cache_dir:)
14
+ @name, @version, _lang = spec
15
+ @cache_dir = cache_dir
16
+ @source_repo = source_repo
17
+ @target_repo = target_repo
18
+ @errors = Errors.new
19
+ end
20
+
21
+ def source_url
22
+ @source_url ||= File.join(source_repo.gems_url, filename)
23
+ end
24
+
25
+ def source_gems_url
26
+ source_repo.gems_url
27
+ end
28
+
29
+ def source_headers
30
+ source_repo.headers
31
+ end
32
+
33
+ def target_url
34
+ @target_url ||= File.join(target_repo.gems_url, filename)
35
+ end
36
+
37
+ def target_gems_url
38
+ target_repo.gems_url
39
+ end
40
+
41
+ def target_headers
42
+ target_repo.headers
43
+ end
44
+
45
+ def cache_path
46
+ @cache_path ||= File.join(cache_dir, filename)
47
+ end
48
+
49
+ def filename
50
+ @filename ||= "#{name}-#{version}.gem"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,35 @@
1
+ module Artifactory
2
+ module GemImport
3
+ class Gem
4
+ class Errors
5
+ def initialize
6
+ @errors = Hash.new { |h, k| h[k] = [] }
7
+ end
8
+
9
+ def add(key, msg)
10
+ @errors[key] << msg
11
+ self
12
+ end
13
+
14
+ def any?
15
+ @errors.values.flatten.any?
16
+ end
17
+
18
+ def on(key)
19
+ @errors[key]
20
+ end
21
+
22
+ def full_messages
23
+ @errors
24
+ .keys
25
+ .map { |key| [key, full_message(key)].join(": ") }
26
+ .join("; ")
27
+ end
28
+
29
+ def full_message(key)
30
+ @errors[key].join(", ")
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ require_relative "gem_specs/downloader"
2
+ require_relative "gem_specs/parser"
3
+ require_relative "gem_specs/specs"
4
+
5
+ module Artifactory
6
+ module GemImport
7
+ module GemSpecs
8
+ module_function
9
+
10
+ def missing_gems(source_repo:, target_repo:, only: /.+/)
11
+ source_specs = get repo: source_repo, only: only
12
+ target_specs = get repo: target_repo, only: only
13
+
14
+ source_specs - target_specs
15
+ end
16
+
17
+ def get(repo:, only: /.+/)
18
+ specs = Specs
19
+ .new(url: repo.specs_url, headers: repo.headers)
20
+ .specs
21
+ rescue Net::HTTPClientException, Net::HTTPFatalError, Net::OpenTimeout, SocketError => err
22
+ raise ClientError, "Could not fetch specs. URL: #{repo.specs_url}, Reason: #{err.message}"
23
+ else
24
+ Specs.filter(specs, only: only).sort
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ require "httparty"
2
+
3
+ module Artifactory
4
+ module GemImport
5
+ module GemSpecs
6
+ class Downloader
7
+ def self.call(url, headers: {})
8
+ new(url, headers).call
9
+ end
10
+
11
+ attr_reader :url, :headers
12
+
13
+ def initialize(url, headers)
14
+ @url = url
15
+ @headers = headers
16
+ end
17
+
18
+ def call
19
+ response = HTTParty.get url,
20
+ headers: headers
21
+
22
+ if response.success?
23
+ response.body
24
+ else
25
+ response.error!
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ require "zlib"
2
+ require "stringio"
3
+
4
+ module Artifactory
5
+ module GemImport
6
+ module GemSpecs
7
+ class Parser
8
+ def self.call(data)
9
+ new(data).call
10
+ end
11
+
12
+ def initialize(data)
13
+ @io = StringIO.new data
14
+ end
15
+
16
+ def call
17
+ Marshal.load inflated_data
18
+ end
19
+
20
+ private
21
+
22
+ def inflated_data
23
+ Zlib::GzipReader.new @io
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ module Artifactory
2
+ module GemImport
3
+ module GemSpecs
4
+ class Specs
5
+ def self.filter(specs, only:)
6
+ specs.select { |spec| spec.first =~ Regexp.new(only) }
7
+ end
8
+
9
+ def initialize(url:, headers: {})
10
+ @url = url
11
+ @headers = headers
12
+ end
13
+
14
+ def specs
15
+ @specs ||= Parser.call fetch_specs
16
+ end
17
+
18
+ private
19
+
20
+ def fetch_specs
21
+ Downloader.call @url, headers: @headers
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ require_relative "gems/cleaner"
2
+ require_relative "gems/downloader"
3
+ require_relative "gems/uploader"
4
+ require_relative "gems/verifier"
5
+
6
+ module Artifactory
7
+ module GemImport
8
+ module Gems
9
+ module_function
10
+
11
+ def downloader
12
+ Downloader.new
13
+ end
14
+
15
+ def uploader
16
+ Uploader.new
17
+ end
18
+
19
+ def verifier
20
+ Verifier.new
21
+ end
22
+
23
+ def cleaner
24
+ Cleaner.new
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ module Artifactory
2
+ module GemImport
3
+ module Gems
4
+ class Cleaner
5
+ def call(url, headers)
6
+ [:ok, cleanup(url, headers)]
7
+ rescue Net::HTTPClientException, Net::HTTPFatalError, Net::OpenTimeout, SocketError => err
8
+ [:error, err.message]
9
+ end
10
+
11
+ private
12
+
13
+ def cleanup(url, headers)
14
+ response = HTTParty.delete url, headers: headers
15
+
16
+ response.success? ? url : response.error!
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,29 @@
1
+ require "httparty"
2
+
3
+ module Artifactory
4
+ module GemImport
5
+ module Gems
6
+ class Downloader
7
+ def call(url, filename)
8
+ download url, filename
9
+ rescue Net::HTTPClientException, Net::HTTPFatalError, Net::OpenTimeout, SocketError => err # TODO handle file (-system) errors
10
+ [:error, err.message]
11
+ else
12
+ [:ok, url]
13
+ end
14
+
15
+ private
16
+
17
+ def download(url, filename)
18
+ File.open(filename, "w") do |file|
19
+ response = HTTParty.get(url, stream_body: true, follow_redirects: true) do |fragment|
20
+ file.write fragment
21
+ end
22
+
23
+ response.error! unless response.success?
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ module Artifactory
2
+ module GemImport
3
+ module Gems
4
+ class Uploader
5
+ def call(url, headers, file_path)
6
+ file = File.open file_path, "r"
7
+
8
+ [:ok, upload(url, headers, file)]
9
+ rescue Net::HTTPClientException, Net::HTTPFatalError, Net::OpenTimeout, SocketError => err # TODO handle File errors, JSON parse errors
10
+ [:error, err.message]
11
+ end
12
+
13
+ private
14
+
15
+ def upload(url, headers, file)
16
+ headers = headers.merge "Content-Length" => file.size.to_s,
17
+ "Transfer-Encoding" => "chunked"
18
+
19
+ response = HTTParty.put url,
20
+ headers: headers,
21
+ body_stream: file
22
+
23
+ response.success? ? JSON.parse(response.body) : response.error!
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ require "digest"
2
+
3
+ module Artifactory
4
+ module GemImport
5
+ module Gems
6
+ class Verifier
7
+ def call(cache_path, foreign_representation)
8
+ md5, foreign_md5 = calculate_checksums File.open(cache_path, "r"),
9
+ foreign_representation
10
+
11
+ md5 == foreign_md5 ? [:ok] :
12
+ [:failed, "Checksum comparison for uploaded gem #{File.basename cache_path} failed. Expected #{md5}, got #{foreign_md5}"]
13
+ end
14
+
15
+ private
16
+
17
+ def calculate_checksums(file, foreign_representation)
18
+ [Digest::MD5.hexdigest(file.read), foreign_representation.dig("checksums", "md5")]
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ module Artifactory
2
+ module GemImport
3
+ class Repo
4
+ attr_reader :url, :headers
5
+
6
+ def initialize(url:, headers:)
7
+ @url = url
8
+ @headers = headers
9
+ end
10
+
11
+ def gems_url
12
+ File.join url, "gems", "/"
13
+ end
14
+
15
+ def specs_url
16
+ File.join url, "specs.4.8.gz"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ module Artifactory
2
+ module GemImport
3
+ VERSION = "0.1.3"
4
+ end
5
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'worker/base'
2
+ require_relative 'worker/importer'
3
+ require_relative 'worker/missing_detector'
4
+ require_relative 'worker/remover'
5
+
6
+
7
+ module Artifactory
8
+ module GemImport
9
+ module Worker
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ module Artifactory
2
+ module GemImport
3
+ module Worker
4
+ class Base
5
+
6
+ private
7
+
8
+ def summary
9
+ summary = bookkeeper.ask!(:summary)
10
+ for_review = reviewer.ask!(:summary)
11
+
12
+ for_review.any? ? summary.merge(review: for_review) : summary
13
+ end
14
+
15
+ def bookkeeper
16
+ @bookkeeper ||= Bookkeeper.counter
17
+ end
18
+
19
+ def publisher
20
+ @publisher ||= Bookkeeper.publisher
21
+ end
22
+
23
+ def reviewer
24
+ @reviewer ||= Bookkeeper.reviewer
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,152 @@
1
+ require "tmpdir"
2
+ require "fileutils"
3
+
4
+ module Artifactory
5
+ module GemImport
6
+ module Worker
7
+ class Importer < Base
8
+ attr_reader :source_repo, :target_repo, :only
9
+
10
+ def initialize(source_repo:, target_repo:, only: /.+/)
11
+ @source_repo = source_repo
12
+ @target_repo = target_repo
13
+ @only = only
14
+ end
15
+
16
+ def import!
17
+ output_counts
18
+
19
+ missing_gems
20
+ .map { |spec| Gem.new spec: spec, source_repo: source_repo, target_repo: target_repo, cache_dir: tmp_dir }
21
+ .each { |gem| process gem }
22
+
23
+ summary
24
+ ensure
25
+ remove_tmp_dir!
26
+ end
27
+
28
+ private
29
+
30
+ def output_counts
31
+ publisher.tell ["Source Repo (#{source_repo.url})", :count, GemSpecs.get(repo: source_repo, only: only).count]
32
+ publisher.tell ["Target Repo (#{target_repo.url})", :count, GemSpecs.get(repo: target_repo, only: only).count]
33
+ publisher.tell ["Missing in target repo", :count, missing_gems.count]
34
+ end
35
+
36
+ def process(gem)
37
+ publisher.tell [gem, :processing]
38
+
39
+ download(gem) and upload(gem) and verify(gem)
40
+
41
+ cleanup(gem) if gem.errors.on(:verify).any?
42
+ end
43
+
44
+ def missing_gems
45
+ @missing_gems ||= GemSpecs.missing_gems source_repo: source_repo,
46
+ target_repo: target_repo,
47
+ only: only
48
+ end
49
+
50
+ def download(gem)
51
+ publisher.tell [gem, :downloading]
52
+
53
+ status, msg = downloader.call gem.source_url,
54
+ gem.cache_path
55
+
56
+ if status == :ok
57
+ bookkeeper.tell [gem, :downloaded, 1]
58
+ else
59
+ gem.errors.add :downlod, msg
60
+ bookkeeper.tell [gem, :download_failed, 1]
61
+ end
62
+
63
+ publisher.tell [gem, :status, status]
64
+
65
+ status == :ok
66
+ end
67
+
68
+ def upload(gem)
69
+ publisher.tell [gem, :uploading]
70
+
71
+ status, msg = uploader.call gem.target_url,
72
+ gem.target_headers,
73
+ gem.cache_path
74
+
75
+ if status == :ok
76
+ gem.foreign_representation = msg
77
+ bookkeeper.tell [gem, :uploaded, 1]
78
+ else
79
+ gem.errors.add :upload, msg
80
+ bookkeeper.tell [gem, :upload_failed, 1]
81
+ end
82
+
83
+ publisher.tell [gem, :status, status]
84
+
85
+ status == :ok
86
+ end
87
+
88
+ def verify(gem)
89
+ publisher.tell [gem, :verifying]
90
+
91
+ status, msg = verifier.call gem.cache_path,
92
+ gem.foreign_representation
93
+
94
+ if status == :ok
95
+ bookkeeper.tell [gem, :verified, 1]
96
+ else
97
+ gem.errors.add :verify, msg
98
+ reviewer.tell gem
99
+ bookkeeper.tell [gem, :verification_failed, 1]
100
+ end
101
+
102
+ publisher.tell [gem, :status, status]
103
+
104
+ status == :ok
105
+ end
106
+
107
+ def cleanup(gem)
108
+ publisher.tell [gem, :cleaning]
109
+
110
+ status, msg = cleaner.call gem.target_url,
111
+ gem.target_headers
112
+
113
+ if status == :ok
114
+ bookkeeper.tell [gem, :cleaned, 1]
115
+ else
116
+ gem.errors.add :cleanup, msg
117
+ reviewer.tell gem
118
+ bookkeeper.tell [gem, :cleanup_failed, 1]
119
+ end
120
+
121
+ publisher.tell [gem, :status, status]
122
+
123
+ status == :ok
124
+ end
125
+
126
+ def downloader
127
+ @downloader ||= Gems.downloader
128
+ end
129
+
130
+ def uploader
131
+ @uploader ||= Gems.uploader
132
+ end
133
+
134
+ def verifier
135
+ @verifier ||= Gems.verifier
136
+ end
137
+
138
+ def cleaner
139
+ @cleaner ||= Gems.cleaner
140
+ end
141
+
142
+ def remove_tmp_dir!
143
+ FileUtils.remove_entry tmp_dir
144
+ end
145
+
146
+ def tmp_dir
147
+ @tmp_dir ||= Dir.mktmpdir
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,29 @@
1
+ module Artifactory
2
+ module GemImport
3
+ module Worker
4
+ class MissingDetector < Base
5
+ attr_reader :source_repo, :target_repo, :only
6
+
7
+ def initialize(source_repo:, target_repo:, only: /.+/)
8
+ @source_repo = source_repo
9
+ @target_repo = target_repo
10
+ @only = only
11
+ end
12
+
13
+ def detect!
14
+ missing_gems
15
+ .map { |spec| Gem.new spec: spec, source_repo: nil, target_repo: nil, cache_dir: nil }
16
+ .map(&:filename)
17
+ end
18
+
19
+ private
20
+
21
+ def missing_gems
22
+ @missing_gems ||= GemSpecs.missing_gems source_repo: source_repo,
23
+ target_repo: target_repo,
24
+ only: only
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,76 @@
1
+ module Artifactory
2
+ module GemImport
3
+ module Worker
4
+ class Remover < Base
5
+ # This gem is required to let the Artifactory Gem Server work properly.
6
+ NEVER_DELETE = %w(rubygems-update).freeze
7
+
8
+ attr_reader :target_repo, :only
9
+
10
+ def initialize(target_repo:, only: /.+/)
11
+ @target_repo = target_repo
12
+ @only = only
13
+ end
14
+
15
+ def remove!
16
+ gems.each &method(:process)
17
+
18
+ summary
19
+ end
20
+
21
+ private
22
+
23
+ def process(gem)
24
+ publisher.tell [gem, :processing, gem.filename]
25
+
26
+ required_gem?(gem) ? skip(gem) : remove(gem)
27
+ end
28
+
29
+ def remove(gem)
30
+ publisher.tell [gem, :removing]
31
+
32
+ status, msg = cleaner.call gem.target_url,
33
+ gem.target_headers
34
+
35
+ if status == :ok
36
+ bookkeeper.tell [gem, :removed, 1]
37
+ else
38
+ gem.errors.add :removal, msg
39
+ reviewer.tell gem
40
+ bookkeeper.tell [gem, :removal_failed, 1]
41
+ end
42
+
43
+ publisher.tell [gem, :status, status]
44
+
45
+ status == :ok
46
+ end
47
+
48
+ def skip(gem)
49
+ publisher.tell [gem, :skipping]
50
+
51
+ # Nothing to do
52
+ bookkeeper.tell [gem, :skipped, 1]
53
+
54
+ publisher.tell [gem, :status, :ok]
55
+
56
+ true
57
+ end
58
+
59
+ def required_gem?(gem)
60
+ NEVER_DELETE.include? gem.name
61
+ end
62
+
63
+ def gems
64
+ @gems ||=
65
+ GemSpecs
66
+ .get(repo: target_repo, only: only)
67
+ .map { |spec| Gem.new spec: spec, source_repo: nil, target_repo: target_repo, cache_dir: nil }
68
+ end
69
+
70
+ def cleaner
71
+ @cleaner ||= Gems.cleaner
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: artifactory-gem_import
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Thomas Scholz
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-07-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: httparty
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.18'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.18'
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ description: Artifactory Gem Import for importing gems from existing Gem Repo into
42
+ a new Artifactory Gem Repo.
43
+ email:
44
+ - thomas.scholz@rubyapps.ch
45
+ executables:
46
+ - artifactory-gem-import
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - ".gitignore"
51
+ - ".rspec"
52
+ - CODE_OF_CONDUCT.md
53
+ - Gemfile
54
+ - README.md
55
+ - Rakefile
56
+ - artifactory-gem_import.gemspec
57
+ - bin/console
58
+ - bin/setup
59
+ - exe/artifactory-gem-import
60
+ - lib/artifactory/gem_import.rb
61
+ - lib/artifactory/gem_import/bookkeeper.rb
62
+ - lib/artifactory/gem_import/bookkeeper/base.rb
63
+ - lib/artifactory/gem_import/bookkeeper/counter.rb
64
+ - lib/artifactory/gem_import/bookkeeper/publisher.rb
65
+ - lib/artifactory/gem_import/bookkeeper/reviewer.rb
66
+ - lib/artifactory/gem_import/cli.rb
67
+ - lib/artifactory/gem_import/gem.rb
68
+ - lib/artifactory/gem_import/gem/errors.rb
69
+ - lib/artifactory/gem_import/gem_specs.rb
70
+ - lib/artifactory/gem_import/gem_specs/downloader.rb
71
+ - lib/artifactory/gem_import/gem_specs/parser.rb
72
+ - lib/artifactory/gem_import/gem_specs/specs.rb
73
+ - lib/artifactory/gem_import/gems.rb
74
+ - lib/artifactory/gem_import/gems/cleaner.rb
75
+ - lib/artifactory/gem_import/gems/downloader.rb
76
+ - lib/artifactory/gem_import/gems/uploader.rb
77
+ - lib/artifactory/gem_import/gems/verifier.rb
78
+ - lib/artifactory/gem_import/repo.rb
79
+ - lib/artifactory/gem_import/version.rb
80
+ - lib/artifactory/gem_import/worker.rb
81
+ - lib/artifactory/gem_import/worker/base.rb
82
+ - lib/artifactory/gem_import/worker/importer.rb
83
+ - lib/artifactory/gem_import/worker/missing_detector.rb
84
+ - lib/artifactory/gem_import/worker/remover.rb
85
+ homepage: https://github.com/tscholz/artifactory-gem_import
86
+ licenses: []
87
+ metadata:
88
+ allowed_push_host: https://rubygems.org
89
+ homepage_uri: https://github.com/tscholz/artifactory-gem_import
90
+ source_code_uri: https://github.com/tscholz/artifactory-gem_import
91
+ changelog_uri: https://github.com/tscholz/artifactory-gem_import/CHANGELOG.md
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 2.3.0
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.0.3
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: Artifactory Gem Import.
111
+ test_files: []