artifact_tools 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 289e3ecd5615e51b01f2a42a2fb09420a52f21f4c0467fce0b0718b6f81da06b
4
+ data.tar.gz: c7cd3a30aca98803c044ab2aef7df0086f517f5cb18d13ef542bda889ba60544
5
+ SHA512:
6
+ metadata.gz: e6b7ae6fd0ad77a9521bb8cd7eefc61af5346817956efcbbca52e6e52ca090ee8ee6d7ede6760acfba5a28170eae4a3d9db4109e8bbac684ee19d1b1422b06d0
7
+ data.tar.gz: d823934f9d66748ac73c040d778905b455a71bbf37143a1c47a7287062f1ebdca435f204f5fc52d0466438d74cc22b2f44f87fde229fd83da5d546b622854de7
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'artifact_tools/downloader'
5
+
6
+ ArtifactTools::Downloader.new(**ArtifactTools::Downloader.parse(ARGV))
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'artifact_tools/uploader'
5
+
6
+ ArtifactTools::Uploader.new(**ArtifactTools::Uploader.parse(ARGV))
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/ssh'
4
+ require 'net/scp'
5
+ require 'fileutils'
6
+ require 'digest'
7
+ require 'artifact_tools/hasher'
8
+
9
+ module ArtifactTools
10
+ # Notifies that there was a mismatch between expected hash of the
11
+ # file(according to the configuration file) and the actual hash of the
12
+ # fetched file
13
+ class HashMismatchError < RuntimeError
14
+ end
15
+
16
+ # Use an object of this class to put/fetch files from storage specified with {ConfigFile}
17
+ class Client
18
+ include ArtifactTools::Hasher
19
+
20
+ # @param config [Hash] Configuration
21
+ # @param user [String] User name to connect to server with, overrides
22
+ # ARTIFACT_STORAGE_USER and the on stored in config
23
+ def initialize(config:, user: nil)
24
+ @config = config
25
+ user ||= ENV['ARTIFACT_STORAGE_USER'] || @config['user']
26
+ @ssh = Net::SSH.start(@config['server'], user, non_interactive: true)
27
+ end
28
+
29
+ # Fetch a file from store
30
+ #
31
+ # @param file [String] Path to file to fetch. Fetches all files from config if omitted.
32
+ # @param dest [String] Optional prefix to add to local path of the file being fetched. Uses cwd if omitted.
33
+ # @param match [Regexp] Optionally fetch only files matching this pattern.
34
+ # @param verify [Boolean] Whether to verify the checksum after the file is received. Slows the fetch.
35
+ #
36
+ # @raise [HashMismatchError] In case checksum doesn't match the one stored in the config file.
37
+ def fetch(file: nil, dest: nil, match: nil, verify: false, force: false)
38
+ files = @config['files'].keys
39
+ files = [file] if file
40
+ files.each do |entry|
41
+ next if match && !entry.match?(match)
42
+
43
+ entry_hash = @config['files'][entry]['hash']
44
+ remote = compose_remote(entry, entry_hash)
45
+ local = compose_local(dest, entry)
46
+ next if !force && local_file_matches(local, entry_hash)
47
+
48
+ @ssh.scp.download!(remote, local)
49
+ verify(entry_hash, local) if verify
50
+ end
51
+ end
52
+
53
+ # Put a file to storage
54
+ #
55
+ # @param file [String] Path to the file to store.
56
+ def put(file:)
57
+ hash = file_hash(file)
58
+ remote = compose_remote(file, hash)
59
+ ensure_remote_path_exists(remote)
60
+ @ssh.scp.upload!(file, remote)
61
+ end
62
+
63
+ private
64
+
65
+ def compose_remote(file, hash)
66
+ basename = File.basename(file)
67
+ "#{@config['dir']}/#{hash}/#{basename}"
68
+ end
69
+
70
+ def ensure_path_exists(local)
71
+ dirname = File.dirname(local)
72
+ return if File.directory?(dirname)
73
+
74
+ FileUtils.mkdir_p(dirname)
75
+ end
76
+
77
+ def ensure_remote_path_exists(remote)
78
+ dirname = File.dirname(remote)
79
+ return if File.directory?(dirname)
80
+
81
+ @ssh.exec!("mkdir -p #{dirname}")
82
+ end
83
+
84
+ def compose_local(dest, file)
85
+ local = file
86
+ local = "#{dest}/#{local}" if dest
87
+ ensure_path_exists(local)
88
+ local
89
+ end
90
+
91
+ def verify(expected_hash, path)
92
+ actual_hash = file_hash(path)
93
+ return unless expected_hash != actual_hash
94
+
95
+ raise HashMismatchError, "File #{path} has hash: #{actual_hash} while it should have: #{expected_hash}"
96
+ end
97
+
98
+ def local_file_matches(local_file, expected_hash)
99
+ File.exist?(local_file) && file_hash(local_file) == expected_hash
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'artifact_tools/hasher'
5
+
6
+ module ArtifactTools
7
+ # Store configuration information about artifacts and where they are stored.
8
+ #
9
+ # It has to contain at least the fields from {REQUIRED_FIELDS} while allowing
10
+ # any key/value which has a value for the user.
11
+ class ConfigFile
12
+ include ArtifactTools::Hasher
13
+ attr_reader :config
14
+
15
+ REQUIRED_FIELDS = %w[server dir files].freeze
16
+
17
+ # Initialize config file
18
+ #
19
+ # @param config [Hash] Provide configuration. Mandatory fields are {REQUIRED_FIELDS}
20
+ def initialize(config:)
21
+ raise 'Invalid config' unless REQUIRED_FIELDS.all? { |k| config.keys.include?(k) }
22
+
23
+ raise 'Invalid config' unless [NilClass, Hash].any? { |klass| config['files'].is_a?(klass) }
24
+
25
+ @config = config
26
+ end
27
+
28
+ # Create ConfigFile from file in YAML format
29
+ #
30
+ # @param file [String] Path to file in YAML format.
31
+ def self.from_file(file)
32
+ ConfigFile.new(config: YAML.load_file(file))
33
+ # Leave error propagation as this is development tool
34
+ end
35
+
36
+ # Saves configuration to file
37
+ #
38
+ # @param file [String] Save in this file. Overwrites the file if present.
39
+ def save(file)
40
+ File.write(file, @config.to_yaml)
41
+ # Leave error propagation as this is development tool
42
+ end
43
+
44
+ # Append file to configuration.
45
+ #
46
+ # @param file [String] Path to the file to store in the configuration
47
+ # @param store_path [String] Use this path as key in the configuration. Optional, if omitted uses file
48
+ # @param opts [Hash] Additional fields to store for the file
49
+ #
50
+ # @note If file exists in the config with key *store_path* then its
51
+ # properties will be merged, where new ones will have priority.
52
+ def append_file(file:, store_path: nil, **opts)
53
+ store_path ||= file
54
+
55
+ # Convert symbols to String
56
+ opts = hash_keys_to_strings(opts)
57
+
58
+ @config['files'] ||= {}
59
+ @config['files'][store_path] = opts
60
+ @config['files'][store_path]['hash'] ||= file_hash(file)
61
+ end
62
+
63
+ private
64
+
65
+ def hash_keys_to_strings(hash)
66
+ hash.transform_keys(&:to_s)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'artifact_tools/client'
4
+ require 'artifact_tools/config_file'
5
+ require 'optparse'
6
+ require 'yaml'
7
+
8
+ module ArtifactTools
9
+ # Downloader allows the user to fetch files from a store specified. All this
10
+ # information is provided by {ConfigFile}.
11
+ class Downloader
12
+ # Downloads requested files
13
+ #
14
+ # @param [Hash] args the arguments for downloading artifacts
15
+ # @argument args :config_file [String] Path to configuration file
16
+ # @argument args :user [String] User to use for download connection
17
+ # @argument args :dest_dir [String] Where to download artifacts to
18
+ # @argument args :verify [Boolean] Whether to verify checksums after download.
19
+ # @argument args :force [Boolean] Whether to download files even if they are already
20
+ # present with the exected hash
21
+ # @argument args :match [Regexp] Whether to verify checksums after download.
22
+ def initialize(args = { verify: true, force: false })
23
+ config = load_config(args[:config_file])
24
+ c = ArtifactTools::Client.new(config: config.config, user: args[:user])
25
+ c.fetch(dest: args[:dest_dir], verify: args[:verify], match: args[:match], force: args[:force])
26
+ end
27
+
28
+ @default_opts = {
29
+ verify: true,
30
+ force: false,
31
+ dest_dir: '.'
32
+ }
33
+ @parse_opts_handlers = {
34
+ ['-c FILE', '--configuration=FILE', 'Pass configuration file'] => lambda { |f, options, _opts|
35
+ options[:config_file] = f
36
+ },
37
+ ['-d DIR', '--destination=DIR', 'Store files in directory'] => lambda { |d, options, _opts|
38
+ options[:dest_dir] = d
39
+ },
40
+ ['-v', '--[no-]verify', TrueClass, "Verify hash on downloaded files. Default: #{@default_opts[:verify]}."] =>
41
+ lambda { |v, options, _opts|
42
+ options[:verify] = v
43
+ },
44
+ [
45
+ '-f', '--[no-]force', TrueClass,
46
+ "Force download of files if they are present with expected hash. Default: #{@default_opts[:force]}."
47
+ ] => lambda { |v, options, _opts|
48
+ options[:force] = v
49
+ },
50
+ ['-u USER', '--user=USER', 'Access server with this username'] => ->(u, options, _opts) { options[:user] = u },
51
+ ['-m REGEXP', '--match=REGEXP', Regexp, 'Download only file which match regular expression'] =>
52
+ lambda { |v, options, _opts|
53
+ options[:match] = v
54
+ },
55
+ ['-h', '--help', 'Show this message'] => lambda { |_h, _options, opts|
56
+ puts opts
57
+ exit
58
+ }
59
+ }
60
+ # Parse command line options to options suitable to Downloader.new
61
+ #
62
+ # @param arguments [Array(String)] Command line options to parse and use.
63
+ # Hint: pass ARGV
64
+ def self.parse(arguments)
65
+ options = @default_opts
66
+ arguments << '-h' if arguments.empty?
67
+ OptionParser.new do |opts|
68
+ opts.banner = "Usage: #{__FILE__} [options]"
69
+ @parse_opts_handlers.each do |args, handler|
70
+ opts.on(*args) { |v| handler.call(v, options, opts) }
71
+ end
72
+ end.parse!(arguments)
73
+
74
+ raise OptionParser::MissingArgument, 'Missing -c/--configuration option' unless options.key?(:config_file)
75
+
76
+ options
77
+ end
78
+
79
+ private
80
+
81
+ def load_config(config_file)
82
+ ArtifactTools::ConfigFile.from_file(config_file)
83
+ # TODO: error check
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArtifactTools
4
+ # wrapper for the hashing algo used
5
+ module Hasher
6
+ # Calculate hash of a file
7
+ #
8
+ # @param path [String] Path to file to hash.
9
+ def file_hash(path)
10
+ hash_algo.file(path).hexdigest
11
+ end
12
+
13
+ private
14
+
15
+ def hash_algo
16
+ # TODO: decide on used algorithm
17
+ Digest::SHA1
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'artifact_tools/client'
4
+ require 'artifact_tools/config_file'
5
+ require 'optparse'
6
+ require 'yaml'
7
+
8
+ module ArtifactTools
9
+ # Uploader allows the user to upload files to a store specified by
10
+ # {ConfigFile}.
11
+ class Uploader
12
+ include ArtifactTools::Hasher
13
+ # Upload requested files
14
+ #
15
+ # @param config_file [String] Path to configuration file
16
+ # @param append [Boolean] Whether to append files to config file
17
+ # @param files [Array(String)] Paths to files to upload
18
+ def initialize(config_file:, files:, append: false)
19
+ # TODO: check for clashes of files, do hash checks?
20
+ @config_file = config_file
21
+ @append = append
22
+ @config = load_config(@config_file)
23
+ c = ArtifactTools::Client.new(config: @config.config)
24
+ files.each do |file|
25
+ update_file(c, file)
26
+ end
27
+ @config.save(config_file)
28
+ end
29
+
30
+ @default_append_opt = false
31
+ @parse_opts_handlers = {
32
+ ['-c FILE', '--configuration=FILE', 'Pass configuration file.'] => lambda { |_opts, v, options|
33
+ options[:config_file] = v
34
+ },
35
+ ['-a', '--append',
36
+ "Append uploaded files to configuration file, if missing. Default: #{@default_append_opt}."] =>
37
+ ->(_opts, v, options) { options[:append] = v },
38
+ ['-h', '--help', 'Show this message'] => lambda { |opts, _v, _options|
39
+ puts opts
40
+ exit
41
+ }
42
+ }
43
+
44
+ # Parse command line options to options suitable to Downloader.new
45
+ #
46
+ # @param arguments [Array(String)] Command line options to parse and use.
47
+ # Hint: pass ARGV
48
+ def self.parse(arguments)
49
+ options = { append: @default_append_opt }
50
+ arguments << '-h' if arguments.empty?
51
+ OptionParser.new do |opts|
52
+ opts.banner = "Usage: #{__FILE__} [options]"
53
+ @parse_opts_handlers.each do |args, handler|
54
+ opts.on(*args) { |v| handler.call(opts, v, options) }
55
+ end
56
+ end.parse!(arguments)
57
+
58
+ raise OptionParser::MissingArgument, 'Missing -c/--configuration option' unless options.key?(:config_file)
59
+
60
+ options.merge({ files: arguments.dup })
61
+ end
62
+
63
+ private
64
+
65
+ def load_config(config_file)
66
+ ArtifactTools::ConfigFile.from_file(config_file)
67
+ end
68
+
69
+ def relative_to_config(file, config_file)
70
+ file = File.expand_path(file)
71
+ config_file = File.expand_path(config_file)
72
+ config_file_dirname = File.dirname(config_file)
73
+ return nil unless file.start_with?(config_file_dirname)
74
+
75
+ file[(config_file_dirname.length + 1)..]
76
+ end
77
+
78
+ # update the current file remotely and append it to the config if needed
79
+ def update_file(client, file)
80
+ client.put(file: file)
81
+ hash = file_hash(file)
82
+ puts "#{hash} #{file}"
83
+ return unless @append
84
+
85
+ rel_path = relative_to_config(file, @config_file)
86
+ raise "#{file} is not relative to config: #{@config_file}" unless rel_path
87
+
88
+ @config.append_file(file: file, store_path: rel_path, hash: hash)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArtifactTools
4
+ VERSION = '0.0.5'
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'artifact_tools/client'
4
+ require 'artifact_tools/version'
metadata ADDED
@@ -0,0 +1,166 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: artifact_tools
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5
5
+ platform: ruby
6
+ authors:
7
+ - Vitosha Labs Open Source team
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-12-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: net-scp
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-ssh
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.2'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.2'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.16'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.16'
125
+ description:
126
+ email:
127
+ executables:
128
+ - artifact_download
129
+ - artifact_upload
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - bin/artifact_download
134
+ - bin/artifact_upload
135
+ - lib/artifact_tools.rb
136
+ - lib/artifact_tools/client.rb
137
+ - lib/artifact_tools/config_file.rb
138
+ - lib/artifact_tools/downloader.rb
139
+ - lib/artifact_tools/hasher.rb
140
+ - lib/artifact_tools/uploader.rb
141
+ - lib/artifact_tools/version.rb
142
+ homepage: https://github.com/vitoshalabs/artifact_tools
143
+ licenses:
144
+ - MIT
145
+ metadata:
146
+ rubygems_mfa_required: 'true'
147
+ post_install_message:
148
+ rdoc_options: []
149
+ require_paths:
150
+ - lib
151
+ required_ruby_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">"
154
+ - !ruby/object:Gem::Version
155
+ version: 2.7.0
156
+ required_rubygems_version: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ requirements: []
162
+ rubygems_version: 3.1.6
163
+ signing_key:
164
+ specification_version: 4
165
+ summary: Provides tools to manage repository artifacts.
166
+ test_files: []