FileSystem: Reworking the extra directory

This commit is contained in:
Zoe Roux 2021-08-04 00:11:18 +02:00
parent 924d03291c
commit 87e1b5aca1
11 changed files with 182 additions and 135 deletions

View File

@ -81,12 +81,13 @@ namespace Kyoo.Controllers
public Task<bool> Exists([NotNull] string path);
/// <summary>
/// Get the extra directory of a show.
/// Get the extra directory of a resource <typeparamref name="T"/>.
/// 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.
/// </summary>
/// <param name="show">The show to proceed</param>
/// <returns>The extra directory of the show</returns>
public string GetExtraDirectory([NotNull] Show show);
/// <param name="resource">The resource to proceed</param>
/// <typeparam name="T">The type of the resource.</typeparam>
/// <returns>The extra directory of the resource.</returns>
public Task<string> GetExtraDirectory<T>([NotNull] T resource);
}
}

View File

@ -225,42 +225,51 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="obj">The source object.</param>
/// <param name="member">A getter function for the member to load</param>
/// <param name="force">
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the source object</typeparam>
/// <typeparam name="T2">The related resource's type</typeparam>
/// <returns>The param <see cref="obj"/></returns>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}})"/>
/// <seealso cref="Load{T}(T, System.String)"/>
/// <seealso cref="Load(IResource, string)"/>
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, T2>> member)
/// <seealso cref="Load{T,T2}(T, System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}}, bool)"/>
/// <seealso cref="Load{T}(T, System.String, bool)"/>
/// <seealso cref="Load(IResource, string, bool)"/>
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, T2>> member, bool force = false)
where T : class, IResource
where T2 : class, IResource, new();
where T2 : class, IResource;
/// <summary>
/// Load a collection of related resource
/// </summary>
/// <param name="obj">The source object.</param>
/// <param name="member">A getter function for the member to load</param>
/// <param name="force">
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the source object</typeparam>
/// <typeparam name="T2">The related resource's type</typeparam>
/// <returns>The param <see cref="obj"/></returns>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,T2}})"/>
/// <seealso cref="Load{T}(T, System.String)"/>
/// <seealso cref="Load(IResource, string)"/>
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, ICollection<T2>>> member)
/// <seealso cref="Load{T,T2}(T, System.Linq.Expressions.Expression{System.Func{T,T2}}, bool)"/>
/// <seealso cref="Load{T}(T, System.String, bool)"/>
/// <seealso cref="Load(IResource, string, bool)"/>
Task<T> Load<T, T2>([NotNull] T obj, Expression<Func<T, ICollection<T2>>> member, bool force = false)
where T : class, IResource
where T2 : class, new();
where T2 : class;
/// <summary>
/// Load a related resource by it's name
/// </summary>
/// <param name="obj">The source object.</param>
/// <param name="memberName">The name of the resource to load (case sensitive)</param>
/// <param name="force">
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the source object</typeparam>
/// <returns>The param <see cref="obj"/></returns>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,T2}})"/>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}})"/>
/// <seealso cref="Load(IResource, string)"/>
Task<T> Load<T>([NotNull] T obj, string memberName)
/// <seealso cref="Load{T,T2}(T, System.Linq.Expressions.Expression{System.Func{T,T2}}, bool)"/>
/// <seealso cref="Load{T,T2}(T, System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}}, bool)"/>
/// <seealso cref="Load(IResource, string, bool)"/>
Task<T> Load<T>([NotNull] T obj, string memberName, bool force = false)
where T : class, IResource;
/// <summary>
@ -268,10 +277,13 @@ namespace Kyoo.Controllers
/// </summary>
/// <param name="obj">The source object.</param>
/// <param name="memberName">The name of the resource to load (case sensitive)</param>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,T2}})"/>
/// <seealso cref="Load{T,T2}(T,System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}})"/>
/// <seealso cref="Load{T}(T, System.String)"/>
Task Load([NotNull] IResource obj, string memberName);
/// <param name="force">
/// <c>true</c> if you want to load the relation even if it is not null, <c>false</c> otherwise.
/// </param>
/// <seealso cref="Load{T,T2}(T, System.Linq.Expressions.Expression{System.Func{T,T2}}, bool)"/>
/// <seealso cref="Load{T,T2}(T, System.Linq.Expressions.Expression{System.Func{T,System.Collections.Generic.ICollection{T2}}}, bool)"/>
/// <seealso cref="Load{T}(T, System.String, bool)"/>
Task Load([NotNull] IResource obj, string memberName, bool force = false);
/// <summary>
/// Get items (A wrapper arround shows or collections) from a library.

View File

@ -162,34 +162,6 @@ namespace Kyoo.Controllers
return await EpisodeRepository.GetOrDefault(showSlug, seasonNumber, episodeNumber);
}
/// <inheritdoc />
public Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member)
where T : class, IResource
where T2 : class, IResource, new()
{
if (member == null)
throw new ArgumentNullException(nameof(member));
return Load(obj, Utility.GetPropertyName(member));
}
/// <inheritdoc />
public Task<T> Load<T, T2>(T obj, Expression<Func<T, ICollection<T2>>> member)
where T : class, IResource
where T2 : class, new()
{
if (member == null)
throw new ArgumentNullException(nameof(member));
return Load(obj, Utility.GetPropertyName(member));
}
/// <inheritdoc />
public async Task<T> Load<T>(T obj, string memberName)
where T : class, IResource
{
await Load(obj as IResource, memberName);
return obj;
}
/// <summary>
/// Set relations between to objects.
/// </summary>
@ -211,11 +183,46 @@ namespace Kyoo.Controllers
}
/// <inheritdoc />
public Task Load(IResource obj, string memberName)
public Task<T> Load<T, T2>(T obj, Expression<Func<T, T2>> member, bool force = false)
where T : class, IResource
where T2 : class, IResource
{
if (member == null)
throw new ArgumentNullException(nameof(member));
return Load(obj, Utility.GetPropertyName(member), force);
}
/// <inheritdoc />
public Task<T> Load<T, T2>(T obj, Expression<Func<T, ICollection<T2>>> member, bool force = false)
where T : class, IResource
where T2 : class
{
if (member == null)
throw new ArgumentNullException(nameof(member));
return Load(obj, Utility.GetPropertyName(member), force);
}
/// <inheritdoc />
public async Task<T> Load<T>(T obj, string memberName, bool force = false)
where T : class, IResource
{
await Load(obj as IResource, memberName, force);
return obj;
}
/// <inheritdoc />
public Task Load(IResource obj, string memberName, bool force = false)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
object existingValue = obj.GetType()
.GetProperties()
.FirstOrDefault(x => string.Equals(x.Name, memberName, StringComparison.InvariantCultureIgnoreCase))
?.GetValue(obj);
if (existingValue != null && !force)
return Task.CompletedTask;
return (obj, member: memberName) switch
{
(Library l, nameof(Library.Providers)) => ProviderRepository

View File

@ -8,7 +8,9 @@ using Autofac.Features.Metadata;
using JetBrains.Annotations;
using Kyoo.Common.Models.Attributes;
using Kyoo.Models;
using Kyoo.Models.Options;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Kyoo.Controllers
{
@ -23,14 +25,31 @@ namespace Kyoo.Controllers
/// </summary>
private readonly ICollection<Meta<Func<IFileSystem>, FileSystemMetadataAttribute>> _fileSystems;
/// <summary>
/// The library manager used to load shows to retrieve their path
/// (only if the option is set to metadata in show)
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Options to check if the metadata should be kept in the show directory or in a kyoo's directory.
/// </summary>
private readonly IOptionsMonitor<BasicOptions> _options;
/// <summary>
/// Create a new <see cref="FileSystemComposite"/> from a list of <see cref="IFileSystem"/> mapped to their
/// metadata.
/// </summary>
/// <param name="fileSystems">The list of filesystem mapped to their metadata.</param>
public FileSystemComposite(ICollection<Meta<Func<IFileSystem>, FileSystemMetadataAttribute>> fileSystems)
/// <param name="libraryManager">The library manager used to load shows to retrieve their path.</param>
/// <param name="options">The options to use.</param>
public FileSystemComposite(ICollection<Meta<Func<IFileSystem>, FileSystemMetadataAttribute>> fileSystems,
ILibraryManager libraryManager,
IOptionsMonitor<BasicOptions> options)
{
_fileSystems = fileSystems;
_libraryManager = libraryManager;
_options = options;
}
@ -132,12 +151,37 @@ namespace Kyoo.Controllers
}
/// <inheritdoc />
public string GetExtraDirectory(Show show)
public async Task<string> GetExtraDirectory<T>(T resource)
{
if (show == null)
throw new ArgumentNullException(nameof(show));
return _GetFileSystemForPath(show.Path, out string _)
.GetExtraDirectory(show);
switch (resource)
{
case Season season:
await _libraryManager.Load(season, x => x.Show);
break;
case Episode episode:
await _libraryManager.Load(episode, x => x.Show);
break;
case Track track:
await _libraryManager.Load(track, x => x.Episode);
await _libraryManager.Load(track.Episode, x => x.Show);
break;
}
IFileSystem fs = resource switch
{
Show show => _GetFileSystemForPath(show.Path, out string _),
Season season => _GetFileSystemForPath(season.Show.Path, out string _),
Episode episode => _GetFileSystemForPath(episode.Show.Path, out string _),
Track track => _GetFileSystemForPath(track.Episode.Show.Path, out string _),
_ => _GetFileSystemForPath(_options.CurrentValue.MetadataPath, out string _)
};
string path = await fs.GetExtraDirectory(resource);
if (resource is IResource res)
path ??= Combine(_options.CurrentValue.MetadataPath, res.Slug, typeof(T).Name);
else
path ??= Combine(_options.CurrentValue.MetadataPath, typeof(T).Name);
await CreateDirectory(path);
return path;
}
}
}

View File

@ -76,7 +76,7 @@ namespace Kyoo.Controllers
}
/// <inheritdoc />
public string GetExtraDirectory(Show show)
public Task<string> GetExtraDirectory<T>(T resource)
{
throw new NotSupportedException("Extras can not be stored inside an http filesystem.");
}
@ -117,7 +117,12 @@ namespace Kyoo.Controllers
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();
// Silence unused warnings
if (_path != null || _rangeSupport || _type == null)
throw new NotImplementedException();
else
throw new NotImplementedException();
}
}
}

View File

@ -4,8 +4,10 @@ using System.IO;
using System.Threading.Tasks;
using Kyoo.Common.Models.Attributes;
using Kyoo.Models;
using Kyoo.Models.Options;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Options;
namespace Kyoo.Controllers
{
@ -20,6 +22,20 @@ namespace Kyoo.Controllers
/// </summary>
private FileExtensionContentTypeProvider _provider;
/// <summary>
/// Options to check if the metadata should be kept in the show directory or in a kyoo's directory.
/// </summary>
private readonly IOptionsMonitor<BasicOptions> _options;
/// <summary>
/// Create a new <see cref="LocalFileSystem"/> with the specified options.
/// </summary>
/// <param name="options">The options to use.</param>
public LocalFileSystem(IOptionsMonitor<BasicOptions> options)
{
_options = options;
}
/// <summary>
/// Get the content type of a file using it's extension.
/// </summary>
@ -104,11 +120,18 @@ namespace Kyoo.Controllers
}
/// <inheritdoc />
public string GetExtraDirectory(Show show)
public Task<string> GetExtraDirectory<T>(T resource)
{
string path = Path.Combine(show.Path, "Extra");
Directory.CreateDirectory(path);
return path;
if (!_options.CurrentValue.MetadataInShow)
return null;
return Task.FromResult(resource switch
{
Show show => Combine(show.Path, "Extra"),
Season season => Combine(season.Show.Path, "Extra", "Season"),
Episode episode => Combine(episode.Show.Path, "Extra", "Episode"),
Track track => Combine(track.Episode.Show.Path, "Track"),
_ => null
});
}
}
}

View File

@ -3,9 +3,7 @@ using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Models.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Kyoo.Controllers
{
@ -22,37 +20,17 @@ namespace Kyoo.Controllers
/// A logger to report errors.
/// </summary>
private readonly ILogger<ThumbnailsManager> _logger;
/// <summary>
/// The options containing the base path of people images and provider logos.
/// </summary>
private readonly IOptionsMonitor<BasicOptions> _options;
/// <summary>
/// A library manager used to load episode and seasons shows if they are not loaded.
/// </summary>
private readonly Lazy<ILibraryManager> _library;
/// <summary>
/// Create a new <see cref="ThumbnailsManager"/>.
/// </summary>
/// <param name="files">The file manager to use.</param>
/// <param name="logger">A logger to report errors</param>
/// <param name="options">The options to use.</param>
/// <param name="library">A library manager used to load shows if they are not loaded.</param>
public ThumbnailsManager(IFileSystem files,
ILogger<ThumbnailsManager> logger,
IOptionsMonitor<BasicOptions> options,
Lazy<ILibraryManager> library)
ILogger<ThumbnailsManager> logger)
{
_files = files;
_logger = logger;
_options = options;
_library = library;
options.OnChange(x =>
{
_files.CreateDirectory(x.PeoplePath);
_files.CreateDirectory(x.ProviderPath);
});
}
/// <summary>
@ -119,35 +97,7 @@ namespace Kyoo.Controllers
_ => $"{imageID}.jpg"
};
// TODO implement a generic way, probably need to rework IFileManager.GetExtraDirectory too.
switch (item)
{
case Show show:
return _files.Combine(_files.GetExtraDirectory(show), imageName);
case Season season:
if (season.Show == null)
await _library.Value.Load(season, x => x.Show);
return _files.Combine(
_files.GetExtraDirectory(season.Show!),
$"season-{season.SeasonNumber}-{imageName}");
case Episode episode:
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)}-{imageName}");
case People actor:
return _files.Combine(_options.CurrentValue.PeoplePath, $"{actor.Slug}-{imageName}");
case Provider provider:
return _files.Combine(_options.CurrentValue.ProviderPath, $"{provider.Slug}-{imageName}");
default:
throw new NotSupportedException($"The type {typeof(T).Name} is not supported.");
}
return _files.Combine(await _files.GetExtraDirectory(item), imageName);
}
}
}

View File

@ -88,10 +88,8 @@ namespace Kyoo.Controllers
public async Task<Track[]> ExtractInfos(Episode episode, bool reextract)
{
if (episode.Show == null)
await _library.Value.Load(episode, x => x.Show);
string dir = _files.GetExtraDirectory(episode.Show);
await _library.Value.Load(episode, x => x.Show);
string dir = await _files.GetExtraDirectory(episode.Show);
if (dir == null)
throw new ArgumentException("Invalid path.");
return await Task.Factory.StartNew(

View File

@ -25,16 +25,6 @@ namespace Kyoo.Models.Options
/// </summary>
public string PluginPath { get; set; } = "plugins/";
/// <summary>
/// The path of the people pictures.
/// </summary>
public string PeoplePath { get; set; } = "people/";
/// <summary>
/// The path of providers icons.
/// </summary>
public string ProviderPath { get; set; } = "providers/";
/// <summary>
/// The temporary folder to cache transmuxed file.
/// </summary>
@ -44,5 +34,22 @@ namespace Kyoo.Models.Options
/// The temporary folder to cache transcoded file.
/// </summary>
public string TranscodePath { get; set; } = "cached/transcode";
/// <summary>
/// <c>true</c> if the metadata of a show/season/episode should be stored in the same directory as video files,
/// <c>false</c> to save them in a kyoo specific directory.
/// </summary>
/// <remarks>
/// Some file systems might discard this option to store them somewhere else.
/// For example, readonly file systems will probably store them in a kyoo specific directory.
/// </remarks>
public bool MetadataInShow { get; set; } = true;
/// <summary>
/// The path for metadata if they are not stored near show (see <see cref="MetadataInShow"/>).
/// Some resources can't be stored near a show and they are stored in this directory
/// (like <see cref="Provider"/>).
/// </summary>
public string MetadataPath { get; set; } = "metadata/";
}
}

View File

@ -383,7 +383,7 @@ namespace Kyoo.Api
try
{
Show show = await _libraryManager.Get<Show>(slug);
string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments");
string path = _files.Combine(await _files.GetExtraDirectory(show), "Attachments");
return (await _files.ListFiles(path))
.ToDictionary(Path.GetFileNameWithoutExtension,
x => $"{BaseURL}api/shows/{slug}/fonts/{Path.GetFileName(x)}");
@ -402,7 +402,7 @@ namespace Kyoo.Api
try
{
Show show = await _libraryManager.Get<Show>(showSlug);
string path = Path.Combine(_files.GetExtraDirectory(show), "Attachments", slug);
string path = _files.Combine(await _files.GetExtraDirectory(show), "Attachments", slug);
return _files.FileResult(path);
}
catch (ItemNotFoundException)

View File

@ -3,10 +3,10 @@
"url": "http://*:5000",
"publicUrl": "http://localhost:5000/",
"pluginsPath": "plugins/",
"peoplePath": "people/",
"providerPath": "providers/",
"transmuxPath": "cached/transmux",
"transcodePath": "cached/transcode"
"transcodePath": "cached/transcode",
"metadataInShow": true,
"metadataPath": "metadata/"
},
"database": {