1058 lines
38 KiB
C#
1058 lines
38 KiB
C#
// (c) Copyright Ascensio System SIA 2010-2022
|
|
//
|
|
// This program is a free software product.
|
|
// You can redistribute it and/or modify it under the terms
|
|
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
|
|
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
|
|
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
|
|
// any third-party rights.
|
|
//
|
|
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
|
|
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
|
|
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
|
|
//
|
|
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
|
|
//
|
|
// The interactive user interfaces in modified source and object code versions of the Program must
|
|
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
|
|
//
|
|
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
|
|
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
|
|
// trademark law for use of our trademarks.
|
|
//
|
|
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
|
|
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
|
|
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
|
|
|
|
namespace ASC.Web.Core.Users;
|
|
|
|
[Transient]
|
|
public sealed class ResizeWorkerItem : DistributedTask
|
|
{
|
|
public ResizeWorkerItem()
|
|
{
|
|
|
|
}
|
|
|
|
public ResizeWorkerItem(int tenantId, Guid userId, byte[] data, long maxFileSize, Size size, IDataStore dataStore, UserPhotoThumbnailSettings settings) : base()
|
|
{
|
|
TenantId = tenantId;
|
|
UserId = userId;
|
|
Data = data;
|
|
MaxFileSize = maxFileSize;
|
|
Size = size;
|
|
DataStore = dataStore;
|
|
Settings = settings;
|
|
}
|
|
|
|
public Size Size { get; }
|
|
|
|
public IDataStore DataStore { get; }
|
|
|
|
public long MaxFileSize { get; }
|
|
|
|
public byte[] Data { get; }
|
|
public int TenantId { get; }
|
|
public Guid UserId { get; }
|
|
|
|
public UserPhotoThumbnailSettings Settings { get; }
|
|
|
|
public override bool Equals(object obj)
|
|
{
|
|
if (obj is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ReferenceEquals(this, obj))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (obj is not ResizeWorkerItem)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return Equals((ResizeWorkerItem)obj);
|
|
}
|
|
|
|
public bool Equals(ResizeWorkerItem other)
|
|
{
|
|
if (other is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ReferenceEquals(this, other))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return other.UserId.Equals(UserId) && other.MaxFileSize == MaxFileSize && other.Size.Equals(Size);
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return HashCode.Combine(UserId, MaxFileSize, Size);
|
|
}
|
|
}
|
|
|
|
[Singletone]
|
|
public class UserPhotoManagerCache
|
|
{
|
|
private readonly ConcurrentDictionary<Guid, ConcurrentDictionary<CacheSize, string>> _photofiles;
|
|
private readonly ICacheNotify<UserPhotoManagerCacheItem> _cacheNotify;
|
|
private readonly HashSet<int> _tenantDiskCache;
|
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
|
|
|
public UserPhotoManagerCache(ICacheNotify<UserPhotoManagerCacheItem> notify, IServiceScopeFactory serviceScopeFactory)
|
|
{
|
|
try
|
|
{
|
|
_photofiles = new ConcurrentDictionary<Guid, ConcurrentDictionary<CacheSize, string>>();
|
|
_tenantDiskCache = new HashSet<int>();
|
|
_cacheNotify = notify;
|
|
|
|
_cacheNotify.Subscribe((data) =>
|
|
{
|
|
_photofiles.GetOrAdd(new Guid(data.UserId), (r) => new ConcurrentDictionary<CacheSize, string>())
|
|
.AddOrUpdate(data.Size, data.FileName, (key, oldValue) => data.FileName);
|
|
}, CacheNotifyAction.InsertOrUpdate);
|
|
|
|
_cacheNotify.Subscribe((data) =>
|
|
{
|
|
ConcurrentDictionary<CacheSize, string> removedValue;
|
|
|
|
if (_photofiles.TryRemove(new Guid(data.UserId), out removedValue))
|
|
{
|
|
using var scope = _serviceScopeFactory.CreateScope();
|
|
scope.ServiceProvider.GetRequiredService<TenantManager>().SetCurrentTenant(data.TenantId);
|
|
var storageFactory = scope.ServiceProvider.GetRequiredService<StorageFactory>();
|
|
|
|
var storage = storageFactory.GetStorage(data.TenantId.ToString(), "userPhotos");
|
|
|
|
try
|
|
{
|
|
storage.DeleteFilesAsync("", data.UserId + "*.*", false).Wait();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
scope.ServiceProvider.GetRequiredService<ILogger<UserPhotoManagerCache>>().ErrorWithException("Trying remove and add photos in same time", ex);
|
|
}
|
|
|
|
|
|
SetCacheLoadedForTenant(false, data.TenantId);
|
|
}
|
|
}, CacheNotifyAction.Remove);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
|
|
}
|
|
_serviceScopeFactory = serviceScopeFactory;
|
|
}
|
|
|
|
public bool IsCacheLoadedForTenant(int tenantId)
|
|
{
|
|
return _tenantDiskCache.Contains(tenantId);
|
|
}
|
|
|
|
public bool SetCacheLoadedForTenant(bool isLoaded, int tenantId)
|
|
{
|
|
return isLoaded ? _tenantDiskCache.Add(tenantId) : _tenantDiskCache.Remove(tenantId);
|
|
}
|
|
|
|
public void ClearCache(Guid userID, int tenantId)
|
|
{
|
|
if (_cacheNotify != null)
|
|
{
|
|
_cacheNotify.Publish(new UserPhotoManagerCacheItem { UserId = userID.ToString(), TenantId = tenantId }, CacheNotifyAction.Remove);
|
|
}
|
|
}
|
|
|
|
public void AddToCache(Guid userID, Size size, string fileName, int tenantId)
|
|
{
|
|
if (_cacheNotify != null)
|
|
{
|
|
_cacheNotify.Publish(new UserPhotoManagerCacheItem { UserId = userID.ToString(), Size = UserPhotoManager.ToCache(size), FileName = fileName, TenantId = tenantId }, CacheNotifyAction.InsertOrUpdate);
|
|
}
|
|
}
|
|
|
|
public string SearchInCache(Guid userId, Size size)
|
|
{
|
|
string fileName = null;
|
|
|
|
if (!_photofiles.TryGetValue(userId, out var val))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (size != Size.Empty && !val.TryGetValue(UserPhotoManager.ToCache(size), out fileName))
|
|
{
|
|
return null;
|
|
}
|
|
else
|
|
{
|
|
fileName = val.Values.FirstOrDefault(x => !string.IsNullOrEmpty(x) && x.Contains("_orig_"));
|
|
}
|
|
|
|
return fileName;
|
|
}
|
|
}
|
|
|
|
[Scope(Additional = typeof(ResizeWorkerItemExtension))]
|
|
public class UserPhotoManager
|
|
{
|
|
public const string CUSTOM_DISTRIBUTED_TASK_QUEUE_NAME = "user_photo_manager";
|
|
|
|
//Regex for parsing filenames into groups with id's
|
|
private static readonly Regex _parseFile =
|
|
new Regex(@"^(?'module'\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1}){0,1}" +
|
|
@"(?'user'\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1}){1}" +
|
|
@"_(?'kind'orig|size){1}_(?'size'(?'width'[0-9]{1,5})-{1}(?'height'[0-9]{1,5})){0,1}\..*", RegexOptions.Compiled);
|
|
|
|
private readonly UserManager _userManager;
|
|
private readonly WebImageSupplier _webImageSupplier;
|
|
private readonly TenantManager _tenantManager;
|
|
private readonly StorageFactory _storageFactory;
|
|
private readonly UserPhotoManagerCache _userPhotoManagerCache;
|
|
private readonly SettingsManager _settingsManager;
|
|
private readonly ILogger<UserPhotoManager> _log;
|
|
|
|
private Tenant _tenant;
|
|
public Tenant Tenant { get { return _tenant ??= _tenantManager.GetCurrentTenant(); } }
|
|
|
|
//note: using auto stop queue
|
|
private readonly DistributedTaskQueue _resizeQueue;//TODO: configure
|
|
|
|
public UserPhotoManager(
|
|
UserManager userManager,
|
|
WebImageSupplier webImageSupplier,
|
|
TenantManager tenantManager,
|
|
StorageFactory storageFactory,
|
|
UserPhotoManagerCache userPhotoManagerCache,
|
|
ILogger<UserPhotoManager> logger,
|
|
IDistributedTaskQueueFactory queueFactory,
|
|
SettingsManager settingsManager)
|
|
{
|
|
_resizeQueue = queueFactory.CreateQueue(CUSTOM_DISTRIBUTED_TASK_QUEUE_NAME);
|
|
_userManager = userManager;
|
|
_webImageSupplier = webImageSupplier;
|
|
_tenantManager = tenantManager;
|
|
_storageFactory = storageFactory;
|
|
_userPhotoManagerCache = userPhotoManagerCache;
|
|
_settingsManager = settingsManager;
|
|
_log = logger;
|
|
}
|
|
|
|
private string _defaultAbsoluteWebPath;
|
|
public string GetDefaultPhotoAbsoluteWebPath()
|
|
{
|
|
return _defaultAbsoluteWebPath ??= _webImageSupplier.GetAbsoluteWebPath(_defaultAvatar);
|
|
}
|
|
|
|
public async Task<string> GetRetinaPhotoURL(Guid userID)
|
|
{
|
|
return await GetSizedPhotoAbsoluteWebPath(userID, RetinaFotoSize);
|
|
}
|
|
|
|
public async Task<string> GetMaxPhotoURL(Guid userID)
|
|
{
|
|
return await GetSizedPhotoAbsoluteWebPath(userID, MaxFotoSize);
|
|
}
|
|
|
|
public async Task<string> GetBigPhotoURL(Guid userID)
|
|
{
|
|
return await GetSizedPhotoAbsoluteWebPath(userID, BigFotoSize);
|
|
}
|
|
|
|
public async Task<string> GetMediumPhotoURL(Guid userID)
|
|
{
|
|
return await GetSizedPhotoAbsoluteWebPath(userID, MediumFotoSize);
|
|
}
|
|
|
|
public async Task<string> GetSmallPhotoURL(Guid userID)
|
|
{
|
|
return await GetSizedPhotoAbsoluteWebPath(userID, SmallFotoSize);
|
|
}
|
|
|
|
|
|
public async Task<string> GetSizedPhotoUrl(Guid userId, int width, int height)
|
|
{
|
|
return await GetSizedPhotoAbsoluteWebPath(userId, new Size(width, height));
|
|
}
|
|
|
|
|
|
private string _defaultSmallPhotoURL;
|
|
public string GetDefaultSmallPhotoURL()
|
|
{
|
|
return _defaultSmallPhotoURL ??= GetDefaultPhotoAbsoluteWebPath(SmallFotoSize);
|
|
}
|
|
|
|
private string _defaultMediumPhotoURL;
|
|
public string GetDefaultMediumPhotoURL()
|
|
{
|
|
return _defaultMediumPhotoURL ??= GetDefaultPhotoAbsoluteWebPath(MediumFotoSize);
|
|
}
|
|
|
|
private string _defaultBigPhotoURL;
|
|
public string GetDefaultBigPhotoURL()
|
|
{
|
|
return _defaultBigPhotoURL ??= GetDefaultPhotoAbsoluteWebPath(BigFotoSize);
|
|
}
|
|
|
|
private string _defaultMaxPhotoURL;
|
|
public string GetDefaultMaxPhotoURL()
|
|
{
|
|
return _defaultMaxPhotoURL ??= GetDefaultPhotoAbsoluteWebPath(MaxFotoSize);
|
|
}
|
|
|
|
private string _defaultRetinaPhotoURL;
|
|
public string GetDefaultRetinaPhotoURL()
|
|
{
|
|
return _defaultRetinaPhotoURL ??= GetDefaultPhotoAbsoluteWebPath(RetinaFotoSize);
|
|
}
|
|
|
|
public static Size OriginalFotoSize { get; } = new Size(1280, 1280);
|
|
|
|
public static Size RetinaFotoSize { get; } = new Size(360, 360);
|
|
|
|
public static Size MaxFotoSize { get; } = new Size(200, 200);
|
|
|
|
public static Size BigFotoSize { get; } = new Size(82, 82);
|
|
|
|
public static Size MediumFotoSize { get; } = new Size(48, 48);
|
|
|
|
public static Size SmallFotoSize { get; } = new Size(32, 32);
|
|
|
|
private static readonly string _defaultRetinaAvatar = "default_user_photo_size_360-360.png";
|
|
private static readonly string _defaultAvatar = "default_user_photo_size_200-200.png";
|
|
private static readonly string _defaultSmallAvatar = "default_user_photo_size_32-32.png";
|
|
private static readonly string _defaultMediumAvatar = "default_user_photo_size_48-48.png";
|
|
private static readonly string _defaultBigAvatar = "default_user_photo_size_82-82.png";
|
|
private static readonly string _tempDomainName = "temp";
|
|
|
|
|
|
public async Task<bool> UserHasAvatar(Guid userID)
|
|
{
|
|
var path = await GetPhotoAbsoluteWebPath(userID);
|
|
var fileName = Path.GetFileName(path);
|
|
return fileName != _defaultAvatar;
|
|
}
|
|
|
|
public async Task<string> GetPhotoAbsoluteWebPath(Guid userID)
|
|
{
|
|
var path = SearchInCache(userID, Size.Empty);
|
|
if (!string.IsNullOrEmpty(path))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
try
|
|
{
|
|
var data = _userManager.GetUserPhoto(userID);
|
|
string photoUrl;
|
|
string fileName;
|
|
if (data == null || data.Length == 0)
|
|
{
|
|
photoUrl = GetDefaultPhotoAbsoluteWebPath();
|
|
fileName = "default";
|
|
}
|
|
else
|
|
{
|
|
(photoUrl, fileName) = await SaveOrUpdatePhoto(userID, data, -1, new Size(-1, -1), false);
|
|
}
|
|
|
|
_userPhotoManagerCache.AddToCache(userID, Size.Empty, fileName, _tenant.Id);
|
|
|
|
return photoUrl;
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
return GetDefaultPhotoAbsoluteWebPath();
|
|
}
|
|
|
|
internal async Task<Size> GetPhotoSize(Guid userID)
|
|
{
|
|
var virtualPath = await GetPhotoAbsoluteWebPath(userID);
|
|
if (virtualPath == null)
|
|
{
|
|
return Size.Empty;
|
|
}
|
|
|
|
try
|
|
{
|
|
var sizePart = virtualPath.Substring(virtualPath.LastIndexOf('_'));
|
|
sizePart = sizePart.Trim('_');
|
|
sizePart = sizePart.Remove(sizePart.LastIndexOf('.'));
|
|
return new Size(int.Parse(sizePart.Split('-')[0]), int.Parse(sizePart.Split('-')[1]));
|
|
}
|
|
catch
|
|
{
|
|
return Size.Empty;
|
|
}
|
|
}
|
|
|
|
private async Task<string> GetSizedPhotoAbsoluteWebPath(Guid userID, Size size)
|
|
{
|
|
var res = SearchInCache(userID, size);
|
|
if (!string.IsNullOrEmpty(res))
|
|
{
|
|
return res;
|
|
}
|
|
|
|
try
|
|
{
|
|
var data = _userManager.GetUserPhoto(userID);
|
|
|
|
if (data == null || data.Length == 0)
|
|
{
|
|
//empty photo. cache default
|
|
var photoUrl = GetDefaultPhotoAbsoluteWebPath(size);
|
|
|
|
_userPhotoManagerCache.AddToCache(userID, size, "default", _tenant.Id);
|
|
return photoUrl;
|
|
}
|
|
|
|
//Enqueue for sizing
|
|
await SizePhoto(userID, data, -1, size);
|
|
}
|
|
catch { }
|
|
|
|
return GetDefaultPhotoAbsoluteWebPath(size);
|
|
}
|
|
|
|
private string GetDefaultPhotoAbsoluteWebPath(Size size)
|
|
{
|
|
return size switch
|
|
{
|
|
Size(var w, var h) when w == RetinaFotoSize.Width && h == RetinaFotoSize.Height => _webImageSupplier.GetAbsoluteWebPath(_defaultRetinaAvatar),
|
|
Size(var w, var h) when w == MaxFotoSize.Width && h == MaxFotoSize.Height => _webImageSupplier.GetAbsoluteWebPath(_defaultAvatar),
|
|
Size(var w, var h) when w == BigFotoSize.Width && h == BigFotoSize.Height => _webImageSupplier.GetAbsoluteWebPath(_defaultBigAvatar),
|
|
Size(var w, var h) when w == SmallFotoSize.Width && h == SmallFotoSize.Height => _webImageSupplier.GetAbsoluteWebPath(_defaultSmallAvatar),
|
|
Size(var w, var h) when w == MediumFotoSize.Width && h == MediumFotoSize.Height => _webImageSupplier.GetAbsoluteWebPath(_defaultMediumAvatar),
|
|
_ => GetDefaultPhotoAbsoluteWebPath()
|
|
};
|
|
}
|
|
|
|
private static readonly HashSet<int> _tenantDiskCache = new HashSet<int>();
|
|
private static readonly object _diskCacheLoaderLock = new object();
|
|
|
|
|
|
private string SearchInCache(Guid userId, Size size)
|
|
{
|
|
if (!_userPhotoManagerCache.IsCacheLoadedForTenant(Tenant.Id))
|
|
{
|
|
LoadDiskCache();
|
|
}
|
|
|
|
var fileName = _userPhotoManagerCache.SearchInCache(userId, size);
|
|
|
|
if (fileName != null && fileName.StartsWith("default"))
|
|
{
|
|
return GetDefaultPhotoAbsoluteWebPath(size);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(fileName))
|
|
{
|
|
var store = GetDataStore();
|
|
return store.GetUriAsync(fileName).Result.ToString();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void LoadDiskCache()
|
|
{
|
|
lock (_diskCacheLoaderLock)
|
|
{
|
|
if (!_userPhotoManagerCache.IsCacheLoadedForTenant(Tenant.Id))
|
|
{
|
|
try
|
|
{
|
|
var listFileNames = GetDataStore().ListFilesRelativeAsync("", "", "*.*", false).ToArrayAsync().Result;
|
|
foreach (var fileName in listFileNames)
|
|
{
|
|
//Try parse fileName
|
|
if (fileName != null)
|
|
{
|
|
var match = _parseFile.Match(fileName);
|
|
if (match.Success && match.Groups["user"].Success)
|
|
{
|
|
var parsedUserId = new Guid(match.Groups["user"].Value);
|
|
var size = Size.Empty;
|
|
if (match.Groups["width"].Success && match.Groups["height"].Success)
|
|
{
|
|
//Parse size
|
|
size = new Size(int.Parse(match.Groups["width"].Value), int.Parse(match.Groups["height"].Value));
|
|
}
|
|
_userPhotoManagerCache.AddToCache(parsedUserId, size, fileName, _tenant.Id);
|
|
}
|
|
}
|
|
}
|
|
_userPhotoManagerCache.SetCacheLoadedForTenant(true, Tenant.Id);
|
|
}
|
|
catch (Exception err)
|
|
{
|
|
_log.ErrorLoadDiskCache(err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
public void ResetThumbnailSettings(Guid userId)
|
|
{
|
|
var thumbSettings = _settingsManager.GetDefault<UserPhotoThumbnailSettings>();
|
|
_settingsManager.SaveForUser(thumbSettings, userId);
|
|
}
|
|
|
|
public async Task<(string, string)> SaveOrUpdatePhoto(Guid userID, byte[] data)
|
|
{
|
|
return await SaveOrUpdatePhoto(userID, data, -1, OriginalFotoSize, true);
|
|
}
|
|
|
|
public void RemovePhoto(Guid idUser)
|
|
{
|
|
_userManager.SaveUserPhoto(idUser, null);
|
|
try
|
|
{
|
|
var storage = GetDataStore();
|
|
storage.DeleteFilesAsync("", idUser + "*.*", false).Wait();
|
|
}
|
|
catch (DirectoryNotFoundException e)
|
|
{
|
|
_log.ErrorRemovePhoto(e);
|
|
}
|
|
|
|
_userManager.SaveUserPhoto(idUser, null);
|
|
_userPhotoManagerCache.ClearCache(idUser, _tenant.Id);
|
|
}
|
|
|
|
public void SyncPhoto(Guid userID, byte[] data)
|
|
{
|
|
data = TryParseImage(data, -1, OriginalFotoSize, out _, out var width, out var height);
|
|
_userManager.SaveUserPhoto(userID, data);
|
|
SetUserPhotoThumbnailSettings(userID, width, height);
|
|
_userPhotoManagerCache.ClearCache(userID, _tenant.Id);
|
|
}
|
|
|
|
|
|
private async Task<(string, string)> SaveOrUpdatePhoto(Guid userID, byte[] data, long maxFileSize, Size size, bool saveInCoreContext)
|
|
{
|
|
data = TryParseImage(data, maxFileSize, size, out var imgFormat, out var width, out var height);
|
|
|
|
var widening = CommonPhotoManager.GetImgFormatName(imgFormat);
|
|
var fileName = string.Format("{0}_orig_{1}-{2}.{3}", userID, width, height, widening);
|
|
|
|
if (saveInCoreContext)
|
|
{
|
|
_userManager.SaveUserPhoto(userID, data);
|
|
SetUserPhotoThumbnailSettings(userID, width, height);
|
|
_userPhotoManagerCache.ClearCache(userID, _tenant.Id);
|
|
|
|
}
|
|
|
|
var store = GetDataStore();
|
|
|
|
var photoUrl = GetDefaultPhotoAbsoluteWebPath();
|
|
if (data != null && data.Length > 0)
|
|
{
|
|
using (var stream = new MemoryStream(data))
|
|
{
|
|
photoUrl = store.SaveAsync(fileName, stream).Result.ToString();
|
|
}
|
|
//Queue resizing
|
|
var t1 = SizePhoto(userID, data, -1, SmallFotoSize, true);
|
|
var t2 = SizePhoto(userID, data, -1, MediumFotoSize, true);
|
|
var t3 = SizePhoto(userID, data, -1, BigFotoSize, true);
|
|
var t4 = SizePhoto(userID, data, -1, MaxFotoSize, true);
|
|
var t5 = SizePhoto(userID, data, -1, RetinaFotoSize, true);
|
|
|
|
await Task.WhenAll(t1, t2, t3, t4, t5);
|
|
}
|
|
return (photoUrl, fileName);
|
|
}
|
|
|
|
private void SetUserPhotoThumbnailSettings(Guid userId, int width, int height)
|
|
{
|
|
var settings = _settingsManager.LoadForUser<UserPhotoThumbnailSettings>(userId);
|
|
|
|
if (!settings.IsDefault)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var max = Math.Max(Math.Max(width, height), SmallFotoSize.Width);
|
|
var min = Math.Max(Math.Min(width, height), SmallFotoSize.Width);
|
|
|
|
var pos = (max - min) / 2;
|
|
|
|
settings = new UserPhotoThumbnailSettings(
|
|
width >= height ? new Point(pos, 0) : new Point(0, pos),
|
|
new Size(min, min));
|
|
|
|
_settingsManager.SaveForUser(settings, userId);
|
|
}
|
|
|
|
private byte[] TryParseImage(byte[] data, long maxFileSize, Size maxsize, out IImageFormat imgFormat, out int width, out int height)
|
|
{
|
|
if (data == null || data.Length <= 0)
|
|
{
|
|
throw new UnknownImageFormatException();
|
|
}
|
|
|
|
if (maxFileSize != -1 && data.Length > maxFileSize)
|
|
{
|
|
throw new ImageSizeLimitException();
|
|
}
|
|
|
|
//data = ImageHelper.RotateImageByExifOrientationData(data, Log);
|
|
|
|
try
|
|
{
|
|
using var img = Image.Load(data, out var format);
|
|
imgFormat = format;
|
|
width = img.Width;
|
|
height = img.Height;
|
|
var maxWidth = maxsize.Width;
|
|
var maxHeight = maxsize.Height;
|
|
|
|
if ((maxHeight != -1 && img.Height > maxHeight) || (maxWidth != -1 && img.Width > maxWidth))
|
|
{
|
|
#region calulate height and width
|
|
|
|
if (width > maxWidth && height > maxHeight)
|
|
{
|
|
|
|
if (width > height)
|
|
{
|
|
height = (int)(height * (double)maxWidth / width + 0.5);
|
|
width = maxWidth;
|
|
}
|
|
else
|
|
{
|
|
width = (int)(width * (double)maxHeight / height + 0.5);
|
|
height = maxHeight;
|
|
}
|
|
}
|
|
|
|
if (width > maxWidth && height <= maxHeight)
|
|
{
|
|
height = (int)(height * (double)maxWidth / width + 0.5);
|
|
width = maxWidth;
|
|
}
|
|
|
|
if (width <= maxWidth && height > maxHeight)
|
|
{
|
|
width = (int)(width * (double)maxHeight / height + 0.5);
|
|
height = maxHeight;
|
|
}
|
|
|
|
var tmpW = width;
|
|
var tmpH = height;
|
|
#endregion
|
|
using var destRound = img.Clone(x => x.Resize(new ResizeOptions
|
|
{
|
|
Size = new Size(tmpW, tmpH),
|
|
Mode = ResizeMode.Stretch
|
|
}));
|
|
|
|
data = CommonPhotoManager.SaveToBytes(destRound);
|
|
}
|
|
return data;
|
|
}
|
|
catch (OutOfMemoryException)
|
|
{
|
|
throw new ImageSizeLimitException();
|
|
}
|
|
catch (ArgumentException error)
|
|
{
|
|
throw new UnknownImageFormatException(error);
|
|
}
|
|
}
|
|
|
|
private async Task<string> SizePhoto(Guid userID, byte[] data, long maxFileSize, Size size)
|
|
{
|
|
return await SizePhoto(userID, data, maxFileSize, size, false);
|
|
}
|
|
|
|
private async Task<string> SizePhoto(Guid userID, byte[] data, long maxFileSize, Size size, bool now)
|
|
{
|
|
if (data == null || data.Length <= 0)
|
|
{
|
|
throw new UnknownImageFormatException();
|
|
}
|
|
|
|
if (maxFileSize != -1 && data.Length > maxFileSize)
|
|
{
|
|
throw new ImageWeightLimitException();
|
|
}
|
|
|
|
var resizeTask = new ResizeWorkerItem(_tenantManager.GetCurrentTenant().Id, userID, data, maxFileSize, size, GetDataStore(), _settingsManager.LoadForUser<UserPhotoThumbnailSettings>(userID));
|
|
var key = $"{userID}{size}";
|
|
resizeTask["key"] = key;
|
|
|
|
if (now)
|
|
{
|
|
//Resize synchronously
|
|
await ResizeImage(resizeTask);
|
|
return await GetSizedPhotoAbsoluteWebPath(userID, size);
|
|
}
|
|
else
|
|
{
|
|
if (!_resizeQueue.GetAllTasks<ResizeWorkerItem>().Any(r => r["key"] == key))
|
|
{
|
|
//Add
|
|
_resizeQueue.EnqueueTask(async (a, b) => await ResizeImage(resizeTask), resizeTask);
|
|
}
|
|
return GetDefaultPhotoAbsoluteWebPath(size);
|
|
//NOTE: return default photo here. Since task will update cache
|
|
}
|
|
}
|
|
|
|
private async Task ResizeImage(ResizeWorkerItem item)
|
|
{
|
|
try
|
|
{
|
|
_tenantManager.SetCurrentTenant(item.TenantId);
|
|
|
|
var data = item.Data;
|
|
using var stream = new MemoryStream(data);
|
|
using var img = Image.Load(stream, out var format);
|
|
var imgFormat = format;
|
|
if (item.Size != img.Size())
|
|
{
|
|
using var img2 = item.Settings.IsDefault ?
|
|
CommonPhotoManager.DoThumbnail(img, item.Size, true, true, true) :
|
|
UserPhotoThumbnailManager.GetImage(img, item.Size, item.Settings);
|
|
data = CommonPhotoManager.SaveToBytes(img2);
|
|
}
|
|
else
|
|
{
|
|
data = CommonPhotoManager.SaveToBytes(img);
|
|
}
|
|
|
|
var widening = CommonPhotoManager.GetImgFormatName(imgFormat);
|
|
var fileName = string.Format("{0}_size_{1}-{2}.{3}", item.UserId, item.Size.Width, item.Size.Height, widening);
|
|
|
|
using var stream2 = new MemoryStream(data);
|
|
await item.DataStore.SaveAsync(fileName, stream2);
|
|
|
|
_userPhotoManagerCache.AddToCache(item.UserId, item.Size, fileName, _tenant.Id);
|
|
}
|
|
catch (ArgumentException error)
|
|
{
|
|
throw new UnknownImageFormatException(error);
|
|
}
|
|
}
|
|
|
|
public string GetTempPhotoAbsoluteWebPath(string fileName)
|
|
{
|
|
return GetDataStore().GetUriAsync(_tempDomainName, fileName).Result.ToString();
|
|
}
|
|
|
|
public string SaveTempPhoto(byte[] data, long maxFileSize, int maxWidth, int maxHeight)
|
|
{
|
|
data = TryParseImage(data, maxFileSize, new Size(maxWidth, maxHeight), out var imgFormat, out _, out _);
|
|
|
|
var fileName = Guid.NewGuid() + "." + CommonPhotoManager.GetImgFormatName(imgFormat);
|
|
|
|
var store = GetDataStore();
|
|
using var stream = new MemoryStream(data);
|
|
return store.SaveAsync(_tempDomainName, fileName, stream).Result.ToString();
|
|
}
|
|
|
|
public byte[] GetTempPhotoData(string fileName)
|
|
{
|
|
using var s = GetDataStore().GetReadStreamAsync(_tempDomainName, fileName).Result;
|
|
var data = new MemoryStream();
|
|
var buffer = new byte[1024 * 10];
|
|
while (true)
|
|
{
|
|
var count = s.Read(buffer, 0, buffer.Length);
|
|
if (count == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
data.Write(buffer, 0, count);
|
|
}
|
|
return data.ToArray();
|
|
}
|
|
|
|
public string GetSizedTempPhotoAbsoluteWebPath(string fileName, int newWidth, int newHeight)
|
|
{
|
|
var store = GetDataStore();
|
|
if (store.IsFileAsync(_tempDomainName, fileName).Result)
|
|
{
|
|
using var s = store.GetReadStreamAsync(_tempDomainName, fileName).Result;
|
|
using var img = Image.Load(s, out var format);
|
|
var imgFormat = format;
|
|
byte[] data;
|
|
|
|
if (img.Width != newWidth || img.Height != newHeight)
|
|
{
|
|
using var img2 = CommonPhotoManager.DoThumbnail(img, new Size(newWidth, newHeight), true, true, true);
|
|
data = CommonPhotoManager.SaveToBytes(img2);
|
|
}
|
|
else
|
|
{
|
|
data = CommonPhotoManager.SaveToBytes(img);
|
|
}
|
|
var widening = CommonPhotoManager.GetImgFormatName(imgFormat);
|
|
var index = fileName.LastIndexOf('.');
|
|
var fileNameWithoutExt = (index != -1) ? fileName.Substring(0, index) : fileName;
|
|
|
|
var trueFileName = fileNameWithoutExt + "_size_" + newWidth.ToString() + "-" + newHeight.ToString() + "." + widening;
|
|
using var stream = new MemoryStream(data);
|
|
return store.SaveAsync(_tempDomainName, trueFileName, stream).Result.ToString();
|
|
}
|
|
return GetDefaultPhotoAbsoluteWebPath(new Size(newWidth, newHeight));
|
|
}
|
|
|
|
public void RemoveTempPhoto(string fileName)
|
|
{
|
|
var index = fileName.LastIndexOf('.');
|
|
var fileNameWithoutExt = (index != -1) ? fileName.Substring(0, index) : fileName;
|
|
try
|
|
{
|
|
var store = GetDataStore();
|
|
store.DeleteFilesAsync(_tempDomainName, "", fileNameWithoutExt + "*.*", false).Wait();
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
|
|
public Image GetPhotoImage(Guid userID, out IImageFormat format)
|
|
{
|
|
try
|
|
{
|
|
var data = _userManager.GetUserPhoto(userID);
|
|
if (data != null)
|
|
{
|
|
var img = Image.Load(data, out var imgFormat);
|
|
format = imgFormat;
|
|
return img;
|
|
}
|
|
}
|
|
catch { }
|
|
format = null;
|
|
return null;
|
|
}
|
|
|
|
public string SaveThumbnail(Guid userID, Image img, IImageFormat format)
|
|
{
|
|
var moduleID = Guid.Empty;
|
|
var widening = CommonPhotoManager.GetImgFormatName(format);
|
|
var size = img.Size();
|
|
var fileName = string.Format("{0}{1}_size_{2}-{3}.{4}", moduleID == Guid.Empty ? "" : moduleID.ToString(), userID, img.Width, img.Height, widening);
|
|
|
|
var store = GetDataStore();
|
|
string photoUrl;
|
|
using (var s = new MemoryStream(CommonPhotoManager.SaveToBytes(img)))
|
|
{
|
|
img.Dispose();
|
|
photoUrl = store.SaveAsync(fileName, s).Result.ToString();
|
|
}
|
|
|
|
_userPhotoManagerCache.AddToCache(userID, size, fileName, _tenant.Id);
|
|
return photoUrl;
|
|
}
|
|
|
|
public byte[] GetUserPhotoData(Guid userId, Size size)
|
|
{
|
|
try
|
|
{
|
|
var pattern = string.Format("{0}_size_{1}-{2}.*", userId, size.Width, size.Height);
|
|
|
|
var fileName = GetDataStore().ListFilesRelativeAsync("", "", pattern, false).ToArrayAsync().Result.FirstOrDefault();
|
|
|
|
if (string.IsNullOrEmpty(fileName))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
using var s = GetDataStore().GetReadStreamAsync("", fileName).Result;
|
|
var data = new MemoryStream();
|
|
var buffer = new byte[1024 * 10];
|
|
while (true)
|
|
{
|
|
var count = s.Read(buffer, 0, buffer.Length);
|
|
if (count == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
data.Write(buffer, 0, count);
|
|
}
|
|
return data.ToArray();
|
|
}
|
|
catch (Exception err)
|
|
{
|
|
_log.ErrorGetUserPhotoData(err);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private IDataStore _dataStore;
|
|
private IDataStore GetDataStore()
|
|
{
|
|
return _dataStore ??= _storageFactory.GetStorage(Tenant.Id.ToString(), "userPhotos");
|
|
}
|
|
|
|
public static CacheSize ToCache(Size size)
|
|
{
|
|
return size switch
|
|
{
|
|
Size(var w, var h) when w == RetinaFotoSize.Width && h == RetinaFotoSize.Height => CacheSize.Retina,
|
|
Size(var w, var h) when w == MaxFotoSize.Width && h == MaxFotoSize.Height => CacheSize.Max,
|
|
Size(var w, var h) when w == BigFotoSize.Width && h == BigFotoSize.Height => CacheSize.Big,
|
|
Size(var w, var h) when w == SmallFotoSize.Width && h == SmallFotoSize.Height => CacheSize.Small,
|
|
Size(var w, var h) when w == MediumFotoSize.Width && h == MediumFotoSize.Height => CacheSize.Medium,
|
|
_ => CacheSize.Original
|
|
};
|
|
}
|
|
}
|
|
|
|
#region Exception Classes
|
|
|
|
public class UnknownImageFormatException : Exception
|
|
{
|
|
public UnknownImageFormatException() : base("unknown image file type") { }
|
|
|
|
public UnknownImageFormatException(Exception inner) : base("unknown image file type", inner) { }
|
|
}
|
|
|
|
public class ImageWeightLimitException : Exception
|
|
{
|
|
public ImageWeightLimitException() : base("image width is too large") { }
|
|
}
|
|
|
|
public class ImageSizeLimitException : Exception
|
|
{
|
|
public ImageSizeLimitException() : base("image size is too large") { }
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
/// <summary>
|
|
/// Helper class for manipulating images.
|
|
/// </summary>
|
|
/*public static class ImageHelper
|
|
{
|
|
/// <summary>
|
|
/// Rotate the given image byte array according to Exif Orientation data
|
|
/// </summary>
|
|
/// <param name="data">source image byte array</param>
|
|
/// <param name="updateExifData">set it to TRUE to update image Exif data after rotation (default is TRUE)</param>
|
|
/// <returns>The rotated image byte array. If no rotation occurred, source data will be returned.</returns>
|
|
public static byte[] RotateImageByExifOrientationData(byte[] data, ILog Log, bool updateExifData = true)
|
|
{
|
|
try
|
|
{
|
|
using var stream = new MemoryStream(data);
|
|
using var img = Image.Load(stream);
|
|
|
|
var fType = RotateImageByExifOrientationData(img, updateExifData);
|
|
if (fType != RotateFlipType.RotateNoneFlipNone)
|
|
{
|
|
using var tempStream = new MemoryStream();
|
|
img.Save(tempStream, System.Drawing.Imaging.ImageFormat.Png);
|
|
data = tempStream.ToArray();
|
|
}
|
|
}
|
|
catch (Exception err)
|
|
{
|
|
Log.Error(err);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rotate the given image file according to Exif Orientation data
|
|
/// </summary>
|
|
/// <param name="sourceFilePath">path of source file</param>
|
|
/// <param name="targetFilePath">path of target file</param>
|
|
/// <param name="targetFormat">target format</param>
|
|
/// <param name="updateExifData">set it to TRUE to update image Exif data after rotation (default is TRUE)</param>
|
|
/// <returns>The RotateFlipType value corresponding to the applied rotation. If no rotation occurred, RotateFlipType.RotateNoneFlipNone will be returned.</returns>
|
|
public static RotateFlipType RotateImageByExifOrientationData(string sourceFilePath, string targetFilePath, ImageFormat targetFormat, bool updateExifData = true)
|
|
{
|
|
// Rotate the image according to EXIF data
|
|
using var bmp = new Bitmap(sourceFilePath);
|
|
var fType = RotateImageByExifOrientationData(bmp, updateExifData);
|
|
if (fType != RotateFlipType.RotateNoneFlipNone)
|
|
{
|
|
bmp.Save(targetFilePath, targetFormat);
|
|
}
|
|
return fType;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rotate the given bitmap according to Exif Orientation data
|
|
/// </summary>
|
|
/// <param name="img">source image</param>
|
|
/// <param name="updateExifData">set it to TRUE to update image Exif data after rotation (default is TRUE)</param>
|
|
/// <returns>The RotateFlipType value corresponding to the applied rotation. If no rotation occurred, RotateFlipType.RotateNoneFlipNone will be returned.</returns>
|
|
public static RotateFlipType RotateImageByExifOrientationData(Image img, bool updateExifData = true)
|
|
{
|
|
const int orientationId = 0x0112;
|
|
var fType = RotateFlipType.RotateNoneFlipNone;
|
|
if (img.PropertyIdList.Contains(orientationId))
|
|
{
|
|
var pItem = img.GetPropertyItem(orientationId);
|
|
fType = GetRotateFlipTypeByExifOrientationData(pItem.Value[0]);
|
|
if (fType != RotateFlipType.RotateNoneFlipNone)
|
|
{
|
|
img.RotateFlip(fType);
|
|
if (updateExifData) img.RemovePropertyItem(orientationId); // Remove Exif orientation tag
|
|
}
|
|
}
|
|
return fType;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the proper System.Drawing.RotateFlipType according to given orientation EXIF metadata
|
|
/// </summary>
|
|
/// <param name="orientation">Exif "Orientation"</param>
|
|
/// <returns>the corresponding System.Drawing.RotateFlipType enum value</returns>
|
|
public static RotateFlipType GetRotateFlipTypeByExifOrientationData(int orientation)
|
|
{
|
|
return orientation switch
|
|
{
|
|
1 => RotateFlipType.RotateNoneFlipNone,
|
|
2 => RotateFlipType.RotateNoneFlipX,
|
|
3 => RotateFlipType.Rotate180FlipNone,
|
|
4 => RotateFlipType.Rotate180FlipX,
|
|
5 => RotateFlipType.Rotate90FlipX,
|
|
6 => RotateFlipType.Rotate90FlipNone,
|
|
7 => RotateFlipType.Rotate270FlipX,
|
|
8 => RotateFlipType.Rotate270FlipNone,
|
|
_ => RotateFlipType.RotateNoneFlipNone,
|
|
};
|
|
}
|
|
}*/
|
|
|
|
public static class SizeExtend
|
|
{
|
|
public static void Deconstruct(this Size size, out int w, out int h)
|
|
{
|
|
(w, h) = (size.Width, size.Height);
|
|
}
|
|
}
|
|
|
|
public static class ResizeWorkerItemExtension
|
|
{
|
|
public static void Register(DIHelper services)
|
|
{
|
|
services.TryAdd<ResizeWorkerItem>();
|
|
services.Configure<DistributedTaskQueueFactoryOptions>(UserPhotoManager.CUSTOM_DISTRIBUTED_TASK_QUEUE_NAME, options =>
|
|
{
|
|
options.MaxThreadsCount = 2;
|
|
});
|
|
}
|
|
}
|