Skip to content

Commit ac57d33

Browse files
committed
Shared Box Client
1 parent 75d38ab commit ac57d33

File tree

1 file changed

+61
-29
lines changed

1 file changed

+61
-29
lines changed
Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
1-
using Box.V2;
1+
using System;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
using System.Threading;
5+
6+
using Microsoft.EntityFrameworkCore;
7+
using Microsoft.Extensions.Logging;
8+
9+
using Box.V2;
210
using Box.V2.Auth;
311
using Box.V2.Config;
12+
413
using ClassTranscribeDatabase.Models;
5-
using Microsoft.EntityFrameworkCore;
6-
using Microsoft.Extensions.Logging;
7-
using System;
8-
using System.Linq;
9-
using System.Threading.Tasks;
14+
1015

1116
namespace ClassTranscribeDatabase.Services
1217
{
1318
public class BoxAPI
1419
{
1520
private readonly SlackLogger _slack;
1621
private readonly ILogger _logger;
22+
private BoxClient? _boxClient;
23+
private DateTimeOffset _lastRefreshed = DateTimeOffset.MinValue;
24+
private SemaphoreSlim _RefreshSemaphore = new SemaphoreSlim(1, 1); // async-safe mutex to ensure only one thread is refreshing the token at a time
25+
1726
public BoxAPI(ILogger<BoxAPI> logger, SlackLogger slack)
1827
{
1928
_logger = logger;
@@ -30,6 +39,7 @@ public async Task CreateAccessTokenAsync(string authCode)
3039
// This implementation is overly chatty with the database, but we rarely create access tokens so it is not a problem
3140
using (var _context = CTDbContext.CreateDbContext())
3241
{
42+
3343
if (!await _context.Dictionaries.Where(d => d.Key == CommonUtils.BOX_ACCESS_TOKEN).AnyAsync())
3444
{
3545
_context.Dictionaries.Add(new Dictionary
@@ -47,13 +57,15 @@ public async Task CreateAccessTokenAsync(string authCode)
4757
await _context.SaveChangesAsync();
4858
}
4959

50-
60+
5161
var accessToken = _context.Dictionaries.Where(d => d.Key == CommonUtils.BOX_ACCESS_TOKEN).First();
5262
var refreshToken = _context.Dictionaries.Where(d => d.Key == CommonUtils.BOX_REFRESH_TOKEN).First();
5363
var config = new BoxConfig(Globals.appSettings.BOX_CLIENT_ID, Globals.appSettings.BOX_CLIENT_SECRET, new Uri("http://locahost"));
54-
var client = new Box.V2.BoxClient(config);
55-
var auth = await client.Auth.AuthenticateAsync(authCode);
56-
_logger.LogInformation("Created Box Tokens");
64+
var tmpClient = new Box.V2.BoxClient(config);
65+
var auth = await tmpClient.Auth.AuthenticateAsync(authCode);
66+
67+
_logger.LogInformation($"Created Box Tokens Access:({auth.AccessToken.Substring(0, 5)}) Refresh({auth.RefreshToken.Substring(0, 5)})");
68+
5769
accessToken.Value = auth.AccessToken;
5870
refreshToken.Value = auth.RefreshToken;
5971
await _context.SaveChangesAsync();
@@ -62,31 +74,37 @@ public async Task CreateAccessTokenAsync(string authCode)
6274
/// <summary>
6375
/// Updates the accessToken and refreshToken. These keys must already exist in the Dictionary table.
6476
/// </summary>
65-
public async Task RefreshAccessTokenAsync()
77+
private async Task<BoxClient> RefreshAccessTokenAsync()
6678
{
79+
// Only one thread should call this at a time (see semaphore in GetBoxClientAsync)
6780
try
6881
{
82+
_logger.LogInformation($"RefreshAccessTokenAsync: Starting");
6983
using (var _context = CTDbContext.CreateDbContext())
7084
{
7185
var accessToken = await _context.Dictionaries.Where(d => d.Key == CommonUtils.BOX_ACCESS_TOKEN).FirstAsync();
7286
var refreshToken = await _context.Dictionaries.Where(d => d.Key == CommonUtils.BOX_REFRESH_TOKEN).FirstAsync();
7387
var config = new BoxConfig(Globals.appSettings.BOX_CLIENT_ID, Globals.appSettings.BOX_CLIENT_SECRET, new Uri("http://locahost"));
74-
var auth = new OAuthSession(accessToken.Value, refreshToken.Value, 3600, "bearer");
75-
var client = new BoxClient(config, auth);
76-
/// Try to refresh the access token
77-
auth = await client.Auth.RefreshAccessTokenAsync(auth.AccessToken);
88+
var initialAuth = new OAuthSession(accessToken.Value, refreshToken.Value, 3600, "bearer");
89+
var initialClient = new BoxClient(config, initialAuth);
90+
/// Refresh the access token
91+
var auth = await initialClient.Auth.RefreshAccessTokenAsync(initialAuth.AccessToken);
7892
/// Create the client again
79-
client = new BoxClient(config, auth);
80-
_logger.LogInformation("Refreshed Tokens");
93+
_logger.LogInformation($"RefreshAccessTokenAsync: New Access Token ({auth.AccessToken.Substring(0, 5)}), New Refresh Token ({auth.RefreshToken.Substring(0, 5)})");
94+
8195
accessToken.Value = auth.AccessToken;
8296
refreshToken.Value = auth.RefreshToken;
97+
_lastRefreshed = DateTimeOffset.Now;
8398
await _context.SaveChangesAsync();
99+
_logger.LogInformation($"RefreshAccessTokenAsync: Creating New Box Client");
100+
var client = new BoxClient(config, auth);
101+
return client;
84102
}
85103
}
86104
catch (Box.V2.Exceptions.BoxSessionInvalidatedException e)
87105
{
88-
_logger.LogError(e, "Box Token Failure.");
89-
await _slack.PostErrorAsync(e, "Box Token Failure.");
106+
_logger.LogError(e, "RefreshAccessTokenAsync: Box Token Failure.");
107+
await _slack.PostErrorAsync(e, "RefreshAccessTokenAsync: Box Token Failure.");
90108
throw;
91109
}
92110
}
@@ -95,18 +113,32 @@ public async Task RefreshAccessTokenAsync()
95113
/// </summary>
96114
public async Task<BoxClient> GetBoxClientAsync()
97115
{
98-
// Todo RefreshAccessTokenAsync could return this information for us; and avoid another trip to the database
99-
await RefreshAccessTokenAsync();
100-
BoxClient boxClient;
101-
using (var _context = CTDbContext.CreateDbContext())
116+
try
102117
{
103-
var accessToken = await _context.Dictionaries.Where(d => d.Key == CommonUtils.BOX_ACCESS_TOKEN).FirstAsync();
104-
var refreshToken = await _context.Dictionaries.Where(d => d.Key == CommonUtils.BOX_REFRESH_TOKEN).FirstAsync();
105-
var config = new BoxConfig(Globals.appSettings.BOX_CLIENT_ID, Globals.appSettings.BOX_CLIENT_SECRET, new Uri("http://locahost"));
106-
var auth = new OAuthSession(accessToken.Value, refreshToken.Value, 3600, "bearer");
107-
boxClient = new Box.V2.BoxClient(config, auth);
118+
await _RefreshSemaphore.WaitAsync(); // // critical section : implementation of an async-safe mutex
119+
var MAX_AGE_MINUTES = 50;
120+
var remain = DateTimeOffset.Now.Subtract(_lastRefreshed).TotalMinutes;
121+
_logger.LogInformation($"GetBoxClientAsync: {remain} minutes since last refresh. Max age {MAX_AGE_MINUTES}.");
122+
if (_boxClient != null && remain < MAX_AGE_MINUTES)
123+
{
124+
return _boxClient;
125+
}
126+
_boxClient = await RefreshAccessTokenAsync();
127+
_logger.LogInformation($"GetBoxClientAsync: _boxClient updated");
128+
}
129+
catch (Exception e)
130+
{
131+
_logger.LogError(e, "GetBoxClientAsync: Box Refresh Failure.");
132+
throw;
108133
}
109-
return boxClient;
134+
finally
135+
{
136+
_logger.LogInformation($"GetBoxClientAsync: Releasing Semaphore and returning");
137+
_RefreshSemaphore.Release(1);
138+
}
139+
140+
return _boxClient;
110141
}
142+
111143
}
112144
}

0 commit comments

Comments
 (0)