vagrant_cloud 2.0.0 → 3.0.1

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,185 +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]
37
- def update(args = {})
38
- # hash arguments kept for backwards compatibility
39
- data = @client.request('put', box_path(args[:organization], args[:name]), box: args)
40
-
41
- # Update was called on *this* object, so update
42
- # objects data locally
43
- @data = data if !args[:organization] && !args[:name]
44
- 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!
45
32
  end
46
33
 
47
- # A generic function to read any box on Vagrant Cloud
48
- # If org and box name is not supplied, it will default to
49
- # reading the given Box object
34
+ # Delete this box
50
35
  #
51
- # @param [String] org - organization of the box to read
52
- # @param [String] box_name - name of the box to read
53
- # @return [Hash]
54
- def delete(org = nil, box_name = nil)
55
- @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
56
49
  end
57
50
 
58
- # A generic function to read any box on Vagrant Cloud
51
+ # Add a new version of this box
59
52
  #
60
- # @param [String] org - organization of the box to read
61
- # @param [String] box_name - name of the box to read
62
- # @return [Hash]
63
- def read(org = nil, box_name = nil)
64
- @client.request('get', box_path(org, box_name))
65
- end
66
-
67
- # @param [String] short_description
68
- # @param [String] description
69
- # @param [Bool] is_private
70
- # @return [Hash]
71
- def create(short_description = nil, description = nil, org = nil, box_name = nil, is_private = false)
72
- update_data = !(org && box_name)
73
-
74
- org ||= account.username
75
- box_name ||= @name
76
- short_description ||= @short_description
77
- description ||= @description
78
-
79
- params = {
80
- name: box_name,
81
- username: org,
82
- is_private: is_private,
83
- short_description: short_description,
84
- description: description
85
- }.delete_if { |_, v| v.nil? }
86
-
87
- data = @client.request('post', '/boxes', box: params)
88
-
89
- # Create was called on *this* object, so update
90
- # objects data locally
91
- @data = data if update_data
92
- data
93
- end
94
-
95
- #--------------------
96
- # Metadata Helpers
97
- #--------------------
98
-
99
- # @return [String]
100
- def description
101
- 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
102
63
  end
103
64
 
104
- # @return [String]
105
- def description_short
106
- 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
107
79
  end
108
80
 
109
- # @return [TrueClass, FalseClass]
110
- def private
111
- !!data['private']
81
+ # @return [Boolean] box exists remotely
82
+ def exist?
83
+ !!created_at
112
84
  end
113
85
 
114
86
  # @return [Array<Version>]
115
- def versions
116
- version_list = data['versions'].map { |data| VagrantCloud::Version.new(self, data['number'], data) }
117
- version_list.sort_by { |version| Gem::Version.new(version.number) }
118
- end
119
-
120
- #------------------------
121
- # Old Version API Helpers
122
- #------------------------
123
-
124
- # @param [Integer] number
125
- # @param [Hash] data
126
- # @return [Version]
127
- def get_version(number, data = nil)
128
- VagrantCloud::Version.new(self, number, data)
129
- end
130
-
131
- # @param [String] name
132
- # @param [String] description
133
- # @return [Version]
134
- def create_version(name, description = nil)
135
- params = { version: name }
136
- params[:description] = description if description
137
- data = @client.request('post', "#{box_path}/versions", version: params)
138
- get_version(data['number'], data)
139
- end
140
-
141
- # @param [String] name
142
- # @param [String] description
143
- # @return [Version]
144
- def ensure_version(name, description = nil)
145
- version = versions.select { |v| v.version == name }.first
146
- version ||= create_version(name, description)
147
- if description && (description != version.description)
148
- 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
149
101
  end
150
- version
102
+ plain_versions
151
103
  end
104
+ alias_method :plain_versions, :versions
105
+ alias_method :versions, :versions_on_demand
152
106
 
153
- # @param [Symbol]
154
- # @return [String]
155
- def param_name(param)
156
- # This needs to return strings, otherwise it won't match the JSON that
157
- # Vagrant Cloud returns.
158
- 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_versions if dirty?(deep: true)
112
+ save_box if dirty?
113
+ self
159
114
  end
160
115
 
161
- private
116
+ protected
162
117
 
163
- # Constructs the box path based on an account and box name.
164
- # If no params are given, it constructs a path for *this* Box object,
165
- # but if both params are given it will construct a path for a one-off request
118
+ # Save the box
166
119
  #
167
- # @param [String] - username
168
- # @param [String] - box_name
169
- # @return [String] - API path to box
170
- def box_path(username = nil, box_name = nil)
171
- if username && box_name
172
- "/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)
173
131
  else
174
- "/box/#{account.username}/#{name}"
132
+ result = organization.account.client.box_create(**req_args)
175
133
  end
134
+ clean(data: result, ignores: [:current_version, :versions])
135
+ self
176
136
  end
177
137
 
178
- # Vagrant Cloud returns keys different from what you set for some params.
179
- # Values in this map should be strings.
180
- ATTR_MAP = {
181
- is_private: 'private',
182
- description: 'description_markdown'
183
- }.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
184
145
  end
185
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