diff --git a/.gitignore b/.gitignore index 4c3da29..f222ac6 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ /bin /obj /.claude +/.codex-build diff --git a/FileShare-API/Configuration/ServicesConfiguration.cs b/FileShare-API/Configuration/ServicesConfiguration.cs index 1f00eef..a8eebc0 100644 --- a/FileShare-API/Configuration/ServicesConfiguration.cs +++ b/FileShare-API/Configuration/ServicesConfiguration.cs @@ -40,6 +40,14 @@ namespace FileShare_API.Configuration // ---- 业务服务 ---- services.AddScoped(); + var thumbnailOptions = configuration + .GetSection(nameof(ThumbnailStorageOptions)) + .Get() + ?? new ThumbnailStorageOptions(); + services.AddSingleton(thumbnailOptions); + services.AddSingleton(sp => + new VideoThumbnailService(sp.GetRequiredService())); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/FileShare-API/Extensions/FileStreamEndpointExtensions.cs b/FileShare-API/Extensions/FileStreamEndpointExtensions.cs index 2fb2098..08617c4 100644 --- a/FileShare-API/Extensions/FileStreamEndpointExtensions.cs +++ b/FileShare-API/Extensions/FileStreamEndpointExtensions.cs @@ -43,6 +43,35 @@ namespace FileShare_API.Extensions .WithName("StreamManagedFileById") .WithTags("FileLibrary"); + app.MapMethods( + "/api/thumbnails/{id:int}", + ["GET", "HEAD"], + async (int id, IThumbnailStreamService thumbnailStreamService, HttpContext httpContext) => + { + var thumbnail = await thumbnailStreamService.GetThumbnailAsync(id); + if (thumbnail is null) + { + return Results.NotFound(); + } + + var stream = System.IO.File.Open( + thumbnail.FilePath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite); + + httpContext.Response.Headers.ContentDisposition = + $"inline; filename=\"{Uri.EscapeDataString(thumbnail.FileName)}\""; + httpContext.Response.Headers.CacheControl = "public, max-age=3600"; + + return Results.File( + stream, + contentType: thumbnail.ContentType, + lastModified: thumbnail.LastModified); + }) + .WithName("StreamManagedThumbnailById") + .WithTags("FileLibrary"); + return app; } } diff --git a/FileShare-API/FileShare-API.csproj b/FileShare-API/FileShare-API.csproj index 09828a5..3c92443 100644 --- a/FileShare-API/FileShare-API.csproj +++ b/FileShare-API/FileShare-API.csproj @@ -26,6 +26,8 @@ + + diff --git a/FileShare-API/appsettings.json b/FileShare-API/appsettings.json index b4d641c..e7b840c 100644 --- a/FileShare-API/appsettings.json +++ b/FileShare-API/appsettings.json @@ -24,5 +24,10 @@ "RecreateDatabase": false, "EnableDetailedLog": false, "Timeout": 30 + }, + "ThumbnailStorageOptions": { + "RootPath": "thumbnails", + "FfmpegPath": "tools/ffmpeg/bin/ffmpeg.exe", + "FfprobePath": "tools/ffmpeg/bin/ffprobe.exe" } } diff --git a/FileShare-EFCore/Database/AppDataContext.cs b/FileShare-EFCore/Database/AppDataContext.cs index 9825961..79a7fd4 100644 --- a/FileShare-EFCore/Database/AppDataContext.cs +++ b/FileShare-EFCore/Database/AppDataContext.cs @@ -25,6 +25,8 @@ namespace FileShare_EFCore.Database /// 文件库文件记录数据 public DbSet ManagedFileRecords => Set(); + public DbSet ManagedThumbnailMaps => Set(); + /// /// 配置实体映射,包括主键、索引和属性约束。 /// @@ -66,8 +68,10 @@ namespace FileShare_EFCore.Database { entity.HasKey(e => e.Id).HasName("pk-managed-file-record"); entity.HasIndex(e => e.LibraryRootId).HasDatabaseName("idx-managed-file-record-root-id"); + entity.HasIndex(e => e.ThumbnailId).HasDatabaseName("idx-managed-file-record-thumbnail-id"); entity.HasIndex(e => e.AbsolutePath).IsUnique().HasDatabaseName("idx-managed-file-record-absolute-path"); entity.HasIndex(e => new { e.MediaType, e.Exists }).HasDatabaseName("idx-managed-file-record-media-type-exists"); + entity.HasIndex(e => e.LastPlayedAt).HasDatabaseName("idx-managed-file-record-last-played-at"); entity.Property(e => e.FileName).HasMaxLength(260); entity.Property(e => e.RelativePath).HasMaxLength(1024); entity.Property(e => e.AbsolutePath).HasMaxLength(2048); @@ -78,6 +82,23 @@ namespace FileShare_EFCore.Database .WithMany(e => e.Files) .HasForeignKey(e => e.LibraryRootId) .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(e => e.Thumbnail) + .WithMany(e => e.Files) + .HasForeignKey(e => e.ThumbnailId) + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("pk-managed-thumbnail-map"); + entity.HasIndex(e => e.LibraryRootId).HasDatabaseName("idx-managed-thumbnail-map-root-id"); + entity.HasIndex(e => e.RelativePath).IsUnique().HasDatabaseName("idx-managed-thumbnail-map-relative-path"); + entity.Property(e => e.RelativePath).HasMaxLength(1024); + entity.Property(e => e.ContentType).HasMaxLength(100); + entity.HasOne(e => e.LibraryRoot) + .WithMany(e => e.Thumbnails) + .HasForeignKey(e => e.LibraryRootId) + .OnDelete(DeleteBehavior.Cascade); }); } } diff --git a/FileShare-EFCore/Migrations/MySQL/20260522082856_AutoMigration_20260522162758.Designer.cs b/FileShare-EFCore/Migrations/MySQL/20260522082856_AutoMigration_20260522162758.Designer.cs new file mode 100644 index 0000000..c843fa5 --- /dev/null +++ b/FileShare-EFCore/Migrations/MySQL/20260522082856_AutoMigration_20260522162758.Designer.cs @@ -0,0 +1,370 @@ +// +using System; +using FileShare_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FileShare_EFCore.Migrations.MySQL +{ + [DbContext(typeof(MySqlAppDataContext))] + [Migration("20260522082856_AutoMigration_20260522162758")] + partial class AutoMigration_20260522162758 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("AbsolutePath") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("varchar(2048)") + .HasColumnName("absolute-path"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("Exists") + .HasColumnType("tinyint(1)") + .HasColumnName("exists"); + + b.Property("Extension") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)") + .HasColumnName("extension"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("varchar(260)") + .HasColumnName("file-name"); + + b.Property("LastPlayedAt") + .HasColumnType("datetime(6)") + .HasColumnName("last-played-at"); + + b.Property("LastSeenAt") + .HasColumnType("datetime(6)") + .HasColumnName("last-seen-at"); + + b.Property("LastWriteTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("last-write-time-utc"); + + b.Property("LibraryRootId") + .HasColumnType("int") + .HasColumnName("library-root-id"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("media-type"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("relative-path"); + + b.Property("SizeBytes") + .HasColumnType("bigint") + .HasColumnName("size-bytes"); + + b.Property("ThumbnailPath") + .HasMaxLength(512) + .HasColumnType("varchar(512)") + .HasColumnName("thumbnail-path"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at"); + + b.Property("VideoDuration") + .HasColumnType("double") + .HasColumnName("video-duration"); + + b.HasKey("Id") + .HasName("pk-managed-file-record"); + + b.HasIndex("AbsolutePath") + .IsUnique() + .HasDatabaseName("idx-managed-file-record-absolute-path"); + + b.HasIndex("LastPlayedAt") + .HasDatabaseName("idx-managed-file-record-last-played-at"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-file-record-root-id"); + + b.HasIndex("MediaType", "Exists") + .HasDatabaseName("idx-managed-file-record-media-type-exists"); + + b.ToTable("managed-file-record", t => + { + t.HasComment("文件库文件记录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("display-name"); + + b.Property("IsAvailable") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true) + .HasColumnName("is-available"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)") + .HasColumnName("is-enabled"); + + b.Property("LastScanCompletedAt") + .HasColumnType("datetime(6)") + .HasColumnName("last-scan-completed-at"); + + b.Property("LastScanError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)") + .HasColumnName("last-scan-error"); + + b.Property("LastScanStartedAt") + .HasColumnType("datetime(6)") + .HasColumnName("last-scan-started-at"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("path"); + + b.Property("ScanIntervalMinutes") + .HasColumnType("int") + .HasColumnName("scan-interval-minutes"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-library-root"); + + b.HasIndex("Path") + .IsUnique() + .HasDatabaseName("idx-managed-library-root-path"); + + b.ToTable("managed-library-root", t => + { + t.HasComment("文件库根目录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Files") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryRoot"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Navigation("Files"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FileShare-EFCore/Migrations/MySQL/20260522082856_AutoMigration_20260522162758.cs b/FileShare-EFCore/Migrations/MySQL/20260522082856_AutoMigration_20260522162758.cs new file mode 100644 index 0000000..c124ad8 --- /dev/null +++ b/FileShare-EFCore/Migrations/MySQL/20260522082856_AutoMigration_20260522162758.cs @@ -0,0 +1,113 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using MySql.EntityFrameworkCore.Metadata; + +#nullable disable + +namespace FileShare_EFCore.Migrations.MySQL +{ + /// + public partial class AutoMigration_20260522162758 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "managed-library-root", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn), + path = table.Column(type: "varchar(1024)", maxLength: 1024, nullable: false), + displayname = table.Column(name: "display-name", type: "varchar(200)", maxLength: 200, nullable: false), + isenabled = table.Column(name: "is-enabled", type: "tinyint(1)", nullable: false), + isavailable = table.Column(name: "is-available", type: "tinyint(1)", nullable: false, defaultValue: true), + scanintervalminutes = table.Column(name: "scan-interval-minutes", type: "int", nullable: false), + lastscanstartedat = table.Column(name: "last-scan-started-at", type: "datetime(6)", nullable: true), + lastscancompletedat = table.Column(name: "last-scan-completed-at", type: "datetime(6)", nullable: true), + lastscanerror = table.Column(name: "last-scan-error", type: "varchar(2000)", maxLength: 2000, nullable: true), + createdat = table.Column(name: "created-at", type: "datetime(6)", nullable: false), + updatedat = table.Column(name: "updated-at", type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk-managed-library-root", x => x.id); + }, + comment: "文件库根目录") + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "managed-file-record", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn), + libraryrootid = table.Column(name: "library-root-id", type: "int", nullable: false), + filename = table.Column(name: "file-name", type: "varchar(260)", maxLength: 260, nullable: false), + relativepath = table.Column(name: "relative-path", type: "varchar(1024)", maxLength: 1024, nullable: false), + absolutepath = table.Column(name: "absolute-path", type: "varchar(2048)", maxLength: 2048, nullable: false), + extension = table.Column(type: "varchar(32)", maxLength: 32, nullable: false), + sizebytes = table.Column(name: "size-bytes", type: "bigint", nullable: false), + lastwritetimeutc = table.Column(name: "last-write-time-utc", type: "datetime(6)", nullable: false), + mediatype = table.Column(name: "media-type", type: "varchar(20)", maxLength: 20, nullable: false), + contenttype = table.Column(name: "content-type", type: "varchar(100)", maxLength: 100, nullable: false), + exists = table.Column(type: "tinyint(1)", nullable: false), + lastseenat = table.Column(name: "last-seen-at", type: "datetime(6)", nullable: false), + thumbnailpath = table.Column(name: "thumbnail-path", type: "varchar(512)", maxLength: 512, nullable: true), + videoduration = table.Column(name: "video-duration", type: "double", nullable: true), + lastplayedat = table.Column(name: "last-played-at", type: "datetime(6)", nullable: true), + createdat = table.Column(name: "created-at", type: "datetime(6)", nullable: false), + updatedat = table.Column(name: "updated-at", type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk-managed-file-record", x => x.id); + table.ForeignKey( + name: "FK_managed-file-record_managed-library-root_library-root-id", + column: x => x.libraryrootid, + principalTable: "managed-library-root", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }, + comment: "文件库文件记录") + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-absolute-path", + table: "managed-file-record", + column: "absolute-path", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-last-played-at", + table: "managed-file-record", + column: "last-played-at"); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-media-type-exists", + table: "managed-file-record", + columns: new[] { "media-type", "exists" }); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-root-id", + table: "managed-file-record", + column: "library-root-id"); + + migrationBuilder.CreateIndex( + name: "idx-managed-library-root-path", + table: "managed-library-root", + column: "path", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "managed-file-record"); + + migrationBuilder.DropTable( + name: "managed-library-root"); + } + } +} diff --git a/FileShare-EFCore/Migrations/MySQL/20260522084325_AddThumbnailMap.Designer.cs b/FileShare-EFCore/Migrations/MySQL/20260522084325_AddThumbnailMap.Designer.cs new file mode 100644 index 0000000..08fd523 --- /dev/null +++ b/FileShare-EFCore/Migrations/MySQL/20260522084325_AddThumbnailMap.Designer.cs @@ -0,0 +1,444 @@ +// +using System; +using FileShare_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FileShare_EFCore.Migrations.MySQL +{ + [DbContext(typeof(MySqlAppDataContext))] + [Migration("20260522084325_AddThumbnailMap")] + partial class AddThumbnailMap + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("AbsolutePath") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("varchar(2048)") + .HasColumnName("absolute-path"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("Exists") + .HasColumnType("tinyint(1)") + .HasColumnName("exists"); + + b.Property("Extension") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)") + .HasColumnName("extension"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("varchar(260)") + .HasColumnName("file-name"); + + b.Property("LastPlayedAt") + .HasColumnType("datetime(6)") + .HasColumnName("last-played-at"); + + b.Property("LastSeenAt") + .HasColumnType("datetime(6)") + .HasColumnName("last-seen-at"); + + b.Property("LastWriteTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("last-write-time-utc"); + + b.Property("LibraryRootId") + .HasColumnType("int") + .HasColumnName("library-root-id"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("media-type"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("relative-path"); + + b.Property("SizeBytes") + .HasColumnType("bigint") + .HasColumnName("size-bytes"); + + b.Property("ThumbnailId") + .HasColumnType("int") + .HasColumnName("thumbnail-id"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at"); + + b.Property("VideoDuration") + .HasColumnType("double") + .HasColumnName("video-duration"); + + b.HasKey("Id") + .HasName("pk-managed-file-record"); + + b.HasIndex("AbsolutePath") + .IsUnique() + .HasDatabaseName("idx-managed-file-record-absolute-path"); + + b.HasIndex("LastPlayedAt") + .HasDatabaseName("idx-managed-file-record-last-played-at"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-file-record-root-id"); + + b.HasIndex("ThumbnailId") + .HasDatabaseName("idx-managed-file-record-thumbnail-id"); + + b.HasIndex("MediaType", "Exists") + .HasDatabaseName("idx-managed-file-record-media-type-exists"); + + b.ToTable("managed-file-record", t => + { + t.HasComment("文件库文件记录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("display-name"); + + b.Property("IsAvailable") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true) + .HasColumnName("is-available"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)") + .HasColumnName("is-enabled"); + + b.Property("LastScanCompletedAt") + .HasColumnType("datetime(6)") + .HasColumnName("last-scan-completed-at"); + + b.Property("LastScanError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)") + .HasColumnName("last-scan-error"); + + b.Property("LastScanStartedAt") + .HasColumnType("datetime(6)") + .HasColumnName("last-scan-started-at"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("path"); + + b.Property("ScanIntervalMinutes") + .HasColumnType("int") + .HasColumnName("scan-interval-minutes"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-library-root"); + + b.HasIndex("Path") + .IsUnique() + .HasDatabaseName("idx-managed-library-root-path"); + + b.ToTable("managed-library-root", t => + { + t.HasComment("文件库根目录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("LibraryRootId") + .HasColumnType("int") + .HasColumnName("library-root-id"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("relative-path"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-thumbnail-map"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-thumbnail-map-root-id"); + + b.HasIndex("RelativePath") + .IsUnique() + .HasDatabaseName("idx-managed-thumbnail-map-relative-path"); + + b.ToTable("managed-thumbnail-map", t => + { + t.HasComment("文件缩略图映射记录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Files") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail") + .WithMany("Files") + .HasForeignKey("ThumbnailId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("LibraryRoot"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Thumbnails") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryRoot"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Navigation("Files"); + + b.Navigation("Thumbnails"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.Navigation("Files"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FileShare-EFCore/Migrations/MySQL/20260522084325_AddThumbnailMap.cs b/FileShare-EFCore/Migrations/MySQL/20260522084325_AddThumbnailMap.cs new file mode 100644 index 0000000..8fbf96c --- /dev/null +++ b/FileShare-EFCore/Migrations/MySQL/20260522084325_AddThumbnailMap.cs @@ -0,0 +1,101 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using MySql.EntityFrameworkCore.Metadata; + +#nullable disable + +namespace FileShare_EFCore.Migrations.MySQL +{ + /// + public partial class AddThumbnailMap : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "thumbnail-path", + table: "managed-file-record"); + + migrationBuilder.AddColumn( + name: "thumbnail-id", + table: "managed-file-record", + type: "int", + nullable: true); + + migrationBuilder.CreateTable( + name: "managed-thumbnail-map", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn), + libraryrootid = table.Column(name: "library-root-id", type: "int", nullable: false), + relativepath = table.Column(name: "relative-path", type: "varchar(1024)", maxLength: 1024, nullable: false), + contenttype = table.Column(name: "content-type", type: "varchar(100)", maxLength: 100, nullable: false), + createdat = table.Column(name: "created-at", type: "datetime(6)", nullable: false), + updatedat = table.Column(name: "updated-at", type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk-managed-thumbnail-map", x => x.id); + table.ForeignKey( + name: "FK_managed-thumbnail-map_managed-library-root_library-root-id", + column: x => x.libraryrootid, + principalTable: "managed-library-root", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }, + comment: "文件缩略图映射记录") + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-thumbnail-id", + table: "managed-file-record", + column: "thumbnail-id"); + + migrationBuilder.CreateIndex( + name: "idx-managed-thumbnail-map-relative-path", + table: "managed-thumbnail-map", + column: "relative-path", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx-managed-thumbnail-map-root-id", + table: "managed-thumbnail-map", + column: "library-root-id"); + + migrationBuilder.AddForeignKey( + name: "FK_managed-file-record_managed-thumbnail-map_thumbnail-id", + table: "managed-file-record", + column: "thumbnail-id", + principalTable: "managed-thumbnail-map", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_managed-file-record_managed-thumbnail-map_thumbnail-id", + table: "managed-file-record"); + + migrationBuilder.DropTable( + name: "managed-thumbnail-map"); + + migrationBuilder.DropIndex( + name: "idx-managed-file-record-thumbnail-id", + table: "managed-file-record"); + + migrationBuilder.DropColumn( + name: "thumbnail-id", + table: "managed-file-record"); + + migrationBuilder.AddColumn( + name: "thumbnail-path", + table: "managed-file-record", + type: "varchar(512)", + maxLength: 512, + nullable: true); + } + } +} diff --git a/FileShare-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs b/FileShare-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs index 41110a6..d8f3226 100644 --- a/FileShare-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs +++ b/FileShare-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using FileShare_EFCore.Database; using Microsoft.EntityFrameworkCore; @@ -79,6 +79,228 @@ namespace FileShare_EFCore.Migrations.MySQL }); }); + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("AbsolutePath") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("varchar(2048)") + .HasColumnName("absolute-path"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("Exists") + .HasColumnType("tinyint(1)") + .HasColumnName("exists"); + + b.Property("Extension") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("varchar(32)") + .HasColumnName("extension"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("varchar(260)") + .HasColumnName("file-name"); + + b.Property("LastPlayedAt") + .HasColumnType("datetime(6)") + .HasColumnName("last-played-at"); + + b.Property("LastSeenAt") + .HasColumnType("datetime(6)") + .HasColumnName("last-seen-at"); + + b.Property("LastWriteTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("last-write-time-utc"); + + b.Property("LibraryRootId") + .HasColumnType("int") + .HasColumnName("library-root-id"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("media-type"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("relative-path"); + + b.Property("SizeBytes") + .HasColumnType("bigint") + .HasColumnName("size-bytes"); + + b.Property("ThumbnailId") + .HasColumnType("int") + .HasColumnName("thumbnail-id"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at"); + + b.Property("VideoDuration") + .HasColumnType("double") + .HasColumnName("video-duration"); + + b.HasKey("Id") + .HasName("pk-managed-file-record"); + + b.HasIndex("AbsolutePath") + .IsUnique() + .HasDatabaseName("idx-managed-file-record-absolute-path"); + + b.HasIndex("LastPlayedAt") + .HasDatabaseName("idx-managed-file-record-last-played-at"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-file-record-root-id"); + + b.HasIndex("ThumbnailId") + .HasDatabaseName("idx-managed-file-record-thumbnail-id"); + + b.HasIndex("MediaType", "Exists") + .HasDatabaseName("idx-managed-file-record-media-type-exists"); + + b.ToTable("managed-file-record", t => + { + t.HasComment("文件库文件记录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("display-name"); + + b.Property("IsAvailable") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true) + .HasColumnName("is-available"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)") + .HasColumnName("is-enabled"); + + b.Property("LastScanCompletedAt") + .HasColumnType("datetime(6)") + .HasColumnName("last-scan-completed-at"); + + b.Property("LastScanError") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)") + .HasColumnName("last-scan-error"); + + b.Property("LastScanStartedAt") + .HasColumnType("datetime(6)") + .HasColumnName("last-scan-started-at"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("path"); + + b.Property("ScanIntervalMinutes") + .HasColumnType("int") + .HasColumnName("scan-interval-minutes"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-library-root"); + + b.HasIndex("Path") + .IsUnique() + .HasDatabaseName("idx-managed-library-root-path"); + + b.ToTable("managed-library-root", t => + { + t.HasComment("文件库根目录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("created-at"); + + b.Property("LibraryRootId") + .HasColumnType("int") + .HasColumnName("library-root-id"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("varchar(1024)") + .HasColumnName("relative-path"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-thumbnail-map"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-thumbnail-map-root-id"); + + b.HasIndex("RelativePath") + .IsUnique() + .HasDatabaseName("idx-managed-thumbnail-map-relative-path"); + + b.ToTable("managed-thumbnail-map", t => + { + t.HasComment("文件缩略图映射记录"); + }); + }); + modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b => { b.Property("Id") @@ -172,6 +394,47 @@ namespace FileShare_EFCore.Migrations.MySQL t.HasComment("天气预报数据实体"); }); }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Files") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail") + .WithMany("Files") + .HasForeignKey("ThumbnailId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("LibraryRoot"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Thumbnails") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryRoot"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Navigation("Files"); + + b.Navigation("Thumbnails"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.Navigation("Files"); + }); #pragma warning restore 612, 618 } } diff --git a/FileShare-EFCore/Migrations/PostgreSQL/20260522082843_AutoMigration_20260522162758.Designer.cs b/FileShare-EFCore/Migrations/PostgreSQL/20260522082843_AutoMigration_20260522162758.Designer.cs new file mode 100644 index 0000000..b3c5b64 --- /dev/null +++ b/FileShare-EFCore/Migrations/PostgreSQL/20260522082843_AutoMigration_20260522162758.Designer.cs @@ -0,0 +1,383 @@ +// +using System; +using FileShare_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace FileShare_EFCore.Migrations.PostgreSQL +{ + [DbContext(typeof(PostgreSqlAppDataContext))] + [Migration("20260522082843_AutoMigration_20260522162758")] + partial class AutoMigration_20260522162758 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AbsolutePath") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("absolute-path"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("Exists") + .HasColumnType("boolean") + .HasColumnName("exists"); + + b.Property("Extension") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("extension"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("character varying(260)") + .HasColumnName("file-name"); + + b.Property("LastPlayedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last-played-at"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last-seen-at"); + + b.Property("LastWriteTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("last-write-time-utc"); + + b.Property("LibraryRootId") + .HasColumnType("integer") + .HasColumnName("library-root-id"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("media-type"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("relative-path"); + + b.Property("SizeBytes") + .HasColumnType("bigint") + .HasColumnName("size-bytes"); + + b.Property("ThumbnailPath") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("thumbnail-path"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at"); + + b.Property("VideoDuration") + .HasColumnType("double precision") + .HasColumnName("video-duration"); + + b.HasKey("Id") + .HasName("pk-managed-file-record"); + + b.HasIndex("AbsolutePath") + .IsUnique() + .HasDatabaseName("idx-managed-file-record-absolute-path"); + + b.HasIndex("LastPlayedAt") + .HasDatabaseName("idx-managed-file-record-last-played-at"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-file-record-root-id"); + + b.HasIndex("MediaType", "Exists") + .HasDatabaseName("idx-managed-file-record-media-type-exists"); + + b.ToTable("managed-file-record", t => + { + t.HasComment("文件库文件记录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display-name"); + + b.Property("IsAvailable") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is-available"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is-enabled"); + + b.Property("LastScanCompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last-scan-completed-at"); + + b.Property("LastScanError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last-scan-error"); + + b.Property("LastScanStartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last-scan-started-at"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("path"); + + b.Property("ScanIntervalMinutes") + .HasColumnType("integer") + .HasColumnName("scan-interval-minutes"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-library-root"); + + b.HasIndex("Path") + .IsUnique() + .HasDatabaseName("idx-managed-library-root-path"); + + b.ToTable("managed-library-root", t => + { + t.HasComment("文件库根目录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("用户主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("天气预报主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("integer") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Files") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryRoot"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Navigation("Files"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FileShare-EFCore/Migrations/PostgreSQL/20260522082843_AutoMigration_20260522162758.cs b/FileShare-EFCore/Migrations/PostgreSQL/20260522082843_AutoMigration_20260522162758.cs new file mode 100644 index 0000000..0c34771 --- /dev/null +++ b/FileShare-EFCore/Migrations/PostgreSQL/20260522082843_AutoMigration_20260522162758.cs @@ -0,0 +1,111 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace FileShare_EFCore.Migrations.PostgreSQL +{ + /// + public partial class AutoMigration_20260522162758 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "managed-library-root", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + path = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + displayname = table.Column(name: "display-name", type: "character varying(200)", maxLength: 200, nullable: false), + isenabled = table.Column(name: "is-enabled", type: "boolean", nullable: false), + isavailable = table.Column(name: "is-available", type: "boolean", nullable: false, defaultValue: true), + scanintervalminutes = table.Column(name: "scan-interval-minutes", type: "integer", nullable: false), + lastscanstartedat = table.Column(name: "last-scan-started-at", type: "timestamp with time zone", nullable: true), + lastscancompletedat = table.Column(name: "last-scan-completed-at", type: "timestamp with time zone", nullable: true), + lastscanerror = table.Column(name: "last-scan-error", type: "character varying(2000)", maxLength: 2000, nullable: true), + createdat = table.Column(name: "created-at", type: "timestamp with time zone", nullable: false), + updatedat = table.Column(name: "updated-at", type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk-managed-library-root", x => x.id); + }, + comment: "文件库根目录"); + + migrationBuilder.CreateTable( + name: "managed-file-record", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + libraryrootid = table.Column(name: "library-root-id", type: "integer", nullable: false), + filename = table.Column(name: "file-name", type: "character varying(260)", maxLength: 260, nullable: false), + relativepath = table.Column(name: "relative-path", type: "character varying(1024)", maxLength: 1024, nullable: false), + absolutepath = table.Column(name: "absolute-path", type: "character varying(2048)", maxLength: 2048, nullable: false), + extension = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + sizebytes = table.Column(name: "size-bytes", type: "bigint", nullable: false), + lastwritetimeutc = table.Column(name: "last-write-time-utc", type: "timestamp with time zone", nullable: false), + mediatype = table.Column(name: "media-type", type: "character varying(20)", maxLength: 20, nullable: false), + contenttype = table.Column(name: "content-type", type: "character varying(100)", maxLength: 100, nullable: false), + exists = table.Column(type: "boolean", nullable: false), + lastseenat = table.Column(name: "last-seen-at", type: "timestamp with time zone", nullable: false), + thumbnailpath = table.Column(name: "thumbnail-path", type: "character varying(512)", maxLength: 512, nullable: true), + videoduration = table.Column(name: "video-duration", type: "double precision", nullable: true), + lastplayedat = table.Column(name: "last-played-at", type: "timestamp with time zone", nullable: true), + createdat = table.Column(name: "created-at", type: "timestamp with time zone", nullable: false), + updatedat = table.Column(name: "updated-at", type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk-managed-file-record", x => x.id); + table.ForeignKey( + name: "FK_managed-file-record_managed-library-root_library-root-id", + column: x => x.libraryrootid, + principalTable: "managed-library-root", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }, + comment: "文件库文件记录"); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-absolute-path", + table: "managed-file-record", + column: "absolute-path", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-last-played-at", + table: "managed-file-record", + column: "last-played-at"); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-media-type-exists", + table: "managed-file-record", + columns: new[] { "media-type", "exists" }); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-root-id", + table: "managed-file-record", + column: "library-root-id"); + + migrationBuilder.CreateIndex( + name: "idx-managed-library-root-path", + table: "managed-library-root", + column: "path", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "managed-file-record"); + + migrationBuilder.DropTable( + name: "managed-library-root"); + } + } +} diff --git a/FileShare-EFCore/Migrations/PostgreSQL/20260522084325_AddThumbnailMap.Designer.cs b/FileShare-EFCore/Migrations/PostgreSQL/20260522084325_AddThumbnailMap.Designer.cs new file mode 100644 index 0000000..896b441 --- /dev/null +++ b/FileShare-EFCore/Migrations/PostgreSQL/20260522084325_AddThumbnailMap.Designer.cs @@ -0,0 +1,459 @@ +// +using System; +using FileShare_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace FileShare_EFCore.Migrations.PostgreSQL +{ + [DbContext(typeof(PostgreSqlAppDataContext))] + [Migration("20260522084325_AddThumbnailMap")] + partial class AddThumbnailMap + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AbsolutePath") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("absolute-path"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("Exists") + .HasColumnType("boolean") + .HasColumnName("exists"); + + b.Property("Extension") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("extension"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("character varying(260)") + .HasColumnName("file-name"); + + b.Property("LastPlayedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last-played-at"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last-seen-at"); + + b.Property("LastWriteTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("last-write-time-utc"); + + b.Property("LibraryRootId") + .HasColumnType("integer") + .HasColumnName("library-root-id"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("media-type"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("relative-path"); + + b.Property("SizeBytes") + .HasColumnType("bigint") + .HasColumnName("size-bytes"); + + b.Property("ThumbnailId") + .HasColumnType("integer") + .HasColumnName("thumbnail-id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at"); + + b.Property("VideoDuration") + .HasColumnType("double precision") + .HasColumnName("video-duration"); + + b.HasKey("Id") + .HasName("pk-managed-file-record"); + + b.HasIndex("AbsolutePath") + .IsUnique() + .HasDatabaseName("idx-managed-file-record-absolute-path"); + + b.HasIndex("LastPlayedAt") + .HasDatabaseName("idx-managed-file-record-last-played-at"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-file-record-root-id"); + + b.HasIndex("ThumbnailId") + .HasDatabaseName("idx-managed-file-record-thumbnail-id"); + + b.HasIndex("MediaType", "Exists") + .HasDatabaseName("idx-managed-file-record-media-type-exists"); + + b.ToTable("managed-file-record", t => + { + t.HasComment("文件库文件记录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display-name"); + + b.Property("IsAvailable") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is-available"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is-enabled"); + + b.Property("LastScanCompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last-scan-completed-at"); + + b.Property("LastScanError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last-scan-error"); + + b.Property("LastScanStartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last-scan-started-at"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("path"); + + b.Property("ScanIntervalMinutes") + .HasColumnType("integer") + .HasColumnName("scan-interval-minutes"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-library-root"); + + b.HasIndex("Path") + .IsUnique() + .HasDatabaseName("idx-managed-library-root-path"); + + b.ToTable("managed-library-root", t => + { + t.HasComment("文件库根目录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("LibraryRootId") + .HasColumnType("integer") + .HasColumnName("library-root-id"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("relative-path"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-thumbnail-map"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-thumbnail-map-root-id"); + + b.HasIndex("RelativePath") + .IsUnique() + .HasDatabaseName("idx-managed-thumbnail-map-relative-path"); + + b.ToTable("managed-thumbnail-map", t => + { + t.HasComment("文件缩略图映射记录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("用户主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id") + .HasComment("天气预报主键"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("integer") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Files") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail") + .WithMany("Files") + .HasForeignKey("ThumbnailId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("LibraryRoot"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Thumbnails") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryRoot"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Navigation("Files"); + + b.Navigation("Thumbnails"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.Navigation("Files"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FileShare-EFCore/Migrations/PostgreSQL/20260522084325_AddThumbnailMap.cs b/FileShare-EFCore/Migrations/PostgreSQL/20260522084325_AddThumbnailMap.cs new file mode 100644 index 0000000..e5fb81b --- /dev/null +++ b/FileShare-EFCore/Migrations/PostgreSQL/20260522084325_AddThumbnailMap.cs @@ -0,0 +1,100 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace FileShare_EFCore.Migrations.PostgreSQL +{ + /// + public partial class AddThumbnailMap : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "thumbnail-path", + table: "managed-file-record"); + + migrationBuilder.AddColumn( + name: "thumbnail-id", + table: "managed-file-record", + type: "integer", + nullable: true); + + migrationBuilder.CreateTable( + name: "managed-thumbnail-map", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + libraryrootid = table.Column(name: "library-root-id", type: "integer", nullable: false), + relativepath = table.Column(name: "relative-path", type: "character varying(1024)", maxLength: 1024, nullable: false), + contenttype = table.Column(name: "content-type", type: "character varying(100)", maxLength: 100, nullable: false), + createdat = table.Column(name: "created-at", type: "timestamp with time zone", nullable: false), + updatedat = table.Column(name: "updated-at", type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk-managed-thumbnail-map", x => x.id); + table.ForeignKey( + name: "FK_managed-thumbnail-map_managed-library-root_library-root-id", + column: x => x.libraryrootid, + principalTable: "managed-library-root", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }, + comment: "文件缩略图映射记录"); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-thumbnail-id", + table: "managed-file-record", + column: "thumbnail-id"); + + migrationBuilder.CreateIndex( + name: "idx-managed-thumbnail-map-relative-path", + table: "managed-thumbnail-map", + column: "relative-path", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx-managed-thumbnail-map-root-id", + table: "managed-thumbnail-map", + column: "library-root-id"); + + migrationBuilder.AddForeignKey( + name: "FK_managed-file-record_managed-thumbnail-map_thumbnail-id", + table: "managed-file-record", + column: "thumbnail-id", + principalTable: "managed-thumbnail-map", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_managed-file-record_managed-thumbnail-map_thumbnail-id", + table: "managed-file-record"); + + migrationBuilder.DropTable( + name: "managed-thumbnail-map"); + + migrationBuilder.DropIndex( + name: "idx-managed-file-record-thumbnail-id", + table: "managed-file-record"); + + migrationBuilder.DropColumn( + name: "thumbnail-id", + table: "managed-file-record"); + + migrationBuilder.AddColumn( + name: "thumbnail-path", + table: "managed-file-record", + type: "character varying(512)", + maxLength: 512, + nullable: true); + } + } +} diff --git a/FileShare-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs b/FileShare-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs index 8cea9b3..2134fc5 100644 --- a/FileShare-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs +++ b/FileShare-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using FileShare_EFCore.Database; using Microsoft.EntityFrameworkCore; @@ -84,6 +84,234 @@ namespace FileShare_EFCore.Migrations.PostgreSQL }); }); + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AbsolutePath") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)") + .HasColumnName("absolute-path"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("Exists") + .HasColumnType("boolean") + .HasColumnName("exists"); + + b.Property("Extension") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("extension"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("character varying(260)") + .HasColumnName("file-name"); + + b.Property("LastPlayedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last-played-at"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last-seen-at"); + + b.Property("LastWriteTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("last-write-time-utc"); + + b.Property("LibraryRootId") + .HasColumnType("integer") + .HasColumnName("library-root-id"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("media-type"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("relative-path"); + + b.Property("SizeBytes") + .HasColumnType("bigint") + .HasColumnName("size-bytes"); + + b.Property("ThumbnailId") + .HasColumnType("integer") + .HasColumnName("thumbnail-id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at"); + + b.Property("VideoDuration") + .HasColumnType("double precision") + .HasColumnName("video-duration"); + + b.HasKey("Id") + .HasName("pk-managed-file-record"); + + b.HasIndex("AbsolutePath") + .IsUnique() + .HasDatabaseName("idx-managed-file-record-absolute-path"); + + b.HasIndex("LastPlayedAt") + .HasDatabaseName("idx-managed-file-record-last-played-at"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-file-record-root-id"); + + b.HasIndex("ThumbnailId") + .HasDatabaseName("idx-managed-file-record-thumbnail-id"); + + b.HasIndex("MediaType", "Exists") + .HasDatabaseName("idx-managed-file-record-media-type-exists"); + + b.ToTable("managed-file-record", t => + { + t.HasComment("文件库文件记录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("display-name"); + + b.Property("IsAvailable") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is-available"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("is-enabled"); + + b.Property("LastScanCompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last-scan-completed-at"); + + b.Property("LastScanError") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("last-scan-error"); + + b.Property("LastScanStartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last-scan-started-at"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("path"); + + b.Property("ScanIntervalMinutes") + .HasColumnType("integer") + .HasColumnName("scan-interval-minutes"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-library-root"); + + b.HasIndex("Path") + .IsUnique() + .HasDatabaseName("idx-managed-library-root-path"); + + b.ToTable("managed-library-root", t => + { + t.HasComment("文件库根目录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created-at"); + + b.Property("LibraryRootId") + .HasColumnType("integer") + .HasColumnName("library-root-id"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasColumnName("relative-path"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-thumbnail-map"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-thumbnail-map-root-id"); + + b.HasIndex("RelativePath") + .IsUnique() + .HasDatabaseName("idx-managed-thumbnail-map-relative-path"); + + b.ToTable("managed-thumbnail-map", t => + { + t.HasComment("文件缩略图映射记录"); + }); + }); + modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b => { b.Property("Id") @@ -181,6 +409,47 @@ namespace FileShare_EFCore.Migrations.PostgreSQL t.HasComment("天气预报数据实体"); }); }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Files") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail") + .WithMany("Files") + .HasForeignKey("ThumbnailId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("LibraryRoot"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Thumbnails") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryRoot"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Navigation("Files"); + + b.Navigation("Thumbnails"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.Navigation("Files"); + }); #pragma warning restore 612, 618 } } diff --git a/FileShare-EFCore/Migrations/SQLite/20260522082814_AutoMigration_20260522162758.Designer.cs b/FileShare-EFCore/Migrations/SQLite/20260522082814_AutoMigration_20260522162758.Designer.cs new file mode 100644 index 0000000..e07bc0b --- /dev/null +++ b/FileShare-EFCore/Migrations/SQLite/20260522082814_AutoMigration_20260522162758.Designer.cs @@ -0,0 +1,368 @@ +// +using System; +using FileShare_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FileShare_EFCore.Migrations.SQLite +{ + [DbContext(typeof(SqliteAppDataContext))] + [Migration("20260522082814_AutoMigration_20260522162758")] + partial class AutoMigration_20260522162758 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); + + modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("TEXT") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("AbsolutePath") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("TEXT") + .HasColumnName("absolute-path"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at"); + + b.Property("Exists") + .HasColumnType("INTEGER") + .HasColumnName("exists"); + + b.Property("Extension") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT") + .HasColumnName("extension"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("TEXT") + .HasColumnName("file-name"); + + b.Property("LastPlayedAt") + .HasColumnType("TEXT") + .HasColumnName("last-played-at"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT") + .HasColumnName("last-seen-at"); + + b.Property("LastWriteTimeUtc") + .HasColumnType("TEXT") + .HasColumnName("last-write-time-utc"); + + b.Property("LibraryRootId") + .HasColumnType("INTEGER") + .HasColumnName("library-root-id"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("media-type"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT") + .HasColumnName("relative-path"); + + b.Property("SizeBytes") + .HasColumnType("INTEGER") + .HasColumnName("size-bytes"); + + b.Property("ThumbnailPath") + .HasMaxLength(512) + .HasColumnType("TEXT") + .HasColumnName("thumbnail-path"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at"); + + b.Property("VideoDuration") + .HasColumnType("REAL") + .HasColumnName("video-duration"); + + b.HasKey("Id") + .HasName("pk-managed-file-record"); + + b.HasIndex("AbsolutePath") + .IsUnique() + .HasDatabaseName("idx-managed-file-record-absolute-path"); + + b.HasIndex("LastPlayedAt") + .HasDatabaseName("idx-managed-file-record-last-played-at"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-file-record-root-id"); + + b.HasIndex("MediaType", "Exists") + .HasDatabaseName("idx-managed-file-record-media-type-exists"); + + b.ToTable("managed-file-record", t => + { + t.HasComment("文件库文件记录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("display-name"); + + b.Property("IsAvailable") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true) + .HasColumnName("is-available"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER") + .HasColumnName("is-enabled"); + + b.Property("LastScanCompletedAt") + .HasColumnType("TEXT") + .HasColumnName("last-scan-completed-at"); + + b.Property("LastScanError") + .HasMaxLength(2000) + .HasColumnType("TEXT") + .HasColumnName("last-scan-error"); + + b.Property("LastScanStartedAt") + .HasColumnType("TEXT") + .HasColumnName("last-scan-started-at"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property("ScanIntervalMinutes") + .HasColumnType("INTEGER") + .HasColumnName("scan-interval-minutes"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-library-root"); + + b.HasIndex("Path") + .IsUnique() + .HasDatabaseName("idx-managed-library-root-path"); + + b.ToTable("managed-library-root", t => + { + t.HasComment("文件库根目录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id") + .HasComment("用户主键"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id") + .HasComment("天气预报主键"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("INTEGER") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Files") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryRoot"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Navigation("Files"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FileShare-EFCore/Migrations/SQLite/20260522082814_AutoMigration_20260522162758.cs b/FileShare-EFCore/Migrations/SQLite/20260522082814_AutoMigration_20260522162758.cs new file mode 100644 index 0000000..47c141a --- /dev/null +++ b/FileShare-EFCore/Migrations/SQLite/20260522082814_AutoMigration_20260522162758.cs @@ -0,0 +1,59 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FileShare_EFCore.Migrations.SQLite +{ + /// + public partial class AutoMigration_20260522162758 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "last-played-at", + table: "managed-file-record", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "thumbnail-path", + table: "managed-file-record", + type: "TEXT", + maxLength: 512, + nullable: true); + + migrationBuilder.AddColumn( + name: "video-duration", + table: "managed-file-record", + type: "REAL", + nullable: true); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-last-played-at", + table: "managed-file-record", + column: "last-played-at"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "idx-managed-file-record-last-played-at", + table: "managed-file-record"); + + migrationBuilder.DropColumn( + name: "last-played-at", + table: "managed-file-record"); + + migrationBuilder.DropColumn( + name: "thumbnail-path", + table: "managed-file-record"); + + migrationBuilder.DropColumn( + name: "video-duration", + table: "managed-file-record"); + } + } +} diff --git a/FileShare-EFCore/Migrations/SQLite/20260522084259_AddThumbnailMap.Designer.cs b/FileShare-EFCore/Migrations/SQLite/20260522084259_AddThumbnailMap.Designer.cs new file mode 100644 index 0000000..8241862 --- /dev/null +++ b/FileShare-EFCore/Migrations/SQLite/20260522084259_AddThumbnailMap.Designer.cs @@ -0,0 +1,442 @@ +// +using System; +using FileShare_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FileShare_EFCore.Migrations.SQLite +{ + [DbContext(typeof(SqliteAppDataContext))] + [Migration("20260522084259_AddThumbnailMap")] + partial class AddThumbnailMap + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); + + modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("TEXT") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("AbsolutePath") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("TEXT") + .HasColumnName("absolute-path"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at"); + + b.Property("Exists") + .HasColumnType("INTEGER") + .HasColumnName("exists"); + + b.Property("Extension") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT") + .HasColumnName("extension"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("TEXT") + .HasColumnName("file-name"); + + b.Property("LastPlayedAt") + .HasColumnType("TEXT") + .HasColumnName("last-played-at"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT") + .HasColumnName("last-seen-at"); + + b.Property("LastWriteTimeUtc") + .HasColumnType("TEXT") + .HasColumnName("last-write-time-utc"); + + b.Property("LibraryRootId") + .HasColumnType("INTEGER") + .HasColumnName("library-root-id"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("media-type"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT") + .HasColumnName("relative-path"); + + b.Property("SizeBytes") + .HasColumnType("INTEGER") + .HasColumnName("size-bytes"); + + b.Property("ThumbnailId") + .HasColumnType("INTEGER") + .HasColumnName("thumbnail-id"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at"); + + b.Property("VideoDuration") + .HasColumnType("REAL") + .HasColumnName("video-duration"); + + b.HasKey("Id") + .HasName("pk-managed-file-record"); + + b.HasIndex("AbsolutePath") + .IsUnique() + .HasDatabaseName("idx-managed-file-record-absolute-path"); + + b.HasIndex("LastPlayedAt") + .HasDatabaseName("idx-managed-file-record-last-played-at"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-file-record-root-id"); + + b.HasIndex("ThumbnailId") + .HasDatabaseName("idx-managed-file-record-thumbnail-id"); + + b.HasIndex("MediaType", "Exists") + .HasDatabaseName("idx-managed-file-record-media-type-exists"); + + b.ToTable("managed-file-record", t => + { + t.HasComment("文件库文件记录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("display-name"); + + b.Property("IsAvailable") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true) + .HasColumnName("is-available"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER") + .HasColumnName("is-enabled"); + + b.Property("LastScanCompletedAt") + .HasColumnType("TEXT") + .HasColumnName("last-scan-completed-at"); + + b.Property("LastScanError") + .HasMaxLength(2000) + .HasColumnType("TEXT") + .HasColumnName("last-scan-error"); + + b.Property("LastScanStartedAt") + .HasColumnType("TEXT") + .HasColumnName("last-scan-started-at"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property("ScanIntervalMinutes") + .HasColumnType("INTEGER") + .HasColumnName("scan-interval-minutes"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-library-root"); + + b.HasIndex("Path") + .IsUnique() + .HasDatabaseName("idx-managed-library-root-path"); + + b.ToTable("managed-library-root", t => + { + t.HasComment("文件库根目录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at"); + + b.Property("LibraryRootId") + .HasColumnType("INTEGER") + .HasColumnName("library-root-id"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT") + .HasColumnName("relative-path"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-thumbnail-map"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-thumbnail-map-root-id"); + + b.HasIndex("RelativePath") + .IsUnique() + .HasDatabaseName("idx-managed-thumbnail-map-relative-path"); + + b.ToTable("managed-thumbnail-map", t => + { + t.HasComment("文件缩略图映射记录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id") + .HasComment("用户主键"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id") + .HasComment("天气预报主键"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("INTEGER") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Files") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail") + .WithMany("Files") + .HasForeignKey("ThumbnailId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("LibraryRoot"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Thumbnails") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryRoot"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Navigation("Files"); + + b.Navigation("Thumbnails"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.Navigation("Files"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FileShare-EFCore/Migrations/SQLite/20260522084259_AddThumbnailMap.cs b/FileShare-EFCore/Migrations/SQLite/20260522084259_AddThumbnailMap.cs new file mode 100644 index 0000000..f03362f --- /dev/null +++ b/FileShare-EFCore/Migrations/SQLite/20260522084259_AddThumbnailMap.cs @@ -0,0 +1,99 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FileShare_EFCore.Migrations.SQLite +{ + /// + public partial class AddThumbnailMap : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "thumbnail-path", + table: "managed-file-record"); + + migrationBuilder.AddColumn( + name: "thumbnail-id", + table: "managed-file-record", + type: "INTEGER", + nullable: true); + + migrationBuilder.CreateTable( + name: "managed-thumbnail-map", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + libraryrootid = table.Column(name: "library-root-id", type: "INTEGER", nullable: false), + relativepath = table.Column(name: "relative-path", type: "TEXT", maxLength: 1024, nullable: false), + contenttype = table.Column(name: "content-type", type: "TEXT", maxLength: 100, nullable: false), + createdat = table.Column(name: "created-at", type: "TEXT", nullable: false), + updatedat = table.Column(name: "updated-at", type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk-managed-thumbnail-map", x => x.id); + table.ForeignKey( + name: "FK_managed-thumbnail-map_managed-library-root_library-root-id", + column: x => x.libraryrootid, + principalTable: "managed-library-root", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }, + comment: "文件缩略图映射记录"); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-thumbnail-id", + table: "managed-file-record", + column: "thumbnail-id"); + + migrationBuilder.CreateIndex( + name: "idx-managed-thumbnail-map-relative-path", + table: "managed-thumbnail-map", + column: "relative-path", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx-managed-thumbnail-map-root-id", + table: "managed-thumbnail-map", + column: "library-root-id"); + + migrationBuilder.AddForeignKey( + name: "FK_managed-file-record_managed-thumbnail-map_thumbnail-id", + table: "managed-file-record", + column: "thumbnail-id", + principalTable: "managed-thumbnail-map", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_managed-file-record_managed-thumbnail-map_thumbnail-id", + table: "managed-file-record"); + + migrationBuilder.DropTable( + name: "managed-thumbnail-map"); + + migrationBuilder.DropIndex( + name: "idx-managed-file-record-thumbnail-id", + table: "managed-file-record"); + + migrationBuilder.DropColumn( + name: "thumbnail-id", + table: "managed-file-record"); + + migrationBuilder.AddColumn( + name: "thumbnail-path", + table: "managed-file-record", + type: "TEXT", + maxLength: 512, + nullable: true); + } + } +} diff --git a/FileShare-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs b/FileShare-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs index c6686f2..788881c 100644 --- a/FileShare-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs +++ b/FileShare-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using FileShare_EFCore.Database; using Microsoft.EntityFrameworkCore; @@ -116,6 +116,10 @@ namespace FileShare_EFCore.Migrations.SQLite .HasColumnType("TEXT") .HasColumnName("file-name"); + b.Property("LastPlayedAt") + .HasColumnType("TEXT") + .HasColumnName("last-played-at"); + b.Property("LastSeenAt") .HasColumnType("TEXT") .HasColumnName("last-seen-at"); @@ -144,10 +148,18 @@ namespace FileShare_EFCore.Migrations.SQLite .HasColumnType("INTEGER") .HasColumnName("size-bytes"); + b.Property("ThumbnailId") + .HasColumnType("INTEGER") + .HasColumnName("thumbnail-id"); + b.Property("UpdatedAt") .HasColumnType("TEXT") .HasColumnName("updated-at"); + b.Property("VideoDuration") + .HasColumnType("REAL") + .HasColumnName("video-duration"); + b.HasKey("Id") .HasName("pk-managed-file-record"); @@ -155,9 +167,15 @@ namespace FileShare_EFCore.Migrations.SQLite .IsUnique() .HasDatabaseName("idx-managed-file-record-absolute-path"); + b.HasIndex("LastPlayedAt") + .HasDatabaseName("idx-managed-file-record-last-played-at"); + b.HasIndex("LibraryRootId") .HasDatabaseName("idx-managed-file-record-root-id"); + b.HasIndex("ThumbnailId") + .HasDatabaseName("idx-managed-file-record-thumbnail-id"); + b.HasIndex("MediaType", "Exists") .HasDatabaseName("idx-managed-file-record-media-type-exists"); @@ -234,6 +252,53 @@ namespace FileShare_EFCore.Migrations.SQLite }); }); + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created-at"); + + b.Property("LibraryRootId") + .HasColumnType("INTEGER") + .HasColumnName("library-root-id"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("TEXT") + .HasColumnName("relative-path"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-thumbnail-map"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-thumbnail-map-root-id"); + + b.HasIndex("RelativePath") + .IsUnique() + .HasDatabaseName("idx-managed-thumbnail-map-relative-path"); + + b.ToTable("managed-thumbnail-map", t => + { + t.HasComment("文件缩略图映射记录"); + }); + }); + modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b => { b.Property("Id") @@ -336,10 +401,35 @@ namespace FileShare_EFCore.Migrations.SQLite .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail") + .WithMany("Files") + .HasForeignKey("ThumbnailId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("LibraryRoot"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Thumbnails") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.Navigation("LibraryRoot"); }); modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Navigation("Files"); + + b.Navigation("Thumbnails"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => { b.Navigation("Files"); }); diff --git a/FileShare-EFCore/Migrations/SqlServer/20260522082829_AutoMigration_20260522162758.Designer.cs b/FileShare-EFCore/Migrations/SqlServer/20260522082829_AutoMigration_20260522162758.Designer.cs new file mode 100644 index 0000000..b18e058 --- /dev/null +++ b/FileShare-EFCore/Migrations/SqlServer/20260522082829_AutoMigration_20260522162758.Designer.cs @@ -0,0 +1,383 @@ +// +using System; +using FileShare_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FileShare_EFCore.Migrations.SqlServer +{ + [DbContext(typeof(SqlServerAppDataContext))] + [Migration("20260522082829_AutoMigration_20260522162758")] + partial class AutoMigration_20260522162758 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime2") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AbsolutePath") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("absolute-path"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("Exists") + .HasColumnType("bit") + .HasColumnName("exists"); + + b.Property("Extension") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)") + .HasColumnName("extension"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("nvarchar(260)") + .HasColumnName("file-name"); + + b.Property("LastPlayedAt") + .HasColumnType("datetime2") + .HasColumnName("last-played-at"); + + b.Property("LastSeenAt") + .HasColumnType("datetime2") + .HasColumnName("last-seen-at"); + + b.Property("LastWriteTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("last-write-time-utc"); + + b.Property("LibraryRootId") + .HasColumnType("int") + .HasColumnName("library-root-id"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)") + .HasColumnName("media-type"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)") + .HasColumnName("relative-path"); + + b.Property("SizeBytes") + .HasColumnType("bigint") + .HasColumnName("size-bytes"); + + b.Property("ThumbnailPath") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)") + .HasColumnName("thumbnail-path"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at"); + + b.Property("VideoDuration") + .HasColumnType("float") + .HasColumnName("video-duration"); + + b.HasKey("Id") + .HasName("pk-managed-file-record"); + + b.HasIndex("AbsolutePath") + .IsUnique() + .HasDatabaseName("idx-managed-file-record-absolute-path"); + + b.HasIndex("LastPlayedAt") + .HasDatabaseName("idx-managed-file-record-last-played-at"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-file-record-root-id"); + + b.HasIndex("MediaType", "Exists") + .HasDatabaseName("idx-managed-file-record-media-type-exists"); + + b.ToTable("managed-file-record", t => + { + t.HasComment("文件库文件记录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("display-name"); + + b.Property("IsAvailable") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true) + .HasColumnName("is-available"); + + b.Property("IsEnabled") + .HasColumnType("bit") + .HasColumnName("is-enabled"); + + b.Property("LastScanCompletedAt") + .HasColumnType("datetime2") + .HasColumnName("last-scan-completed-at"); + + b.Property("LastScanError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("last-scan-error"); + + b.Property("LastScanStartedAt") + .HasColumnType("datetime2") + .HasColumnName("last-scan-started-at"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)") + .HasColumnName("path"); + + b.Property("ScanIntervalMinutes") + .HasColumnType("int") + .HasColumnName("scan-interval-minutes"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-library-root"); + + b.HasIndex("Path") + .IsUnique() + .HasDatabaseName("idx-managed-library-root-path"); + + b.ToTable("managed-library-root", t => + { + t.HasComment("文件库根目录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Files") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryRoot"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Navigation("Files"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FileShare-EFCore/Migrations/SqlServer/20260522082829_AutoMigration_20260522162758.cs b/FileShare-EFCore/Migrations/SqlServer/20260522082829_AutoMigration_20260522162758.cs new file mode 100644 index 0000000..1b41adc --- /dev/null +++ b/FileShare-EFCore/Migrations/SqlServer/20260522082829_AutoMigration_20260522162758.cs @@ -0,0 +1,110 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FileShare_EFCore.Migrations.SqlServer +{ + /// + public partial class AutoMigration_20260522162758 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "managed-library-root", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + path = table.Column(type: "nvarchar(1024)", maxLength: 1024, nullable: false), + displayname = table.Column(name: "display-name", type: "nvarchar(200)", maxLength: 200, nullable: false), + isenabled = table.Column(name: "is-enabled", type: "bit", nullable: false), + isavailable = table.Column(name: "is-available", type: "bit", nullable: false, defaultValue: true), + scanintervalminutes = table.Column(name: "scan-interval-minutes", type: "int", nullable: false), + lastscanstartedat = table.Column(name: "last-scan-started-at", type: "datetime2", nullable: true), + lastscancompletedat = table.Column(name: "last-scan-completed-at", type: "datetime2", nullable: true), + lastscanerror = table.Column(name: "last-scan-error", type: "nvarchar(2000)", maxLength: 2000, nullable: true), + createdat = table.Column(name: "created-at", type: "datetime2", nullable: false), + updatedat = table.Column(name: "updated-at", type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk-managed-library-root", x => x.id); + }, + comment: "文件库根目录"); + + migrationBuilder.CreateTable( + name: "managed-file-record", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + libraryrootid = table.Column(name: "library-root-id", type: "int", nullable: false), + filename = table.Column(name: "file-name", type: "nvarchar(260)", maxLength: 260, nullable: false), + relativepath = table.Column(name: "relative-path", type: "nvarchar(1024)", maxLength: 1024, nullable: false), + absolutepath = table.Column(name: "absolute-path", type: "nvarchar(2048)", maxLength: 2048, nullable: false), + extension = table.Column(type: "nvarchar(32)", maxLength: 32, nullable: false), + sizebytes = table.Column(name: "size-bytes", type: "bigint", nullable: false), + lastwritetimeutc = table.Column(name: "last-write-time-utc", type: "datetime2", nullable: false), + mediatype = table.Column(name: "media-type", type: "nvarchar(20)", maxLength: 20, nullable: false), + contenttype = table.Column(name: "content-type", type: "nvarchar(100)", maxLength: 100, nullable: false), + exists = table.Column(type: "bit", nullable: false), + lastseenat = table.Column(name: "last-seen-at", type: "datetime2", nullable: false), + thumbnailpath = table.Column(name: "thumbnail-path", type: "nvarchar(512)", maxLength: 512, nullable: true), + videoduration = table.Column(name: "video-duration", type: "float", nullable: true), + lastplayedat = table.Column(name: "last-played-at", type: "datetime2", nullable: true), + createdat = table.Column(name: "created-at", type: "datetime2", nullable: false), + updatedat = table.Column(name: "updated-at", type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk-managed-file-record", x => x.id); + table.ForeignKey( + name: "FK_managed-file-record_managed-library-root_library-root-id", + column: x => x.libraryrootid, + principalTable: "managed-library-root", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }, + comment: "文件库文件记录"); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-absolute-path", + table: "managed-file-record", + column: "absolute-path", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-last-played-at", + table: "managed-file-record", + column: "last-played-at"); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-media-type-exists", + table: "managed-file-record", + columns: new[] { "media-type", "exists" }); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-root-id", + table: "managed-file-record", + column: "library-root-id"); + + migrationBuilder.CreateIndex( + name: "idx-managed-library-root-path", + table: "managed-library-root", + column: "path", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "managed-file-record"); + + migrationBuilder.DropTable( + name: "managed-library-root"); + } + } +} diff --git a/FileShare-EFCore/Migrations/SqlServer/20260522084325_AddThumbnailMap.Designer.cs b/FileShare-EFCore/Migrations/SqlServer/20260522084325_AddThumbnailMap.Designer.cs new file mode 100644 index 0000000..4b391a2 --- /dev/null +++ b/FileShare-EFCore/Migrations/SqlServer/20260522084325_AddThumbnailMap.Designer.cs @@ -0,0 +1,459 @@ +// +using System; +using FileShare_EFCore.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FileShare_EFCore.Migrations.SqlServer +{ + [DbContext(typeof(SqlServerAppDataContext))] + [Migration("20260522084325_AddThumbnailMap")] + partial class AddThumbnailMap + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("Device") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("device"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2") + .HasColumnName("expires-at"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)") + .HasColumnName("ip-address"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("replaced-by-token-hash"); + + b.Property("RevokedAt") + .HasColumnType("datetime2") + .HasColumnName("revoked-at"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)") + .HasColumnName("token-hash"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("user-id"); + + b.HasKey("Id") + .HasName("pk-api-refresh-token"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("idx-api-refresh-token-hash"); + + b.HasIndex("UserId") + .HasDatabaseName("idx-api-refresh-token-user-id"); + + b.ToTable("api-refresh-token", t => + { + t.HasComment("API refresh token"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AbsolutePath") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("absolute-path"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("Exists") + .HasColumnType("bit") + .HasColumnName("exists"); + + b.Property("Extension") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)") + .HasColumnName("extension"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("nvarchar(260)") + .HasColumnName("file-name"); + + b.Property("LastPlayedAt") + .HasColumnType("datetime2") + .HasColumnName("last-played-at"); + + b.Property("LastSeenAt") + .HasColumnType("datetime2") + .HasColumnName("last-seen-at"); + + b.Property("LastWriteTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("last-write-time-utc"); + + b.Property("LibraryRootId") + .HasColumnType("int") + .HasColumnName("library-root-id"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)") + .HasColumnName("media-type"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)") + .HasColumnName("relative-path"); + + b.Property("SizeBytes") + .HasColumnType("bigint") + .HasColumnName("size-bytes"); + + b.Property("ThumbnailId") + .HasColumnType("int") + .HasColumnName("thumbnail-id"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at"); + + b.Property("VideoDuration") + .HasColumnType("float") + .HasColumnName("video-duration"); + + b.HasKey("Id") + .HasName("pk-managed-file-record"); + + b.HasIndex("AbsolutePath") + .IsUnique() + .HasDatabaseName("idx-managed-file-record-absolute-path"); + + b.HasIndex("LastPlayedAt") + .HasDatabaseName("idx-managed-file-record-last-played-at"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-file-record-root-id"); + + b.HasIndex("ThumbnailId") + .HasDatabaseName("idx-managed-file-record-thumbnail-id"); + + b.HasIndex("MediaType", "Exists") + .HasDatabaseName("idx-managed-file-record-media-type-exists"); + + b.ToTable("managed-file-record", t => + { + t.HasComment("文件库文件记录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("display-name"); + + b.Property("IsAvailable") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true) + .HasColumnName("is-available"); + + b.Property("IsEnabled") + .HasColumnType("bit") + .HasColumnName("is-enabled"); + + b.Property("LastScanCompletedAt") + .HasColumnType("datetime2") + .HasColumnName("last-scan-completed-at"); + + b.Property("LastScanError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("last-scan-error"); + + b.Property("LastScanStartedAt") + .HasColumnType("datetime2") + .HasColumnName("last-scan-started-at"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)") + .HasColumnName("path"); + + b.Property("ScanIntervalMinutes") + .HasColumnType("int") + .HasColumnName("scan-interval-minutes"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-library-root"); + + b.HasIndex("Path") + .IsUnique() + .HasDatabaseName("idx-managed-library-root-path"); + + b.ToTable("managed-library-root", t => + { + t.HasComment("文件库根目录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("LibraryRootId") + .HasColumnType("int") + .HasColumnName("library-root-id"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)") + .HasColumnName("relative-path"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-thumbnail-map"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-thumbnail-map-root-id"); + + b.HasIndex("RelativePath") + .IsUnique() + .HasDatabaseName("idx-managed-thumbnail-map-relative-path"); + + b.ToTable("managed-thumbnail-map", t => + { + t.HasComment("文件缩略图映射记录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("用户主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("email") + .HasComment("用户邮箱"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("name") + .HasComment("用户名称"); + + b.Property("PasswordHash") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("password-hash") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("phone-number") + .HasComment("电话号码"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-user"); + + b.ToTable("user", t => + { + t.HasComment("用户实体,演示数据库 CRUD 操作"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id") + .HasComment("天气预报主键"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at") + .HasComment("创建时间"); + + b.Property("Date") + .HasColumnType("date") + .HasColumnName("date") + .HasComment("预报日期"); + + b.Property("Summary") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("summary") + .HasComment("天气摘要"); + + b.Property("TemperatureC") + .HasColumnType("int") + .HasColumnName("temperature-c") + .HasComment("摄氏温度"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at") + .HasComment("更新时间"); + + b.HasKey("Id") + .HasName("pk-weather-forecast"); + + b.ToTable("weather-forecast", t => + { + t.HasComment("天气预报数据实体"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Files") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail") + .WithMany("Files") + .HasForeignKey("ThumbnailId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("LibraryRoot"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Thumbnails") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryRoot"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Navigation("Files"); + + b.Navigation("Thumbnails"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.Navigation("Files"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FileShare-EFCore/Migrations/SqlServer/20260522084325_AddThumbnailMap.cs b/FileShare-EFCore/Migrations/SqlServer/20260522084325_AddThumbnailMap.cs new file mode 100644 index 0000000..42392eb --- /dev/null +++ b/FileShare-EFCore/Migrations/SqlServer/20260522084325_AddThumbnailMap.cs @@ -0,0 +1,99 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FileShare_EFCore.Migrations.SqlServer +{ + /// + public partial class AddThumbnailMap : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "thumbnail-path", + table: "managed-file-record"); + + migrationBuilder.AddColumn( + name: "thumbnail-id", + table: "managed-file-record", + type: "int", + nullable: true); + + migrationBuilder.CreateTable( + name: "managed-thumbnail-map", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + libraryrootid = table.Column(name: "library-root-id", type: "int", nullable: false), + relativepath = table.Column(name: "relative-path", type: "nvarchar(1024)", maxLength: 1024, nullable: false), + contenttype = table.Column(name: "content-type", type: "nvarchar(100)", maxLength: 100, nullable: false), + createdat = table.Column(name: "created-at", type: "datetime2", nullable: false), + updatedat = table.Column(name: "updated-at", type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk-managed-thumbnail-map", x => x.id); + table.ForeignKey( + name: "FK_managed-thumbnail-map_managed-library-root_library-root-id", + column: x => x.libraryrootid, + principalTable: "managed-library-root", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }, + comment: "文件缩略图映射记录"); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-thumbnail-id", + table: "managed-file-record", + column: "thumbnail-id"); + + migrationBuilder.CreateIndex( + name: "idx-managed-thumbnail-map-relative-path", + table: "managed-thumbnail-map", + column: "relative-path", + unique: true); + + migrationBuilder.CreateIndex( + name: "idx-managed-thumbnail-map-root-id", + table: "managed-thumbnail-map", + column: "library-root-id"); + + migrationBuilder.AddForeignKey( + name: "FK_managed-file-record_managed-thumbnail-map_thumbnail-id", + table: "managed-file-record", + column: "thumbnail-id", + principalTable: "managed-thumbnail-map", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_managed-file-record_managed-thumbnail-map_thumbnail-id", + table: "managed-file-record"); + + migrationBuilder.DropTable( + name: "managed-thumbnail-map"); + + migrationBuilder.DropIndex( + name: "idx-managed-file-record-thumbnail-id", + table: "managed-file-record"); + + migrationBuilder.DropColumn( + name: "thumbnail-id", + table: "managed-file-record"); + + migrationBuilder.AddColumn( + name: "thumbnail-path", + table: "managed-file-record", + type: "nvarchar(512)", + maxLength: 512, + nullable: true); + } + } +} diff --git a/FileShare-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs b/FileShare-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs index 60e1449..90669f8 100644 --- a/FileShare-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs +++ b/FileShare-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using FileShare_EFCore.Database; using Microsoft.EntityFrameworkCore; @@ -84,6 +84,234 @@ namespace FileShare_EFCore.Migrations.SqlServer }); }); + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AbsolutePath") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)") + .HasColumnName("absolute-path"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("Exists") + .HasColumnType("bit") + .HasColumnName("exists"); + + b.Property("Extension") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)") + .HasColumnName("extension"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("nvarchar(260)") + .HasColumnName("file-name"); + + b.Property("LastPlayedAt") + .HasColumnType("datetime2") + .HasColumnName("last-played-at"); + + b.Property("LastSeenAt") + .HasColumnType("datetime2") + .HasColumnName("last-seen-at"); + + b.Property("LastWriteTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("last-write-time-utc"); + + b.Property("LibraryRootId") + .HasColumnType("int") + .HasColumnName("library-root-id"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)") + .HasColumnName("media-type"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)") + .HasColumnName("relative-path"); + + b.Property("SizeBytes") + .HasColumnType("bigint") + .HasColumnName("size-bytes"); + + b.Property("ThumbnailId") + .HasColumnType("int") + .HasColumnName("thumbnail-id"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at"); + + b.Property("VideoDuration") + .HasColumnType("float") + .HasColumnName("video-duration"); + + b.HasKey("Id") + .HasName("pk-managed-file-record"); + + b.HasIndex("AbsolutePath") + .IsUnique() + .HasDatabaseName("idx-managed-file-record-absolute-path"); + + b.HasIndex("LastPlayedAt") + .HasDatabaseName("idx-managed-file-record-last-played-at"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-file-record-root-id"); + + b.HasIndex("ThumbnailId") + .HasDatabaseName("idx-managed-file-record-thumbnail-id"); + + b.HasIndex("MediaType", "Exists") + .HasDatabaseName("idx-managed-file-record-media-type-exists"); + + b.ToTable("managed-file-record", t => + { + t.HasComment("文件库文件记录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("display-name"); + + b.Property("IsAvailable") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true) + .HasColumnName("is-available"); + + b.Property("IsEnabled") + .HasColumnType("bit") + .HasColumnName("is-enabled"); + + b.Property("LastScanCompletedAt") + .HasColumnType("datetime2") + .HasColumnName("last-scan-completed-at"); + + b.Property("LastScanError") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)") + .HasColumnName("last-scan-error"); + + b.Property("LastScanStartedAt") + .HasColumnType("datetime2") + .HasColumnName("last-scan-started-at"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)") + .HasColumnName("path"); + + b.Property("ScanIntervalMinutes") + .HasColumnType("int") + .HasColumnName("scan-interval-minutes"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-library-root"); + + b.HasIndex("Path") + .IsUnique() + .HasDatabaseName("idx-managed-library-root-path"); + + b.ToTable("managed-library-root", t => + { + t.HasComment("文件库根目录"); + }); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("content-type"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("created-at"); + + b.Property("LibraryRootId") + .HasColumnType("int") + .HasColumnName("library-root-id"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)") + .HasColumnName("relative-path"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2") + .HasColumnName("updated-at"); + + b.HasKey("Id") + .HasName("pk-managed-thumbnail-map"); + + b.HasIndex("LibraryRootId") + .HasDatabaseName("idx-managed-thumbnail-map-root-id"); + + b.HasIndex("RelativePath") + .IsUnique() + .HasDatabaseName("idx-managed-thumbnail-map-relative-path"); + + b.ToTable("managed-thumbnail-map", t => + { + t.HasComment("文件缩略图映射记录"); + }); + }); + modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b => { b.Property("Id") @@ -181,6 +409,47 @@ namespace FileShare_EFCore.Migrations.SqlServer t.HasComment("天气预报数据实体"); }); }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Files") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail") + .WithMany("Files") + .HasForeignKey("ThumbnailId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("LibraryRoot"); + + b.Navigation("Thumbnail"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot") + .WithMany("Thumbnails") + .HasForeignKey("LibraryRootId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryRoot"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b => + { + b.Navigation("Files"); + + b.Navigation("Thumbnails"); + }); + + modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b => + { + b.Navigation("Files"); + }); #pragma warning restore 612, 618 } } diff --git a/FileShare-EFCore/Models/ManagedFileRecord.cs b/FileShare-EFCore/Models/ManagedFileRecord.cs index bd77e43..779eb93 100644 --- a/FileShare-EFCore/Models/ManagedFileRecord.cs +++ b/FileShare-EFCore/Models/ManagedFileRecord.cs @@ -67,6 +67,18 @@ namespace FileShare_EFCore.Models [Column("last-seen-at")] public DateTime LastSeenAt { get; set; } = DateTime.UtcNow; + /// 视频缩略图路径(相对于 wwwroot)。 + [Column("thumbnail-id")] + public int? ThumbnailId { get; set; } + + /// 视频时长(秒)。 + [Column("video-duration")] + public double? VideoDuration { get; set; } + + /// 最近一次播放时间 UTC。 + [Column("last-played-at")] + public DateTime? LastPlayedAt { get; set; } + /// 创建时间。 [Column("created-at")] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; @@ -77,5 +89,7 @@ namespace FileShare_EFCore.Models /// 所属根目录。 public ManagedLibraryRoot? LibraryRoot { get; set; } + + public ManagedThumbnailMap? Thumbnail { get; set; } } } diff --git a/FileShare-EFCore/Models/ManagedLibraryRoot.cs b/FileShare-EFCore/Models/ManagedLibraryRoot.cs index 1880100..80ba977 100644 --- a/FileShare-EFCore/Models/ManagedLibraryRoot.cs +++ b/FileShare-EFCore/Models/ManagedLibraryRoot.cs @@ -62,5 +62,7 @@ namespace FileShare_EFCore.Models /// 文件记录。 public List Files { get; set; } = new(); + + public List Thumbnails { get; set; } = new(); } } diff --git a/FileShare-EFCore/Models/ManagedThumbnailMap.cs b/FileShare-EFCore/Models/ManagedThumbnailMap.cs new file mode 100644 index 0000000..9420340 --- /dev/null +++ b/FileShare-EFCore/Models/ManagedThumbnailMap.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace FileShare_EFCore.Models +{ + [Comment("文件缩略图映射记录")] + [Table("managed-thumbnail-map")] + public class ManagedThumbnailMap + { + [Key] + [Column("id")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + [Column("library-root-id")] + public int LibraryRootId { get; set; } + + [Column("relative-path")] + [MaxLength(1024)] + public string RelativePath { get; set; } = string.Empty; + + [Column("content-type")] + [MaxLength(100)] + public string ContentType { get; set; } = "image/jpeg"; + + [Column("created-at")] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [Column("updated-at")] + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + public ManagedLibraryRoot? LibraryRoot { get; set; } + + public List Files { get; set; } = new(); + } +} diff --git a/FileShare-PC/FileShare-PC.csproj b/FileShare-PC/FileShare-PC.csproj index a608e09..b5eff9a 100644 --- a/FileShare-PC/FileShare-PC.csproj +++ b/FileShare-PC/FileShare-PC.csproj @@ -13,6 +13,8 @@ PreserveNewest + + diff --git a/FileShare-PC/Program.cs b/FileShare-PC/Program.cs index 1378834..7c42088 100644 --- a/FileShare-PC/Program.cs +++ b/FileShare-PC/Program.cs @@ -12,6 +12,7 @@ using FileShare_Services.Services.FileLibrary; using Microsoft.Extensions.DependencyInjection; using Serilog; using System; +using System.IO; namespace FileShare_PC { @@ -71,9 +72,13 @@ namespace FileShare_PC services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(new ThumbnailStorageOptions()); + services.AddSingleton(sp => + new VideoThumbnailService(sp.GetRequiredService())); services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // ---- 端点注册 ---- var endpointBuilder = new ServiceEndpointBuilder(); diff --git a/FileShare-PC/Views/MainWindow.axaml.cs b/FileShare-PC/Views/MainWindow.axaml.cs index 2e8e205..c77ebd3 100644 --- a/FileShare-PC/Views/MainWindow.axaml.cs +++ b/FileShare-PC/Views/MainWindow.axaml.cs @@ -554,7 +554,8 @@ namespace FileShare_PC.Views { try { - if (await TryHandleLocalMediaStreamAsync(context)) + if (await TryHandleLocalMediaStreamAsync(context) + || await TryHandleLocalThumbnailAsync(context)) { return; } @@ -691,6 +692,69 @@ namespace FileShare_PC.Views return true; } + private async Task TryHandleLocalThumbnailAsync(HttpListenerContext context) + { + var request = context.Request; + var response = context.Response; + var segments = (request.Url?.AbsolutePath ?? string.Empty) + .Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + if (segments.Length != 3 + || !string.Equals(segments[0], "api", StringComparison.OrdinalIgnoreCase) + || !string.Equals(segments[1], "thumbnails", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + AddLocalMediaHeaders(response); + + if (!string.Equals(request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase) + && !string.Equals(request.HttpMethod, "HEAD", StringComparison.OrdinalIgnoreCase)) + { + response.StatusCode = (int)HttpStatusCode.MethodNotAllowed; + response.Close(); + return true; + } + + if (!int.TryParse(segments[2], out var id) || id <= 0) + { + response.StatusCode = (int)HttpStatusCode.NotFound; + response.Close(); + return true; + } + + using var scope = _services.CreateScope(); + var thumbnailService = scope.ServiceProvider.GetService(); + var thumbnail = thumbnailService is null ? null : await thumbnailService.GetThumbnailAsync(id); + if (thumbnail is null || !File.Exists(thumbnail.FilePath)) + { + response.StatusCode = (int)HttpStatusCode.NotFound; + response.Close(); + return true; + } + + var fileInfo = new FileInfo(thumbnail.FilePath); + response.StatusCode = (int)HttpStatusCode.OK; + response.ContentType = thumbnail.ContentType; + response.ContentLength64 = fileInfo.Length; + response.Headers["Cache-Control"] = "public, max-age=3600"; + response.Headers["Content-Disposition"] = + $"inline; filename=\"{Uri.EscapeDataString(thumbnail.FileName)}\""; + response.Headers["Last-Modified"] = thumbnail.LastModified.ToUniversalTime().ToString("R"); + + if (string.Equals(request.HttpMethod, "HEAD", StringComparison.OrdinalIgnoreCase) + || fileInfo.Length == 0) + { + response.Close(); + return true; + } + + await using var input = File.Open(thumbnail.FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + await input.CopyToAsync(response.OutputStream); + response.OutputStream.Close(); + return true; + } + /// /// 为媒体流响应添加 CORS 和 Range 请求头,允许浏览器跨域访问和分段请求。 /// diff --git a/FileShare-Services/Endpoints/AppEndpoints.cs b/FileShare-Services/Endpoints/AppEndpoints.cs index c0a007c..90caea8 100644 --- a/FileShare-Services/Endpoints/AppEndpoints.cs +++ b/FileShare-Services/Endpoints/AppEndpoints.cs @@ -69,6 +69,14 @@ namespace FileShare_Services.Endpoints .WithOpenApi("FileLibrary", "浏览文件库目录结构。") .WithName("BrowseDirectory"); + endpoints.MapGet("api/files/recent", (service, request, _) => service.GetRecentFilesAsync(request)) + .WithOpenApi("FileLibrary", "获取最近添加或最近播放的文件。") + .WithName("GetRecentFiles"); + + endpoints.MapPost("api/files/played", (service, request, _) => service.MarkFilePlayedAsync(request)) + .WithOpenApi("FileLibrary", "标记文件已播放。") + .WithName("MarkFilePlayed"); + endpoints.MapGet("api/files/detail", (service, request, _) => service.GetFileAsync(request)) .WithOpenApi("FileLibrary", "查询文件详情。") .WithName("GetFileDetail"); diff --git a/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs b/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs index a45b321..5f2e06d 100644 --- a/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs +++ b/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs @@ -46,6 +46,19 @@ namespace FileShare_Services.Services.FileLibrary public sealed record FileQueryRequest( [property: JsonPropertyName("id")] int Id); + /// + /// 获取最近文件的请求。 + /// + public sealed record RecentFilesRequest( + [property: JsonPropertyName("type")] string Type = "added", + [property: JsonPropertyName("count")] int Count = 12); + + /// + /// 标记文件已播放的请求。 + /// + public sealed record MarkFilePlayedRequest( + [property: JsonPropertyName("id")] int Id); + /// /// 分页搜索已扫描文件的请求。 /// @@ -105,7 +118,10 @@ namespace FileShare_Services.Services.FileLibrary string ContentType, string StreamUrl, string? TextUrl, - bool BrowserPlayable); + bool BrowserPlayable, + string? ThumbnailUrl = null, + double? VideoDuration = null, + DateTime? LastPlayedAt = null); /// /// 浏览文件库目录结构的请求。 diff --git a/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs b/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs index e553796..878af83 100644 --- a/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs +++ b/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs @@ -29,7 +29,7 @@ namespace FileShare_Services.Services.FileLibrary /// public async Task AddRootAsync(AddLibraryRootRequest request) { - return ResponseHelper.Ok(await fileLibrary.AddRootAsync(request), "文件库目录已添加并完成扫描。"); + return ResponseHelper.Ok(await fileLibrary.AddRootAsync(request), "文件库目录已添加,后续扫描将自动入库。"); } /// @@ -87,6 +87,20 @@ namespace FileShare_Services.Services.FileLibrary return ResponseHelper.Ok(result); } + /// + public async Task GetRecentFilesAsync(RecentFilesRequest request) + { + var items = await fileLibrary.GetRecentFilesAsync(request.Type, request.Count); + return ResponseHelper.Ok(items); + } + + /// + public async Task MarkFilePlayedAsync(MarkFilePlayedRequest request) + { + await fileLibrary.MarkFilePlayedAsync(request.Id); + return ResponseHelper.Succeed(); + } + /// /// 验证文件 ID 是否有效,无效时抛出 。 /// diff --git a/FileShare-Services/Services/FileLibrary/FileLibraryService.cs b/FileShare-Services/Services/FileLibrary/FileLibraryService.cs index d2c227c..f8af9d6 100644 --- a/FileShare-Services/Services/FileLibrary/FileLibraryService.cs +++ b/FileShare-Services/Services/FileLibrary/FileLibraryService.cs @@ -10,7 +10,7 @@ namespace FileShare_Services.Services.FileLibrary /// /// 文件库核心业务服务,实现磁盘枚举、目录管理、文件扫描与检索。 /// - public sealed class FileLibraryService(AppDataContext db) : IFileLibraryService + public sealed class FileLibraryService(AppDataContext db, IVideoThumbnailService thumbnailService) : IFileLibraryService { /// /// 默认扫描间隔(分钟),当请求未指定间隔时使用。 @@ -80,7 +80,9 @@ namespace FileShare_Services.Services.FileLibrary existing.DisplayName = ResolveDisplayName(normalized, request.DisplayName); existing.ScanIntervalMinutes = NormalizeInterval(request.ScanIntervalMinutes); await db.SaveChangesAsync(cancellationToken); - return await ScanRootAsync(existing.Id, cancellationToken); + var existingCount = await db.ManagedFileRecords + .CountAsync(file => file.LibraryRootId == existing.Id && file.Exists, cancellationToken); + return ToRootDto(existing, existingCount); } var root = new ManagedLibraryRoot @@ -95,7 +97,7 @@ namespace FileShare_Services.Services.FileLibrary db.ManagedLibraryRoots.Add(root); await db.SaveChangesAsync(cancellationToken); - return await ScanRootAsync(root.Id, cancellationToken); + return ToRootDto(root, 0); } /// @@ -178,6 +180,24 @@ namespace FileShare_Services.Services.FileLibrary record.ContentType = contentType; record.Exists = true; record.LastSeenAt = DateTime.UtcNow; + + if (mediaType == "video" && record.ThumbnailId is null) + { + var thumbnail = await thumbnailService.GenerateThumbnailAsync(root.Id, absolutePath, cancellationToken); + if (thumbnail is not null) + { + var map = new ManagedThumbnailMap + { + LibraryRootId = root.Id, + RelativePath = thumbnail.RelativePath, + ContentType = thumbnail.ContentType, + }; + db.ManagedThumbnailMaps.Add(map); + record.Thumbnail = map; + } + + record.VideoDuration ??= thumbnailService.GetVideoDuration(absolutePath); + } } foreach (var stale in existing.Values.Where(file => !seen.Contains(file.AbsolutePath))) @@ -356,6 +376,34 @@ namespace FileShare_Services.Services.FileLibrary return new TextPreviewDto(file.Id, file.FileName, content, stream.Length > MaxTextPreviewBytes); } + /// + public async Task> GetRecentFilesAsync(string type, int count = 12, CancellationToken cancellationToken = default) + { + var query = db.ManagedFileRecords + .AsNoTracking() + .Where(file => file.Exists && file.LibraryRoot != null && file.LibraryRoot.IsAvailable); + + query = type == "played" + ? query.Where(file => file.LastPlayedAt != null).OrderByDescending(file => file.LastPlayedAt) + : query.OrderByDescending(file => file.CreatedAt); + + return await query + .Take(Math.Clamp(count, 1, 48)) + .Select(file => ToFileDto(file)) + .ToListAsync(cancellationToken); + } + + /// + public async Task MarkFilePlayedAsync(int id, CancellationToken cancellationToken = default) + { + var record = await db.ManagedFileRecords.FindAsync([id], cancellationToken); + if (record is not null) + { + record.LastPlayedAt = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + } + } + /// /// 深度优先遍历目录树,枚举所有被 支持的媒体文件路径。 /// 遇到无权限的目录时跳过该分支继续遍历。 @@ -508,7 +556,10 @@ namespace FileShare_Services.Services.FileLibrary file.ContentType, $"/api/files/{file.Id}/stream", file.MediaType == "text" ? $"/api/files/text?id={file.Id}" : null, - MediaFileTypes.IsBrowserPlayable(file.Extension)); + MediaFileTypes.IsBrowserPlayable(file.Extension), + file.ThumbnailId is null ? null : $"/api/thumbnails/{file.ThumbnailId}", + file.VideoDuration, + file.LastPlayedAt); } } diff --git a/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs b/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs index 21e3b23..994b432 100644 --- a/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs +++ b/FileShare-Services/Services/FileLibrary/IFileLibraryEndpointService.cs @@ -84,5 +84,9 @@ namespace FileShare_Services.Services.FileLibrary /// 包含根目录 ID 和路径的请求。 /// API 响应。 Task BrowseDirectoryAsync(BrowseDirectoryRequest request); + + Task GetRecentFilesAsync(RecentFilesRequest request); + + Task MarkFilePlayedAsync(MarkFilePlayedRequest request); } } diff --git a/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs b/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs index 6ea9da5..a4e5d6f 100644 --- a/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs +++ b/FileShare-Services/Services/FileLibrary/IFileLibraryService.cs @@ -97,5 +97,9 @@ namespace FileShare_Services.Services.FileLibrary /// 取消令牌。 /// 目录浏览响应,包含子目录和文件列表。 Task BrowseDirectoryAsync(BrowseDirectoryRequest request, CancellationToken cancellationToken = default); + + Task> GetRecentFilesAsync(string type, int count = 12, CancellationToken cancellationToken = default); + + Task MarkFilePlayedAsync(int id, CancellationToken cancellationToken = default); } } diff --git a/FileShare-Services/Services/FileLibrary/IVideoThumbnailService.cs b/FileShare-Services/Services/FileLibrary/IVideoThumbnailService.cs new file mode 100644 index 0000000..14872b5 --- /dev/null +++ b/FileShare-Services/Services/FileLibrary/IVideoThumbnailService.cs @@ -0,0 +1,11 @@ +namespace FileShare_Services.Services.FileLibrary +{ + public interface IVideoThumbnailService + { + Task GenerateThumbnailAsync(int libraryRootId, string videoPath, CancellationToken ct = default); + string GetAbsolutePath(string relativePath); + double? GetVideoDuration(string videoPath); + } + + public sealed record GeneratedThumbnail(string RelativePath, string ContentType); +} diff --git a/FileShare-Services/Services/FileLibrary/ThumbnailStorageOptions.cs b/FileShare-Services/Services/FileLibrary/ThumbnailStorageOptions.cs new file mode 100644 index 0000000..6ccf26c --- /dev/null +++ b/FileShare-Services/Services/FileLibrary/ThumbnailStorageOptions.cs @@ -0,0 +1,9 @@ +namespace FileShare_Services.Services.FileLibrary +{ + public sealed class ThumbnailStorageOptions + { + public string RootPath { get; set; } = Path.Combine(AppContext.BaseDirectory, "thumbnails"); + public string FfmpegPath { get; set; } = Path.Combine("tools", "ffmpeg", "bin", "ffmpeg.exe"); + public string FfprobePath { get; set; } = Path.Combine("tools", "ffmpeg", "bin", "ffprobe.exe"); + } +} diff --git a/FileShare-Services/Services/FileLibrary/ThumbnailStreamService.cs b/FileShare-Services/Services/FileLibrary/ThumbnailStreamService.cs new file mode 100644 index 0000000..7fa8077 --- /dev/null +++ b/FileShare-Services/Services/FileLibrary/ThumbnailStreamService.cs @@ -0,0 +1,38 @@ +using FileShare_EFCore.Database; +using FileShare_Services.Core; +using Microsoft.EntityFrameworkCore; + +namespace FileShare_Services.Services.FileLibrary +{ + public interface IThumbnailStreamService + { + Task GetThumbnailAsync(int id, CancellationToken cancellationToken = default); + } + + public sealed class ThumbnailStreamService(AppDataContext db, IVideoThumbnailService thumbnails) : IThumbnailStreamService + { + public async Task GetThumbnailAsync(int id, CancellationToken cancellationToken = default) + { + var thumbnail = await db.ManagedThumbnailMaps + .AsNoTracking() + .FirstOrDefaultAsync(item => item.Id == id, cancellationToken); + + if (thumbnail is null) + { + return null; + } + + var absolutePath = thumbnails.GetAbsolutePath(thumbnail.RelativePath); + if (!File.Exists(absolutePath)) + { + return null; + } + + return new FileStreamResponse( + absolutePath, + Path.GetFileName(absolutePath), + thumbnail.ContentType, + File.GetLastWriteTimeUtc(absolutePath)); + } + } +} diff --git a/FileShare-Services/Services/FileLibrary/VideoThumbnailService.cs b/FileShare-Services/Services/FileLibrary/VideoThumbnailService.cs new file mode 100644 index 0000000..d1ce836 --- /dev/null +++ b/FileShare-Services/Services/FileLibrary/VideoThumbnailService.cs @@ -0,0 +1,136 @@ +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; + +namespace FileShare_Services.Services.FileLibrary +{ + public sealed class VideoThumbnailService : IVideoThumbnailService + { + private readonly string _thumbnailDir; + private readonly string _ffmpegPath; + private readonly string _ffprobePath; + + public VideoThumbnailService(ThumbnailStorageOptions options) + { + _thumbnailDir = Path.IsPathRooted(options.RootPath) + ? options.RootPath + : Path.Combine(AppContext.BaseDirectory, options.RootPath); + _ffmpegPath = ResolveExecutablePath(options.FfmpegPath); + _ffprobePath = ResolveExecutablePath(options.FfprobePath); + Directory.CreateDirectory(_thumbnailDir); + } + + public Task GenerateThumbnailAsync(int libraryRootId, string videoPath, CancellationToken ct = default) + { + var hash = ComputeHash(videoPath); + var relativePath = Path.Combine($"root-{libraryRootId}", $"{hash}.jpg"); + var outputPath = GetAbsolutePath(relativePath); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!); + if (File.Exists(outputPath)) + return Task.FromResult(new GeneratedThumbnail(relativePath, "image/jpeg")); + + try + { + var args = $"-ss 00:00:01 -i \"{videoPath}\" -vframes 1 -q:v 5 -vf \"scale=320:-2\" \"{outputPath}\" -y"; + + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = _ffmpegPath, + Arguments = args, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + }, + EnableRaisingEvents = true, + }; + + process.Start(); + + using (ct.Register(() => + { + try { process.Kill(); } catch { /* 进程可能已退出 */ } + })) + { + process.WaitForExit(10000); + } + + if (File.Exists(outputPath) && new FileInfo(outputPath).Length > 0) + return Task.FromResult(new GeneratedThumbnail(relativePath, "image/jpeg")); + + return Task.FromResult(null); + } + catch (Exception ex) + { + Serilog.Log.Warning(ex, "生成视频缩略图失败 {VideoPath}", videoPath); + return Task.FromResult(null); + } + } + + public string GetAbsolutePath(string relativePath) + { + var root = Path.GetFullPath(_thumbnailDir); + var fullPath = Path.GetFullPath(Path.Combine(root, relativePath)); + if (!fullPath.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) + && !string.Equals(fullPath, root, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("Thumbnail path is outside of the configured storage root."); + } + + return fullPath; + } + + private static string ResolveExecutablePath(string executablePath) + { + if (Path.IsPathRooted(executablePath)) + { + return executablePath; + } + + return executablePath.Contains(Path.DirectorySeparatorChar) + || executablePath.Contains(Path.AltDirectorySeparatorChar) + ? Path.Combine(AppContext.BaseDirectory, executablePath) + : executablePath; + } + + public double? GetVideoDuration(string videoPath) + { + try + { + var args = $"-v error -show_entries format=duration -of csv=p=0 \"{videoPath}\""; + + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = _ffprobePath, + Arguments = args, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + }, + }; + + process.Start(); + var output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(5000); + + if (double.TryParse(output, out var duration) && duration > 0) + return Math.Round(duration, 1); + + return null; + } + catch (Exception ex) + { + Serilog.Log.Warning(ex, "获取视频时长失败 {VideoPath}", videoPath); + return null; + } + } + + public string ComputeHash(string videoPath) => + Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(videoPath))).ToLowerInvariant(); + } +} diff --git a/FileShare-Web-VUE/src/api/index.ts b/FileShare-Web-VUE/src/api/index.ts index 3b11001..1a686e4 100644 --- a/FileShare-Web-VUE/src/api/index.ts +++ b/FileShare-Web-VUE/src/api/index.ts @@ -43,6 +43,9 @@ export interface FileRecordDto { streamUrl: string textUrl: string | null browserPlayable: boolean + thumbnailUrl: string | null + videoDuration: number | null + lastPlayedAt: string | null } export interface BrowseDirectoryResponse { @@ -98,5 +101,10 @@ export const api = { getTextPreview: (id: number) => request(`files/text${qs({ id })}`), mediaUrl: (path: string) => apiUrl(path), + thumbnailUrl: (path: string) => apiUrl(path), + getRecentFiles: (type: string, count = 12) => + request(`files/recent${qs({ type, count })}`), + markFilePlayed: (id: number) => + request('files/played', { method: 'POST', body: { id } }), qrCode: () => request<{ url: string; qrCodeBase64: string }>('qrcode'), } diff --git a/FileShare-Web-VUE/src/assets/main.css b/FileShare-Web-VUE/src/assets/main.css index 7cd55a5..cb944da 100644 --- a/FileShare-Web-VUE/src/assets/main.css +++ b/FileShare-Web-VUE/src/assets/main.css @@ -498,19 +498,6 @@ a { font-weight: 800; } -.player-panel video { - width: 100%; - max-height: 54vh; - margin-top: 12px; - border-radius: 10px; - background: #111827; -} - -.player-panel audio { - width: 100%; - margin-top: 18px; -} - .open-media-link { display: block; width: 100%; @@ -524,19 +511,6 @@ a { font-weight: 800; } -.player-panel pre { - overflow: auto; - max-height: 54vh; - margin: 12px 0 0; - border: 1px solid var(--line); - border-radius: 10px; - padding: 12px; - background: #f8fafc; - color: #111827; - white-space: pre-wrap; - overflow-wrap: anywhere; -} - .mobile-list { display: grid; gap: 9px; @@ -724,6 +698,257 @@ a { background: var(--panel); } +/* Root tabs */ +.root-tabs { + display: flex; + gap: 4px; + margin-bottom: 14px; + border: 1px solid var(--line); + border-radius: 12px; + padding: 4px; + background: var(--panel); +} + +.root-tabs button { + flex: 1; + min-height: 36px; + border: none; + border-radius: 10px; + font-weight: 700; + font-size: 14px; + color: var(--muted); + background: transparent; +} + +.root-tabs button.active { + color: #fff; + background: var(--accent); +} + +/* View toggle */ +.view-toggle-bar { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.view-toggle { + display: flex; + gap: 0; + border: 1px solid var(--line); + border-radius: 8px; + overflow: hidden; +} + +.view-toggle button { + min-height: 30px; + border: none; + border-radius: 0; + padding: 4px 14px; + font-size: 13px; + font-weight: 700; + color: var(--muted); + background: transparent; +} + +.view-toggle button.active { + color: #fff; + background: var(--accent); +} + +.view-toggle button + button { + border-left: 1px solid var(--line); +} + +/* Section header with view toggle inline */ +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.section-header h3 { + margin: 0; + font-size: 15px; + font-weight: 800; + color: var(--muted); +} + +/* File grid / card view */ +.file-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); + gap: 10px; + border: 1px solid var(--line); + border-radius: 14px; + padding: 10px; + background: var(--panel); +} + +.file-card { + display: grid; + grid-template-rows: auto 1fr; + width: 100%; + min-height: 0; + border: 1px solid var(--line); + border-radius: 12px; + padding: 0; + overflow: hidden; + text-align: left; + background: #fff; +} + +.file-card.active { + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent); +} + +.file-card:hover:not(:disabled) { + border-color: var(--accent); +} + +.card-thumb { + display: block; + width: 100%; + aspect-ratio: 16 / 10; + object-fit: contain; + background: #111827; +} + +.card-thumb-placeholder { + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 16 / 10; + border-radius: 999px; + margin: 12px auto 0; + width: 44px; + height: 44px; + color: #fff; + background: var(--accent-strong); + font-size: 12px; + font-weight: 800; + text-transform: uppercase; +} + +.card-body { + display: grid; + align-content: start; + gap: 3px; + padding: 10px 10px 12px; + min-width: 0; +} + +.card-body strong { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + font-size: 14px; + font-weight: 700; + line-height: 1.3; +} + +.card-body small { + color: var(--muted); + font-size: 12px; +} + +/* File thumbnail in list view */ +.file-thumb { + flex: 0 0 72px; + width: 72px; + height: 48px; + border-radius: 6px; + object-fit: contain; + background: #111827; +} + +.file-list .mobile-file { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 10px; + width: 100%; + min-height: 60px; + padding: 8px 10px; + text-align: left; +} + +.file-list .mobile-file > span:last-child { + display: grid; + min-width: 0; +} + +/* Video info bar */ +.video-info-bar { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 10px; + padding: 8px 0; + font-size: 13px; + color: var(--muted); +} + +.video-info-bar span { + border-radius: 999px; + padding: 3px 9px; + background: #f2f4f7; + font-weight: 600; +} + +/* Player media wrapper */ +.player-media-wrapper { + margin-top: 12px; +} + +.player-media-wrapper video { + width: 100%; + max-height: 54vh; + border-radius: 10px; + background: #111827; +} + +.player-media-wrapper audio { + width: 100%; + margin-top: 6px; +} + +.player-media-wrapper pre { + overflow: auto; + max-height: 54vh; + border: 1px solid var(--line); + border-radius: 10px; + padding: 12px; + background: #f8fafc; + color: #111827; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.unsupported-player { + text-align: center; +} + +.unsupported-thumb { + display: block; + width: 100%; + max-width: 360px; + margin: 0 auto 12px; + border-radius: 10px; + aspect-ratio: 16 / 9; + object-fit: contain; + background: #111827; +} + +/* Recent files section */ +.recent-files { + margin-top: 4px; +} + @media (max-width: 1100px) { .admin-layout, .admin-browser { diff --git a/FileShare-Web-VUE/src/components/ClientPage.vue b/FileShare-Web-VUE/src/components/ClientPage.vue index f688924..3ca4e2e 100644 --- a/FileShare-Web-VUE/src/components/ClientPage.vue +++ b/FileShare-Web-VUE/src/components/ClientPage.vue @@ -15,6 +15,12 @@ const rootId = ref() const browsePath = ref([]) const isBrowsingRoots = ref(true) +// Recent files & tabs +const activeTab = ref<'recent-added' | 'recent-played' | 'libraries'>('libraries') +const recentFiles = ref([]) +const recentLoading = ref(false) +const viewMode = ref<'list' | 'grid'>('list') + const qrModal = ref | null>(null) const activeRoots = computed(() => roots.value.filter((root) => root.isEnabled && root.isAvailable)) @@ -34,8 +40,13 @@ const breadcrumbs = computed(() => { }) const selectedMediaUrl = computed(() => selectedFile.value ? api.mediaUrl(selectedFile.value.streamUrl) : '') +const selectedThumbnailUrl = computed(() => + selectedFile.value?.thumbnailUrl ? api.thumbnailUrl(selectedFile.value.thumbnailUrl) : '' +) const clientTitle = computed(() => { + if (activeTab.value === 'recent-added') return '最近添加' + if (activeTab.value === 'recent-played') return '最近播放' if (isBrowsingRoots.value) return '文件库' const root = roots.value.find((r) => r.id === rootId.value) const dir = browsePath.value.length > 0 ? browsePath.value[browsePath.value.length - 1] : '' @@ -54,6 +65,13 @@ function formatSize(bytes: number) { return `${value.toFixed(value >= 10 ? 1 : 2)} ${units[index]}` } +function formatDuration(seconds: number | null) { + if (!seconds) return '' + const m = Math.floor(seconds / 60) + const s = Math.floor(seconds % 60) + return `${m}:${s.toString().padStart(2, '0')}` +} + function formatDate(value: string | null) { if (!value) return '' return new Date(value).toLocaleString() @@ -80,12 +98,35 @@ async function browseDirectory() { } } +async function loadRecentFiles(type: string) { + try { + errorMessage.value = '' + recentLoading.value = true + recentFiles.value = await api.getRecentFiles(type, 24) + } catch (error) { + setError(error) + } finally { + recentLoading.value = false + } +} + +function switchTab(tab: 'recent-added' | 'recent-played' | 'libraries') { + activeTab.value = tab + isBrowsingRoots.value = true + browseData.value = null + selectedFile.value = null + textPreview.value = null + if (tab === 'recent-added') loadRecentFiles('added') + else if (tab === 'recent-played') loadRecentFiles('played') +} + async function enterRoot(id: number) { rootId.value = id isBrowsingRoots.value = false browsePath.value = [] selectedFile.value = null textPreview.value = null + activeTab.value = 'libraries' await browseDirectory() } @@ -96,6 +137,7 @@ function backToRoots() { browsePath.value = [] selectedFile.value = null textPreview.value = null + activeTab.value = 'libraries' } async function navigateTo(path: string) { @@ -119,6 +161,11 @@ async function enterSubdirectory(name: string) { async function selectFile(file: FileRecordDto) { selectedFile.value = file textPreview.value = null + + if (file.mediaType === 'video' || file.mediaType === 'audio') { + api.markFilePlayed(file.id).catch(() => {}) + } + if (file.mediaType !== 'text') return try { @@ -146,7 +193,9 @@ onMounted(async () => {

{{ clientTitle }}

- + + + @@ -161,8 +210,99 @@ onMounted(async () => {

{{ errorMessage }}

+ + + + +
+
+
+
+ + +
+
+ +

加载中...

+

+ {{ activeTab === 'recent-added' ? '暂无最近添加的文件' : '暂无播放记录' }} +

+ + +
+ +
+ + +
+ +
+
+ -
+
-