vagrant_cloud 2.0.3 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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