diff --git a/Refhub/Areas/Admin/Views/ManageBook/Index.cshtml b/Refhub/Areas/Admin/Views/ManageBook/Index.cshtml index 4f38e1e7..7c28c408 100644 --- a/Refhub/Areas/Admin/Views/ManageBook/Index.cshtml +++ b/Refhub/Areas/Admin/Views/ManageBook/Index.cshtml @@ -2,70 +2,77 @@ @using Refhub.Tools.ExtensionMethod @model IEnumerable @{ - Layout = "_AdminLayout"; - ViewBag.TitleHeaderPage = "مدیریت کتاب ها"; + Layout = "_AdminLayout"; + ViewBag.TitleHeaderPage = "مدیریت کتاب ها"; } @{ - var searchText = Context.Request.Query["searchtext"].ToString(); + var searchText = Context.Request.Query["searchtext"].ToString(); }
-
+
- -
-
-
-
+ +
+
+
+
- ساخت کتاب جدید -
-
-
- -
- -
-
-
-
+ ساخت کتاب جدید +
+
+
+ +
+ +
+
+
+
-
- -
- - - - - - - + + +
+
#نام کتابعملیات
+ + + + + + - @foreach (var item in Model) - { - - - - - - } - + @foreach (var item in Model) + { + + + + + + } - -
#نام کتابعملیات
@item.Title - حذف - ویرایش -
+ + @if (!string.IsNullOrEmpty(item.ImagePath)) + { + + } + + @item.Title + حذف + ویرایش +
-
- -
- -
-
-
+ + + +
+ +
+ +
+
+
@* @section Scripts { diff --git a/Refhub/Areas/Admin/Views/ManageBook/Update.cshtml b/Refhub/Areas/Admin/Views/ManageBook/Update.cshtml index 4811439f..6c9cf703 100644 --- a/Refhub/Areas/Admin/Views/ManageBook/Update.cshtml +++ b/Refhub/Areas/Admin/Views/ManageBook/Update.cshtml @@ -47,7 +47,11 @@
- + @if (!string.IsNullOrEmpty(Model.ImagePath)) + { + + } + @@ -72,5 +76,6 @@
@section Scripts { + } diff --git a/Refhub/Areas/Admin/Views/Shared/_ValidationScriptsPartial.cshtml b/Refhub/Areas/Admin/Views/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 00000000..0d42b75e --- /dev/null +++ b/Refhub/Areas/Admin/Views/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,5 @@ + + + + + diff --git a/Refhub/Components/Views/Home/BestBookView.cshtml b/Refhub/Components/Views/Home/BestBookView.cshtml index c4dbfdbc..cfb7a2bb 100644 --- a/Refhub/Components/Views/Home/BestBookView.cshtml +++ b/Refhub/Components/Views/Home/BestBookView.cshtml @@ -21,7 +21,10 @@
- @item.Title + @if (!string.IsNullOrEmpty(item.ImagePath)) + { + @item.Title + }
diff --git a/Refhub/Components/Views/Home/LastBookView.cshtml b/Refhub/Components/Views/Home/LastBookView.cshtml index c163caa7..c531cecc 100644 --- a/Refhub/Components/Views/Home/LastBookView.cshtml +++ b/Refhub/Components/Views/Home/LastBookView.cshtml @@ -3,70 +3,72 @@
-
- -
-
-

اخرین کتال ها

-

هر موضوعی را در هر زمان مطالعه کنید. هزاران کتاب آموزشی را با کمترین قیمت جستجو کنید!

-
-
+
+ +
+
+

اخرین کتال ها

+

هر موضوعی را در هر زمان مطالعه کنید. هزاران کتاب آموزشی را با کمترین قیمت جستجو کنید!

+
+
- -
- -
-
- - @foreach (var item in Model) - { - -
-
- - @item.Title - -
- - - -
@item.Title
- @*

با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک

*@ - -
    -
  • -
  • -
  • -
  • -
  • -
  • 4.0/5.0
  • -
-
- - -
-
- - } - - - -
-
- - - -
- -
+ +
+ +
+
+ + @foreach (var item in Model) + { + +
+
+ + @if (!string.IsNullOrEmpty(item.ImagePath)) + { + @item.Title + } + +
+ + + +
@item.Title
+ @*

با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک

*@ + +
    +
  • +
  • +
  • +
  • +
  • +
  • 4.0/5.0
  • +
+
+ + +
+
+ + } + + + +
+
+ + +
+ +
\ No newline at end of file diff --git a/Refhub/Controllers/BookController.cs b/Refhub/Controllers/BookController.cs index a07a8ce3..6374ca9c 100644 --- a/Refhub/Controllers/BookController.cs +++ b/Refhub/Controllers/BookController.cs @@ -1,10 +1,16 @@ -using Microsoft.AspNetCore.Authorization; +using Amazon.S3; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.StaticFiles; +using Refhub.Models.Enums; +using Refhub.Service.Implement; using Refhub.Service.Interface; +using Refhub.Tools.Exceptions; +using Refhub.Tools.Static; namespace Refhub.Controllers; -public class BookController(IBookService bookService) : Controller +public class BookController(IBookService bookService, IFileUploaderService s3FileUploaderService, ILogger logger, IMessageService messageService) : Controller { [HttpGet("BookDetails/{slug}")] public async Task BookDetails(string slug, CancellationToken ct) @@ -19,18 +25,49 @@ public async Task BookDetails(string slug, CancellationToken ct) return bookDetails == null ? NotFound() : View(bookDetails); } + private bool IsValidFileUrl(string fileUrl) + { + return !string.IsNullOrWhiteSpace(fileUrl) && Uri.TryCreate(fileUrl, UriKind.Absolute, out _); + } + [Authorize] - [HttpGet] - public async Task DownloadFile(string filePath, CancellationToken ct) + [HttpGet("download")] + public async Task DownloadFile([FromQuery] string fileUrl, CancellationToken ct) { - filePath = Path.Combine(Directory.GetCurrentDirectory(), $"wwwroot{filePath}"); - if (string.IsNullOrEmpty(filePath) || !System.IO.File.Exists(filePath)) + try { - return NotFound(); + if (!IsValidFileUrl(fileUrl)) + { + return NotFound(messageService.Get("InvalidFileName")); + } + + + // دریافت فایل از S3 + var stream = await s3FileUploaderService.DownloadFileAsync(fileUrl, ct, BucketNameStatic.GetName(BucketNames.BookPdf)); + + // The file name for the user should be extracted from the URL + var fileName = Path.GetFileName(new Uri(fileUrl).AbsolutePath); + + // تعیین نوع فایل با توجه به پسوند + var contentTypeProvider = new FileExtensionContentTypeProvider(); + if (!contentTypeProvider.TryGetContentType(fileName, out string contentType)) + { + contentType = "application/octet-stream"; // پیش‌فرض اگر پسوند ناشناس باشد + } + + return File(stream, contentType, fileName); } + catch (FileDownloadException s3Ex) + { + logger.LogError(s3Ex, "Error downloading file from S3: {Message}", s3Ex.Message); - var fileBytes = await System.IO.File.ReadAllBytesAsync(filePath, ct); - return File(fileBytes, "application/octet-stream", Path.GetFileName(filePath)); + return NotFound(messageService.Get("FileNotFound")); + } + catch (Exception ex) + { + logger.LogError(ex, "Unexpected error occurred while downloading the file."); + return StatusCode(500,messageService.Get("DownloadError")); + } } private readonly int _pageSize = 3; @@ -41,4 +78,6 @@ public async Task List(string searchText, string authorFilter, st return View(viewModel); } + + } diff --git a/Refhub/Data/Configuration/BookAuthorConfiguration.cs b/Refhub/Data/Configuration/BookAuthorConfiguration.cs index 380f0178..6ac24d61 100644 --- a/Refhub/Data/Configuration/BookAuthorConfiguration.cs +++ b/Refhub/Data/Configuration/BookAuthorConfiguration.cs @@ -10,13 +10,15 @@ public void Configure(EntityTypeBuilder builder) { builder.HasKey(ba => new { ba.BookId, ba.AuthorId }); - builder.HasOne(ba => ba.Book) - .WithMany(b => b.BookAuthors) - .HasForeignKey(ba => ba.BookId); + .WithMany(b => b.BookAuthors) + .HasForeignKey(ba => ba.BookId) + .OnDelete(DeleteBehavior.Cascade); builder.HasOne(ba => ba.Author) .WithMany(a => a.BookAuthors) - .HasForeignKey(ba => ba.AuthorId); + .HasForeignKey(ba => ba.AuthorId) + .OnDelete(DeleteBehavior.Cascade); + } } diff --git a/Refhub/Data/Configuration/BookConfiguration.cs b/Refhub/Data/Configuration/BookConfiguration.cs index fc4bde9a..4b2a00d2 100644 --- a/Refhub/Data/Configuration/BookConfiguration.cs +++ b/Refhub/Data/Configuration/BookConfiguration.cs @@ -26,11 +26,15 @@ public void Configure(EntityTypeBuilder builder) builder.HasMany(b => b.BookAuthors).WithOne(ba => ba.Book) .HasForeignKey(ba => ba.BookId); + + + builder.HasMany(b => b.RelatedTo).WithOne(rt => rt.Book) - .HasForeignKey(bt => bt.BookId); + .HasForeignKey(bt => bt.BookId).OnDelete(DeleteBehavior.Restrict); builder.HasMany(b => b.RelatedFrom).WithOne(rf => rf.RelatedBook) - .HasForeignKey(rf => rf.RelatedBookId); + .HasForeignKey(rf => rf.RelatedBookId).OnDelete(DeleteBehavior.Restrict); + builder.HasOne(b => b.User).WithMany(rf => rf.Books) diff --git a/Refhub/Migrations/20250611081248_fix-mig.Designer.cs b/Refhub/Migrations/20250611081248_fix-mig.Designer.cs new file mode 100644 index 00000000..1789c9ae --- /dev/null +++ b/Refhub/Migrations/20250611081248_fix-mig.Designer.cs @@ -0,0 +1,541 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Refhub.Data.Context; + +#nullable disable + +namespace Refhub.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250611081248_fix-mig")] + partial class fixmig + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Refhub.Data.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Refhub.Data.Models.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Authors"); + }); + + modelBuilder.Entity("Refhub.Data.Models.Book", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ImagePath") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PageCount") + .HasColumnType("int"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(155) + .HasColumnType("nvarchar(155)"); + + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("Slug") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("Refhub.Data.Models.BookAuthor", b => + { + b.Property("BookId") + .HasColumnType("int"); + + b.Property("AuthorId") + .HasColumnType("int"); + + b.HasKey("BookId", "AuthorId"); + + b.HasIndex("AuthorId"); + + b.ToTable("BookAuthors"); + }); + + modelBuilder.Entity("Refhub.Data.Models.BookKeyword", b => + { + b.Property("BookId") + .HasColumnType("int"); + + b.Property("KeywordId") + .HasColumnType("int"); + + b.HasKey("BookId", "KeywordId"); + + b.HasIndex("KeywordId"); + + b.ToTable("BookKeywords"); + }); + + modelBuilder.Entity("Refhub.Data.Models.BookRelation", b => + { + b.Property("BookId") + .HasColumnType("int"); + + b.Property("RelatedBookId") + .HasColumnType("int"); + + b.HasKey("BookId", "RelatedBookId"); + + b.HasIndex("RelatedBookId"); + + b.ToTable("BookRelations"); + }); + + modelBuilder.Entity("Refhub.Data.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("slug") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Refhub.Data.Models.Keyword", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Word") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.ToTable("Keywords"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Refhub.Data.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Refhub.Data.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Refhub.Data.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Refhub.Data.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Refhub.Data.Models.Book", b => + { + b.HasOne("Refhub.Data.Models.Category", "Category") + .WithMany("Books") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Refhub.Data.Models.ApplicationUser", "User") + .WithMany("Books") + .HasForeignKey("UserId"); + + b.Navigation("Category"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Refhub.Data.Models.BookAuthor", b => + { + b.HasOne("Refhub.Data.Models.Author", "Author") + .WithMany("BookAuthors") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Refhub.Data.Models.Book", "Book") + .WithMany("BookAuthors") + .HasForeignKey("BookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Book"); + }); + + modelBuilder.Entity("Refhub.Data.Models.BookKeyword", b => + { + b.HasOne("Refhub.Data.Models.Book", "Book") + .WithMany("BookKeywords") + .HasForeignKey("BookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Refhub.Data.Models.Keyword", "Keyword") + .WithMany("BookKeywords") + .HasForeignKey("KeywordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Book"); + + b.Navigation("Keyword"); + }); + + modelBuilder.Entity("Refhub.Data.Models.BookRelation", b => + { + b.HasOne("Refhub.Data.Models.Book", "Book") + .WithMany("RelatedTo") + .HasForeignKey("BookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Refhub.Data.Models.Book", "RelatedBook") + .WithMany("RelatedFrom") + .HasForeignKey("RelatedBookId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Book"); + + b.Navigation("RelatedBook"); + }); + + modelBuilder.Entity("Refhub.Data.Models.ApplicationUser", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("Refhub.Data.Models.Author", b => + { + b.Navigation("BookAuthors"); + }); + + modelBuilder.Entity("Refhub.Data.Models.Book", b => + { + b.Navigation("BookAuthors"); + + b.Navigation("BookKeywords"); + + b.Navigation("RelatedFrom"); + + b.Navigation("RelatedTo"); + }); + + modelBuilder.Entity("Refhub.Data.Models.Category", b => + { + b.Navigation("Books"); + }); + + modelBuilder.Entity("Refhub.Data.Models.Keyword", b => + { + b.Navigation("BookKeywords"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Refhub/Migrations/20250611081248_fix-mig.cs b/Refhub/Migrations/20250611081248_fix-mig.cs new file mode 100644 index 00000000..801a9ba2 --- /dev/null +++ b/Refhub/Migrations/20250611081248_fix-mig.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Refhub.Migrations +{ + /// + public partial class fixmig : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Slug", + table: "Books", + type: "nvarchar(450)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.CreateIndex( + name: "IX_Books_Slug", + table: "Books", + column: "Slug", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Books_Slug", + table: "Books"); + + migrationBuilder.AlterColumn( + name: "Slug", + table: "Books", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + } + } +} diff --git a/Refhub/Migrations/20250612083534_AddUniqueIndexToBookSlugAndModifyType.cs b/Refhub/Migrations/20250612083534_AddUniqueIndexToBookSlugAndModifyType.cs index 57859506..a123764f 100644 --- a/Refhub/Migrations/20250612083534_AddUniqueIndexToBookSlugAndModifyType.cs +++ b/Refhub/Migrations/20250612083534_AddUniqueIndexToBookSlugAndModifyType.cs @@ -18,6 +18,11 @@ protected override void Up(MigrationBuilder migrationBuilder) oldClrType: typeof(string), oldType: "nvarchar(max)"); + migrationBuilder.Sql(@" + IF EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_Books_Slug' AND object_id = OBJECT_ID('Books')) + BEGIN + DROP INDEX IX_Books_Slug ON Books; + END"); migrationBuilder.CreateIndex( name: "IX_Books_Slug", table: "Books", diff --git a/Refhub/Models/AppSetting/S3Configuration.cs b/Refhub/Models/AppSetting/S3Configuration.cs new file mode 100644 index 00000000..1f155976 --- /dev/null +++ b/Refhub/Models/AppSetting/S3Configuration.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +/// +/// Strongly-typed binding for the “AWS:S3” section. +/// Do NOT commit real keys – store them in UserSecrets / env-vars. +/// +namespace Refhub.Models.AppSetting +{ + public sealed class S3Configuration + { + [Required] public string Region { get; init; } = default!; + [Required] public string AccessKey { get; init; } = default!; + [Required] public string SecretKey { get; init; } = default!; + [Required] public string BucketName { get; init; } = default!; + public string? ServiceURL { get; init; } + } +} diff --git a/Refhub/Models/Books/UpdateBookVM.cs b/Refhub/Models/Books/UpdateBookVM.cs index 07176e27..d1006613 100644 --- a/Refhub/Models/Books/UpdateBookVM.cs +++ b/Refhub/Models/Books/UpdateBookVM.cs @@ -14,8 +14,8 @@ public class UpdateBookVM [Required(ErrorMessageResourceType = typeof(Messages), ErrorMessageResourceName = nameof(Messages.Book_SlugRequired))] - - [MaxLength(450,ErrorMessageResourceType = typeof(Messages), + + [MaxLength(450, ErrorMessageResourceType = typeof(Messages), ErrorMessageResourceName = nameof(Messages.Book_SlugMaxLength))] public string Slug { get; set; } @@ -28,8 +28,11 @@ public class UpdateBookVM public string? FilePath { get; set; } public IFormFile? Image { get; set; } + [Url(ErrorMessageResourceType = typeof(Messages), +ErrorMessageResourceName = nameof(Messages.Book_ImagePathMustBeUrl))] public string? ImagePath { get; set; } + public string? UserId { get; set; } // Foreign Key [Required(ErrorMessageResourceType = typeof(Messages), diff --git a/Refhub/Models/Enums/BucketNames.cs b/Refhub/Models/Enums/BucketNames.cs new file mode 100644 index 00000000..c6bb8e91 --- /dev/null +++ b/Refhub/Models/Enums/BucketNames.cs @@ -0,0 +1,12 @@ +namespace Refhub.Models.Enums +{ + public enum BucketNames + { + None = 0, + BookImages, + BookPdf, + + + } + +} diff --git a/Refhub/Models/Enums/DirectoryTypes.cs b/Refhub/Models/Enums/DirectoryTypes.cs new file mode 100644 index 00000000..943b81f5 --- /dev/null +++ b/Refhub/Models/Enums/DirectoryTypes.cs @@ -0,0 +1,12 @@ +namespace Refhub.Models.Enums +{ + public enum DirectoryTypes + { + None = 0, + Images, + Books, + Files, + + } + +} diff --git a/Refhub/Program.cs b/Refhub/Program.cs index a697eac4..3f7eeceb 100644 --- a/Refhub/Program.cs +++ b/Refhub/Program.cs @@ -3,6 +3,7 @@ using Refhub.Tools.ExtensionMethod; + namespace Refhub; public class Program @@ -22,6 +23,7 @@ public static void Main(string[] args) #region CustomExtentionMethod + builder.BindS3Model(); builder.Services.AddCustomService(); builder.Services.ConfigureContext(builder.Configuration); builder.Services.ConfigureCookie(); diff --git a/Refhub/Refhub.csproj b/Refhub/Refhub.csproj index f863d3da..e6c22627 100644 --- a/Refhub/Refhub.csproj +++ b/Refhub/Refhub.csproj @@ -1,31 +1,36 @@  - - net9.0 - enable - enable - + + net9.0 + enable + enable + c6b529e8-deca-497c-84d4-0bddb8042e65 + - - - - + + + + - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + true + - ResXFileCodeGenerator + PublicResXFileCodeGenerator Messages.Designer.cs diff --git a/Refhub/Resources/Messages.Designer.cs b/Refhub/Resources/Messages.Designer.cs index a2641382..48dd1b56 100644 --- a/Refhub/Resources/Messages.Designer.cs +++ b/Refhub/Resources/Messages.Designer.cs @@ -22,7 +22,7 @@ namespace Refhub.Resources { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Messages { + public class Messages { private static global::System.Resources.ResourceManager resourceMan; @@ -36,7 +36,7 @@ internal Messages() { /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Refhub.Resources.Messages", typeof(Messages).Assembly); @@ -51,7 +51,7 @@ internal Messages() { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -63,7 +63,7 @@ internal Messages() { /// /// Looks up a localized string similar to The email entered is already registered.. /// - internal static string Account_EmailAlready { + public static string Account_EmailAlready { get { return ResourceManager.GetString("Account_EmailAlready", resourceCulture); } @@ -72,7 +72,7 @@ internal static string Account_EmailAlready { /// /// Looks up a localized string similar to An error has occurred.. /// - internal static string Account_Error_AddToRole { + public static string Account_Error_AddToRole { get { return ResourceManager.GetString("Account_Error_AddToRole", resourceCulture); } @@ -81,7 +81,7 @@ internal static string Account_Error_AddToRole { /// /// Looks up a localized string similar to The login attempt is invalid.. /// - internal static string Account_LoginInvalid { + public static string Account_LoginInvalid { get { return ResourceManager.GetString("Account_LoginInvalid", resourceCulture); } @@ -90,7 +90,7 @@ internal static string Account_LoginInvalid { /// /// Looks up a localized string similar to Registration encountered an error.. /// - internal static string Account_RegisterInvalid { + public static string Account_RegisterInvalid { get { return ResourceManager.GetString("Account_RegisterInvalid", resourceCulture); } @@ -99,7 +99,7 @@ internal static string Account_RegisterInvalid { /// /// Looks up a localized string similar to At least one associated author is required.. /// - internal static string Book_AnotherIdMinLength { + public static string Book_AnotherIdMinLength { get { return ResourceManager.GetString("Book_AnotherIdMinLength", resourceCulture); } @@ -108,7 +108,7 @@ internal static string Book_AnotherIdMinLength { /// /// Looks up a localized string similar to Choose a writer. /// - internal static string Book_AnotherIdRequired { + public static string Book_AnotherIdRequired { get { return ResourceManager.GetString("Book_AnotherIdRequired", resourceCulture); } @@ -117,7 +117,7 @@ internal static string Book_AnotherIdRequired { /// /// Looks up a localized string similar to Choose a category. /// - internal static string Book_CategoryIdRequired { + public static string Book_CategoryIdRequired { get { return ResourceManager.GetString("Book_CategoryIdRequired", resourceCulture); } @@ -126,16 +126,25 @@ internal static string Book_CategoryIdRequired { /// /// Looks up a localized string similar to Select a file. /// - internal static string Book_FileRequired { + public static string Book_FileRequired { get { return ResourceManager.GetString("Book_FileRequired", resourceCulture); } } + /// + /// Looks up a localized string similar to The book image path must be a valid URL.. + /// + public static string Book_ImagePathMustBeUrl { + get { + return ResourceManager.GetString("Book_ImagePathMustBeUrl", resourceCulture); + } + } + /// /// Looks up a localized string similar to Select a Image. /// - internal static string Book_ImageRequired { + public static string Book_ImageRequired { get { return ResourceManager.GetString("Book_ImageRequired", resourceCulture); } @@ -144,7 +153,7 @@ internal static string Book_ImageRequired { /// /// Looks up a localized string similar to Enter the number of pages and select. /// - internal static string Book_PageCountRequired { + public static string Book_PageCountRequired { get { return ResourceManager.GetString("Book_PageCountRequired", resourceCulture); } @@ -153,7 +162,7 @@ internal static string Book_PageCountRequired { /// /// Looks up a localized string similar to The title length in the browser should be a maximum of 450 characters.. /// - internal static string Book_SlugMaxLength { + public static string Book_SlugMaxLength { get { return ResourceManager.GetString("Book_SlugMaxLength", resourceCulture); } @@ -162,7 +171,7 @@ internal static string Book_SlugMaxLength { /// /// Looks up a localized string similar to Enter the title in the browser.. /// - internal static string Book_SlugRequired { + public static string Book_SlugRequired { get { return ResourceManager.GetString("Book_SlugRequired", resourceCulture); } @@ -171,7 +180,7 @@ internal static string Book_SlugRequired { /// /// Looks up a localized string similar to Enter the book title. /// - internal static string Book_TitleRequired { + public static string Book_TitleRequired { get { return ResourceManager.GetString("Book_TitleRequired", resourceCulture); } @@ -180,7 +189,7 @@ internal static string Book_TitleRequired { /// /// Looks up a localized string similar to Enter the description of category book . /// - internal static string Category_Description { + public static string Category_Description { get { return ResourceManager.GetString("Category_Description", resourceCulture); } @@ -189,7 +198,7 @@ internal static string Category_Description { /// /// Looks up a localized string similar to Enter the category book title. /// - internal static string Category_NameRequired { + public static string Category_NameRequired { get { return ResourceManager.GetString("Category_NameRequired", resourceCulture); } @@ -198,7 +207,7 @@ internal static string Category_NameRequired { /// /// Looks up a localized string similar to Confirm Password Invalid. /// - internal static string ConfirmPassword_Compare_Invalid { + public static string ConfirmPassword_Compare_Invalid { get { return ResourceManager.GetString("ConfirmPassword_Compare_Invalid", resourceCulture); } @@ -207,16 +216,25 @@ internal static string ConfirmPassword_Compare_Invalid { /// /// Looks up a localized string similar to Enter Confirm Password!. /// - internal static string ConfirmPassword_Required { + public static string ConfirmPassword_Required { get { return ResourceManager.GetString("ConfirmPassword_Required", resourceCulture); } } + /// + /// Looks up a localized string similar to An error occurred while downloading the file. + /// + public static string DownloadError { + get { + return ResourceManager.GetString("DownloadError", resourceCulture); + } + } + /// /// Looks up a localized string similar to The Email is not Valid!. /// - internal static string Email_Format_Invalid { + public static string Email_Format_Invalid { get { return ResourceManager.GetString("Email_Format_Invalid", resourceCulture); } @@ -225,7 +243,7 @@ internal static string Email_Format_Invalid { /// /// Looks up a localized string similar to Enter the Email . /// - internal static string Email_Required { + public static string Email_Required { get { return ResourceManager.GetString("Email_Required", resourceCulture); } @@ -234,7 +252,7 @@ internal static string Email_Required { /// /// Looks up a localized string similar to Author not found. /// - internal static string Error_AuthorNotFound { + public static string Error_AuthorNotFound { get { return ResourceManager.GetString("Error_AuthorNotFound", resourceCulture); } @@ -243,7 +261,7 @@ internal static string Error_AuthorNotFound { /// /// Looks up a localized string similar to Uncertain. /// - internal static string Error_NotDefined { + public static string Error_NotDefined { get { return ResourceManager.GetString("Error_NotDefined", resourceCulture); } @@ -252,25 +270,43 @@ internal static string Error_NotDefined { /// /// Looks up a localized string similar to Slug has already been used.. /// - internal static string Error_SlugExists { + public static string Error_SlugExists { get { return ResourceManager.GetString("Error_SlugExists", resourceCulture); } } + /// + /// Looks up a localized string similar to The requested file was not found. + /// + public static string FileNotFound { + get { + return ResourceManager.GetString("FileNotFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to FullName is Required. /// - internal static string FullNameRequired { + public static string FullNameRequired { get { return ResourceManager.GetString("FullNameRequired", resourceCulture); } } + /// + /// Looks up a localized string similar to The provided file name is invalid. + /// + public static string InvalidFileName { + get { + return ResourceManager.GetString("InvalidFileName", resourceCulture); + } + } + /// /// Looks up a localized string similar to Keyword found. /// - internal static string Keyword_AlreadyExists { + public static string Keyword_AlreadyExists { get { return ResourceManager.GetString("Keyword_AlreadyExists", resourceCulture); } @@ -279,7 +315,7 @@ internal static string Keyword_AlreadyExists { /// /// Looks up a localized string similar to Keyword not found. /// - internal static string Keyword_NotFound { + public static string Keyword_NotFound { get { return ResourceManager.GetString("Keyword_NotFound", resourceCulture); } @@ -288,7 +324,7 @@ internal static string Keyword_NotFound { /// /// Looks up a localized string similar to choose a keyword!. /// - internal static string Keyword_Required { + public static string Keyword_Required { get { return ResourceManager.GetString("Keyword_Required", resourceCulture); } @@ -297,7 +333,7 @@ internal static string Keyword_Required { /// /// Looks up a localized string similar to The name must be a maximum of 50 characters.. /// - internal static string Name_Max_50 { + public static string Name_Max_50 { get { return ResourceManager.GetString("Name_Max_50", resourceCulture); } @@ -306,7 +342,7 @@ internal static string Name_Max_50 { /// /// Looks up a localized string similar to Must contain at least one letter, one number and 6 characters.. /// - internal static string Password_Regex_Invalid { + public static string Password_Regex_Invalid { get { return ResourceManager.GetString("Password_Regex_Invalid", resourceCulture); } @@ -315,7 +351,7 @@ internal static string Password_Regex_Invalid { /// /// Looks up a localized string similar to Enter The Password!. /// - internal static string Password_Required { + public static string Password_Required { get { return ResourceManager.GetString("Password_Required", resourceCulture); } @@ -324,7 +360,7 @@ internal static string Password_Required { /// /// Looks up a localized string similar to Remember Me! . /// - internal static string RememberMe_Display { + public static string RememberMe_Display { get { return ResourceManager.GetString("RememberMe_Display", resourceCulture); } @@ -333,7 +369,7 @@ internal static string RememberMe_Display { /// /// Looks up a localized string similar to The slug must be a maximum of 450 characters.. /// - internal static string Slug_Max_450 { + public static string Slug_Max_450 { get { return ResourceManager.GetString("Slug_Max_450", resourceCulture); } @@ -342,7 +378,7 @@ internal static string Slug_Max_450 { /// /// Looks up a localized string similar to Choose a slug name. /// - internal static string Slug_NameRequired { + public static string Slug_NameRequired { get { return ResourceManager.GetString("Slug_NameRequired", resourceCulture); } @@ -351,7 +387,7 @@ internal static string Slug_NameRequired { /// /// Looks up a localized string similar to Slug is Required. /// - internal static string SlugRequired { + public static string SlugRequired { get { return ResourceManager.GetString("SlugRequired", resourceCulture); } @@ -360,7 +396,7 @@ internal static string SlugRequired { /// /// Looks up a localized string similar to refhub. /// - internal static string Title { + public static string Title { get { return ResourceManager.GetString("Title", resourceCulture); } @@ -369,7 +405,7 @@ internal static string Title { /// /// Looks up a localized string similar to User is not found. /// - internal static string User_NotFound { + public static string User_NotFound { get { return ResourceManager.GetString("User_NotFound", resourceCulture); } diff --git a/Refhub/Resources/Messages.fa.resx b/Refhub/Resources/Messages.fa.resx index ec92ad70..9bf3af38 100644 --- a/Refhub/Resources/Messages.fa.resx +++ b/Refhub/Resources/Messages.fa.resx @@ -207,6 +207,9 @@ خطایی رخ داده است + + کلید واژه از قبل وجود دارد + طول عنوان در مرورگر حداکثر 450 حرف باشد @@ -219,7 +222,16 @@ نام در مرورگر باید حداکثر 450 حرف باشد - - کلید واژه یافت شد. + + فایل مورد نظر پیدا نشد. + + + نام فایل وارد شده نامعتبر است + + + خطا در دانلود فایل رخ داد + + + مسیر تصویر کتاب باید یک URL معتبر باشد \ No newline at end of file diff --git a/Refhub/Resources/Messages.resx b/Refhub/Resources/Messages.resx index 95237132..35c680e0 100644 --- a/Refhub/Resources/Messages.resx +++ b/Refhub/Resources/Messages.resx @@ -222,4 +222,16 @@ The slug must be a maximum of 450 characters. + + The requested file was not found + + + The provided file name is invalid + + + An error occurred while downloading the file + + + The book image path must be a valid URL. + \ No newline at end of file diff --git a/Refhub/Service/Implement/BookService.cs b/Refhub/Service/Implement/BookService.cs index f483b640..7919071b 100644 --- a/Refhub/Service/Implement/BookService.cs +++ b/Refhub/Service/Implement/BookService.cs @@ -5,6 +5,7 @@ using Refhub.Models.Books; using Refhub.Models.Category; using Refhub.Models.DTO; +using Refhub.Models.Enums; using Refhub.Service.Interface; using Refhub.Tools.Static; @@ -99,6 +100,16 @@ public async Task CreateAnotherAsync(string fullname, string slug, Cancell var author = new Author { FullName = fullname, Slug = slug }; await context.Authors.AddAsync(author, ct); + try + { + await context.SaveChangesAsync(ct); + } + catch (Exception ex) + { + // Log the exception or handle it appropriately + Console.WriteLine($"Error saving changes: {ex.Message}"); + throw; + } return true; } @@ -148,13 +159,15 @@ public async Task CreateBookAsync(CreateBookVM book, CancellationToken ct) { try { + if (await context.Books.AnyAsync(a => a.Slug.Equals(book.Slug), ct)) + return false; var bookAuthors = book.AnotherId.Select(a => new BookAuthor { AuthorId = a }).ToList(); - var filePath = await uploaderService.UpdloadFile(book.File, FolderNameStatic.GetDirectoryBooks, FolderNameStatic.GetDirectoryImages, book.Slug); - var imagePath = await uploaderService.UpdloadFile(book.Image, FolderNameStatic.GetDirectoryBooks, FolderNameStatic.GetDirectoryImages, book.Slug); + var filePath = await uploaderService.UploadFile(book.File, BucketNameStatic.GetName(BucketNames.BookPdf), book.Slug); + var imagePath = await uploaderService.UploadFile(book.Image, BucketNameStatic.GetName(BucketNames.BookImages), book.Slug); if (string.IsNullOrWhiteSpace(filePath) || string.IsNullOrWhiteSpace(imagePath)) { @@ -208,20 +221,20 @@ public async Task UpdateBookAsync(UpdateBookVM book, CancellationToken ct) { if (!string.IsNullOrWhiteSpace(existingBook.FilePath)) { - await uploaderService.DeleteFile(FolderNameStatic.GetDirectoryBooks, FolderNameStatic.GetDirectoryImages, existingBook.FilePath); + await uploaderService.DeleteFile(existingBook.FilePath, BucketNameStatic.GetName(BucketNames.BookPdf)); } - existingBook.FilePath = await uploaderService.UpdloadFile(book.File, FolderNameStatic.GetDirectoryBooks, FolderNameStatic.GetDirectoryImages, book.Slug); + existingBook.FilePath = await uploaderService.UploadFile(book.File, BucketNameStatic.GetName(BucketNames.BookPdf), book.Slug); } if (book.Image != null) { if (!string.IsNullOrWhiteSpace(existingBook.ImagePath)) { - await uploaderService.DeleteFile(FolderNameStatic.GetDirectoryBooks, FolderNameStatic.GetDirectoryImages, existingBook.ImagePath); + await uploaderService.DeleteFile(existingBook.ImagePath, BucketNameStatic.GetName(BucketNames.BookImages)); } - existingBook.ImagePath = await uploaderService.UpdloadFile(book.Image, FolderNameStatic.GetDirectoryBooks, FolderNameStatic.GetDirectoryImages, book.Slug); + existingBook.ImagePath = await uploaderService.UploadFile(book.Image, BucketNameStatic.GetName(BucketNames.BookImages), book.Slug); } // حذف نویسنده‌های قبلی و اضافه کردن جدید @@ -252,7 +265,8 @@ public async Task DeleteBookAsync(int bookId, CancellationToken ct) { try { - var book = await context.Books.FirstOrDefaultAsync(b => b.Id == bookId, ct); + var book = await context.Books + .FirstOrDefaultAsync(b => b.Id == bookId, ct); if (book == null) { return false; @@ -260,14 +274,20 @@ public async Task DeleteBookAsync(int bookId, CancellationToken ct) if (!string.IsNullOrWhiteSpace(book.ImagePath)) { - await uploaderService.DeleteFile(FolderNameStatic.GetDirectoryBooks, FolderNameStatic.GetDirectoryImages, book.ImagePath); + await uploaderService.DeleteFile(book.ImagePath, BucketNameStatic.GetName(BucketNames.BookImages)); } if (!string.IsNullOrWhiteSpace(book.FilePath)) { - await uploaderService.DeleteFile(FolderNameStatic.GetDirectoryBooks, FolderNameStatic.GetDirectoryImages, book.FilePath); + await uploaderService.DeleteFile(book.FilePath, BucketNameStatic.GetName(BucketNames.BookPdf)); } + var relations = await context.BookRelations + .Where(br => br.BookId == bookId || br.RelatedBookId == bookId) + .ToListAsync(ct); + if (relations.Any()) + context.BookRelations.RemoveRange(relations); + context.Books.Remove(book); await context.SaveChangesAsync(ct); return true; diff --git a/Refhub/Service/Implement/LocalFileUploaderService.cs b/Refhub/Service/Implement/LocalFileUploaderService.cs deleted file mode 100644 index f1a4aa78..00000000 --- a/Refhub/Service/Implement/LocalFileUploaderService.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Refhub.Service.Interface; - -namespace Refhub.Service.Implement; - -public class LocalFileUploaderService : IFileUploaderService -{ - public async Task UpdloadFile(IFormFile file, string directoryName, string Type, string Name) - { - if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "Files", Type))) - { - Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "Files", Type)); - } - - if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "Files", Type, directoryName))) - { - Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "Files", Type, directoryName)); - } - - Name = Name.Replace(" ", "-") + new Random().Next(1111, 9999); - string name = Name + Path.GetExtension(file.FileName); - string path = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "Files", Type, directoryName, name); - using (Stream stream = new FileStream(path, FileMode.Create)) - { - file.CopyTo(stream); - } - - return name; - } - - public async Task DeleteFile(string directoryName, string Type, string Name) - { - if (File.Exists(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "Files", Type, directoryName, Name))) - { - File.Delete(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "Files", Type, directoryName, Name)); - } - - } -} diff --git a/Refhub/Service/Implement/S3FileUploaderService.cs b/Refhub/Service/Implement/S3FileUploaderService.cs new file mode 100644 index 00000000..ff8cab3d --- /dev/null +++ b/Refhub/Service/Implement/S3FileUploaderService.cs @@ -0,0 +1,128 @@ +using Microsoft.Extensions.Options; +using Refhub.Models.AppSetting; +using Refhub.Service.Interface; + +namespace Refhub.Service.Implement +{ + + using Amazon; + using Amazon.Runtime; + using Amazon.S3; + using Amazon.S3.Model; + using Refhub.Models.Enums; + using Refhub.Resources; + using Refhub.Tools.Exceptions; + using System.Threading; + + public class S3FileUploaderService : IFileUploaderService + { + private readonly IAmazonS3 _s3Client; + private readonly IOptions _s3Options; + + public S3FileUploaderService(IOptions s3Options) + { + this._s3Options = s3Options; + + var credentials = new BasicAWSCredentials(s3Options.Value.AccessKey, s3Options.Value.SecretKey); + + var config = new AmazonS3Config + { + RegionEndpoint = RegionEndpoint.GetBySystemName(s3Options.Value.Region), // Dynamically set region endpoint + ServiceURL = s3Options.Value.ServiceURL, + ForcePathStyle = true // بسیار مهم برای کار با آروان‌کلاد + }; + + _s3Client = new AmazonS3Client(credentials, config); + + } + private string GenerateS3Url(string key, string bucketName) + { + // برای آروان کلاد: + return $"{_s3Options.Value.ServiceURL}/{bucketName.ToLower()}/{key}"; + } + + + private string GetKey(string realUrl, string bucketName) + { + // برای آروان کلاد: + + var prefix = $"{_s3Options.Value.ServiceURL}/{bucketName.ToLower()}/"; + + if (realUrl.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + return realUrl.Substring(prefix.Length); + } + + throw new ArgumentException("The provided URL does not start with the expected prefix."); + + + } + + public async Task UploadFile(IFormFile file, string bucketName, string name) + { + var key = $"{name.Replace(" ", "-")}_{Guid.NewGuid()}{Path.GetExtension(file.FileName)}"; + + using var stream = file.OpenReadStream(); + var request = new PutObjectRequest + { + BucketName = bucketName.ToLower(), + Key = key, + InputStream = stream, + ContentType = file.ContentType, + + CannedACL = S3CannedACL.Private + }; + + await _s3Client.PutObjectAsync(request); + return GenerateS3Url(key, bucketName); + } + + public async Task DeleteFile(string realUrl, string bucketName) + { + + + var key = GetKey(realUrl, bucketName); + + var request = new DeleteObjectRequest + { + BucketName = bucketName.ToLower(), + Key = key + }; + + await _s3Client.DeleteObjectAsync(request); + } + + public async Task DownloadFileAsync(string fileUrl, CancellationToken ct, string bucketName) + { + if (string.IsNullOrWhiteSpace(fileUrl)) + throw new ArgumentException("File URL cannot be null or empty", nameof(fileUrl)); + + if (string.IsNullOrWhiteSpace(bucketName)) + throw new ArgumentException("Bucket name cannot be null or empty", nameof(bucketName)); + var key = GetKey(fileUrl, bucketName); + var request = new GetObjectRequest + { + BucketName = bucketName.ToLower(), + Key = key + }; + + try + { + using var response = await _s3Client.GetObjectAsync(request, ct); + var ms = new MemoryStream(); + await response.ResponseStream.CopyToAsync(ms, ct); + ms.Position = 0; + + return ms; + } + catch (AmazonS3Exception ex) + { + throw new FileDownloadException("Error downloading file from S3", ex); + } + } + + + } + + +} diff --git a/Refhub/Service/Interface/IFileUploaderService.cs b/Refhub/Service/Interface/IFileUploaderService.cs index 5f920873..3b6a4e6c 100644 --- a/Refhub/Service/Interface/IFileUploaderService.cs +++ b/Refhub/Service/Interface/IFileUploaderService.cs @@ -2,6 +2,7 @@ public interface IFileUploaderService { - Task UpdloadFile(IFormFile file, string directoryName, string Type, string Name); - Task DeleteFile(string directoryName, string Type, string Name); + Task UploadFile(IFormFile file, string bucketName, string name); + Task DeleteFile(string realUrl, string bucketName); + Task DownloadFileAsync(string fileUrl, CancellationToken ct, string bucketName); } diff --git a/Refhub/Tools/Exceptions/EntityNotFoundException.cs b/Refhub/Tools/Exceptions/EntityNotFoundException.cs index add63e75..4c4a6e48 100644 --- a/Refhub/Tools/Exceptions/EntityNotFoundException.cs +++ b/Refhub/Tools/Exceptions/EntityNotFoundException.cs @@ -10,5 +10,4 @@ public EntityNotFoundException(string message, Exception innerException) : base( } } - } diff --git a/Refhub/Tools/Exceptions/FileDownloadException.cs b/Refhub/Tools/Exceptions/FileDownloadException.cs new file mode 100644 index 00000000..999716d0 --- /dev/null +++ b/Refhub/Tools/Exceptions/FileDownloadException.cs @@ -0,0 +1,13 @@ +namespace Refhub.Tools.Exceptions +{ + public class FileDownloadException : Exception + { + public FileDownloadException (string message) : base(message) + { + } + public FileDownloadException(string message, Exception innerException) : base(message, innerException) + { + } + + } +} diff --git a/Refhub/Tools/ExtentionMethod/AddServiceExtentionMethod.cs b/Refhub/Tools/ExtentionMethod/AddServiceExtentionMethod.cs index 6eda8660..13f1781e 100644 --- a/Refhub/Tools/ExtentionMethod/AddServiceExtentionMethod.cs +++ b/Refhub/Tools/ExtentionMethod/AddServiceExtentionMethod.cs @@ -1,4 +1,5 @@ using Refhub.Service.Implement; + using Refhub.Service.Interface; namespace Refhub.Tools.ExtensionMethod; @@ -15,7 +16,7 @@ public static IServiceCollection AddCustomService(this IServiceCollection collec collection.AddScoped(); collection.AddScoped(); - collection.AddScoped(); + collection.AddScoped(); return collection; } } diff --git a/Refhub/Tools/ExtentionMethod/BindAppSettingToModelExtentionMethod.cs b/Refhub/Tools/ExtentionMethod/BindAppSettingToModelExtentionMethod.cs new file mode 100644 index 00000000..f92440b6 --- /dev/null +++ b/Refhub/Tools/ExtentionMethod/BindAppSettingToModelExtentionMethod.cs @@ -0,0 +1,15 @@ +using Refhub.Models.AppSetting; +using Refhub.Service.Implement; +using Refhub.Service.Interface; + +namespace Refhub.Tools.ExtensionMethod; + +public static class BindAppSettingToModelExtensionMethod +{ + public static WebApplicationBuilder BindS3Model(this WebApplicationBuilder builder) + { + builder.Services.Configure( + builder.Configuration.GetSection("AWS:S3")); + return builder; + } +} diff --git a/Refhub/Tools/ExtentionMethod/PathExtionMethod.cs b/Refhub/Tools/ExtentionMethod/PathExtionMethod.cs deleted file mode 100644 index f1bb2a00..00000000 --- a/Refhub/Tools/ExtentionMethod/PathExtionMethod.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Refhub.Tools.Static; - -namespace Refhub.Tools.ExtensionMethod; - -public static class PathImageExtionMethod -{ - public static string ConvertForBookPathImage(this string imageName) - { - return string.IsNullOrEmpty(imageName) - ? string.Empty - : $"/{FolderNameStatic.GetDirectoryFiles}/{FolderNameStatic.GetDirectoryImages}/{FolderNameStatic.GetDirectoryBooks}/{imageName}"; - } - public static string ConvertForBookPathFile(this string fileName) - { - return string.IsNullOrEmpty(fileName) - ? string.Empty - : $"\\{FolderNameStatic.GetDirectoryFiles}\\{FolderNameStatic.GetDirectoryImages}\\{FolderNameStatic.GetDirectoryBooks}\\{fileName}"; - } -} diff --git a/Refhub/Tools/Static/BucketNameStatic.cs b/Refhub/Tools/Static/BucketNameStatic.cs new file mode 100644 index 00000000..f6514da7 --- /dev/null +++ b/Refhub/Tools/Static/BucketNameStatic.cs @@ -0,0 +1,18 @@ +using Refhub.Models.Enums; + +namespace Refhub.Tools.Static; + +public static class BucketNameStatic +{ + public static string GetName(BucketNames bucketNames) + { + return bucketNames switch + { + BucketNames.BookPdf => nameof(BucketNames.BookPdf), + BucketNames.BookImages => nameof(BucketNames.BookImages), + + + _ => throw new ArgumentOutOfRangeException(nameof(bucketNames), $"Unsupported bucket name: {bucketNames}") + }; + } +} diff --git a/Refhub/Tools/Static/FolderNameStatic.cs b/Refhub/Tools/Static/FolderNameStatic.cs deleted file mode 100644 index e71d974f..00000000 --- a/Refhub/Tools/Static/FolderNameStatic.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Refhub.Tools.Static; - -public static class FolderNameStatic -{ - public static string GetDirectoryImages = "Images"; - public static string GetDirectoryBooks = "Books"; - public static string GetDirectoryFiles = "Files"; -} diff --git a/Refhub/Views/Book/BookDetails.cshtml b/Refhub/Views/Book/BookDetails.cshtml index fb4d594b..029fe632 100644 --- a/Refhub/Views/Book/BookDetails.cshtml +++ b/Refhub/Views/Book/BookDetails.cshtml @@ -22,7 +22,7 @@ @if (!string.IsNullOrEmpty(Model.ImagePath)) {
- @(Model.Title ?? + @(Model.Title ??
} else @@ -74,7 +74,8 @@ @if (!string.IsNullOrEmpty(Model.FilePath)) { } else @@ -90,7 +91,7 @@ @if (!string.IsNullOrEmpty(relatedBook.RelatedBook?.FilePath)) {
  • - + دانلود فایل: @(relatedBook.RelatedBook?.Title ?? "بدون عنوان")
  • diff --git a/Refhub/Views/Book/List.cshtml b/Refhub/Views/Book/List.cshtml index ac609b73..ecdf0ed3 100644 --- a/Refhub/Views/Book/List.cshtml +++ b/Refhub/Views/Book/List.cshtml @@ -100,8 +100,11 @@ Page content START --> diff --git a/Refhub/Views/Shared/_ValidationScriptsPartial.cshtml b/Refhub/Views/Shared/_ValidationScriptsPartial.cshtml index 0d42b75e..bc6104a2 100644 --- a/Refhub/Views/Shared/_ValidationScriptsPartial.cshtml +++ b/Refhub/Views/Shared/_ValidationScriptsPartial.cshtml @@ -1,5 +1,4 @@  - diff --git a/Refhub/appsettings.Development.json b/Refhub/appsettings.Development.json index 8967cd3e..1b3c284c 100644 --- a/Refhub/appsettings.Development.json +++ b/Refhub/appsettings.Development.json @@ -1,11 +1,9 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } } - }, - "ConnectionStrings": { - "DefaultConnection": "Data Source=.;Initial Catalog=RefHubDB;Integrated Security=True;Multiple Active Result Sets=True;Trust Server Certificate=True" - } }