Skip to content

Commit 9867580

Browse files
committed
AquaMai 拖放安装
1 parent 2fd4a0c commit 9867580

File tree

8 files changed

+244
-2
lines changed

8 files changed

+244
-2
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using MaiChartManager.Utils;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Mono.Cecil;
4+
using PeNet;
5+
6+
namespace MaiChartManager.Controllers.Mod;
7+
8+
[ApiController]
9+
[Route("MaiChartManagerServlet/[action]Api")]
10+
public class ManualInstallController(StaticSettings settings, ILogger<InstallationController> logger) : ControllerBase
11+
{
12+
public record CheckAquaMaiFileResult(bool isValid, string? version = null, AquaMaiSignatureV2.VerifyResult? signature = null, string? buildDate = null);
13+
14+
[HttpPost]
15+
public CheckAquaMaiFileResult CheckAquaMaiFile(IFormFile file)
16+
{
17+
using var ms = new MemoryStream();
18+
file.CopyTo(ms);
19+
try
20+
{
21+
var bytes = ms.ToArray();
22+
var pe = new PeFile(bytes);
23+
if (!pe.IsDotNet) return new CheckAquaMaiFileResult(false);
24+
var table = pe.Resources?.VsVersionInfo?.StringFileInfo.StringTable.FirstOrDefault(it => it.ProductName != null);
25+
if (table?.ProductName != "AquaMai")
26+
{
27+
return new CheckAquaMaiFileResult(false);
28+
}
29+
var version = table.FileVersion;
30+
31+
var signature = AquaMaiSignatureV2.VerifySignature(bytes);
32+
string? buildDate = null;
33+
34+
ms.Seek(0, SeekOrigin.Begin);
35+
var cecil = AssemblyDefinition.ReadAssembly(ms);
36+
if (cecil != null)
37+
{
38+
var clas = GetTypeDefinition(cecil, "AquaMai", "BuildInfo");
39+
var field = clas?.Fields.FirstOrDefault(f => f.Name == "BuildDate");
40+
if (field is { HasConstant: true })
41+
{
42+
buildDate = field.Constant as string;
43+
}
44+
}
45+
46+
return new CheckAquaMaiFileResult(true, version, signature, buildDate);
47+
}
48+
catch (Exception ex)
49+
{
50+
logger.LogError(ex, "Error in CheckAquaMaiFile");
51+
return new CheckAquaMaiFileResult(false);
52+
}
53+
}
54+
55+
[HttpPost]
56+
public void InstallAquaMaiFile(IFormFile file)
57+
{
58+
using var fs = System.IO.File.Open(ModPaths.AquaMaiDllInstalledPath, FileMode.Create);
59+
file.CopyTo(fs);
60+
}
61+
62+
[NonAction]
63+
private static TypeDefinition? GetTypeDefinition(AssemblyDefinition assemblyDefinition, string nameSpace, string className)
64+
{
65+
// 遍历程序集中的所有模块
66+
foreach (var module in assemblyDefinition.Modules)
67+
{
68+
// 遍历模块中的所有类型
69+
foreach (var type in module.Types)
70+
{
71+
if (type.Name == className && type.Namespace == nameSpace)
72+
{
73+
return type;
74+
}
75+
}
76+
}
77+
78+
return null;
79+
}
80+
}

MaiChartManager/Front/src/client/apiGen.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ export interface Chart {
5757
problems?: string[] | null;
5858
}
5959

60+
export interface CheckAquaMaiFileResult {
61+
isValid?: boolean;
62+
version?: string | null;
63+
signature?: VerifyResult;
64+
buildDate?: string | null;
65+
}
66+
6067
export interface CheckConflictEntry {
6168
type?: AssetType;
6269
upperDir?: string | null;
@@ -1466,6 +1473,51 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
14661473
...params,
14671474
}),
14681475

1476+
/**
1477+
* No description
1478+
*
1479+
* @tags ManualInstall
1480+
* @name CheckAquaMaiFile
1481+
* @request POST:/MaiChartManagerServlet/CheckAquaMaiFileApi
1482+
*/
1483+
CheckAquaMaiFile: (
1484+
data: {
1485+
/** @format binary */
1486+
file?: File;
1487+
},
1488+
params: RequestParams = {},
1489+
) =>
1490+
this.request<CheckAquaMaiFileResult, any>({
1491+
path: `/MaiChartManagerServlet/CheckAquaMaiFileApi`,
1492+
method: "POST",
1493+
body: data,
1494+
type: ContentType.FormData,
1495+
format: "json",
1496+
...params,
1497+
}),
1498+
1499+
/**
1500+
* No description
1501+
*
1502+
* @tags ManualInstall
1503+
* @name InstallAquaMaiFile
1504+
* @request POST:/MaiChartManagerServlet/InstallAquaMaiFileApi
1505+
*/
1506+
InstallAquaMaiFile: (
1507+
data: {
1508+
/** @format binary */
1509+
file?: File;
1510+
},
1511+
params: RequestParams = {},
1512+
) =>
1513+
this.request<void, any>({
1514+
path: `/MaiChartManagerServlet/InstallAquaMaiFileApi`,
1515+
method: "POST",
1516+
body: data,
1517+
type: ContentType.FormData,
1518+
...params,
1519+
}),
1520+
14691521
/**
14701522
* No description
14711523
*
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import api from '@/client/api';
2+
import { CheckAquaMaiFileResult, PubKeyId, VerifyStatus } from '@/client/apiGen';
3+
import { t } from '@/locales';
4+
import { globalCapture, updateModInfo } from '@/store/refs';
5+
import { NButton, NFlex, NModal, NTime, useMessage } from 'naive-ui';
6+
import { defineComponent, PropType, ref, computed, watch } from 'vue';
7+
import { updateAquaMaiConfig } from '../ModManager/ConfigEditor';
8+
9+
const currentFile = ref<File>();
10+
const checkResult = ref<CheckAquaMaiFileResult>();
11+
12+
export const setManualInstallAquaMai = async (file: FileSystemFileHandle) => {
13+
checkResult.value = undefined;
14+
try {
15+
const f = await file.getFile();
16+
const res = await api.CheckAquaMaiFile({ file: f });
17+
checkResult.value = res.data;
18+
if (checkResult.value?.isValid)
19+
currentFile.value = f;
20+
}
21+
catch (error) {
22+
globalCapture(error, t('mod.manualInstall.checkFailed'));
23+
}
24+
}
25+
26+
export default defineComponent({
27+
// props: {
28+
// },
29+
setup(props, { emit }) {
30+
const message = useMessage();
31+
32+
const installAquaMai = async () => {
33+
if (!currentFile.value) return;
34+
try {
35+
await api.InstallAquaMaiFile({ file: currentFile.value });
36+
currentFile.value = undefined;
37+
message.success(t('mod.manualInstall.installSuccess'));
38+
await updateModInfo();
39+
await updateAquaMaiConfig();
40+
}
41+
catch (error) {
42+
globalCapture(error, t('mod.manualInstall.installFailed'));
43+
}
44+
}
45+
46+
47+
return () => <>
48+
<NModal
49+
preset="card"
50+
class="w-[min(90vw,50em)]"
51+
title={t('mod.manualInstall.invalidAquaMaiFile')}
52+
show={checkResult.value?.isValid === false}
53+
onUpdateShow={() => checkResult.value = undefined}
54+
>
55+
{t('mod.manualInstall.invalidAquaMaiFileMessage')}
56+
</NModal>
57+
<NModal
58+
preset="card"
59+
class="w-[min(90vw,50em)]"
60+
title={t('mod.manualInstall.confirmInstallTitle')}
61+
show={currentFile.value !== undefined}
62+
onUpdateShow={() => currentFile.value = undefined}
63+
>{{
64+
default: () => <div class="flex flex-col gap-2 items-center">
65+
<div class="flex gap-2 items-center">
66+
{checkResult.value?.signature?.status === VerifyStatus.Valid ?
67+
<div class="text-green-5 i-tabler:certificate text-2em" />
68+
: <div class="text-red-5 i-tabler:certificate-off text-2em" />}
69+
{checkResult.value?.signature?.status === VerifyStatus.Valid && checkResult.value.signature?.keyId === PubKeyId.Local &&
70+
<div class="text-green-6">{t('mod.signature.verifiedOfficial')}</div>}
71+
{checkResult.value?.signature?.status === VerifyStatus.Valid && checkResult.value.signature?.keyId === PubKeyId.CI &&
72+
<div class="text-green-6">{t('mod.signature.verifiedCI')}</div>}
73+
{checkResult.value?.signature?.status === VerifyStatus.NotFound &&
74+
<div class="text-red-6">{t('mod.signature.notFound')}</div>}
75+
{checkResult.value?.signature?.status === VerifyStatus.InvalidSignature &&
76+
<div class="text-red-6">{t('mod.signature.invalid')}</div>}
77+
</div>
78+
<div>{t('mod.manualInstall.version')}: v{checkResult.value?.version}</div>
79+
<div>{t('mod.manualInstall.buildDate')}: {checkResult.value?.buildDate ? <NTime time={new Date(checkResult.value.buildDate)} format="yyyy-MM-dd HH:mm:ss" /> : 'N/A'}</div>
80+
</div>,
81+
footer: () => <NFlex justify="end">
82+
<NButton onClick={() => currentFile.value = undefined}>{t('common.cancel')}</NButton>
83+
<NButton onClick={installAquaMai} type={checkResult.value?.signature?.status === VerifyStatus.Valid ? "primary" : "warning"}>{t('common.confirm')}</NButton>
84+
</NFlex>
85+
}}</NModal>
86+
</>;
87+
},
88+
});

MaiChartManager/Front/src/components/DragDropDispatcher/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { uploadFlow as uploadFlowAcbAwb } from '@/components/MusicEdit/AcbAwb';
66
import { selectedADir, selectedMusic } from '@/store/refs';
77
import { upload as uploadJacket } from '@/components/JacketBox';
88
import ReplaceChartModal, { replaceChartFileHandle } from './ReplaceChartModal';
9+
import AquaMaiManualInstaller, { setManualInstallAquaMai } from './AquaMaiManualInstaller';
910

1011
export const mainDivRef = shallowRef<HTMLDivElement>();
1112

@@ -32,9 +33,12 @@ export default defineComponent({
3233
startProcessMusicImport(handles.length === 1 ? handles[0] : handles);
3334
}
3435
else if (handles.length === 1 && handles[0] instanceof FileSystemFileHandle) {
36+
const file = handles[0] as FileSystemFileHandle;
37+
if (file.kind === 'file' && file.name.endsWith('.dll')) {
38+
setManualInstallAquaMai(file);
39+
}
3540
if (selectedADir.value === 'A000') return;
3641
if (!selectedMusic.value) return;
37-
const file = handles[0] as FileSystemFileHandle;
3842
if (file.kind === 'file' && (firstType.startsWith('video/') || ['dat', 'usm'].includes(file.name.toLowerCase().split('.').pop()!))) {
3943
uploadFlowMovie(file);
4044
}
@@ -97,6 +101,7 @@ export default defineComponent({
97101
</div>} */}
98102
</div>}
99103
<ReplaceChartModal />
104+
<AquaMaiManualInstaller />
100105
</>;
101106
},
102107
});

MaiChartManager/Front/src/components/ModManager/ConfigEditor.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { useI18n } from 'vue-i18n';
1212
import { debounce } from 'perfect-debounce';
1313
import AquaMaiSignatureStatusDisplay from "./AquaMaiSignatureStatusDisplay";
1414

15+
export let updateAquaMaiConfig = async (forceDefault = false, skipSignatureCheck = false)=>void 0;
16+
1517
export default defineComponent({
1618
props: {
1719
show: Boolean,
@@ -31,7 +33,7 @@ export default defineComponent({
3133
const { t } = useI18n();
3234
const errTitle = ref('');
3335

34-
const updateAquaMaiConfig = async (forceDefault = false, skipSignatureCheck = false) => {
36+
updateAquaMaiConfig = async (forceDefault = false, skipSignatureCheck = false) => {
3537
try {
3638
configReadErr.value = ''
3739
configReadErrTitle.value = ''

MaiChartManager/Front/src/locales/zh.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,15 @@ mod:
350350
verifiedCI: 已验证的 AquaMai 官方持续集成构建
351351
notFound: 这个 AquaMai 没有有效的签名,很可能不是官方版本
352352
invalid: 这个 AquaMai 的签名无效,很可能不是官方版本
353+
manualInstall:
354+
invalidAquaMaiFile: 无效的 AquaMai DLL
355+
invalidAquaMaiFileMessage: 拖放有效的 AquaMai.dll 到这里来安装它
356+
checkFailed: 检测 AquaMai 拖放时遇到错误
357+
confirmInstallTitle: 安装这个 AquaMai.dll 吗?
358+
version: 版本
359+
buildDate: 构建日期
360+
installFailed: 手动安装 AquaMai 失败
361+
installSuccess: 安装成功
353362
tools:
354363
title: 工具
355364
audioConvert: 音频转换(ACB + AWB)

MaiChartManager/MaiChartManager.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
<PackageReference Include="NAudio.Lame" Version="2.1.0"/>
6464
<PackageReference Include="NAudio.Vorbis" Version="1.5.0"/>
6565
<PackageReference Include="OSVersionExt" Version="3.0.0"/>
66+
<PackageReference Include="PeNet" Version="5.1.0" />
67+
<PackageReference Include="PeNet.Asn1" Version="2.0.2" />
6668
<PackageReference Include="Pluralsight.Crypto" Version="1.1.0"/>
6769
<PackageReference Include="pythonnet" Version="3.0.5"/>
6870
<PackageReference Include="Sentry" Version="5.14.1"/>

MaiChartManager/Utils/AquaMaiSignatureV2.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ private struct AquaMaiSignatureBlock
3434
{
3535
return null;
3636
}
37+
if (stru.Version != 1)
38+
{
39+
return null;
40+
}
3741
return stru;
3842
}
3943
finally

0 commit comments

Comments
 (0)