From bd596638a7767b42c67c8d7aea2ae6fbf4d5734f Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 21 Jul 2021 18:30:43 +0200 Subject: [PATCH] Creating a file system composite --- Kyoo.Authentication/Views/AccountApi.cs | 8 +- .../{IFileManager.cs => IFileSystem.cs} | 26 +--- Kyoo.Common/Controllers/ITask.cs | 7 + Kyoo.Common/Kyoo.Common.csproj | 1 + .../Attributes/FileSystemMetadataAttribute.cs | 35 +++++ Kyoo.Common/Models/Resources/Episode.cs | 2 +- Kyoo.Common/Models/Resources/Show.cs | 4 +- Kyoo.Common/Models/WatchItem.cs | 2 +- Kyoo.Common/Utility/Utility.cs | 2 +- .../RepositoryActivator.cs | 0 .../{Library => Database}/RepositoryTests.cs | 0 .../SpecificTests/CollectionsTests.cs | 0 .../SpecificTests/EpisodeTests.cs | 0 .../SpecificTests/GenreTests.cs | 0 .../SpecificTests/LibraryItemTest.cs | 0 .../SpecificTests/LibraryTests.cs | 0 .../SpecificTests/PeopleTests.cs | 0 .../SpecificTests/ProviderTests.cs | 0 .../SpecificTests/SanityTests.cs | 0 .../SpecificTests/SeasonTests.cs | 0 .../SpecificTests/ShowTests.cs | 0 .../SpecificTests/StudioTests.cs | 0 .../SpecificTests/TrackTests.cs | 0 .../SpecificTests/UserTests.cs | 0 .../{Library => Database}/TestContext.cs | 0 .../{Library => Database}/TestSample.cs | 0 .../FileSystems/FileSystemComposite.cs | 143 ++++++++++++++++++ .../Controllers/FileSystems/HttpFileSystem.cs | 123 +++++++++++++++ .../LocalFileSystem.cs} | 33 +--- Kyoo/Controllers/ThumbnailsManager.cs | 56 +++++-- Kyoo/Controllers/Transcoder.cs | 15 +- Kyoo/CoreModule.cs | 8 +- Kyoo/Kyoo.csproj | 1 + Kyoo/Startup.cs | 2 + Kyoo/Tasks/Crawler.cs | 4 +- Kyoo/Tasks/Housekeeping.cs | 6 +- Kyoo/Views/EpisodeApi.cs | 4 +- Kyoo/Views/PeopleApi.cs | 4 +- Kyoo/Views/ProviderApi.cs | 4 +- Kyoo/Views/SeasonApi.cs | 4 +- Kyoo/Views/ShowApi.cs | 4 +- Kyoo/Views/SubtitleApi.cs | 10 +- Kyoo/Views/VideoApi.cs | 4 +- 43 files changed, 412 insertions(+), 100 deletions(-) rename Kyoo.Common/Controllers/{IFileManager.cs => IFileSystem.cs} (78%) create mode 100644 Kyoo.Common/Models/Attributes/FileSystemMetadataAttribute.cs rename Kyoo.Tests/{Library => Database}/RepositoryActivator.cs (100%) rename Kyoo.Tests/{Library => Database}/RepositoryTests.cs (100%) rename Kyoo.Tests/{Library => Database}/SpecificTests/CollectionsTests.cs (100%) rename Kyoo.Tests/{Library => Database}/SpecificTests/EpisodeTests.cs (100%) rename Kyoo.Tests/{Library => Database}/SpecificTests/GenreTests.cs (100%) rename Kyoo.Tests/{Library => Database}/SpecificTests/LibraryItemTest.cs (100%) rename Kyoo.Tests/{Library => Database}/SpecificTests/LibraryTests.cs (100%) rename Kyoo.Tests/{Library => Database}/SpecificTests/PeopleTests.cs (100%) rename Kyoo.Tests/{Library => Database}/SpecificTests/ProviderTests.cs (100%) rename Kyoo.Tests/{Library => Database}/SpecificTests/SanityTests.cs (100%) rename Kyoo.Tests/{Library => Database}/SpecificTests/SeasonTests.cs (100%) rename Kyoo.Tests/{Library => Database}/SpecificTests/ShowTests.cs (100%) rename Kyoo.Tests/{Library => Database}/SpecificTests/StudioTests.cs (100%) rename Kyoo.Tests/{Library => Database}/SpecificTests/TrackTests.cs (100%) rename Kyoo.Tests/{Library => Database}/SpecificTests/UserTests.cs (100%) rename Kyoo.Tests/{Library => Database}/TestContext.cs (100%) rename Kyoo.Tests/{Library => Database}/TestSample.cs (100%) create mode 100644 Kyoo/Controllers/FileSystems/FileSystemComposite.cs create mode 100644 Kyoo/Controllers/FileSystems/HttpFileSystem.cs rename Kyoo/Controllers/{FileManager.cs => FileSystems/LocalFileSystem.cs} (77%) diff --git a/Kyoo.Authentication/Views/AccountApi.cs b/Kyoo.Authentication/Views/AccountApi.cs index 59d964f0..4288250e 100644 --- a/Kyoo.Authentication/Views/AccountApi.cs +++ b/Kyoo.Authentication/Views/AccountApi.cs @@ -36,7 +36,7 @@ namespace Kyoo.Authentication.Views /// /// A file manager to send profile pictures /// - private readonly IFileManager _files; + private readonly IFileSystem _files; /// /// Options about authentication. Those options are monitored and reloads are supported. /// @@ -50,7 +50,7 @@ namespace Kyoo.Authentication.Views /// A file manager to send profile pictures /// Authentication options (this may be hot reloaded) public AccountApi(IUserRepository users, - IFileManager files, + IFileSystem files, IOptions options) { _users = users; @@ -205,8 +205,8 @@ namespace Kyoo.Authentication.Views user.Username = data.Username; if (data.Picture?.Length > 0) { - string path = Path.Combine(_options.Value.ProfilePicturePath, user.ID.ToString()); - await using Stream file = _files.NewFile(path); + string path = _files.Combine(_options.Value.ProfilePicturePath, user.ID.ToString()); + await using Stream file = await _files.NewFile(path); await data.Picture.CopyToAsync(file); } return await _users.Edit(user, false); diff --git a/Kyoo.Common/Controllers/IFileManager.cs b/Kyoo.Common/Controllers/IFileSystem.cs similarity index 78% rename from Kyoo.Common/Controllers/IFileManager.cs rename to Kyoo.Common/Controllers/IFileSystem.cs index d39cff6e..2fb1c6d7 100644 --- a/Kyoo.Common/Controllers/IFileManager.cs +++ b/Kyoo.Common/Controllers/IFileSystem.cs @@ -10,7 +10,7 @@ namespace Kyoo.Controllers /// /// A service to abstract the file system to allow custom file systems (like distant file systems or external providers) /// - public interface IFileManager + public interface IFileSystem { // TODO find a way to handle Transmux/Transcode with this system. @@ -41,14 +41,14 @@ namespace Kyoo.Controllers /// The path of the file /// If the file could not be found. /// A reader to read the file. - public Stream GetReader([NotNull] string path); + public Task GetReader([NotNull] string path); /// /// Create a new file at . /// /// The path of the new file. /// A writer to write to the new file. - public Stream NewFile([NotNull] string path); + public Task NewFile([NotNull] string path); /// /// Create a new directory at the given path @@ -87,24 +87,6 @@ namespace Kyoo.Controllers /// /// The show to proceed /// The extra directory of the show - public string GetExtraDirectory(Show show); - - /// - /// Get the extra directory of a season. - /// This method is in this system to allow a filesystem to use a different metadata policy for one. - /// It can be useful if the filesystem is readonly. - /// - /// The season to proceed - /// The extra directory of the season - public string GetExtraDirectory(Season season); - - /// - /// Get the extra directory of an episode. - /// This method is in this system to allow a filesystem to use a different metadata policy for one. - /// It can be useful if the filesystem is readonly. - /// - /// The episode to proceed - /// The extra directory of the episode - public string GetExtraDirectory(Episode episode); + public string GetExtraDirectory([NotNull] Show show); } } \ No newline at end of file diff --git a/Kyoo.Common/Controllers/ITask.cs b/Kyoo.Common/Controllers/ITask.cs index 3267837b..1896bbcd 100644 --- a/Kyoo.Common/Controllers/ITask.cs +++ b/Kyoo.Common/Controllers/ITask.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Kyoo.Models.Attributes; +using Kyoo.Models.Exceptions; namespace Kyoo.Controllers { @@ -210,6 +211,12 @@ namespace Kyoo.Controllers /// they will be set to an available service from the service container before calling this method. /// They also will be removed after this method return (or throw) to prevent dangling services. /// + /// + /// An exception meaning that the task has failed for handled reasons like invalid arguments, + /// invalid environment, missing plugins or failures not related to a default in the code. + /// This exception allow the task to display a failure message to the end user while others exceptions + /// will be displayed as unhandled exceptions and display a stack trace. + /// public Task Run([NotNull] TaskParameters arguments, [NotNull] IProgress progress, CancellationToken cancellationToken); diff --git a/Kyoo.Common/Kyoo.Common.csproj b/Kyoo.Common/Kyoo.Common.csproj index ada2ed60..844be997 100644 --- a/Kyoo.Common/Kyoo.Common.csproj +++ b/Kyoo.Common/Kyoo.Common.csproj @@ -27,6 +27,7 @@ + diff --git a/Kyoo.Common/Models/Attributes/FileSystemMetadataAttribute.cs b/Kyoo.Common/Models/Attributes/FileSystemMetadataAttribute.cs new file mode 100644 index 00000000..464c44d0 --- /dev/null +++ b/Kyoo.Common/Models/Attributes/FileSystemMetadataAttribute.cs @@ -0,0 +1,35 @@ +using System; +using System.ComponentModel.Composition; +using Kyoo.Controllers; + +namespace Kyoo.Common.Models.Attributes +{ + /// + /// An attribute to inform how a works. + /// + [MetadataAttribute] + [AttributeUsage(AttributeTargets.Class)] + public class FileSystemMetadataAttribute : Attribute + { + /// + /// The scheme(s) used to identify this path. + /// It can be something like http, https, ftp, file and so on. + /// + /// + /// If multiples files with the same schemes exists, an exception will be thrown. + /// + public string[] Scheme { get; } + + /// + /// true if the scheme should be removed from the path before calling + /// methods of this , false otherwise. + /// + public bool StripScheme { get; set; } + + + public FileSystemMetadataAttribute(string[] schemes) + { + Scheme = schemes; + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index e7cf056a..7f76a8a4 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -95,7 +95,7 @@ namespace Kyoo.Models public int? AbsoluteNumber { get; set; } /// - /// The path of the video file for this episode. Any format supported by a is allowed. + /// The path of the video file for this episode. Any format supported by a is allowed. /// [SerializeIgnore] public string Path { get; set; } diff --git a/Kyoo.Common/Models/Resources/Show.cs b/Kyoo.Common/Models/Resources/Show.cs index 656d6ffb..c9d5ca8e 100644 --- a/Kyoo.Common/Models/Resources/Show.cs +++ b/Kyoo.Common/Models/Resources/Show.cs @@ -31,7 +31,7 @@ namespace Kyoo.Models /// /// The path of the root directory of this show. - /// This can be any kind of path supported by + /// This can be any kind of path supported by /// [SerializeIgnore] public string Path { get; set; } @@ -46,7 +46,7 @@ namespace Kyoo.Models public Status? Status { get; set; } /// - /// An URL to a trailer. This could be any path supported by the . + /// An URL to a trailer. This could be any path supported by the . /// /// TODO for now, this is set to a youtube url. It should be cached and converted to a local file. public string TrailerUrl { get; set; } diff --git a/Kyoo.Common/Models/WatchItem.cs b/Kyoo.Common/Models/WatchItem.cs index eaf437c7..241ed9a9 100644 --- a/Kyoo.Common/Models/WatchItem.cs +++ b/Kyoo.Common/Models/WatchItem.cs @@ -62,7 +62,7 @@ namespace Kyoo.Models public DateTime? ReleaseDate { get; set; } /// - /// The path of the video file for this episode. Any format supported by a is allowed. + /// The path of the video file for this episode. Any format supported by a is allowed. /// [SerializeIgnore] public string Path { get; set; } diff --git a/Kyoo.Common/Utility/Utility.cs b/Kyoo.Common/Utility/Utility.cs index 6b6dc472..2f7e14ad 100644 --- a/Kyoo.Common/Utility/Utility.cs +++ b/Kyoo.Common/Utility/Utility.cs @@ -254,7 +254,7 @@ namespace Kyoo /// /// To run for a List where you don't know the type at compile type, /// you could do: - /// + /// /// Utility.RunGenericMethod<object>( /// typeof(Utility), /// nameof(MergeLists), diff --git a/Kyoo.Tests/Library/RepositoryActivator.cs b/Kyoo.Tests/Database/RepositoryActivator.cs similarity index 100% rename from Kyoo.Tests/Library/RepositoryActivator.cs rename to Kyoo.Tests/Database/RepositoryActivator.cs diff --git a/Kyoo.Tests/Library/RepositoryTests.cs b/Kyoo.Tests/Database/RepositoryTests.cs similarity index 100% rename from Kyoo.Tests/Library/RepositoryTests.cs rename to Kyoo.Tests/Database/RepositoryTests.cs diff --git a/Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs b/Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs similarity index 100% rename from Kyoo.Tests/Library/SpecificTests/CollectionsTests.cs rename to Kyoo.Tests/Database/SpecificTests/CollectionsTests.cs diff --git a/Kyoo.Tests/Library/SpecificTests/EpisodeTests.cs b/Kyoo.Tests/Database/SpecificTests/EpisodeTests.cs similarity index 100% rename from Kyoo.Tests/Library/SpecificTests/EpisodeTests.cs rename to Kyoo.Tests/Database/SpecificTests/EpisodeTests.cs diff --git a/Kyoo.Tests/Library/SpecificTests/GenreTests.cs b/Kyoo.Tests/Database/SpecificTests/GenreTests.cs similarity index 100% rename from Kyoo.Tests/Library/SpecificTests/GenreTests.cs rename to Kyoo.Tests/Database/SpecificTests/GenreTests.cs diff --git a/Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs b/Kyoo.Tests/Database/SpecificTests/LibraryItemTest.cs similarity index 100% rename from Kyoo.Tests/Library/SpecificTests/LibraryItemTest.cs rename to Kyoo.Tests/Database/SpecificTests/LibraryItemTest.cs diff --git a/Kyoo.Tests/Library/SpecificTests/LibraryTests.cs b/Kyoo.Tests/Database/SpecificTests/LibraryTests.cs similarity index 100% rename from Kyoo.Tests/Library/SpecificTests/LibraryTests.cs rename to Kyoo.Tests/Database/SpecificTests/LibraryTests.cs diff --git a/Kyoo.Tests/Library/SpecificTests/PeopleTests.cs b/Kyoo.Tests/Database/SpecificTests/PeopleTests.cs similarity index 100% rename from Kyoo.Tests/Library/SpecificTests/PeopleTests.cs rename to Kyoo.Tests/Database/SpecificTests/PeopleTests.cs diff --git a/Kyoo.Tests/Library/SpecificTests/ProviderTests.cs b/Kyoo.Tests/Database/SpecificTests/ProviderTests.cs similarity index 100% rename from Kyoo.Tests/Library/SpecificTests/ProviderTests.cs rename to Kyoo.Tests/Database/SpecificTests/ProviderTests.cs diff --git a/Kyoo.Tests/Library/SpecificTests/SanityTests.cs b/Kyoo.Tests/Database/SpecificTests/SanityTests.cs similarity index 100% rename from Kyoo.Tests/Library/SpecificTests/SanityTests.cs rename to Kyoo.Tests/Database/SpecificTests/SanityTests.cs diff --git a/Kyoo.Tests/Library/SpecificTests/SeasonTests.cs b/Kyoo.Tests/Database/SpecificTests/SeasonTests.cs similarity index 100% rename from Kyoo.Tests/Library/SpecificTests/SeasonTests.cs rename to Kyoo.Tests/Database/SpecificTests/SeasonTests.cs diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Database/SpecificTests/ShowTests.cs similarity index 100% rename from Kyoo.Tests/Library/SpecificTests/ShowTests.cs rename to Kyoo.Tests/Database/SpecificTests/ShowTests.cs diff --git a/Kyoo.Tests/Library/SpecificTests/StudioTests.cs b/Kyoo.Tests/Database/SpecificTests/StudioTests.cs similarity index 100% rename from Kyoo.Tests/Library/SpecificTests/StudioTests.cs rename to Kyoo.Tests/Database/SpecificTests/StudioTests.cs diff --git a/Kyoo.Tests/Library/SpecificTests/TrackTests.cs b/Kyoo.Tests/Database/SpecificTests/TrackTests.cs similarity index 100% rename from Kyoo.Tests/Library/SpecificTests/TrackTests.cs rename to Kyoo.Tests/Database/SpecificTests/TrackTests.cs diff --git a/Kyoo.Tests/Library/SpecificTests/UserTests.cs b/Kyoo.Tests/Database/SpecificTests/UserTests.cs similarity index 100% rename from Kyoo.Tests/Library/SpecificTests/UserTests.cs rename to Kyoo.Tests/Database/SpecificTests/UserTests.cs diff --git a/Kyoo.Tests/Library/TestContext.cs b/Kyoo.Tests/Database/TestContext.cs similarity index 100% rename from Kyoo.Tests/Library/TestContext.cs rename to Kyoo.Tests/Database/TestContext.cs diff --git a/Kyoo.Tests/Library/TestSample.cs b/Kyoo.Tests/Database/TestSample.cs similarity index 100% rename from Kyoo.Tests/Library/TestSample.cs rename to Kyoo.Tests/Database/TestSample.cs diff --git a/Kyoo/Controllers/FileSystems/FileSystemComposite.cs b/Kyoo/Controllers/FileSystems/FileSystemComposite.cs new file mode 100644 index 00000000..f2f87a02 --- /dev/null +++ b/Kyoo/Controllers/FileSystems/FileSystemComposite.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Autofac.Features.Metadata; +using JetBrains.Annotations; +using Kyoo.Common.Models.Attributes; +using Kyoo.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Kyoo.Controllers +{ + /// + /// A composite that merge every available + /// using . + /// + public class FileSystemComposite : IFileSystem + { + /// + /// The list of mapped to their metadata. + /// + private readonly ICollection, FileSystemMetadataAttribute>> _fileSystems; + + /// + /// Create a new from a list of mapped to their + /// metadata. + /// + /// The list of filesystem mapped to their metadata. + public FileSystemComposite(ICollection, FileSystemMetadataAttribute>> fileSystems) + { + _fileSystems = fileSystems; + } + + + /// + /// Retrieve the file system that should be used for a given path. + /// + /// + /// The path that was requested. + /// + /// + /// The path that the returned file system wants + /// (respecting ). + /// + /// No file system was registered for the given path. + /// The file system that should be used for a given path + [NotNull] + private IFileSystem _GetFileSystemForPath([NotNull] string path, [NotNull] out string usablePath) + { + Regex schemeMatcher = new(@"(.+)://(.*)", RegexOptions.Compiled); + Match match = schemeMatcher.Match(path); + + if (!match.Success) + { + usablePath = path; + Meta, FileSystemMetadataAttribute> defaultFs = _fileSystems + .SingleOrDefault(x => x.Metadata.Scheme.Contains("")); + if (defaultFs == null) + throw new ArgumentException($"No file system registered for the default scheme."); + return defaultFs.Value.Invoke(); + } + string scheme = match.Groups[1].Value; + Meta, FileSystemMetadataAttribute> ret = _fileSystems + .SingleOrDefault(x => x.Metadata.Scheme.Contains(scheme)); + if (ret == null) + throw new ArgumentException($"No file system registered for the scheme: {scheme}."); + usablePath = ret.Metadata.StripScheme ? match.Groups[2].Value : path; + return ret.Value.Invoke(); + } + + /// + public IActionResult FileResult(string path, bool rangeSupport = false, string type = null) + { + if (path == null) + return new NotFoundResult(); + return _GetFileSystemForPath(path, out string relativePath) + .FileResult(relativePath); + } + + /// + public Task GetReader(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + return _GetFileSystemForPath(path, out string relativePath) + .GetReader(relativePath); + } + + /// + public Task NewFile(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + return _GetFileSystemForPath(path, out string relativePath) + .NewFile(relativePath); + } + + /// + public Task CreateDirectory(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + return _GetFileSystemForPath(path, out string relativePath) + .CreateDirectory(relativePath); + } + + /// + public string Combine(params string[] paths) + { + return _GetFileSystemForPath(paths[0], out string relativePath) + .Combine(paths[1..].Prepend(relativePath).ToArray()); + } + + /// + public Task> ListFiles(string path, SearchOption options = SearchOption.TopDirectoryOnly) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + return _GetFileSystemForPath(path, out string relativePath) + .ListFiles(relativePath, options); + } + + /// + public Task Exists(string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + return _GetFileSystemForPath(path, out string relativePath) + .Exists(relativePath); + } + + /// + public string GetExtraDirectory(Show show) + { + if (show == null) + throw new ArgumentNullException(nameof(show)); + return _GetFileSystemForPath(show.Path, out string _) + .GetExtraDirectory(show); + } + } +} \ No newline at end of file diff --git a/Kyoo/Controllers/FileSystems/HttpFileSystem.cs b/Kyoo/Controllers/FileSystems/HttpFileSystem.cs new file mode 100644 index 00000000..3d2ea991 --- /dev/null +++ b/Kyoo/Controllers/FileSystems/HttpFileSystem.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Kyoo.Common.Models.Attributes; +using Kyoo.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Kyoo.Controllers +{ + /// + /// A for http/https links. + /// + [FileSystemMetadata(new [] {"http", "https"})] + public class HttpFileSystem : IFileSystem + { + /// + /// The http client factory used to create clients. + /// + private readonly IHttpClientFactory _clientFactory; + + /// + /// Create a using the given client factory. + /// + /// The http client factory used to create clients. + public HttpFileSystem(IHttpClientFactory factory) + { + _clientFactory = factory; + } + + + /// + public IActionResult FileResult(string path, bool rangeSupport = false, string type = null) + { + if (path == null) + return new NotFoundResult(); + return new HttpForwardResult(new Uri(path), rangeSupport, type); + } + + /// + public Task GetReader(string path) + { + HttpClient client = _clientFactory.CreateClient(); + return client.GetStreamAsync(path); + } + + /// + public Task NewFile(string path) + { + throw new NotSupportedException("An http filesystem is readonly, a new file can't be created."); + } + + /// + public Task CreateDirectory(string path) + { + throw new NotSupportedException("An http filesystem is readonly, a directory can't be created."); + } + + /// + public string Combine(params string[] paths) + { + return Path.Combine(paths); + } + + /// + public Task> ListFiles(string path, SearchOption options = SearchOption.TopDirectoryOnly) + { + throw new NotSupportedException("Listing files is not supported on an http filesystem."); + } + + /// + public Task Exists(string path) + { + throw new NotSupportedException("Checking if a file exists is not supported on an http filesystem."); + } + + /// + public string GetExtraDirectory(Show show) + { + throw new NotSupportedException("Extras can not be stored inside an http filesystem."); + } + } + + /// + /// An to proxy an http request. + /// + public class HttpForwardResult : IActionResult + { + /// + /// The path of the request to forward. + /// + private readonly Uri _path; + /// + /// Should the proxied result support ranges requests? + /// + private readonly bool _rangeSupport; + /// + /// If not null, override the content type of the resulting request. + /// + private readonly string _type; + + /// + /// Create a new . + /// + /// The path of the request to forward. + /// Should the proxied result support ranges requests? + /// If not null, override the content type of the resulting request. + public HttpForwardResult(Uri path, bool rangeSupport, string type = null) + { + _path = path; + _rangeSupport = rangeSupport; + _type = type; + } + + /// + public Task ExecuteResultAsync(ActionContext context) + { + // TODO implement that, example: https://github.com/twitchax/AspNetCore.Proxy/blob/14dd0f212d7abb43ca1bf8c890d5efb95db66acb/src/Core/Extensions/Http.cs#L15 + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Kyoo/Controllers/FileManager.cs b/Kyoo/Controllers/FileSystems/LocalFileSystem.cs similarity index 77% rename from Kyoo/Controllers/FileManager.cs rename to Kyoo/Controllers/FileSystems/LocalFileSystem.cs index 4aa89467..dc1c4b9d 100644 --- a/Kyoo/Controllers/FileManager.cs +++ b/Kyoo/Controllers/FileSystems/LocalFileSystem.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using Kyoo.Common.Models.Attributes; using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; @@ -9,9 +10,10 @@ using Microsoft.AspNetCore.StaticFiles; namespace Kyoo.Controllers { /// - /// A for the local filesystem (using System.IO). + /// A for the local filesystem (using System.IO). /// - public class FileManager : IFileManager + [FileSystemMetadata(new [] {"", "file"}, StripScheme = true)] + public class LocalFileSystem : IFileSystem { /// /// An extension provider to get content types from files extensions. @@ -54,19 +56,19 @@ namespace Kyoo.Controllers } /// - public Stream GetReader(string path) + public Task GetReader(string path) { if (path == null) throw new ArgumentNullException(nameof(path)); - return File.OpenRead(path); + return Task.FromResult(File.OpenRead(path)); } /// - public Stream NewFile(string path) + public Task NewFile(string path) { if (path == null) throw new ArgumentNullException(nameof(path)); - return File.Create(path); + return Task.FromResult(File.Create(path)); } /// @@ -108,24 +110,5 @@ namespace Kyoo.Controllers Directory.CreateDirectory(path); return path; } - - /// - public string GetExtraDirectory(Season season) - { - if (season.Show == null) - throw new NotImplementedException("Can't get season's extra directory when season.Show == null."); - // TODO use a season.Path here. - string path = Path.Combine(season.Show.Path, "Extra"); - Directory.CreateDirectory(path); - return path; - } - - /// - public string GetExtraDirectory(Episode episode) - { - string path = Path.Combine(Path.GetDirectoryName(episode.Path)!, "Extra"); - Directory.CreateDirectory(path); - return path; - } } } \ No newline at end of file diff --git a/Kyoo/Controllers/ThumbnailsManager.cs b/Kyoo/Controllers/ThumbnailsManager.cs index f76123ee..389891b3 100644 --- a/Kyoo/Controllers/ThumbnailsManager.cs +++ b/Kyoo/Controllers/ThumbnailsManager.cs @@ -17,7 +17,7 @@ namespace Kyoo.Controllers /// /// The file manager used to download the image if the file is distant /// - private readonly IFileManager _files; + private readonly IFileSystem _files; /// /// A logger to report errors. /// @@ -26,6 +26,10 @@ namespace Kyoo.Controllers /// The options containing the base path of people images and provider logos. /// private readonly IOptionsMonitor _options; + /// + /// A library manager used to load episode and seasons shows if they are not loaded. + /// + private readonly Lazy _library; /// /// Create a new . @@ -33,13 +37,16 @@ namespace Kyoo.Controllers /// The file manager to use. /// A logger to report errors /// The options to use. - public ThumbnailsManager(IFileManager files, + /// A library manager used to load shows if they are not loaded. + public ThumbnailsManager(IFileSystem files, ILogger logger, - IOptionsMonitor options) + IOptionsMonitor options, + Lazy library) { _files = files; _logger = logger; _options = options; + _library = library; options.OnChange(x => { @@ -66,7 +73,7 @@ namespace Kyoo.Controllers } /// - /// An helper function to download an image using a . + /// An helper function to download an image using a . /// /// The distant url of the image /// The local path of the image @@ -79,8 +86,8 @@ namespace Kyoo.Controllers try { - await using Stream reader = _files.GetReader(url); - await using Stream local = _files.NewFile(localPath); + await using Stream reader = await _files.GetReader(url); + await using Stream local = await _files.NewFile(localPath); await reader.CopyToAsync(local); return true; } @@ -185,7 +192,7 @@ namespace Kyoo.Controllers if (episode.Thumb == null) return false; - string localPath = await GetEpisodeThumb(episode); + string localPath = await _GetEpisodeThumb(episode); if (alwaysDownload || !await _files.Exists(localPath)) return await _DownloadImage(episode.Thumb, localPath, $"The thumbnail of {episode.Slug}"); return false; @@ -218,13 +225,25 @@ namespace Kyoo.Controllers { if (item == null) throw new ArgumentNullException(nameof(item)); - return Task.FromResult(item switch + return item switch { - Show show => _files.Combine(_files.GetExtraDirectory(show), "poster.jpg"), - Season season => _files.Combine(_files.GetExtraDirectory(season), $"season-{season.SeasonNumber}.jpg"), - People people => _files.Combine(_options.CurrentValue.PeoplePath, $"{people.Slug}.jpg"), + Show show => Task.FromResult(_files.Combine(_files.GetExtraDirectory(show), "poster.jpg")), + Season season => _GetSeasonPoster(season), + People actor => Task.FromResult(_files.Combine(_options.CurrentValue.PeoplePath, $"{actor.Slug}.jpg")), _ => throw new NotSupportedException($"The type {typeof(T).Name} does not have a poster.") - }); + }; + } + + /// + /// Retrieve the path of a season's poster. + /// + /// The season to retrieve the poster from. + /// The path of the season's poster. + private async Task _GetSeasonPoster(Season season) + { + if (season.Show == null) + await _library.Value.Load(season, x => x.Show); + return _files.Combine(_files.GetExtraDirectory(season.Show), $"season-{season.SeasonNumber}.jpg"); } /// @@ -236,14 +255,21 @@ namespace Kyoo.Controllers return item switch { Show show => Task.FromResult(_files.Combine(_files.GetExtraDirectory(show), "backdrop.jpg")), - Episode episode => GetEpisodeThumb(episode), + Episode episode => _GetEpisodeThumb(episode), _ => throw new NotSupportedException($"The type {typeof(T).Name} does not have a thumbnail.") }; } - private async Task GetEpisodeThumb(Episode episode) + /// + /// Get the path for an episode's thumbnail. + /// + /// The episode to retrieve the thumbnail from + /// The path of the given episode's thumbnail. + private async Task _GetEpisodeThumb(Episode episode) { - string dir = _files.Combine(_files.GetExtraDirectory(episode), "Thumbnails"); + if (episode.Show == null) + await _library.Value.Load(episode, x => x.Show); + string dir = _files.Combine(_files.GetExtraDirectory(episode.Show), "Thumbnails"); await _files.CreateDirectory(dir); return _files.Combine(dir, $"{Path.GetFileNameWithoutExtension(episode.Path)}.jpg"); } diff --git a/Kyoo/Controllers/Transcoder.cs b/Kyoo/Controllers/Transcoder.cs index 8f6e006e..47291753 100644 --- a/Kyoo/Controllers/Transcoder.cs +++ b/Kyoo/Controllers/Transcoder.cs @@ -72,24 +72,29 @@ namespace Kyoo.Controllers } } - private readonly IFileManager _files; + private readonly IFileSystem _files; private readonly IOptions _options; + private readonly Lazy _library; - public Transcoder(IFileManager files, IOptions options) + public Transcoder(IFileSystem files, IOptions options, Lazy library) { _files = files; _options = options; + _library = library; if (TranscoderAPI.init() != Marshal.SizeOf()) throw new BadTranscoderException(); } - public Task ExtractInfos(Episode episode, bool reextract) + public async Task ExtractInfos(Episode episode, bool reextract) { - string dir = _files.GetExtraDirectory(episode); + if (episode.Show == null) + await _library.Value.Load(episode, x => x.Show); + + string dir = _files.GetExtraDirectory(episode.Show); if (dir == null) throw new ArgumentException("Invalid path."); - return Task.Factory.StartNew( + return await Task.Factory.StartNew( () => TranscoderAPI.ExtractInfos(episode.Path, dir, reextract), TaskCreationOptions.LongRunning); } diff --git a/Kyoo/CoreModule.cs b/Kyoo/CoreModule.cs index 1632644f..2f5c7cfb 100644 --- a/Kyoo/CoreModule.cs +++ b/Kyoo/CoreModule.cs @@ -34,7 +34,7 @@ namespace Kyoo /// public ICollection Provides => new[] { - typeof(IFileManager), + typeof(IFileSystem), typeof(ITranscoder), typeof(IThumbnailsManager), typeof(IMetadataProvider), @@ -101,13 +101,17 @@ namespace Kyoo /// public void Configure(ContainerBuilder builder) { + builder.RegisterComposite(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().SingleInstance(); + builder.RegisterComposite(); builder.Register(x => (AProviderComposite)x.Resolve()); diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index b1bad964..205b8458 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -36,6 +36,7 @@ + diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 04a24408..59a14f7d 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -1,6 +1,7 @@ using System; using System.IO; using Autofac; +using Autofac.Extras.AttributeMetadata; using Kyoo.Authentication; using Kyoo.Controllers; using Kyoo.Models.Options; @@ -76,6 +77,7 @@ namespace Kyoo public void ConfigureContainer(ContainerBuilder builder) { + builder.RegisterModule(); builder.RegisterInstance(_plugins).As().ExternallyOwned(); builder.RegisterTask(); _plugins.ConfigureContainer(builder); diff --git a/Kyoo/Tasks/Crawler.cs b/Kyoo/Tasks/Crawler.cs index 0d38e34e..43567bec 100644 --- a/Kyoo/Tasks/Crawler.cs +++ b/Kyoo/Tasks/Crawler.cs @@ -46,7 +46,7 @@ namespace Kyoo.Tasks /// /// The file manager used walk inside directories and check they existences. /// - [Injected] public IFileManager FileManager { private get; set; } + [Injected] public IFileSystem FileSystem { private get; set; } /// /// A task manager used to create sub tasks for each episode to add to the database. /// @@ -111,7 +111,7 @@ namespace Kyoo.Tasks Logger.LogInformation("Scanning library {Library} at {Paths}", library.Name, library.Paths); foreach (string path in library.Paths) { - ICollection files = await FileManager.ListFiles(path, SearchOption.AllDirectories); + ICollection files = await FileSystem.ListFiles(path, SearchOption.AllDirectories); if (cancellationToken.IsCancellationRequested) return; diff --git a/Kyoo/Tasks/Housekeeping.cs b/Kyoo/Tasks/Housekeeping.cs index 03084aaa..1d7789aa 100644 --- a/Kyoo/Tasks/Housekeeping.cs +++ b/Kyoo/Tasks/Housekeeping.cs @@ -39,7 +39,7 @@ namespace Kyoo.Tasks /// /// The file manager used walk inside directories and check they existences. /// - [Injected] public IFileManager FileManager { private get; set; } + [Injected] public IFileSystem FileSystem { private get; set; } /// /// The logger used to inform the user that episodes has been removed. /// @@ -58,7 +58,7 @@ namespace Kyoo.Tasks progress.Report(count / delCount * 100); count++; - if (await FileManager.Exists(show.Path)) + if (await FileSystem.Exists(show.Path)) continue; Logger.LogWarning("Show {Name}'s folder has been deleted (was {Path}), removing it from kyoo", show.Title, show.Path); @@ -70,7 +70,7 @@ namespace Kyoo.Tasks progress.Report(count / delCount * 100); count++; - if (await FileManager.Exists(episode.Path)) + if (await FileSystem.Exists(episode.Path)) continue; Logger.LogWarning("Episode {Slug}'s file has been deleted (was {Path}), removing it from kyoo", episode.Slug, episode.Path); diff --git a/Kyoo/Views/EpisodeApi.cs b/Kyoo/Views/EpisodeApi.cs index a0d92ff8..b7d83e27 100644 --- a/Kyoo/Views/EpisodeApi.cs +++ b/Kyoo/Views/EpisodeApi.cs @@ -21,11 +21,11 @@ namespace Kyoo.Api { private readonly ILibraryManager _libraryManager; private readonly IThumbnailsManager _thumbnails; - private readonly IFileManager _files; + private readonly IFileSystem _files; public EpisodeApi(ILibraryManager libraryManager, IOptions options, - IFileManager files, + IFileSystem files, IThumbnailsManager thumbnails) : base(libraryManager.EpisodeRepository, options.Value.PublicUrl) { diff --git a/Kyoo/Views/PeopleApi.cs b/Kyoo/Views/PeopleApi.cs index 6a468272..bb757a60 100644 --- a/Kyoo/Views/PeopleApi.cs +++ b/Kyoo/Views/PeopleApi.cs @@ -18,12 +18,12 @@ namespace Kyoo.Api public class PeopleApi : CrudApi { private readonly ILibraryManager _libraryManager; - private readonly IFileManager _files; + private readonly IFileSystem _files; private readonly IThumbnailsManager _thumbs; public PeopleApi(ILibraryManager libraryManager, IOptions options, - IFileManager files, + IFileSystem files, IThumbnailsManager thumbs) : base(libraryManager.PeopleRepository, options.Value.PublicUrl) { diff --git a/Kyoo/Views/ProviderApi.cs b/Kyoo/Views/ProviderApi.cs index ede70dec..026b79ef 100644 --- a/Kyoo/Views/ProviderApi.cs +++ b/Kyoo/Views/ProviderApi.cs @@ -17,11 +17,11 @@ namespace Kyoo.Api { private readonly IThumbnailsManager _thumbnails; private readonly ILibraryManager _libraryManager; - private readonly IFileManager _files; + private readonly IFileSystem _files; public ProviderApi(ILibraryManager libraryManager, IOptions options, - IFileManager files, + IFileSystem files, IThumbnailsManager thumbnails) : base(libraryManager.ProviderRepository, options.Value.PublicUrl) { diff --git a/Kyoo/Views/SeasonApi.cs b/Kyoo/Views/SeasonApi.cs index 85944c6b..8d08dd0c 100644 --- a/Kyoo/Views/SeasonApi.cs +++ b/Kyoo/Views/SeasonApi.cs @@ -20,12 +20,12 @@ namespace Kyoo.Api { private readonly ILibraryManager _libraryManager; private readonly IThumbnailsManager _thumbs; - private readonly IFileManager _files; + private readonly IFileSystem _files; public SeasonApi(ILibraryManager libraryManager, IOptions options, IThumbnailsManager thumbs, - IFileManager files) + IFileSystem files) : base(libraryManager.SeasonRepository, options.Value.PublicUrl) { _libraryManager = libraryManager; diff --git a/Kyoo/Views/ShowApi.cs b/Kyoo/Views/ShowApi.cs index 73822a9c..04854849 100644 --- a/Kyoo/Views/ShowApi.cs +++ b/Kyoo/Views/ShowApi.cs @@ -23,11 +23,11 @@ namespace Kyoo.Api public class ShowApi : CrudApi { private readonly ILibraryManager _libraryManager; - private readonly IFileManager _files; + private readonly IFileSystem _files; private readonly IThumbnailsManager _thumbs; public ShowApi(ILibraryManager libraryManager, - IFileManager files, + IFileSystem files, IThumbnailsManager thumbs, IOptions options) : base(libraryManager.ShowRepository, options.Value.PublicUrl) diff --git a/Kyoo/Views/SubtitleApi.cs b/Kyoo/Views/SubtitleApi.cs index 5426078d..89d036aa 100644 --- a/Kyoo/Views/SubtitleApi.cs +++ b/Kyoo/Views/SubtitleApi.cs @@ -14,9 +14,9 @@ namespace Kyoo.Api public class SubtitleApi : ControllerBase { private readonly ILibraryManager _libraryManager; - private readonly IFileManager _files; + private readonly IFileSystem _files; - public SubtitleApi(ILibraryManager libraryManager, IFileManager files) + public SubtitleApi(ILibraryManager libraryManager, IFileSystem files) { _libraryManager = libraryManager; _files = files; @@ -71,9 +71,9 @@ namespace Kyoo.Api public class ConvertSubripToVtt : IActionResult { private readonly string _path; - private readonly IFileManager _files; + private readonly IFileSystem _files; - public ConvertSubripToVtt(string subtitlePath, IFileManager files) + public ConvertSubripToVtt(string subtitlePath, IFileSystem files) { _path = subtitlePath; _files = files; @@ -92,7 +92,7 @@ namespace Kyoo.Api await writer.WriteLineAsync(""); await writer.WriteLineAsync(""); - using StreamReader reader = new(_files.GetReader(_path)); + using StreamReader reader = new(await _files.GetReader(_path)); string line; while ((line = await reader.ReadLineAsync()) != null) { diff --git a/Kyoo/Views/VideoApi.cs b/Kyoo/Views/VideoApi.cs index 5c48f7bc..cbc2937b 100644 --- a/Kyoo/Views/VideoApi.cs +++ b/Kyoo/Views/VideoApi.cs @@ -18,12 +18,12 @@ namespace Kyoo.Api private readonly ILibraryManager _libraryManager; private readonly ITranscoder _transcoder; private readonly IOptions _options; - private readonly IFileManager _files; + private readonly IFileSystem _files; public VideoApi(ILibraryManager libraryManager, ITranscoder transcoder, IOptions options, - IFileManager files) + IFileSystem files) { _libraryManager = libraryManager; _transcoder = transcoder;