1
This commit is contained in:
59
Yes.Application/Admins/Themes/DeleteThemeCommandHandler.cs
Normal file
59
Yes.Application/Admins/Themes/DeleteThemeCommandHandler.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
namespace Yes.Application.Admins.Themes
|
||||
{
|
||||
public record DeleteThemeCommand(string ThemeName) : IRequest<DeleteThemeCommandResponse>;
|
||||
|
||||
public record DeleteThemeCommandResponse();
|
||||
public class DeleteThemeCommandHandler(
|
||||
IWebHostEnvironment env,
|
||||
BlogDbContext db,
|
||||
IIdentityContext identity,
|
||||
IOptionsMonitor<BlogSettings> options) : IRequestHandler<DeleteThemeCommand, DeleteThemeCommandResponse>
|
||||
{
|
||||
private readonly IWebHostEnvironment _env = env;
|
||||
private readonly BlogSettings _settings = options.CurrentValue;
|
||||
private readonly BlogDbContext _db = db;
|
||||
private readonly IIdentityContext _identity = identity;
|
||||
|
||||
public async Task<DeleteThemeCommandResponse> Handle(DeleteThemeCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var identityUser = await _db.Users.FindAsync(_identity.Id);
|
||||
if (identityUser == null)
|
||||
{
|
||||
throw new UserNotExistsException(_identity.Id);
|
||||
}
|
||||
|
||||
if (!identityUser.IsSystemUser())
|
||||
{
|
||||
throw new AccessDeniedException();
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var themeName = request.ThemeName;
|
||||
|
||||
if (_settings.Theme == themeName)
|
||||
{
|
||||
throw new ThemeInUseException();
|
||||
}
|
||||
|
||||
|
||||
var targetDirectory = Path.Combine(_env.ContentRootPath, "files", "themes", themeName);
|
||||
|
||||
|
||||
if (Directory.Exists(targetDirectory))
|
||||
Directory.Delete(targetDirectory, recursive: true);
|
||||
|
||||
return new DeleteThemeCommandResponse();
|
||||
}
|
||||
catch (Exception ex) when (ex is OverflowException)
|
||||
{
|
||||
throw new FileOverflowException();
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidDataException)
|
||||
{
|
||||
throw new FileNotSupportedException(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
54
Yes.Application/Admins/Themes/DownloadThemeCommandHandler.cs
Normal file
54
Yes.Application/Admins/Themes/DownloadThemeCommandHandler.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
namespace Yes.Application.Admins.Themes
|
||||
{
|
||||
public record DownloadThemeCommand(string ThemeName) : IRequest<DownloadThemeCommandResponse>;
|
||||
|
||||
public record DownloadThemeCommandResponse(string Filepath);
|
||||
public class DownloadThemeCommandHandler(
|
||||
IWebHostEnvironment env,
|
||||
BlogDbContext db,
|
||||
IIdentityContext identity,
|
||||
IOptionsMonitor<BlogSettings> options) : IRequestHandler<DownloadThemeCommand, DownloadThemeCommandResponse>
|
||||
{
|
||||
private readonly IWebHostEnvironment _env = env;
|
||||
private readonly BlogSettings _settings = options.CurrentValue;
|
||||
private readonly BlogDbContext _db = db;
|
||||
private readonly IIdentityContext _identity = identity;
|
||||
|
||||
public async Task<DownloadThemeCommandResponse> Handle(DownloadThemeCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var identityUser = await _db.Users.FindAsync(_identity.Id);
|
||||
if (identityUser == null)
|
||||
{
|
||||
throw new UserNotExistsException(_identity.Id);
|
||||
}
|
||||
|
||||
if (!identityUser.IsSystemUser())
|
||||
{
|
||||
throw new AccessDeniedException();
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var themeName = request.ThemeName;
|
||||
|
||||
|
||||
var targetDirectory = Path.Combine(_env.ContentRootPath, "files", "themes", themeName);
|
||||
var zipPath = Path.Combine(_env.ContentRootPath, "files", "themes", $"{themeName}.zip");
|
||||
|
||||
ZipHelper.CompressFolderToZip(targetDirectory, zipPath);
|
||||
|
||||
|
||||
return new DownloadThemeCommandResponse(zipPath);
|
||||
}
|
||||
catch (Exception ex) when (ex is OverflowException)
|
||||
{
|
||||
throw new FileOverflowException();
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidDataException)
|
||||
{
|
||||
throw new FileNotSupportedException(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@
|
||||
string Description,
|
||||
string Homepage,
|
||||
string DirName,
|
||||
bool IsInUse
|
||||
bool IsInUse,
|
||||
DateTime LastWriteTime
|
||||
);
|
||||
|
||||
public class GetThemesQueryHandler(IOptionsMonitor<BlogSettings> options, IMapper mapper, IWebHostEnvironment env) : IRequestHandler<GetThemesQuery, List<GetThemesQueryResponse>>
|
||||
@@ -32,11 +33,14 @@
|
||||
var jsonPath = Path.Combine(directory, "config.json");
|
||||
if(!File.Exists(jsonPath)) continue;
|
||||
|
||||
var lastWriteTime = File.GetCreationTime(jsonPath);
|
||||
|
||||
var json = File.ReadAllText(jsonPath);
|
||||
|
||||
var theme = JsonConvert.DeserializeObject<ThemeModel>(json);
|
||||
|
||||
theme.DirName = dir.Name;
|
||||
theme.LastWriteTime = lastWriteTime;
|
||||
if (theme.DirName == _settings.Theme)
|
||||
{
|
||||
theme.IsInUse = true;
|
||||
@@ -45,7 +49,7 @@
|
||||
themes.Add(_mapper.Map<GetThemesQueryResponse>(theme));
|
||||
}
|
||||
|
||||
themes = themes.OrderByDescending(x=>x.IsInUse).ToList();
|
||||
themes = themes.OrderByDescending(x=>x.IsInUse).ThenBy(x=>x.LastWriteTime).ToList();
|
||||
|
||||
return themes;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Yes.Application.Admins.Themes
|
||||
{
|
||||
public record UploadThemeCommand(IFormFile File) : IRequest<UploadThemeCommandResponse>;
|
||||
public record UploadThemeCommand(IFormFile File, string Theme) : IRequest<UploadThemeCommandResponse>;
|
||||
|
||||
public record UploadThemeCommandResponse(string DirName);
|
||||
public class UploadThemeCommandHandler(
|
||||
@@ -31,6 +31,12 @@
|
||||
try
|
||||
{
|
||||
var file = request.File;
|
||||
var theme = request.Theme;
|
||||
|
||||
if(string.IsNullOrWhiteSpace(theme))
|
||||
{
|
||||
theme = Guid.NewGuid().ToString().ToLower();
|
||||
}
|
||||
|
||||
var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
var allowedExts = _settings.AllowedUploadExts;
|
||||
@@ -81,14 +87,7 @@
|
||||
|
||||
var targetDirectory = Path.Combine(_env.ContentRootPath, "files", "themes");
|
||||
|
||||
ZipHelper.ExtractToDirectory(filePath, targetDirectory, newDirName);
|
||||
|
||||
|
||||
string targetFilePath = Path.Combine(targetDirectory, newDirName, "config.json");
|
||||
if (!File.Exists(targetFilePath))
|
||||
{
|
||||
throw new InvalidDataException("缺少config.json文件");
|
||||
}
|
||||
ZipHelper.ExtractAndProcessZip(filePath, targetDirectory, theme);
|
||||
|
||||
return new UploadThemeCommandResponse(newDirName);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@using Yes.Domain.Core.Helpers
|
||||
<footer>
|
||||
<footer>
|
||||
<hr>
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
@page
|
||||
@model Yes.Blog.Areas.Admin.Pages.Theme.ChangeModel
|
||||
@{
|
||||
}
|
||||
@@ -35,13 +35,21 @@
|
||||
<div :class="item.isInUse ? 'panel-body bg-success' : 'panel-body'">
|
||||
<img :src="`/themes/${item.dirName}/screenshot.png`" :alt="item.name" class="panel-image">
|
||||
<div class="panel-content">
|
||||
<h3 x-text="item.name"></h3>
|
||||
|
||||
<h3><span x-text="item.name"></span><span x-text="` `+item.dirName" class="small-text"></span></h3>
|
||||
<p>版本:<span x-text="item.version"></span></p>
|
||||
<p>作者:<span x-text="item.author"></span></p>
|
||||
<p>简介:<span x-text="item.description"></span></p>
|
||||
<p>主页:<span x-text="item.homepage"></span></p>
|
||||
<a class="activate" href="javascript:;" x-show="!item.isInUse" x-on:click="showModal = true;this.theme=item.dirName;">切换到该主题</a>
|
||||
<a class="activate" href="/admin/theme/detail" x-show="item.isInUse">自定义模板内容</a>
|
||||
<p>更新日期:<span x-text="friendlyTime(item.lastWriteTime)"></span></p>
|
||||
<a class="activate btn btn-default btn-sm" :href="`/admin/theme/upgrade?theme=${item.dirName}`"><i class="fa fa-upload"></i>更新主题</a>
|
||||
<a class="activate btn btn-default btn-sm" href="/admin/theme/detail" x-show="item.isInUse"><i class="fa fa-code"></i>自定义模板内容</a>
|
||||
|
||||
<a class="activate btn btn-default btn-sm" :href="`/admin/api/themes/${item.dirName}/download`" target="_blank"><i class="fa fa-download"></i>导出</a>
|
||||
|
||||
<a class="activate btn btn-default btn-sm" href="javascript:;" x-show="!item.isInUse" x-on:click="this.theme=item.dirName;editTheme()"><i class="fa fa-exchange"></i>切换到该主题</a>
|
||||
|
||||
<a class="activate btn btn-default btn-sm" href="javascript:;" x-show="!item.isInUse" x-on:click="showModal = true;this.theme=item.dirName;"><i class="fa fa-trash"></i>删除该主题</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,13 +68,13 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="error-text">
|
||||
<i class="fa fa-warning modal-icon"></i>确认要切换到该主题吗?
|
||||
<i class="fa fa-warning modal-icon"></i>确认要删除该主题吗?
|
||||
<br>操作后不可恢复.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button x-on:click="showModal = false" class="btn btn-default" data-dismiss="modal">取消</button>
|
||||
<button x-on:click="showModal = false; confirmAction()" class="btn btn-danger" data-dismiss="modal">确认</button>
|
||||
<button x-on:click="showModal = false; confirmDelete()" class="btn btn-danger" data-dismiss="modal">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
82
Yes.Blog/Areas/Admin/Pages/Theme/Upgrade.cshtml
Normal file
82
Yes.Blog/Areas/Admin/Pages/Theme/Upgrade.cshtml
Normal file
@@ -0,0 +1,82 @@
|
||||
@page
|
||||
@model Yes.Blog.Areas.Admin.Pages.Theme.UpgradeModel
|
||||
@{
|
||||
}
|
||||
|
||||
|
||||
<template x-data x-if="$store.errorMessage||1">
|
||||
<div class="alert alert-danger floating-div" x-show="$store.errorMessage" x-transition x-html="$store.errorMessage"></div>
|
||||
</template>
|
||||
|
||||
<div class="header">
|
||||
<div class="stats">
|
||||
</div>
|
||||
|
||||
|
||||
<h1 class="page-title">更新模板</h1>
|
||||
<ul class="breadcrumb">
|
||||
<li>
|
||||
<a asp-page="/Index">管理中心</a>
|
||||
</li>
|
||||
<li>
|
||||
<a asp-page="/Theme/Index">模板列表</a>
|
||||
</li>
|
||||
<li class="active">更新模板</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="main-content" x-data="data">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
|
||||
<div class="form-group">
|
||||
<label>选择要导入的模板文件</label>
|
||||
<div id="img-container">
|
||||
<template x-if="uploadFile">
|
||||
<div class="img-thumb img-item">
|
||||
<img class="thumb-icon" src="/admin/images/zip.png" />
|
||||
<a href="javascript:;" class="img-remove" x-on:click="uploadFile='';tempfile=null">x</a>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!uploadFile">
|
||||
<div class="img-up-add img-item">
|
||||
<span class="img-add-icon" x-on:click="addFile()">+</span>
|
||||
</div>
|
||||
</template>
|
||||
<input type="hidden" x-model="uploadFile">
|
||||
<input type="file" name="files" id="img-file-input" x-model="tempfile" x-on:change="handleThemeUpload($event.target.files[0])">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="btn-toolbar list-toolbar">
|
||||
<button class="btn btn-primary" x-on:click="save()"><i class="fa fa-save"></i> 提交</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<partial name="_Footer" />
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="~/admin/js/qiniu.min.js" asp-append-version="true"></script>
|
||||
<script src="~/admin/js/theme/upgrade.js" asp-append-version="true"></script>
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Yes.Blog.Areas.Admin.Pages.Theme
|
||||
{
|
||||
public class ChangeModel : BasePageModel
|
||||
public class UpgradeModel : BasePageModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
@using Yes.Blog
|
||||
@using Yes.Domain.Core.Helpers
|
||||
@namespace Yes.Blog.Areas.Admin.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Yes.Infrastructure.Authorizations;
|
||||
|
||||
namespace Yes.Blog.Endpoints.Admins.Auths
|
||||
namespace Yes.Blog.Endpoints.Admins.Auths
|
||||
{
|
||||
public class UserLoginEndpoint : AuthEndpointScheme, IEndpoint
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Yes.Infrastructure.Authorizations;
|
||||
|
||||
namespace Yes.Blog.Endpoints.Admins.Auths
|
||||
namespace Yes.Blog.Endpoints.Admins.Auths
|
||||
{
|
||||
public class ValidateEndpoint : AuthEndpointScheme, IEndpoint
|
||||
{
|
||||
|
||||
22
Yes.Blog/Endpoints/Admins/Themes/DeleteThemeEndpoint.cs
Normal file
22
Yes.Blog/Endpoints/Admins/Themes/DeleteThemeEndpoint.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace Yes.Blog.Endpoints.Admins.Themes
|
||||
{
|
||||
public class DeleteThemeEndpoint : ThemeEndpointScheme, IEndpoint
|
||||
{
|
||||
public void Map(IEndpointRouteBuilder app) => app.MapDelete("/themes", Handle);
|
||||
|
||||
public record Request([FromQuery(Name = "theme")] string ThemeName);
|
||||
|
||||
|
||||
private async Task<IResult> Handle(
|
||||
[AsParameters] Request request,
|
||||
IMediator mediator, IMapper mapper, CancellationToken cancellationToken)
|
||||
{
|
||||
var command = new DeleteThemeCommand(request.ThemeName);
|
||||
var response = await mediator.Send(command, cancellationToken);
|
||||
|
||||
return Result.Ok();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
25
Yes.Blog/Endpoints/Admins/Themes/DownloadThemeEndpoint.cs
Normal file
25
Yes.Blog/Endpoints/Admins/Themes/DownloadThemeEndpoint.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace Yes.Blog.Endpoints.Admins.Themes
|
||||
{
|
||||
public class DownloadThemeEndpoint : ThemeEndpointScheme, IEndpoint
|
||||
{
|
||||
public void Map(IEndpointRouteBuilder app) => app.MapGet("/themes/{themeName}/download", Handle);
|
||||
|
||||
public record Request([FromRoute(Name = "themeName")] string ThemeName);
|
||||
|
||||
|
||||
private async Task<Microsoft.AspNetCore.Http.IResult> Handle(
|
||||
[AsParameters] Request request,
|
||||
IMediator mediator, IMapper mapper, CancellationToken cancellationToken)
|
||||
{
|
||||
var command = new DownloadThemeCommand(request.ThemeName);
|
||||
var response = await mediator.Send(command, cancellationToken);
|
||||
|
||||
string contentType = "application/zip";
|
||||
string fileDownloadName = Path.GetFileName(response.Filepath);
|
||||
|
||||
return Results.File(response.Filepath, contentType, fileDownloadName);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,11 +4,11 @@
|
||||
{
|
||||
public void Map(IEndpointRouteBuilder app) => app.MapPut("/themes", Handle).WithRequestValidation<Request>();
|
||||
|
||||
internal record Request(
|
||||
public record Request(
|
||||
string Theme
|
||||
);
|
||||
|
||||
internal class RequestValidator : AbstractValidator<Request>
|
||||
public class RequestValidator : AbstractValidator<Request>
|
||||
{
|
||||
public RequestValidator()
|
||||
{
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
internal class UpdateThemeMappingProfile : IRegister
|
||||
public class UpdateThemeMappingProfile : IRegister
|
||||
{
|
||||
public void Register(TypeAdapterConfig config)
|
||||
{
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
{
|
||||
public void Map(IEndpointRouteBuilder app) => app.MapPost("/themes/upload", Handle).DisableAntiforgery();
|
||||
|
||||
|
||||
private async Task<IResult> Handle(IFormFile File, IMediator mediator, IMapper mapper, CancellationToken cancellationToken)
|
||||
internal record Request(
|
||||
string? Theme
|
||||
);
|
||||
private async Task<IResult> Handle([AsParameters] Request request, IFormFile File, IMediator mediator, IMapper mapper, CancellationToken cancellationToken)
|
||||
{
|
||||
var command = new UploadThemeCommand(File);
|
||||
var command = new UploadThemeCommand(File, request.Theme??"");
|
||||
var response = await mediator.Send(command, cancellationToken);
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
|
||||
using Hangfire;
|
||||
|
||||
namespace Yes.Blog
|
||||
{
|
||||
public class Program
|
||||
|
||||
@@ -1065,7 +1065,7 @@ tag {
|
||||
|
||||
.theme-box .panel-image {
|
||||
max-height: 200px;
|
||||
margin: 10px 20px 10px 10px;
|
||||
margin: 20px 20px 20px 10px;
|
||||
}
|
||||
|
||||
.theme-box .panel-content {
|
||||
@@ -1095,3 +1095,8 @@ tag {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.small-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
});
|
||||
},
|
||||
showModal: false,
|
||||
confirmAction() {
|
||||
editTheme() {
|
||||
this.isSubmitting = true;
|
||||
axios.put('themes', { theme: theme })
|
||||
.then(data => {
|
||||
@@ -28,6 +28,16 @@
|
||||
this.isSubmitting = false;
|
||||
});
|
||||
},
|
||||
confirmDelete() {
|
||||
this.isSubmitting = true;
|
||||
axios.delete('themes?theme=' + theme)
|
||||
.then(data => {
|
||||
this.loadItems();
|
||||
})
|
||||
.finally(() => {
|
||||
this.isSubmitting = false;
|
||||
});
|
||||
},
|
||||
init() {
|
||||
this.loadItems();
|
||||
|
||||
|
||||
57
Yes.Blog/wwwroot/admin/js/theme/upgrade.js
Normal file
57
Yes.Blog/wwwroot/admin/js/theme/upgrade.js
Normal file
@@ -0,0 +1,57 @@
|
||||
document.addEventListener('alpine:init', function () {
|
||||
Alpine.data('data', function () {
|
||||
|
||||
const themeName = getUrlParameter("theme") || "default";
|
||||
|
||||
return {
|
||||
file: {
|
||||
|
||||
},
|
||||
tagInput: "",
|
||||
tempfile: null,
|
||||
coverUrl: "",
|
||||
files: [],
|
||||
isSubmitting: false,
|
||||
themeName: "",
|
||||
fileName: "",
|
||||
uploadFile:"",
|
||||
isZipFile(filename) {
|
||||
if (typeof filename !== 'string') return false;
|
||||
return /\.zip$/i.test(filename);
|
||||
},
|
||||
handleThemeUpload(file) {
|
||||
if (!this.isZipFile(file.name)) {
|
||||
window.pushError("请上传zip文件");
|
||||
return;
|
||||
}
|
||||
this.uploadFile = file;
|
||||
},
|
||||
save() {
|
||||
const formData = new FormData();
|
||||
formData.append('file', this.uploadFile);
|
||||
|
||||
axios.post('/themes/upload?theme=' + this.themeName, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
location.href = "/admin/theme";
|
||||
})
|
||||
},
|
||||
addFile() {
|
||||
const fileinput = document.getElementById('img-file-input');
|
||||
if (fileinput) {
|
||||
fileinput.click();
|
||||
}
|
||||
},
|
||||
init() {
|
||||
this.themeName = themeName;
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
document.title = "自定义模板内容 - " + window.config.name + "";
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"id": "a4c8e6a8-b128-45cc-84bc-2de58f05d035",
|
||||
"name": "Default Theme",
|
||||
"version": "v1.0.0",
|
||||
"author": "Hyrule",
|
||||
|
||||
13
Yes.Domain/Core/Exceptions/ThemeInUseException.cs
Normal file
13
Yes.Domain/Core/Exceptions/ThemeInUseException.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Yes.Domain.Core.Exceptions
|
||||
{
|
||||
|
||||
public class ThemeInUseException : BaseException
|
||||
{
|
||||
|
||||
|
||||
public ThemeInUseException() : base($"模板正在使用,不能删除!")
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,7 @@
|
||||
public string DirName { get; set; }
|
||||
|
||||
public bool IsInUse { get; set; }
|
||||
|
||||
public DateTime LastWriteTime { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,11 @@ global using System.Net;
|
||||
global using System.Runtime.Serialization;
|
||||
global using System.Security.Authentication;
|
||||
global using System.Security.Claims;
|
||||
global using System.Security.Cryptography;
|
||||
global using System.Text;
|
||||
global using System.Text.RegularExpressions;
|
||||
global using System.Web;
|
||||
global using System.Xml.Linq;
|
||||
global using Dapper;
|
||||
global using FluentMigrator;
|
||||
global using FluentMigrator.Runner;
|
||||
@@ -47,9 +50,6 @@ global using Yes.Infrastructure.Authorizations.Identity;
|
||||
global using Yes.Infrastructure.Authorizations.Identity.Authroizations;
|
||||
global using Yes.Infrastructure.Authorizations.Identity.Context;
|
||||
global using Yes.Infrastructure.Data.DbContexts;
|
||||
global using Yes.Infrastructure.Migrator.Migrations;
|
||||
global using System.Security;
|
||||
global using System.Security.Cryptography;
|
||||
global using Yes.Infrastructure.Helpers;
|
||||
global using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
global using System.Xml.Linq;
|
||||
global using Yes.Infrastructure.Migrator.Migrations;
|
||||
global using static ICSharpCode.SharpZipLib.Zip.Compression.Deflater;
|
||||
|
||||
@@ -1,7 +1,91 @@
|
||||
namespace Yes.Infrastructure.Helpers
|
||||
|
||||
|
||||
namespace Yes.Infrastructure.Helpers
|
||||
{
|
||||
public class ZipHelper
|
||||
{
|
||||
public static void ExtractAndProcessZip(string zipFilePath, string targetDirectory, string folderName)
|
||||
{
|
||||
#region 参数验证
|
||||
if (string.IsNullOrEmpty(zipFilePath))
|
||||
throw new ArgumentException("ZIP 文件路径不能为空。", nameof(zipFilePath));
|
||||
|
||||
if (string.IsNullOrEmpty(targetDirectory))
|
||||
throw new ArgumentException("目标目录路径不能为空。", nameof(targetDirectory));
|
||||
|
||||
if (!File.Exists(zipFilePath))
|
||||
throw new FileNotFoundException("指定的 ZIP 文件不存在。", zipFilePath);
|
||||
|
||||
if (!Directory.Exists(targetDirectory))
|
||||
Directory.CreateDirectory(targetDirectory);
|
||||
#endregion
|
||||
|
||||
|
||||
string tempExtractPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
Directory.CreateDirectory(tempExtractPath);
|
||||
try
|
||||
{
|
||||
// 解压到临时目录
|
||||
System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempExtractPath, overwriteFiles: true);
|
||||
|
||||
// 找到实际文件目录(跳过单层空文件夹)
|
||||
string contentRoot = GetContentRootDirectory(tempExtractPath);
|
||||
|
||||
|
||||
string targetFolderPath = Path.Combine(targetDirectory, folderName);
|
||||
|
||||
|
||||
#region 创建目标文件夹并移动文件
|
||||
Directory.CreateDirectory(targetFolderPath);
|
||||
foreach (var file in Directory.GetFiles(contentRoot))
|
||||
{
|
||||
string destFile = Path.Combine(targetFolderPath, Path.GetFileName(file));
|
||||
File.Move(file, destFile, true);
|
||||
}
|
||||
|
||||
foreach (var dir in Directory.GetDirectories(contentRoot))
|
||||
{
|
||||
string destDir = Path.Combine(targetFolderPath, Path.GetFileName(dir));
|
||||
|
||||
// 若目标目录存在,先删除
|
||||
if (Directory.Exists(destDir))
|
||||
{
|
||||
Directory.Delete(destDir, recursive: true);
|
||||
}
|
||||
|
||||
Directory.Move(dir, destDir);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 清理临时目录
|
||||
if (Directory.Exists(tempExtractPath))
|
||||
Directory.Delete(tempExtractPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归查找实际内容目录(跳过仅包含单个文件夹的层级)
|
||||
/// </summary>
|
||||
private static string GetContentRootDirectory(string startPath)
|
||||
{
|
||||
string currentPath = startPath;
|
||||
|
||||
while (true)
|
||||
{
|
||||
string[] subDirs = Directory.GetDirectories(currentPath);
|
||||
string[] files = Directory.GetFiles(currentPath);
|
||||
|
||||
if (files.Length > 0 || subDirs.Length == 0)
|
||||
return currentPath;
|
||||
|
||||
if (subDirs.Length == 1)
|
||||
currentPath = subDirs[0];
|
||||
else
|
||||
return currentPath; // 多个子目录,停止递归
|
||||
}
|
||||
}
|
||||
public static void ExtractToDirectory(string zipPath, string targetDirectory, string themeName)
|
||||
{
|
||||
if (!Directory.Exists(targetDirectory))
|
||||
@@ -100,24 +184,91 @@
|
||||
|
||||
}
|
||||
|
||||
private static string SanitizeEntryPath(string entryPath, string extractRoot)
|
||||
{
|
||||
// 路径安全处理
|
||||
string fullPath = Path.GetFullPath(Path.Combine(extractRoot, entryPath));
|
||||
|
||||
// 验证是否在解压根目录内(防止路径遍历)
|
||||
if (!fullPath.StartsWith(Path.GetFullPath(extractRoot)))
|
||||
{
|
||||
throw new SecurityException("非法路径访问!");
|
||||
}
|
||||
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
private static bool IsSafeFile(string fileName)
|
||||
{
|
||||
var ext = Path.GetExtension(fileName).ToLower();
|
||||
return new[] { ".liquid", ".js", ".css", ".html", ".md", ".ttf", ".svg", ".eot", ".woff", ".woff2", ".toml", ".xml" }.Contains(ext);
|
||||
}
|
||||
|
||||
private static string GenerateSafeFolderName(string input, int maxLength = 200, char replaceSpaceWith = '_')
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
return "UnnamedFolder";
|
||||
|
||||
// 定义所有非法字符(Windows + Linux)
|
||||
string invalidChars = Regex.Escape(new string(Path.GetInvalidFileNameChars()) + "<>:\"|?*");
|
||||
Regex invalidCharRegex = new Regex($"[{invalidChars}]");
|
||||
|
||||
// 替换非法字符为空格
|
||||
string sanitized = invalidCharRegex.Replace(input, " ");
|
||||
|
||||
// 替换连续空格为单个空格
|
||||
sanitized = Regex.Replace(sanitized, @"\s+", " ").Trim();
|
||||
|
||||
// 替换空格为指定字符
|
||||
sanitized = sanitized.Replace(" ", replaceSpaceWith.ToString());
|
||||
|
||||
// 处理保留名称(Windows)
|
||||
string[] reservedNames = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" };
|
||||
if (Array.Exists(reservedNames, name => name.Equals(sanitized, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
sanitized += "_safe";
|
||||
}
|
||||
|
||||
// 截断到最大长度
|
||||
if (sanitized.Length > maxLength)
|
||||
{
|
||||
sanitized = sanitized.Substring(0, maxLength);
|
||||
}
|
||||
|
||||
// 如果最终结果为空,返回默认值
|
||||
if (string.IsNullOrEmpty(sanitized))
|
||||
{
|
||||
sanitized = "UnnamedFolder";
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 将指定路径的文件夹打包为 ZIP 文件
|
||||
/// </summary>
|
||||
/// <param name="sourceFolderPath">要压缩的文件夹路径</param>
|
||||
/// <param name="destinationZipPath">生成的 ZIP 文件路径</param>
|
||||
/// <param name="compressionLevel">压缩级别</param>
|
||||
/// <param name="overwrite">是否覆盖已存在的 ZIP 文件</param>
|
||||
public static void CompressFolderToZip(
|
||||
string sourceFolderPath,
|
||||
string destinationZipPath,
|
||||
CompressionLevel compressionLevel = CompressionLevel.BEST_SPEED,
|
||||
bool overwrite = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sourceFolderPath))
|
||||
throw new ArgumentException("源文件夹路径不能为空。", nameof(sourceFolderPath));
|
||||
|
||||
if (string.IsNullOrEmpty(destinationZipPath))
|
||||
throw new ArgumentException("目标 ZIP 文件路径不能为空。", nameof(destinationZipPath));
|
||||
|
||||
if (!Directory.Exists(sourceFolderPath))
|
||||
throw new DirectoryNotFoundException($"源文件夹 '{sourceFolderPath}' 不存在。");
|
||||
|
||||
var destinationDirectory = Path.GetDirectoryName(destinationZipPath);
|
||||
if (!Directory.Exists(destinationDirectory))
|
||||
Directory.CreateDirectory(destinationDirectory);
|
||||
|
||||
if (File.Exists(destinationZipPath) && overwrite)
|
||||
File.Delete(destinationZipPath);
|
||||
|
||||
try
|
||||
{
|
||||
System.IO.Compression.ZipFile.CreateFromDirectory(sourceFolderPath,destinationZipPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new IOException($"压缩文件夹失败:{ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user