StarWeb/StarServer支持/files下载插件,先下载到临时目录,避免出现下载半截的情况
大石头 authored at 2023-02-28 14:54:51
5.49 KiB
Stardust
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.FileProviders.Internal;
using Microsoft.Extensions.FileProviders.Physical;
using Microsoft.Extensions.Primitives;
using NewLife;
using NewLife.Log;

namespace Stardust.Server.Services;

/// <summary>文件缓存提供者。本地文件不存在时,从上级拉取</summary>
public class CacheFileProvider : IFileProvider
{
    #region 属性
    private static readonly Char[] _pathSeparators = new Char[2]
    {
        Path.DirectorySeparatorChar,
        Path.AltDirectorySeparatorChar
    };

    private readonly ExclusionFilters _filters;

    /// <summary>根目录</summary>
    public String Root { get; }

    /// <summary>服务端地址。本地文件不存在时,将从这里下载</summary>
    public String Server { get; set; }
    #endregion

    //public CacheFileProvider(String root) : this(root, ExclusionFilters.Sensitive) { }

    public CacheFileProvider(String root, String server, ExclusionFilters filters = ExclusionFilters.Sensitive)
    {
        if (!Path.IsPathRooted(root)) throw new ArgumentException("The path must be absolute.", nameof(root));

        Root = Path.GetFullPath(root).EnsureEnd(Path.DirectorySeparatorChar + "");
        if (!Directory.Exists(Root)) throw new DirectoryNotFoundException(Root);

        Server = server.TrimEnd('/');
        _filters = filters;
    }

    private String GetFullPath(String path)
    {
        if (PathNavigatesAboveRoot(path)) return null;

        String fullPath;
        try
        {
            fullPath = Path.GetFullPath(Path.Combine(Root, path));
        }
        catch
        {
            return null;
        }

        return !fullPath.StartsWithIgnoreCase(Root) ? null : fullPath;
    }

    public IFileInfo GetFileInfo(String subpath)
    {
        if (String.IsNullOrEmpty(subpath) || HasInvalidPathChars(subpath)) return new NotFoundFileInfo(subpath);

        subpath = subpath.TrimStart(_pathSeparators);
        if (Path.IsPathRooted(subpath)) return new NotFoundFileInfo(subpath);

        var fullPath = GetFullPath(subpath);
        if (fullPath == null) return new NotFoundFileInfo(subpath);

        // 本地不存在时,从服务器下载
        if (!File.Exists(fullPath) && Path.GetFileName(fullPath).Contains('.'))
        {
            var url = subpath.Replace("\\", "/");
            url = Server.Contains("{0}") ? Server.Replace("{0}", url) : Server + url.EnsureStart("/");

            XTrace.WriteLine("下载:{0}", url);

            // 先下载到临时目录,避免出现下载半截的情况
            var tmp = Path.GetTempFileName();
            using var fs = new FileStream(tmp, FileMode.OpenOrCreate);

            using var client = new HttpClient();
            using var rs = client.GetStreamAsync(url).Result;
            rs.CopyTo(fs);
            fs.Flush();
            fs.SetLength(fs.Position);
            fs.Dispose();

            // 移动临时文件到最终目录
            fullPath.EnsureDirectory(true);
            File.Move(tmp, fullPath);

            XTrace.WriteLine("下载完成:{0}", fullPath);
        }

        var fileInfo = new FileInfo(fullPath);
        return IsExcluded(fileInfo, _filters) ? new NotFoundFileInfo(subpath) : new PhysicalFileInfo(fileInfo);
    }

    public static Boolean IsExcluded(FileSystemInfo fileInfo, ExclusionFilters filters)
    {
        if (filters == ExclusionFilters.None) return false;

        return fileInfo.Name.StartsWith(".", StringComparison.Ordinal) && (filters & ExclusionFilters.DotPrefixed) != 0 ||
            fileInfo.Exists && (

                (fileInfo.Attributes & FileAttributes.Hidden) != 0 && (filters & ExclusionFilters.Hidden) != 0 ||
                (fileInfo.Attributes & FileAttributes.System) != 0 && (filters & ExclusionFilters.System) != 0
            );
    }

    public IDirectoryContents GetDirectoryContents(String subpath)
    {
        try
        {
            if (subpath == null || HasInvalidPathChars(subpath)) return NotFoundDirectoryContents.Singleton;

            subpath = subpath.TrimStart(_pathSeparators);
            if (Path.IsPathRooted(subpath)) return NotFoundDirectoryContents.Singleton;

            var fullPath = GetFullPath(subpath);
            return fullPath == null || !Directory.Exists(fullPath)
                ? NotFoundDirectoryContents.Singleton
                : new PhysicalDirectoryContents(fullPath, _filters);
        }
        catch (DirectoryNotFoundException) { }
        catch (IOException) { }

        return NotFoundDirectoryContents.Singleton;
    }

    public IChangeToken Watch(String filter) => NullChangeToken.Singleton;

    internal static Boolean HasInvalidPathChars(String path) => path.IndexOfAny(_invalidFileNameChars) != -1;

    private static readonly Char[] _invalidFileNameChars = (from c in Path.GetInvalidFileNameChars()
                                                            where c != Path.DirectorySeparatorChar && c != Path.AltDirectorySeparatorChar
                                                            select c).ToArray();

    internal static Boolean PathNavigatesAboveRoot(String path)
    {
        var stringTokenizer = new StringTokenizer(path, _pathSeparators);
        var num = 0;
        foreach (var item in stringTokenizer)
        {
            if (item.Equals(".") || item.Equals("")) continue;
            if (item.Equals(".."))
            {
                num--;
                if (num == -1) return true;
            }
            else
                num++;
        }
        return false;
    }
}