vagrant_cloud 2.0.1 → 3.0.2

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