Compare commits

...
16 Commits
Author SHA1 Message Date
admin777 809ca92464 fix: 修复嵌套目录浏览空白和文件扫描重复插入问题 2026-07-10 19:25:53 +08:00
admin777 6bdcb48c97 feat: Vue Router 集成 + 压缩密码编码修复
前端:
- 引入 vue-router,HTML5 History 模式,干净 URL
- ClientPage.vue 拆分为 5 个独立路由视图组件
- 提取共享状态到 composables(useLibraryRoots/useMediaPlayer/useViewMode/useModals)
- 新增 DefaultLayout 统一布局
- 导航链接改为 router-link
- FileCard 网格视图修复删除/压缩按钮 hover 不显示问题

后端:
- 压缩密码编码修复:DotNetZip/SharpZipLib 不支持 UTF-8 密码
- 替换为 SharpZipLib + 中文字符替换为 # 的 ASCII 安全密码方案
2026-07-10 15:22:09 +08:00
admin777 87f5c1ffb8 feat: 新增文件压缩功能和通用确认弹窗
后端新增 POST api/files/compress 接口,使用 DotNetZip 将单个文件压缩为 ZIP,
密码固定为文件名去掉后缀,压缩前自动删除同名旧 ZIP。
前端新增 ConfirmModal 通用确认弹窗,替代所有 window.confirm/alert,
在文件列表中增加压缩按钮,压缩前显示确认弹窗。
2026-07-10 10:35:32 +08:00
admin777 60878f9b6e feat: 新增永久删除文件接口和前端删除按钮
后端新增 POST api/files/delete 接口,同时删除物理文件、缩略图和数据库记录。
前端在 FileCard 和 FileListItem 中增加 hover 可见的删除按钮,点击后弹出确认提示。
2026-07-09 17:42:27 +08:00
admin777 8c92d2fbac perf: 优化文件浏览性能、扫描流程和前端 ref 冲突
- BrowseDirectoryAsync: 将分组/过滤/排序/分页从内存推到数据库,
    避免每次翻页加载全量文件实体(20万文件场景从 ~200MB 降到 KB 级)
  - ScanRootAsync: 使用 ExecuteUpdateAsync 原子抢占扫描锁防止并发扫描;
    缩略图/视频时长从扫描循环内同步生成改为扫描完成后 SemaphoreSlim(4) 批量生成,
    扫描耗时从数小时降到数十秒
  - ScanDueRootsAsync: 增加 LastScanStartedAt 判断,跳过正在执行中的扫描
  - ClientPage: 6 个重复的 ref="mediaPlayer" 改为 :ref="setMediaPlayer" 函数引用,
    修复 Vue 3 v-for 内字符串 ref 互相覆盖导致的播放器事件指向错误实例问题
2026-06-12 15:45:06 +08:00
admin777 8d9c7f17ff 重构客户端页面并补充前端分页
- 拆分 ClientPage 为多个客户端子组件
- 将媒体播放器显示在当前选中文件下方
- 抽取并复用分页组件
- 为搜索结果补充分页状态和翻页控件
- 保持最近文件逻辑不变
2026-05-24 15:43:31 +08:00
admin777 c6b05c12e5 fix(web): 恢复视频继续播放和进度保存
- 将播放器收敛为单个共享实例,避免列表/网格内多个 video ref 导致进度保存失效
- 恢复视频播放器上方的继续播放提示
- 在播放、暂停、拖动、结束、切换页面和离开页面时保存播放位置
- 保留文件浏览分页和排序参数的前端调用
2026-05-23 11:03:51 +08:00
admin777 27e4029f4a feat: 新增可配置的文件库扫描日志记录及缩略图清理功能
- 为文件库的定时轮询及根目录扫描过程,新增扫描生​​命周期日志
- 允许通过 `appsettings` 配置文件,自定义定时扫描的轮询间隔
- 当检测到媒体文件已被删除时,自动清理过期的缩略图映射记录及对应的缩略图文件
2026-05-22 20:09:22 +08:00
admin777 6ef410fdfa feat: 文件搜索、视频续播与目录文件过滤
- 前端 Header 新增搜索栏,接入已有 SearchFiles API,结果支持列表/网格视图
- 新增 PlaybackPosition 数据库列与 /api/files/progress 端点,播放进度存服务端
- 播放中每 5 秒自动保存进度,再次打开视频时弹出"继续播放"提示
- 目录浏览新增媒体类型过滤条(全部/视频/音频/文本),前端即时过滤
- 新增 4 种数据库迁移(AddPlaybackPosition)
2026-05-22 17:44:35 +08:00
admin777andClaude Opus 4.7 ff19f4759f docs: 为缩略图、最近文件相关新增类型补全中文 XML 文档注释
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:11:11 +08:00
admin777 2c20f9bb54 feat: 视频缩略图生成、最近文件面板与前端视图重构
- 新增 VideoThumbnailService,基于 ffmpeg 截取视频缩略图,ffprobe 提取时长
  - 新增 ManagedThumbnailMap 模型及多数据库迁移,存储缩略图元数据
  - 新增 /api/thumbnails/{id} 缩略图流端点
  - 新增最近添加/最近播放 API 与前端面板,支持列表/网格双视图切换
  - FileRecordDto 扩展 thumbnailUrl、videoDuration、lastPlayedAt 字段
  - 前端新增文件库 Tab 导航、卡片网格视图、视频海报与时长信息栏
  - 添加文件库目录不再同步全量扫描,改为后台异步自动扫描
2026-05-22 17:01:49 +08:00
admin777andClaude Opus 4.7 6acc92ca27 fix: 修复空 catch 吞异常问题,端口号抽离到配置,统一日志为 Serilog
- 所有空 catch 块补全日志记录,统一使用 Serilog/AppLog
- 按场景分级:Error(意外失败)、Warning(次要问题)、Information(预期内)
- 端口 HttpPort/HttpsPort 抽离到 appsettings.json Server 配置节
- QrCodeService 通过 IConfiguration 读取端口,消除硬编码
- 前端通过 Vite proxy 转发 /api,http.ts 统一使用 origin 地址
- 移除所有 Debug.WriteLine 和 Serilog.Log.Debug 日志

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 15:17:59 +08:00
admin777andClaude Opus 4.7 d93098638d docs: 补全 C# XML 文档注释,覆盖所有公开与内部成员
为 14 个项目中缺少 XML 注释的类、接口、方法、属性、字段、record、
枚举等成员补全中文文档注释。接口方法在接口层定义完整注释,实现类
使用 <inheritdoc /> 引用。私有辅助方法结合业务语义编写注释。

扫描结果:missing-csharp-docs.txt 缺失项归零。
构建结果:0 警告,0 错误。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 14:45:07 +08:00
admin777 9f8da2c063 Rename projects to FileShare 2026-05-22 14:29:22 +08:00
admin777andClaude Opus 4.7 8270cf198b feat: 前端组件拆分与文件库目录浏览功能
- 将 App.vue 拆分为 AdminPage、ClientPage、QrCodeModal 三个独立组件
- 新增 BrowseDirectory 接口,基于 RelativePath 实现层级目录浏览
- 前端新增面包屑导航、文件夹网格、文件列表等目录浏览 UI
- 新增对应 CSS 样式(breadcrumb、folder-grid、file-list 等)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:59:45 +08:00
admin777 d84bbb3a18 feat: 二维码访问功能,统一端点管道增强,端点迁移至 Services 层
- 新增二维码生成端点,自动检测局域网 IP,前端扫一扫即可打开网站
  - 提取 IApiResponse 接口,ServiceRequestBinder 支持强类型请求 DTO 绑定
  - FileStream 端点迁移至 AppEndpoints 统一注册,管道支持 FileStreamResponse 原始文件返回
  - 文件库端点全面使用 MapGet<TService, TRequest> 泛型注册
  - 移除 Avalonia-API/Extensions 中的业务端点文件,统一由 Services 层管理
2026-05-22 11:18:47 +08:00
234 changed files with 17495 additions and 2537 deletions
+27 -28
View File
@@ -2,36 +2,35 @@
# 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。
################################################################################
/Avalonia-PC/bin
/Avalonia-PC/.vs
/Avalonia-PC/obj
/Avalonia-API/bin
/Avalonia-API/obj
/Avalonia-Services/bin
/Avalonia-Services/obj
/Avalonia-Web-VUE/.vscode
/Avalonia-Web-VUE/obj
/Avalonia-Web-VUE/node_modules
/Avalonia-Web-VUE/dist
/Avalonia-Web-VUE/.vscode
/avalonia-web-react/obj
/avalonia-web-react/obj
/avalonia-web-react/node_modules
/avalonia-web-react/dist
/Avalonia-EFCore/bin
/Avalonia-EFCore/obj
/Avalonia-Common/bin
/Avalonia-Common/obj
/Avalonia-API/logs
/Avalonia-API/avalonia-api.db
/Avalonia-API/avalonia-api.db-shm
/Avalonia-API/avalonia-api.db-wal
/Avalonia-API/app.db
/Avalonia-API/app.db-shm
/Avalonia-API/app.db-wal
/Avalonia-API/wwwroot
/FileShare-PC/bin
/FileShare-PC/.vs
/FileShare-PC/obj
/FileShare-API/bin
/FileShare-API/obj
/FileShare-Services/bin
/FileShare-Services/obj
/FileShare-Web-VUE/.vscode
/FileShare-Web-VUE/obj
/FileShare-Web-VUE/node_modules
/FileShare-Web-VUE/dist
/FileShare-Web-VUE/.vscode
/FileShare-EFCore/bin
/FileShare-EFCore/obj
/FileShare-Common/bin
/FileShare-Common/obj
/FileShare-API/logs
/FileShare-API/fileshare-api.db
/FileShare-API/fileshare-api.db-shm
/FileShare-API/fileshare-api.db-wal
/FileShare-API/app.db
/FileShare-API/app.db-shm
/FileShare-API/app.db-wal
/FileShare-API/wwwroot
/package-output
/package-scripts/tools
/.vs
/bin
/obj
/.claude
/.codex-build
/FileShare-API/Properties
+4
View File
@@ -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.
-50
View File
@@ -1,50 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Avalonia_API</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="8.1.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia-Services\Avalonia-Services.csproj" />
<ProjectReference Include="..\Avalonia-Common\Avalonia-Common.csproj" />
<ProjectReference Include="..\Avalonia-EFCore\Avalonia-EFCore.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers\" />
</ItemGroup>
<Target Name="RestoreFrontendPackages" BeforeTargets="Build" Condition="'$(SkipFrontendBuild)' != 'true' And Exists('..\Avalonia-Web-VUE\package.json') And !Exists('..\Avalonia-Web-VUE\node_modules')">
<Message Importance="high" Text="Restoring Avalonia-Web-VUE npm packages..." />
<Exec WorkingDirectory="..\Avalonia-Web-VUE" Command="npm.cmd install" />
</Target>
<Target Name="BuildFrontend" BeforeTargets="Build" DependsOnTargets="RestoreFrontendPackages" Condition="'$(SkipFrontendBuild)' != 'true' And Exists('..\Avalonia-Web-VUE\package.json')">
<Message Importance="high" Text="Building Avalonia-Web-VUE into Avalonia-API/wwwroot..." />
<Exec WorkingDirectory="..\Avalonia-Web-VUE" Command="npm.cmd run build-only" />
<RemoveDir Directories="wwwroot" />
<MakeDir Directories="wwwroot" />
<ItemGroup>
<FrontendDist Include="..\Avalonia-Web-VUE\dist\**\*.*" />
</ItemGroup>
<Copy
SourceFiles="@(FrontendDist)"
DestinationFiles="@(FrontendDist->'wwwroot\%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="false" />
</Target>
</Project>
-6
View File
@@ -1,6 +0,0 @@
@Avalonia_API_HostAddress = http://localhost:5206
GET {{Avalonia_API_HostAddress}}/weatherforecast/
Accept: application/json
###
@@ -1,47 +0,0 @@
using Avalonia_EFCore.Database;
using Microsoft.EntityFrameworkCore;
namespace Avalonia_API.Extensions
{
public static class FileStreamEndpointExtensions
{
public static IEndpointRouteBuilder MapFileStreamEndpoints(this IEndpointRouteBuilder app)
{
app.MapMethods("/api/files/{id:int}/stream", ["GET", "HEAD"], async (int id, AppDataContext db, HttpContext httpContext) =>
{
// Browsers cancel in-flight range requests aggressively while seeking.
// Keep this small metadata lookup independent from RequestAborted so
// EF does not throw TaskCanceledException before the file is opened.
var file = await db.ManagedFileRecords
.AsNoTracking()
.Include(item => item.LibraryRoot)
.FirstOrDefaultAsync(item =>
item.Id == id
&& item.Exists
&& item.LibraryRoot != null
&& item.LibraryRoot.IsAvailable);
if (file is null || !System.IO.File.Exists(file.AbsolutePath))
{
return Results.NotFound();
}
var stream = System.IO.File.Open(file.AbsolutePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
httpContext.Response.Headers.ContentDisposition = $"inline; filename=\"{Uri.EscapeDataString(file.FileName)}\"";
httpContext.Response.Headers.AcceptRanges = "bytes";
httpContext.Response.Headers.CacheControl = "public, max-age=3600";
return Results.File(
stream,
contentType: file.ContentType,
fileDownloadName: null,
lastModified: file.LastWriteTimeUtc,
enableRangeProcessing: true);
})
.WithName("StreamManagedFile")
.WithTags("FileLibrary");
return app;
}
}
}
@@ -1,38 +0,0 @@
using Avalonia_Services.Services.FileLibrary;
namespace Avalonia_API.Services
{
public sealed class FileLibraryScanHostedService(IServiceScopeFactory scopeFactory, ILogger<FileLibraryScanHostedService> logger)
: BackgroundService
{
private static readonly TimeSpan Interval = TimeSpan.FromMinutes(1);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await ScanAsync(stoppingToken);
using var timer = new PeriodicTimer(Interval);
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await ScanAsync(stoppingToken);
}
}
private async Task ScanAsync(CancellationToken cancellationToken)
{
try
{
await using var scope = scopeFactory.CreateAsyncScope();
var scanner = scope.ServiceProvider.GetRequiredService<IFileLibraryService>();
await scanner.ScanDueRootsAsync(cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
}
catch (Exception ex)
{
logger.LogWarning(ex, "文件库定时扫描失败。");
}
}
}
}
@@ -1,178 +0,0 @@
// <auto-generated />
using System;
using Avalonia_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Avalonia_EFCore.Migrations.MySQL
{
[DbContext(typeof(MySqlAppDataContext))]
partial class MySqlAppDataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime(6)")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("varchar(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("varchar(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("datetime(6)")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)")
.HasColumnName("token-hash");
b.Property<int>("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("Avalonia_EFCore.Models.UserEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("用户主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("updated-at")
.HasComment("更新时间");
b.HasKey("Id")
.HasName("pk-user");
b.ToTable("user", t =>
{
t.HasComment("用户实体,演示数据库 CRUD 操作");
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("天气预报主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("int")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("updated-at")
.HasComment("更新时间");
b.HasKey("Id")
.HasName("pk-weather-forecast");
b.ToTable("weather-forecast", t =>
{
t.HasComment("天气预报数据实体");
});
});
#pragma warning restore 612, 618
}
}
}
@@ -1,187 +0,0 @@
// <auto-generated />
using System;
using Avalonia_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Avalonia_EFCore.Migrations.PostgreSQL
{
[DbContext(typeof(PostgreSqlAppDataContext))]
partial class PostgreSqlAppDataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("token-hash");
b.Property<int>("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("Avalonia_EFCore.Models.UserEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasComment("用户主键");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasComment("天气预报主键");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("integer")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated-at")
.HasComment("更新时间");
b.HasKey("Id")
.HasName("pk-weather-forecast");
b.ToTable("weather-forecast", t =>
{
t.HasComment("天气预报数据实体");
});
});
#pragma warning restore 612, 618
}
}
}
@@ -1,187 +0,0 @@
// <auto-generated />
using System;
using Avalonia_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Avalonia_EFCore.Migrations.SqlServer
{
[DbContext(typeof(SqlServerAppDataContext))]
partial class SqlServerAppDataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime2")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("datetime2")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)")
.HasColumnName("token-hash");
b.Property<int>("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("Avalonia_EFCore.Models.UserEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("用户主键");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2")
.HasColumnName("updated-at")
.HasComment("更新时间");
b.HasKey("Id")
.HasName("pk-user");
b.ToTable("user", t =>
{
t.HasComment("用户实体,演示数据库 CRUD 操作");
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("天气预报主键");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("int")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2")
.HasColumnName("updated-at")
.HasComment("更新时间");
b.HasKey("Id")
.HasName("pk-weather-forecast");
b.ToTable("weather-forecast", t =>
{
t.HasComment("天气预报数据实体");
});
});
#pragma warning restore 612, 618
}
}
}
-11
View File
@@ -1,11 +0,0 @@
<Solution>
<Project Path="../Avalonia-API/Avalonia-API.csproj" Id="e33aba9a-a56b-4f6b-8eaa-3acbed65ebad" />
<Project Path="../Avalonia-Common/Avalonia-Common.csproj" Id="caed4118-2161-4382-90b8-35fb4efe3b5f" />
<Project Path="../Avalonia-EFCore/Avalonia-EFCore.csproj" Id="64557501-62a7-4863-b2bf-1570b8c6fecb" />
<Project Path="../Avalonia-Services/Avalonia-Services.csproj" Id="b8757cf9-5422-4c67-acae-3c967c95f866" />
<Project Path="../Avalonia-Web-VUE/avalonia-web-vue.esproj">
<Build />
<Deploy />
</Project>
<Project Path="Avalonia-PC.csproj" />
</Solution>
-188
View File
@@ -1,188 +0,0 @@
using Avalonia_Common.Core;
using Avalonia_EFCore.Database;
using Avalonia_EFCore.Models;
using Avalonia_Services.Core;
using Avalonia_Services.Services;
using Avalonia_Services.Services.FileLibrary;
using Microsoft.EntityFrameworkCore;
namespace Avalonia_Services.Endpoints
{
/// <summary>
/// 统一端点配置 —— 所有业务端点在此定义一次。
/// 这是 Avalonia-API 和 Avalonia-PC 的唯一入口。
/// </summary>
public static class AppEndpoints
{
/// <summary>
/// 配置所有业务端点。调用方传入 builder,按需叠加鉴权、过滤器等。
/// </summary>
/// <param name="builder">端点构建器</param>
/// <param name="includeDetails">是否在错误响应中包含异常详情(开发环境 true</param>
public static ServiceEndpointBuilder Configure(ServiceEndpointBuilder builder, bool includeDetails = false)
{
// ---- 全局异常拦截(自动捕获所有端点中未处理的异常) ----
builder.Endpoints.AddGlobalFilter(new GlobalExceptionFilter(includeDetails));
builder.ConfigureEndpoints(endpoints =>
{
// ---- 全局日志过滤器(记录每个请求) ----
endpoints.AddGlobalFilter(async (ctx, next) =>
{
Serilog.Log.Debug("→ {Method} {Path}", ctx.Method, ctx.Path);
await next(ctx);
Serilog.Log.Debug("← {Method} {Path} | {StatusCode}", ctx.Method, ctx.Path, ctx.StatusCode);
});
// ---- 业务端点注册 ----
// 天气预报(从数据库读取)
endpoints.MapGet("api/wData", GetWeatherForecastsAsync)
.WithOpenApi("Weather", "获取天气预报信息。")
.WithName("GetWeatherForecast");
// 获取用户(演示从数据库查询)
endpoints.MapGet("api/getUser", GetUserFromDatabaseAsync)
.WithName("GetUser");
// 处理数据(POST — 演示参数处理)
endpoints.MapPost("api/processData", ProcessDataAsync)
.WithName("ProcessData");
endpoints.MapGet<IFileLibraryEndpointService>("api/library/drives", (service, ctx) => service.GetDrivesAsync(ctx))
.WithOpenApi("FileLibrary", "查询服务器磁盘。")
.WithName("GetLibraryDrives");
endpoints.MapGet<IFileLibraryEndpointService>("api/library/directories", (service, ctx) => service.GetDirectoriesAsync(ctx))
.WithOpenApi("FileLibrary", "查询服务器目录。")
.WithName("GetLibraryDirectories");
endpoints.MapGet<IFileLibraryEndpointService>("api/library/roots", (service, ctx) => service.GetRootsAsync(ctx))
.WithOpenApi("FileLibrary", "查询文件库目录。")
.WithName("GetLibraryRoots");
endpoints.MapPost<IFileLibraryEndpointService>("api/library/roots", (service, ctx) => service.AddRootAsync(ctx))
.WithOpenApi("FileLibrary", "添加文件库目录。")
.WithName("AddLibraryRoot");
endpoints.MapPost<IFileLibraryEndpointService>("api/library/roots/enabled", (service, ctx) => service.SetRootEnabledAsync(ctx))
.WithOpenApi("FileLibrary", "启用或禁用文件库目录。")
.WithName("SetLibraryRootEnabled");
endpoints.MapPost<IFileLibraryEndpointService>("api/library/roots/delete", (service, ctx) => service.DeleteRootAsync(ctx))
.WithOpenApi("FileLibrary", "删除文件库目录。")
.WithName("DeleteLibraryRoot");
endpoints.MapPost<IFileLibraryEndpointService>("api/library/roots/scan", (service, ctx) => service.ScanRootAsync(ctx))
.WithOpenApi("FileLibrary", "立即扫描文件库目录。")
.WithName("ScanLibraryRoot");
endpoints.MapGet<IFileLibraryEndpointService>("api/files", (service, ctx) => service.SearchFilesAsync(ctx))
.WithOpenApi("FileLibrary", "分页查询已扫描文件。")
.WithName("SearchFiles");
endpoints.MapGet<IFileLibraryEndpointService>("api/files/detail", (service, ctx) => service.GetFileAsync(ctx))
.WithOpenApi("FileLibrary", "查询文件详情。")
.WithName("GetFileDetail");
endpoints.MapGet<IFileLibraryEndpointService>("api/files/text", (service, ctx) => service.GetTextPreviewAsync(ctx))
.WithOpenApi("FileLibrary", "预览文本文件。")
.WithName("GetTextPreview");
// ---- 需要鉴权的端点示例 ----
// endpoints.MapGet("api/admin/dashboard", AdminDashboardAsync)
// .WithName("AdminDashboard")
// .RequireAuthorization = true
// .Policy = "AdminOnly";
});
return builder;
}
#region
/// <summary>
/// 从数据库查询天气预报(优先数据库,回退到内存生成)。
/// </summary>
private static async Task<object?> GetWeatherForecastsAsync(ServiceEndpointContext ctx)
{
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
// 尝试从数据库读取
if (sp?.GetService(typeof(AppDataContext)) is AppDataContext db)
{
var dbForecasts = await db.WeatherForecasts
.OrderByDescending(f => f.Date)
.Take(5)
.ToListAsync();
if (dbForecasts.Count > 0)
{
return ResponseHelper.Ok(dbForecasts, "获取天气预报成功(来自数据库)");
}
}
// 回退:内存生成(数据库为空时)
var service = sp?.GetService(typeof(WeatherForecastService)) as WeatherForecastService
?? new WeatherForecastService();
var forecasts = service.GetWeatherForecasts();
return ResponseHelper.Ok(forecasts, "获取天气预报成功(内存生成)");
}
/// <summary>
/// 从数据库获取用户信息(演示数据库查询),若无数据则返回演示用户。
/// </summary>
/// <param name="ctx">服务端点上下文。</param>
/// <returns>用户信息。</returns>
private static async Task<object?> GetUserFromDatabaseAsync(ServiceEndpointContext ctx)
{
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
// 尝试从数据库读取用户
if (sp?.GetService(typeof(AppDataContext)) is AppDataContext db)
{
var users = await db.Set<UserEntity>().Take(1).ToListAsync();
if (users.Count > 0)
{
return ResponseHelper.Ok(users[0], "获取用户成功(来自数据库)");
}
}
// 回退:演示数据
await Task.Delay(100);
var user = new { id = 1, name = "张三", email = "zhangsan@example.com" };
return ResponseHelper.Ok(user);
}
/// <summary>
/// 处理前端发送的数据(POST 演示),将数据存入数据库或转为大写返回。
/// </summary>
/// <param name="ctx">服务端点上下文。</param>
/// <returns>处理结果。</returns>
private static async Task<object?> ProcessDataAsync(ServiceEndpointContext ctx)
{
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
// 演示:将收到的数据存入数据库
var input = ctx.Body ?? string.Empty;
if (sp?.GetService(typeof(AppDataContext)) is AppDataContext db && !string.IsNullOrWhiteSpace(input))
{
var forecast = new WeatherForecastEntity
{
Date = DateOnly.FromDateTime(DateTime.Now),
TemperatureC = 20,
Summary = input,
};
db.WeatherForecasts.Add(forecast);
await db.SaveChangesAsync();
return ResponseHelper.Ok(forecast, "数据已存入数据库");
}
await Task.Delay(200);
return ResponseHelper.Ok(new { input, processed = input.ToUpperInvariant() });
}
#endregion
}
}
@@ -1,64 +0,0 @@
using System.Text.Json.Serialization;
namespace Avalonia_Services.Services.FileLibrary
{
public sealed record AddLibraryRootRequest(
[property: JsonPropertyName("path")] string? Path,
[property: JsonPropertyName("displayName")] string? DisplayName = null,
[property: JsonPropertyName("scanIntervalMinutes")] int? ScanIntervalMinutes = null);
public sealed record UpdateLibraryRootRequest(
[property: JsonPropertyName("id")] int Id,
[property: JsonPropertyName("isEnabled")] bool IsEnabled);
public sealed record ScanLibraryRootRequest(
[property: JsonPropertyName("id")] int Id);
public sealed record DeleteLibraryRootRequest(
[property: JsonPropertyName("id")] int Id);
public sealed record DriveDto(
string Name,
string DisplayName,
string RootDirectory,
string DriveType,
long? TotalSize,
long? AvailableFreeSpace,
bool IsReady);
public sealed record DirectoryDto(
string Name,
string FullPath);
public sealed record LibraryRootDto(
int Id,
string Path,
string DisplayName,
bool IsEnabled,
bool IsAvailable,
int ScanIntervalMinutes,
DateTime? LastScanStartedAt,
DateTime? LastScanCompletedAt,
string? LastScanError,
int FileCount);
public sealed record FileRecordDto(
int Id,
int LibraryRootId,
string FileName,
string RelativePath,
string Extension,
long SizeBytes,
DateTime LastWriteTimeUtc,
string MediaType,
string ContentType,
string StreamUrl,
string? TextUrl,
bool BrowserPlayable);
public sealed record TextPreviewDto(
int Id,
string FileName,
string Content,
bool Truncated);
}
@@ -1,99 +0,0 @@
using Avalonia_Common.Core;
using Avalonia_Services.Core;
using System.Text.Json;
namespace Avalonia_Services.Services.FileLibrary
{
public sealed class FileLibraryEndpointService(IFileLibraryService fileLibrary) : IFileLibraryEndpointService
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
};
public async Task<object?> GetDrivesAsync(ServiceEndpointContext ctx)
{
return ResponseHelper.Ok(await fileLibrary.GetDrivesAsync());
}
public async Task<object?> GetDirectoriesAsync(ServiceEndpointContext ctx)
{
var path = ctx.Query.GetValueOrDefault("path");
return ResponseHelper.Ok(await fileLibrary.GetDirectoriesAsync(path));
}
public async Task<object?> GetRootsAsync(ServiceEndpointContext ctx)
{
return ResponseHelper.Ok(await fileLibrary.GetRootsAsync());
}
public async Task<object?> AddRootAsync(ServiceEndpointContext ctx)
{
var request = ReadBody<AddLibraryRootRequest>(ctx);
return ResponseHelper.Ok(await fileLibrary.AddRootAsync(request), "文件库目录已添加并完成扫描。");
}
public async Task<object?> SetRootEnabledAsync(ServiceEndpointContext ctx)
{
var request = ReadBody<UpdateLibraryRootRequest>(ctx);
return ResponseHelper.Ok(await fileLibrary.SetRootEnabledAsync(request), "文件库目录状态已更新。");
}
public async Task<object?> DeleteRootAsync(ServiceEndpointContext ctx)
{
var request = ReadBody<DeleteLibraryRootRequest>(ctx);
await fileLibrary.DeleteRootAsync(request);
return ResponseHelper.Succeed("文件库目录已删除。");
}
public async Task<object?> ScanRootAsync(ServiceEndpointContext ctx)
{
var request = ReadBody<ScanLibraryRootRequest>(ctx);
return ResponseHelper.Ok(await fileLibrary.ScanRootAsync(request.Id), "文件库目录扫描完成。");
}
public async Task<object?> SearchFilesAsync(ServiceEndpointContext ctx)
{
return await fileLibrary.SearchFilesAsync(ctx);
}
public async Task<object?> GetFileAsync(ServiceEndpointContext ctx)
{
var id = ReadId(ctx);
var file = await fileLibrary.GetFileAsync(id);
return file is null
? ResponseHelper.Failure(404, "文件不存在或尚未扫描入库。")
: ResponseHelper.Ok(file);
}
public async Task<object?> GetTextPreviewAsync(ServiceEndpointContext ctx)
{
var id = ReadId(ctx);
var preview = await fileLibrary.GetTextPreviewAsync(id);
return preview is null
? ResponseHelper.Failure(404, "文本文件不存在或无法预览。")
: ResponseHelper.Ok(preview);
}
private static T ReadBody<T>(ServiceEndpointContext ctx)
{
if (string.IsNullOrWhiteSpace(ctx.Body))
{
throw new InvalidOperationException("请求体不能为空。");
}
var body = JsonSerializer.Deserialize<T>(ctx.Body, JsonOptions);
return body ?? throw new InvalidOperationException("请求体格式错误。");
}
private static int ReadId(ServiceEndpointContext ctx)
{
if (int.TryParse(ctx.Query.GetValueOrDefault("id"), out var id) && id > 0)
{
return id;
}
throw new InvalidOperationException("id 参数无效。");
}
}
}
@@ -1,409 +0,0 @@
using Avalonia_Common.Core;
using Avalonia_EFCore.Database;
using Avalonia_EFCore.Models;
using Avalonia_Services.Core;
using Microsoft.EntityFrameworkCore;
using System.Text;
namespace Avalonia_Services.Services.FileLibrary
{
public sealed class FileLibraryService(AppDataContext db) : IFileLibraryService
{
private const int DefaultScanIntervalMinutes = 5;
private const int MaxTextPreviewBytes = 1024 * 1024;
public Task<List<DriveDto>> GetDrivesAsync(CancellationToken cancellationToken = default)
{
var drives = DriveInfo.GetDrives()
.Select(drive => new DriveDto(
drive.Name,
drive.IsReady ? $"{drive.Name} ({drive.VolumeLabel})" : drive.Name,
drive.RootDirectory.FullName,
drive.DriveType.ToString(),
SafeDriveValue(drive, d => d.TotalSize),
SafeDriveValue(drive, d => d.AvailableFreeSpace),
drive.IsReady))
.OrderBy(drive => drive.Name)
.ToList();
return Task.FromResult(drives);
}
public Task<List<DirectoryDto>> GetDirectoriesAsync(string? path, CancellationToken cancellationToken = default)
{
var normalized = NormalizeExistingDirectory(path);
var directories = Directory.EnumerateDirectories(normalized)
.Select(directory => new DirectoryInfo(directory))
.OrderBy(directory => directory.Name)
.Select(directory => new DirectoryDto(directory.Name, directory.FullName))
.ToList();
return Task.FromResult(directories);
}
public async Task<List<LibraryRootDto>> GetRootsAsync(CancellationToken cancellationToken = default)
{
var counts = await db.ManagedFileRecords
.Where(file => file.Exists)
.GroupBy(file => file.LibraryRootId)
.Select(group => new { RootId = group.Key, Count = group.Count() })
.ToDictionaryAsync(item => item.RootId, item => item.Count, cancellationToken);
var roots = await db.ManagedLibraryRoots
.OrderBy(root => root.Path)
.ToListAsync(cancellationToken);
return roots.Select(root => ToRootDto(root, counts.GetValueOrDefault(root.Id))).ToList();
}
public async Task<LibraryRootDto> AddRootAsync(AddLibraryRootRequest request, CancellationToken cancellationToken = default)
{
var normalized = NormalizeExistingDirectory(request.Path);
var existing = await db.ManagedLibraryRoots.FirstOrDefaultAsync(root => root.Path == normalized, cancellationToken);
if (existing is not null)
{
existing.IsEnabled = true;
existing.IsAvailable = true;
existing.DisplayName = ResolveDisplayName(normalized, request.DisplayName);
existing.ScanIntervalMinutes = NormalizeInterval(request.ScanIntervalMinutes);
await db.SaveChangesAsync(cancellationToken);
return await ScanRootAsync(existing.Id, cancellationToken);
}
var root = new ManagedLibraryRoot
{
Path = normalized,
DisplayName = ResolveDisplayName(normalized, request.DisplayName),
ScanIntervalMinutes = NormalizeInterval(request.ScanIntervalMinutes),
IsEnabled = true,
IsAvailable = true,
};
db.ManagedLibraryRoots.Add(root);
await db.SaveChangesAsync(cancellationToken);
return await ScanRootAsync(root.Id, cancellationToken);
}
public async Task<LibraryRootDto> SetRootEnabledAsync(UpdateLibraryRootRequest request, CancellationToken cancellationToken = default)
{
var root = await db.ManagedLibraryRoots.FirstOrDefaultAsync(item => item.Id == request.Id, cancellationToken)
?? throw new InvalidOperationException("文件库目录不存在。");
root.IsEnabled = request.IsEnabled;
await db.SaveChangesAsync(cancellationToken);
var count = await db.ManagedFileRecords.CountAsync(file => file.LibraryRootId == root.Id && file.Exists, cancellationToken);
return ToRootDto(root, count);
}
public async Task DeleteRootAsync(DeleteLibraryRootRequest request, CancellationToken cancellationToken = default)
{
var root = await db.ManagedLibraryRoots.FirstOrDefaultAsync(item => item.Id == request.Id, cancellationToken)
?? throw new InvalidOperationException("文件库目录不存在。");
db.ManagedLibraryRoots.Remove(root);
await db.SaveChangesAsync(cancellationToken);
}
public async Task<LibraryRootDto> ScanRootAsync(int rootId, CancellationToken cancellationToken = default)
{
var root = await db.ManagedLibraryRoots.FirstOrDefaultAsync(item => item.Id == rootId, cancellationToken)
?? throw new InvalidOperationException("文件库目录不存在。");
root.LastScanStartedAt = DateTime.UtcNow;
root.LastScanError = null;
await db.SaveChangesAsync(cancellationToken);
try
{
if (!Directory.Exists(root.Path))
{
throw new DirectoryNotFoundException($"目录不存在:{root.Path}");
}
root.IsAvailable = true;
root.IsEnabled = true;
var existing = await db.ManagedFileRecords
.Where(file => file.LibraryRootId == root.Id)
.ToDictionaryAsync(file => file.AbsolutePath, StringComparer.OrdinalIgnoreCase, cancellationToken);
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var path in EnumerateSupportedFiles(root.Path))
{
cancellationToken.ThrowIfCancellationRequested();
var info = new FileInfo(path);
if (!info.Exists || !MediaFileTypes.TryGet(info.Extension.ToLowerInvariant(), out var mediaType, out var contentType, out _))
{
continue;
}
var absolutePath = info.FullName;
seen.Add(absolutePath);
if (!existing.TryGetValue(absolutePath, out var record))
{
record = new ManagedFileRecord
{
LibraryRootId = root.Id,
AbsolutePath = absolutePath,
};
db.ManagedFileRecords.Add(record);
}
record.FileName = info.Name;
record.RelativePath = Path.GetRelativePath(root.Path, absolutePath);
record.Extension = info.Extension.ToLowerInvariant();
record.SizeBytes = info.Length;
record.LastWriteTimeUtc = info.LastWriteTimeUtc;
record.MediaType = mediaType;
record.ContentType = contentType;
record.Exists = true;
record.LastSeenAt = DateTime.UtcNow;
}
foreach (var stale in existing.Values.Where(file => !seen.Contains(file.AbsolutePath)))
{
stale.Exists = false;
}
root.LastScanCompletedAt = DateTime.UtcNow;
await db.SaveChangesAsync(cancellationToken);
}
catch (Exception ex)
{
root.IsAvailable = false;
root.IsEnabled = false;
root.LastScanError = ex.Message;
root.LastScanCompletedAt = DateTime.UtcNow;
await db.SaveChangesAsync(CancellationToken.None);
throw;
}
var count = await db.ManagedFileRecords.CountAsync(file => file.LibraryRootId == root.Id && file.Exists, cancellationToken);
return ToRootDto(root, count);
}
public async Task ScanDueRootsAsync(CancellationToken cancellationToken = default)
{
var now = DateTime.UtcNow;
var roots = await db.ManagedLibraryRoots
.Where(root => root.IsEnabled && root.IsAvailable)
.ToListAsync(cancellationToken);
foreach (var root in roots)
{
var interval = Math.Max(1, root.ScanIntervalMinutes);
var isDue = root.LastScanCompletedAt is null || root.LastScanCompletedAt.Value.AddMinutes(interval) <= now;
if (!isDue)
{
continue;
}
try
{
await ScanRootAsync(root.Id, cancellationToken);
}
catch
{
// ScanRootAsync records the error on the root. Continue scanning other roots.
}
}
}
public async Task<PagedResponse<FileRecordDto>> SearchFilesAsync(ServiceEndpointContext ctx, CancellationToken cancellationToken = default)
{
var page = ParseInt(ctx.Query.GetValueOrDefault("page"), 1, 1, 100000);
var pageSize = ParseInt(ctx.Query.GetValueOrDefault("pageSize"), 24, 1, 100);
var mediaType = ctx.Query.GetValueOrDefault("mediaType")?.Trim();
var keyword = ctx.Query.GetValueOrDefault("keyword")?.Trim();
var rootId = ParseInt(ctx.Query.GetValueOrDefault("rootId"), 0, 0, int.MaxValue);
var query = db.ManagedFileRecords
.AsNoTracking()
.Where(file => file.Exists && file.LibraryRoot != null && file.LibraryRoot.IsAvailable);
if (!string.IsNullOrWhiteSpace(mediaType) && !mediaType.Equals("all", StringComparison.OrdinalIgnoreCase))
{
query = query.Where(file => file.MediaType == mediaType);
}
if (rootId > 0)
{
query = query.Where(file => file.LibraryRootId == rootId);
}
if (!string.IsNullOrWhiteSpace(keyword))
{
query = query.Where(file => file.FileName.Contains(keyword) || file.RelativePath.Contains(keyword));
}
var total = await query.CountAsync(cancellationToken);
var items = await query
.OrderBy(file => file.MediaType)
.ThenBy(file => file.RelativePath)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(file => ToFileDto(file))
.ToListAsync(cancellationToken);
return PagedResponse<FileRecordDto>.From(items, total, page, pageSize);
}
public async Task<FileRecordDto?> GetFileAsync(int id, CancellationToken cancellationToken = default)
{
return await db.ManagedFileRecords
.AsNoTracking()
.Where(file => file.Id == id && file.Exists && file.LibraryRoot != null && file.LibraryRoot.IsAvailable)
.Select(file => ToFileDto(file))
.FirstOrDefaultAsync(cancellationToken);
}
public async Task<TextPreviewDto?> GetTextPreviewAsync(int id, CancellationToken cancellationToken = default)
{
var file = await db.ManagedFileRecords
.AsNoTracking()
.Include(item => item.LibraryRoot)
.FirstOrDefaultAsync(item =>
item.Id == id
&& item.Exists
&& item.MediaType == "text"
&& item.LibraryRoot != null
&& item.LibraryRoot.IsAvailable,
cancellationToken);
if (file is null || !File.Exists(file.AbsolutePath))
{
return null;
}
await using var stream = File.OpenRead(file.AbsolutePath);
var limit = (int)Math.Min(stream.Length, MaxTextPreviewBytes);
var buffer = new byte[limit];
var read = await stream.ReadAsync(buffer.AsMemory(0, limit), cancellationToken);
var content = Encoding.UTF8.GetString(buffer, 0, read);
return new TextPreviewDto(file.Id, file.FileName, content, stream.Length > MaxTextPreviewBytes);
}
private static IEnumerable<string> EnumerateSupportedFiles(string rootPath)
{
var pending = new Stack<string>();
pending.Push(rootPath);
while (pending.Count > 0)
{
var current = pending.Pop();
IEnumerable<string> directories;
IEnumerable<string> files;
try
{
directories = Directory.EnumerateDirectories(current);
files = Directory.EnumerateFiles(current);
}
catch
{
continue;
}
foreach (var directory in directories)
{
pending.Push(directory);
}
foreach (var file in files)
{
if (MediaFileTypes.TryGet(Path.GetExtension(file), out _, out _, out _))
{
yield return file;
}
}
}
}
private static string NormalizeExistingDirectory(string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new InvalidOperationException("目录路径不能为空。");
}
var fullPath = Path.GetFullPath(path.Trim());
if (!Directory.Exists(fullPath))
{
throw new DirectoryNotFoundException($"目录不存在:{fullPath}");
}
return new DirectoryInfo(fullPath).FullName;
}
private static string ResolveDisplayName(string path, string? displayName)
{
if (!string.IsNullOrWhiteSpace(displayName))
{
return displayName.Trim();
}
var directory = new DirectoryInfo(path);
return string.IsNullOrWhiteSpace(directory.Name) ? directory.FullName : directory.Name;
}
private static int NormalizeInterval(int? interval)
{
return Math.Clamp(interval ?? DefaultScanIntervalMinutes, 1, 1440);
}
private static long? SafeDriveValue(DriveInfo drive, Func<DriveInfo, long> selector)
{
try
{
return drive.IsReady ? selector(drive) : null;
}
catch
{
return null;
}
}
private static LibraryRootDto ToRootDto(ManagedLibraryRoot root, int fileCount)
{
return new LibraryRootDto(
root.Id,
root.Path,
root.DisplayName,
root.IsEnabled,
root.IsAvailable,
root.ScanIntervalMinutes,
root.LastScanStartedAt,
root.LastScanCompletedAt,
root.LastScanError,
fileCount);
}
private static FileRecordDto ToFileDto(ManagedFileRecord file)
{
return new FileRecordDto(
file.Id,
file.LibraryRootId,
file.FileName,
file.RelativePath,
file.Extension,
file.SizeBytes,
file.LastWriteTimeUtc,
file.MediaType,
file.ContentType,
$"/api/files/{file.Id}/stream",
file.MediaType == "text" ? $"/api/files/text?id={file.Id}" : null,
MediaFileTypes.IsBrowserPlayable(file.Extension));
}
private static int ParseInt(string? value, int fallback, int min, int max)
{
return int.TryParse(value, out var parsed)
? Math.Clamp(parsed, min, max)
: fallback;
}
}
}
@@ -1,27 +0,0 @@
using Avalonia_Services.Core;
namespace Avalonia_Services.Services.FileLibrary
{
public interface IFileLibraryEndpointService
{
Task<object?> GetDrivesAsync(ServiceEndpointContext ctx);
Task<object?> GetDirectoriesAsync(ServiceEndpointContext ctx);
Task<object?> GetRootsAsync(ServiceEndpointContext ctx);
Task<object?> AddRootAsync(ServiceEndpointContext ctx);
Task<object?> SetRootEnabledAsync(ServiceEndpointContext ctx);
Task<object?> DeleteRootAsync(ServiceEndpointContext ctx);
Task<object?> ScanRootAsync(ServiceEndpointContext ctx);
Task<object?> SearchFilesAsync(ServiceEndpointContext ctx);
Task<object?> GetFileAsync(ServiceEndpointContext ctx);
Task<object?> GetTextPreviewAsync(ServiceEndpointContext ctx);
}
}
@@ -1,30 +0,0 @@
using Avalonia_Common.Core;
using Avalonia_Services.Core;
namespace Avalonia_Services.Services.FileLibrary
{
public interface IFileLibraryService
{
Task<List<DriveDto>> GetDrivesAsync(CancellationToken cancellationToken = default);
Task<List<DirectoryDto>> GetDirectoriesAsync(string? path, CancellationToken cancellationToken = default);
Task<List<LibraryRootDto>> GetRootsAsync(CancellationToken cancellationToken = default);
Task<LibraryRootDto> AddRootAsync(AddLibraryRootRequest request, CancellationToken cancellationToken = default);
Task<LibraryRootDto> SetRootEnabledAsync(UpdateLibraryRootRequest request, CancellationToken cancellationToken = default);
Task DeleteRootAsync(DeleteLibraryRootRequest request, CancellationToken cancellationToken = default);
Task<LibraryRootDto> ScanRootAsync(int rootId, CancellationToken cancellationToken = default);
Task ScanDueRootsAsync(CancellationToken cancellationToken = default);
Task<PagedResponse<FileRecordDto>> SearchFilesAsync(ServiceEndpointContext ctx, CancellationToken cancellationToken = default);
Task<FileRecordDto?> GetFileAsync(int id, CancellationToken cancellationToken = default);
Task<TextPreviewDto?> GetTextPreviewAsync(int id, CancellationToken cancellationToken = default);
}
}
-435
View File
@@ -1,435 +0,0 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { api, type DirectoryDto, type DriveDto, type FileRecordDto, type LibraryRootDto, type MediaType, type TextPreviewDto } from './api'
const isAdminPage = computed(() => window.location.pathname.toLowerCase().startsWith('/admin'))
const roots = ref<LibraryRootDto[]>([])
const drives = ref<DriveDto[]>([])
const directories = ref<DirectoryDto[]>([])
const files = ref<FileRecordDto[]>([])
const selectedFile = ref<FileRecordDto | null>(null)
const textPreview = ref<TextPreviewDto | null>(null)
const currentPath = ref('')
const manualPath = ref('')
const keyword = ref('')
const mediaType = ref<MediaType>('all')
const rootId = ref<number | undefined>()
const isBrowsingRoots = ref(true)
const page = ref(1)
const pageSize = 24
const total = ref(0)
const loading = ref(false)
const scanningId = ref<number | null>(null)
const errorMessage = ref('')
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize)))
const availableRoots = computed(() => roots.value.filter((root) => root.isAvailable))
const activeRoots = computed(() => roots.value.filter((root) => root.isEnabled && root.isAvailable))
const selectedRoot = computed(() => roots.value.find((root) => root.id === selectedFile.value?.libraryRootId))
const totalRootFiles = computed(() => roots.value.reduce((sum, root) => sum + root.fileCount, 0))
const selectedMediaUrl = computed(() => selectedFile.value ? api.mediaUrl(selectedFile.value.streamUrl) : '')
const clientTitle = computed(() => {
if (isBrowsingRoots.value) return '文件库'
return rootId.value ? roots.value.find((root) => root.id === rootId.value)?.displayName ?? '文件' : '文件'
})
function formatSize(bytes: number) {
if (bytes < 1024) return `${bytes} B`
const units = ['KB', 'MB', 'GB', 'TB']
let value = bytes / 1024
let index = 0
while (value >= 1024 && index < units.length - 1) {
value /= 1024
index += 1
}
return `${value.toFixed(value >= 10 ? 1 : 2)} ${units[index]}`
}
function formatDate(value: string | null) {
if (!value) return '未扫描'
return new Date(value).toLocaleString()
}
function setError(error: unknown) {
errorMessage.value = error instanceof Error ? error.message : '操作失败'
}
async function loadRoots() {
roots.value = await api.getRoots()
}
async function loadDrives() {
drives.value = await api.getDrives()
}
async function openDirectory(path: string) {
try {
errorMessage.value = ''
currentPath.value = path
manualPath.value = path
directories.value = await api.getDirectories(path)
} catch (error) {
setError(error)
}
}
async function addRoot(path = manualPath.value) {
try {
errorMessage.value = ''
loading.value = true
await api.addRoot({ path, scanIntervalMinutes: 5 })
await Promise.all([loadRoots(), loadFiles()])
} catch (error) {
setError(error)
} finally {
loading.value = false
}
}
async function toggleRoot(root: LibraryRootDto) {
try {
errorMessage.value = ''
await api.setRootEnabled(root.id, !root.isEnabled)
await loadRoots()
} catch (error) {
setError(error)
}
}
async function deleteRoot(root: LibraryRootDto) {
try {
errorMessage.value = ''
await api.deleteRoot(root.id)
if (rootId.value === root.id) rootId.value = undefined
await Promise.all([loadRoots(), loadFiles()])
} catch (error) {
setError(error)
}
}
async function scanRoot(root: LibraryRootDto) {
try {
errorMessage.value = ''
scanningId.value = root.id
await api.scanRoot(root.id)
await Promise.all([loadRoots(), loadFiles()])
} catch (error) {
setError(error)
} finally {
scanningId.value = null
}
}
async function loadFiles(resetPage = false) {
try {
errorMessage.value = ''
if (resetPage) page.value = 1
const result = await api.searchFiles({
page: page.value,
pageSize,
mediaType: mediaType.value,
keyword: keyword.value,
rootId: rootId.value,
})
files.value = result.items
total.value = result.total
if (!selectedFile.value || !files.value.some((file) => file.id === selectedFile.value?.id)) {
selectedFile.value = null
textPreview.value = null
}
} catch (error) {
setError(error)
}
}
async function openClientRoot(id: number) {
rootId.value = id
isBrowsingRoots.value = false
await loadFiles(true)
}
function backToRoots() {
isBrowsingRoots.value = true
rootId.value = undefined
selectedFile.value = null
textPreview.value = null
}
async function selectFile(file: FileRecordDto | null) {
selectedFile.value = file
textPreview.value = null
if (!file || file.mediaType !== 'text') return
try {
textPreview.value = await api.getTextPreview(file.id)
} catch (error) {
setError(error)
}
}
async function changePage(next: number) {
page.value = Math.min(Math.max(1, next), totalPages.value)
await loadFiles()
}
async function refreshAll() {
await Promise.all([loadRoots(), loadFiles()])
}
onMounted(async () => {
loading.value = true
try {
await loadRoots()
if (isAdminPage.value) {
await loadDrives()
} else {
total.value = activeRoots.value.reduce((sum, root) => sum + root.fileCount, 0)
}
} catch (error) {
setError(error)
} finally {
loading.value = false
}
})
</script>
<template>
<main v-if="isAdminPage" class="admin-shell">
<header class="admin-hero">
<div>
<p class="eyebrow">FileShare Admin</p>
<h1>文件库管理</h1>
<p>添加服务器本机磁盘或目录系统按状态定时扫描异常目录会自动下线并停止自动扫描</p>
</div>
<div class="admin-metrics">
<div>
<strong>{{ roots.length }}</strong>
<span>目录</span>
</div>
<div>
<strong>{{ activeRoots.length }}</strong>
<span>正常启用</span>
</div>
<div>
<strong>{{ totalRootFiles }}</strong>
<span>入库文件</span>
</div>
</div>
</header>
<p v-if="errorMessage" class="error-banner">{{ errorMessage }}</p>
<section class="admin-layout">
<section class="admin-card path-card">
<div class="card-heading">
<div>
<h2>添加扫描目录</h2>
<p>选择服务器路径或直接输入绝对路径</p>
</div>
<a href="/" class="client-link">客户端</a>
</div>
<label class="field">
<span>服务器路径</span>
<div class="inline-form">
<input v-model="manualPath" type="text" placeholder="例如 D:\Media 或 E:\" />
<button class="primary-button" type="button" :disabled="loading || !manualPath" @click="addRoot()">添加并扫描</button>
</div>
</label>
<div class="admin-browser">
<div class="drive-list">
<h3>磁盘</h3>
<button
v-for="drive in drives"
:key="drive.name"
class="drive-row"
type="button"
:disabled="!drive.isReady"
@click="openDirectory(drive.rootDirectory)"
>
<span>{{ drive.displayName }}</span>
<small>{{ drive.driveType }} · {{ drive.availableFreeSpace !== null ? formatSize(drive.availableFreeSpace) : '不可用' }}</small>
</button>
</div>
<div class="directory-list">
<div class="browser-header">
<h3>目录</h3>
<button type="button" class="text-button" :disabled="!currentPath" @click="addRoot(currentPath)">添加当前目录</button>
</div>
<p class="current-path">{{ currentPath || '请选择一个磁盘' }}</p>
<button
v-for="directory in directories"
:key="directory.fullPath"
class="directory-row"
type="button"
@click="openDirectory(directory.fullPath)"
>
{{ directory.name }}
</button>
</div>
</div>
</section>
<section class="admin-card roots-card">
<div class="card-heading">
<div>
<h2>目录状态</h2>
<p>异常目录不会自动扫描客户端也无法访问其中的文件手动扫描成功后恢复正常</p>
</div>
<button class="secondary-button" type="button" :disabled="loading" @click="refreshAll">刷新</button>
</div>
<div class="root-table">
<article v-for="root in roots" :key="root.id" class="root-item">
<div class="root-main">
<span :class="['status-pill', root.isAvailable ? 'ok' : 'bad']">{{ root.isAvailable ? '正常' : '异常' }}</span>
<div>
<strong>{{ root.displayName }}</strong>
<p>{{ root.path }}</p>
</div>
</div>
<div class="root-meta">
<span>{{ root.fileCount }} 个文件</span>
<span>{{ formatDate(root.lastScanCompletedAt) }}</span>
<span>{{ root.isEnabled ? '启用' : '停用' }}</span>
</div>
<p v-if="root.lastScanError" class="root-error">{{ root.lastScanError }}</p>
<div class="root-actions">
<button type="button" class="secondary-button" @click="scanRoot(root)">
{{ scanningId === root.id ? '扫描中' : root.isAvailable ? '立即扫描' : '手动扫描恢复' }}
</button>
<button type="button" class="secondary-button" :disabled="!root.isAvailable" @click="toggleRoot(root)">
{{ root.isEnabled ? '停用' : '启用' }}
</button>
<button type="button" class="danger-button" @click="deleteRoot(root)">删除</button>
</div>
</article>
<p v-if="roots.length === 0" class="empty-state">还没有添加扫描目录</p>
</div>
</section>
</section>
</main>
<main v-else class="client-shell">
<header class="mobile-header">
<div>
<h1>{{ clientTitle }}</h1>
<p>{{ isBrowsingRoots ? `${activeRoots.length} 个目录` : `${total} 个文件` }}</p>
</div>
<div class="mobile-header-actions">
<button v-if="!isBrowsingRoots" type="button" class="back-button" @click="backToRoots">返回</button>
<a href="/admin" class="admin-link">管理</a>
</div>
</header>
<p v-if="errorMessage" class="error-banner">{{ errorMessage }}</p>
<section v-if="isBrowsingRoots" class="root-picker">
<button
v-for="root in activeRoots"
:key="root.id"
class="root-tile"
type="button"
@click="openClientRoot(root.id)"
>
<span>{{ root.displayName }}</span>
<strong>{{ root.fileCount }}</strong>
<small>{{ root.path }}</small>
</button>
<p v-if="activeRoots.length === 0" class="empty-state">暂无可访问目录</p>
</section>
<section v-else class="mobile-filters">
<input v-model="keyword" type="search" placeholder="搜索文件" @keyup.enter="loadFiles(true)" />
<div class="filter-row">
<select v-model="mediaType" @change="loadFiles(true)">
<option value="all">全部</option>
<option value="video">视频</option>
<option value="audio">音频</option>
<option value="text">文本</option>
</select>
<select v-model="rootId" @change="loadFiles(true)">
<option v-for="root in availableRoots" :key="root.id" :value="root.id">{{ root.displayName }}</option>
</select>
<button class="primary-button" type="button" @click="loadFiles(true)">查询</button>
</div>
</section>
<section v-if="!isBrowsingRoots" class="player-panel">
<p v-if="!selectedFile" class="empty-state">请选择一个文件</p>
<template v-else>
<div class="player-title">
<div>
<h2>{{ selectedFile.fileName }}</h2>
<p>{{ selectedRoot?.displayName ?? '文件库' }} · {{ selectedFile.relativePath }}</p>
</div>
<span>{{ selectedFile.extension }}</span>
</div>
<video
v-if="selectedFile.mediaType === 'video' && selectedFile.browserPlayable"
:key="selectedFile.id"
controls
playsinline
webkit-playsinline
preload="metadata"
>
<source :src="selectedMediaUrl" :type="selectedFile.contentType" />
</video>
<audio
v-else-if="selectedFile.mediaType === 'audio' && selectedFile.browserPlayable"
:key="selectedFile.id"
controls
preload="metadata"
>
<source :src="selectedMediaUrl" :type="selectedFile.contentType" />
</audio>
<pre v-else-if="selectedFile.mediaType === 'text'">{{ textPreview?.content ?? '加载中...' }}</pre>
<p v-else class="unsupported">浏览器不支持在线播放此格式</p>
<a
v-if="selectedFile.mediaType !== 'text'"
class="open-media-link"
:href="selectedMediaUrl"
target="_blank"
rel="noreferrer"
>
新窗口打开原视频/音频
</a>
<p v-if="textPreview?.truncated" class="hint">文本超过 1 MB已截断显示</p>
</template>
</section>
<section v-if="!isBrowsingRoots" class="mobile-list">
<button
v-for="file in files"
:key="file.id"
class="mobile-file"
:class="{ active: selectedFile?.id === file.id }"
type="button"
@click="selectFile(file)"
>
<span class="type-badge">{{ file.mediaType }}</span>
<span>
<strong>{{ file.fileName }}</strong>
<small>{{ file.relativePath }}</small>
<small>
{{ formatSize(file.sizeBytes) }} · {{ formatDate(file.lastWriteTimeUtc) }}
<template v-if="file.mediaType !== 'text' && !file.browserPlayable"> · 手机可能不支持</template>
</small>
</span>
</button>
<p v-if="files.length === 0" class="empty-state">暂无可查看文件</p>
</section>
<nav v-if="!isBrowsingRoots" class="mobile-pager">
<button type="button" :disabled="page <= 1" @click="changePage(page - 1)">上一页</button>
<span>{{ page }} / {{ totalPages }}</span>
<button type="button" :disabled="page >= totalPages" @click="changePage(page + 1)">下一页</button>
</nav>
</main>
</template>
@@ -1,12 +1,11 @@
using Avalonia_Common.Core;
using Avalonia_EFCore.Database;
using Avalonia_EFCore.Models;
using Avalonia_Services.Core;
using Avalonia_Services.Services.AuthService;
using FileShare_Common.Core;
using FileShare_EFCore.Database;
using FileShare_EFCore.Models;
using FileShare_Services.Core;
using FileShare_Services.Services.AuthService;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
namespace Avalonia_API.Authentication
namespace FileShare_API.Authentication
{
/// <summary>
/// API 鉴权端点服务,实现 <see cref="IApiAuthEndpointService"/>
@@ -17,21 +16,15 @@ namespace Avalonia_API.Authentication
JwtTokenService jwtTokenService,
RefreshTokenService refreshTokenService) : IApiAuthEndpointService
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
};
/// <summary>
/// 处理用户登录请求。根据账号(邮箱或用户名)查找或创建用户,
/// 生成 JWT Access Token 和 Refresh Token 并返回。
/// </summary>
/// <param name="ctx">服务端点上下文,包含请求体、请求头等信息。</param>
/// <returns>包含 AccessToken、RefreshToken 及过期时间的认证响应。</returns>
public async Task<object?> LoginAsync(ServiceEndpointContext ctx)
public async Task<IApiResponse> LoginAsync(ApiLoginRequest request, ServiceEndpointContext ctx)
{
var request = Deserialize<ApiLoginRequest>(ctx.Body);
if (string.IsNullOrWhiteSpace(request?.Account))
if (string.IsNullOrWhiteSpace(request.Account))
{
ctx.StatusCode = 400;
return ResponseHelper.Failure(400, "账号不能为空");
@@ -72,11 +65,10 @@ namespace Avalonia_API.Authentication
/// </summary>
/// <param name="ctx">服务端点上下文,包含请求体中的 RefreshToken。</param>
/// <returns>新的 Token 对;若 Refresh Token 无效则返回 401 错误。</returns>
public async Task<object?> RefreshAsync(ServiceEndpointContext ctx)
public async Task<IApiResponse> RefreshAsync(ApiRefreshTokenRequest request, ServiceEndpointContext ctx)
{
var request = Deserialize<ApiRefreshTokenRequest>(ctx.Body);
var rotated = await refreshTokenService.RotateAsync(
request?.RefreshToken,
request.RefreshToken,
ctx.GetHeader("User-Agent"),
GetRemoteIpAddress(ctx));
@@ -109,26 +101,12 @@ namespace Avalonia_API.Authentication
/// </summary>
/// <param name="ctx">服务端点上下文,包含请求体中的 RefreshToken。</param>
/// <returns>登出成功的响应。</returns>
public async Task<object?> LogoutAsync(ServiceEndpointContext ctx)
public async Task<IApiResponse> LogoutAsync(ApiLogoutRequest request, ServiceEndpointContext ctx)
{
var request = Deserialize<ApiLogoutRequest>(ctx.Body);
await refreshTokenService.RevokeAsync(request?.RefreshToken);
await refreshTokenService.RevokeAsync(request.RefreshToken);
return ResponseHelper.Succeed("退出成功");
}
/// <summary>
/// 将 JSON 请求体反序列化为指定类型。
/// </summary>
/// <typeparam name="T">目标类型。</typeparam>
/// <param name="body">JSON 请求体字符串,可为空。</param>
/// <returns>反序列化后的对象;若 body 为空则返回默认值。</returns>
private static T? Deserialize<T>(string? body)
{
return string.IsNullOrWhiteSpace(body)
? default
: JsonSerializer.Deserialize<T>(body, JsonOptions);
}
/// <summary>
/// 从上下文的 Items 中提取 ASP.NET Core HttpContext,并获取客户端远程 IP 地址。
/// </summary>
@@ -1,4 +1,4 @@
namespace Avalonia_API.Authentication
namespace FileShare_API.Authentication
{
/// <summary>
/// JWT 鉴权配置选项,从 appsettings.json 的 Jwt 节绑定。
@@ -8,12 +8,12 @@ namespace Avalonia_API.Authentication
/// <summary>
/// 获取或设置 Token 签发者。
/// </summary>
public string Issuer { get; set; } = "Avalonia-API";
public string Issuer { get; set; } = "FileShare-API";
/// <summary>
/// 获取或设置 Token 受众。
/// </summary>
public string Audience { get; set; } = "Avalonia-Client";
public string Audience { get; set; } = "FileShare-Client";
/// <summary>
/// 获取或设置签名密钥(至少 32 字节)。
@@ -1,11 +1,11 @@
using Avalonia_EFCore.Models;
using FileShare_EFCore.Models;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace Avalonia_API.Authentication
namespace FileShare_API.Authentication
{
/// <summary>
/// JWT Token 服务,负责创建包含用户声明和角色的 Access Token。
@@ -1,11 +1,11 @@
using Avalonia_EFCore.Database;
using Avalonia_EFCore.Models;
using FileShare_EFCore.Database;
using FileShare_EFCore.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using System.Security.Cryptography;
using System.Text;
namespace Avalonia_API.Authentication
namespace FileShare_API.Authentication
{
/// <summary>
/// Refresh Token 服务,负责创建、查找、撤销和轮换 Refresh Token
@@ -1,17 +1,18 @@
using Avalonia_API.Authentication;
using Avalonia_API.Services;
using Avalonia_EFCore.Database;
using Avalonia_Services.Core;
using Avalonia_Services.Endpoints;
using Avalonia_Services.Services;
using Avalonia_Services.Services.AuthService;
using Avalonia_Services.Services.FileLibrary;
using FileShare_API.Authentication;
using FileShare_API.Services;
using FileShare_EFCore.Database;
using FileShare_Services.Core;
using FileShare_Services.Endpoints;
using FileShare_Services.Services;
using FileShare_Services.Services.AuthService;
using FileShare_Services.Services.FileLibrary;
using FileShare_Services.Services.QrCode;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
namespace Avalonia_API.Configuration
namespace FileShare_API.Configuration
{
/// <summary>
/// API 项目服务配置扩展类,负责注册数据库、鉴权、业务服务和统一端点。
@@ -20,7 +21,7 @@ namespace Avalonia_API.Configuration
{
/// <summary>
/// 注册统一端点及其依赖的服务(含数据库)。
/// 所有业务端点定义在 Avalonia-Services/Endpoints/AppEndpoints.cs。
/// 所有业务端点定义在 FileShare-Services/Endpoints/AppEndpoints.cs。
/// </summary>
public static IServiceCollection AddUnifiedApiServices(this IServiceCollection services, IConfiguration configuration)
{
@@ -39,8 +40,19 @@ namespace Avalonia_API.Configuration
// ---- 业务服务 ----
services.AddScoped<WeatherForecastService>();
var thumbnailOptions = configuration
.GetSection(nameof(ThumbnailStorageOptions))
.Get<ThumbnailStorageOptions>()
?? new ThumbnailStorageOptions();
services.AddSingleton(thumbnailOptions);
services.AddSingleton<IVideoThumbnailService>(sp =>
new VideoThumbnailService(sp.GetRequiredService<ThumbnailStorageOptions>()));
services.AddScoped<IThumbnailStreamService, ThumbnailStreamService>();
services.AddScoped<IFileLibraryService, FileLibraryService>();
services.AddScoped<IFileLibraryEndpointService, FileLibraryEndpointService>();
services.AddScoped<IFileStreamService, FileStreamService>();
services.AddScoped<IQrCodeService, QrCodeService>();
services.Configure<FileLibraryScanOptions>(configuration.GetSection(nameof(FileLibraryScanOptions)));
services.AddHostedService<FileLibraryScanHostedService>();
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(AppContext.BaseDirectory, "data-protection-keys")));
@@ -0,0 +1,78 @@
using FileShare_Services.Services.FileLibrary;
namespace FileShare_API.Extensions
{
/// <summary>
/// API-only raw file stream endpoints used by browser media elements.
/// </summary>
public static class FileStreamEndpointExtensions
{
/// <summary>
/// Map the media URL emitted by <see cref="FileRecordDto"/>.
/// </summary>
public static IEndpointRouteBuilder MapFileStreamEndpoints(this IEndpointRouteBuilder app)
{
app.MapMethods(
"/api/files/{id:int}/stream",
["GET", "HEAD"],
async (int id, IFileStreamService fileStreamService, HttpContext httpContext) =>
{
var fileResponse = await fileStreamService.GetFileStreamAsync(id);
if (fileResponse is null)
{
return Results.NotFound();
}
var stream = System.IO.File.Open(
fileResponse.FilePath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite);
httpContext.Response.Headers.ContentDisposition =
$"inline; filename=\"{Uri.EscapeDataString(fileResponse.FileName)}\"";
httpContext.Response.Headers.AcceptRanges = "bytes";
httpContext.Response.Headers.CacheControl = "public, max-age=3600";
return Results.File(
stream,
contentType: fileResponse.ContentType,
lastModified: fileResponse.LastModified,
enableRangeProcessing: true);
})
.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;
}
}
}
@@ -1,14 +1,14 @@
using Avalonia_Services.Core;
using FileShare_Services.Core;
using Microsoft.AspNetCore.Authorization;
using AspNetCoreFilterContext = Microsoft.AspNetCore.Http.EndpointFilterInvocationContext;
using AspNetCoreFilterDelegate = Microsoft.AspNetCore.Http.EndpointFilterDelegate;
// 解决与 ASP.NET Core 同名类型的冲突
using UnifiedFilter = Avalonia_Services.Core.IEndpointFilter;
using UnifiedFilter = FileShare_Services.Core.IEndpointFilter;
namespace Avalonia_API.Extensions
namespace FileShare_API.Extensions
{
/// <summary>
/// 将 Avalonia-Services 的统一端点映射到 ASP.NET Core Minimal API。
/// 将 FileShare-Services 的统一端点映射到 ASP.NET Core Minimal API。
/// 支持鉴权、过滤器、中间件的完整 ASP.NET Core 管道。
/// </summary>
public static class UnifiedEndpointExtensions
@@ -81,7 +81,8 @@ namespace Avalonia_API.Extensions
routeHandlerBuilder.WithSummary(endpoint.OpenApiSummary);
}
if (endpoint.OpenApiRequestType is not null)
if (endpoint.OpenApiRequestType is not null
&& endpoint.HttpMethod is "POST" or "PUT")
{
routeHandlerBuilder.Accepts(endpoint.OpenApiRequestType, "application/json");
}
@@ -146,6 +147,23 @@ namespace Avalonia_API.Extensions
httpContext.Response.Headers[kvp.Key] = kvp.Value;
}
if (result is FileStreamResponse fileResponse)
{
if (!System.IO.File.Exists(fileResponse.FilePath))
return Results.NotFound();
var stream = System.IO.File.Open(fileResponse.FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
httpContext.Response.Headers.ContentDisposition = $"inline; filename=\"{Uri.EscapeDataString(fileResponse.FileName)}\"";
httpContext.Response.Headers.AcceptRanges = "bytes";
httpContext.Response.Headers.CacheControl = "public, max-age=3600";
return Results.File(
stream,
contentType: fileResponse.ContentType,
lastModified: fileResponse.LastModified,
enableRangeProcessing: true);
}
return result is not null ? Results.Json(result) : Results.Ok();
};
}
@@ -175,6 +193,14 @@ namespace Avalonia_API.Extensions
ctx.Query[query.Key] = query.Value.ToString();
}
foreach (var routeValue in httpContext.Request.RouteValues)
{
if (routeValue.Value is not null)
{
ctx.RouteValues[routeValue.Key] = routeValue.Value.ToString() ?? string.Empty;
}
}
if (httpContext.Request.ContentLength > 0)
{
using var reader = new StreamReader(httpContext.Request.Body);
+49
View File
@@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>FileShare_API</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="8.1.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FileShare-Services\FileShare-Services.csproj" />
<ProjectReference Include="..\FileShare-Common\FileShare-Common.csproj" />
<ProjectReference Include="..\FileShare-EFCore\FileShare-EFCore.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers\" />
<Content Include="..\tools\ffmpeg\bin\ffmpeg.exe" Link="tools\ffmpeg\bin\ffmpeg.exe" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<Content Include="..\tools\ffmpeg\bin\ffprobe.exe" Link="tools\ffmpeg\bin\ffprobe.exe" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
<Target Name="RestoreFrontendPackages" BeforeTargets="Build" Condition="'$(SkipFrontendBuild)' != 'true' And Exists('..\FileShare-Web-VUE\package.json') And !Exists('..\FileShare-Web-VUE\node_modules')">
<Message Importance="high" Text="Restoring FileShare-Web-VUE npm packages..." />
<Exec WorkingDirectory="..\FileShare-Web-VUE" Command="npm.cmd install" />
</Target>
<Target Name="BuildFrontend" BeforeTargets="Build" DependsOnTargets="RestoreFrontendPackages" Condition="'$(SkipFrontendBuild)' != 'true' And Exists('..\FileShare-Web-VUE\package.json')">
<Message Importance="high" Text="Building FileShare-Web-VUE into FileShare-API/wwwroot..." />
<Exec WorkingDirectory="..\FileShare-Web-VUE" Command="npm.cmd run build-only" />
<RemoveDir Directories="wwwroot" />
<MakeDir Directories="wwwroot" />
<ItemGroup>
<FrontendDist Include="..\FileShare-Web-VUE\dist\**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(FrontendDist)" DestinationFiles="@(FrontendDist->'wwwroot\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="false" />
</Target>
</Project>
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ActiveDebugProfile>http</ActiveDebugProfile>
<NameOfLastUsedPublishProfile>D:\Project\FileShare\FileShare-API\Properties\PublishProfiles\FolderProfile.pubxml</NameOfLastUsedPublishProfile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup>
<ActiveDebugProfile>Avalonia-PC</ActiveDebugProfile>
</PropertyGroup>
</Project>
+6
View File
@@ -0,0 +1,6 @@
@FileShare_API_HostAddress = http://localhost:5206
GET {{FileShare_API_HostAddress}}/weatherforecast/
Accept: application/json
###
@@ -1,20 +1,22 @@
using Avalonia_API.Configuration;
using Avalonia_API.Extensions;
using Avalonia_Common.Infrastructure;
using Avalonia_EFCore.Database;
using Avalonia_Services.Core;
using FileShare_API.Configuration;
using FileShare_API.Extensions;
using FileShare_Common.Infrastructure;
using FileShare_EFCore.Database;
using FileShare_Services.Core;
using Serilog;
// 初始化日志系统
Log.Logger = LoggingConfiguration.CreateDefaultLogger(logDir: "logs");
Log.Information("Avalonia-API 正在启动...");
Log.Information("FileShare-API 正在启动...");
try
{
var builder = WebApplication.CreateBuilder(args);
// 配置 Kestrel 监听所有本机 IP
builder.WebHost.UseUrls("http://0.0.0.0:5206", "https://0.0.0.0:7165");
// 配置 Kestrel 监听所有本机 IP(端口从 Server 配置节读取)
var httpPort = builder.Configuration.GetValue<int>("Server:HttpPort", 5206);
var httpsPort = builder.Configuration.GetValue<int>("Server:HttpsPort", 7165);
builder.WebHost.UseUrls($"http://0.0.0.0:{httpPort}", $"https://0.0.0.0:{httpsPort}");
// 使用 Serilog 作为日志提供程序
builder.Host.UseSerilog();
@@ -30,7 +32,7 @@ try
.AllowAnyMethod());
});
// 注册统一端点及业务服务(入口在 Avalonia-Services/Endpoints/AppEndpoints.cs
// 注册统一端点及业务服务(入口在 FileShare-Services/Endpoints/AppEndpoints.cs
builder.Services.AddUnifiedApiServices(builder.Configuration);
var app = builder.Build();
@@ -46,7 +48,7 @@ try
app.MapOpenApi();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/openapi/v1.json", "Avalonia API v1");
options.SwaggerEndpoint("/openapi/v1.json", "FileShare API v1");
options.RoutePrefix = "swagger";
});
}
@@ -67,7 +69,7 @@ try
}
catch (Exception ex)
{
Log.Fatal(ex, "Avalonia-API 启动失败");
Log.Fatal(ex, "FileShare-API 启动失败");
}
finally
{
@@ -0,0 +1,62 @@
using FileShare_Services.Services.FileLibrary;
using Microsoft.Extensions.Options;
namespace FileShare_API.Services
{
/// <summary>
/// 文件库定时扫描后台服务,按配置间隔检查是否有需要扫描的文件库目录。
/// </summary>
public sealed class FileLibraryScanHostedService(
IServiceScopeFactory scopeFactory,
IOptions<FileLibraryScanOptions> options,
ILogger<FileLibraryScanHostedService> logger)
: BackgroundService
{
/// <summary>后台服务检查到期扫描任务的轮询间隔。</summary>
private readonly TimeSpan _interval = TimeSpan.FromMinutes(Math.Max(1, options.Value.PollingIntervalMinutes));
/// <summary>
/// 启动扫描循环,首次立即执行,之后按配置的轮询间隔重复执行。
/// </summary>
/// <param name="stoppingToken">应用关闭时触发的取消令牌。</param>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("文件库定时扫描服务已启动,轮询间隔 {IntervalMinutes} 分钟。", _interval.TotalMinutes);
await ScanAsync(stoppingToken);
using var timer = new PeriodicTimer(_interval);
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await ScanAsync(stoppingToken);
}
}
/// <summary>
/// 创建临时作用域并调用 <see cref="IFileLibraryService.ScanDueRootsAsync"/> 执行一次到期扫描。
/// </summary>
/// <param name="cancellationToken">取消令牌。</param>
private async Task ScanAsync(CancellationToken cancellationToken)
{
var startedAt = DateTime.UtcNow;
logger.LogInformation("文件库定时扫描轮询开始。");
try
{
await using var scope = scopeFactory.CreateAsyncScope();
var scanner = scope.ServiceProvider.GetRequiredService<IFileLibraryService>();
await scanner.ScanDueRootsAsync(cancellationToken);
logger.LogInformation(
"文件库定时扫描轮询完成,耗时 {ElapsedMilliseconds} ms。",
(DateTime.UtcNow - startedAt).TotalMilliseconds);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
logger.LogInformation("文件库定时扫描轮询已取消。");
}
catch (Exception ex)
{
logger.LogWarning(ex, "文件库定时扫描失败。");
}
}
}
}
@@ -0,0 +1,13 @@
namespace FileShare_API.Services
{
/// <summary>
/// 文件库定时扫描后台服务配置。
/// </summary>
public sealed class FileLibraryScanOptions
{
/// <summary>
/// 后台服务检查到期文件库目录的轮询间隔分钟数。
/// </summary>
public int PollingIntervalMinutes { get; set; } = 1;
}
}
@@ -6,9 +6,13 @@
}
},
"AllowedHosts": "*",
"Server": {
"HttpPort": 5206,
"HttpsPort": 7165
},
"Jwt": {
"Issuer": "Avalonia-API",
"Audience": "Avalonia-Client",
"Issuer": "FileShare-API",
"Audience": "FileShare-Client",
"SigningKey": "change-this-development-signing-key-at-least-32-bytes",
"AccessTokenMinutes": 60,
"RefreshTokenDays": 30
@@ -20,5 +24,13 @@
"RecreateDatabase": false,
"EnableDetailedLog": false,
"Timeout": 30
},
"ThumbnailStorageOptions": {
"RootPath": "thumbnails",
"FfmpegPath": "tools/ffmpeg/bin/ffmpeg.exe",
"FfprobePath": "tools/ffmpeg/bin/ffprobe.exe"
},
"FileLibraryScanOptions": {
"PollingIntervalMinutes": 5
}
}
@@ -1,13 +1,25 @@
using System.Text.Json.Serialization;
namespace Avalonia_Common.Core
namespace FileShare_Common.Core
{
/// <summary>
/// 统一端点响应契约。
/// </summary>
public interface IApiResponse
{
/// <summary>是否成功。</summary>
bool Success { get; }
/// <summary>业务状态码。</summary>
int Code { get; }
}
/// <summary>
/// 统一 API 返回格式。
/// 所有接口的返回都包装为此格式,确保前端收到一致的数据结构。
/// </summary>
/// <typeparam name="T">业务数据类型</typeparam>
public class ApiResponse<T>
public class ApiResponse<T> : IApiResponse
{
/// <summary>是否成功</summary>
[JsonPropertyName("success")]
@@ -113,7 +125,7 @@ namespace Avalonia_Common.Core
/// <summary>
/// 分页返回格式
/// </summary>
public class PagedResponse<T>
public class PagedResponse<T> : IApiResponse
{
/// <summary>
/// 获取或设置操作是否成功。
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Avalonia_Common</RootNamespace>
<RootNamespace>FileShare_Common</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -1,10 +1,10 @@
using Serilog;
using Serilog.Events;
namespace Avalonia_Common.Infrastructure
namespace FileShare_Common.Infrastructure
{
/// <summary>
/// Serilog 日志配置 —— 可在 Avalonia-API 和 Avalonia-PC 中共享。
/// Serilog 日志配置 —— 可在 FileShare-API 和 FileShare-PC 中共享。
/// </summary>
public static class LoggingConfiguration
{
@@ -1,12 +1,12 @@
using Avalonia_EFCore.Models;
using FileShare_EFCore.Models;
using Microsoft.EntityFrameworkCore;
namespace Avalonia_EFCore.Database
namespace FileShare_EFCore.Database
{
/// <summary>
/// 应用数据库上下文 —— 继承自 Avalonia-EFCore 的 AppDbContext。
/// 应用数据库上下文 —— 继承自 FileShare-EFCore 的 AppDbContext。
/// 所有业务实体在此注册 DbSet。
/// 这是 Avalonia-API 和 Avalonia-PC 共用的具体数据上下文。
/// 这是 FileShare-API 和 FileShare-PC 共用的具体数据上下文。
/// </summary>
public class AppDataContext(DatabaseConfiguration dbConfig) : AppDbContext(dbConfig)
{
@@ -25,6 +25,9 @@ namespace Avalonia_EFCore.Database
/// <summary>文件库文件记录数据</summary>
public DbSet<ManagedFileRecord> ManagedFileRecords => Set<ManagedFileRecord>();
/// <summary>文件缩略图映射数据</summary>
public DbSet<ManagedThumbnailMap> ManagedThumbnailMaps => Set<ManagedThumbnailMap>();
/// <summary>
/// 配置实体映射,包括主键、索引和属性约束。
/// </summary>
@@ -66,8 +69,11 @@ namespace Avalonia_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.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);
@@ -78,6 +84,23 @@ namespace Avalonia_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<ManagedThumbnailMap>(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);
});
}
}
@@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore.Design;
namespace Avalonia_EFCore.Database
namespace FileShare_EFCore.Database
{
/// <summary>
/// 设计时 DbContext 工厂,用于 EF Core 迁移工具生成迁移代码。
@@ -58,8 +58,18 @@ namespace Avalonia_EFCore.Database
=> new(DesignTimeDatabaseConfiguration.Create(args, DatabaseProvider.MySQL));
}
/// <summary>
/// 为设计时工具(dotnet ef migrations)提供数据库连接配置。
/// </summary>
internal static class DesignTimeDatabaseConfiguration
{
/// <summary>
/// 解析命令行参数中的 --provider 选项,创建对应的 <see cref="DatabaseConfiguration"/>。
/// 未指定提供程序时默认使用 SQLite。
/// </summary>
/// <param name="args">命令行参数。</param>
/// <param name="defaultProvider">未指定时的默认数据库提供程序。</param>
/// <returns>对应数据库提供程序的配置。</returns>
public static DatabaseConfiguration Create(string[] args, DatabaseProvider defaultProvider = DatabaseProvider.SQLite)
{
DatabaseProviderRegistry.RegisterDefaults();
@@ -67,14 +77,20 @@ namespace Avalonia_EFCore.Database
var provider = GetProvider(args) ?? defaultProvider;
return provider switch
{
DatabaseProvider.SQLite => DatabaseConfiguration.ForSQLite("avalonia-api.db"),
DatabaseProvider.SqlServer => DatabaseConfiguration.ForSqlServer("(localdb)\\MSSQLLocalDB", "AvaloniaApi"),
DatabaseProvider.PostgreSQL => DatabaseConfiguration.ForPostgreSQL("localhost", "avalonia_api", "postgres", "postgres"),
DatabaseProvider.MySQL => DatabaseConfiguration.ForMySQL("localhost", "avalonia_api", "root", "root"),
_ => DatabaseConfiguration.ForSQLite("avalonia-api.db"),
DatabaseProvider.SQLite => DatabaseConfiguration.ForSQLite("fileshare-api.db"),
DatabaseProvider.SqlServer => DatabaseConfiguration.ForSqlServer("(localdb)\\MSSQLLocalDB", "FileShareApi"),
DatabaseProvider.PostgreSQL => DatabaseConfiguration.ForPostgreSQL("localhost", "fileshare_api", "postgres", "postgres"),
DatabaseProvider.MySQL => DatabaseConfiguration.ForMySQL("localhost", "fileshare_api", "root", "root"),
_ => DatabaseConfiguration.ForSQLite("fileshare-api.db"),
};
}
/// <summary>
/// 从命令行参数中解析 --provider 选项的值。
/// 支持 <c>--provider sqlite</c> 和 <c>--provider=sqlite</c> 两种格式。
/// </summary>
/// <param name="args">命令行参数。</param>
/// <returns>解析到的数据库提供程序,未指定或无法识别时返回 null。</returns>
private static DatabaseProvider? GetProvider(string[] args)
{
for (var i = 0; i < args.Length; i++)
@@ -4,7 +4,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia_EFCore.Database
namespace FileShare_EFCore.Database
{
/// <summary>
/// 应用数据库上下文基类 —— 自动根据 DatabaseConfiguration 选择数据库提供程序。
@@ -1,4 +1,4 @@
namespace Avalonia_EFCore.Database
namespace FileShare_EFCore.Database
{
/// <summary>
/// 支持的数据库提供程序类型。
@@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Avalonia_EFCore.Database
namespace FileShare_EFCore.Database
{
/// <summary>
/// 数据库服务注册扩展 —— 在 Program.cs 中一行配置数据库。
@@ -40,6 +40,12 @@ namespace Avalonia_EFCore.Database
return services;
}
/// <summary>
/// 根据 <see cref="DatabaseConfiguration.Provider"/> 注册对应具体类型的 <see cref="AppDataContext"/> 实现。
/// </summary>
/// <param name="services">服务集合。</param>
/// <param name="config">数据库配置。</param>
/// <exception cref="NotSupportedException">数据库提供程序未注册时抛出。</exception>
private static void AddProviderAppDataContext(this IServiceCollection services, DatabaseConfiguration config)
{
switch (config.Provider)
@@ -1,4 +1,4 @@
using Avalonia_Common.Infrastructure;
using FileShare_Common.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@@ -9,7 +9,7 @@ using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia_EFCore.Database
namespace FileShare_EFCore.Database
{
/// <summary>
/// 数据库管理器 —— 负责连接测试、自动迁移、种子数据、版本检查。
@@ -95,8 +95,9 @@ namespace Avalonia_EFCore.Database
{
return await _context.Database.CanConnectAsync();
}
catch
catch (Exception ex)
{
AppLog.Warning("数据库连接测试失败 Provider={Provider} Error={Error}", _config.Provider, ex.Message);
return false;
}
}
@@ -1,12 +1,12 @@
using Microsoft.EntityFrameworkCore;
namespace Avalonia_EFCore.Database
namespace FileShare_EFCore.Database
{
/// <summary>
/// 数据库提供程序注册表 —— 统一注册所有支持的提供程序配置委托。
/// 具体使用哪个提供程序由各宿主项目决定:
/// Avalonia-API:从 appsettings.json 的 DatabaseConfiguration 节读取;
/// Avalonia-PC :固定使用 SQLite。
/// FileShare-API:从 appsettings.json 的 DatabaseConfiguration 节读取;
/// FileShare-PC :固定使用 SQLite。
/// </summary>
public static class DatabaseProviderRegistry
{
@@ -37,7 +37,7 @@ namespace Avalonia_EFCore.Database
}
/// <summary>
/// 注册所有内置提供程序的默认配置(四个包均已内置在 Avalonia-EFCore 中)。
/// 注册所有内置提供程序的默认配置(四个包均已内置在 FileShare-EFCore 中)。
/// 注册完成后由调用方根据自身需求选择具体的 <see cref="DatabaseProvider"/>。
/// </summary>
public static void RegisterDefaults()
@@ -1,4 +1,4 @@
namespace Avalonia_EFCore.Database
namespace FileShare_EFCore.Database
{
/// <summary>
/// SQLite 专用 DbContext,用于隔离 SQLite 迁移集。
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Avalonia_EFCore</RootNamespace>
<RootNamespace>FileShare_EFCore</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -22,7 +22,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia-Common\Avalonia-Common.csproj" />
<ProjectReference Include="..\FileShare-Common\FileShare-Common.csproj" />
</ItemGroup>
</Project>
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using Avalonia_EFCore.Database;
using FileShare_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Avalonia_EFCore.Migrations.MySQL
namespace FileShare_EFCore.Migrations.MySQL
{
[DbContext(typeof(MySqlAppDataContext))]
[Migration("20260520082626_AutoMigration_20260520162543")]
@@ -22,7 +22,7 @@ namespace Avalonia_EFCore.Migrations.MySQL
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
@@ -82,7 +82,7 @@ namespace Avalonia_EFCore.Migrations.MySQL
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -127,7 +127,7 @@ namespace Avalonia_EFCore.Migrations.MySQL
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1,10 +1,10 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using MySql.EntityFrameworkCore.Metadata;
#nullable disable
namespace Avalonia_EFCore.Migrations.MySQL
namespace FileShare_EFCore.Migrations.MySQL
{
/// <inheritdoc />
public partial class AutoMigration_20260520162543 : Migration
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using Avalonia_EFCore.Database;
using FileShare_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Avalonia_EFCore.Migrations.MySQL
namespace FileShare_EFCore.Migrations.MySQL
{
[DbContext(typeof(MySqlAppDataContext))]
[Migration("20260520083306_AutoMigration_20260520163216")]
@@ -22,7 +22,7 @@ namespace Avalonia_EFCore.Migrations.MySQL
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
@@ -82,7 +82,7 @@ namespace Avalonia_EFCore.Migrations.MySQL
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -133,7 +133,7 @@ namespace Avalonia_EFCore.Migrations.MySQL
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Avalonia_EFCore.Migrations.MySQL
namespace FileShare_EFCore.Migrations.MySQL
{
/// <inheritdoc />
public partial class AutoMigration_20260520163216 : Migration
@@ -0,0 +1,370 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime(6)")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("varchar(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("varchar(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("datetime(6)")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("varchar(2048)")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("tinyint(1)")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("varchar(32)")
.HasColumnName("extension");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("varchar(260)")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("datetime(6)")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("int")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)")
.HasColumnName("media-type");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("bigint")
.HasColumnName("size-bytes");
b.Property<string>("ThumbnailPath")
.HasMaxLength(512)
.HasColumnType("varchar(512)")
.HasColumnName("thumbnail-path");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("tinyint(1)")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("varchar(2000)")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("int")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("用户主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("天气预报主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("int")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -0,0 +1,113 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using MySql.EntityFrameworkCore.Metadata;
#nullable disable
namespace FileShare_EFCore.Migrations.MySQL
{
/// <inheritdoc />
public partial class AutoMigration_20260522162758 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "managed-library-root",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn),
path = table.Column<string>(type: "varchar(1024)", maxLength: 1024, nullable: false),
displayname = table.Column<string>(name: "display-name", type: "varchar(200)", maxLength: 200, nullable: false),
isenabled = table.Column<bool>(name: "is-enabled", type: "tinyint(1)", nullable: false),
isavailable = table.Column<bool>(name: "is-available", type: "tinyint(1)", nullable: false, defaultValue: true),
scanintervalminutes = table.Column<int>(name: "scan-interval-minutes", type: "int", nullable: false),
lastscanstartedat = table.Column<DateTime>(name: "last-scan-started-at", type: "datetime(6)", nullable: true),
lastscancompletedat = table.Column<DateTime>(name: "last-scan-completed-at", type: "datetime(6)", nullable: true),
lastscanerror = table.Column<string>(name: "last-scan-error", type: "varchar(2000)", maxLength: 2000, nullable: true),
createdat = table.Column<DateTime>(name: "created-at", type: "datetime(6)", nullable: false),
updatedat = table.Column<DateTime>(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<int>(type: "int", nullable: false)
.Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn),
libraryrootid = table.Column<int>(name: "library-root-id", type: "int", nullable: false),
filename = table.Column<string>(name: "file-name", type: "varchar(260)", maxLength: 260, nullable: false),
relativepath = table.Column<string>(name: "relative-path", type: "varchar(1024)", maxLength: 1024, nullable: false),
absolutepath = table.Column<string>(name: "absolute-path", type: "varchar(2048)", maxLength: 2048, nullable: false),
extension = table.Column<string>(type: "varchar(32)", maxLength: 32, nullable: false),
sizebytes = table.Column<long>(name: "size-bytes", type: "bigint", nullable: false),
lastwritetimeutc = table.Column<DateTime>(name: "last-write-time-utc", type: "datetime(6)", nullable: false),
mediatype = table.Column<string>(name: "media-type", type: "varchar(20)", maxLength: 20, nullable: false),
contenttype = table.Column<string>(name: "content-type", type: "varchar(100)", maxLength: 100, nullable: false),
exists = table.Column<bool>(type: "tinyint(1)", nullable: false),
lastseenat = table.Column<DateTime>(name: "last-seen-at", type: "datetime(6)", nullable: false),
thumbnailpath = table.Column<string>(name: "thumbnail-path", type: "varchar(512)", maxLength: 512, nullable: true),
videoduration = table.Column<double>(name: "video-duration", type: "double", nullable: true),
lastplayedat = table.Column<DateTime>(name: "last-played-at", type: "datetime(6)", nullable: true),
createdat = table.Column<DateTime>(name: "created-at", type: "datetime(6)", nullable: false),
updatedat = table.Column<DateTime>(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);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "managed-file-record");
migrationBuilder.DropTable(
name: "managed-library-root");
}
}
}
@@ -0,0 +1,444 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime(6)")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("varchar(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("varchar(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("datetime(6)")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("varchar(2048)")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("tinyint(1)")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("varchar(32)")
.HasColumnName("extension");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("varchar(260)")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("datetime(6)")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("int")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)")
.HasColumnName("media-type");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("bigint")
.HasColumnName("size-bytes");
b.Property<int?>("ThumbnailId")
.HasColumnType("int")
.HasColumnName("thumbnail-id");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("tinyint(1)")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("varchar(2000)")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("int")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<int>("LibraryRootId")
.HasColumnType("int")
.HasColumnName("library-root-id");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)")
.HasColumnName("relative-path");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("用户主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("天气预报主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("int")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -0,0 +1,101 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using MySql.EntityFrameworkCore.Metadata;
#nullable disable
namespace FileShare_EFCore.Migrations.MySQL
{
/// <inheritdoc />
public partial class AddThumbnailMap : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "thumbnail-path",
table: "managed-file-record");
migrationBuilder.AddColumn<int>(
name: "thumbnail-id",
table: "managed-file-record",
type: "int",
nullable: true);
migrationBuilder.CreateTable(
name: "managed-thumbnail-map",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn),
libraryrootid = table.Column<int>(name: "library-root-id", type: "int", nullable: false),
relativepath = table.Column<string>(name: "relative-path", type: "varchar(1024)", maxLength: 1024, nullable: false),
contenttype = table.Column<string>(name: "content-type", type: "varchar(100)", maxLength: 100, nullable: false),
createdat = table.Column<DateTime>(name: "created-at", type: "datetime(6)", nullable: false),
updatedat = table.Column<DateTime>(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);
}
/// <inheritdoc />
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<string>(
name: "thumbnail-path",
table: "managed-file-record",
type: "varchar(512)",
maxLength: 512,
nullable: true);
}
}
}
@@ -0,0 +1,448 @@
// <auto-generated />
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("20260522092342_AddPlaybackPosition")]
partial class AddPlaybackPosition
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime(6)")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("varchar(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("varchar(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("datetime(6)")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("varchar(2048)")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("tinyint(1)")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("varchar(32)")
.HasColumnName("extension");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("varchar(260)")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("datetime(6)")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("int")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)")
.HasColumnName("media-type");
b.Property<double?>("PlaybackPosition")
.HasColumnType("double")
.HasColumnName("playback-position");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("bigint")
.HasColumnName("size-bytes");
b.Property<int?>("ThumbnailId")
.HasColumnType("int")
.HasColumnName("thumbnail-id");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("tinyint(1)")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("varchar(2000)")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("int")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<int>("LibraryRootId")
.HasColumnType("int")
.HasColumnName("library-root-id");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)")
.HasColumnName("relative-path");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("用户主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("天气预报主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("int")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FileShare_EFCore.Migrations.MySQL
{
/// <inheritdoc />
public partial class AddPlaybackPosition : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<double>(
name: "playback-position",
table: "managed-file-record",
type: "double",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "playback-position",
table: "managed-file-record");
}
}
}
@@ -0,0 +1,455 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime(6)")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("varchar(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("varchar(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("datetime(6)")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("varchar(2048)")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("tinyint(1)")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("varchar(32)")
.HasColumnName("extension");
b.Property<DateTime?>("FileCreationTimeUtc")
.HasColumnType("datetime(6)")
.HasColumnName("file-creation-time-utc");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("varchar(260)")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("datetime(6)")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("int")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)")
.HasColumnName("media-type");
b.Property<double?>("PlaybackPosition")
.HasColumnType("double")
.HasColumnName("playback-position");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("bigint")
.HasColumnName("size-bytes");
b.Property<int?>("ThumbnailId")
.HasColumnType("int")
.HasColumnName("thumbnail-id");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("tinyint(1)")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("varchar(2000)")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("int")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<int>("LibraryRootId")
.HasColumnType("int")
.HasColumnName("library-root-id");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)")
.HasColumnName("relative-path");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("用户主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("天气预报主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("int")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -0,0 +1,38 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FileShare_EFCore.Migrations.MySQL
{
/// <inheritdoc />
public partial class AutoMigration_20260523103531 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
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");
}
/// <inheritdoc />
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");
}
}
}
@@ -0,0 +1,452 @@
// <auto-generated />
using System;
using FileShare_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FileShare_EFCore.Migrations.MySQL
{
[DbContext(typeof(MySqlAppDataContext))]
partial class MySqlAppDataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime(6)")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("varchar(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("varchar(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("datetime(6)")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("varchar(2048)")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("tinyint(1)")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("varchar(32)")
.HasColumnName("extension");
b.Property<DateTime?>("FileCreationTimeUtc")
.HasColumnType("datetime(6)")
.HasColumnName("file-creation-time-utc");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("varchar(260)")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("datetime(6)")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("int")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("varchar(20)")
.HasColumnName("media-type");
b.Property<double?>("PlaybackPosition")
.HasColumnType("double")
.HasColumnName("playback-position");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("bigint")
.HasColumnName("size-bytes");
b.Property<int?>("ThumbnailId")
.HasColumnType("int")
.HasColumnName("thumbnail-id");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("tinyint(1)")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("varchar(2000)")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("datetime(6)")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("int")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at");
b.Property<int>("LibraryRootId")
.HasColumnType("int")
.HasColumnName("library-root-id");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("varchar(1024)")
.HasColumnName("relative-path");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("用户主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("varchar(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("varchar(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("天气预报主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("int")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using Avalonia_EFCore.Database;
using FileShare_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@@ -9,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Avalonia_EFCore.Migrations.PostgreSQL
namespace FileShare_EFCore.Migrations.PostgreSQL
{
[DbContext(typeof(PostgreSqlAppDataContext))]
[Migration("20260520082617_AutoMigration_20260520162543")]
@@ -25,7 +25,7 @@ namespace Avalonia_EFCore.Migrations.PostgreSQL
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
@@ -87,7 +87,7 @@ namespace Avalonia_EFCore.Migrations.PostgreSQL
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -134,7 +134,7 @@ namespace Avalonia_EFCore.Migrations.PostgreSQL
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1,10 +1,10 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Avalonia_EFCore.Migrations.PostgreSQL
namespace FileShare_EFCore.Migrations.PostgreSQL
{
/// <inheritdoc />
public partial class AutoMigration_20260520162543 : Migration
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using Avalonia_EFCore.Database;
using FileShare_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@@ -9,7 +9,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Avalonia_EFCore.Migrations.PostgreSQL
namespace FileShare_EFCore.Migrations.PostgreSQL
{
[DbContext(typeof(PostgreSqlAppDataContext))]
[Migration("20260520083254_AutoMigration_20260520163216")]
@@ -25,7 +25,7 @@ namespace Avalonia_EFCore.Migrations.PostgreSQL
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
@@ -87,7 +87,7 @@ namespace Avalonia_EFCore.Migrations.PostgreSQL
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -140,7 +140,7 @@ namespace Avalonia_EFCore.Migrations.PostgreSQL
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Avalonia_EFCore.Migrations.PostgreSQL
namespace FileShare_EFCore.Migrations.PostgreSQL
{
/// <inheritdoc />
public partial class AutoMigration_20260520163216 : Migration
@@ -0,0 +1,383 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("boolean")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("extension");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("character varying(260)")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("integer")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("media-type");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("bigint")
.HasColumnName("size-bytes");
b.Property<string>("ThumbnailPath")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("thumbnail-path");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("integer")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasComment("用户主键");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasComment("天气预报主键");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("integer")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -0,0 +1,111 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FileShare_EFCore.Migrations.PostgreSQL
{
/// <inheritdoc />
public partial class AutoMigration_20260522162758 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "managed-library-root",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
path = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
displayname = table.Column<string>(name: "display-name", type: "character varying(200)", maxLength: 200, nullable: false),
isenabled = table.Column<bool>(name: "is-enabled", type: "boolean", nullable: false),
isavailable = table.Column<bool>(name: "is-available", type: "boolean", nullable: false, defaultValue: true),
scanintervalminutes = table.Column<int>(name: "scan-interval-minutes", type: "integer", nullable: false),
lastscanstartedat = table.Column<DateTime>(name: "last-scan-started-at", type: "timestamp with time zone", nullable: true),
lastscancompletedat = table.Column<DateTime>(name: "last-scan-completed-at", type: "timestamp with time zone", nullable: true),
lastscanerror = table.Column<string>(name: "last-scan-error", type: "character varying(2000)", maxLength: 2000, nullable: true),
createdat = table.Column<DateTime>(name: "created-at", type: "timestamp with time zone", nullable: false),
updatedat = table.Column<DateTime>(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<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
libraryrootid = table.Column<int>(name: "library-root-id", type: "integer", nullable: false),
filename = table.Column<string>(name: "file-name", type: "character varying(260)", maxLength: 260, nullable: false),
relativepath = table.Column<string>(name: "relative-path", type: "character varying(1024)", maxLength: 1024, nullable: false),
absolutepath = table.Column<string>(name: "absolute-path", type: "character varying(2048)", maxLength: 2048, nullable: false),
extension = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
sizebytes = table.Column<long>(name: "size-bytes", type: "bigint", nullable: false),
lastwritetimeutc = table.Column<DateTime>(name: "last-write-time-utc", type: "timestamp with time zone", nullable: false),
mediatype = table.Column<string>(name: "media-type", type: "character varying(20)", maxLength: 20, nullable: false),
contenttype = table.Column<string>(name: "content-type", type: "character varying(100)", maxLength: 100, nullable: false),
exists = table.Column<bool>(type: "boolean", nullable: false),
lastseenat = table.Column<DateTime>(name: "last-seen-at", type: "timestamp with time zone", nullable: false),
thumbnailpath = table.Column<string>(name: "thumbnail-path", type: "character varying(512)", maxLength: 512, nullable: true),
videoduration = table.Column<double>(name: "video-duration", type: "double precision", nullable: true),
lastplayedat = table.Column<DateTime>(name: "last-played-at", type: "timestamp with time zone", nullable: true),
createdat = table.Column<DateTime>(name: "created-at", type: "timestamp with time zone", nullable: false),
updatedat = table.Column<DateTime>(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);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "managed-file-record");
migrationBuilder.DropTable(
name: "managed-library-root");
}
}
}
@@ -0,0 +1,459 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("boolean")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("extension");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("character varying(260)")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("integer")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("media-type");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("bigint")
.HasColumnName("size-bytes");
b.Property<int?>("ThumbnailId")
.HasColumnType("integer")
.HasColumnName("thumbnail-id");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("integer")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<int>("LibraryRootId")
.HasColumnType("integer")
.HasColumnName("library-root-id");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("relative-path");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasComment("用户主键");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasComment("天气预报主键");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("integer")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -0,0 +1,100 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FileShare_EFCore.Migrations.PostgreSQL
{
/// <inheritdoc />
public partial class AddThumbnailMap : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "thumbnail-path",
table: "managed-file-record");
migrationBuilder.AddColumn<int>(
name: "thumbnail-id",
table: "managed-file-record",
type: "integer",
nullable: true);
migrationBuilder.CreateTable(
name: "managed-thumbnail-map",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
libraryrootid = table.Column<int>(name: "library-root-id", type: "integer", nullable: false),
relativepath = table.Column<string>(name: "relative-path", type: "character varying(1024)", maxLength: 1024, nullable: false),
contenttype = table.Column<string>(name: "content-type", type: "character varying(100)", maxLength: 100, nullable: false),
createdat = table.Column<DateTime>(name: "created-at", type: "timestamp with time zone", nullable: false),
updatedat = table.Column<DateTime>(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);
}
/// <inheritdoc />
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<string>(
name: "thumbnail-path",
table: "managed-file-record",
type: "character varying(512)",
maxLength: 512,
nullable: true);
}
}
}
@@ -0,0 +1,463 @@
// <auto-generated />
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("20260522092330_AddPlaybackPosition")]
partial class AddPlaybackPosition
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("boolean")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("extension");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("character varying(260)")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("integer")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("media-type");
b.Property<double?>("PlaybackPosition")
.HasColumnType("double precision")
.HasColumnName("playback-position");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("bigint")
.HasColumnName("size-bytes");
b.Property<int?>("ThumbnailId")
.HasColumnType("integer")
.HasColumnName("thumbnail-id");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("integer")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<int>("LibraryRootId")
.HasColumnType("integer")
.HasColumnName("library-root-id");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("relative-path");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasComment("用户主键");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasComment("天气预报主键");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("integer")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FileShare_EFCore.Migrations.PostgreSQL
{
/// <inheritdoc />
public partial class AddPlaybackPosition : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<double>(
name: "playback-position",
table: "managed-file-record",
type: "double precision",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "playback-position",
table: "managed-file-record");
}
}
}
@@ -0,0 +1,470 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("boolean")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("extension");
b.Property<DateTime?>("FileCreationTimeUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("file-creation-time-utc");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("character varying(260)")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("integer")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("media-type");
b.Property<double?>("PlaybackPosition")
.HasColumnType("double precision")
.HasColumnName("playback-position");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("bigint")
.HasColumnName("size-bytes");
b.Property<int?>("ThumbnailId")
.HasColumnType("integer")
.HasColumnName("thumbnail-id");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("integer")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<int>("LibraryRootId")
.HasColumnType("integer")
.HasColumnName("library-root-id");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("relative-path");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasComment("用户主键");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasComment("天气预报主键");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("integer")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -0,0 +1,38 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FileShare_EFCore.Migrations.PostgreSQL
{
/// <inheritdoc />
public partial class AutoMigration_20260523103531 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
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");
}
/// <inheritdoc />
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");
}
}
}
@@ -0,0 +1,467 @@
// <auto-generated />
using System;
using FileShare_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FileShare_EFCore.Migrations.PostgreSQL
{
[DbContext(typeof(PostgreSqlAppDataContext))]
partial class PostgreSqlAppDataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("boolean")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("extension");
b.Property<DateTime?>("FileCreationTimeUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("file-creation-time-utc");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("character varying(260)")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("integer")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("media-type");
b.Property<double?>("PlaybackPosition")
.HasColumnType("double precision")
.HasColumnName("playback-position");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("bigint")
.HasColumnName("size-bytes");
b.Property<int?>("ThumbnailId")
.HasColumnType("integer")
.HasColumnName("thumbnail-id");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("integer")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at");
b.Property<int>("LibraryRootId")
.HasColumnType("integer")
.HasColumnName("library-root-id");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasColumnName("relative-path");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasComment("用户主键");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasComment("天气预报主键");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("integer")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -1,12 +1,12 @@
using System;
using Avalonia_EFCore.Database;
using FileShare_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Avalonia_EFCore.Migrations.SQLite
namespace FileShare_EFCore.Migrations.SQLite
{
[DbContext(typeof(SqliteAppDataContext))]
[Migration("20260514000100_InitialCreate")]
@@ -18,7 +18,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0");
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
{
b.Property<int>("Id")
.HasComment("用户主键")
@@ -52,7 +52,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
{
b.Property<int>("Id")
.HasComment("天气预报主键")
@@ -4,7 +4,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Avalonia_EFCore.Migrations.SQLite
namespace FileShare_EFCore.Migrations.SQLite
{
/// <summary>
/// 初始数据库基线。后续软件版本只追加新的 Migration,不修改已发布 Migration。
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using Avalonia_EFCore.Database;
using FileShare_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Avalonia_EFCore.Migrations.SQLite
namespace FileShare_EFCore.Migrations.SQLite
{
[DbContext(typeof(SqliteAppDataContext))]
[Migration("20260515072045_AutoMigration_20260515152037")]
@@ -20,7 +20,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -65,7 +65,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Avalonia_EFCore.Migrations.SQLite
namespace FileShare_EFCore.Migrations.SQLite
{
/// <inheritdoc />
public partial class AutoMigration_20260515152037 : Migration
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using Avalonia_EFCore.Database;
using FileShare_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Avalonia_EFCore.Migrations.SQLite
namespace FileShare_EFCore.Migrations.SQLite
{
[DbContext(typeof(SqliteAppDataContext))]
[Migration("20260515085847_AutoMigration_20260515165835")]
@@ -20,7 +20,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
@@ -80,7 +80,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -125,7 +125,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1,9 +1,9 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Avalonia_EFCore.Migrations.SQLite
namespace FileShare_EFCore.Migrations.SQLite
{
/// <inheritdoc />
public partial class AutoMigration_20260515165835 : Migration
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using Avalonia_EFCore.Database;
using FileShare_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Avalonia_EFCore.Migrations.SQLite
namespace FileShare_EFCore.Migrations.SQLite
{
[DbContext(typeof(SqliteAppDataContext))]
[Migration("20260520083230_AutoMigration_20260520163216")]
@@ -20,7 +20,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
@@ -80,7 +80,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -131,7 +131,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Avalonia_EFCore.Migrations.SQLite
namespace FileShare_EFCore.Migrations.SQLite
{
/// <inheritdoc />
public partial class AutoMigration_20260520163216 : Migration
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using Avalonia_EFCore.Database;
using FileShare_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Avalonia_EFCore.Migrations.SQLite
namespace FileShare_EFCore.Migrations.SQLite
{
[DbContext(typeof(SqliteAppDataContext))]
[Migration("20260521080213_AddFileLibrary")]
@@ -20,7 +20,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
@@ -80,7 +80,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.ManagedFileRecord", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -170,7 +170,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.ManagedLibraryRoot", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -237,7 +237,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -288,7 +288,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -331,9 +331,9 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.ManagedFileRecord", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
{
b.HasOne("Avalonia_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
.WithMany("Files")
.HasForeignKey("LibraryRootId")
.OnDelete(DeleteBehavior.Cascade)
@@ -342,7 +342,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
b.Navigation("LibraryRoot");
});
modelBuilder.Entity("Avalonia_EFCore.Models.ManagedLibraryRoot", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
{
b.Navigation("Files");
});
@@ -1,9 +1,9 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Avalonia_EFCore.Migrations.SQLite
namespace FileShare_EFCore.Migrations.SQLite
{
/// <inheritdoc />
public partial class AddFileLibrary : Migration
@@ -1,23 +1,26 @@
// <auto-generated />
using System;
using Avalonia_EFCore.Database;
using FileShare_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Avalonia_EFCore.Migrations.SQLite
namespace FileShare_EFCore.Migrations.SQLite
{
[DbContext(typeof(SqliteAppDataContext))]
partial class SqliteAppDataContextModelSnapshot : ModelSnapshot
[Migration("20260522082814_AutoMigration_20260522162758")]
partial class AutoMigration_20260522162758
{
protected override void BuildModel(ModelBuilder modelBuilder)
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
@@ -77,7 +80,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.ManagedFileRecord", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -116,6 +119,10 @@ namespace Avalonia_EFCore.Migrations.SQLite
.HasColumnType("TEXT")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("TEXT")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("TEXT")
.HasColumnName("last-seen-at");
@@ -144,10 +151,19 @@ namespace Avalonia_EFCore.Migrations.SQLite
.HasColumnType("INTEGER")
.HasColumnName("size-bytes");
b.Property<string>("ThumbnailPath")
.HasMaxLength(512)
.HasColumnType("TEXT")
.HasColumnName("thumbnail-path");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasColumnName("updated-at");
b.Property<double?>("VideoDuration")
.HasColumnType("REAL")
.HasColumnName("video-duration");
b.HasKey("Id")
.HasName("pk-managed-file-record");
@@ -155,6 +171,9 @@ namespace Avalonia_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");
@@ -167,7 +186,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.ManagedLibraryRoot", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -234,7 +253,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -285,7 +304,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -328,9 +347,9 @@ namespace Avalonia_EFCore.Migrations.SQLite
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.ManagedFileRecord", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
{
b.HasOne("Avalonia_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
.WithMany("Files")
.HasForeignKey("LibraryRootId")
.OnDelete(DeleteBehavior.Cascade)
@@ -339,7 +358,7 @@ namespace Avalonia_EFCore.Migrations.SQLite
b.Navigation("LibraryRoot");
});
modelBuilder.Entity("Avalonia_EFCore.Models.ManagedLibraryRoot", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
{
b.Navigation("Files");
});
@@ -0,0 +1,59 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FileShare_EFCore.Migrations.SQLite
{
/// <inheritdoc />
public partial class AutoMigration_20260522162758 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "last-played-at",
table: "managed-file-record",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "thumbnail-path",
table: "managed-file-record",
type: "TEXT",
maxLength: 512,
nullable: true);
migrationBuilder.AddColumn<double>(
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");
}
/// <inheritdoc />
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");
}
}
}
@@ -0,0 +1,442 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("TEXT")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("TEXT")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("TEXT")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("TEXT")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("INTEGER")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT")
.HasColumnName("extension");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("TEXT")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("TEXT")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("TEXT")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("TEXT")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("INTEGER")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasColumnName("media-type");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("INTEGER")
.HasColumnName("size-bytes");
b.Property<int?>("ThumbnailId")
.HasColumnType("INTEGER")
.HasColumnName("thumbnail-id");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("TEXT")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("TEXT")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("TEXT")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("INTEGER")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<int>("LibraryRootId")
.HasColumnType("INTEGER")
.HasColumnName("library-root-id");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasColumnName("relative-path");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id")
.HasComment("用户主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id")
.HasComment("天气预报主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("TEXT")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("INTEGER")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -0,0 +1,99 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FileShare_EFCore.Migrations.SQLite
{
/// <inheritdoc />
public partial class AddThumbnailMap : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "thumbnail-path",
table: "managed-file-record");
migrationBuilder.AddColumn<int>(
name: "thumbnail-id",
table: "managed-file-record",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateTable(
name: "managed-thumbnail-map",
columns: table => new
{
id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
libraryrootid = table.Column<int>(name: "library-root-id", type: "INTEGER", nullable: false),
relativepath = table.Column<string>(name: "relative-path", type: "TEXT", maxLength: 1024, nullable: false),
contenttype = table.Column<string>(name: "content-type", type: "TEXT", maxLength: 100, nullable: false),
createdat = table.Column<DateTime>(name: "created-at", type: "TEXT", nullable: false),
updatedat = table.Column<DateTime>(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);
}
/// <inheritdoc />
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<string>(
name: "thumbnail-path",
table: "managed-file-record",
type: "TEXT",
maxLength: 512,
nullable: true);
}
}
}
@@ -0,0 +1,446 @@
// <auto-generated />
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("20260522092304_AddPlaybackPosition")]
partial class AddPlaybackPosition
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("TEXT")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("TEXT")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("TEXT")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("TEXT")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("INTEGER")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT")
.HasColumnName("extension");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("TEXT")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("TEXT")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("TEXT")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("TEXT")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("INTEGER")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasColumnName("media-type");
b.Property<double?>("PlaybackPosition")
.HasColumnType("REAL")
.HasColumnName("playback-position");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("INTEGER")
.HasColumnName("size-bytes");
b.Property<int?>("ThumbnailId")
.HasColumnType("INTEGER")
.HasColumnName("thumbnail-id");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("TEXT")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("TEXT")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("TEXT")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("INTEGER")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<int>("LibraryRootId")
.HasColumnType("INTEGER")
.HasColumnName("library-root-id");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasColumnName("relative-path");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id")
.HasComment("用户主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id")
.HasComment("天气预报主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("TEXT")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("INTEGER")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FileShare_EFCore.Migrations.SQLite
{
/// <inheritdoc />
public partial class AddPlaybackPosition : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<double>(
name: "playback-position",
table: "managed-file-record",
type: "REAL",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "playback-position",
table: "managed-file-record");
}
}
}
@@ -0,0 +1,453 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("TEXT")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("TEXT")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("TEXT")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("TEXT")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("INTEGER")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT")
.HasColumnName("extension");
b.Property<DateTime?>("FileCreationTimeUtc")
.HasColumnType("TEXT")
.HasColumnName("file-creation-time-utc");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("TEXT")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("TEXT")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("TEXT")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("TEXT")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("INTEGER")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasColumnName("media-type");
b.Property<double?>("PlaybackPosition")
.HasColumnType("REAL")
.HasColumnName("playback-position");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("INTEGER")
.HasColumnName("size-bytes");
b.Property<int?>("ThumbnailId")
.HasColumnType("INTEGER")
.HasColumnName("thumbnail-id");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("TEXT")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("TEXT")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("TEXT")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("INTEGER")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<int>("LibraryRootId")
.HasColumnType("INTEGER")
.HasColumnName("library-root-id");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasColumnName("relative-path");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id")
.HasComment("用户主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id")
.HasComment("天气预报主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("TEXT")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("INTEGER")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -0,0 +1,38 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FileShare_EFCore.Migrations.SQLite
{
/// <inheritdoc />
public partial class AutoMigration_20260523103531 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
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");
}
/// <inheritdoc />
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");
}
}
}
@@ -0,0 +1,450 @@
// <auto-generated />
using System;
using FileShare_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FileShare_EFCore.Migrations.SQLite
{
[DbContext(typeof(SqliteAppDataContext))]
partial class SqliteAppDataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("TEXT")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("TEXT")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("TEXT")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("TEXT")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("INTEGER")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT")
.HasColumnName("extension");
b.Property<DateTime?>("FileCreationTimeUtc")
.HasColumnType("TEXT")
.HasColumnName("file-creation-time-utc");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("TEXT")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("TEXT")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("TEXT")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("TEXT")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("INTEGER")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT")
.HasColumnName("media-type");
b.Property<double?>("PlaybackPosition")
.HasColumnType("REAL")
.HasColumnName("playback-position");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("INTEGER")
.HasColumnName("size-bytes");
b.Property<int?>("ThumbnailId")
.HasColumnType("INTEGER")
.HasColumnName("thumbnail-id");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("TEXT")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("TEXT")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("TEXT")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("INTEGER")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at");
b.Property<int>("LibraryRootId")
.HasColumnType("INTEGER")
.HasColumnName("library-root-id");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasColumnName("relative-path");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id")
.HasComment("用户主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id")
.HasComment("天气预报主键");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("TEXT")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("INTEGER")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using Avalonia_EFCore.Database;
using FileShare_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Avalonia_EFCore.Migrations.SqlServer
namespace FileShare_EFCore.Migrations.SqlServer
{
[DbContext(typeof(SqlServerAppDataContext))]
[Migration("20260520082607_AutoMigration_20260520162543")]
@@ -25,7 +25,7 @@ namespace Avalonia_EFCore.Migrations.SqlServer
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
@@ -87,7 +87,7 @@ namespace Avalonia_EFCore.Migrations.SqlServer
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -134,7 +134,7 @@ namespace Avalonia_EFCore.Migrations.SqlServer
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1,9 +1,9 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Avalonia_EFCore.Migrations.SqlServer
namespace FileShare_EFCore.Migrations.SqlServer
{
/// <inheritdoc />
public partial class AutoMigration_20260520162543 : Migration
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using Avalonia_EFCore.Database;
using FileShare_EFCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Avalonia_EFCore.Migrations.SqlServer
namespace FileShare_EFCore.Migrations.SqlServer
{
[DbContext(typeof(SqlServerAppDataContext))]
[Migration("20260520083242_AutoMigration_20260520163216")]
@@ -25,7 +25,7 @@ namespace Avalonia_EFCore.Migrations.SqlServer
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
@@ -87,7 +87,7 @@ namespace Avalonia_EFCore.Migrations.SqlServer
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -140,7 +140,7 @@ namespace Avalonia_EFCore.Migrations.SqlServer
});
});
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Avalonia_EFCore.Migrations.SqlServer
namespace FileShare_EFCore.Migrations.SqlServer
{
/// <inheritdoc />
public partial class AutoMigration_20260520163216 : Migration
@@ -0,0 +1,383 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime2")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("datetime2")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("bit")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("nvarchar(32)")
.HasColumnName("extension");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("nvarchar(260)")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("datetime2")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("datetime2")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("datetime2")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("int")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)")
.HasColumnName("media-type");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("bigint")
.HasColumnName("size-bytes");
b.Property<string>("ThumbnailPath")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)")
.HasColumnName("thumbnail-path");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("bit")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("datetime2")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("datetime2")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("int")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("用户主键");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("天气预报主键");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("int")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -0,0 +1,110 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FileShare_EFCore.Migrations.SqlServer
{
/// <inheritdoc />
public partial class AutoMigration_20260522162758 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "managed-library-root",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
path = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: false),
displayname = table.Column<string>(name: "display-name", type: "nvarchar(200)", maxLength: 200, nullable: false),
isenabled = table.Column<bool>(name: "is-enabled", type: "bit", nullable: false),
isavailable = table.Column<bool>(name: "is-available", type: "bit", nullable: false, defaultValue: true),
scanintervalminutes = table.Column<int>(name: "scan-interval-minutes", type: "int", nullable: false),
lastscanstartedat = table.Column<DateTime>(name: "last-scan-started-at", type: "datetime2", nullable: true),
lastscancompletedat = table.Column<DateTime>(name: "last-scan-completed-at", type: "datetime2", nullable: true),
lastscanerror = table.Column<string>(name: "last-scan-error", type: "nvarchar(2000)", maxLength: 2000, nullable: true),
createdat = table.Column<DateTime>(name: "created-at", type: "datetime2", nullable: false),
updatedat = table.Column<DateTime>(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<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
libraryrootid = table.Column<int>(name: "library-root-id", type: "int", nullable: false),
filename = table.Column<string>(name: "file-name", type: "nvarchar(260)", maxLength: 260, nullable: false),
relativepath = table.Column<string>(name: "relative-path", type: "nvarchar(1024)", maxLength: 1024, nullable: false),
absolutepath = table.Column<string>(name: "absolute-path", type: "nvarchar(2048)", maxLength: 2048, nullable: false),
extension = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
sizebytes = table.Column<long>(name: "size-bytes", type: "bigint", nullable: false),
lastwritetimeutc = table.Column<DateTime>(name: "last-write-time-utc", type: "datetime2", nullable: false),
mediatype = table.Column<string>(name: "media-type", type: "nvarchar(20)", maxLength: 20, nullable: false),
contenttype = table.Column<string>(name: "content-type", type: "nvarchar(100)", maxLength: 100, nullable: false),
exists = table.Column<bool>(type: "bit", nullable: false),
lastseenat = table.Column<DateTime>(name: "last-seen-at", type: "datetime2", nullable: false),
thumbnailpath = table.Column<string>(name: "thumbnail-path", type: "nvarchar(512)", maxLength: 512, nullable: true),
videoduration = table.Column<double>(name: "video-duration", type: "float", nullable: true),
lastplayedat = table.Column<DateTime>(name: "last-played-at", type: "datetime2", nullable: true),
createdat = table.Column<DateTime>(name: "created-at", type: "datetime2", nullable: false),
updatedat = table.Column<DateTime>(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);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "managed-file-record");
migrationBuilder.DropTable(
name: "managed-library-root");
}
}
}
@@ -0,0 +1,459 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime2")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("datetime2")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("bit")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("nvarchar(32)")
.HasColumnName("extension");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("nvarchar(260)")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("datetime2")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("datetime2")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("datetime2")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("int")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)")
.HasColumnName("media-type");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("bigint")
.HasColumnName("size-bytes");
b.Property<int?>("ThumbnailId")
.HasColumnType("int")
.HasColumnName("thumbnail-id");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("bit")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("datetime2")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("datetime2")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("int")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at");
b.Property<int>("LibraryRootId")
.HasColumnType("int")
.HasColumnName("library-root-id");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)")
.HasColumnName("relative-path");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("用户主键");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("天气预报主键");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("int")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -0,0 +1,99 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FileShare_EFCore.Migrations.SqlServer
{
/// <inheritdoc />
public partial class AddThumbnailMap : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "thumbnail-path",
table: "managed-file-record");
migrationBuilder.AddColumn<int>(
name: "thumbnail-id",
table: "managed-file-record",
type: "int",
nullable: true);
migrationBuilder.CreateTable(
name: "managed-thumbnail-map",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
libraryrootid = table.Column<int>(name: "library-root-id", type: "int", nullable: false),
relativepath = table.Column<string>(name: "relative-path", type: "nvarchar(1024)", maxLength: 1024, nullable: false),
contenttype = table.Column<string>(name: "content-type", type: "nvarchar(100)", maxLength: 100, nullable: false),
createdat = table.Column<DateTime>(name: "created-at", type: "datetime2", nullable: false),
updatedat = table.Column<DateTime>(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);
}
/// <inheritdoc />
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<string>(
name: "thumbnail-path",
table: "managed-file-record",
type: "nvarchar(512)",
maxLength: 512,
nullable: true);
}
}
}
@@ -0,0 +1,463 @@
// <auto-generated />
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("20260522092317_AddPlaybackPosition")]
partial class AddPlaybackPosition
{
/// <inheritdoc />
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<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at");
b.Property<string>("Device")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("device");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime2")
.HasColumnName("expires-at");
b.Property<string>("IpAddress")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)")
.HasColumnName("ip-address");
b.Property<string>("ReplacedByTokenHash")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)")
.HasColumnName("replaced-by-token-hash");
b.Property<DateTime?>("RevokedAt")
.HasColumnType("datetime2")
.HasColumnName("revoked-at");
b.Property<string>("TokenHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)")
.HasColumnName("token-hash");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AbsolutePath")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)")
.HasColumnName("absolute-path");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at");
b.Property<bool>("Exists")
.HasColumnType("bit")
.HasColumnName("exists");
b.Property<string>("Extension")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("nvarchar(32)")
.HasColumnName("extension");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(260)
.HasColumnType("nvarchar(260)")
.HasColumnName("file-name");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("datetime2")
.HasColumnName("last-played-at");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("datetime2")
.HasColumnName("last-seen-at");
b.Property<DateTime>("LastWriteTimeUtc")
.HasColumnType("datetime2")
.HasColumnName("last-write-time-utc");
b.Property<int>("LibraryRootId")
.HasColumnType("int")
.HasColumnName("library-root-id");
b.Property<string>("MediaType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)")
.HasColumnName("media-type");
b.Property<double?>("PlaybackPosition")
.HasColumnType("float")
.HasColumnName("playback-position");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)")
.HasColumnName("relative-path");
b.Property<long>("SizeBytes")
.HasColumnType("bigint")
.HasColumnName("size-bytes");
b.Property<int?>("ThumbnailId")
.HasColumnType("int")
.HasColumnName("thumbnail-id");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2")
.HasColumnName("updated-at");
b.Property<double?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("display-name");
b.Property<bool>("IsAvailable")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true)
.HasColumnName("is-available");
b.Property<bool>("IsEnabled")
.HasColumnType("bit")
.HasColumnName("is-enabled");
b.Property<DateTime?>("LastScanCompletedAt")
.HasColumnType("datetime2")
.HasColumnName("last-scan-completed-at");
b.Property<string>("LastScanError")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)")
.HasColumnName("last-scan-error");
b.Property<DateTime?>("LastScanStartedAt")
.HasColumnType("datetime2")
.HasColumnName("last-scan-started-at");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)")
.HasColumnName("path");
b.Property<int>("ScanIntervalMinutes")
.HasColumnType("int")
.HasColumnName("scan-interval-minutes");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)")
.HasColumnName("content-type");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at");
b.Property<int>("LibraryRootId")
.HasColumnType("int")
.HasColumnName("library-root-id");
b.Property<string>("RelativePath")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)")
.HasColumnName("relative-path");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("用户主键");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("email")
.HasComment("用户邮箱");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)")
.HasColumnName("name")
.HasComment("用户名称");
b.Property<string>("PasswordHash")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("password-hash")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)")
.HasColumnName("phone-number")
.HasComment("电话号码");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id")
.HasComment("天气预报主键");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("created-at")
.HasComment("创建时间");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date")
.HasComment("预报日期");
b.Property<string>("Summary")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("summary")
.HasComment("天气摘要");
b.Property<int>("TemperatureC")
.HasColumnType("int")
.HasColumnName("temperature-c")
.HasComment("摄氏温度");
b.Property<DateTime>("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
}
}
}
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FileShare_EFCore.Migrations.SqlServer
{
/// <inheritdoc />
public partial class AddPlaybackPosition : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<double>(
name: "playback-position",
table: "managed-file-record",
type: "float",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "playback-position",
table: "managed-file-record");
}
}
}

Some files were not shown because too many files have changed in this diff Show More