Bladeren bron

File upload and track creation

Lukas Angerer 3 jaren geleden
bovenliggende
commit
4437b22582

+ 1 - 0
src/RunnersMeet.Server/AppServer.cs

@@ -44,6 +44,7 @@ public class AppServer
 	private void ConfigureServices(IServiceCollection services, IConfigurationRoot config)
 	{
 		services.AddSingleton<IDatabase, Database>();
+		services.AddSingleton<IFileStorage, FileStorage>();
 		services.AddScoped<QueryFactory, QueryFactory>();
 
 		services.Configure<PersistenceOptions>(config.GetSection(PersistenceOptions.Persistence));

+ 15 - 1
src/RunnersMeet.Server/Controllers/TracksController.cs

@@ -1,3 +1,4 @@
+using System.Security.Claims;
 using Microsoft.AspNetCore.Mvc;
 using RunnersMeet.Server.Domain;
 using RunnersMeet.Server.Persistence;
@@ -8,10 +9,12 @@ namespace RunnersMeet.Server.Controllers;
 [ApiController]
 public class TracksController : ControllerBase
 {
+	private readonly IFileStorage _fileStorage;
 	private readonly QueryFactory _queryFactory;
 
-	public TracksController(QueryFactory queryFactory)
+	public TracksController(IFileStorage fileStorage, QueryFactory queryFactory)
 	{
+		_fileStorage = fileStorage;
 		_queryFactory = queryFactory;
 	}
 
@@ -20,4 +23,15 @@ public class TracksController : ControllerBase
 	{
 		return Ok(_queryFactory.TracksQuery().Get());
 	}
+
+	[HttpPost]
+	public async Task<ActionResult<Track>> CreateTrack([FromForm] IFormFile file, [FromForm] string? name, CancellationToken cancellationToken = default)
+	{
+		var fileName = await _fileStorage.UploadFileAsync(file, name, cancellationToken);
+
+		var userId = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "<unknown>";
+		var track = _queryFactory.CreateTrackCommand().Create(userId, fileName);
+
+		return Ok(track);
+	}
 }

+ 4 - 1
src/RunnersMeet.Server/Domain/Track.cs

@@ -1,9 +1,12 @@
+using LiteDB;
+
 namespace RunnersMeet.Server.Domain;
 
 public class Track : IUserData
 {
+	public ObjectId TrackId { get; set; }
 	public string Owner { get; set; }
-	public string FileName { get; set; }
+	public string FileHash { get; set; }
 	public string DisplayName { get; set; }
 	public double Distance { get; set; }
 	public double ElevationUp { get; set; }

+ 30 - 0
src/RunnersMeet.Server/Persistence/CreateTrackCommand.cs

@@ -0,0 +1,30 @@
+using RunnersMeet.Server.Domain;
+
+namespace RunnersMeet.Server.Persistence;
+
+public class CreateTrackCommand
+{
+	private readonly IDatabase _database;
+
+	public CreateTrackCommand(IDatabase database)
+	{
+		_database = database;
+	}
+
+	public Track Create(string owner, FileName fileName)
+	{
+		var track = new Track
+		{
+			Owner = owner,
+			FileHash = fileName.Hash,
+			DisplayName = fileName.DisplayName,
+			Distance = 0.0,
+			ElevationUp = 0.0,
+			ElevationDown = 0.0,
+		};
+
+		_database.Tracks.Insert(track);
+
+		return track;
+	}
+}

+ 1 - 1
src/RunnersMeet.Server/Persistence/Database.cs

@@ -14,6 +14,6 @@ public class Database : IDatabase
 		_db = new LiteDatabase(persistenceOptions.DataFilePath);
 
 		Tracks = _db.GetCollection<Track>("tracks");
-		Tracks.EnsureIndex(t => t.FileName);
+		Tracks.EnsureIndex(t => t.Owner);
 	}
 }

+ 28 - 0
src/RunnersMeet.Server/Persistence/FileName.cs

@@ -0,0 +1,28 @@
+using System.Security.Cryptography;
+
+namespace RunnersMeet.Server.Persistence;
+
+public class FileName
+{
+	public string Hash { get; }
+	public string DisplayName { get; }
+
+	private FileName(string hash, string displayName)
+	{
+		Hash = hash;
+		DisplayName = displayName;
+	}
+
+	public string GetPath()
+	{
+		return Path.Combine(Hash.Substring(0, 2), Hash.Substring(2, 2), Hash);
+	}
+
+	public static async Task<FileName> FromFormUploadAsync(IFormFile file, string? displayName, CancellationToken cancellationToken = default)
+	{
+		var sha1 = SHA1.Create();
+		var hash = Convert.ToHexString(await sha1.ComputeHashAsync(file.OpenReadStream(), cancellationToken));
+
+		return new FileName(hash, displayName ?? file.FileName);
+	}
+}

+ 33 - 0
src/RunnersMeet.Server/Persistence/FileStorage.cs

@@ -0,0 +1,33 @@
+namespace RunnersMeet.Server.Persistence;
+
+public class FileStorage : IFileStorage
+{
+	private readonly IPersistenceOptions _persistenceOptions;
+
+	public FileStorage(IPersistenceOptions persistenceOptions)
+	{
+		_persistenceOptions = persistenceOptions;
+	}
+
+	public async Task<FileName> UploadFileAsync(IFormFile file, string? displayName, CancellationToken cancellationToken = default)
+	{
+		var fileName = await FileName.FromFormUploadAsync(file, displayName, cancellationToken);
+		await using var fileStream = CreateFile(fileName);
+
+		await file.CopyToAsync(fileStream, cancellationToken);
+
+		return fileName;
+	}
+
+	private FileStream CreateFile(FileName name)
+	{
+		var path = Path.Combine(_persistenceOptions.FileStorageRootPath, name.GetPath());
+		if (File.Exists(path))
+		{
+			throw new ArgumentException($"File with the hash {name.Hash} already exists");
+		}
+		Directory.CreateDirectory(Path.GetDirectoryName(path)!);
+
+		return File.Create(path);
+	}
+}

+ 6 - 0
src/RunnersMeet.Server/Persistence/IFileStorage.cs

@@ -0,0 +1,6 @@
+namespace RunnersMeet.Server.Persistence;
+
+public interface IFileStorage
+{
+	public Task<FileName> UploadFileAsync(IFormFile file, string? displayName, CancellationToken cancellationToken = default);
+}

+ 5 - 0
src/RunnersMeet.Server/Persistence/QueryFactory.cs

@@ -13,4 +13,9 @@ public class QueryFactory
 	{
 		return new TracksQuery(_database);
 	}
+
+	public CreateTrackCommand CreateTrackCommand()
+	{
+		return new CreateTrackCommand(_database);
+	}
 }

+ 1 - 1
src/RunnersMeet.Server/Persistence/TracksQuery.cs

@@ -13,6 +13,6 @@ public class TracksQuery : ITracksQuery
 
 	public IEnumerable<Track> Get()
 	{
-		return _database.Tracks.Query().OrderBy(t => t.FileName).ToEnumerable();
+		return _database.Tracks.Query().OrderBy(t => t.DisplayName).ToEnumerable();
 	}
 }