azure_file_shares 0.1.5
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.
- checksums.yaml +7 -0
- data/Readme.md +483 -0
- data/lib/azure_file_shares/auth/token_provider.rb +92 -0
- data/lib/azure_file_shares/client.rb +99 -0
- data/lib/azure_file_shares/configuration.rb +58 -0
- data/lib/azure_file_shares/errors/api_error.rb +18 -0
- data/lib/azure_file_shares/errors/configuration_error.rb +6 -0
- data/lib/azure_file_shares/operations/base_operation.rb +90 -0
- data/lib/azure_file_shares/operations/file_operations.rb +798 -0
- data/lib/azure_file_shares/operations/file_shares_operations.rb +78 -0
- data/lib/azure_file_shares/operations/snapshots_operations.rb +62 -0
- data/lib/azure_file_shares/resources/file_share.rb +80 -0
- data/lib/azure_file_shares/resources/file_share_snapshot.rb +75 -0
- data/lib/azure_file_shares/version.rb +3 -0
- data/lib/azure_file_shares.rb +54 -0
- metadata +198 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8bf520f5d27bda7dba8b8f98be6697141e122bc345e65b8518f705fb5bdfaa74
|
4
|
+
data.tar.gz: 46f29aff6a2ac4735d7f78829134416103ace8e53de9858d1357d894581057d0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dd7cea851a63d01ed0298837ecbe50433e9026c7e397ba7752fe7cd3b15f0144e7c7ffda3c63a1ea54113be69379e6990d20fb7cc5df15d794e5cf8c65427d4c
|
7
|
+
data.tar.gz: 2e52b6bda00b44a8226e7d163594113c18fb4511585edfd1afea391dad0fead13da33a1e5ec150eca010fcb8a0daee9a305d581b3f8b0e05a5cc1139c246b14a
|
data/Readme.md
ADDED
@@ -0,0 +1,483 @@
|
|
1
|
+
# Azure File Shares
|
2
|
+
|
3
|
+
A Ruby gem for interacting with the Microsoft Azure File Shares API. This gem provides a simple, object-oriented interface for managing Azure File Shares and their snapshots, as well as file and directory operations.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'azure_file_shares'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
```bash
|
16
|
+
$ bundle install
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
$ gem install azure_file_shares
|
23
|
+
```
|
24
|
+
|
25
|
+
## Requirements
|
26
|
+
|
27
|
+
- Ruby 2.6 or higher
|
28
|
+
- Azure account with appropriate permissions
|
29
|
+
- Registered application in Microsoft Entra ID with API permissions
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
### Configuration Options
|
34
|
+
|
35
|
+
The gem supports two main modes of operation, each with different configuration requirements:
|
36
|
+
|
37
|
+
### 1. Full Access (ARM API + Storage API)
|
38
|
+
|
39
|
+
For complete functionality including share management and file operations:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
AzureFileShares.configure do |config|
|
43
|
+
# Required for ARM operations (share management)
|
44
|
+
config.tenant_id = 'your-tenant-id'
|
45
|
+
config.client_id = 'your-client-id'
|
46
|
+
config.client_secret = 'your-client-secret'
|
47
|
+
config.subscription_id = 'your-subscription-id'
|
48
|
+
config.resource_group_name = 'your-resource-group-name'
|
49
|
+
|
50
|
+
# Required for all operations
|
51
|
+
config.storage_account_name = 'your-storage-account-name'
|
52
|
+
config.storage_account_key = 'your-storage-account-key'
|
53
|
+
|
54
|
+
# Optional settings
|
55
|
+
config.api_version = '2024-01-01' # Default
|
56
|
+
config.request_timeout = 60 # Default (in seconds)
|
57
|
+
config.logger = Logger.new(STDOUT) # Optional
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
### 2. File Operations Only (Storage API)
|
62
|
+
|
63
|
+
If you only need to work with files and directories within existing shares, you can use this simplified configuration:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
AzureFileShares.configure do |config|
|
67
|
+
# Minimal configuration for file operations
|
68
|
+
config.storage_account_name = 'your-storage-account-name'
|
69
|
+
config.storage_account_key = 'your-storage-account-key'
|
70
|
+
end
|
71
|
+
|
72
|
+
# Now you can use file operations
|
73
|
+
client = AzureFileShares.client
|
74
|
+
|
75
|
+
# Works with existing shares
|
76
|
+
client.files.upload_file('share-name', 'path/to/directory', 'file.txt', 'file content')
|
77
|
+
client.files.list('share-name', 'path/to/directory')
|
78
|
+
client.files.download_file('share-name', 'path/to/directory', 'file.txt')
|
79
|
+
|
80
|
+
# But share management operations will fail
|
81
|
+
# client.file_shares.list # This would throw an error
|
82
|
+
```
|
83
|
+
|
84
|
+
With the simplified configuration, you can perform all file and directory operations but cannot manage shares. Share management operations require the full configuration including resource group details.
|
85
|
+
|
86
|
+
|
87
|
+
## Using SAS Tokens for Authentication
|
88
|
+
|
89
|
+
For file operations, you can use a Shared Access Signature (SAS) token instead of a storage account key. This is often more reliable and has less stringent permission requirements:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
# Configure with SAS token
|
93
|
+
AzureFileShares.configure do |config|
|
94
|
+
config.storage_account_name = 'your-storage-account-name'
|
95
|
+
config.sas_token = 'sv=2020-08-04&ss=f&srt=co&sp=rwdlc&se=2025-04-30T21:00:00Z&st=2025-04-29T13:00:00Z&spr=https&sig=XXXXXXXXXXXXX'
|
96
|
+
end
|
97
|
+
|
98
|
+
# Or set it on an existing client
|
99
|
+
client = AzureFileShares.client
|
100
|
+
client.sas_token = 'sv=2020-08-04&ss=f&srt=co&sp=rwdlc&se=2025-04-30T21:00:00Z&st=2025-04-29T13:00:00Z&spr=https&sig=XXXXXXXXXXXXX'
|
101
|
+
|
102
|
+
# Then use file operations as normal
|
103
|
+
client.files.list('share-name', 'path/to/directory')
|
104
|
+
```
|
105
|
+
|
106
|
+
### Generating a SAS Token
|
107
|
+
|
108
|
+
You can generate a SAS token in several ways:
|
109
|
+
|
110
|
+
1. **Azure Portal**:
|
111
|
+
- Go to your Storage Account
|
112
|
+
- Select "File shares" and choose your share
|
113
|
+
- Click "Generate SAS" in the top menu
|
114
|
+
- Configure permissions and expiry
|
115
|
+
- Click "Generate SAS token and URL"
|
116
|
+
- Copy just the token part (the query string starting with "?sv=")
|
117
|
+
|
118
|
+
2. **Azure CLI**:
|
119
|
+
```bash
|
120
|
+
az storage file generate-sas --account-name <storage-account> --account-key <account-key> --path <file-path> --share-name <share-name> --permissions r --expiry <expiry-date>
|
121
|
+
```
|
122
|
+
|
123
|
+
3. **Azure PowerShell**:
|
124
|
+
```powershell
|
125
|
+
New-AzStorageFileSASToken -Context $ctx -ShareName <share-name> -Path <file-path> -Permission r -ExpiryTime <expiry-date>
|
126
|
+
```
|
127
|
+
|
128
|
+
4. **Programmatically** using this gem:
|
129
|
+
```ruby
|
130
|
+
sas_url = AzureFileShares.client.files.generate_file_sas_url(
|
131
|
+
'share-name',
|
132
|
+
'path/to/directory',
|
133
|
+
'file.txt',
|
134
|
+
expiry: Time.now + 86400, # 1 day
|
135
|
+
permissions: 'r' # Read-only
|
136
|
+
)
|
137
|
+
```
|
138
|
+
|
139
|
+
Using SAS tokens can be more reliable than Shared Key authentication and provides more granular control over permissions and access duration.
|
140
|
+
|
141
|
+
### Working with File Shares
|
142
|
+
|
143
|
+
#### Listing File Shares
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
# Get all file shares in the storage account
|
147
|
+
shares = AzureFileShares.client.file_shares.list
|
148
|
+
|
149
|
+
# Get shares with pagination and filtering
|
150
|
+
shares = AzureFileShares.client.file_shares.list(
|
151
|
+
maxpagesize: 10,
|
152
|
+
filter: "properties/shareQuota gt 5120"
|
153
|
+
)
|
154
|
+
|
155
|
+
# Access share properties
|
156
|
+
shares.each do |share|
|
157
|
+
puts "Share: #{share.name}"
|
158
|
+
puts " Quota: #{share.quota} GiB"
|
159
|
+
puts " Access Tier: #{share.access_tier}"
|
160
|
+
puts " Last Modified: #{share.last_modified_time}"
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
#### Getting a Specific Share
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
# Get a specific file share by name
|
168
|
+
share = AzureFileShares.client.file_shares.get('my-share-name')
|
169
|
+
```
|
170
|
+
|
171
|
+
#### Creating a New Share
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
# Create a new file share with default settings
|
175
|
+
share = AzureFileShares.client.file_shares.create('new-share-name')
|
176
|
+
|
177
|
+
# Create a new file share with specific settings
|
178
|
+
share = AzureFileShares.client.file_shares.create(
|
179
|
+
'new-share-name',
|
180
|
+
{
|
181
|
+
shareQuota: 5120, # 5 TB quota
|
182
|
+
accessTier: 'Hot',
|
183
|
+
enabledProtocols: 'SMB'
|
184
|
+
}
|
185
|
+
)
|
186
|
+
```
|
187
|
+
|
188
|
+
#### Updating a Share
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
# Update an existing file share
|
192
|
+
updated_share = AzureFileShares.client.file_shares.update(
|
193
|
+
'my-share-name',
|
194
|
+
{
|
195
|
+
shareQuota: 10240, # 10 TB quota
|
196
|
+
accessTier: 'Cool'
|
197
|
+
}
|
198
|
+
)
|
199
|
+
```
|
200
|
+
|
201
|
+
#### Deleting a Share
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
# Delete a file share
|
205
|
+
AzureFileShares.client.file_shares.delete('my-share-name')
|
206
|
+
|
207
|
+
# Delete a file share and its snapshots
|
208
|
+
AzureFileShares.client.file_shares.delete('my-share-name', delete_snapshots: true)
|
209
|
+
```
|
210
|
+
|
211
|
+
### Working with Snapshots
|
212
|
+
|
213
|
+
#### Creating a Snapshot
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
# Create a snapshot of a file share
|
217
|
+
snapshot = AzureFileShares.client.snapshots.create('my-share-name')
|
218
|
+
|
219
|
+
# Create a snapshot with metadata
|
220
|
+
snapshot = AzureFileShares.client.snapshots.create(
|
221
|
+
'my-share-name',
|
222
|
+
{
|
223
|
+
'created_by' => 'backup_service',
|
224
|
+
'backup_id' => '12345'
|
225
|
+
}
|
226
|
+
)
|
227
|
+
|
228
|
+
# Access snapshot details
|
229
|
+
puts "Snapshot created at: #{snapshot.timestamp}"
|
230
|
+
puts "Creation time: #{snapshot.creation_time}"
|
231
|
+
```
|
232
|
+
|
233
|
+
#### Listing Snapshots for a Share
|
234
|
+
|
235
|
+
```ruby
|
236
|
+
# List all snapshots for a file share
|
237
|
+
snapshots = AzureFileShares.client.snapshots.list('my-share-name')
|
238
|
+
|
239
|
+
# List snapshots with pagination
|
240
|
+
snapshots = AzureFileShares.client.snapshots.list('my-share-name', maxpagesize: 10)
|
241
|
+
```
|
242
|
+
|
243
|
+
#### Getting a Specific Snapshot
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
# Get a specific snapshot by share name and snapshot timestamp
|
247
|
+
snapshot = AzureFileShares.client.snapshots.get(
|
248
|
+
'my-share-name',
|
249
|
+
'2023-04-01T12:00:00.0000000Z'
|
250
|
+
)
|
251
|
+
```
|
252
|
+
|
253
|
+
#### Deleting a Snapshot
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
# Delete a specific snapshot
|
257
|
+
AzureFileShares.client.snapshots.delete(
|
258
|
+
'my-share-name',
|
259
|
+
'2023-04-01T12:00:00.0000000Z'
|
260
|
+
)
|
261
|
+
```
|
262
|
+
|
263
|
+
## Working with Files and Directories
|
264
|
+
|
265
|
+
Before using file operations, make sure to set up your storage account key:
|
266
|
+
|
267
|
+
```ruby
|
268
|
+
# Configure with storage account key
|
269
|
+
AzureFileShares.configure do |config|
|
270
|
+
# Basic configuration as above
|
271
|
+
config.storage_account_key = 'your-storage-account-key'
|
272
|
+
end
|
273
|
+
|
274
|
+
# Or set it on an existing client
|
275
|
+
AzureFileShares.client.storage_account_key = 'your-storage-account-key'
|
276
|
+
```
|
277
|
+
|
278
|
+
### Directory Operations
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
# Create a directory
|
282
|
+
AzureFileShares.client.files.create_directory('my-share-name', 'path/to/directory')
|
283
|
+
|
284
|
+
# Check if a directory exists
|
285
|
+
if AzureFileShares.client.files.directory_exists?('my-share-name', 'path/to/directory')
|
286
|
+
puts "Directory exists"
|
287
|
+
end
|
288
|
+
|
289
|
+
# List files and directories
|
290
|
+
contents = AzureFileShares.client.files.list('my-share-name', 'path/to/directory')
|
291
|
+
|
292
|
+
# Access directories
|
293
|
+
contents[:directories].each do |dir|
|
294
|
+
puts "Directory: #{dir[:name]}"
|
295
|
+
puts " Last Modified: #{dir[:properties][:last_modified]}"
|
296
|
+
end
|
297
|
+
|
298
|
+
# Access files
|
299
|
+
contents[:files].each do |file|
|
300
|
+
puts "File: #{file[:name]}"
|
301
|
+
puts " Size: #{file[:properties][:content_length]} bytes"
|
302
|
+
puts " Type: #{file[:properties][:content_type]}"
|
303
|
+
end
|
304
|
+
|
305
|
+
# Delete a directory (use recursive: true to delete contents)
|
306
|
+
AzureFileShares.client.files.delete_directory('my-share-name', 'path/to/directory', recursive: true)
|
307
|
+
```
|
308
|
+
|
309
|
+
### File Operations
|
310
|
+
|
311
|
+
#### Uploading Files
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
# Upload a file from a string
|
315
|
+
content = "This is the content of my file"
|
316
|
+
AzureFileShares.client.files.upload_file(
|
317
|
+
'my-share-name', # Share name
|
318
|
+
'path/to/directory', # Directory path (use '' for root)
|
319
|
+
'myfile.txt', # File name
|
320
|
+
content, # File content
|
321
|
+
content_type: 'text/plain' # Optional content type
|
322
|
+
)
|
323
|
+
|
324
|
+
# Upload a file from disk
|
325
|
+
content = File.read('local/path/to/myfile.txt')
|
326
|
+
AzureFileShares.client.files.upload_file(
|
327
|
+
'my-share-name',
|
328
|
+
'path/to/directory',
|
329
|
+
'myfile.txt',
|
330
|
+
content
|
331
|
+
)
|
332
|
+
|
333
|
+
# Upload with metadata
|
334
|
+
AzureFileShares.client.files.upload_file(
|
335
|
+
'my-share-name',
|
336
|
+
'path/to/directory',
|
337
|
+
'myfile.txt',
|
338
|
+
content,
|
339
|
+
metadata: {
|
340
|
+
'created_by' => 'user123',
|
341
|
+
'department' => 'engineering'
|
342
|
+
}
|
343
|
+
)
|
344
|
+
```
|
345
|
+
|
346
|
+
#### Downloading Files
|
347
|
+
|
348
|
+
```ruby
|
349
|
+
# Download a file
|
350
|
+
content = AzureFileShares.client.files.download_file(
|
351
|
+
'my-share-name',
|
352
|
+
'path/to/directory',
|
353
|
+
'myfile.txt'
|
354
|
+
)
|
355
|
+
|
356
|
+
# Save to disk
|
357
|
+
File.write('local/path/to/downloaded.txt', content)
|
358
|
+
|
359
|
+
# Download a range of bytes
|
360
|
+
partial_content = AzureFileShares.client.files.download_file(
|
361
|
+
'my-share-name',
|
362
|
+
'path/to/directory',
|
363
|
+
'myfile.txt',
|
364
|
+
range: 0..1023 # First 1KB
|
365
|
+
)
|
366
|
+
```
|
367
|
+
|
368
|
+
#### File Management
|
369
|
+
|
370
|
+
```ruby
|
371
|
+
# Check if a file exists
|
372
|
+
if AzureFileShares.client.files.file_exists?('my-share-name', 'path/to/directory', 'myfile.txt')
|
373
|
+
puts "File exists"
|
374
|
+
end
|
375
|
+
|
376
|
+
# Get file properties
|
377
|
+
properties = AzureFileShares.client.files.get_file_properties(
|
378
|
+
'my-share-name',
|
379
|
+
'path/to/directory',
|
380
|
+
'myfile.txt'
|
381
|
+
)
|
382
|
+
|
383
|
+
puts "File size: #{properties[:content_length]} bytes"
|
384
|
+
puts "Content type: #{properties[:content_type]}"
|
385
|
+
puts "Last modified: #{properties[:last_modified]}"
|
386
|
+
puts "Metadata: #{properties[:metadata]}"
|
387
|
+
|
388
|
+
# Delete a file
|
389
|
+
AzureFileShares.client.files.delete_file(
|
390
|
+
'my-share-name',
|
391
|
+
'path/to/directory',
|
392
|
+
'myfile.txt'
|
393
|
+
)
|
394
|
+
|
395
|
+
# Copy a file
|
396
|
+
AzureFileShares.client.files.copy_file(
|
397
|
+
'source-share', # Source share name
|
398
|
+
'source/directory', # Source directory path
|
399
|
+
'source-file.txt', # Source file name
|
400
|
+
'dest-share', # Destination share name
|
401
|
+
'dest/directory', # Destination directory path
|
402
|
+
'dest-file.txt' # Destination file name
|
403
|
+
)
|
404
|
+
|
405
|
+
# Generate a SAS URL for a file (time-limited access)
|
406
|
+
sas_url = AzureFileShares.client.files.generate_file_sas_url(
|
407
|
+
'my-share-name',
|
408
|
+
'path/to/directory',
|
409
|
+
'myfile.txt',
|
410
|
+
expiry: Time.now + 3600, # 1 hour from now
|
411
|
+
permissions: 'r' # Read-only access
|
412
|
+
)
|
413
|
+
|
414
|
+
puts "Access file at: #{sas_url}"
|
415
|
+
```
|
416
|
+
|
417
|
+
## Error Handling
|
418
|
+
|
419
|
+
The gem uses custom error classes to provide meaningful error information:
|
420
|
+
|
421
|
+
```ruby
|
422
|
+
begin
|
423
|
+
share = AzureFileShares.client.file_shares.get('non-existent-share')
|
424
|
+
rescue AzureFileShares::Errors::ApiError => e
|
425
|
+
puts "API Error (#{e.status}): #{e.message}"
|
426
|
+
puts "Response: #{e.response}"
|
427
|
+
end
|
428
|
+
|
429
|
+
begin
|
430
|
+
AzureFileShares.configure do |config|
|
431
|
+
# Missing required fields
|
432
|
+
end
|
433
|
+
AzureFileShares.client.file_shares.list
|
434
|
+
rescue AzureFileShares::Errors::ConfigurationError => e
|
435
|
+
puts "Configuration Error: #{e.message}"
|
436
|
+
end
|
437
|
+
```
|
438
|
+
|
439
|
+
## Creating Microsoft Entra App Registration
|
440
|
+
|
441
|
+
Before using this gem, you need to register an application in Microsoft Entra ID:
|
442
|
+
|
443
|
+
1. Sign in to the [Azure portal](https://portal.azure.com)
|
444
|
+
2. Navigate to **Microsoft Entra ID** > **App registrations** > **New registration**
|
445
|
+
3. Enter a name for your application
|
446
|
+
4. Select the appropriate supported account type
|
447
|
+
5. Click **Register**
|
448
|
+
6. Once registered, note the **Application (client) ID** and **Directory (tenant) ID**
|
449
|
+
7. Navigate to **Certificates & secrets** > **Client secrets** > **New client secret**
|
450
|
+
8. Create a new secret and note the value (you won't be able to see it again)
|
451
|
+
9. Navigate to **API permissions** and add the following permissions:
|
452
|
+
- Microsoft.Storage > user_impersonation
|
453
|
+
10. Click **Grant admin consent** for your directory
|
454
|
+
|
455
|
+
You also need to assign appropriate RBAC roles to the registered application for your storage account:
|
456
|
+
- **Storage File Data SMB Share Contributor** (for full access)
|
457
|
+
- **Storage File Data SMB Share Reader** (for read access)
|
458
|
+
|
459
|
+
## Development
|
460
|
+
|
461
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
462
|
+
|
463
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
464
|
+
|
465
|
+
## Testing
|
466
|
+
|
467
|
+
To run the test suite:
|
468
|
+
|
469
|
+
```bash
|
470
|
+
$ bundle exec rspec
|
471
|
+
```
|
472
|
+
|
473
|
+
## Contributing
|
474
|
+
|
475
|
+
1. Fork it
|
476
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
477
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
478
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
479
|
+
5. Create a new Pull Request
|
480
|
+
|
481
|
+
## License
|
482
|
+
|
483
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module AzureFileShares
|
2
|
+
module Auth
|
3
|
+
# Handles authentication with Azure AD to retrieve access tokens
|
4
|
+
class TokenProvider
|
5
|
+
# Azure AD authentication endpoint
|
6
|
+
TOKEN_ENDPOINT = "https://login.microsoftonline.com/%s/oauth2/v2.0/token"
|
7
|
+
# Default token expiry buffer in seconds (5 minutes)
|
8
|
+
TOKEN_EXPIRY_BUFFER = 300
|
9
|
+
# Default resource scope
|
10
|
+
DEFAULT_SCOPE = "https://management.azure.com/.default"
|
11
|
+
|
12
|
+
attr_reader :tenant_id, :client_id, :client_secret, :scope
|
13
|
+
|
14
|
+
# Initialize a new TokenProvider
|
15
|
+
# @param [String] tenant_id Azure tenant ID
|
16
|
+
# @param [String] client_id Azure client ID (application ID)
|
17
|
+
# @param [String] client_secret Azure client secret
|
18
|
+
# @param [String] scope Resource scope, defaults to management.azure.com
|
19
|
+
def initialize(tenant_id, client_id, client_secret, scope = DEFAULT_SCOPE)
|
20
|
+
@tenant_id = tenant_id
|
21
|
+
@client_id = client_id
|
22
|
+
@client_secret = client_secret
|
23
|
+
@scope = scope
|
24
|
+
@token = nil
|
25
|
+
@token_expires_at = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get a valid access token, refreshing if necessary
|
29
|
+
# @return [String] Access token
|
30
|
+
def access_token
|
31
|
+
refresh_token if token_expired?
|
32
|
+
@token
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Check if the current token is expired or will expire soon
|
38
|
+
# @return [Boolean] true if token needs refresh
|
39
|
+
def token_expired?
|
40
|
+
return true if @token.nil? || @token_expires_at.nil?
|
41
|
+
|
42
|
+
Time.now.to_i >= (@token_expires_at - TOKEN_EXPIRY_BUFFER)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Refresh the access token
|
46
|
+
# @return [String] New access token
|
47
|
+
def refresh_token
|
48
|
+
endpoint = format(TOKEN_ENDPOINT, tenant_id)
|
49
|
+
|
50
|
+
response = Faraday.new(url: endpoint) do |conn|
|
51
|
+
conn.request :url_encoded
|
52
|
+
conn.adapter Faraday.default_adapter
|
53
|
+
end.post do |req|
|
54
|
+
req.body = {
|
55
|
+
client_id: client_id,
|
56
|
+
client_secret: client_secret,
|
57
|
+
grant_type: "client_credentials",
|
58
|
+
scope: scope,
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
handle_token_response(response)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Handle the token response from Azure AD
|
66
|
+
# @param [Faraday::Response] response The HTTP response
|
67
|
+
# @return [String] Access token
|
68
|
+
# @raise [AzureFileShares::Errors::ApiError] If token request fails
|
69
|
+
def handle_token_response(response)
|
70
|
+
if response.status != 200
|
71
|
+
raise AzureFileShares::Errors::ApiError.new(
|
72
|
+
"Failed to obtain access token: #{response.body}",
|
73
|
+
response.status,
|
74
|
+
response.body
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
data = JSON.parse(response.body)
|
79
|
+
@token = data["access_token"]
|
80
|
+
# Subtract a small buffer from expiry time to ensure token validity
|
81
|
+
@token_expires_at = Time.now.to_i + data["expires_in"].to_i
|
82
|
+
@token
|
83
|
+
rescue JSON::ParserError => e
|
84
|
+
raise AzureFileShares::Errors::ApiError.new(
|
85
|
+
"Failed to parse token response: #{e.message}",
|
86
|
+
response.status,
|
87
|
+
response.body
|
88
|
+
)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module AzureFileShares
|
2
|
+
# Client for interacting with the Azure File Shares API
|
3
|
+
class Client
|
4
|
+
attr_reader :configuration
|
5
|
+
|
6
|
+
# Delegate configuration methods to configuration object
|
7
|
+
%i[
|
8
|
+
tenant_id client_id client_secret subscription_id
|
9
|
+
resource_group_name storage_account_name api_version
|
10
|
+
base_url request_timeout logger
|
11
|
+
].each do |method|
|
12
|
+
define_method(method) do
|
13
|
+
configuration.send(method)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Initialize a new client
|
18
|
+
# @param [AzureFileShares::Configuration] configuration Client configuration
|
19
|
+
def initialize(configuration = nil)
|
20
|
+
@configuration = configuration || AzureFileShares.configuration
|
21
|
+
@configuration.validate!
|
22
|
+
@token_provider = if @configuration.tenant_id && @configuration.client_id && @configuration.client_secret
|
23
|
+
Auth::TokenProvider.new(
|
24
|
+
@configuration.tenant_id,
|
25
|
+
@configuration.client_id,
|
26
|
+
@configuration.client_secret
|
27
|
+
)
|
28
|
+
end
|
29
|
+
@connection = nil
|
30
|
+
@operations = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
# Storage account key from configuration
|
34
|
+
# @return [String] Storage account key
|
35
|
+
def storage_account_key
|
36
|
+
@configuration.storage_account_key
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get the HTTP connection
|
40
|
+
# @return [Faraday::Connection] Faraday connection
|
41
|
+
def connection
|
42
|
+
@connection ||= create_connection
|
43
|
+
end
|
44
|
+
|
45
|
+
# Get an access token for authentication
|
46
|
+
# @return [String] Access token
|
47
|
+
# @raise [AzureFileShares::Errors::ConfigurationError] if OAuth credentials are missing
|
48
|
+
def access_token
|
49
|
+
if @token_provider.nil?
|
50
|
+
raise AzureFileShares::Errors::ConfigurationError,
|
51
|
+
"OAuth credentials (tenant_id, client_id, client_secret) are required for ARM operations"
|
52
|
+
end
|
53
|
+
@token_provider.access_token
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get a FileSharesOperations instance
|
57
|
+
# @return [AzureFileShares::Operations::FileSharesOperations]
|
58
|
+
def file_shares
|
59
|
+
@operations[:file_shares] ||= Operations::FileSharesOperations.new(self)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Get a SnapshotsOperations instance
|
63
|
+
# @return [AzureFileShares::Operations::SnapshotsOperations]
|
64
|
+
def snapshots
|
65
|
+
@operations[:snapshots] ||= Operations::SnapshotsOperations.new(self)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Get a FileOperations instance
|
69
|
+
# @return [AzureFileShares::Operations::FileOperations]
|
70
|
+
def files
|
71
|
+
@operations[:files] ||= Operations::FileOperations.new(self)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# Create a new Faraday connection
|
77
|
+
# @return [Faraday::Connection] Faraday connection
|
78
|
+
def create_connection
|
79
|
+
Faraday.new do |conn|
|
80
|
+
conn.options.timeout = configuration.request_timeout
|
81
|
+
conn.request :json
|
82
|
+
conn.response :json, content_type: /\bjson$/
|
83
|
+
conn.response :logger, configuration.logger if configuration.logger
|
84
|
+
conn.request :retry, {
|
85
|
+
max: 3,
|
86
|
+
interval: 0.5,
|
87
|
+
interval_randomness: 0.5,
|
88
|
+
backoff_factor: 2,
|
89
|
+
exceptions: [
|
90
|
+
Faraday::ConnectionFailed,
|
91
|
+
Faraday::TimeoutError,
|
92
|
+
Faraday::SSLError,
|
93
|
+
],
|
94
|
+
}
|
95
|
+
conn.adapter Faraday.default_adapter
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|