artifactory-gem_import 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []