From d5eed4e85d789c7f0f8bf1858be022fa332875ff Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Thu, 17 Dec 2020 11:27:19 -0600 Subject: [PATCH] Lots of changes to get code ready to add library. --- API/Controllers/LibraryController.cs | 65 +++++++++ API/Controllers/UsersController.cs | 44 +++++- API/DTOs/CreateLibraryDto.cs | 19 +++ API/DTOs/LibraryDto.cs | 13 ++ API/DTOs/MemberDto.cs | 4 + API/Data/DataContext.cs | 1 + API/Data/LibraryRepository.cs | 45 ++++++ .../20201215195007_AddedLibrary.Designer.cs | 128 ++++++++++++++++++ .../Migrations/20201215195007_AddedLibrary.cs | 71 ++++++++++ .../Migrations/DataContextModelSnapshot.cs | 72 ++++++++++ API/Data/UserRepository.cs | 6 +- API/Entities/AppUser.cs | 3 + API/Entities/FolderPath.cs | 10 ++ API/Entities/Library.cs | 16 +++ API/Entities/LibraryType.cs | 16 +++ .../ApplicationServiceExtensions.cs | 2 + API/Helpers/AutoMapperProfiles.cs | 7 +- API/Interfaces/IDirectoryService.cs | 10 ++ API/Interfaces/ILibraryRepository.cs | 20 +++ API/Services/DirectoryService.cs | 21 +++ 20 files changed, 570 insertions(+), 3 deletions(-) create mode 100644 API/Controllers/LibraryController.cs create mode 100644 API/DTOs/CreateLibraryDto.cs create mode 100644 API/DTOs/LibraryDto.cs create mode 100644 API/Data/LibraryRepository.cs create mode 100644 API/Data/Migrations/20201215195007_AddedLibrary.Designer.cs create mode 100644 API/Data/Migrations/20201215195007_AddedLibrary.cs create mode 100644 API/Entities/FolderPath.cs create mode 100644 API/Entities/Library.cs create mode 100644 API/Entities/LibraryType.cs create mode 100644 API/Interfaces/IDirectoryService.cs create mode 100644 API/Interfaces/ILibraryRepository.cs create mode 100644 API/Services/DirectoryService.cs diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs new file mode 100644 index 000000000..45d51894f --- /dev/null +++ b/API/Controllers/LibraryController.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using API.Data; +using API.DTOs; +using API.Entities; +using API.Extensions; +using API.Interfaces; +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace API.Controllers +{ + public class LibraryController : BaseApiController + { + private readonly DataContext _context; + private readonly IDirectoryService _directoryService; + private readonly ILibraryRepository _libraryRepository; + private readonly ILogger _logger; + private readonly IUserRepository _userRepository; + + public LibraryController(DataContext context, IDirectoryService directoryService, + ILibraryRepository libraryRepository, ILogger logger, IUserRepository userRepository) + { + _context = context; + _directoryService = directoryService; + _libraryRepository = libraryRepository; + _logger = logger; + _userRepository = userRepository; + } + + /// + /// Returns a list of directories for a given path. If path is empty, returns root drives. + /// + /// + /// + [HttpGet("list")] + public ActionResult> GetDirectories(string path) + { + // TODO: We need some sort of validation other than our auth layer + _logger.Log(LogLevel.Debug, "Listing Directories for " + path); + + if (string.IsNullOrEmpty(path)) + { + return Ok(Directory.GetLogicalDrives()); + } + + if (!Directory.Exists(@path)) return BadRequest("This is not a valid path"); + + return Ok(_directoryService.ListDirectory(path)); + } + + [HttpGet] + public async Task>> GetLibraries() + { + return Ok(await _libraryRepository.GetLibrariesAsync()); + } + + + + } +} \ No newline at end of file diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index dd27d3f15..71f1677d7 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -1,10 +1,14 @@ using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using API.Data; using API.DTOs; +using API.Entities; +using API.Extensions; using API.Interfaces; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace API.Controllers { @@ -12,11 +16,13 @@ namespace API.Controllers { private readonly DataContext _context; private readonly IUserRepository _userRepository; + private readonly ILibraryRepository _libraryRepository; - public UsersController(DataContext context, IUserRepository userRepository) + public UsersController(DataContext context, IUserRepository userRepository, ILibraryRepository libraryRepository) { _context = context; _userRepository = userRepository; + _libraryRepository = libraryRepository; } [HttpGet] @@ -24,5 +30,41 @@ namespace API.Controllers { return Ok(await _userRepository.GetMembersAsync()); } + + [HttpPost("add-library")] + public async Task AddLibrary(CreateLibraryDto createLibraryDto) + { + //_logger.Log(LogLevel.Debug, "Creating a new " + createLibraryDto.Type + " library"); + var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); + + + if (await _libraryRepository.LibraryExists(createLibraryDto.Name)) + { + return BadRequest("Library name already exists. Please choose a unique name to the server."); + } + + // TODO: We probably need to clean the folders before we insert + var library = new Library() + { + Name = createLibraryDto.Name, + Type = createLibraryDto.Type, + //Folders = createLibraryDto.Folders + }; + + + user.Libraries.Add(library); + _userRepository.Update(user); + + if (await _userRepository.SaveAllAsync()) + { + return Ok(); + } + + + + + + return BadRequest("Not implemented"); + } } } \ No newline at end of file diff --git a/API/DTOs/CreateLibraryDto.cs b/API/DTOs/CreateLibraryDto.cs new file mode 100644 index 000000000..3e3263d48 --- /dev/null +++ b/API/DTOs/CreateLibraryDto.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using API.Entities; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal; + +namespace API.DTOs +{ + public class CreateLibraryDto + { + [Required] + public string Name { get; set; } + [Required] + public LibraryType Type { get; set; } + [Required] + [MinLength(1)] + public IEnumerable Folders { get; set; } + } +} \ No newline at end of file diff --git a/API/DTOs/LibraryDto.cs b/API/DTOs/LibraryDto.cs new file mode 100644 index 000000000..31e51e173 --- /dev/null +++ b/API/DTOs/LibraryDto.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using API.Entities; + +namespace API.DTOs +{ + public class LibraryDto + { + public string Name { get; set; } + public string CoverImage { get; set; } + public LibraryType Type { get; set; } + public ICollection Folders { get; set; } + } +} \ No newline at end of file diff --git a/API/DTOs/MemberDto.cs b/API/DTOs/MemberDto.cs index a1f8b377b..7c42553a0 100644 --- a/API/DTOs/MemberDto.cs +++ b/API/DTOs/MemberDto.cs @@ -1,4 +1,7 @@ using System; +using System.Collections; +using System.Collections.Generic; +using API.Entities; namespace API.DTOs { @@ -12,5 +15,6 @@ namespace API.DTOs public DateTime Created { get; set; } public DateTime LastActive { get; set; } public bool IsAdmin { get; set; } + //public IEnumerable Libraries { get; set; } } } \ No newline at end of file diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index bca4e24be..f5ed3afbe 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -11,5 +11,6 @@ namespace API.Data } public DbSet Users { get; set; } + public DbSet Library { get; set; } } } \ No newline at end of file diff --git a/API/Data/LibraryRepository.cs b/API/Data/LibraryRepository.cs new file mode 100644 index 000000000..d63b588e9 --- /dev/null +++ b/API/Data/LibraryRepository.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using API.DTOs; +using API.Entities; +using API.Interfaces; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Microsoft.EntityFrameworkCore; + +namespace API.Data +{ + public class LibraryRepository : ILibraryRepository + { + private readonly DataContext _context; + private readonly IMapper _mapper; + + public LibraryRepository(DataContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public void Update(Library library) + { + _context.Entry(library).State = EntityState.Modified; + } + + public async Task SaveAllAsync() + { + return await _context.SaveChangesAsync() > 0; + } + + public async Task> GetLibrariesAsync() + { + return await _context.Library + .Include(f => f.Folders) + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + public async Task LibraryExists(string libraryName) + { + return await _context.Library.AnyAsync(x => x.Name == libraryName); + } + } +} \ No newline at end of file diff --git a/API/Data/Migrations/20201215195007_AddedLibrary.Designer.cs b/API/Data/Migrations/20201215195007_AddedLibrary.Designer.cs new file mode 100644 index 000000000..4a657771e --- /dev/null +++ b/API/Data/Migrations/20201215195007_AddedLibrary.Designer.cs @@ -0,0 +1,128 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20201215195007_AddedLibrary")] + partial class AddedLibrary + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("BLOB"); + + b.Property("PasswordSalt") + .HasColumnType("BLOB"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", null) + .WithMany("Folders") + .HasForeignKey("LibraryId"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Libraries") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Libraries"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20201215195007_AddedLibrary.cs b/API/Data/Migrations/20201215195007_AddedLibrary.cs new file mode 100644 index 000000000..f1c4adf56 --- /dev/null +++ b/API/Data/Migrations/20201215195007_AddedLibrary.cs @@ -0,0 +1,71 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class AddedLibrary : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Library", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: true), + CoverImage = table.Column(type: "TEXT", nullable: true), + Type = table.Column(type: "INTEGER", nullable: false), + AppUserId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Library", x => x.Id); + table.ForeignKey( + name: "FK_Library_Users_AppUserId", + column: x => x.AppUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "FolderPath", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Path = table.Column(type: "TEXT", nullable: true), + LibraryId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_FolderPath", x => x.Id); + table.ForeignKey( + name: "FK_FolderPath_Library_LibraryId", + column: x => x.LibraryId, + principalTable: "Library", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_FolderPath_LibraryId", + table: "FolderPath", + column: "LibraryId"); + + migrationBuilder.CreateIndex( + name: "IX_Library_AppUserId", + table: "Library", + column: "AppUserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "FolderPath"); + + migrationBuilder.DropTable( + name: "Library"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 9ed038826..b341bb481 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -48,6 +48,78 @@ namespace API.Data.Migrations b.ToTable("Users"); }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", null) + .WithMany("Folders") + .HasForeignKey("LibraryId"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Libraries") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Libraries"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + }); #pragma warning restore 612, 618 } } diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs index 2308525a4..bb69ccaa8 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -48,12 +48,16 @@ namespace API.Data public async Task> GetMembersAsync() { - return await _context.Users.ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + return await _context.Users.Include(x => x.Libraries) + .Include(x => x.Libraries) + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); } public async Task GetMemberAsync(string username) { return await _context.Users.Where(x => x.UserName == username) + .Include(x => x.Libraries) .ProjectTo(_mapper.ConfigurationProvider) .SingleOrDefaultAsync(); } diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index a15e894c4..a67272ccd 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using API.Entities.Interfaces; @@ -14,6 +16,7 @@ namespace API.Entities public DateTime Created { get; set; } = DateTime.Now; public DateTime LastActive { get; set; } public bool IsAdmin { get; set; } + public ICollection Libraries { get; set; } [ConcurrencyCheck] public uint RowVersion { get; set; } diff --git a/API/Entities/FolderPath.cs b/API/Entities/FolderPath.cs new file mode 100644 index 000000000..d1e49180f --- /dev/null +++ b/API/Entities/FolderPath.cs @@ -0,0 +1,10 @@ +namespace API.Entities +{ + public class FolderPath + { + public int Id { get; set; } + public string Path { get; set; } + public Library Library { get; set; } + public int LibraryId { get; set; } + } +} \ No newline at end of file diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs new file mode 100644 index 000000000..06291ede0 --- /dev/null +++ b/API/Entities/Library.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace API.Entities +{ + public class Library + { + public int Id { get; set; } + public string Name { get; set; } + public string CoverImage { get; set; } + public LibraryType Type { get; set; } + public ICollection Folders { get; set; } + + public AppUser AppUser { get; set; } + public int AppUserId { get; set; } + } +} \ No newline at end of file diff --git a/API/Entities/LibraryType.cs b/API/Entities/LibraryType.cs new file mode 100644 index 000000000..6136042b0 --- /dev/null +++ b/API/Entities/LibraryType.cs @@ -0,0 +1,16 @@ +using System.ComponentModel; + +namespace API.Entities +{ + public enum LibraryType + { + [Description("Manga")] + Manga = 0, + [Description("Comic")] + Comic = 1, + [Description("Book")] + Book = 2, + [Description("Raw")] + Raw = 3 + } +} \ No newline at end of file diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index 07b096c95..454c7c2f2 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -16,6 +16,8 @@ namespace API.Extensions services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddDbContext(options => { options.UseSqlite(config.GetConnectionString("DefaultConnection")); diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 49da92f21..9e98f2ee0 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -1,4 +1,5 @@ -using API.DTOs; +using System.Linq; +using API.DTOs; using API.Entities; using AutoMapper; @@ -9,6 +10,10 @@ namespace API.Helpers public AutoMapperProfiles() { CreateMap(); + CreateMap() + .ForMember(dest => dest.Folders, + opt => + opt.MapFrom(src => src.Folders.Select(x => x.Path).ToList())); } } } \ No newline at end of file diff --git a/API/Interfaces/IDirectoryService.cs b/API/Interfaces/IDirectoryService.cs new file mode 100644 index 000000000..87a2b9f98 --- /dev/null +++ b/API/Interfaces/IDirectoryService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace API.Interfaces +{ + public interface IDirectoryService + { + IEnumerable ListDirectory(string rootPath); + } +} \ No newline at end of file diff --git a/API/Interfaces/ILibraryRepository.cs b/API/Interfaces/ILibraryRepository.cs new file mode 100644 index 000000000..625fba35e --- /dev/null +++ b/API/Interfaces/ILibraryRepository.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using API.DTOs; +using API.Entities; + +namespace API.Interfaces +{ + public interface ILibraryRepository + { + void Update(Library library); + Task SaveAllAsync(); + Task> GetLibrariesAsync(); + /// + /// Checks to see if a library of the same name exists. We only allow unique library names, no duplicates per LibraryType. + /// + /// + /// + Task LibraryExists(string libraryName); + } +} \ No newline at end of file diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs new file mode 100644 index 000000000..1b333b74c --- /dev/null +++ b/API/Services/DirectoryService.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using API.Interfaces; + +namespace API.Services +{ + public class DirectoryService : IDirectoryService + { + public IEnumerable ListDirectory(string rootPath) + { + // TODO: Filter out Hidden and System folders + // DirectoryInfo di = new DirectoryInfo(@path); + // var dirs = di.GetDirectories() + // .Where(dir => (dir.Attributes & FileAttributes.Hidden & FileAttributes.System) == 0).ToImmutableList(); + // + + return Directory.GetDirectories(@rootPath); + } + } +} \ No newline at end of file