diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f234b07 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,4 @@ +# Codex Rules + +- Do not manually edit files under `FileShare-EFCore/Migrations/**`. These files are generated by EF scripts. +- When database schema changes are needed, update the EF model/configuration only and ask the user to generate migrations with the project scripts. diff --git a/FileShare-EFCore/Database/AppDataContext.cs b/FileShare-EFCore/Database/AppDataContext.cs index 9b10a90..68ac116 100644 --- a/FileShare-EFCore/Database/AppDataContext.cs +++ b/FileShare-EFCore/Database/AppDataContext.cs @@ -73,6 +73,7 @@ namespace FileShare_EFCore.Database 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.HasIndex(e => e.FileCreationTimeUtc).HasDatabaseName("idx-managed-file-record-file-creation-time"); entity.Property(e => e.FileName).HasMaxLength(260); entity.Property(e => e.RelativePath).HasMaxLength(1024); entity.Property(e => e.AbsolutePath).HasMaxLength(2048); diff --git a/FileShare-EFCore/Migrations/MySQL/20260523023700_AutoMigration_20260523103531.Designer.cs b/FileShare-EFCore/Migrations/MySQL/20260523023700_AutoMigration_20260523103531.Designer.cs new file mode 100644 index 0000000..1f643d7 --- /dev/null +++ b/FileShare-EFCore/Migrations/MySQL/20260523023700_AutoMigration_20260523103531.Designer.cs @@ -0,0 +1,455 @@ +// +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("20260523023700_AutoMigration_20260523103531")] + partial class AutoMigration_20260523103531 + { + /// + 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("FileCreationTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("file-creation-time-utc"); + + 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("PlaybackPosition") + .HasColumnType("double") + .HasColumnName("playback-position"); + + 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("FileCreationTimeUtc") + .HasDatabaseName("idx-managed-file-record-file-creation-time"); + + 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/20260523023700_AutoMigration_20260523103531.cs b/FileShare-EFCore/Migrations/MySQL/20260523023700_AutoMigration_20260523103531.cs new file mode 100644 index 0000000..1521a77 --- /dev/null +++ b/FileShare-EFCore/Migrations/MySQL/20260523023700_AutoMigration_20260523103531.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FileShare_EFCore.Migrations.MySQL +{ + /// + public partial class AutoMigration_20260523103531 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "file-creation-time-utc", + table: "managed-file-record", + type: "datetime(6)", + nullable: true); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-file-creation-time", + table: "managed-file-record", + column: "file-creation-time-utc"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "idx-managed-file-record-file-creation-time", + table: "managed-file-record"); + + migrationBuilder.DropColumn( + name: "file-creation-time-utc", + table: "managed-file-record"); + } + } +} diff --git a/FileShare-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs b/FileShare-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs index add6e0d..55c038d 100644 --- a/FileShare-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs +++ b/FileShare-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs @@ -112,6 +112,10 @@ namespace FileShare_EFCore.Migrations.MySQL .HasColumnType("varchar(32)") .HasColumnName("extension"); + b.Property("FileCreationTimeUtc") + .HasColumnType("datetime(6)") + .HasColumnName("file-creation-time-utc"); + b.Property("FileName") .IsRequired() .HasMaxLength(260) @@ -173,6 +177,9 @@ namespace FileShare_EFCore.Migrations.MySQL .IsUnique() .HasDatabaseName("idx-managed-file-record-absolute-path"); + b.HasIndex("FileCreationTimeUtc") + .HasDatabaseName("idx-managed-file-record-file-creation-time"); + b.HasIndex("LastPlayedAt") .HasDatabaseName("idx-managed-file-record-last-played-at"); diff --git a/FileShare-EFCore/Migrations/PostgreSQL/20260523023643_AutoMigration_20260523103531.Designer.cs b/FileShare-EFCore/Migrations/PostgreSQL/20260523023643_AutoMigration_20260523103531.Designer.cs new file mode 100644 index 0000000..90bdaa3 --- /dev/null +++ b/FileShare-EFCore/Migrations/PostgreSQL/20260523023643_AutoMigration_20260523103531.Designer.cs @@ -0,0 +1,470 @@ +// +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("20260523023643_AutoMigration_20260523103531")] + partial class AutoMigration_20260523103531 + { + /// + 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("FileCreationTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("file-creation-time-utc"); + + 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("PlaybackPosition") + .HasColumnType("double precision") + .HasColumnName("playback-position"); + + 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("FileCreationTimeUtc") + .HasDatabaseName("idx-managed-file-record-file-creation-time"); + + 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/20260523023643_AutoMigration_20260523103531.cs b/FileShare-EFCore/Migrations/PostgreSQL/20260523023643_AutoMigration_20260523103531.cs new file mode 100644 index 0000000..e63ff72 --- /dev/null +++ b/FileShare-EFCore/Migrations/PostgreSQL/20260523023643_AutoMigration_20260523103531.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FileShare_EFCore.Migrations.PostgreSQL +{ + /// + public partial class AutoMigration_20260523103531 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "file-creation-time-utc", + table: "managed-file-record", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-file-creation-time", + table: "managed-file-record", + column: "file-creation-time-utc"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "idx-managed-file-record-file-creation-time", + table: "managed-file-record"); + + migrationBuilder.DropColumn( + name: "file-creation-time-utc", + table: "managed-file-record"); + } + } +} diff --git a/FileShare-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs b/FileShare-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs index f84c2a0..5534954 100644 --- a/FileShare-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs +++ b/FileShare-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs @@ -119,6 +119,10 @@ namespace FileShare_EFCore.Migrations.PostgreSQL .HasColumnType("character varying(32)") .HasColumnName("extension"); + b.Property("FileCreationTimeUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("file-creation-time-utc"); + b.Property("FileName") .IsRequired() .HasMaxLength(260) @@ -180,6 +184,9 @@ namespace FileShare_EFCore.Migrations.PostgreSQL .IsUnique() .HasDatabaseName("idx-managed-file-record-absolute-path"); + b.HasIndex("FileCreationTimeUtc") + .HasDatabaseName("idx-managed-file-record-file-creation-time"); + b.HasIndex("LastPlayedAt") .HasDatabaseName("idx-managed-file-record-last-played-at"); diff --git a/FileShare-EFCore/Migrations/SQLite/20260523023607_AutoMigration_20260523103531.Designer.cs b/FileShare-EFCore/Migrations/SQLite/20260523023607_AutoMigration_20260523103531.Designer.cs new file mode 100644 index 0000000..5fe6daf --- /dev/null +++ b/FileShare-EFCore/Migrations/SQLite/20260523023607_AutoMigration_20260523103531.Designer.cs @@ -0,0 +1,453 @@ +// +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("20260523023607_AutoMigration_20260523103531")] + partial class AutoMigration_20260523103531 + { + /// + 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("FileCreationTimeUtc") + .HasColumnType("TEXT") + .HasColumnName("file-creation-time-utc"); + + 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("PlaybackPosition") + .HasColumnType("REAL") + .HasColumnName("playback-position"); + + 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("FileCreationTimeUtc") + .HasDatabaseName("idx-managed-file-record-file-creation-time"); + + 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/20260523023607_AutoMigration_20260523103531.cs b/FileShare-EFCore/Migrations/SQLite/20260523023607_AutoMigration_20260523103531.cs new file mode 100644 index 0000000..d0cfa99 --- /dev/null +++ b/FileShare-EFCore/Migrations/SQLite/20260523023607_AutoMigration_20260523103531.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FileShare_EFCore.Migrations.SQLite +{ + /// + public partial class AutoMigration_20260523103531 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "file-creation-time-utc", + table: "managed-file-record", + type: "TEXT", + nullable: true); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-file-creation-time", + table: "managed-file-record", + column: "file-creation-time-utc"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "idx-managed-file-record-file-creation-time", + table: "managed-file-record"); + + migrationBuilder.DropColumn( + name: "file-creation-time-utc", + table: "managed-file-record"); + } + } +} diff --git a/FileShare-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs b/FileShare-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs index b71579a..0cbc4bb 100644 --- a/FileShare-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs +++ b/FileShare-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs @@ -110,6 +110,10 @@ namespace FileShare_EFCore.Migrations.SQLite .HasColumnType("TEXT") .HasColumnName("extension"); + b.Property("FileCreationTimeUtc") + .HasColumnType("TEXT") + .HasColumnName("file-creation-time-utc"); + b.Property("FileName") .IsRequired() .HasMaxLength(260) @@ -171,6 +175,9 @@ namespace FileShare_EFCore.Migrations.SQLite .IsUnique() .HasDatabaseName("idx-managed-file-record-absolute-path"); + b.HasIndex("FileCreationTimeUtc") + .HasDatabaseName("idx-managed-file-record-file-creation-time"); + b.HasIndex("LastPlayedAt") .HasDatabaseName("idx-managed-file-record-last-played-at"); diff --git a/FileShare-EFCore/Migrations/SqlServer/20260523023625_AutoMigration_20260523103531.Designer.cs b/FileShare-EFCore/Migrations/SqlServer/20260523023625_AutoMigration_20260523103531.Designer.cs new file mode 100644 index 0000000..c30264c --- /dev/null +++ b/FileShare-EFCore/Migrations/SqlServer/20260523023625_AutoMigration_20260523103531.Designer.cs @@ -0,0 +1,470 @@ +// +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("20260523023625_AutoMigration_20260523103531")] + partial class AutoMigration_20260523103531 + { + /// + 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("FileCreationTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("file-creation-time-utc"); + + 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("PlaybackPosition") + .HasColumnType("float") + .HasColumnName("playback-position"); + + 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("FileCreationTimeUtc") + .HasDatabaseName("idx-managed-file-record-file-creation-time"); + + 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/20260523023625_AutoMigration_20260523103531.cs b/FileShare-EFCore/Migrations/SqlServer/20260523023625_AutoMigration_20260523103531.cs new file mode 100644 index 0000000..4a7bfa3 --- /dev/null +++ b/FileShare-EFCore/Migrations/SqlServer/20260523023625_AutoMigration_20260523103531.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FileShare_EFCore.Migrations.SqlServer +{ + /// + public partial class AutoMigration_20260523103531 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "file-creation-time-utc", + table: "managed-file-record", + type: "datetime2", + nullable: true); + + migrationBuilder.CreateIndex( + name: "idx-managed-file-record-file-creation-time", + table: "managed-file-record", + column: "file-creation-time-utc"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "idx-managed-file-record-file-creation-time", + table: "managed-file-record"); + + migrationBuilder.DropColumn( + name: "file-creation-time-utc", + table: "managed-file-record"); + } + } +} diff --git a/FileShare-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs b/FileShare-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs index 04eb52d..bb8c437 100644 --- a/FileShare-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs +++ b/FileShare-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs @@ -119,6 +119,10 @@ namespace FileShare_EFCore.Migrations.SqlServer .HasColumnType("nvarchar(32)") .HasColumnName("extension"); + b.Property("FileCreationTimeUtc") + .HasColumnType("datetime2") + .HasColumnName("file-creation-time-utc"); + b.Property("FileName") .IsRequired() .HasMaxLength(260) @@ -180,6 +184,9 @@ namespace FileShare_EFCore.Migrations.SqlServer .IsUnique() .HasDatabaseName("idx-managed-file-record-absolute-path"); + b.HasIndex("FileCreationTimeUtc") + .HasDatabaseName("idx-managed-file-record-file-creation-time"); + b.HasIndex("LastPlayedAt") .HasDatabaseName("idx-managed-file-record-last-played-at"); diff --git a/FileShare-EFCore/Models/ManagedFileRecord.cs b/FileShare-EFCore/Models/ManagedFileRecord.cs index d21f66e..3d603e0 100644 --- a/FileShare-EFCore/Models/ManagedFileRecord.cs +++ b/FileShare-EFCore/Models/ManagedFileRecord.cs @@ -49,6 +49,10 @@ namespace FileShare_EFCore.Models [Column("last-write-time-utc")] public DateTime LastWriteTimeUtc { get; set; } + /// 鏂囦欢鍦ㄦ枃浠剁郴缁熶腑鐨勫垱寤烘椂闂?UTC銆?/summary> + [Column("file-creation-time-utc")] + public DateTime? FileCreationTimeUtc { get; set; } + /// 媒体类型:text、video、audio。 [Column("media-type")] [MaxLength(20)] diff --git a/FileShare-Services/Endpoints/AppEndpoints.cs b/FileShare-Services/Endpoints/AppEndpoints.cs index b77627c..0726cf3 100644 --- a/FileShare-Services/Endpoints/AppEndpoints.cs +++ b/FileShare-Services/Endpoints/AppEndpoints.cs @@ -2,6 +2,7 @@ using FileShare_Common.Core; using FileShare_Services.Core; using FileShare_Services.Services.FileLibrary; using FileShare_Services.Services.QrCode; +using Microsoft.Extensions.DependencyInjection; namespace FileShare_Services.Endpoints { @@ -119,12 +120,15 @@ namespace FileShare_Services.Endpoints private static async Task GetFileStreamAsync(ServiceEndpointContext ctx) { var sp = ctx.Items["ServiceProvider"] as IServiceProvider; - var service = sp?.GetService(typeof(IFileStreamService)) as IFileStreamService; - if (service is null) return null; + if (sp is null) return null; if (!int.TryParse(ctx.Query.GetValueOrDefault("id"), out var id) || id <= 0) return null; + using var scope = sp.CreateScope(); + var service = scope.ServiceProvider.GetService(); + if (service is null) return null; + return await service.GetFileStreamAsync(id); } diff --git a/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs b/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs index 98daf1b..118ba65 100644 --- a/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs +++ b/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs @@ -64,7 +64,7 @@ namespace FileShare_Services.Services.FileLibrary /// public sealed record SaveFileProgressRequest( [property: JsonPropertyName("id")] int Id, - [property: JsonPropertyName("position")] double Position); + [property: JsonPropertyName("position")] double? Position); /// /// 分页搜索已扫描文件的请求。 @@ -74,7 +74,9 @@ namespace FileShare_Services.Services.FileLibrary [property: JsonPropertyName("pageSize")] int PageSize = 24, [property: JsonPropertyName("mediaType")] string? MediaType = null, [property: JsonPropertyName("keyword")] string? Keyword = null, - [property: JsonPropertyName("rootId")] int RootId = 0); + [property: JsonPropertyName("rootId")] int RootId = 0, + [property: JsonPropertyName("sortBy")] string? SortBy = null, + [property: JsonPropertyName("sortDirection")] string? SortDirection = null); /// /// 磁盘驱动器信息。 @@ -121,6 +123,7 @@ namespace FileShare_Services.Services.FileLibrary string Extension, long SizeBytes, DateTime LastWriteTimeUtc, + DateTime? FileCreationTimeUtc, string MediaType, string ContentType, string StreamUrl, @@ -136,7 +139,12 @@ namespace FileShare_Services.Services.FileLibrary /// public sealed record BrowseDirectoryRequest( [property: JsonPropertyName("rootId")] int RootId = 0, - [property: JsonPropertyName("path")] string? Path = null); + [property: JsonPropertyName("path")] string? Path = null, + [property: JsonPropertyName("page")] int Page = 1, + [property: JsonPropertyName("pageSize")] int PageSize = 48, + [property: JsonPropertyName("mediaType")] string? MediaType = null, + [property: JsonPropertyName("sortBy")] string? SortBy = "name", + [property: JsonPropertyName("sortDirection")] string? SortDirection = "asc"); /// /// 浏览文件库目录的响应,包含当前路径、子目录列表和文件列表。 @@ -144,7 +152,11 @@ namespace FileShare_Services.Services.FileLibrary public sealed record BrowseDirectoryResponse( string CurrentPath, List Subdirectories, - List Files); + List Files, + int Total, + int Page, + int PageSize, + int TotalPages); /// /// 文本文件预览内容,支持截断标记。 diff --git a/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs b/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs index f74fbfa..4bb5c60 100644 --- a/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs +++ b/FileShare-Services/Services/FileLibrary/FileLibraryEndpointService.cs @@ -104,7 +104,12 @@ namespace FileShare_Services.Services.FileLibrary /// public async Task SaveFileProgressAsync(SaveFileProgressRequest request) { - await fileLibrary.SaveFileProgressAsync(request.Id, request.Position); + if (request.Id <= 0 || request.Position is null || double.IsNaN(request.Position.Value) || double.IsInfinity(request.Position.Value)) + { + return ResponseHelper.Failure(400, "参数错误"); + } + + await fileLibrary.SaveFileProgressAsync(request.Id, request.Position.Value); return ResponseHelper.Succeed(); } diff --git a/FileShare-Services/Services/FileLibrary/FileLibraryService.cs b/FileShare-Services/Services/FileLibrary/FileLibraryService.cs index ec9b7f9..b78ca8b 100644 --- a/FileShare-Services/Services/FileLibrary/FileLibraryService.cs +++ b/FileShare-Services/Services/FileLibrary/FileLibraryService.cs @@ -180,6 +180,7 @@ namespace FileShare_Services.Services.FileLibrary record.Extension = info.Extension.ToLowerInvariant(); record.SizeBytes = info.Length; record.LastWriteTimeUtc = info.LastWriteTimeUtc; + record.FileCreationTimeUtc = info.CreationTimeUtc; record.MediaType = mediaType; record.ContentType = contentType; record.Exists = true; @@ -334,9 +335,7 @@ namespace FileShare_Services.Services.FileLibrary } var total = await query.CountAsync(cancellationToken); - var items = await query - .OrderBy(file => file.MediaType) - .ThenBy(file => file.RelativePath) + var items = await ApplyFileSort(query, request.SortBy, request.SortDirection) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(file => ToFileDto(file)) @@ -359,6 +358,9 @@ namespace FileShare_Services.Services.FileLibrary public async Task BrowseDirectoryAsync(BrowseDirectoryRequest request, CancellationToken cancellationToken = default) { var rootId = request.RootId; + var page = Math.Clamp(request.Page, 1, 100000); + var pageSize = Math.Clamp(request.PageSize, 1, 100); + var mediaType = request.MediaType?.Trim(); // URL 友好的正斜杠,用于响应和内存处理 var prefix = (request.Path ?? "").Trim().Replace('\\', '/').Trim('/'); // Windows 反斜杠,用于数据库查询 @@ -378,7 +380,7 @@ namespace FileShare_Services.Services.FileLibrary var allFiles = await query.ToListAsync(cancellationToken); var subdirs = new HashSet(StringComparer.OrdinalIgnoreCase); - var currentFiles = new List(); + var currentFiles = new List(); foreach (var file in allFiles) { @@ -390,7 +392,7 @@ namespace FileShare_Services.Services.FileLibrary var slashIndex = remaining.IndexOf('/'); if (slashIndex < 0) { - currentFiles.Add(ToFileDto(file)); + currentFiles.Add(file); } else { @@ -398,10 +400,28 @@ namespace FileShare_Services.Services.FileLibrary } } + if (!string.IsNullOrWhiteSpace(mediaType) && !mediaType.Equals("all", StringComparison.OrdinalIgnoreCase)) + { + currentFiles = currentFiles + .Where(file => file.MediaType.Equals(mediaType, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + var total = currentFiles.Count; + var files = ApplyFileSort(currentFiles, request.SortBy, request.SortDirection) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(ToFileDto) + .ToList(); + return new BrowseDirectoryResponse( prefix, subdirs.OrderBy(d => d, StringComparer.OrdinalIgnoreCase).ToList(), - currentFiles); + files, + total, + page, + pageSize, + pageSize > 0 ? (int)Math.Ceiling((double)total / pageSize) : 0); } /// @@ -586,6 +606,76 @@ namespace FileShare_Services.Services.FileLibrary return Math.Clamp(interval ?? DefaultScanIntervalMinutes, 1, 1440); } + private static IQueryable ApplyFileSort( + IQueryable query, + string? sortBy, + string? sortDirection) + { + var descending = IsDescending(sortDirection); + return NormalizeSortBy(sortBy) switch + { + "size" => descending + ? query.OrderByDescending(file => file.SizeBytes).ThenBy(file => file.FileName) + : query.OrderBy(file => file.SizeBytes).ThenBy(file => file.FileName), + "created" => descending + ? query.OrderByDescending(file => file.FileCreationTimeUtc ?? file.LastWriteTimeUtc).ThenBy(file => file.FileName) + : query.OrderBy(file => file.FileCreationTimeUtc ?? file.LastWriteTimeUtc).ThenBy(file => file.FileName), + "modified" => descending + ? query.OrderByDescending(file => file.LastWriteTimeUtc).ThenBy(file => file.FileName) + : query.OrderBy(file => file.LastWriteTimeUtc).ThenBy(file => file.FileName), + "type" => descending + ? query.OrderByDescending(file => file.MediaType).ThenByDescending(file => file.Extension).ThenBy(file => file.FileName) + : query.OrderBy(file => file.MediaType).ThenBy(file => file.Extension).ThenBy(file => file.FileName), + _ => descending + ? query.OrderByDescending(file => file.FileName).ThenBy(file => file.RelativePath) + : query.OrderBy(file => file.FileName).ThenBy(file => file.RelativePath), + }; + } + + private static IOrderedEnumerable ApplyFileSort( + IEnumerable files, + string? sortBy, + string? sortDirection) + { + var descending = IsDescending(sortDirection); + return NormalizeSortBy(sortBy) switch + { + "size" => descending + ? files.OrderByDescending(file => file.SizeBytes).ThenBy(file => file.FileName) + : files.OrderBy(file => file.SizeBytes).ThenBy(file => file.FileName), + "created" => descending + ? files.OrderByDescending(file => file.FileCreationTimeUtc ?? file.LastWriteTimeUtc).ThenBy(file => file.FileName) + : files.OrderBy(file => file.FileCreationTimeUtc ?? file.LastWriteTimeUtc).ThenBy(file => file.FileName), + "modified" => descending + ? files.OrderByDescending(file => file.LastWriteTimeUtc).ThenBy(file => file.FileName) + : files.OrderBy(file => file.LastWriteTimeUtc).ThenBy(file => file.FileName), + "type" => descending + ? files.OrderByDescending(file => file.MediaType).ThenByDescending(file => file.Extension).ThenBy(file => file.FileName) + : files.OrderBy(file => file.MediaType).ThenBy(file => file.Extension).ThenBy(file => file.FileName), + _ => descending + ? files.OrderByDescending(file => file.FileName).ThenBy(file => file.RelativePath) + : files.OrderBy(file => file.FileName).ThenBy(file => file.RelativePath), + }; + } + + private static string NormalizeSortBy(string? sortBy) + { + return sortBy?.Trim().ToLowerInvariant() switch + { + "size" or "sizebytes" => "size", + "created" or "createdat" or "createtime" or "creationtime" => "created", + "modified" or "lastwrite" or "lastwritetime" or "lastwritetimeutc" or "time" or "date" => "modified", + "type" or "mediatype" or "extension" or "ext" => "type", + _ => "name", + }; + } + + private static bool IsDescending(string? sortDirection) + { + return sortDirection?.Trim().Equals("desc", StringComparison.OrdinalIgnoreCase) == true + || sortDirection?.Trim().Equals("descending", StringComparison.OrdinalIgnoreCase) == true; + } + /// /// 安全获取驱动器属性值,驱动器未就绪或访问异常时返回 null。 /// @@ -641,6 +731,7 @@ namespace FileShare_Services.Services.FileLibrary file.Extension, file.SizeBytes, file.LastWriteTimeUtc, + file.FileCreationTimeUtc, file.MediaType, file.ContentType, $"/api/files/{file.Id}/stream", diff --git a/FileShare-Web-VUE/src/api/index.ts b/FileShare-Web-VUE/src/api/index.ts index 1ee18b6..56b872d 100644 --- a/FileShare-Web-VUE/src/api/index.ts +++ b/FileShare-Web-VUE/src/api/index.ts @@ -38,6 +38,7 @@ export interface FileRecordDto { extension: string sizeBytes: number lastWriteTimeUtc: string + fileCreationTimeUtc: string | null mediaType: 'text' | 'video' | 'audio' contentType: string streamUrl: string @@ -53,6 +54,10 @@ export interface BrowseDirectoryResponse { currentPath: string subdirectories: string[] files: FileRecordDto[] + total: number + page: number + pageSize: number + totalPages: number } export interface TextPreviewDto { @@ -95,10 +100,10 @@ export const api = { request('library/roots/delete', { method: 'POST', body: { id } }), scanRoot: (id: number) => request('library/roots/scan', { method: 'POST', body: { id } }), - searchFiles: (params: { page: number; pageSize: number; mediaType?: MediaType; keyword?: string; rootId?: number }) => + searchFiles: (params: { page: number; pageSize: number; mediaType?: MediaType; keyword?: string; rootId?: number; sortBy?: string; sortDirection?: string }) => request>(`files${qs(params)}`), - browseDirectory: (rootId: number, path: string) => - request(`files/browse${qs({ rootId, path })}`), + browseDirectory: (params: { rootId: number; path: string; page?: number; pageSize?: number; mediaType?: MediaType; sortBy?: string; sortDirection?: string }) => + request(`files/browse${qs(params)}`), getTextPreview: (id: number) => request(`files/text${qs({ id })}`), mediaUrl: (path: string) => apiUrl(path), diff --git a/FileShare-Web-VUE/src/assets/main.css b/FileShare-Web-VUE/src/assets/main.css index c264eb1..da09d47 100644 --- a/FileShare-Web-VUE/src/assets/main.css +++ b/FileShare-Web-VUE/src/assets/main.css @@ -768,6 +768,8 @@ a { display: flex; align-items: center; justify-content: space-between; + flex-wrap: wrap; + gap: 8px; margin-bottom: 8px; } @@ -778,6 +780,35 @@ a { color: var(--muted); } +.file-toolbar-right { + display: flex; + align-items: center; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; +} + +.sort-control { + display: flex; + align-items: center; + gap: 6px; + color: var(--muted); + font-size: 13px; + font-weight: 700; +} + +.sort-control select { + width: auto; + min-width: 118px; + min-height: 32px; + padding: 4px 28px 4px 9px; + font-size: 13px; +} + +.sort-control.compact select { + min-width: 88px; +} + /* File grid / card view */ .file-grid { display: grid; @@ -1132,6 +1163,10 @@ a { .view-toggle { align-self: flex-start; } + + .file-toolbar-right { + justify-content: flex-start; + } } @media (min-width: 900px) { diff --git a/FileShare-Web-VUE/src/components/ClientPage.vue b/FileShare-Web-VUE/src/components/ClientPage.vue index b46c283..438cbca 100644 --- a/FileShare-Web-VUE/src/components/ClientPage.vue +++ b/FileShare-Web-VUE/src/components/ClientPage.vue @@ -1,5 +1,5 @@