first commit
This commit is contained in:
parent
8147e29185
commit
c70d7501e9
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
bin/
|
||||
obj/
|
||||
.vscode/
|
||||
*.db
|
||||
saves
|
||||
|
87
Controllers/FileController.cs
Normal file
87
Controllers/FileController.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
40
Controllers/FileListController.cs
Normal file
40
Controllers/FileListController.cs
Normal 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
35
IO/DatabaseFactory.cs
Normal 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
77
IO/FileDatabase.cs
Normal 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
11
IO/IDatabase.cs
Normal 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
44
IO/StorageProvider.cs
Normal 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
26
IO/TempFile.cs
Normal 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
29
IO/XmlReader.cs
Normal 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
13
Models/FileModel.cs
Normal 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; }
|
||||
}
|
9
Models/Request/FileCreateModel.cs
Normal file
9
Models/Request/FileCreateModel.cs
Normal 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; }
|
||||
}
|
5
Models/Request/FileListReadModel.cs
Normal file
5
Models/Request/FileListReadModel.cs
Normal file
@ -0,0 +1,5 @@
|
||||
namespace StorageServer.Models.Request;
|
||||
|
||||
public class FileListReadModel {
|
||||
public int Page { get; set; } = 0;
|
||||
}
|
8
Models/Request/FileReadModel.cs
Normal file
8
Models/Request/FileReadModel.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace StorageServer.Models.Request;
|
||||
|
||||
public class FileReadModel {
|
||||
[Required]
|
||||
public long FileID { get; set; }
|
||||
}
|
7
Models/Response/FileCreateResponseModel.cs
Normal file
7
Models/Response/FileCreateResponseModel.cs
Normal 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; }
|
||||
}
|
20
Models/Response/FileListResponseModel.cs
Normal file
20
Models/Response/FileListResponseModel.cs
Normal 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
6
Models/SettingModel.cs
Normal 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
65
Program.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
37
Properties/launchSettings.json
Normal file
37
Properties/launchSettings.json
Normal 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
13
StorageServer.csproj
Normal 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>
|
8
appsettings.Development.json
Normal file
8
appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
9
appsettings.json
Normal file
9
appsettings.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
5
storage_config.xml
Normal file
5
storage_config.xml
Normal 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>
|
Loading…
Reference in New Issue
Block a user