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