Skip to content

Commit 71db5d5

Browse files
bgraingerloic-sharma
authored andcommitted
Add support for Google buckets storage (loic-sharma#233)
1 parent 873b133 commit 71db5d5

File tree

10 files changed

+212
-6
lines changed

10 files changed

+212
-6
lines changed

BaGet.sln

+7
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BaGet.Database.MySql", "src
4040
EndProject
4141
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaGet.Database.PostgreSql", "src\BaGet.Database.PostgreSql\BaGet.Database.PostgreSql.csproj", "{F48F201A-4DEE-4D5B-9C0B-59490FE942FA}"
4242
EndProject
43+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BaGet.GCP", "src\BaGet.GCP\BaGet.GCP.csproj", "{D7D60BA0-FF7F-4B37-815C-74D487C5176E}"
44+
EndProject
4345
Global
4446
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4547
Debug|Any CPU = Debug|Any CPU
@@ -102,6 +104,10 @@ Global
102104
{F48F201A-4DEE-4D5B-9C0B-59490FE942FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
103105
{F48F201A-4DEE-4D5B-9C0B-59490FE942FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
104106
{F48F201A-4DEE-4D5B-9C0B-59490FE942FA}.Release|Any CPU.Build.0 = Release|Any CPU
107+
{D7D60BA0-FF7F-4B37-815C-74D487C5176E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
108+
{D7D60BA0-FF7F-4B37-815C-74D487C5176E}.Debug|Any CPU.Build.0 = Debug|Any CPU
109+
{D7D60BA0-FF7F-4B37-815C-74D487C5176E}.Release|Any CPU.ActiveCfg = Release|Any CPU
110+
{D7D60BA0-FF7F-4B37-815C-74D487C5176E}.Release|Any CPU.Build.0 = Release|Any CPU
105111
EndGlobalSection
106112
GlobalSection(SolutionProperties) = preSolution
107113
HideSolutionNode = FALSE
@@ -121,6 +127,7 @@ Global
121127
{4C513AFC-BA7B-471D-B8F6-268E7AD2074C} = {26A0B557-53FB-4B9A-94C4-BCCF1BDCB0CC}
122128
{A4375529-E855-4D46-AA4F-B3FE630C3DE1} = {26A0B557-53FB-4B9A-94C4-BCCF1BDCB0CC}
123129
{F48F201A-4DEE-4D5B-9C0B-59490FE942FA} = {26A0B557-53FB-4B9A-94C4-BCCF1BDCB0CC}
130+
{D7D60BA0-FF7F-4B37-815C-74D487C5176E} = {26A0B557-53FB-4B9A-94C4-BCCF1BDCB0CC}
124131
EndGlobalSection
125132
GlobalSection(ExtensibilityGlobals) = postSolution
126133
SolutionGuid = {1423C027-2C90-417F-8629-2A4CF107C055}

docs/cloud/gcp.md

+38-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,44 @@
1-
# Running BaGet on the Google Cloud
1+
# Running BaGet on Google Cloud
22

33
!!! warning
44
This page is a work in progress!
55

6-
Sadly, BaGet does not support GCP today. We're open source and accept contributions!
6+
We're open source and accept contributions!
77
[Fork us on GitHub](https://github.com/loic-sharma/BaGet).
88

9-
For now, please refer to the [Azure documentation](azure).
9+
## Google Cloud Storage
10+
11+
Packages can be stored in [Google Cloud Storage](https://cloud.google.com/storage/).
12+
13+
### Setup
14+
15+
Follow the instructions in [Using Cloud Storage](https://cloud.google.com/appengine/docs/flexible/dotnet/using-cloud-storage) to:
16+
17+
* Create a bucket
18+
* Set up a service account and download credentials
19+
* Set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to the path to the JSON file you downloaded
20+
21+
### Configuration
22+
23+
Configure BaGet to use GCS by updating the [`appsettings.json`](https://github.com/loic-sharma/BaGet/blob/master/src/BaGet/appsettings.json) file:
24+
25+
```json
26+
{
27+
...
28+
29+
"Storage": {
30+
"Type": "GoogleCloud",
31+
"BucketName": "your-gcs-bucket"
32+
},
33+
34+
...
35+
}
36+
```
37+
38+
## Google Cloud SQL
39+
40+
* TODO
41+
42+
## Google AppEngine
43+
44+
* TODO

src/BaGet.AWS/S3StorageService.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@ public class S3StorageService : IStorageService
1717
private readonly string _prefix;
1818
private readonly AmazonS3Client _client;
1919

20-
public S3StorageService(IOptions<S3StorageOptions> options, AmazonS3Client client)
20+
public S3StorageService(IOptionsSnapshot<S3StorageOptions> options, AmazonS3Client client)
2121
{
22+
if (options == null)
23+
throw new ArgumentNullException(nameof(options));
24+
2225
_bucket = options.Value.Bucket;
2326
_prefix = options.Value.Prefix;
24-
_client = client;
27+
_client = client ?? throw new ArgumentNullException(nameof(client));
2528

2629
if (!string.IsNullOrEmpty(_prefix) && !_prefix.EndsWith(Separator))
2730
_prefix += Separator;

src/BaGet.Core/Configuration/StorageOptions.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public enum StorageType
99
{
1010
FileSystem = 0,
1111
AzureBlobStorage = 1,
12-
AwsS3 = 2
12+
AwsS3 = 2,
13+
GoogleCloud = 3,
1314
}
1415
}

src/BaGet.GCP/BaGet.GCP.csproj

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="Google.Cloud.Storage.V1" Version="2.2.1" />
9+
</ItemGroup>
10+
11+
<ItemGroup>
12+
<ProjectReference Include="..\BaGet.Core\BaGet.Core.csproj" />
13+
</ItemGroup>
14+
15+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using BaGet.Core.Configuration;
3+
4+
namespace BaGet.GCP.Configuration
5+
{
6+
public class GoogleCloudStorageOptions : StorageOptions
7+
{
8+
[Required]
9+
public string BucketName { get; set; }
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using BaGet.Core.Services;
2+
using BaGet.GCP.Services;
3+
using Microsoft.Extensions.DependencyInjection;
4+
5+
namespace BaGet.GCP.Extensions
6+
{
7+
public static class ServiceCollectionExtensions
8+
{
9+
public static IServiceCollection AddGoogleCloudStorageService(this IServiceCollection services)
10+
{
11+
services.AddTransient<GoogleCloudStorageService>();
12+
return services;
13+
}
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Security.Cryptography;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using BaGet.Core.Services;
9+
using BaGet.GCP.Configuration;
10+
using Google;
11+
using Google.Cloud.Storage.V1;
12+
using Microsoft.Extensions.Options;
13+
14+
namespace BaGet.GCP.Services
15+
{
16+
public class GoogleCloudStorageService : IStorageService
17+
{
18+
private readonly string _bucketName;
19+
20+
public GoogleCloudStorageService(IOptionsSnapshot<GoogleCloudStorageOptions> options)
21+
{
22+
if (options == null)
23+
throw new ArgumentNullException(nameof(options));
24+
25+
_bucketName = options.Value.BucketName;
26+
}
27+
28+
public async Task<Stream> GetAsync(string path, CancellationToken cancellationToken = default)
29+
{
30+
using (var storage = await StorageClient.CreateAsync())
31+
{
32+
var stream = new MemoryStream();
33+
await storage.DownloadObjectAsync(_bucketName, CoercePath(path), stream, cancellationToken: cancellationToken);
34+
stream.Position = 0;
35+
return stream;
36+
}
37+
}
38+
39+
public Task<Uri> GetDownloadUriAsync(string path, CancellationToken cancellationToken = default)
40+
{
41+
// returns an Authenticated Browser Download URL: https://cloud.google.com/storage/docs/request-endpoints#cookieauth
42+
return Task.FromResult(new Uri($"https://storage.googleapis.com/{_bucketName}/{CoercePath(path).TrimStart('/')}"));
43+
}
44+
45+
public async Task<PutResult> PutAsync(string path, Stream content, string contentType, CancellationToken cancellationToken = default)
46+
{
47+
using (var storage = await StorageClient.CreateAsync())
48+
using (var seekableContent = new MemoryStream())
49+
{
50+
await content.CopyToAsync(seekableContent, 65536, cancellationToken);
51+
seekableContent.Position = 0;
52+
53+
var objectName = CoercePath(path);
54+
55+
try
56+
{
57+
// attempt to upload, succeeding only if the object doesn't exist
58+
await storage.UploadObjectAsync(_bucketName, objectName, contentType, seekableContent, new UploadObjectOptions { IfGenerationMatch = 0 }, cancellationToken);
59+
return PutResult.Success;
60+
}
61+
catch (GoogleApiException e) when (e.HttpStatusCode == HttpStatusCode.PreconditionFailed)
62+
{
63+
// the object already exists; get the hash of its content from its metadata
64+
var existingObject = await storage.GetObjectAsync(_bucketName, objectName, cancellationToken: cancellationToken);
65+
var existingHash = Convert.FromBase64String(existingObject.Md5Hash);
66+
67+
// hash the content that was uploaded
68+
seekableContent.Position = 0;
69+
byte[] contentHash;
70+
using (var md5 = MD5.Create())
71+
contentHash = md5.ComputeHash(seekableContent);
72+
73+
// conflict if the two hashes are different
74+
return existingHash.SequenceEqual(contentHash) ? PutResult.AlreadyExists : PutResult.Conflict;
75+
}
76+
}
77+
}
78+
79+
public async Task DeleteAsync(string path, CancellationToken cancellationToken = default)
80+
{
81+
using (var storage = await StorageClient.CreateAsync())
82+
{
83+
try
84+
{
85+
var obj = await storage.GetObjectAsync(_bucketName, CoercePath(path), cancellationToken: cancellationToken);
86+
await storage.DeleteObjectAsync(obj, cancellationToken: cancellationToken);
87+
}
88+
catch (GoogleApiException e) when (e.HttpStatusCode == HttpStatusCode.NotFound)
89+
{
90+
}
91+
}
92+
}
93+
94+
private static string CoercePath(string path)
95+
{
96+
// although Google Cloud Storage objects exist in a flat namespace, using forward slashes allows the objects to
97+
// be exposed as nested subdirectories, e.g., when browsing via Google Cloud Console
98+
return path.Replace('\\', '/');
99+
}
100+
}
101+
}

src/BaGet/BaGet.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<ProjectReference Include="..\BaGet.Database.Sqlite\BaGet.Database.Sqlite.csproj" />
3232
<ProjectReference Include="..\BaGet.Database.SqlServer\BaGet.Database.SqlServer.csproj" />
3333
<ProjectReference Include="..\BaGet.Database.PostgreSql\BaGet.Database.PostgreSql.csproj" />
34+
<ProjectReference Include="..\BaGet.GCP\BaGet.GCP.csproj" />
3435
<ProjectReference Include="..\BaGet.Protocol\BaGet.Protocol.csproj" />
3536
</ItemGroup>
3637

src/BaGet/Extensions/IServiceCollectionExtensions.cs

+17
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
using BaGet.Database.PostgreSql;
1919
using BaGet.Database.Sqlite;
2020
using BaGet.Database.SqlServer;
21+
using BaGet.GCP.Configuration;
22+
using BaGet.GCP.Extensions;
23+
using BaGet.GCP.Services;
2124
using BaGet.Protocol;
2225
using Microsoft.EntityFrameworkCore;
2326
using Microsoft.Extensions.Configuration;
@@ -44,6 +47,7 @@ public static IServiceCollection ConfigureBaGet(
4447

4548
services.ConfigureAzure(configuration);
4649
services.ConfigureAws(configuration);
50+
services.ConfigureGcp(configuration);
4751

4852
if (httpServices)
4953
{
@@ -144,6 +148,15 @@ public static IServiceCollection ConfigureAws(
144148
return services;
145149
}
146150

151+
public static IServiceCollection ConfigureGcp(
152+
this IServiceCollection services,
153+
IConfiguration configuration)
154+
{
155+
services.ConfigureAndValidate<GoogleCloudStorageOptions>(configuration.GetSection(nameof(BaGetOptions.Storage)));
156+
157+
return services;
158+
}
159+
147160
public static IServiceCollection AddStorageProviders(this IServiceCollection services)
148161
{
149162
services.AddTransient<FileStorageService>();
@@ -152,6 +165,7 @@ public static IServiceCollection AddStorageProviders(this IServiceCollection ser
152165

153166
services.AddBlobStorageService();
154167
services.AddS3StorageService();
168+
services.AddGoogleCloudStorageService();
155169

156170
services.AddTransient<IStorageService>(provider =>
157171
{
@@ -168,6 +182,9 @@ public static IServiceCollection AddStorageProviders(this IServiceCollection ser
168182
case StorageType.AwsS3:
169183
return provider.GetRequiredService<S3StorageService>();
170184

185+
case StorageType.GoogleCloud:
186+
return provider.GetRequiredService<GoogleCloudStorageService>();
187+
171188
default:
172189
throw new InvalidOperationException(
173190
$"Unsupported storage service: {options.Value.Storage.Type}");

0 commit comments

Comments
 (0)