.NET Core 文件系统

1. ASP.NET Core 静态文件

在 ASP.NET Core 中,静态文件(如 HTML、CSS、图像和 JavaScript)默认存储在项目的 wwwroot 文件夹中。为了安全起见,框架不会自动提供这些文件,必须显式配置中间件。

1.1 启用静态文件服务

默认情况下,ASP.NET Core 不提供服务静态文件的功能。即使文件存在于 wwwroot 目录中,直接访问也会返回 404。需要在请求管道中调用 UseStaticFiles() 中间件。

步骤:

确保项目根目录下存在 wwwroot 文件夹。

在 Program.cs 中注册中间件。

cs 复制代码
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// 启用默认 wwwroot 目录的静态文件服务
// 必须放在 UseRouting 之前或之后,但在 MapControllers/MapRazorPages 之前
app.UseStaticFiles();

app.MapGet("/", () => "Hello World!");
app.Run();

1.2 新增静态文件目录

如果希望从 wwwroot 以外的目录提供静态文件(例如 MyStaticFiles),需要配置 StaticFileOptions 并指定 FileProvider。

步骤:

1.创建一个新的文件夹,例如 MyStaticFiles。

  1. 在 Program.cs 中添加额外的 UseStaticFiles 调用。
cs 复制代码
using Microsoft.Extensions.FileProviders;
using System.IO;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// 默认 wwwroot
app.UseStaticFiles();

// 自定义目录:MyStaticFiles
// 访问路径: /static/xxx.png
var myStaticFilesPath = Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles");
if (Directory.Exists(myStaticFilesPath))
{
    app.UseStaticFiles(new StaticFileOptions
    {
        FileProvider = new PhysicalFileProvider(myStaticFilesPath),
        RequestPath = "/static"
    });
}

app.Run();

1.3 自定义 HTTP 响应头

可以通过 OnPrepareResponse 回调为静态文件设置自定义缓存策略或其他 HTTP 头。

步骤:

在 StaticFileOptions 中配置 OnPrepareResponse。

cs 复制代码
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;

// ... 在 Program.cs 中

app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        // 设置缓存控制头,例如缓存 1 年
        const int durationInSeconds = 60 * 60 * 24 * 365;
        ctx.Context.Response.Headers.Append(
            "Cache-Control", 
            $"public, max-age={durationInSeconds}"
        );
        
        // 添加自定义头
        ctx.Context.Response.Headers.Append("X-Custom-Header", "MyValue");
    }
});

1.4 静态文件的授权管理

默认情况下,UseStaticFiles 不提供授权检查。如果需要保护某些静态文件(如用户私有图片),可以使用 MapWhen 或自定义中间件,或者将文件放在受控区域并通过控制器动作流式传输。但更简单的方式是使用 UseStaticFiles 结合授权中间件的顺序,或者使用专门的端点映射。

对于简单的基于角色的访问,通常建议通过 Controller 行动作来提供受保护的文件,因为 UseStaticFiles 是管道早期的中间件,很难直接集成复杂的授权逻辑。但如果必须使用中间件方式,可以这样处理:

cs 复制代码
// 注意:这种方式较为粗糙,推荐对于敏感文件使用 Controller + Authorize Attribute
app.Use(async (context, next) =>
{
    if (context.Request.Path.StartsWithSegments("/private"))
    {
        if (!context.User.Identity.IsAuthenticated)
        {
            context.Response.StatusCode = 401;
            return;
        }
    }
    await next();
});

// 然后配置 /private 路径的静态文件
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "PrivateFiles")),
    RequestPath = "/private"
});

1.5 开启目录浏览服务

默认情况下,ASP.NET Core 禁止目录浏览以保障安全。如果需要列出目录内容,需启用 UseDirectoryBrowser。

步骤:

添加 UseDirectoryBrowser 中间件。

确保对应的 UseStaticFiles 也配置了相同的 FileProvider。

cs 复制代码
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;

// ...

var dirPath = Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles");
if (Directory.Exists(dirPath))
{
    var fileProvider = new PhysicalFileProvider(dirPath);

    // 启用目录浏览
    app.UseDirectoryBrowser(new DirectoryBrowserOptions
    {
        FileProvider = fileProvider,
        RequestPath = "/static"
    });

    // 必须同时启用静态文件服务,否则点击文件无法下载/查看
    app.UseStaticFiles(new StaticFileOptions
    {
        FileProvider = fileProvider,
        RequestPath = "/static"
    });
}

1.6 自定义文件扩展名

ASP.NET Core 默认只识别常见的 MIME 类型。如果需要提供未知扩展名的文件(如 .xyz),需要配置 ContentTypeProvider。

步骤:

创建 FileExtensionContentTypeProvider 实例。

添加新的映射关系。

cs 复制代码
using Microsoft.AspNetCore.StaticFiles;

// ...

var provider = new FileExtensionContentTypeProvider();
// 添加自定义扩展名映射
provider.Mappings[".xyz"] = "application/x-custom-type";
provider.Mappings[".log"] = "text/plain";

app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = provider
});

2. 自定义一个简单的文件系统

ASP.NET Core 使用 IFileProvider 接口抽象文件系统。我们可以实现自定义的 IFileProvider 来从非物理存储(如 Redis、数据库、内存)提供文件。

2.1 文件和目录接口核心概念

要实现自定义文件系统,主要涉及三个接口:

IFileProvider: 入口点,用于获取 IDirectoryContents 和 IFileInfo。

IFileInfo: 描述单个文件的元数据(名称、长度、最后修改时间等)和内容流。

IDirectoryContents: 描述目录内容的集合。

2.2 RedisFileProvider 实现

以下是一个简化的示例,展示如何从 Redis 中读取文件内容。将文件内容存储在 Redis 字符串中,键名为文件路径。

安装 StackExchange.Redis NuGet 包。

RedisFileProvider

cs 复制代码
using Microsoft.Extensions.FileProviders;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

public class RedisFileProvider : IFileProvider
{
    private readonly IDatabase _redisDb;
    private readonly string _keyPrefix;

    public RedisFileProvider(IConnectionMultiplexer redis, string keyPrefix = "files:")
    {
        _redisDb = redis.GetDatabase();
        _keyPrefix = keyPrefix;
    }

    public IFileInfo GetFileInfo(string subpath)
    {
        if (string.IsNullOrEmpty(subpath) || subpath.Contains(".."))
        {
            return new NotFoundFileInfo(subpath);
        }

        // 清理路径,移除前导斜杠
        var cleanPath = subpath.TrimStart('/');
        var key = $"{_keyPrefix}{cleanPath}";

        // 检查键是否存在
        var exists = _redisDb.KeyExists(key);
        if (!exists)
        {
            return new NotFoundFileInfo(subpath);
        }

        // 获取文件大小和最后修改时间(这里简化处理,实际可能需要额外存储元数据)
        var value = _redisDb.StringGet(key);
        if (value.IsNullOrEmpty)
        {
            return new NotFoundFileInfo(subpath);
        }

        return new RedisFileInfo(subpath, value.Length, DateTime.UtcNow);
    }

    public IDirectoryContents GetDirectoryContents(string subpath)
    {
        // Redis 不是层级文件系统,模拟目录浏览非常复杂且低效
        // 这里仅返回空集合,表示不支持目录浏览
        return new NotFoundDirectoryContents();
    }

    public IChangeToken Watch(string filter)
    {
        // 简化实现,返回不触发的 Token
        return NullChangeToken.Singleton;
    }
}

public class RedisFileInfo : IFileInfo
{
    private readonly string _name;
    private readonly long _length;
    private readonly DateTimeOffset _lastModified;

    public RedisFileInfo(string name, long length, DateTimeOffset lastModified)
    {
        _name = name;
        _length = length;
        _lastModified = lastModified;
    }

    public bool Exists => true;
    public long Length => _length;
    public string PhysicalPath => null; // 无物理路径
    public string Name => Path.GetFileName(_name);
    public DateTimeOffset LastModified => _lastModified;
    public bool IsDirectory => false;

    public Stream CreateReadStream()
    {
        // 在实际生产中,应该从 Redis 重新读取数据流
        // 这里为了演示简化,假设数据已在构造函数中获取或需要重新查询
        // 注意:IFileInfo 应该是轻量级的,CreateReadStream 才真正加载数据
        // 由于 RedisFileInfo 构造时没有持有 byte[],这里需要访问 Redis
        // 为了保持类纯净,通常 RedisFileProvider 会在 GetFileInfo 时不加载内容,
        // 而在 CreateReadStream 时加载。
        
        // 修正:由于 IFileInfo 接口限制,我们需要在 Provider 中注入 Redis 连接或在 FileInfo 中持有引用
        // 此处为简化代码结构,假设我们通过某种方式能读取到流
        // 在实际实现中,RedisFileInfo 应持有 IDatabase 引用和 Key
        
        throw new NotImplementedException("在实际实现中,此处应从 Redis 读取 Stream");
    }
}

// 更完善的 RedisFileInfo 实现
public class RedisFileInfoComplete : IFileInfo
{
    private readonly IDatabase _redisDb;
    private readonly string _key;
    private readonly string _name;
    
    public RedisFileInfoComplete(IDatabase redisDb, string key, string name)
    {
        _redisDb = redisDb;
        _key = key;
        _name = name;
    }

    public bool Exists => _redisDb.KeyExists(_key);
    public long Length => Exists ? _redisDb.StringLength(_key) : 0;
    public string PhysicalPath => null;
    public string Name => Path.GetFileName(_name);
    public DateTimeOffset LastModified => DateTimeOffset.UtcNow; // 简化
    public bool IsDirectory => false;

    public Stream CreateReadStream()
    {
        var bytes = _redisDb.StringGet(_key);
        if (bytes.IsNullOrEmpty)
        {
            return Stream.Null;
        }
        return new MemoryStream(bytes.ToArray());
    }
}

Program

cs 复制代码
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using StackExchange.Redis;
using System;
using System.IO;

var builder = WebApplication.CreateBuilder(args);

// 配置 Redis 连接 (假设本地 Redis)
var redis = ConnectionMultiplexer.Connect("localhost:6379");
builder.Services.AddSingleton<IConnectionMultiplexer>(redis);

var app = builder.Build();

// 初始化一些测试数据到 Redis
var db = redis.GetDatabase();
db.StringSet("files:test.txt", "Hello from Redis!");
db.StringSet("files/readme.md", "# Redis File Provider Demo");

// 使用自定义 RedisFileProvider
// 注意:需要使用上面定义的 RedisFileInfoComplete 逻辑更新 RedisFileProvider
// 这里为了代码可运行性,展示配置方式

/* 
 * 由于上面的 RedisFileProvider 示例中 CreateReadStream 未完整实现,
 * 在实际项目中请确保 RedisFileInfo 能够访问 Redis 数据库以读取流。
 */

// 假设我们已经完善了 RedisFileProvider 和 RedisFileInfoComplete
// app.UseStaticFiles(new StaticFileOptions
// {
//     FileProvider = new RedisFileProvider(redis),
//     RequestPath = "/redis-files"
// });

app.MapGet("/", () => "Check /redis-files/test.txt (if configured correctly)");

app.Run();

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using StackExchange.Redis;
using System;
using System.IO;

var builder = WebApplication.CreateBuilder(args);

// 配置 Redis 连接 (假设本地 Redis)
var redis = ConnectionMultiplexer.Connect("localhost:6379");
builder.Services.AddSingleton<IConnectionMultiplexer>(redis);

var app = builder.Build();

// 初始化一些测试数据到 Redis
var db = redis.GetDatabase();
db.StringSet("files:test.txt", "Hello from Redis!");
db.StringSet("files/readme.md", "# Redis File Provider Demo");

// 使用自定义 RedisFileProvider
// 注意:需要使用上面定义的 RedisFileInfoComplete 逻辑更新 RedisFileProvider
// 这里为了代码可运行性,展示配置方式

/* 
 * 由于上面的 RedisFileProvider 示例中 CreateReadStream 未完整实现,
 * 在实际项目中请确保 RedisFileInfo 能够访问 Redis 数据库以读取流。
 */

// 假设我们已经完善了 RedisFileProvider 和 RedisFileInfoComplete
// app.UseStaticFiles(new StaticFileOptions
// {
//     FileProvider = new RedisFileProvider(redis),
//     RequestPath = "/redis-files"
// });

app.MapGet("/", () => "Check /redis-files/test.txt (if configured correctly)");

app.Run();

2.3 使用自定义文件系统

在 Program.cs 中,将自定义的 IFileProvider 传递给 UseStaticFiles 中间件。

  • 依赖注入:可以将 IFileProvider 注册为服务,以便在应用的其他部分复用。
  • 混合使用:可以同时使用多个 UseStaticFiles,一个用于物理文件(wwwroot),一个用于 Redis 文件。
  • 性能考虑 :自定义 IFileProvider 中的 GetFileInfo 和 CreateReadStream 应尽量高效。对于 Redis,避免在 GetFileInfo 中加载大文件内容,仅在 CreateReadStream 中加载。

示例:

cs 复制代码
// 在 Program.cs 中

// 1. 获取 Redis 连接
var redis = ConnectionMultiplexer.Connect("localhost:6379");

// 2. 配置静态文件中间件使用 RedisProvider
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new RedisFileProvider(redis), // 需使用包含完整逻辑的版本
    RequestPath = "/cloud"
});

// 现在访问 /cloud/test.txt 将从 Redis 读取 key "files:test.txt" 的内容
相关推荐
FPGA小迷弟1 小时前
FPGA 与卫星:从星载硬件到在轨智能的完整技术解析
fpga开发·架构·fpga·1024程序员节·无线电
IsITGirl2 小时前
VMware Fusion(Mac)Windows ARM 虚拟机 NAT (vmnet8) 报错解决记录
windows·macos·.net·桥接模式
SZYSK2 小时前
OpenHarmony 上读卡、推理和双屏刷新抢 CPU:核验机与消费机的调度差
物联网·算法·harmonyos·1024程序员节
FLJwu2 小时前
《运动相机硬件防护选型实战》专栏|第二篇(PPTC 自恢复保险丝)
android·经验分享·数码相机·1024程序员节·运动相机
xsd202411182 小时前
OCEC 24×40 开闭眼脑电分类:从 α 波节律到实时 BCI 落地方案
人工智能·1024程序员节
Ivanqhz2 小时前
MoE + LLM 应用
数据库·人工智能·pytorch·python·深度学习·算法·1024程序员节
紧跟先前的步伐11 小时前
Go AI Agent 实战 24|工具系统工程化收尾与阶段验收
人工智能·ai·golang·1024程序员节
吠品12 小时前
智谱开源 ZCode 之后,真正的考试才开始
单片机·嵌入式硬件·1024程序员节
日日大王学编程13 小时前
#Feed 流项目复盘:Redis 时间线缓存与 Outbox 消息可靠性设计
golang·1024程序员节