简体   繁体   中英

Serve static files outside wwwroot, but handle PhysicalFileProvider when directory does not exist

I am trying to serve files from outside wwwroot but also handle a setup situation where the directory may not exist yet. For example, if I built a site which relies on this directory, but a user did not follow install instructions and create the directory first.

For the sake of argument, let's pretend I want a simple website which has a page that will read the contents of that directory and display some files. This page might also allow you to download files from /Documents subdirectory. The example directory would be C:\\Example\\Documents .

As this is an aspnet core mvc project, I would use UseStaticFiles() in the Configure method inside Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory) {
    if (env.IsDevelopment()) {
        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();
    } else {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseStaticFiles();

    // If C:/Example/Documents does not exist, an exception is thrown
    // ArgumentException: The directory name C:\Example\Documents\ is invalid
    // What is worse, the users is not directed to /Home/Error due to the application failing to properly start.
    app.UseStaticFiles(new StaticFileOptions() {
        FileProvider = new PhysicalFileProvider("C:/Example/Documents"),
        RequestPath = new PathString("/Documents"),
    });

    app.UseMvc(routes => {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

Now I have a two part problem. The app.UseExceptionHandler("/Home/Error"); does not seem to take effect at all if an exception is thrown in this method. If the path used in PhysicalFileProvider does not exist, an internal server error 500 is thrown and the page is then displayed with an application failed to start message. If I am in developer mode, however, I do see the developer exception page with the actual problem. I really just want to make a friendly error page to handle this situation.

The other part of the problem is what happens when the users fix the issue by creating the directory and adding content. Once created, they would then refresh the page. The fix is to force close the application and restart it. Ideally, I would like it to recover on its own. Would I need to do the UseStaticHandle piece in the Controller? Can you even do that? My attempt failed when trying to use dependency injection on IApplicationBuilder in my controller, but I also did not expect it to work.

I am honestly unsure what the best way to handle this would be. The ideal situation is to allow the application to start properly, which could be accomplished with a Directory.Exists() check around the PhysicalFileProvider , and then forward the user to an error page, but the user would still need to restart the application when they fix the issue.

Exception thrown by PhysicalFileProvider:

System.ArgumentException: The directory name D:\Daten7\ is invalid.
Parameter name: path
   at System.IO.FileSystemWatcher..ctor(String path, String filter)
   at System.IO.FileSystemWatcher..ctor(String path)
   at Microsoft.Extensions.FileProviders.PhysicalFileProvider.CreateFileWatcher(String root)
   at Microsoft.Extensions.FileProviders.PhysicalFileProvider..ctor(String root)
   at WebApplication1.Startup.Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)

In Startup.cs replace PhysicalFileProvider with PhysicalFileProvider2. Please find source code of PhysicalFileProvider2 below.

Detailed Description of PhysicalFileProvider2

Class PhysicalFileProvider of ASP.Net core insists that directory given as argument to constructor already exists on startup and if it not exists refuses to start up properly

I wrote class PhysicalFileProvider2 which can be used instead. It can be instantiated even if the directory does not exist. But on first invocation of any of its method the directory is created if it not already exists. Now startup properly works and users can add files to directory, which then are properly served.

public class PhysicalFileProvider2 : IFileProvider
{
    private string root;
    private PhysicalFileProvider physicalFileProvider;

    public PhysicalFileProvider2(string root)
    {
        this.root = root;
    }

    private PhysicalFileProvider GetPhysicalFileProvider()
    {
        if (!File.Exists(root))
        {
            Directory.CreateDirectory(root);
        }
        if (physicalFileProvider == null)
        {
            physicalFileProvider = new PhysicalFileProvider(root);
        }
        return physicalFileProvider;
    }

    public IDirectoryContents GetDirectoryContents(string subpath)
    {
        return GetPhysicalFileProvider().GetDirectoryContents(subpath);
    }

    public IFileInfo GetFileInfo(string subpath)
    {
        return GetPhysicalFileProvider().GetFileInfo(subpath);
    }

    public IChangeToken Watch(string filter)
    {
        return GetPhysicalFileProvider().Watch(filter);
    }
}

From a general POV, you shouldn't just allow access to all the folders. Just a single folder is good enough.

Like, if user stores documents on your server, then at the very least you would have a "Documents" folder, inside it you can have user create folders and store files in those sub-folders.

The file provider API needs an existing folder when the app starts but it makes complete sense if the "Documents" folder doesn't exist before startup. However, the PhysicalFileProvider will still throw.

To get around this issue, you do something similar to this in at the top of your 'Configure' method, ie, at the start of middleware configuration pipeline.

var path = Path.Combine(app.Environment.ContentRootPath, "Documents");
if (!Directory.Exists(path))
{
    Directory.CreateDirectory(path);
}
app.UseStaticFiles(new StaticFileOptions
{
    RequestPath = "/docs",
    FileProvider = new PhysicalFileProvider(path)
});

When using the new WebApplicationBuilder introduced in .NET 6 you do the same after you have used builder.build();

This creates the directory before the PhysicalFileProvider is built. You can create as many folders and directories you want, but 'path' is the base path. So it atleast needs that to exist.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM