kamihama-server/KamihamaWeb/Services/DiskCacheService.cs

203 lines
7.0 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using KamihamaWeb.Interfaces;
using KamihamaWeb.Util;
using Microsoft.Extensions.Configuration;
using RestSharp;
using Serilog;
namespace KamihamaWeb.Services
{
public class DiskCacheService: IDiskCacheSingleton
{
public DiskCacheService(IConfiguration config, IRestSharpTransient rest) : this(Guid.NewGuid(), config, rest)
{
}
public DiskCacheService(Guid guid, IConfiguration config, IRestSharpTransient rest)
{
Guid = guid;
Rest = rest;
if (string.IsNullOrEmpty(config["MagiRecoServer:CacheDirectory"]))
{
throw new Exception("CacheDirectory is not set in the configuration! Exiting.");
}
CacheDirectory = config["MagiRecoServer:CacheDirectory"];
ScenarioCacheDirectory = $"{config["MagiRecoServer:CacheDirectory"]}/scenario/json/general/";
Directory.CreateDirectory(CacheDirectory);
Directory.CreateDirectory(ScenarioCacheDirectory);
}
public Guid Guid { get; }
private IRestSharpTransient Rest { get; set; }
private string CacheDirectory { get; set; } = "";
private string ScenarioCacheDirectory { get; set; } = "";
public async Task<DiskCacheItem> Get(string cacheItem, string versionMd5, bool forceOrigin = false)
{
// Remember: don't allow directory traversal attacks...
var filename = CryptUtil.CalculateSha256(cacheItem + "?" + versionMd5);
var filePath = Path.Combine(CacheDirectory, filename);
if (!forceOrigin && cacheItem.StartsWith("scenario/json/general"))
{
var generalJson = Path.Combine(ScenarioCacheDirectory, filename);
if (File.Exists(generalJson))
{
return new DiskCacheItem()
{
Data = File.Open(generalJson, FileMode.Open, FileAccess.Read, FileShare.Read),
Result = DiskCacheResultType.Success
};
}
else
{
Log.Information($"Cache item {generalJson} not found! Falling back to origin.");
}
}
if (File.Exists(filePath))
{
Log.Debug($"Loading {cacheItem} from disk ({filePath}).");
var maxLoops = 5;
while (maxLoops-- > 0)
{
try
{
var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
if (stream.Length == 0)
{
Log.Information($"Empty file found, deleting {filePath}.");
File.Delete(filePath);
return await FastFetch(cacheItem, filePath, versionMd5);
}
else
{
return new DiskCacheItem()
{
Data = stream
};
}
}
catch (IOException) // File in use, wait
{
Log.Information("Failed, file is already being downloaded, retrying in 1000ms.");
await Task.Delay(1000);
}
}
Log.Warning($"Max loops exceeded in DiskCacheService.Get() for {cacheItem}.");
return new DiskCacheItem()
{
Result = DiskCacheResultType.Failed
};
}
Log.Information($"Fetching {cacheItem}.");
return await FastFetch(cacheItem, filePath, versionMd5);
}
public async Task<string> Store(string filepath, byte[] storeContents, StoreType type)
{
string storePath;
switch (type)
{
case StoreType.ScenarioGeneral:
var md5 = CryptUtil.CalculateMd5Bytes(storeContents);
var filename = CryptUtil.CalculateSha256(filepath + "?" + md5);
storePath = Path.Combine(ScenarioCacheDirectory, filename);
await File.WriteAllBytesAsync(storePath, storeContents);
break;
default:
throw new Exception("Invalid StoreType.");
}
return storePath;
}
public enum StoreType
{
ScenarioGeneral
}
private async Task<DiskCacheItem> FastFetch(string item, string path, string md5)
{
var maxLoops = 5;
var deleteFlag = false;
while (maxLoops-- > 0)
{
try
{
FileStream file = File.Open(path, FileMode.Create, FileAccess.ReadWrite,
FileShare.None); // Open file
try
{
if (file.Length > 0)
{
return new DiskCacheItem()
{
Data = file,
Result = DiskCacheResultType.Success
};
}
var stream = await Rest.FetchAsset(item + $"?{md5}");
if (stream.Data == null)
{
deleteFlag = true;
return stream;
}
((MemoryStream) stream.Data).WriteTo(file);
stream.Data.Seek(0, SeekOrigin.Begin);
return stream;
}
catch (Exception)
{
deleteFlag = true;
}
finally
{
file.Close();
await file.DisposeAsync();
if (deleteFlag)
{
File.Delete(path);
}
}
}
catch (IOException)
{
await Task.Delay(500);
}
}
Log.Warning($"Max loops exceeded in DiskCacheService.FastFetch() for {item}.");
File.Delete(path);
return new DiskCacheItem()
{
Result = DiskCacheResultType.Failed
};
}
}
public class DiskCacheItem
{
public Stream Data { get; set; } = null;
public DiskCacheResultType Result { get; set; } = DiskCacheResultType.Success;
}
public enum DiskCacheResultType
{
Success = 0,
Failed = 500,
NotFound = 404
}
}