vagrant_cloud 1.1.0 → 3.0.0

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.
@@ -1,97 +1,136 @@
1
1
  module VagrantCloud
2
- class Box
3
- attr_accessor :account
4
- attr_accessor :name
5
- attr_accessor :data
2
+ class Box < Data::Mutable
3
+ autoload :Provider, "vagrant_cloud/box/provider"
4
+ autoload :Version, "vagrant_cloud/box/version"
6
5
 
7
- # @param [String] account
8
- # @param [String] name
9
- # @param [Hash] data
10
- def initialize(account, name, data = nil)
11
- @account = account
12
- @name = name
13
- @data = data
14
- end
6
+ attr_reader :organization
7
+ attr_required :name
8
+ attr_optional :created_at, :updated_at, :tag, :short_description,
9
+ :description_html, :description_markdown, :private, :downloads,
10
+ :current_version, :versions, :description, :username
15
11
 
16
- # @return [String]
17
- def description
18
- data['description_markdown'].to_s
19
- end
12
+ attr_mutable :short_description, :description, :private, :versions
20
13
 
21
- # @return [String]
22
- def description_short
23
- data['short_description'].to_s
14
+ # Create a new instance
15
+ #
16
+ # @return [Box]
17
+ def initialize(organization:, **opts)
18
+ @organization = organization
19
+ @versions_loaded = false
20
+ opts[:username] = organization.username
21
+ super(opts)
22
+ if opts[:versions] && !opts[:versions].empty?
23
+ self.versions= Array(opts[:versions]).map do |version|
24
+ Box::Version.load(box: self, **version)
25
+ end
26
+ end
27
+ if opts[:current_version]
28
+ clean(data: {current_version: Box::Version.
29
+ load(box: self, **opts[:current_version])})
30
+ end
31
+ clean!
24
32
  end
25
33
 
26
- # @return [TrueClass, FalseClass]
27
- def private
28
- !!data['private']
34
+ # Delete this box
35
+ #
36
+ # @return [nil]
37
+ # @note This will delete the box, and all versions
38
+ def delete
39
+ if exist?
40
+ organization.account.client.box_delete(username: username, name: name)
41
+ end
42
+ nil
29
43
  end
30
44
 
31
- # @return [Array<Version>]
32
- def versions
33
- version_list = data['versions'].map { |data| VagrantCloud::Version.new(self, data['number'], data) }
34
- version_list.sort_by { |version| Gem::Version.new(version.number) }
45
+ # Add a new version of this box
46
+ #
47
+ # @param [String] version Version number
48
+ # @return [Version]
49
+ def add_version(version)
50
+ if versions.any? { |v| v.version == version }
51
+ raise Error::BoxError::VersionExistsError,
52
+ "Version #{version} already exists for box #{tag}"
53
+ end
54
+ v = Version.new(box: self, version: version)
55
+ clean(data: {versions: versions + [v]})
56
+ v
35
57
  end
36
58
 
37
- # @return [Hash]
38
- def data
39
- @data ||= account.request('get', "/box/#{account.username}/#{name}")
59
+ # Check if this instance is dirty
60
+ #
61
+ # @param [Boolean] deep Check nested instances
62
+ # @return [Boolean] instance is dirty
63
+ def dirty?(key=nil, deep: false)
64
+ if key
65
+ super(key)
66
+ else
67
+ d = super() || !exist?
68
+ if deep && !d
69
+ d = Array(plain_versions).any? { |v| v.dirty?(deep: true) }
70
+ end
71
+ d
72
+ end
40
73
  end
41
74
 
42
- # @param [Hash] args
43
- def update(args = {})
44
- @data = account.request('put', "/box/#{account.username}/#{name}", box: args)
75
+ # @return [Boolean] box exists remotely
76
+ def exist?
77
+ !!created_at
45
78
  end
46
79
 
47
- def delete
48
- account.request('delete', "/box/#{account.username}/#{name}")
80
+ # @return [Array<Version>]
81
+ # @note This is used to allow versions information to be loaded
82
+ # only when requested
83
+ def versions_on_demand
84
+ if !@versions_loaded
85
+ r = self.organization.account.client.box_get(username: username, name: name)
86
+ v = Array(r[:versions]).map do |version|
87
+ Box::Version.load(box: self, **version)
88
+ end
89
+ clean(data: {versions: v + Array(plain_versions)})
90
+ @versions_loaded = true
91
+ end
92
+ plain_versions
49
93
  end
94
+ alias_method :plain_versions, :versions
95
+ alias_method :versions, :versions_on_demand
50
96
 
51
- # @param [Integer] number
52
- # @param [Hash] data
53
- # @return [Version]
54
- def get_version(number, data = nil)
55
- VagrantCloud::Version.new(self, number, data)
97
+ # Save the box if any changes have been made
98
+ #
99
+ # @return [self]
100
+ def save
101
+ save_versions if dirty?(deep: true)
102
+ save_box if dirty?
103
+ self
56
104
  end
57
105
 
58
- # @param [String] name
59
- # @param [String] description
60
- # @return [Version]
61
- def create_version(name, description = nil)
62
- params = { version: name }
63
- params[:description] = description if description
64
- data = account.request('post', "/box/#{account.username}/#{self.name}/versions", version: params)
65
- get_version(data['number'], data)
66
- end
106
+ protected
67
107
 
68
- # @param [String] name
69
- # @param [String] description
70
- # @return [Version]
71
- def ensure_version(name, description = nil)
72
- version = versions.select { |version| version.version == name }.first
73
- version = create_version(name, description) unless version
74
- if description && (description != version.description)
75
- version.update(description)
108
+ # Save the box
109
+ #
110
+ # @return [self]
111
+ def save_box
112
+ req_args = {
113
+ username: username,
114
+ name: name,
115
+ short_description: short_description,
116
+ description: description,
117
+ is_private: self.private
118
+ }
119
+ if exist?
120
+ result = organization.account.client.box_update(**req_args)
121
+ else
122
+ result = organization.account.client.box_create(**req_args)
76
123
  end
77
- version
124
+ clean(data: result, ignores: [:current_version, :versions])
125
+ self
78
126
  end
79
127
 
80
- # @param [Symbol]
81
- # @return [String]
82
- def param_name(param)
83
- # This needs to return strings, otherwise it won't match the JSON that
84
- # Vagrant Cloud returns.
85
- ATTR_MAP.fetch(param, param.to_s)
128
+ # Save the versions if any require saving
129
+ #
130
+ # @return [self]
131
+ def save_versions
132
+ versions.map(&:save)
133
+ self
86
134
  end
87
-
88
- private
89
-
90
- # Vagrant Cloud returns keys different from what you set for some params.
91
- # Values in this map should be strings.
92
- ATTR_MAP = {
93
- is_private: 'private',
94
- description: 'description_markdown'
95
- }.freeze
96
135
  end
97
136
  end
@@ -0,0 +1,173 @@
1
+ module VagrantCloud
2
+ class Box
3
+ class Provider < Data::Mutable
4
+
5
+ # Result for upload requests to upload directly to the
6
+ # storage backend.
7
+ #
8
+ # @param [String] upload_url URL for uploading file asset
9
+ # @param [String] callback_url URL callback to PUT after successful upload
10
+ DirectUpload = Struct.new(:upload_url, :callback_url, keyword_init: true)
11
+
12
+ attr_reader :version
13
+ attr_required :name
14
+ attr_optional :hosted, :created_at, :updated_at,
15
+ :checksum, :checksum_type, :original_url, :download_url,
16
+ :url
17
+
18
+ attr_mutable :url, :checksum, :checksum_type
19
+
20
+ def initialize(version:, **opts)
21
+ if !version.is_a?(Version)
22
+ raise TypeError, "Expecting type `#{Version.name}` but received `#{version.class.name}`"
23
+ end
24
+ @version = version
25
+ super(**opts)
26
+ end
27
+
28
+ # Delete this provider
29
+ #
30
+ # @return [nil]
31
+ def delete
32
+ if exist?
33
+ version.box.organization.account.client.box_version_provider_delete(
34
+ username: version.box.username,
35
+ name: version.box.name,
36
+ version: version.version,
37
+ provider: name
38
+ )
39
+ version.providers.delete(self)
40
+ end
41
+ nil
42
+ end
43
+
44
+ # Upload box file to be hosted on VagrantCloud. This
45
+ # method provides different behaviors based on the
46
+ # parameters passed. When the `direct` option is enabled
47
+ # the upload target will be directly to the backend
48
+ # storage. However, when the `direct` option is used the
49
+ # upload process becomes a two steps where a callback
50
+ # must be called after the upload is complete.
51
+ #
52
+ # If the path is provided, the file will be uploaded
53
+ # and the callback will be requested if the `direct`
54
+ # option is enabled.
55
+ #
56
+ # If a block is provided, the upload URL will be yielded
57
+ # to the block. If the `direct` option is set, the callback
58
+ # will be automatically requested after the block execution
59
+ # has completed.
60
+ #
61
+ # If no path or block is provided, the upload URL will
62
+ # be returned. If the `direct` option is set, the
63
+ # `DirectUpload` instance will be yielded and it is
64
+ # the caller's responsibility to issue the callback
65
+ #
66
+ # @param [String] path Path to asset
67
+ # @param [Boolean] direct Upload directly to backend storage
68
+ # @yieldparam [String] url URL to upload asset
69
+ # @return [self, Object, String, DirectUpload] self when path provided, result of yield when block provided, URL otherwise
70
+ # @note The callback request uses PUT request method
71
+ def upload(path: nil, direct: false)
72
+ if !exist?
73
+ raise Error::BoxError::ProviderNotFoundError,
74
+ "Provider #{name} not found for box #{version.box.tag} version #{version.version}"
75
+ end
76
+ if path && block_given?
77
+ raise ArgumentError,
78
+ "Only path or block may be provided, not both"
79
+ end
80
+ if path && !File.exist?(path)
81
+ raise Errno::ENOENT, path
82
+ end
83
+ req_args = {
84
+ username: version.box.username,
85
+ name: version.box.name,
86
+ version: version.version,
87
+ provider: name
88
+ }
89
+ if direct
90
+ r = version.box.organization.account.client.box_version_provider_upload_direct(**req_args)
91
+ else
92
+ r = version.box.organization.account.client.box_version_provider_upload(**req_args)
93
+ end
94
+ result = DirectUpload.new(
95
+ upload_url: r[:upload_path],
96
+ callback_url: r[:callback]
97
+ )
98
+ if block_given?
99
+ block_r = yield result.upload_url
100
+ Excon.put(result.callback_url) if direct
101
+ block_r
102
+ elsif path
103
+ File.open(path, "rb") do |file|
104
+ chunks = lambda { file.read(Excon.defaults[:chunk_size]).to_s }
105
+ # When performing a direct upload, we must POST the request
106
+ # to the provided upload URL. If it's just a regular upload
107
+ # then we just PUT to the upload URL.
108
+ if direct
109
+ Excon.post(result.upload_url, request_block: chunks)
110
+ else
111
+ Excon.put(result.upload_url, request_block: chunks)
112
+ end
113
+ end
114
+ Excon.put(result.callback_url) if direct
115
+ self
116
+ else
117
+ # When returning upload information for requester to complete,
118
+ # return upload URL when `direct` option is false, otherwise
119
+ # return the `DirectUpload` instance
120
+ direct ? result : result.upload_url
121
+ end
122
+ end
123
+
124
+ # @return [Boolean] provider exists remotely
125
+ def exist?
126
+ !!created_at
127
+ end
128
+
129
+ # Check if this instance is dirty
130
+ #
131
+ # @param [Boolean] deep Check nested instances
132
+ # @return [Boolean] instance is dirty
133
+ def dirty?(key=nil, **args)
134
+ if key
135
+ super(key)
136
+ else
137
+ super || !exist?
138
+ end
139
+ end
140
+
141
+ # Save the provider if any changes have been made
142
+ #
143
+ # @return [self]
144
+ def save
145
+ save_provider if dirty?
146
+ self
147
+ end
148
+
149
+ protected
150
+
151
+ # Save the provider
152
+ #
153
+ # @return [self]
154
+ def save_provider
155
+ req_args = {
156
+ username: version.box.username,
157
+ name: version.box.name,
158
+ version: version.version,
159
+ provider: name,
160
+ checksum: checksum,
161
+ checksum_type: checksum_type
162
+ }
163
+ if exist?
164
+ result = version.box.organization.account.client.box_version_provider_update(**req_args)
165
+ else
166
+ result = version.box.organization.account.client.box_version_provider_create(**req_args)
167
+ end
168
+ clean(data: result)
169
+ self
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,161 @@
1
+ module VagrantCloud
2
+ class Box
3
+ class Version < Data::Mutable
4
+ attr_reader :box
5
+ attr_required :version
6
+ attr_optional :status, :description_html, :description_markdown,
7
+ :created_at, :updated_at, :number, :providers, :description
8
+
9
+ attr_mutable :description
10
+
11
+ def initialize(box:, **opts)
12
+ if !box.is_a?(Box)
13
+ raise TypeError, "Expecting type `#{Box.name}` but received `#{box.class.name}`"
14
+ end
15
+ @box = box
16
+ opts[:providers] = Array(opts[:providers]).map do |provider|
17
+ if provider.is_a?(Provider)
18
+ provider
19
+ else
20
+ Provider.load(version: self, **provider)
21
+ end
22
+ end
23
+ super(opts)
24
+ clean!
25
+ end
26
+
27
+ # Delete this version
28
+ #
29
+ # @return [nil]
30
+ # @note This will delete the version, and all providers
31
+ def delete
32
+ if exist?
33
+ box.organization.account.client.box_version_delete(
34
+ username: box.username,
35
+ name: box.name,
36
+ version: version
37
+ )
38
+ # Remove self from box
39
+ box.versions.delete(self)
40
+ end
41
+ nil
42
+ end
43
+
44
+ # Release this version
45
+ #
46
+ # @return [self]
47
+ def release
48
+ if released?
49
+ raise Error::BoxError::VersionStatusChangeError,
50
+ "Version #{version} is already released for box #{box.tag}"
51
+ end
52
+ if !exist?
53
+ raise Error::BoxError::VersionStatusChangeError,
54
+ "Version #{version} for box #{box.tag} must be saved before release"
55
+ end
56
+ result = box.organization.account.client.box_version_release(
57
+ username: box.username,
58
+ name: box.name,
59
+ version: version
60
+ )
61
+ clean(data: result, only: :status)
62
+ self
63
+ end
64
+
65
+ # Revoke this version
66
+ #
67
+ # @return [self]
68
+ def revoke
69
+ if !released?
70
+ raise Error::BoxError::VersionStatusChangeError,
71
+ "Version #{version} is not yet released for box #{box.tag}"
72
+ end
73
+ result = box.organization.account.client.box_version_revoke(
74
+ username: box.username,
75
+ name: box.name,
76
+ version: version
77
+ )
78
+ clean(data: result, only: :status)
79
+ self
80
+ end
81
+
82
+ # @return [Boolean]
83
+ def released?
84
+ status == "active"
85
+ end
86
+
87
+ # Add a new provider for this version
88
+ #
89
+ # @param [String] pname Name of provider
90
+ # @return [Provider]
91
+ def add_provider(pname)
92
+ if providers.any? { |p| p.name == pname }
93
+ raise Error::BoxError::VersionProviderExistsError,
94
+ "Provider #{pname} already exists for box #{box.tag} version #{version}"
95
+ end
96
+ pv = Provider.new(version: self, name: pname)
97
+ clean(data: {providers: providers + [pv]})
98
+ pv
99
+ end
100
+
101
+ # Check if this instance is dirty
102
+ #
103
+ # @param [Boolean] deep Check nested instances
104
+ # @return [Boolean] instance is dirty
105
+ def dirty?(key=nil, deep: false)
106
+ if key
107
+ super(key)
108
+ else
109
+ d = super() || !exist?
110
+ if deep && !d
111
+ d = providers.any? { |p| p.dirty?(deep: true) }
112
+ end
113
+ d
114
+ end
115
+ end
116
+
117
+ # @return [Boolean] version exists remotely
118
+ def exist?
119
+ !!created_at
120
+ end
121
+
122
+ # Save the version if any changes have been made
123
+ #
124
+ # @return [self]
125
+ def save
126
+ save_version if dirty?
127
+ save_providers if dirty?(deep: true)
128
+ self
129
+ end
130
+
131
+ protected
132
+
133
+ # Save the version
134
+ #
135
+ # @return [self]
136
+ def save_version
137
+ params = {
138
+ username: box.username,
139
+ name: box.name,
140
+ version: version,
141
+ description: description
142
+ }
143
+ if exist?
144
+ result = box.organization.account.client.box_version_update(**params)
145
+ else
146
+ result = box.organization.account.client.box_version_create(**params)
147
+ end
148
+ clean(data: result, ignores: :providers)
149
+ self
150
+ end
151
+
152
+ # Save the providers if any require saving
153
+ #
154
+ # @return [self]
155
+ def save_providers
156
+ Array(providers).map(&:save)
157
+ self
158
+ end
159
+ end
160
+ end
161
+ end