Skip to content

Commit 0960dab

Browse files
kramercsuflors
authored andcommitted
Add S3 uploader
1 parent 8f5b7a1 commit 0960dab

12 files changed

+1766
-511
lines changed

DiscordBee.cs

+43-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace MusicBeePlugin
44
using MusicBeePlugin.DiscordTools;
55
using MusicBeePlugin.DiscordTools.Assets;
66
using MusicBeePlugin.DiscordTools.Assets.Uploader;
7+
using MusicBeePlugin.S3Client;
78
using MusicBeePlugin.UI;
89
using System;
910
using System.Collections.Generic;
@@ -31,6 +32,7 @@ public partial class Plugin
3132
private readonly Timer _updateTimer = new Timer(300);
3233
private string _imgurAssetCachePath;
3334
private string _imgurAlbum;
35+
private string _s3AssetCachePath;
3436

3537
public Plugin()
3638
{
@@ -80,13 +82,32 @@ public PluginInfo Initialise(IntPtr apiInterfacePtr)
8082
var settingsFilePath = $"{workingDir}\\{_about.Name}.settings";
8183
_imgurAssetCachePath = $"{workingDir}\\{_about.Name}-Imgur.cache";
8284
_imgurAlbum = $"{workingDir}\\{_about.Name}-Imgur.album";
85+
_s3AssetCachePath = $"{workingDir}\\{_about.Name}-S3.cache";
8386

8487
_settings = Settings.GetInstance(settingsFilePath);
8588
_settings.SettingChanged += SettingChangedCallback;
8689

8790
_discordClient.ArtworkUploadEnabled = _settings.UploadArtwork;
8891
_discordClient.DiscordId = _settings.DiscordAppId;
89-
UpdateAssetManager(_imgurAssetCachePath, new ImgurUploader(_imgurAlbum, _settings.ImgurClientId));
92+
93+
switch (_settings.ArtworkUploader)
94+
{
95+
case "Imgur":
96+
UpdateAssetManager(_imgurAssetCachePath, new ImgurUploader(_imgurAlbum, _settings.ImgurClientId));
97+
break;
98+
case "Amazon S3":
99+
UpdateAssetManager(_s3AssetCachePath, new S3Uploader(new S3Config
100+
{
101+
AccessKeyId = _settings.S3AccessKeyId,
102+
SecretAccessKey = _settings.S3SecretAccessKey,
103+
Endpoint = _settings.S3Endpoint,
104+
BucketName = _settings.S3BucketName,
105+
Prefix = _settings.S3Prefix,
106+
CustomDomain = _settings.S3CustomDomain
107+
}));
108+
break;
109+
}
110+
90111
ToolStripMenuItem mainMenuItem = (ToolStripMenuItem)_mbApiInterface.MB_AddMenuItem($"mnuTools/{_about.Name}", null, null);
91112
mainMenuItem.DropDown.Items.Add("Uploader Health", null, ShowUploaderHealth);
92113

@@ -138,10 +159,30 @@ private void SettingChangedCallback(object sender, Settings.SettingChangedEventA
138159
{
139160
_discordClient.ArtworkUploadEnabled = _settings.UploadArtwork;
140161
}
141-
if (e.SettingProperty.Equals("ImgurClientId"))
162+
if (_settings.ArtworkUploader.Equals("Imgur") && e.SettingProperty.Equals("ImgurClientId"))
142163
{
143164
UpdateAssetManager(_imgurAssetCachePath, new ImgurUploader(_imgurAlbum, _settings.ImgurClientId));
144165
}
166+
if (_settings.ArtworkUploader.Equals("Amazon S3"))
167+
{
168+
if (e.SettingProperty.Equals("S3AccessKeyId") ||
169+
e.SettingProperty.Equals("S3SecretAccessKey") ||
170+
e.SettingProperty.Equals("S3Endpoint") ||
171+
e.SettingProperty.Equals("S3BucketName") ||
172+
e.SettingProperty.Equals("S3Prefix") ||
173+
e.SettingProperty.Equals("S3CustomDomain"))
174+
{
175+
UpdateAssetManager(_s3AssetCachePath, new S3Uploader(new S3Config
176+
{
177+
AccessKeyId = _settings.S3AccessKeyId,
178+
SecretAccessKey = _settings.S3SecretAccessKey,
179+
Endpoint = _settings.S3Endpoint,
180+
BucketName = _settings.S3BucketName,
181+
Prefix = _settings.S3Prefix,
182+
CustomDomain = _settings.S3CustomDomain
183+
}));
184+
}
185+
}
145186
}
146187

147188
public string GetVersionString()

DiscordBee.csproj

+16
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
<Compile Include="DiscordTools\Assets\UploaderHealthInfo.cs" />
9797
<Compile Include="DiscordTools\Assets\Uploader\CachingUploader.cs" />
9898
<Compile Include="DiscordTools\Assets\Uploader\DelegatingUploader.cs" />
99+
<Compile Include="DiscordTools\Assets\Uploader\S3Uploader.cs" />
99100
<Compile Include="DiscordTools\Assets\Uploader\ImgurUploader.cs" />
100101
<Compile Include="DiscordTools\Assets\Uploader\ResizingUploader.cs" />
101102
<Compile Include="DiscordTools\Assets\UploadResult.cs" />
@@ -106,6 +107,9 @@
106107
<Compile Include="ImgurClient\ImgurAuthenticator.cs" />
107108
<Compile Include="ImgurClient\ImgurClient.cs" />
108109
<Compile Include="ImgurClient\Types\ImgurResponse.cs" />
110+
<Compile Include="S3Client\S3Client.cs" />
111+
<Compile Include="S3Client\S3Config.cs" />
112+
<Compile Include="S3Client\Types\S3Image.cs" />
109113
<Compile Include="SortableBindingList.cs" />
110114
<Compile Include="LayoutHandler.cs" />
111115
<Compile Include="MusicBeeInterface.cs" />
@@ -118,6 +122,12 @@
118122
</Compile>
119123
<Compile Include="Properties\AssemblyInfo.cs" />
120124
<Compile Include="Settings.cs" />
125+
<Compile Include="UI\S3SettingsWindow.cs">
126+
<SubType>Form</SubType>
127+
</Compile>
128+
<Compile Include="UI\S3SettingsWindow.Designer.cs">
129+
<DependentUpon>S3SettingsWindow.cs</DependentUpon>
130+
</Compile>
121131
<Compile Include="UI\SettingsWindow.cs">
122132
<SubType>Form</SubType>
123133
</Compile>
@@ -163,6 +173,9 @@
163173
<EmbeddedResource Include="UI\PlaceholderTableWindow.resx">
164174
<DependentUpon>PlaceholderTableWindow.cs</DependentUpon>
165175
</EmbeddedResource>
176+
<EmbeddedResource Include="UI\S3SettingsWindow.resx">
177+
<DependentUpon>S3SettingsWindow.cs</DependentUpon>
178+
</EmbeddedResource>
166179
<EmbeddedResource Include="UI\SettingsWindow.resx">
167180
<DependentUpon>SettingsWindow.cs</DependentUpon>
168181
</EmbeddedResource>
@@ -171,6 +184,9 @@
171184
</EmbeddedResource>
172185
</ItemGroup>
173186
<ItemGroup>
187+
<PackageReference Include="AWSSDK.S3">
188+
<Version>3.7.305.22</Version>
189+
</PackageReference>
174190
<PackageReference Include="DiscordRichPresence">
175191
<Version>1.1.3.18</Version>
176192
</PackageReference>
+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
namespace MusicBeePlugin.DiscordTools.Assets.Uploader
2+
{
3+
using MusicBeePlugin.S3Client;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.IO;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
public class S3Uploader : IAssetUploader
12+
{
13+
private readonly S3Client _client;
14+
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
15+
16+
public S3Uploader(S3Config config)
17+
{
18+
_client = new S3Client(config);
19+
}
20+
21+
public Task<bool> DeleteAsset(AlbumCoverData assetData)
22+
{
23+
throw new NotImplementedException();
24+
}
25+
26+
public void Dispose()
27+
{
28+
_client.Dispose();
29+
}
30+
31+
public async Task<Dictionary<string, string>> GetAssets()
32+
{
33+
var ret = new Dictionary<string, string>();
34+
var images = await _client.GetAlbumImages();
35+
36+
foreach (var image in images)
37+
{
38+
var hash = Path.GetFileName(image.Key);
39+
if (string.IsNullOrEmpty(hash))
40+
{
41+
continue;
42+
}
43+
ret[hash] = image.Link;
44+
}
45+
46+
return ret;
47+
}
48+
49+
public async Task<bool> Init()
50+
{
51+
Debug.WriteLine(" ---> Waiting for semaphore");
52+
await _semaphore.WaitAsync();
53+
Debug.WriteLine(" <--- Waiting for semaphore");
54+
try
55+
{
56+
// Nothing to do.
57+
}
58+
finally
59+
{
60+
Debug.WriteLine(" ---> Releasing semaphore");
61+
_semaphore.Release();
62+
}
63+
return true;
64+
}
65+
66+
public bool IsAssetCached(AlbumCoverData assetData)
67+
{
68+
return false;
69+
}
70+
71+
public UploaderHealthInfo GetHealth()
72+
{
73+
// The S3 API does not have rate limit headers, so we assume it's always healthy
74+
var health = new UploaderHealthInfo
75+
{
76+
IsHealthy = true
77+
};
78+
return health;
79+
}
80+
81+
public async Task<UploadResult> UploadAsset(AlbumCoverData assetData)
82+
{
83+
var uploaded = await _client.UploadImage(assetData.Hash, assetData.ImageB64);
84+
return new UploadResult { Hash = assetData.Hash, Link = uploaded.Link };
85+
}
86+
}
87+
}

S3Client/S3Client.cs

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
namespace MusicBeePlugin.S3Client
2+
{
3+
using Amazon.S3;
4+
using Amazon.S3.Model;
5+
using MusicBeePlugin.S3Client.Types;
6+
using System;
7+
using System.Drawing;
8+
using System.Drawing.Imaging;
9+
using System.Linq;
10+
using System.Threading.Tasks;
11+
12+
public class S3Client : IDisposable
13+
{
14+
private readonly S3Config _config;
15+
private readonly AmazonS3Client _client;
16+
17+
public S3Client(S3Config config)
18+
{
19+
_config = config;
20+
var clientConfig = new AmazonS3Config
21+
{
22+
ServiceURL = config.Endpoint,
23+
};
24+
_client = new AmazonS3Client(config.AccessKeyId, config.SecretAccessKey, clientConfig);
25+
}
26+
27+
public async Task<S3Image[]> GetAlbumImages()
28+
{
29+
var listRequest = new ListObjectsV2Request
30+
{
31+
BucketName = _config.BucketName,
32+
Prefix = _config.Prefix,
33+
};
34+
35+
var response = await _client.ListObjectsV2Async(listRequest);
36+
return response.S3Objects.Select(obj => new S3Image
37+
{
38+
Key = obj.Key,
39+
Link = $"{GetBaseUrl()}/{obj.Key}",
40+
}).ToArray();
41+
}
42+
43+
public async Task<S3Image> UploadImage(string hash, string dataB64)
44+
{
45+
var key = $"{_config.Prefix}/{hash}";
46+
var imageStream = new System.IO.MemoryStream(Convert.FromBase64String(dataB64));
47+
48+
// Get content type of image for S3
49+
string contentType = "image/unknown";
50+
using (var image = Image.FromStream(imageStream))
51+
{
52+
Guid guid = image.RawFormat.Guid;
53+
foreach (ImageCodecInfo codec in ImageCodecInfo.GetImageDecoders())
54+
{
55+
if (codec.FormatID == guid)
56+
{
57+
contentType = codec.MimeType;
58+
break;
59+
}
60+
}
61+
}
62+
63+
var putRequest = new PutObjectRequest
64+
{
65+
BucketName = _config.BucketName,
66+
Key = $"{_config.Prefix}/{hash}",
67+
ContentType = contentType,
68+
InputStream = imageStream,
69+
CannedACL = S3CannedACL.PublicRead,
70+
};
71+
72+
var response = await _client.PutObjectAsync(putRequest);
73+
if (response.HttpStatusCode == System.Net.HttpStatusCode.OK)
74+
{
75+
return new S3Image
76+
{
77+
Key = key,
78+
Link = $"{GetBaseUrl()}/{key}",
79+
};
80+
}
81+
else
82+
{
83+
return null;
84+
}
85+
}
86+
87+
public async Task<bool> TestConnection()
88+
{
89+
// Check if an S3 request can be successfully made
90+
try
91+
{
92+
var listRequest = new ListObjectsV2Request
93+
{
94+
BucketName = _config.BucketName,
95+
Prefix = _config.Prefix,
96+
};
97+
98+
var response = await _client.ListObjectsV2Async(listRequest);
99+
return response.HttpStatusCode == System.Net.HttpStatusCode.OK;
100+
}
101+
catch
102+
{
103+
return false;
104+
}
105+
}
106+
107+
public void Dispose()
108+
{
109+
_client?.Dispose();
110+
GC.SuppressFinalize(this);
111+
}
112+
113+
private string GetBaseUrl()
114+
{
115+
return _config.CustomDomain ?? _config.Endpoint;
116+
}
117+
}
118+
}

S3Client/S3Config.cs

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
namespace MusicBeePlugin.S3Client
2+
{
3+
using System;
4+
using System.Reflection;
5+
6+
public class S3Config
7+
{
8+
public string AccessKeyId { get; set; }
9+
public string SecretAccessKey { get; set; }
10+
11+
private readonly string _endpoint;
12+
public string Endpoint
13+
{
14+
get => _endpoint;
15+
set => PrefixSchemeIfNecessary("_endpoint", value);
16+
}
17+
18+
public string BucketName { get; set; }
19+
public string Prefix { get; set; }
20+
21+
private readonly string _customDomain;
22+
public string CustomDomain {
23+
get => _customDomain;
24+
set => PrefixSchemeIfNecessary("_customDomain", value);
25+
}
26+
27+
private void PrefixSchemeIfNecessary(string fieldName, string value)
28+
{
29+
FieldInfo target = GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
30+
if (target == null)
31+
{
32+
return;
33+
}
34+
35+
if (string.IsNullOrEmpty(value))
36+
{
37+
target.SetValue(this, value);
38+
return;
39+
}
40+
41+
if (value.StartsWith($"{Uri.UriSchemeHttp}://") || value.StartsWith($"{Uri.UriSchemeHttps}://"))
42+
{
43+
target.SetValue(this, value);
44+
return;
45+
}
46+
47+
target.SetValue(this, $"{Uri.UriSchemeHttps}://{value}");
48+
}
49+
}
50+
}

S3Client/Types/S3Image.cs

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace MusicBeePlugin.S3Client.Types
2+
{
3+
public class S3Image
4+
{
5+
public string Key { get; set; }
6+
public string Link { get; set; }
7+
}
8+
}

0 commit comments

Comments
 (0)