vagrant_cloud 2.0.2 → 3.0.3

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