first commit

This commit is contained in:
亮太 櫻井 2024-03-24 10:29:56 +09:00
parent 8147e29185
commit c70d7501e9
22 changed files with 560 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
bin/
obj/
.vscode/
*.db
saves

View File

@ -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<IActionResult> 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();
}
}
}

View File

@ -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<FileModel> fileModels = this.database.ReadAll();
int readCount = 100;
IEnumerable<FileModel> seekedModels = fileModels.Skip(readCount * model.Page).Take(readCount);
List<FileListResponseModel> fileLists = new List<FileListResponseModel>();
foreach (FileModel fileModel in seekedModels) {
fileLists.Add(new FileListResponseModel(fileModel));
}
return Json(fileLists);
} catch (Exception) {
return Problem();
}
}
}

35
IO/DatabaseFactory.cs Normal file
View File

@ -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<FileModel>(model);
}
public FileModel Read(long id) {
return this.database.Select<FileModel>(id);
}
public IEnumerable<FileModel> ReadAll() {
return this.database.SelectAll<FileModel>();
}
public bool Update(FileModel model) {
return this.database.Update<FileModel>(model);
}
public bool Delete(long id) {
return this.database.Delete(id);
}
}

77
IO/FileDatabase.cs Normal file
View File

@ -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>(T model)
{
try {
return this.liteDatabase.GetCollection<T>(tableName).Insert(model);
} catch (LiteException e) {
throw new IOException(e.Message, e);
}
}
public T Select<T>(long id)
{
try {
return this.liteDatabase.GetCollection<T>(tableName).FindById(id);
} catch (LiteException e) {
throw new IOException(e.Message, e);
}
}
public IEnumerable<T> SelectAll<T>()
{
try {
return this.liteDatabase.GetCollection<T>(tableName).FindAll();
} catch (LiteException e) {
throw new IOException(e.Message, e);
}
}
public bool Update<T>(T model)
{
try {
return this.liteDatabase.GetCollection<T>(tableName).Update(model);
} catch (LiteException e) {
throw new IOException(e.Message, e);
}
}
}

11
IO/IDatabase.cs Normal file
View File

@ -0,0 +1,11 @@
namespace StorageServer.IO;
public interface IDatabase : IDisposable {
public IEnumerable<T> SelectAll<T>();
public T Select<T>(long id);
public long Insert<T>(T model);
public bool Update<T>(T model);
public bool Delete(long id);
public long Count();
}

44
IO/StorageProvider.cs Normal file
View File

@ -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<string> 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);
}
}

26
IO/TempFile.cs Normal file
View File

@ -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<FileStream> 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.
}
}
}

29
IO/XmlReader.cs Normal file
View File

@ -0,0 +1,29 @@
using System.Xml.Serialization;
namespace StorageServer.IO;
public static class XmlReader {
public static void Save<T>(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<T>(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);
}
}
}

13
Models/FileModel.cs Normal file
View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -0,0 +1,5 @@
namespace StorageServer.Models.Request;
public class FileListReadModel {
public int Page { get; set; } = 0;
}

View File

@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;
namespace StorageServer.Models.Request;
public class FileReadModel {
[Required]
public long FileID { get; set; }
}

View File

@ -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; }
}

View File

@ -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; }
}

6
Models/SettingModel.cs Normal file
View File

@ -0,0 +1,6 @@
namespace StorageServer.Models;
public class SettingModel {
public string DBFilePath { get; set; }
public string SavePath { get; set; }
}

65
Program.cs Normal file
View File

@ -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<SettingModel>(settingPath, new SettingModel() {
DBFilePath = "files.db",
SavePath = "saves",
});
}
SettingModel setting = XmlReader.Read<SettingModel>(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();
}
}

View File

@ -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"
}
}
}
}

13
StorageServer.csproj Normal file
View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LiteDB" Version="5.0.17" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
appsettings.json Normal file
View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

5
storage_config.xml Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<SettingModel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<DBFilePath>files.db</DBFilePath>
<SavePath>saves</SavePath>
</SettingModel>