From c70d7501e947adee1e9c5d2b202f226a7c96f5d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=AE=E5=A4=AA=20=E6=AB=BB=E4=BA=95?= Date: Sun, 24 Mar 2024 10:29:56 +0900 Subject: [PATCH] first commit --- .gitignore | 6 ++ Controllers/FileController.cs | 87 ++++++++++++++++++++++ Controllers/FileListController.cs | 40 ++++++++++ IO/DatabaseFactory.cs | 35 +++++++++ IO/FileDatabase.cs | 77 +++++++++++++++++++ IO/IDatabase.cs | 11 +++ IO/StorageProvider.cs | 44 +++++++++++ IO/TempFile.cs | 26 +++++++ IO/XmlReader.cs | 29 ++++++++ Models/FileModel.cs | 13 ++++ Models/Request/FileCreateModel.cs | 9 +++ Models/Request/FileListReadModel.cs | 5 ++ Models/Request/FileReadModel.cs | 8 ++ Models/Response/FileCreateResponseModel.cs | 7 ++ Models/Response/FileListResponseModel.cs | 20 +++++ Models/SettingModel.cs | 6 ++ Program.cs | 65 ++++++++++++++++ Properties/launchSettings.json | 37 +++++++++ StorageServer.csproj | 13 ++++ appsettings.Development.json | 8 ++ appsettings.json | 9 +++ storage_config.xml | 5 ++ 22 files changed, 560 insertions(+) create mode 100644 .gitignore create mode 100644 Controllers/FileController.cs create mode 100644 Controllers/FileListController.cs create mode 100644 IO/DatabaseFactory.cs create mode 100644 IO/FileDatabase.cs create mode 100644 IO/IDatabase.cs create mode 100644 IO/StorageProvider.cs create mode 100644 IO/TempFile.cs create mode 100644 IO/XmlReader.cs create mode 100644 Models/FileModel.cs create mode 100644 Models/Request/FileCreateModel.cs create mode 100644 Models/Request/FileListReadModel.cs create mode 100644 Models/Request/FileReadModel.cs create mode 100644 Models/Response/FileCreateResponseModel.cs create mode 100644 Models/Response/FileListResponseModel.cs create mode 100644 Models/SettingModel.cs create mode 100644 Program.cs create mode 100644 Properties/launchSettings.json create mode 100644 StorageServer.csproj create mode 100644 appsettings.Development.json create mode 100644 appsettings.json create mode 100644 storage_config.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4bb6281 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bin/ +obj/ +.vscode/ +*.db +saves + diff --git a/Controllers/FileController.cs b/Controllers/FileController.cs new file mode 100644 index 0000000..e6ce3b5 --- /dev/null +++ b/Controllers/FileController.cs @@ -0,0 +1,87 @@ +using Microsoft.AspNetCore.Mvc; +using StorageServer.IO; +using StorageServer.Models; +using StorageServer.Models.Request; +using StorageServer.Models.Response; + +namespace StorageServer.Controllers; + +public class FileController : Controller +{ + private readonly DatabaseFactory database; + private readonly StorageProvider provider; + + public FileController(DatabaseFactory database, StorageProvider provider) { + this.database = database; + this.provider = provider; + } + + [HttpPost] + [DisableRequestSizeLimit] + public async Task Create(FileCreateModel model) { + if (!ModelState.IsValid) { + return BadRequest(); + } + + if (model.File is null || string.IsNullOrWhiteSpace(model.File.ContentType)) { + return BadRequest(); + } + + try { + using (TempFile tempFile = new TempFile()) { + string fileName = string.IsNullOrWhiteSpace(model.FileName) ? model.File.FileName : model.FileName; + FileModel fileModel = new FileModel() { + FileName = fileName, + MimeType = model.File.ContentType, + CreatedAt = DateTime.Now, + }; + + using (FileStream fs = tempFile.Open()) { + await model.File.CopyToAsync(fs); + fs.Seek(0, SeekOrigin.Begin); + + string hash = await this.provider.ComputeStreamHash(fs); + fileModel.HashValue = hash; + } + + long fileId = this.database.Create(fileModel); + fileModel.FileID = fileId; + + using (FileStream fs = tempFile.Open()) { + await this.provider.SaveFile(fileModel, fs); + } + + return Json(new FileCreateResponseModel() { + FileId = fileId, + FileName = fileName, + MimeType = fileModel.MimeType, + }); + } + } catch (Exception) { + return Problem(); + } + } + + [HttpGet] + public IActionResult Read(FileReadModel model) { + if (!ModelState.IsValid) { + return BadRequest(); + } + + try { + FileModel fileModel = this.database.Read(model.FileID); + if (fileModel is null) { + return NotFound(); + } + + Stream stream = this.provider.GetFile(fileModel); + this.HttpContext.Response.RegisterForDispose(stream); + + return File(stream, fileModel.MimeType, fileModel.FileName, true); + } catch (FileNotFoundException) { + return NotFound(); + } catch (Exception) { + return Problem(); + } + } +} diff --git a/Controllers/FileListController.cs b/Controllers/FileListController.cs new file mode 100644 index 0000000..8d6733b --- /dev/null +++ b/Controllers/FileListController.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Mvc; +using StorageServer.IO; +using StorageServer.Models; +using StorageServer.Models.Request; +using StorageServer.Models.Response; + +namespace StorageServer.Controllers; + +public class FileListController : Controller +{ + private readonly DatabaseFactory database; + public FileListController(DatabaseFactory database) { + this.database = database; + } + + public IActionResult Read(FileListReadModel model) { + if (!ModelState.IsValid) { + return BadRequest(); + } + + if (model.Page < 0) { + return BadRequest(); + } + + try { + IEnumerable fileModels = this.database.ReadAll(); + int readCount = 100; + IEnumerable seekedModels = fileModels.Skip(readCount * model.Page).Take(readCount); + + List fileLists = new List(); + foreach (FileModel fileModel in seekedModels) { + fileLists.Add(new FileListResponseModel(fileModel)); + } + + return Json(fileLists); + } catch (Exception) { + return Problem(); + } + } +} diff --git a/IO/DatabaseFactory.cs b/IO/DatabaseFactory.cs new file mode 100644 index 0000000..671235c --- /dev/null +++ b/IO/DatabaseFactory.cs @@ -0,0 +1,35 @@ +namespace StorageServer.IO; + +using StorageServer.Models; + +public class DatabaseFactory : IDisposable { + private IDatabase database; + + public DatabaseFactory(string fileName) { + this.database = new FileDatabase(fileName); + } + + public void Dispose() { + this.database.Dispose(); + } + + public long Create(FileModel model) { + return this.database.Insert(model); + } + + public FileModel Read(long id) { + return this.database.Select(id); + } + + public IEnumerable ReadAll() { + return this.database.SelectAll(); + } + + public bool Update(FileModel model) { + return this.database.Update(model); + } + + public bool Delete(long id) { + return this.database.Delete(id); + } +} diff --git a/IO/FileDatabase.cs b/IO/FileDatabase.cs new file mode 100644 index 0000000..38d5bc2 --- /dev/null +++ b/IO/FileDatabase.cs @@ -0,0 +1,77 @@ + +using LiteDB; + +namespace StorageServer.IO; + +public class FileDatabase : IDatabase +{ + private LiteDatabase liteDatabase; + private const string tableName = "files"; + + public FileDatabase(string fileName) { + this.liteDatabase = new LiteDatabase(new ConnectionString() { + Filename = fileName, + Connection = ConnectionType.Direct, + }, new() { + EnumAsInteger = true, + }); + } + + public void Dispose() { + this.liteDatabase.Dispose(); + } + + public long Count() + { + try { + return this.liteDatabase.GetCollection(tableName).LongCount(); + } catch (LiteException e) { + throw new IOException(e.Message, e); + } + } + + public bool Delete(long id) + { + try { + return this.liteDatabase.GetCollection(tableName).Delete(id); + } catch (LiteException e) { + throw new IOException(e.Message, e); + } + } + + public long Insert(T model) + { + try { + return this.liteDatabase.GetCollection(tableName).Insert(model); + } catch (LiteException e) { + throw new IOException(e.Message, e); + } + } + + public T Select(long id) + { + try { + return this.liteDatabase.GetCollection(tableName).FindById(id); + } catch (LiteException e) { + throw new IOException(e.Message, e); + } + } + + public IEnumerable SelectAll() + { + try { + return this.liteDatabase.GetCollection(tableName).FindAll(); + } catch (LiteException e) { + throw new IOException(e.Message, e); + } + } + + public bool Update(T model) + { + try { + return this.liteDatabase.GetCollection(tableName).Update(model); + } catch (LiteException e) { + throw new IOException(e.Message, e); + } + } +} diff --git a/IO/IDatabase.cs b/IO/IDatabase.cs new file mode 100644 index 0000000..e1751a6 --- /dev/null +++ b/IO/IDatabase.cs @@ -0,0 +1,11 @@ +namespace StorageServer.IO; + +public interface IDatabase : IDisposable { + public IEnumerable SelectAll(); + public T Select(long id); + public long Insert(T model); + public bool Update(T model); + public bool Delete(long id); + + public long Count(); +} diff --git a/IO/StorageProvider.cs b/IO/StorageProvider.cs new file mode 100644 index 0000000..0d0afee --- /dev/null +++ b/IO/StorageProvider.cs @@ -0,0 +1,44 @@ +using System.Security.Cryptography; +using StorageServer.Models; + +namespace StorageServer.IO; + +public class StorageProvider { + public static int BufferSize = 16384; + + private string saveDirectory; + + public StorageProvider(string saveDirectory) { + this.saveDirectory = saveDirectory; + if (!Directory.Exists(saveDirectory)) { + Directory.CreateDirectory(saveDirectory); + } + } + + public async Task ComputeStreamHash(Stream stream) { + using (Stream buffered = new BufferedStream(stream, BufferSize)) { + byte[] checksum = await SHA256.HashDataAsync(buffered); + return BitConverter.ToString(checksum); + } + } + + public async Task SaveFile(FileModel fileModel, FileStream fs) { + string extension = Path.GetExtension(fileModel.FileName); + string saveFileName = string.Format("{0}{1}", fileModel.FileID, extension); + string saveFullName = Path.Combine(this.saveDirectory, saveFileName); + using (FileStream dest = new FileStream(saveFullName, FileMode.OpenOrCreate)) { + await fs.CopyToAsync(dest); + } + } + + public Stream GetFile(FileModel fileModel) { + string extension = Path.GetExtension(fileModel.FileName); + string saveFileName = string.Format("{0}{1}", fileModel.FileID, extension); + string saveFullName = Path.Combine(this.saveDirectory, saveFileName); + if (!File.Exists(saveFullName)) { + throw new FileNotFoundException(); + } + + return new FileStream(saveFullName, FileMode.Open); + } +} diff --git a/IO/TempFile.cs b/IO/TempFile.cs new file mode 100644 index 0000000..fdec744 --- /dev/null +++ b/IO/TempFile.cs @@ -0,0 +1,26 @@ +namespace StorageServer.IO; + +public class TempFile : IDisposable { + public string FileName { get; } + public TempFile() { + this.FileName = Path.GetTempFileName(); + } + + public FileStream Open() { + return new FileStream(this.FileName, FileMode.Open); + } + + public async Task CopyToAsync(Stream stream) { + FileStream fs = this.Open(); + await fs.CopyToAsync(stream); + return fs; + } + + public void Dispose() { + try { + File.Delete(this.FileName); + } catch (Exception) { + // unhandled exception error. + } + } +} \ No newline at end of file diff --git a/IO/XmlReader.cs b/IO/XmlReader.cs new file mode 100644 index 0000000..1c0a7c1 --- /dev/null +++ b/IO/XmlReader.cs @@ -0,0 +1,29 @@ +using System.Xml.Serialization; + +namespace StorageServer.IO; + +public static class XmlReader { + public static void Save(string filePath, T obj) { + try { + XmlSerializer serializer = new XmlSerializer(typeof(T)); + using (FileStream fs = new FileStream(filePath, FileMode.OpenOrCreate)) { + serializer.Serialize(fs, obj); + } + } catch (Exception e) { + Console.WriteLine($"Error: {e.Message}"); + } + } + + public static T Read(string filePath) { + try { + XmlSerializer serializer = new XmlSerializer(typeof(T)); + using (FileStream fs = new FileStream(filePath, FileMode.OpenOrCreate)) { + return (T) serializer.Deserialize(fs); + } + } catch (Exception e) { + Console.WriteLine($"Error: {e.Message}"); + return default(T); + } + } +} + diff --git a/Models/FileModel.cs b/Models/FileModel.cs new file mode 100644 index 0000000..231ec2a --- /dev/null +++ b/Models/FileModel.cs @@ -0,0 +1,13 @@ +using LiteDB; + +namespace StorageServer.Models; + +public class FileModel { + [BsonId] + public long FileID { get; set; } + + public string FileName { get; set; } + public string MimeType { get; set; } + public string HashValue { get; set; } + public DateTime CreatedAt { get; set; } +} diff --git a/Models/Request/FileCreateModel.cs b/Models/Request/FileCreateModel.cs new file mode 100644 index 0000000..93c56b0 --- /dev/null +++ b/Models/Request/FileCreateModel.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace StorageServer.Models.Request; + +public class FileCreateModel { + [Required] + public IFormFile File { get; set; } + public string FileName { get; set; } +} diff --git a/Models/Request/FileListReadModel.cs b/Models/Request/FileListReadModel.cs new file mode 100644 index 0000000..84d4373 --- /dev/null +++ b/Models/Request/FileListReadModel.cs @@ -0,0 +1,5 @@ +namespace StorageServer.Models.Request; + +public class FileListReadModel { + public int Page { get; set; } = 0; +} diff --git a/Models/Request/FileReadModel.cs b/Models/Request/FileReadModel.cs new file mode 100644 index 0000000..e6ef186 --- /dev/null +++ b/Models/Request/FileReadModel.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace StorageServer.Models.Request; + +public class FileReadModel { + [Required] + public long FileID { get; set; } +} diff --git a/Models/Response/FileCreateResponseModel.cs b/Models/Response/FileCreateResponseModel.cs new file mode 100644 index 0000000..0c9fcf0 --- /dev/null +++ b/Models/Response/FileCreateResponseModel.cs @@ -0,0 +1,7 @@ +namespace StorageServer.Models.Response; + +public class FileCreateResponseModel { + public long FileId { get; set; } = -1; + public string FileName { get; set; } + public string MimeType { get; set; } +} diff --git a/Models/Response/FileListResponseModel.cs b/Models/Response/FileListResponseModel.cs new file mode 100644 index 0000000..63b3f9b --- /dev/null +++ b/Models/Response/FileListResponseModel.cs @@ -0,0 +1,20 @@ +namespace StorageServer.Models.Response; + +public class FileListResponseModel { + public FileListResponseModel() {} + public FileListResponseModel(FileModel model) { + this.FileID = model.FileID; + this.FileName = model.FileName; + this.MimeType = model.MimeType; + this.HashValue = model.HashValue; + this.CreatedAt = model.CreatedAt; + } + + public long FileID { get; set; } + + public string FileName { get; set; } + public string MimeType { get; set; } + public string HashValue { get; set; } + public DateTime CreatedAt { get; set; } + +} diff --git a/Models/SettingModel.cs b/Models/SettingModel.cs new file mode 100644 index 0000000..faeb284 --- /dev/null +++ b/Models/SettingModel.cs @@ -0,0 +1,6 @@ +namespace StorageServer.Models; + +public class SettingModel { + public string DBFilePath { get; set; } + public string SavePath { get; set; } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..efedb5c --- /dev/null +++ b/Program.cs @@ -0,0 +1,65 @@ +using StorageServer.IO; +using StorageServer.Models; + +namespace StorageServer; + +public class Program { + public static void Main(string[] args) { + const string settingPath = "storage_config.xml"; + const string allowOrigins = "_storageserver_allow_origin"; + if (!File.Exists(settingPath)) { + XmlReader.Save(settingPath, new SettingModel() { + DBFilePath = "files.db", + SavePath = "saves", + }); + } + + SettingModel setting = XmlReader.Read(settingPath); + + DatabaseFactory factory = new DatabaseFactory(setting.DBFilePath); + StorageProvider provider = new StorageProvider(setting.SavePath); + + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddSingleton(factory); + builder.Services.AddSingleton(provider); + // Add services to the container. + builder.Services.AddControllersWithViews(); + + builder.Services.AddCors(options => { + options.AddPolicy(allowOrigins, policy => { + policy.AllowAnyOrigin(); + policy.AllowAnyHeader(); + policy.AllowAnyMethod(); + }); + }); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (!app.Environment.IsDevelopment()) + { + app.UseExceptionHandler("/Home/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseCors(allowOrigins); + + app.UseAuthorization(); + + app.MapControllerRoute( + name: "default", + pattern: "{controller=File}/{action=Read}/{FileId?}"); + + app.Run(); + + factory.Dispose(); + } +} + diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..b5527ec --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,37 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:38186", + "sslPort": 44352 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5070", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7261;http://localhost:5070", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/StorageServer.csproj b/StorageServer.csproj new file mode 100644 index 0000000..42f17f4 --- /dev/null +++ b/StorageServer.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + disable + enable + + + + + + + diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/storage_config.xml b/storage_config.xml new file mode 100644 index 0000000..c2ddc64 --- /dev/null +++ b/storage_config.xml @@ -0,0 +1,5 @@ + + + files.db + saves + \ No newline at end of file