vagrant_cloud 2.0.3 → 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,190 +1,136 @@
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 [VagrantCloud::Account] 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(username: username, name: name)
41
+ end
42
+ nil
61
43
  end
62
44
 
63
- # A generic function to read any box on Vagrant Cloud
45
+ # Add a new version of this box
64
46
  #
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
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
107
57
  end
108
58
 
109
- # @return [String]
110
- def description_short
111
- data['short_description'].to_s
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
112
73
  end
113
74
 
114
- # @return [TrueClass, FalseClass]
115
- def private
116
- !!data['private']
75
+ # @return [Boolean] box exists remotely
76
+ def exist?
77
+ !!created_at
117
78
  end
118
79
 
119
80
  # @return [Array<Version>]
120
- def versions
121
- version_list = data['versions'].map { |data| VagrantCloud::Version.new(self, data['number'], data, nil, @client.access_token, @client.url_base) }
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, nil, @client.access_token, @client.url_base)
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)
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
154
91
  end
155
- version
92
+ plain_versions
156
93
  end
94
+ alias_method :plain_versions, :versions
95
+ alias_method :versions, :versions_on_demand
157
96
 
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)
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
164
104
  end
165
105
 
166
- private
106
+ protected
167
107
 
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
108
+ # Save the box
171
109
  #
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}"
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)
178
121
  else
179
- "/box/#{account.username}/#{name}"
122
+ result = organization.account.client.box_create(**req_args)
180
123
  end
124
+ clean(data: result, ignores: [:current_version, :versions])
125
+ self
181
126
  end
182
127
 
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
128
+ # Save the versions if any require saving
129
+ #
130
+ # @return [self]
131
+ def save_versions
132
+ versions.map(&:save)
133
+ self
134
+ end
189
135
  end
190
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