diff --git a/ShowPics.Data.Abstractions/IFilesData.cs b/ShowPics.Data.Abstractions/IFilesData.cs index 0108a45..d8a0baf 100644 --- a/ShowPics.Data.Abstractions/IFilesData.cs +++ b/ShowPics.Data.Abstractions/IFilesData.cs @@ -11,6 +11,8 @@ public interface IFilesData { Task> GetAllAsync(CancellationToken cancellationToken = default(CancellationToken)); ICollection GetAll(); + ICollection GetTopLevelFolders(int foldersDepth, int filesDepth); + Folder GetFolder(string logicalPath, int foldersDepth, int filesDepth); void Remove(File file); void Remove(Folder folder); void Add(File file); diff --git a/ShowPics.Data/FilesData.cs b/ShowPics.Data/FilesData.cs index 5676261..a7d197a 100644 --- a/ShowPics.Data/FilesData.cs +++ b/ShowPics.Data/FilesData.cs @@ -6,6 +6,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using System.Linq; +using Microsoft.EntityFrameworkCore.Query; + namespace ShowPics.Data { public class FilesData : IFilesData @@ -42,6 +45,72 @@ public ICollection GetAll() return _dbContext.Folders.Include(x => x.Files).ToListAsync().Result; } + public ICollection GetTopLevelFolders(int foldersDepth, int filesDepth) + { + if (foldersDepth < 0 || foldersDepth > 2) + throw new ArgumentOutOfRangeException(nameof(foldersDepth), "Argument must be between 0 and 2"); + if (filesDepth < 0 || filesDepth > 2) + throw new ArgumentOutOfRangeException(nameof(filesDepth), "Argument must be between 0 and 2"); + if (filesDepth > foldersDepth + 1) + throw new ArgumentOutOfRangeException(nameof(filesDepth), $"Argument must not be greater than {nameof(foldersDepth)} + 1"); + + IQueryable foldersQueryable = _dbContext.Folders; + if (foldersDepth > 0) + { + var includable = foldersQueryable.Include(x => x.Children); + if (foldersDepth > 1) + { + includable = includable.ThenInclude(x => x.Children); + } + foldersQueryable = includable; + } + if (filesDepth > 0) + { + foldersQueryable = foldersQueryable.Include(x => x.Files); + + if (filesDepth > 1) + { + foldersQueryable = foldersQueryable.Include(x => x.Children).ThenInclude(x => x.Files); + } + } + + var result = foldersQueryable.Where(x => x.ParentId == null).ToListAsync().Result; + return result; + } + + public Folder GetFolder(string logicalPath, int foldersDepth, int filesDepth) + { + if (foldersDepth < 0 || foldersDepth > 2) + throw new ArgumentOutOfRangeException(nameof(foldersDepth), "Argument must be between 0 and 2"); + if (filesDepth < 0 || filesDepth > 2) + throw new ArgumentOutOfRangeException(nameof(filesDepth), "Argument must be between 0 and 2"); + if (filesDepth > foldersDepth + 1) + throw new ArgumentOutOfRangeException(nameof(filesDepth), $"Argument must not be greater than {nameof(foldersDepth)} + 1"); + + IQueryable foldersQueryable = _dbContext.Folders; + if (foldersDepth > 0) + { + var includable = foldersQueryable.Include(x => x.Children); + if (foldersDepth > 1) + { + includable = includable.ThenInclude(x => x.Children); + } + foldersQueryable = includable; + } + if (filesDepth > 0) + { + foldersQueryable = foldersQueryable.Include(x => x.Files); + + if (filesDepth > 1) + { + foldersQueryable = foldersQueryable.Include(x => x.Children).ThenInclude(x => x.Files); + } + } + + var result = foldersQueryable.SingleOrDefaultAsync(x => x.Path == logicalPath).Result; + return result; + } + public async Task> GetAllAsync(CancellationToken cancellationToken = default(CancellationToken)) { return await _dbContext.Folders.Include(x => x.Files).ToListAsync(cancellationToken); diff --git a/ShowPics.Dtos/DirectoryDto.cs b/ShowPics.Dtos/DirectoryDto.cs index c627ef9..a636aae 100644 --- a/ShowPics.Dtos/DirectoryDto.cs +++ b/ShowPics.Dtos/DirectoryDto.cs @@ -8,6 +8,7 @@ namespace ShowPics.Dtos public class DirectoryDto : FileSystemObject { [JsonProperty(Order = 0)] - public IList Children { get; set; } = new List(); + public IList Children { get; set; } + public bool HasSubdirectories { get; set; } } } diff --git a/ShowPics.Dtos/FileSystemObject.cs b/ShowPics.Dtos/FileSystemObject.cs index 7d5e97f..e456e7c 100644 --- a/ShowPics.Dtos/FileSystemObject.cs +++ b/ShowPics.Dtos/FileSystemObject.cs @@ -8,6 +8,7 @@ public abstract class FileSystemObject { public string Type => GetType().Name; public virtual string Path { get; set; } + public virtual string ApiPath { get; set; } public virtual string Name { get; set; } } } diff --git a/ShowPics.Utilities/PathHelper.cs b/ShowPics.Utilities/PathHelper.cs index 90e567d..c5dd7f9 100644 --- a/ShowPics.Utilities/PathHelper.cs +++ b/ShowPics.Utilities/PathHelper.cs @@ -55,6 +55,16 @@ public string GetThumbnailPath(string originalPath, bool isFile) throw new Exception($"Could not map original path '{originalPath}' to thumbnail path."); } + public string GetApiPath(string originalPath) + { + if ((originalPath + "/").StartsWith(_folderSettings.Value.OriginalsLogicalPrefix + "/")) + { + var path = JoinLogicalPaths("api/Files", originalPath.Substring(_folderSettings.Value.OriginalsLogicalPrefix.Length).TrimStart('/')); + return path; + } + throw new Exception($"Could not map original path '{originalPath}' to thumbnail path."); + } + public string GetParentPath(string logicalPath) { var split = logicalPath.Split('/', options: StringSplitOptions.RemoveEmptyEntries); diff --git a/ShowPics/ClientApp/src/app/media-browser/file-service-dtos.ts b/ShowPics/ClientApp/src/app/media-browser/file-service-dtos.ts index fe8da5b..d9f4cb5 100644 --- a/ShowPics/ClientApp/src/app/media-browser/file-service-dtos.ts +++ b/ShowPics/ClientApp/src/app/media-browser/file-service-dtos.ts @@ -1,13 +1,15 @@ export class FileSystemObject { type: string; path: string; + apiPath: string; thumbnailPath: string; name: string; children: FileSystemObject[]; contentType: string; width: number; height: number; - subfolders: FileSystemObject[]; + hasSubdirectories: boolean; + subdirectories: FileSystemObject[]; } export class FileSystemObjectTypes { diff --git a/ShowPics/ClientApp/src/app/media-browser/file.service.ts b/ShowPics/ClientApp/src/app/media-browser/file.service.ts index f747f49..2b9b5be 100644 --- a/ShowPics/ClientApp/src/app/media-browser/file.service.ts +++ b/ShowPics/ClientApp/src/app/media-browser/file.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { FileSystemObject } from './file-service-dtos'; @@ -7,7 +7,7 @@ import { FileSystemObject } from './file-service-dtos'; @Injectable() export class FileService { - private filesUrl = 'api/Files'; // URL to web api + public static readonly rootUrl = '/api/Files'; // URL to web api private static handleError(error: any): Promise { console.error('An error occurred', error); // for demo purposes only @@ -17,14 +17,14 @@ export class FileService { constructor(private http: HttpClient) { } - getFiles(): Observable { - return this.http.get(this.filesUrl) + getFiles(path: string, depth: number): Observable { + return this.http.get(path, { + params: { + depth: depth.toString() + } + }) .pipe( catchError(FileService.handleError) ); } - - getUri(path: string) { - return "/" + this.filesUrl + "/" + path; - } } diff --git a/ShowPics/ClientApp/src/app/media-browser/tree/tree.component.ts b/ShowPics/ClientApp/src/app/media-browser/tree/tree.component.ts index 23cc67d..f3a36a4 100644 --- a/ShowPics/ClientApp/src/app/media-browser/tree/tree.component.ts +++ b/ShowPics/ClientApp/src/app/media-browser/tree/tree.component.ts @@ -1,7 +1,9 @@ import { Component, EventEmitter, OnInit, Output } from '@angular/core'; import { FileService } from '../file.service'; import { FileSystemObject, FileSystemObjectTypes } from '../file-service-dtos'; -import { TREE_ACTIONS, KEYS, IActionMapping, ITreeOptions } from 'angular-tree-component'; +import { TREE_ACTIONS, KEYS, IActionMapping, ITreeOptions, TreeNode } from 'angular-tree-component'; +import { pipe, Observable, of } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; @Component({ selector: 'app-tree', @@ -14,32 +16,42 @@ export class TreeComponent implements OnInit { options: ITreeOptions = { idField: 'path', - childrenField: 'subfolders' + getChildren: (treeNode: TreeNode) => this.getChildren(treeNode.data).toPromise(), + hasChildrenField: 'hasSubdirectories', + childrenField: 'subdirectories' } - @Output() onSelected = new EventEmitter(); constructor(private fileService: FileService) { } + getChildren(node: FileSystemObject): Observable { + if (node.children == null) { + return this.fileService.getFiles(node.apiPath, 1) + .pipe( + tap((fso: FileSystemObject) => node.children = fso.children), + map((fso: FileSystemObject) => fso.children.filter(x => x.type === FileSystemObjectTypes.DIRECTORY)) + ); + } + else { + return of(node.children.filter(x => x.type === FileSystemObjectTypes.DIRECTORY)) + } + } + getTree(): void { - this.fileService.getFiles().subscribe(fso => { - this.fillSubfolders(fso); + this.fileService.getFiles(FileService.rootUrl, 1).subscribe(fso => { this.tree = fso.children }); } - fillSubfolders(fso: FileSystemObject) { - fso.subfolders = fso.children.filter(x => x.type === FileSystemObjectTypes.DIRECTORY); - fso.subfolders.forEach(x => this.fillSubfolders(x)); - } - ngOnInit() { this.getTree(); } onSelect(fso: FileSystemObject) { - this.selectedObject = fso; - this.onSelected.emit(fso); + this.getChildren(fso).subscribe(() => { + this.selectedObject = fso; + this.onSelected.emit(fso); + }) } } diff --git a/ShowPics/Controllers/FilesController.cs b/ShowPics/Controllers/FilesController.cs index b13a340..f17a080 100644 --- a/ShowPics/Controllers/FilesController.cs +++ b/ShowPics/Controllers/FilesController.cs @@ -33,24 +33,25 @@ public FilesController(IOptions options, IFilesData filesData, P } [HttpGet("{*path}")] - public ActionResult Get(string path) + public ActionResult Get(string path, int depth = 1) { - var folders = _filesData.GetAll(); - if (string.IsNullOrEmpty(path)) { + var folders = _filesData.GetTopLevelFolders(depth, Math.Max(depth - 1, 0)); return Ok(new DirectoryDto() { Name = "", - Path = "/", - Children = folders.OrderBy(x => x.Name).Where(x => x.ParentId == null).Select(x => MapToDto(x)).ToList() + Path = _pathHelper.PathToUrl(_options.Value.OriginalsLogicalPrefix), + ApiPath = _pathHelper.PathToUrl(_pathHelper.GetApiPath(_options.Value.OriginalsLogicalPrefix)), + HasSubdirectories = folders.Any(), + Children = depth == 0 ? null : folders.OrderBy(x => x.Name).Select(x => MapToDto(x, depth - 1)).ToList() }); } else { - var folder = folders.SingleOrDefault(x => x.Path == _pathHelper.JoinLogicalPaths(_options.Value.OriginalsLogicalPrefix, path)); + var folder = _filesData.GetFolder(_pathHelper.JoinLogicalPaths(_options.Value.OriginalsLogicalPrefix, path), depth + 1, depth); if (folder != null) - return Ok(MapToDto(folder)); + return Ok(MapToDto(folder, depth)); var file = _filesData.GetFile(_pathHelper.JoinLogicalPaths(_options.Value.OriginalsLogicalPrefix, path)); if (file != null) return Ok(MapToDto(file)); @@ -66,19 +67,30 @@ FileSystemObject MapToDto(Entities.File file) ContentType = _mimeTypeMapping.GetValueOrDefault(Path.GetExtension(file.Name)), Name = file.Name, Path = _pathHelper.PathToUrl(file.Path), + ApiPath = _pathHelper.PathToUrl(_pathHelper.GetApiPath(file.Path)), Height = file.Height, Width = file.Width, ThumbnailPath = _pathHelper.PathToUrl(file.ThumbnailPath) }; } - FileSystemObject MapToDto(Entities.Folder folder) + FileSystemObject MapToDto(Entities.Folder folder, int? depth) { return new DirectoryDto() { Path = _pathHelper.PathToUrl(folder.Path), + ApiPath = _pathHelper.PathToUrl(_pathHelper.GetApiPath(folder.Path)), Name = folder.Name, - Children = folder.Children.OrderBy(x => x.Name).Select(MapToDto).Union(folder.Files.OrderBy(x => x.OriginalCreationTime ?? x.ModificationTimestamp).Select(MapToDto)).ToList() + HasSubdirectories = folder.Children.Any(), + Children = depth == 0 + ? null + : folder.Children.OrderBy(x => x.Name) + .Select(x => MapToDto(x, depth - 1)) + .Union( + folder.Files.OrderBy(x => x.OriginalCreationTime ?? x.ModificationTimestamp) + .Select(MapToDto) + ) + .ToList() }; } }