Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add S3 uploader #244

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions DiscordBee.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace MusicBeePlugin
using MusicBeePlugin.DiscordTools;
using MusicBeePlugin.DiscordTools.Assets;
using MusicBeePlugin.DiscordTools.Assets.Uploader;
using MusicBeePlugin.S3Client;
using MusicBeePlugin.UI;
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -31,6 +32,7 @@ public partial class Plugin
private readonly Timer _updateTimer = new Timer(300);
private string _imgurAssetCachePath;
private string _imgurAlbum;
private string _s3AssetCachePath;

public Plugin()
{
Expand Down Expand Up @@ -80,13 +82,32 @@ public PluginInfo Initialise(IntPtr apiInterfacePtr)
var settingsFilePath = $"{workingDir}\\{_about.Name}.settings";
_imgurAssetCachePath = $"{workingDir}\\{_about.Name}-Imgur.cache";
_imgurAlbum = $"{workingDir}\\{_about.Name}-Imgur.album";
_s3AssetCachePath = $"{workingDir}\\{_about.Name}-S3.cache";

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

_discordClient.ArtworkUploadEnabled = _settings.UploadArtwork;
_discordClient.DiscordId = _settings.DiscordAppId;
UpdateAssetManager(_imgurAssetCachePath, new ImgurUploader(_imgurAlbum, _settings.ImgurClientId));

switch (_settings.ArtworkUploader)
{
case "Imgur":
UpdateAssetManager(_imgurAssetCachePath, new ImgurUploader(_imgurAlbum, _settings.ImgurClientId));
break;
case "Amazon S3":
UpdateAssetManager(_s3AssetCachePath, new S3Uploader(new S3Config
{
AccessKeyId = _settings.S3AccessKeyId,
SecretAccessKey = _settings.S3SecretAccessKey,
Endpoint = _settings.S3Endpoint,
BucketName = _settings.S3BucketName,
Prefix = _settings.S3Prefix,
CustomDomain = _settings.S3CustomDomain
}));
break;
}

ToolStripMenuItem mainMenuItem = (ToolStripMenuItem)_mbApiInterface.MB_AddMenuItem($"mnuTools/{_about.Name}", null, null);
mainMenuItem.DropDown.Items.Add("Uploader Health", null, ShowUploaderHealth);

Expand Down Expand Up @@ -138,10 +159,30 @@ private void SettingChangedCallback(object sender, Settings.SettingChangedEventA
{
_discordClient.ArtworkUploadEnabled = _settings.UploadArtwork;
}
if (e.SettingProperty.Equals("ImgurClientId"))
if (_settings.ArtworkUploader.Equals("Imgur") && e.SettingProperty.Equals("ImgurClientId"))
{
UpdateAssetManager(_imgurAssetCachePath, new ImgurUploader(_imgurAlbum, _settings.ImgurClientId));
}
if (_settings.ArtworkUploader.Equals("Amazon S3"))
{
if (e.SettingProperty.Equals("S3AccessKeyId") ||
e.SettingProperty.Equals("S3SecretAccessKey") ||
e.SettingProperty.Equals("S3Endpoint") ||
e.SettingProperty.Equals("S3BucketName") ||
e.SettingProperty.Equals("S3Prefix") ||
e.SettingProperty.Equals("S3CustomDomain"))
{
UpdateAssetManager(_s3AssetCachePath, new S3Uploader(new S3Config
{
AccessKeyId = _settings.S3AccessKeyId,
SecretAccessKey = _settings.S3SecretAccessKey,
Endpoint = _settings.S3Endpoint,
BucketName = _settings.S3BucketName,
Prefix = _settings.S3Prefix,
CustomDomain = _settings.S3CustomDomain
}));
}
}
}

public string GetVersionString()
Expand Down
16 changes: 16 additions & 0 deletions DiscordBee.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
<Compile Include="DiscordTools\Assets\UploaderHealthInfo.cs" />
<Compile Include="DiscordTools\Assets\Uploader\CachingUploader.cs" />
<Compile Include="DiscordTools\Assets\Uploader\DelegatingUploader.cs" />
<Compile Include="DiscordTools\Assets\Uploader\S3Uploader.cs" />
<Compile Include="DiscordTools\Assets\Uploader\ImgurUploader.cs" />
<Compile Include="DiscordTools\Assets\Uploader\ResizingUploader.cs" />
<Compile Include="DiscordTools\Assets\UploadResult.cs" />
Expand All @@ -106,6 +107,9 @@
<Compile Include="ImgurClient\ImgurAuthenticator.cs" />
<Compile Include="ImgurClient\ImgurClient.cs" />
<Compile Include="ImgurClient\Types\ImgurResponse.cs" />
<Compile Include="S3Client\S3Client.cs" />
<Compile Include="S3Client\S3Config.cs" />
<Compile Include="S3Client\Types\S3Image.cs" />
<Compile Include="SortableBindingList.cs" />
<Compile Include="LayoutHandler.cs" />
<Compile Include="MusicBeeInterface.cs" />
Expand All @@ -118,6 +122,12 @@
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Settings.cs" />
<Compile Include="UI\S3SettingsWindow.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="UI\S3SettingsWindow.Designer.cs">
<DependentUpon>S3SettingsWindow.cs</DependentUpon>
</Compile>
<Compile Include="UI\SettingsWindow.cs">
<SubType>Form</SubType>
</Compile>
Expand Down Expand Up @@ -163,6 +173,9 @@
<EmbeddedResource Include="UI\PlaceholderTableWindow.resx">
<DependentUpon>PlaceholderTableWindow.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="UI\S3SettingsWindow.resx">
<DependentUpon>S3SettingsWindow.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="UI\SettingsWindow.resx">
<DependentUpon>SettingsWindow.cs</DependentUpon>
</EmbeddedResource>
Expand All @@ -171,6 +184,9 @@
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.S3">
<Version>3.7.305.22</Version>
</PackageReference>
<PackageReference Include="DiscordRichPresence">
<Version>1.1.3.18</Version>
</PackageReference>
Expand Down
87 changes: 87 additions & 0 deletions DiscordTools/Assets/Uploader/S3Uploader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
namespace MusicBeePlugin.DiscordTools.Assets.Uploader
{
using MusicBeePlugin.S3Client;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

public class S3Uploader : IAssetUploader
{
private readonly S3Client _client;
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

public S3Uploader(S3Config config)
{
_client = new S3Client(config);
}

public Task<bool> DeleteAsset(AlbumCoverData assetData)
{
throw new NotImplementedException();
}

public void Dispose()
{
_client.Dispose();
}

public async Task<Dictionary<string, string>> GetAssets()
{
var ret = new Dictionary<string, string>();
var images = await _client.GetAlbumImages();

foreach (var image in images)
{
var hash = Path.GetFileName(image.Key);
if (string.IsNullOrEmpty(hash))
{
continue;
}
ret[hash] = image.Link;
}

return ret;
}

public async Task<bool> Init()
{
Debug.WriteLine(" ---> Waiting for semaphore");
await _semaphore.WaitAsync();
Debug.WriteLine(" <--- Waiting for semaphore");
try
{
// Nothing to do.
}
finally
{
Debug.WriteLine(" ---> Releasing semaphore");
_semaphore.Release();
}
return true;
}

public bool IsAssetCached(AlbumCoverData assetData)
{
return false;
}

public UploaderHealthInfo GetHealth()
{
// The S3 API does not have rate limit headers, so we assume it's always healthy
var health = new UploaderHealthInfo
{
IsHealthy = true
};
return health;
}

public async Task<UploadResult> UploadAsset(AlbumCoverData assetData)
{
var uploaded = await _client.UploadImage(assetData.Hash, assetData.ImageB64);
return new UploadResult { Hash = assetData.Hash, Link = uploaded.Link };
}
}
}
135 changes: 135 additions & 0 deletions S3Client/S3Client.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
namespace MusicBeePlugin.S3Client
{
using Amazon.S3;
using Amazon.S3.Model;
using MusicBeePlugin.S3Client.Types;
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Threading.Tasks;

public class S3Client : IDisposable
{
private readonly S3Config _config;
private readonly AmazonS3Client _client;
private readonly string _baseUrl;

public S3Client(S3Config config)
{
_config = config;
var clientConfig = new AmazonS3Config
{
ServiceURL = config.Endpoint,
};
_client = new AmazonS3Client(config.AccessKeyId, config.SecretAccessKey, clientConfig);
_baseUrl = GetBaseUrl();
}

public async Task<S3Image[]> GetAlbumImages()
{
var listRequest = new ListObjectsV2Request
{
BucketName = _config.BucketName,
Prefix = _config.Prefix,
};

var response = await _client.ListObjectsV2Async(listRequest);
return response.S3Objects.Select(obj => new S3Image
{
Key = obj.Key,
Link = $"{_baseUrl}{obj.Key}",
}).ToArray();
}

public async Task<S3Image> UploadImage(string hash, string dataB64)
{
var key = $"{_config.Prefix}/{hash}";
var imageStream = new System.IO.MemoryStream(Convert.FromBase64String(dataB64));

// Get content type of image for S3
string contentType = "image/unknown";
using (var image = Image.FromStream(imageStream))
{
Guid guid = image.RawFormat.Guid;
foreach (ImageCodecInfo codec in ImageCodecInfo.GetImageDecoders())
{
if (codec.FormatID == guid)
{
contentType = codec.MimeType;
break;
}
}
}

var putRequest = new PutObjectRequest
{
BucketName = _config.BucketName,
Key = $"{_config.Prefix}/{hash}",
ContentType = contentType,
InputStream = imageStream,
CannedACL = S3CannedACL.PublicRead,
};

var response = await _client.PutObjectAsync(putRequest);
if (response.HttpStatusCode == System.Net.HttpStatusCode.OK)
{
return new S3Image
{
Key = key,
Link = $"{_baseUrl}{key}",
};
} else
{
return null;
}
}

public async Task<bool> TestConnection()
{
// Check if an S3 request can be successfully made
try
{
var listRequest = new ListObjectsV2Request
{
BucketName = _config.BucketName,
Prefix = _config.Prefix,
};

var response = await _client.ListObjectsV2Async(listRequest);
return response.HttpStatusCode == System.Net.HttpStatusCode.OK;
}
catch
{
return false;
}
}

public void Dispose()
{
_client?.Dispose();
GC.SuppressFinalize(this);
}

private string GetBaseUrl()
{
if (!string.IsNullOrEmpty(_config.CustomDomain))
{
return _config.CustomDomain;
}

// Fetch the bucket region
var regionResponse = _client.GetBucketLocationAsync(new GetBucketLocationRequest
{
BucketName = _config.BucketName
}).GetAwaiter().GetResult();

var region = regionResponse.Location?.Value ?? "us-east-1"; // Default to us-east-1 if region is null

// Prefix the bucket name to the endpoint's host
var endpoint = new UriBuilder(_config.Endpoint);
endpoint.Host = $"{_config.BucketName}.{endpoint.Host}";
return endpoint.Uri.ToString();
}
}
}
Loading