浏览代码

增加网盘程序

懒得勤快 4 年之前
父节点
当前提交
cddf92ce44
共有 32 个文件被更改,包括 1509 次插入2 次删除
  1. 3 0
      .gitignore
  2. 二进制
      src/Masuit.MyBlogs.Core/App_Data/OneDrive.db
  3. 265 0
      src/Masuit.MyBlogs.Core/Controllers/Drive/AdminController.cs
  4. 244 0
      src/Masuit.MyBlogs.Core/Controllers/Drive/DefaultController.cs
  5. 13 0
      src/Masuit.MyBlogs.Core/Controllers/Drive/DriveController.cs
  6. 68 0
      src/Masuit.MyBlogs.Core/Controllers/Drive/UserController.cs
  7. 76 0
      src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/Configuration.cs
  8. 77 0
      src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/ProtectedApiCallHelper.cs
  9. 20 0
      src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/ServiceCollectionExtension.cs
  10. 46 0
      src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/TokenCacheHelper.cs
  11. 143 0
      src/Masuit.MyBlogs.Core/Infrastructure/Drive/DriveAccountService.cs
  12. 159 0
      src/Masuit.MyBlogs.Core/Infrastructure/Drive/DriveService.cs
  13. 55 0
      src/Masuit.MyBlogs.Core/Infrastructure/Drive/IDriveAccountService.cs
  14. 14 0
      src/Masuit.MyBlogs.Core/Infrastructure/Drive/IDriveService.cs
  15. 70 0
      src/Masuit.MyBlogs.Core/Infrastructure/Drive/SettingService.cs
  16. 93 0
      src/Masuit.MyBlogs.Core/Infrastructure/Drive/TokenService.cs
  17. 28 0
      src/Masuit.MyBlogs.Core/Infrastructure/DriveContext.cs
  18. 11 1
      src/Masuit.MyBlogs.Core/Masuit.MyBlogs.Core.csproj
  19. 14 0
      src/Masuit.MyBlogs.Core/Models/Drive/DriveFile.cs
  20. 14 0
      src/Masuit.MyBlogs.Core/Models/Drive/ErrorResponse.cs
  21. 13 0
      src/Masuit.MyBlogs.Core/Models/Drive/Setting.cs
  22. 14 0
      src/Masuit.MyBlogs.Core/Models/Drive/Site.cs
  23. 23 0
      src/Masuit.MyBlogs.Core/Program.cs
  24. 2 0
      src/Masuit.MyBlogs.Core/Startup.cs
  25. 24 0
      src/Masuit.MyBlogs.Core/Views/Drive/Index.cshtml
  26. 19 1
      src/Masuit.MyBlogs.Core/appsettings.json
  27. 1 0
      src/Masuit.MyBlogs.Core/wwwroot/drive/css/app.377a312e.css
  28. 0 0
      src/Masuit.MyBlogs.Core/wwwroot/drive/css/chunk-vendors.39ab6aaa.css
  29. 0 0
      src/Masuit.MyBlogs.Core/wwwroot/drive/js/app.8b25141a.js
  30. 0 0
      src/Masuit.MyBlogs.Core/wwwroot/drive/js/app.8b25141a.js.map
  31. 0 0
      src/Masuit.MyBlogs.Core/wwwroot/drive/js/chunk-vendors.b37660ae.js
  32. 0 0
      src/Masuit.MyBlogs.Core/wwwroot/drive/js/chunk-vendors.b37660ae.js.map

+ 3 - 0
.gitignore

@@ -274,3 +274,6 @@ Test
 material_admin_v-1.5-2
 /src/Masuit.MyBlogs.Core/App_Data/subjects.json
 /src/Masuit.MyBlogs.Core/wwwroot/subjects.json
+/src/Masuit.MyBlogs.Core/TokenCache.bin
+/src/Masuit.MyBlogs.Core/OneDrive.db
+/src/Masuit.MyBlogs.Core/App_Data/OneDrive.template.db

二进制
src/Masuit.MyBlogs.Core/App_Data/OneDrive.db


+ 265 - 0
src/Masuit.MyBlogs.Core/Controllers/Drive/AdminController.cs

@@ -0,0 +1,265 @@
+using Masuit.MyBlogs.Core.Extensions;
+using Masuit.MyBlogs.Core.Extensions.DriveHelpers;
+using Masuit.MyBlogs.Core.Infrastructure.Drive;
+using Masuit.MyBlogs.Core.Models.Drive;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Masuit.MyBlogs.Core.Controllers.Drive
+{
+    [MyAuthorize]
+    [ApiController]
+    [Route("api/[controller]")]
+    public class AdminController : Controller
+    {
+        private readonly IDriveAccountService _driveAccount;
+        private readonly SettingService _setting;
+        private readonly TokenService _tokenService;
+        public AdminController(IDriveAccountService driveAccount, SettingService setting, TokenService tokenService)
+        {
+            _driveAccount = driveAccount;
+            _setting = setting;
+            _tokenService = tokenService;
+        }
+        /// <summary>
+        /// 重定向到 M$ 的 Oauth
+        /// </summary>
+        /// <returns></returns>
+        [AllowAnonymous]
+        [HttpGet("bind/url")]
+        public async Task<RedirectResult> RedirectToBinding()
+        {
+            string url = await _driveAccount.GetAuthorizationRequestUrl();
+            var result = new RedirectResult(url);
+            return result;
+        }
+        /// <summary>
+        /// 从 Oauth 重定向的url
+        /// </summary>
+        /// <returns></returns>
+        [AllowAnonymous]
+        [HttpGet("bind/new")]
+        public async Task<IActionResult> NewBinding(string code)
+        {
+            try
+            {
+                var result = await _tokenService.Authorize(code);
+                if (result.AccessToken != null)
+                {
+                    await _setting.Set("AccountStatus", "已认证");
+                    return Redirect("/#/admin");
+                }
+            }
+            catch (Exception ex)
+            {
+                return StatusCode(500, new ErrorResponse()
+                {
+                    message = ex.Message
+                });
+            }
+            return StatusCode(500, new ErrorResponse()
+            {
+                message = "未知错误"
+            });
+        }
+        /// <summary>
+        /// 添加 SharePoint Site
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost("sites")]
+        public async Task<IActionResult> AddSite(AddSiteModel model)
+        {
+            try
+            {
+                await _driveAccount.AddSiteId(model.siteName, model.nickName);
+            }
+            catch (Exception ex)
+            {
+                return StatusCode(500, new ErrorResponse()
+                {
+                    message = ex.Message
+                });
+            }
+            return StatusCode(201);
+        }
+
+        /// <summary>
+        /// 获取基本内容
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("info")]
+        public async Task<IActionResult> GetInfo()
+        {
+            try
+            {
+                List<DriveAccountService.DriveInfo> driveInfo = new List<DriveAccountService.DriveInfo>();
+                if (_setting.Get("AccountStatus") == "已认证")
+                {
+                    driveInfo = await _driveAccount.GetDriveInfo();
+                }
+                return Json(new
+                {
+                    officeName = Configuration.AccountName,
+                    officeType = Enum.GetName(typeof(Configuration.OfficeType), Configuration.Type),
+                    driveInfo,
+                    appName = _setting.Get("AppName"),
+                    webName = _setting.Get("WebName"),
+                    defaultDrive = _setting.Get("DefaultDrive"),
+                    accountStatus = _setting.Get("AccountStatus"),
+                    readme = _setting.Get("Readme"),
+                    footer = _setting.Get("Footer"),
+                    allowAnonymouslyUpload = !string.IsNullOrEmpty(_setting.Get("AllowAnonymouslyUpload")) && Convert.ToBoolean(_setting.Get("AllowAnonymouslyUpload")),
+                    uploadPassword = _setting.Get("UploadPassword"),
+                }, new JsonSerializerSettings()
+                {
+                    ContractResolver = new CamelCasePropertyNamesContractResolver()
+                });
+            }
+            catch (Exception ex)
+            {
+                return StatusCode(500, new ErrorResponse()
+                {
+                    message = ex.Message
+                });
+            }
+        }
+
+        /// <summary>
+        /// 设置readme
+        /// </summary>
+        /// <param name="model"></param>
+        /// <returns></returns>
+        [HttpPost("readme")]
+        public async Task<IActionResult> UpdateReadme(ReadmeModel model)
+        {
+            try
+            {
+                await _setting.Set("Readme", model.text);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(500, new ErrorResponse()
+                {
+                    message = e.Message
+                });
+            };
+            return StatusCode(204);
+        }
+
+        /// <summary>
+        /// 更新
+        /// </summary>
+        /// <param name="settings"></param>
+        /// <returns></returns>
+        [HttpPost("settings")]
+        public async Task<IActionResult> UpdateSetting(UpdateSettings toSaveSetting)
+        {
+            try
+            {
+                await _setting.Set("AppName", toSaveSetting.appName);
+                await _setting.Set("WebName", toSaveSetting.webName);
+                await _setting.Set("DefaultDrive", toSaveSetting.defaultDrive);
+                await _setting.Set("Footer", toSaveSetting.footer);
+                await _setting.Set("UploadPassword", toSaveSetting.uploadPassword);
+                await _setting.Set("AllowAnonymouslyUpload", toSaveSetting.allowAnonymouslyUpload.ToString());
+            }
+            catch (Exception e)
+            {
+                return StatusCode(500, new ErrorResponse()
+                {
+                    message = e.Message
+                });
+            };
+            return StatusCode(204);
+        }
+
+        /// <summary>
+        /// 解除绑定
+        /// </summary>
+        /// <param name="nickName"></param>
+        /// <returns></returns>
+        [HttpDelete("sites")]
+        public async Task<IActionResult> Unbind(string nickName)
+        {
+            try
+            {
+                await _driveAccount.Unbind(nickName);
+            }
+            catch (Exception e)
+            {
+                return StatusCode(500, new ErrorResponse()
+                {
+                    message = e.Message
+                });
+            };
+            return StatusCode(204);
+        }
+
+        /// <summary>
+        /// 更新站点设置
+        /// </summary>
+        /// <param name="nickName"></param>
+        /// <returns></returns>
+        [HttpPost("sites/settings")]
+        public async Task<IActionResult> UpdateSiteSettings(SiteSettingsModel model)
+        {
+            try
+            {
+                var site = _driveAccount.SiteContext.Sites.SingleOrDefault(site => site.Name == model.siteName);
+                if (site != null)
+                {
+                    site.NickName = model.nickName;
+                    site.HiddenFolders = model.hiddenFolders.Split(',');
+                    _driveAccount.SiteContext.Sites.Update(site);
+                    await _driveAccount.SiteContext.SaveChangesAsync();
+                }
+            }
+            catch (Exception ex)
+            {
+                return StatusCode(500, new ErrorResponse()
+                {
+                    message = ex.Message
+                });
+            }
+            return StatusCode(204);
+        }
+
+        #region 接收表单模型
+        public class UpdateSettings
+        {
+            public string appName { get; set; }
+            public string webName { get; set; }
+            public string navImg { get; set; }
+            public string defaultDrive { get; set; }
+            public string readme { get; set; }
+            public string footer { get; set; }
+            public bool allowAnonymouslyUpload { get; set; }
+            public string uploadPassword { get; set; }
+        }
+
+        public class AddSiteModel
+        {
+            public string siteName { get; set; }
+            public string nickName { get; set; }
+        }
+
+        public class SiteSettingsModel
+        {
+            public string siteName { get; set; }
+            public string nickName { get; set; }
+            public string hiddenFolders { get; set; }
+        }
+
+        public class ReadmeModel
+        {
+            public string text { get; set; }
+        }
+        #endregion
+    }
+}

+ 244 - 0
src/Masuit.MyBlogs.Core/Controllers/Drive/DefaultController.cs

@@ -0,0 +1,244 @@
+using Masuit.MyBlogs.Core.Extensions.DriveHelpers;
+using Masuit.MyBlogs.Core.Infrastructure.Drive;
+using Masuit.MyBlogs.Core.Models.Drive;
+using Masuit.MyBlogs.Core.Models.DTO;
+using Masuit.MyBlogs.Core.Models.ViewModel;
+using Masuit.Tools.Core.Net;
+using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Masuit.MyBlogs.Core.Controllers.Drive
+{
+    [ApiController]
+    [Route("api/")]
+    public class DefaultController : Controller
+    {
+        readonly IDriveAccountService _siteService;
+        readonly IDriveService _driveService;
+        readonly SettingService _setting;
+
+        public UserInfoDto CurrentUser => HttpContext.Session.Get<UserInfoDto>(SessionKey.UserInfo) ?? new UserInfoDto();
+
+        public DefaultController(IDriveAccountService siteService, IDriveService driveService, SettingService setting)
+        {
+            this._siteService = siteService;
+            this._driveService = driveService;
+            this._setting = setting;
+        }
+
+
+        #region Actions
+        /// <summary>
+        /// 返回所有sites
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("sites")]
+        public IActionResult GetSites()
+        {
+            return Json(_siteService.GetSites(), new JsonSerializerSettings()
+            {
+                ContractResolver = new CamelCasePropertyNamesContractResolver()
+            });
+        }
+        /// <summary>
+        /// 根据路径获取文件夹内容
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("sites/{siteName}/{**path}")]
+        public async Task<IActionResult> GetDrectory(string siteName, string path)
+        {
+            if (string.IsNullOrEmpty(siteName))
+            {
+                return NotFound(new ErrorResponse()
+                {
+                    message = "找不到请求的 Site Name"
+                });
+            }
+            if (string.IsNullOrEmpty(path))
+            {
+                try
+                {
+                    var result = await _driveService.GetRootItems(siteName, CurrentUser.IsAdmin);
+                    return Json(result, new JsonSerializerSettings()
+                    {
+                        ContractResolver = new CamelCasePropertyNamesContractResolver()
+                    });
+                }
+                catch (Exception e)
+                {
+                    return StatusCode(500, e.Message);
+                }
+            }
+            else
+            {
+                var result = await _driveService.GetDriveItemsByPath(path, siteName, CurrentUser.IsAdmin);
+                if (result == null)
+                {
+                    return NotFound(new ErrorResponse()
+                    {
+                        message = $"路径{path}不存在"
+                    });
+                }
+                return Json(result, new JsonSerializerSettings()
+                {
+                    ContractResolver = new CamelCasePropertyNamesContractResolver()
+                });
+            }
+        }
+        // catch-all 参数匹配路径
+        /// <summary>
+        /// 下载文件
+        /// </summary>
+        /// <param name="path"></param>
+        /// <returns></returns>
+        [HttpGet("files/{siteName}/{**path}")]
+        public async Task<IActionResult> Download(string siteName, string path)
+        {
+            DriveFile result;
+            try
+            {
+                result = await _driveService.GetDriveItemByPath(path, siteName);
+                if (result != null)
+                {
+                    return new RedirectResult(result.DownloadUrl);
+                }
+
+                return NotFound(new ErrorResponse()
+                {
+                    message = $"所求的{path}不存在"
+                });
+            }
+            catch (Exception e)
+            {
+                return StatusCode(500, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// 获取基本信息
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("info")]
+        public IActionResult GetInfo()
+        {
+            bool isAollowAnonymous = !string.IsNullOrEmpty(_setting.Get("AllowAnonymouslyUpload")) && Convert.ToBoolean(_setting.Get("AllowAnonymouslyUpload"));
+            return Json(new
+            {
+                appName = _setting.Get("AppName"),
+                webName = _setting.Get("WebName"),
+                defaultDrive = _setting.Get("DefaultDrive"),
+                readme = _setting.Get("Readme"),
+                footer = _setting.Get("Footer"),
+                allowUpload = isAollowAnonymous
+            }, new JsonSerializerSettings()
+            {
+                ContractResolver = new CamelCasePropertyNamesContractResolver()
+            });
+        }
+        /// <summary>
+        /// 获得readme
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("readme")]
+        public IActionResult GetReadme()
+        {
+            return Json(new
+            {
+                readme = _setting.Get("Readme")
+            }, new JsonSerializerSettings()
+            {
+                ContractResolver = new CamelCasePropertyNamesContractResolver()
+            });
+        }
+
+        /// <summary>
+        /// 获取文件分片上传路径
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("upload/{siteName}/{**fileName}")]
+        public async Task<IActionResult> GetUploadUrl(string siteName, string fileName)
+        {
+            bool isAollowAnonymous = !string.IsNullOrEmpty(_setting.Get("AllowAnonymouslyUpload")) && Convert.ToBoolean(_setting.Get("AllowAnonymouslyUpload"));
+            if (!isAollowAnonymous)
+            {
+                if (Request.Headers.ContainsKey("Authorization"))
+                {
+                    if (!CurrentUser.IsAdmin)
+                    {
+                        return Unauthorized(new ErrorResponse()
+                        {
+                            message = "未经授权的访问"
+                        });
+                    }
+                }
+                else
+                {
+                    return Unauthorized(new ErrorResponse()
+                    {
+                        message = "未经授权的访问"
+                    });
+                }
+            }
+            string path = Path.Combine($"upload/{Guid.NewGuid()}", fileName);
+            try
+            {
+                var result = await _driveService.GetUploadUrl(path, siteName);
+                return Json(new
+                {
+                    requestUrl = result,
+                    fileUrl = $"{Configuration.BaseUri}/api/files/{siteName}/{path}"
+                }, new JsonSerializerSettings()
+                {
+                    ContractResolver = new CamelCasePropertyNamesContractResolver()
+                });
+            }
+            catch (Exception e)
+            {
+                return StatusCode(500, e.Message);
+            }
+        }
+
+        /// <summary>
+        /// 获取文件分片上传路径
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("cli/upload/{siteName}/:/{**path}")]
+        public async Task<IActionResult> GetUploadUrl(string siteName, string path, string uploadPassword)
+        {
+            if (uploadPassword != _setting.Get("UploadPassword"))
+            {
+                return Unauthorized(new ErrorResponse()
+                {
+                    message = "上传密码错误"
+                });
+            }
+            if (string.IsNullOrEmpty(path))
+            {
+                return BadRequest(new ErrorResponse()
+                {
+                    message = "必须存在上传路径"
+                });
+            }
+            try
+            {
+                var result = await _driveService.GetUploadUrl(path, siteName);
+                return Json(new
+                {
+                    requestUrl = result
+                }, new JsonSerializerSettings()
+                {
+                    ContractResolver = new CamelCasePropertyNamesContractResolver()
+                });
+            }
+            catch (Exception e)
+            {
+                return StatusCode(500, e.Message);
+            }
+        }
+        #endregion
+    }
+}

+ 13 - 0
src/Masuit.MyBlogs.Core/Controllers/Drive/DriveController.cs

@@ -0,0 +1,13 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace Masuit.MyBlogs.Core.Controllers.Drive
+{
+    public class DriveController : Controller
+    {
+        [HttpGet("/drive")]
+        public IActionResult Index()
+        {
+            return View();
+        }
+    }
+}

+ 68 - 0
src/Masuit.MyBlogs.Core/Controllers/Drive/UserController.cs

@@ -0,0 +1,68 @@
+using Masuit.MyBlogs.Core.Common;
+using Masuit.MyBlogs.Core.Configs;
+using Masuit.MyBlogs.Core.Extensions.Hangfire;
+using Masuit.MyBlogs.Core.Infrastructure.Services.Interface;
+using Masuit.MyBlogs.Core.Models.Drive;
+using Masuit.MyBlogs.Core.Models.Enum;
+using Masuit.MyBlogs.Core.Models.ViewModel;
+using Masuit.Tools.Core.Net;
+using Masuit.Tools.Security;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Web;
+
+namespace Masuit.MyBlogs.Core.Controllers.Drive
+{
+    [ApiController]
+    [Route("api/[controller]")]
+    public class UserController : Controller
+    {
+        public IUserInfoService UserInfoService { get; set; }
+
+        /// <summary>
+        /// 验证 返回Token
+        /// </summary>
+        /// <param name="model"></param>
+        /// <returns></returns>
+        [AllowAnonymous]
+        [HttpPost("authenticate")]
+        public IActionResult Authenticate(AuthenticateModel model)
+        {
+            var user = UserInfoService.Login(model.Username, model.Password);
+            if (user is null)
+            {
+                return BadRequest(new ErrorResponse()
+                {
+                    message = "错误的用户名或密码"
+                });
+            }
+            Response.Cookies.Append("username", HttpUtility.UrlEncode(model.Username.Trim()), new CookieOptions()
+            {
+                Expires = DateTime.Now.AddYears(1),
+                SameSite = SameSiteMode.Lax
+            });
+            Response.Cookies.Append("password", model.Password.Trim().DesEncrypt(AppConfig.BaiduAK), new CookieOptions()
+            {
+                Expires = DateTime.Now.AddYears(1),
+                SameSite = SameSiteMode.Lax
+            });
+            HttpContext.Session.Set(SessionKey.UserInfo, user);
+            HangfireHelper.CreateJob(typeof(IHangfireBackJob), nameof(HangfireBackJob.LoginRecord), "default", user, HttpContext.Connection.RemoteIpAddress.ToString(), LoginType.Default);
+            return Ok(new
+            {
+                error = false,
+                id = model.Username,
+                username = model.Username,
+                token = model.Username
+            });
+        }
+    }
+    public class AuthenticateModel
+    {
+        public string Username { get; set; }
+
+        public string Password { get; set; }
+    }
+}

+ 76 - 0
src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/Configuration.cs

@@ -0,0 +1,76 @@
+using Microsoft.Extensions.Configuration;
+using System.IO;
+
+namespace Masuit.MyBlogs.Core.Extensions.DriveHelpers
+{
+    public class Configuration
+    {
+        private static readonly IConfigurationRoot ConfigurationRoot;
+        static Configuration()
+        {
+            var builder = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json", true, true);
+            ConfigurationRoot = builder.Build();
+        }
+
+        /// <summary>
+        /// 数据库连接字符串
+        /// </summary>
+        public static string ConnectionString => ConfigurationRoot["OneDrive:ConnectionString"];
+
+        /// <summary>
+        /// Graph连接 ClientID
+        /// </summary>
+        public static string ClientId => ConfigurationRoot["OneDrive:ClientId"];
+
+        /// <summary>
+        /// Graph连接 ClientSecret
+        /// </summary>
+        public static string ClientSecret => ConfigurationRoot["OneDrive:ClientSecret"];
+
+        /// <summary>
+        /// Binding 回调 Url
+        /// </summary>
+        public static string BaseUri => ConfigurationRoot["OneDrive:BaseUri"];
+
+        /// <summary>
+        /// 返回 Scopes
+        /// </summary>
+        /// <value></value>
+        public static string[] Scopes => new string[] { "Sites.ReadWrite.All", "Files.ReadWrite.All" };
+
+        /// <summary>
+        /// 代理路径
+        /// </summary>
+        public static string Proxy => ConfigurationRoot["OneDrive:Proxy"];
+
+        /// <summary>
+        /// 账户名称
+        /// </summary>
+        public static string AccountName => ConfigurationRoot["OneDrive:AccountName"];
+
+        /// <summary>
+        /// 域名
+        /// </summary>
+        public static string DominName => ConfigurationRoot["OneDrive:DominName"];
+
+        /// <summary>
+        /// Office 类型
+        /// </summary>
+        /// <param name="="></param>
+        /// <returns></returns>
+        public static OfficeType Type => (ConfigurationRoot["OneDrive:Type"] == "China") ? OfficeType.China : OfficeType.Global;
+
+        /// <summary>
+        /// Graph Api
+        /// </summary>
+        /// <param name="="></param>
+        /// <returns></returns>
+        public static string GraphApi => (ConfigurationRoot["OneDrive:Type"] == "China") ? "https://microsoftgraph.chinacloudapi.cn" : "https://graph.microsoft.com";
+
+        public enum OfficeType
+        {
+            Global,
+            China
+        }
+    }
+}

+ 77 - 0
src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/ProtectedApiCallHelper.cs

@@ -0,0 +1,77 @@
+using System;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Masuit.MyBlogs.Core.Extensions.DriveHelpers
+{
+    /// <summary>
+    /// Helper class to call a protected API and process its result
+    /// </summary>
+    public class ProtectedApiCallHelper
+    {
+        /// <summary>
+        /// Constructor
+        /// </summary>
+        /// <param name="httpClient">HttpClient used to call the protected API</param>
+        public ProtectedApiCallHelper(HttpClient httpClient)
+        {
+            HttpClient = httpClient;
+        }
+
+        protected HttpClient HttpClient { get; }
+
+
+        /// <summary>
+        /// Calls the protected Web API and processes the result
+        /// </summary>
+        /// <param name="webApiUrl">Url of the Web API to call (supposed to return Json)</param>
+        /// <param name="accessToken">Access token used as a bearer security token to call the Web API</param>
+        /// <param name="processResult">Callback used to process the result of the call to the Web API</param>
+        public async Task CallWebApiAndProcessResultASync(string webApiUrl, string accessToken, Action<JObject> processResult, Method method = Method.Get, HttpContent sendContent = null)
+        {
+            if (!string.IsNullOrEmpty(accessToken))
+            {
+                var defaultRequetHeaders = HttpClient.DefaultRequestHeaders;
+                if (defaultRequetHeaders.Accept.All(m => m.MediaType != "application/json"))
+                {
+                    HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+                }
+                defaultRequetHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
+                var response = method switch
+                {
+                    Method.Get => await HttpClient.GetAsync(webApiUrl),
+                    Method.Post => await HttpClient.PostAsync(webApiUrl, sendContent),
+                    Method.Put => await HttpClient.PutAsync(webApiUrl, sendContent),
+                    _ => new HttpResponseMessage()
+                };
+                if (response.IsSuccessStatusCode)
+                {
+                    string json = await response.Content.ReadAsStringAsync();
+                    JObject result = JsonConvert.DeserializeObject(json) as JObject;
+                    processResult(result);
+                }
+                else
+                {
+                    string content = await response.Content.ReadAsStringAsync();
+                    JObject result = JsonConvert.DeserializeObject(content) as JObject;
+                    throw new Exception(result.Property("error").Value["message"].ToString());
+                }
+            }
+            else
+            {
+                throw new Exception("未提供Token");
+            }
+        }
+
+        public enum Method
+        {
+            Post,
+            Get,
+            Put
+        }
+    }
+}

+ 20 - 0
src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/ServiceCollectionExtension.cs

@@ -0,0 +1,20 @@
+using Masuit.MyBlogs.Core.Infrastructure;
+using Masuit.MyBlogs.Core.Infrastructure.Drive;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Masuit.MyBlogs.Core.Extensions.DriveHelpers
+{
+    public static class ServiceCollectionExtension
+    {
+        public static IServiceCollection AddOneDrive(this IServiceCollection services)
+        {
+            services.AddDbContext<DriveContext>(ServiceLifetime.Transient);
+            //不要被 CG Token获取采用单一实例
+            services.AddSingleton(new TokenService());
+            services.AddTransient<IDriveAccountService, DriveAccountService>();
+            services.AddTransient<IDriveService, DriveService>();
+            services.AddScoped<SettingService>();
+            return services;
+        }
+    }
+}

+ 46 - 0
src/Masuit.MyBlogs.Core/Extensions/DriveHelpers/TokenCacheHelper.cs

@@ -0,0 +1,46 @@
+using Microsoft.Identity.Client;
+using System.IO;
+
+namespace Masuit.MyBlogs.Core.Extensions.DriveHelpers
+{
+    /// <summary>
+    /// 缓存 Token
+    /// </summary>
+    static class TokenCacheHelper
+    {
+        public static void EnableSerialization(ITokenCache tokenCache)
+        {
+            tokenCache.SetBeforeAccess(BeforeAccessNotification);
+            tokenCache.SetAfterAccess(AfterAccessNotification);
+        }
+
+        /// <summary>
+        /// Path to the token cache
+        /// </summary>
+        public static readonly string CacheFilePath = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "TokenCache.bin");
+
+        private static readonly object FileLock = new object();
+
+
+        private static void BeforeAccessNotification(TokenCacheNotificationArgs args)
+        {
+            lock (FileLock)
+            {
+                args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath) ? File.ReadAllBytes(CacheFilePath) : null);
+            }
+        }
+
+        private static void AfterAccessNotification(TokenCacheNotificationArgs args)
+        {
+            // if the access operation resulted in a cache update
+            if (args.HasStateChanged)
+            {
+                lock (FileLock)
+                {
+                    // reflect changesgs in the persistent store
+                    File.WriteAllBytes(CacheFilePath, args.TokenCache.SerializeMsalV3());
+                }
+            }
+        }
+    }
+}

+ 143 - 0
src/Masuit.MyBlogs.Core/Infrastructure/Drive/DriveAccountService.cs

@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Masuit.MyBlogs.Core.Extensions.DriveHelpers;
+using Masuit.MyBlogs.Core.Models.Drive;
+using Microsoft.Identity.Client;
+
+namespace Masuit.MyBlogs.Core.Infrastructure.Drive
+{
+    public class DriveAccountService : IDriveAccountService
+    {
+        private IConfidentialClientApplication app;
+        public DriveContext SiteContext { get; set; }
+        /// <summary>
+        /// Graph实例
+        /// </summary>
+        /// <value></value>
+        public Microsoft.Graph.GraphServiceClient Graph { get; set; }
+
+        public DriveAccountService(DriveContext siteContext, TokenService tokenService)
+        {
+            this.SiteContext = siteContext;
+            this.app = tokenService.app;
+            this.Graph = tokenService.Graph;
+        }
+        /// <summary>
+        /// 返回 Oauth 验证url
+        /// </summary>
+        /// <returns></returns>
+        public async Task<string> GetAuthorizationRequestUrl()
+        {
+            var redirectUrl = await app.GetAuthorizationRequestUrl(Configuration.Scopes).ExecuteAsync();
+            return redirectUrl.AbsoluteUri;
+        }
+        /// <summary>
+        /// 添加 SharePoint Site-ID 到数据库
+        /// </summary>
+        /// <param name="siteName"></param>
+        /// <param name="dominName"></param>
+        /// <returns></returns>
+        public async Task AddSiteId(string siteName, string nickName)
+        {
+            Site site = new Site();
+            //使用 Onedrive
+            if (siteName == "onedrive")
+            {
+                site.Name = siteName;
+                site.NickName = nickName;
+            }
+            else
+            {
+                using HttpClient httpClient = new HttpClient
+                {
+                    Timeout = TimeSpan.FromSeconds(20)
+                };
+                var apiCaller = new ProtectedApiCallHelper(httpClient);
+                await apiCaller.CallWebApiAndProcessResultASync($"{Configuration.GraphApi}/v1.0/sites/{Configuration.DominName}:/sites/{siteName}", GetToken(), result =>
+                {
+                    site.SiteId = result.Properties().Single((prop) => prop.Name == "id").Value.ToString();
+                    site.Name = result.Properties().Single((prop) => prop.Name == "name").Value.ToString();
+                    site.NickName = nickName;
+                });
+            }
+            if (!SiteContext.Sites.Any(s => s.SiteId == site.SiteId))
+            {
+                //若是首次添加则设置为默认的驱动器
+                using (SettingService setting = new SettingService(new DriveContext()))
+                {
+                    if (!SiteContext.Sites.Any())
+                    {
+                        await setting.Set("DefaultDrive", site.Name);
+                    }
+                }
+                await SiteContext.Sites.AddAsync(site);
+                await SiteContext.SaveChangesAsync();
+            }
+            else
+            {
+                throw new Exception("站点已被创建");
+            }
+        }
+
+        public List<Site> GetSites()
+        {
+            List<Site> result = SiteContext.Sites.ToList();
+            return result;
+        }
+
+        /// <summary>
+        /// 获取 Drive Info
+        /// </summary>
+        /// <returns></returns>
+        public async Task<List<DriveInfo>> GetDriveInfo()
+        {
+            List<DriveInfo> drivesInfo = new List<DriveInfo>();
+            foreach (var item in SiteContext.Sites.ToArray())
+            {
+                Microsoft.Graph.Drive drive;
+                //Onedrive
+                if (string.IsNullOrEmpty(item.SiteId))
+                {
+                    drive = await Graph.Me.Drive.Request().GetAsync();
+                }
+                else
+                {
+                    drive = await Graph.Sites[item.SiteId].Drive.Request().GetAsync();
+                }
+                drivesInfo.Add(new DriveInfo()
+                {
+                    Quota = drive.Quota,
+                    NickName = item.NickName,
+                    Name = item.Name,
+                    HiddenFolders = item.HiddenFolders
+                });
+            }
+            return drivesInfo;
+        }
+
+        public async Task Unbind(string nickName)
+        {
+            SiteContext.Sites.Remove(SiteContext.Sites.Single(site => site.NickName == nickName));
+            await SiteContext.SaveChangesAsync();
+        }
+
+        /// <summary>
+        /// 获取 Token
+        /// </summary>
+        /// <returns></returns>
+        public string GetToken()
+        {
+            return app.AcquireTokenSilent(Configuration.Scopes, Configuration.AccountName).ExecuteAsync().Result.AccessToken;
+        }
+        public class DriveInfo
+        {
+            public Microsoft.Graph.Quota Quota { get; set; }
+            public string NickName { get; set; }
+            public string Name { get; set; }
+            public string[] HiddenFolders { get; set; }
+        }
+    }
+}

+ 159 - 0
src/Masuit.MyBlogs.Core/Infrastructure/Drive/DriveService.cs

@@ -0,0 +1,159 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Masuit.MyBlogs.Core.Extensions.DriveHelpers;
+using Masuit.MyBlogs.Core.Models.Drive;
+using Microsoft.Graph;
+using Site = Masuit.MyBlogs.Core.Models.Drive.Site;
+
+namespace Masuit.MyBlogs.Core.Infrastructure.Drive
+{
+    public class DriveService : IDriveService
+    {
+        IDriveAccountService accountService;
+        GraphServiceClient graph;
+        DriveContext driveContext;
+        public DriveService(IDriveAccountService accountService, DriveContext driveContext)
+        {
+            this.accountService = accountService;
+            graph = accountService.Graph;
+            this.driveContext = driveContext;
+        }
+        /// <summary>
+        /// 获取根目录的所有项目
+        /// </summary>
+        /// <returns></returns>
+        public async Task<List<DriveFile>> GetRootItems(string siteName = "onedrive", bool showHiddenFolders = false)
+        {
+            string siteId = GetSiteId(siteName);
+            var drive = (siteName != "onedrive") ? graph.Sites[siteId].Drive : graph.Me.Drive;
+            var result = await drive.Root.Children.Request().GetAsync();
+            List<DriveFile> files = GetItems(result, siteName, showHiddenFolders);
+            return files;
+        }
+
+        /// <summary>
+        /// 根据路径获取文件夹下所有项目
+        /// </summary>
+        /// <param name="path"></param>
+        /// <returns></returns>
+        public async Task<List<DriveFile>> GetDriveItemsByPath(string path, string siteName = "onedrive", bool showHiddenFolders = false)
+        {
+            string siteId = GetSiteId(siteName);
+            var drive = (siteName != "onedrive") ? graph.Sites[siteId].Drive : graph.Me.Drive;
+            var result = await drive.Root.ItemWithPath(path).Children.Request().GetAsync();
+            List<DriveFile> files = GetItems(result, siteName, showHiddenFolders);
+            return files;
+        }
+        /// <summary>
+        /// 根据路径获取项目
+        /// </summary>
+        /// <param name="path"></param>
+        /// <returns></returns>
+        public async Task<DriveFile> GetDriveItemByPath(string path, string siteName = "onedrive")
+        {
+            string[] imgArray = { ".png", ".jpg", ".jpeg", ".bmp", ".webp" };
+            string extension = Path.GetExtension(path);
+            string siteId = GetSiteId(siteName);
+            var drive = (siteName != "onedrive") ? graph.Sites[siteId].Drive : graph.Me.Drive;
+            //这么写是因为:分块上传图片后直接获取会报错。
+            if (imgArray.Contains(extension))
+            {
+                await drive.Root.ItemWithPath(path).Thumbnails.Request().GetAsync();
+            }
+            var result = await drive.Root.ItemWithPath(path).Request().GetAsync();
+            DriveFile file = GetItem(result);
+            return file;
+        }
+
+        /// <summary>
+        /// 获得上传url
+        /// </summary>
+        /// <param name="path"></param>
+        /// <param name="siteName"></param>
+        /// <returns></returns>
+        public async Task<string> GetUploadUrl(string path, string siteName = "onedrive")
+        {
+            string siteId = GetSiteId(siteName);
+            var drive = siteName != "onedrive" ? graph.Sites[siteId].Drive : graph.Me.Drive;
+            string requestUrl = drive.Root.ItemWithPath(path).CreateUploadSession().Request().RequestUrl;
+            ProtectedApiCallHelper apiCallHelper = new ProtectedApiCallHelper(new HttpClient());
+            string uploadUrl = "";
+            await apiCallHelper.CallWebApiAndProcessResultASync(requestUrl, accountService.GetToken(), o =>
+            {
+                uploadUrl = o["uploadUrl"].ToString();
+            }, ProtectedApiCallHelper.Method.Post);
+            return uploadUrl;
+        }
+
+        #region PrivateMethod
+        private DriveFile GetItem(DriveItem result)
+        {
+            DriveFile file = new DriveFile()
+            {
+                CreatedTime = result.CreatedDateTime,
+                Name = result.Name,
+                Size = result.Size,
+                Id = result.Id
+            };
+            if (result.AdditionalData != null)
+            {
+                //可能是文件夹
+                if (result.AdditionalData.TryGetValue("@microsoft.graph.downloadUrl", out var downloadUrl))
+                    file.DownloadUrl = (string)downloadUrl;
+            }
+
+            return file;
+        }
+
+        private List<DriveFile> GetItems(IDriveItemChildrenCollectionPage result, string siteName = "onedrive", bool showHiddenFolders = false)
+        {
+            List<DriveFile> files = new List<DriveFile>();
+            string[] hiddenFolders = driveContext.Sites.Single(site => site.Name == siteName).HiddenFolders;
+            foreach (var item in result)
+            {
+                //要隐藏文件
+                if (!showHiddenFolders)
+                {
+                    //跳过隐藏的文件
+                    if (hiddenFolders != null)
+                    {
+                        if (hiddenFolders.Any(str => str == item.Name))
+                        {
+                            continue;
+                        }
+                    }
+                }
+                DriveFile file = new DriveFile()
+                {
+                    CreatedTime = item.CreatedDateTime,
+                    Name = item.Name,
+                    Size = item.Size,
+                    Id = item.Id
+                };
+                if (item.AdditionalData != null)
+                {
+                    //可能是文件夹
+                    if (item.AdditionalData.TryGetValue("@microsoft.graph.downloadUrl", out var downloadUrl))
+                        file.DownloadUrl = (string)downloadUrl;
+                }
+                files.Add(file);
+            }
+
+            return files;
+        }
+
+        /// <summary>
+        /// 根据名称返回siteid
+        /// </summary>
+        /// <returns></returns>
+        private string GetSiteId(string siteName)
+        {
+            Site site = driveContext.Sites.SingleOrDefault(s => s.Name == siteName);
+            return site?.SiteId;
+        }
+        #endregion
+    }
+}

+ 55 - 0
src/Masuit.MyBlogs.Core/Infrastructure/Drive/IDriveAccountService.cs

@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Masuit.MyBlogs.Core.Models.Drive;
+using static Masuit.MyBlogs.Core.Infrastructure.Drive.DriveAccountService;
+
+namespace Masuit.MyBlogs.Core.Infrastructure.Drive
+{
+    public interface IDriveAccountService
+    {
+
+        public DriveContext SiteContext { get; set; }
+        /// <summary>
+        /// 返回 Oauth 验证url
+        /// </summary>
+        /// <returns></returns>
+        public Task<string> GetAuthorizationRequestUrl();
+        /// <summary>
+        /// 添加 SharePoint Site-ID
+        /// </summary>
+        /// <param name="siteName"></param>
+        /// <param name="dominName"></param>
+        /// <returns></returns>
+        public Task AddSiteId(string siteName, string nickName);
+
+        /// <summary>
+        /// Graph实例
+        /// </summary>
+        /// <value></value>
+        public Microsoft.Graph.GraphServiceClient Graph { get; set; }
+        /// <summary>
+        /// 返回所有 sharepoint site
+        /// </summary>
+        /// <returns></returns>
+        public List<Site> GetSites();
+
+        /// <summary>
+        /// 获取驱动器信息
+        /// </summary>
+        /// <returns></returns>
+        public Task<List<DriveInfo>> GetDriveInfo();
+
+        /// <summary>
+        /// 解除绑定
+        /// </summary>
+        /// <param name="nickName"></param>
+        /// <returns></returns>
+        public Task Unbind(string nickName);
+
+        /// <summary>
+        /// 获得账户 Token
+        /// </summary>
+        /// <returns></returns>
+        public string GetToken();
+    }
+}

+ 14 - 0
src/Masuit.MyBlogs.Core/Infrastructure/Drive/IDriveService.cs

@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Masuit.MyBlogs.Core.Models.Drive;
+
+namespace Masuit.MyBlogs.Core.Infrastructure.Drive
+{
+    public interface IDriveService
+    {
+        public Task<List<DriveFile>> GetRootItems(string siteName, bool showHiddenFolders);
+        public Task<List<DriveFile>> GetDriveItemsByPath(string path, string siteName, bool showHiddenFolders);
+        public Task<DriveFile> GetDriveItemByPath(string path, string siteName);
+        public Task<string> GetUploadUrl(string path, string siteName = "onedrive");
+    }
+}

+ 70 - 0
src/Masuit.MyBlogs.Core/Infrastructure/Drive/SettingService.cs

@@ -0,0 +1,70 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Masuit.MyBlogs.Core.Models.Drive;
+
+namespace Masuit.MyBlogs.Core.Infrastructure.Drive
+{
+    public class SettingService : IDisposable
+    {
+        private readonly DriveContext _context;
+        public SettingService(DriveContext context)
+        {
+            _context = context;
+        }
+
+        /// <summary>
+        /// 获取设置
+        /// </summary>
+        /// <param name="key"></param>
+        /// <returns></returns>
+        public string Get(string key)
+        {
+            //判断是否存在
+            if (!_context.Settings.Any(setting => setting.Key == key))
+            {
+                return null;
+            }
+            var result = _context.Settings.SingleOrDefault(setting => setting.Key == key).Value;
+            return result;
+        }
+
+        /// <summary>
+        /// 创建或更新设置
+        /// </summary>
+        /// <param name="key"></param>
+        /// <param name="value"></param>
+        /// <returns></returns>
+        public async Task Set(string key, string value)
+        {
+            if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(key))
+            {
+                throw new Exception("键值对为空");
+            }
+            if (string.IsNullOrEmpty(value))
+                value = "";
+            //已经存在
+            if (_context.Settings.Any(setting => setting.Key == key))
+            {
+                Setting setting = _context.Settings.Single(setting => setting.Key == key);
+                setting.Value = value;
+                _context.Settings.Update(setting);
+            }
+            else
+            {
+                Setting setting = new Setting()
+                {
+                    Key = key,
+                    Value = value
+                };
+                await _context.Settings.AddAsync(setting);
+            }
+            await _context.SaveChangesAsync();
+        }
+
+        public void Dispose()
+        {
+            _context.Dispose();
+        }
+    }
+}

+ 93 - 0
src/Masuit.MyBlogs.Core/Infrastructure/Drive/TokenService.cs

@@ -0,0 +1,93 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Masuit.MyBlogs.Core.Extensions.DriveHelpers;
+using Microsoft.Graph.Auth;
+using Microsoft.Identity.Client;
+
+namespace Masuit.MyBlogs.Core.Infrastructure.Drive
+{
+    public class TokenService
+    {
+        public AuthorizationCodeProvider authProvider;
+
+        public AuthenticationResult authorizeResult;
+
+        /// <summary>
+        /// Graph实例
+        /// </summary>
+        /// <value></value>
+        public Microsoft.Graph.GraphServiceClient Graph { get; set; }
+
+        public IConfidentialClientApplication app;
+
+        public TokenService()
+        {
+            if (Configuration.Type == Configuration.OfficeType.China)
+            {
+                app = ConfidentialClientApplicationBuilder.Create(Configuration.ClientId).WithClientSecret(Configuration.ClientSecret).WithRedirectUri(Configuration.BaseUri + "/api/admin/bind/new").WithAuthority(AzureCloudInstance.AzureChina, "common").Build();
+            }
+            else
+            {
+                app = ConfidentialClientApplicationBuilder.Create(Configuration.ClientId).WithClientSecret(Configuration.ClientSecret).WithRedirectUri(Configuration.BaseUri + "/api/admin/bind/new").WithAuthority(AzureCloudInstance.AzurePublic, "common").Build();
+            }
+
+            //缓存Token
+            TokenCacheHelper.EnableSerialization(app.UserTokenCache);
+            //这里要传入一个 Scope 否则默认使用 https://graph.microsoft.com/.default
+            //而导致无法使用世纪互联版本
+            authProvider = new AuthorizationCodeProvider(app, Configuration.Scopes);
+            //获取Token
+            if (File.Exists(TokenCacheHelper.CacheFilePath))
+            {
+                authorizeResult = authProvider.ClientApplication.AcquireTokenSilent(Configuration.Scopes, Configuration.AccountName).ExecuteAsync().Result;
+                //Debug.WriteLine(authorizeResult.AccessToken);
+            }
+
+            //启用代理
+            if (!string.IsNullOrEmpty(Configuration.Proxy))
+            {
+                // Configure your proxy
+                var httpClientHandler = new HttpClientHandler
+                {
+                    Proxy = new WebProxy(Configuration.Proxy),
+                    UseDefaultCredentials = true
+                };
+                var httpProvider = new Microsoft.Graph.HttpProvider(httpClientHandler, false)
+                {
+                    OverallTimeout = TimeSpan.FromSeconds(10)
+                };
+                Graph = new Microsoft.Graph.GraphServiceClient($"{Configuration.GraphApi}/v1.0", authProvider, httpProvider);
+            }
+            else
+            {
+                Graph = new Microsoft.Graph.GraphServiceClient($"{Configuration.GraphApi}/v1.0", authProvider);
+            }
+
+            //定时更新Token
+            _ = new Timer(o =>
+              {
+                  if (File.Exists(TokenCacheHelper.CacheFilePath))
+                  {
+                      //TODO:自动刷新 Token 失效
+                      authorizeResult = authProvider.ClientApplication.AcquireTokenSilent(Configuration.Scopes, Configuration.AccountName).ExecuteAsync().Result;
+                  }
+              }, null, TimeSpan.FromSeconds(0), TimeSpan.FromHours(1));
+        }
+
+        /// <summary>
+        /// 验证
+        /// </summary>
+        /// <param name="code"></param>
+        /// <returns></returns>
+        public async Task<AuthenticationResult> Authorize(string code)
+        {
+            AuthorizationCodeProvider authorizationCodeProvider = new AuthorizationCodeProvider(app);
+            authorizeResult = await authorizationCodeProvider.ClientApplication.AcquireTokenByAuthorizationCode(Configuration.Scopes, code).ExecuteAsync();
+            return authorizeResult;
+        }
+    }
+}

+ 28 - 0
src/Masuit.MyBlogs.Core/Infrastructure/DriveContext.cs

@@ -0,0 +1,28 @@
+using Masuit.MyBlogs.Core.Extensions.DriveHelpers;
+using Masuit.MyBlogs.Core.Models.Drive;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using System;
+
+namespace Masuit.MyBlogs.Core.Infrastructure
+{
+    public class DriveContext : DbContext
+    {
+        public DbSet<Setting> Settings { get; set; }
+        public DbSet<Site> Sites { get; set; }
+
+        protected override void OnConfiguring(DbContextOptionsBuilder builder)
+        {
+            builder.UseSqlite(Configuration.ConnectionString);
+        }
+
+        protected override void OnModelCreating(ModelBuilder modelBuilder)
+        {
+            var converter = new ValueConverter<string[], string>(
+                model => string.Join(',', model),
+                data => data.Split(',', StringSplitOptions.None)
+            );
+            modelBuilder.Entity<Site>().Property("HiddenFolders").HasConversion(converter);
+        }
+    }
+}

+ 11 - 1
src/Masuit.MyBlogs.Core/Masuit.MyBlogs.Core.csproj

@@ -25,6 +25,13 @@
       <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     </PropertyGroup>
 
+    <ItemGroup>
+      <None Include="wwwroot\drive\js\app.8b25141a.js" />
+      <None Include="wwwroot\drive\js\app.8b25141a.js.map" />
+      <None Include="wwwroot\drive\js\chunk-vendors.b37660ae.js" />
+      <None Include="wwwroot\drive\js\chunk-vendors.b37660ae.js.map" />
+    </ItemGroup>
+
     <ItemGroup>
         <ProjectReference Include="..\..\..\Masuit.LuceneEFCore.SearchEngine\Masuit.LuceneEFCore.SearchEngine\Masuit.LuceneEFCore.SearchEngine.csproj" />
         <ProjectReference Include="..\..\..\Masuit.Tools\Masuit.Tools.Core\Masuit.Tools.Core.csproj" />
@@ -45,9 +52,12 @@
         <PackageReference Include="IP2Region" Version="1.2.0" />
         <PackageReference Include="Karambolo.AspNetCore.Bundling.NUglify" Version="3.4.1" />
         <PackageReference Include="MaxMind.GeoIP2" Version="4.0.1" />
+        <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0" />
         <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.0" />
         <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.0" />
         <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="5.0.0" />
+        <PackageReference Include="Microsoft.Graph" Version="3.20.0" />
+        <PackageReference Include="Microsoft.Graph.Auth" Version="1.0.0-preview.6" />
         <PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.2.1" />
         <PackageReference Include="OpenXmlPowerTools-NetStandard" Version="4.4.21" />
         <PackageReference Include="MiniProfiler.EntityFrameworkCore" Version="4.2.1" />
@@ -59,7 +69,7 @@
         <PackageReference Include="TimeZoneConverter" Version="3.3.0" />
         <PackageReference Include="WilderMinds.RssSyndication" Version="1.7.0" />
         <PackageReference Include="WinInsider.System.Net.Http.Formatting" Version="1.0.14" />
-        <PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="5.1.4" />
+        <PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="5.1.5" />
     </ItemGroup>
     <ItemGroup>
         <Content Update="appsettings.json">

+ 14 - 0
src/Masuit.MyBlogs.Core/Models/Drive/DriveFile.cs

@@ -0,0 +1,14 @@
+using System;
+
+namespace Masuit.MyBlogs.Core.Models.Drive
+{
+    public class DriveFile
+    {
+        public string Name { get; set; }
+        public string DownloadUrl { get; set; }
+        public long? Size { get; set; }
+        public DateTimeOffset? CreatedTime { get; set; }
+        public string Id { get; set; }
+    }
+    
+}

+ 14 - 0
src/Masuit.MyBlogs.Core/Models/Drive/ErrorResponse.cs

@@ -0,0 +1,14 @@
+namespace Masuit.MyBlogs.Core.Models.Drive
+{
+    /// <summary>
+    /// 返回模型
+    /// </summary>
+    public class ErrorResponse
+    {
+        /// <summary>
+        /// 信息
+        /// </summary>
+        /// <value></value>
+        public string message { get; set; }
+    }
+}

+ 13 - 0
src/Masuit.MyBlogs.Core/Models/Drive/Setting.cs

@@ -0,0 +1,13 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Masuit.MyBlogs.Core.Models.Drive
+{
+    public class Setting{
+        [Key]
+        public int id {get; set;}
+        [Required]
+        public string Key { get; set; }
+        [Required]
+        public string Value { get; set; }
+    }
+}

+ 14 - 0
src/Masuit.MyBlogs.Core/Models/Drive/Site.cs

@@ -0,0 +1,14 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Masuit.MyBlogs.Core.Models.Drive
+{
+    public class Site
+    {
+        [Key]
+        public int Id { get; set; }
+        public string Name { get; set; }
+        public string SiteId { get; set; }
+        public string NickName { get; set; }
+        public string[] HiddenFolders { get; set; }
+    }
+}

+ 23 - 0
src/Masuit.MyBlogs.Core/Program.cs

@@ -1,6 +1,8 @@
 using Autofac.Extensions.DependencyInjection;
 using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Hubs;
+using Masuit.MyBlogs.Core.Infrastructure;
+using Masuit.MyBlogs.Core.Infrastructure.Drive;
 using Masuit.Tools;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.Extensions.Caching.Memory;
@@ -8,7 +10,9 @@ using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using System;
+using System.IO;
 using Z.EntityFramework.Plus;
+using Configuration = Masuit.MyBlogs.Core.Extensions.DriveHelpers.Configuration;
 
 namespace Masuit.MyBlogs.Core
 {
@@ -25,6 +29,7 @@ namespace Masuit.MyBlogs.Core
             {
                 AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
             };
+            Init();
             MyHub.Init();
             CreateWebHostBuilder(args).Build().Run();
         }
@@ -43,5 +48,23 @@ namespace Masuit.MyBlogs.Core
             opt.Limits.MaxRequestBodySize = null;
             Console.WriteLine($"应用程序监听端口:http:{port},https:{sslport}");
         }).UseStartup<Startup>());
+
+        public static void Init()
+        {
+            //初始化
+            if (!File.Exists(Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "OneDrive.db")))
+            {
+                File.Copy("App_Data\\OneDrive.template.db", "App_Data\\OneDrive.db");
+                Console.WriteLine("数据库创建成功");
+            }
+
+            using SettingService settingService = new SettingService(new DriveContext());
+            if (settingService.Get("IsInit") != "true")
+            {
+                settingService.Set("IsInit", "true").Wait();
+                Console.WriteLine("数据初始化成功");
+                Console.WriteLine($"请登录 {Configuration.BaseUri}/#/admin 进行身份及其他配置");
+            }
+        }
     }
 }

+ 2 - 0
src/Masuit.MyBlogs.Core/Startup.cs

@@ -11,6 +11,7 @@ using Masuit.MyBlogs.Core.Common;
 using Masuit.MyBlogs.Core.Common.Mails;
 using Masuit.MyBlogs.Core.Configs;
 using Masuit.MyBlogs.Core.Extensions;
+using Masuit.MyBlogs.Core.Extensions.DriveHelpers;
 using Masuit.MyBlogs.Core.Extensions.Firewall;
 using Masuit.MyBlogs.Core.Extensions.Hangfire;
 using Masuit.MyBlogs.Core.Hubs;
@@ -141,6 +142,7 @@ namespace Masuit.MyBlogs.Core
                 options.PopupShowTimeWithChildren = true;
                 options.PopupShowTrivial = true;
             }).AddEntityFramework();
+            services.AddOneDrive();
             services.AddMapper().AddAutofac().AddMyMvc().Configure<ForwardedHeadersOptions>(options => // X-Forwarded-For
             {
                 options.ForwardLimit = null;

+ 24 - 0
src/Masuit.MyBlogs.Core/Views/Drive/Index.cshtml

@@ -0,0 +1,24 @@
+
+@{
+    Layout = null;
+}
+<!DOCTYPE html>
+<html lang="zh">
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="favicon.ico">
+    <title>懒勤网盘</title>
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@@mdi/font@latest/css/materialdesignicons.min.css">
+    <link href="https://cdn.jsdelivr.net/npm/@@mdi/[email protected]/css/materialdesignicons.min.css" rel="stylesheet">
+    <link href="/drive/css/chunk-vendors.39ab6aaa.css" rel="stylesheet">
+    <link href="/drive/css/app.377a312e.css" rel="stylesheet">
+</head>
+<body>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+    <script type="text/javascript" src="/drive/js/chunk-vendors.b37660ae.js"></script>
+    <script type="text/javascript" src="/drive/js/app.8b25141a.js"></script>
+</body>
+</html>

+ 19 - 1
src/Masuit.MyBlogs.Core/appsettings.json

@@ -60,5 +60,23 @@
             "AccessKeySecret": "AccessKeySecret",
             "BucketName": "BucketName"
         }
-    }
+    },
+    "OneDrive": {
+        /*获取 ClientId 与 ClientSecret
+        登录Azure:https://portal.azure.com/ 或 https://portal.azure.cn/ (世纪互联)
+        点击 Azure Active Directory / 应用注册 / 新注册
+        名字任意取,账户类型为 任何组织目录(任何 Azure AD 目录 - 多租户)中的帐户,重定向URL为 https://你的域名/api/admin/bind/new
+        点击 概述,记录应用程序(客户端)ID,即为 ClientId
+        点击 API 权限 / 添加权限 / Microsoft Graph / 委托的权限
+        勾选 Files.ReadWrite.All 和 Sites.ReadWrite.All
+        点击 证书和密码 / 新客户端密码,创建密码并记录为 ClientSecret*/
+        "ConnectionString": "Data Source=App_Data\\OneDrive.db;",
+        "Proxy": "",
+        "BaseUri": "https://masuit.com/drive",
+        "ClientId": "ClientId",
+        "ClientSecret": "ClientSecret",
+        "Type": "Global",
+        "AccountName": "[email protected]",
+        "DominName": "masuit-my.sharepoint.com"
+    } 
 }

+ 1 - 0
src/Masuit.MyBlogs.Core/wwwroot/drive/css/app.377a312e.css

@@ -0,0 +1 @@
+.footer p{margin-bottom:0!important}.readme img{width:100%}.float-btn{position:fixed;right:16px;bottom:16px;z-index:1}

文件差异内容过多而无法显示
+ 0 - 0
src/Masuit.MyBlogs.Core/wwwroot/drive/css/chunk-vendors.39ab6aaa.css


文件差异内容过多而无法显示
+ 0 - 0
src/Masuit.MyBlogs.Core/wwwroot/drive/js/app.8b25141a.js


文件差异内容过多而无法显示
+ 0 - 0
src/Masuit.MyBlogs.Core/wwwroot/drive/js/app.8b25141a.js.map


文件差异内容过多而无法显示
+ 0 - 0
src/Masuit.MyBlogs.Core/wwwroot/drive/js/chunk-vendors.b37660ae.js


文件差异内容过多而无法显示
+ 0 - 0
src/Masuit.MyBlogs.Core/wwwroot/drive/js/chunk-vendors.b37660ae.js.map


部分文件因为文件数量过多而无法显示