Compare commits

..

No commits in common. "be4e8124bf80012fb4625ed5d3104acfecffaf1b" and "2037f1ac4a0a049275ac25a4a66372bab5abc6aa" have entirely different histories.

13 changed files with 58 additions and 249 deletions

View file

@ -41,8 +41,6 @@ jobs:
run: | run: |
cp Tool/JsScript/* release/JsScript/ cp Tool/JsScript/* release/JsScript/
Compress-Archive -Path "release/*" -DestinationPath "FS_win_dotnet8.zip" -Force Compress-Archive -Path "release/*" -DestinationPath "FS_win_dotnet8.zip" -Force
$version = $env:GITHUB_REF -replace 'refs/tags/', ''
Write-Host "VERSION=$version" >> $env:GITHUB_ENV
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
@ -50,6 +48,6 @@ jobs:
files: | files: |
FS_win_dotnet8.zip FS_win_dotnet8.zip
LICENSE LICENSE
name: ${{ env.VERSION}} name: ${{ github.ref }}
draft: false draft: false
prerelease: true prerelease: true

View file

@ -69,14 +69,14 @@ public class WebSocPipeLine<TSocket>(TSocket socket, bool isAES) : AbsPipeLine(i
using var content = new MultipartFormDataContent(); using var content = new MultipartFormDataContent();
using var fileStream = new FileStream(filePath, FileMode.Open); using var fileStream = new FileStream(filePath, FileMode.Open);
// TODO 上传进度回调 // TODO 上传进度回调
var progress = new Progress<double>( // var progress = new Progress<double>(
(current) => // (current) =>
{ // {
progressCb(current); // progressCb(current);
} // }
); // );
var fileContent = new ProgressStreamContent(fileStream, progress); //var fileContent = new ProgressStreamContent(fileStream, progress);
content.Add(fileContent, "file", Path.GetFileName(filePath)); content.Add(new StreamContent(fileStream), "file", Path.GetFileName(filePath));
var it = await client.PostAsync("http://" + url + "/UploadFile", content); var it = await client.PostAsync("http://" + url + "/UploadFile", content);
if (it.StatusCode != System.Net.HttpStatusCode.OK) if (it.StatusCode != System.Net.HttpStatusCode.OK)
{ {
@ -103,8 +103,8 @@ public class WebSocPipeLine<TSocket>(TSocket socket, bool isAES) : AbsPipeLine(i
protected override async Task Listen(Func<byte[], bool> receiveCb) protected override async Task Listen(Func<byte[], bool> receiveCb)
{ {
//warning 最大支持10MB这由需要同步的文件数量大小决定 UTF-8 每个字符汉字视为4个字节数字1个 英文字母2个。1MB=256KB*425万个字符能描述就行 //warning 最大支持1MB这由需要同步的文件数量大小决定 UTF-8 每个字符汉字视为4个字节数字1个 英文字母2个。1MB=256KB*425万个字符能描述就行
var buffer = new byte[10 * 1024 * 1024]; var buffer = new byte[1024 * 1024];
while (Socket.State == WebSocketState.Open) while (Socket.State == WebSocketState.Open)
{ {

View file

@ -4,12 +4,8 @@ public enum SyncMsgType
{ {
Error = 0, Error = 0,
General = 1, General = 1,
//进度消息
Process = 2, Process = 2,
// DirFilePack = 3
//文件展示消息
DirFileDiff = 3
} }
public enum SyncProcessStep public enum SyncProcessStep

View file

@ -1,17 +1,12 @@
using System;
using System.Net; using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
namespace Common; namespace Common;
public class ProgressStreamContent(Stream stream_, IProgress<double> progress) public class ProgressStreamContent(Stream stream_, IProgress<double> progress)
: StreamContent(stream_, 5 * 1024 * 1024) : StreamContent(stream_, 4096)
{ {
private readonly Stream FileStream = stream_; private readonly Stream FileStream = stream_;
private readonly int BufferSize = 5 * 1024 * 1024; private readonly int BufferSize = 4096;
private readonly IProgress<double> Progress = progress; private readonly IProgress<double> Progress = progress;
protected override async Task SerializeToStreamAsync( protected override async Task SerializeToStreamAsync(

View file

@ -9,7 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalServer", "LocalServer\
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerTest", "ServerTest\ServerTest.csproj", "{0D507943-43A3-4227-903F-E123A5CAF7F4}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerTest", "ServerTest\ServerTest.csproj", "{0D507943-43A3-4227-903F-E123A5CAF7F4}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "Common\Common.csproj", "{3EED9D63-BC7B-455F-BA15-95BB52311ED8}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{3EED9D63-BC7B-455F-BA15-95BB52311ED8}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution

View file

@ -146,7 +146,6 @@ public class DeployHelper(LocalSyncServer context)
ProcessStartInfo startbuildInfo = ProcessStartInfo startbuildInfo =
new() new()
{ {
//p:DeployOnBuild=true
FileName = LocalSyncServer.MSBuildAbPath, // The command to execute (can be any command line tool) FileName = LocalSyncServer.MSBuildAbPath, // The command to execute (can be any command line tool)
Arguments = Arguments =
$" {Context.NotNullSyncConfig.LocalProjectAbsolutePath} /t:ResolveReferences" $" {Context.NotNullSyncConfig.LocalProjectAbsolutePath} /t:ResolveReferences"
@ -287,11 +286,6 @@ public class DiffFileAndPackHelper(LocalSyncServer context)
Context.NotNullSyncConfig.LocalRootPath Context.NotNullSyncConfig.LocalRootPath
); );
e.DiffDirInfo.WriteByThisInfo(PackOp); e.DiffDirInfo.WriteByThisInfo(PackOp);
Context
.LocalPipe.SendMsg(
CreateMsg(JsonSerializer.Serialize(e.DiffDirInfo), SyncMsgType.DirFileDiff)
)
.Wait();
} }
}); });
Context.LocalPipe.SendMsg(CreateMsg("文件差异比较成功!")).Wait(); Context.LocalPipe.SendMsg(CreateMsg("文件差异比较成功!")).Wait();
@ -324,7 +318,6 @@ public class DeployMSSqlHelper(LocalSyncServer context)
} }
else else
{ {
Context.LocalPipe.SendMsg(CreateMsg("正在打包数据库...")).Wait();
var arguments = var arguments =
$" /Action:Extract /TargetFile:{LocalSyncServer.TempRootFile}/{Context.NotNullSyncConfig.Id.ToString()}/{Context.NotNullSyncConfig.Id.ToString()}.dacpac" $" /Action:Extract /TargetFile:{LocalSyncServer.TempRootFile}/{Context.NotNullSyncConfig.Id.ToString()}/{Context.NotNullSyncConfig.Id.ToString()}.dacpac"
// 不要log file 了 // 不要log file 了
@ -394,7 +387,7 @@ public class UploadPackedHelper(LocalSyncServer context)
$"{LocalSyncServer.TempRootFile}/{Context.NotNullSyncConfig.Id}.zip", $"{LocalSyncServer.TempRootFile}/{Context.NotNullSyncConfig.Id}.zip",
(double current) => (double current) =>
{ {
// 每上传1Mb 更新一下进度 //这里可能需要降低获取上传进度的频率
Context Context
.LocalPipe.SendMsg(CreateMsg(current.ToString(), SyncMsgType.Process)) .LocalPipe.SendMsg(CreateMsg(current.ToString(), SyncMsgType.Process))
.Wait(); .Wait();

View file

@ -53,7 +53,6 @@ public class SyncFilesController(RemoteSyncServerFactory factory, SqliteDbContex
/// </summary> /// </summary>
/// <param name="file"></param> /// <param name="file"></param>
/// <returns></returns> /// <returns></returns>
[DisableRequestSizeLimit]
[HttpPost("/UploadFile")] [HttpPost("/UploadFile")]
public async Task<IActionResult> UploadFile(IFormFile file) public async Task<IActionResult> UploadFile(IFormFile file)
{ {

View file

@ -41,7 +41,7 @@ if (app.Environment.IsDevelopment())
} }
app.UseWebSockets(); app.UseWebSockets();
app.Urls.Clear(); //app.Urls.Clear();
app.Urls.Add("http://0.0.0.0:6819"); //app.Urls.Add("http://0.0.0.0:6819");
app.MapControllers(); app.MapControllers();
app.Run(); app.Run();

View file

@ -11,8 +11,7 @@
"AllowedHosts": "*", "AllowedHosts": "*",
"TempDir": "D:\\FileSyncTest\\dtemp", "TempDir": "D:\\FileSyncTest\\dtemp",
"NamePwds": [ "NamePwds": [
[ "Test", "t123" ], [ "Test", "t123" ]
[ "FYMF", "FYMF" ]
], ],
"SqlPackageAbPath": "C:\\Users\\ZHAOLEI\\.dotnet\\tools\\sqlpackage.exe" "SqlPackageAbPath": "C:\\Users\\ZHAOLEI\\.dotnet\\tools\\sqlpackage.exe"
} }

View file

@ -4,23 +4,23 @@ import WebSocket from "ws";
//#region ############################## 配置文件 ################################### //#region ############################## 配置文件 ###################################
const LocalHost = "127.0.0.1"; const LocalHost = "127.0.0.1";
let config = { const config = {
//发布的名称,每个项目具有唯一的一个名称 //发布的名称,每个项目具有唯一的一个名称
Name: "Test", Name: "FYMF",
RemotePwd: "t123", RemotePwd: "FYMF",
//远程服务器地址,也就是发布的目的地,它是正式环境 //远程服务器地址,也就是发布的目的地,它是正式环境
RemoteUrl: "127.0.0.1:6819", RemoteUrl: "127.0.0.1:8007",
//是否发布数据库 sqlserver //是否发布数据库 sqlserver
IsDeployDb: true, IsDeployDb: true,
//是否发布前重新构建项目 //是否发布前重新构建项目
IsDeployProject: true, IsDeployProject: false,
//项目地址 //项目地址
LocalProjectAbsolutePath: LocalProjectAbsolutePath:
"D:/git/HMES-H7-HNFY/HMES-H7-HNFYMF/HMES-H7-HNFYMF.WEB", "D:/git/HMES-H7-HNFY/HMES-H7-HNFYMF/HMES-H7-HNFYMF.WEB",
//源文件目录地址,是要发布的文件根目录,它是绝对路径,!执行发布时将发布到这个目录! //源文件目录地址,是要发布的文件根目录,它是绝对路径,!执行发布时将发布到这个目录!
LocalRootPath: "D:/FileSyncTest/src", LocalRootPath: "D:/FileSyncTest/src",
//目标文件目录地址,也就是部署服务的机器上的项目文件根目录,它是绝对路径 //目标文件目录地址,也就是部署服务的机器上的项目文件根目录,它是绝对路径
RemoteRootPath: "D:/FileSyncTest/dst", RemoteRootPath: "D:/FYMF",
//源数据库配置 SqlServer,将会同步数据库的结构 //源数据库配置 SqlServer,将会同步数据库的结构
SrcDb: { SrcDb: {
//Host //Host
@ -44,7 +44,7 @@ let config = {
ServerName: "127.0.0.1", ServerName: "127.0.0.1",
DatabaseName: "HMES_H7_HNFYMF", DatabaseName: "HMES_H7_HNFYMF",
User: "sa", User: "sa",
Password: "0", Password: "Yuanmo520...",
TrustServerCertificate: "True", TrustServerCertificate: "True",
}, },
//子目录配置每个子目录都有自己不同的发布策略它是相对路径即相对于LocalRootPath和RemoteRootPath(注意 '/',这将拼成一个完整的路径),文件数据依此进行, //子目录配置每个子目录都有自己不同的发布策略它是相对路径即相对于LocalRootPath和RemoteRootPath(注意 '/',这将拼成一个完整的路径),文件数据依此进行,
@ -73,52 +73,6 @@ let config = {
// } // }
// ] // ]
}; };
config = {
Name: "FYMF",
RemoteUrl: "212.129.223.183:6819",
RemotePwd: "FYMF",
IsDeployDb: false,
IsDeployProject: false,
LocalProjectAbsolutePath: "D:/git/HMES-H7-HNFY/HMES-H7-HNFYMF/HMES-H7-HNFYMF.WEB",
LocalRootPath: "D:/FileSyncTest/src",
RemoteRootPath: "E:/HMES_H7_HNFY_PREON",
SrcDb: {
ServerName: "172.16.12.2",
DatabaseName: "HMES_H7_HNFYMF",
User: "hmes-h7",
Password: "Hmes-h7666",
TrustServerCertificate: "True",
SyncTablesData: [
"dbo.sys_Button",
"dbo.sys_Menu",
"dbo.sys_Module",
"dbo.sys_Page",
"dbo.CommonPara"
]
},
DstDb: {
ServerName: "172.16.80.1",
DatabaseName: "HMES_H7_HNFYMF_PRE",
User: "hnfypre",
Password: "pre0823",
TrustServerCertificate: "True"
},
DirFileConfigs: [
{
DirPath: "/",
Excludes: [
"Web.config",
"Log",
"Content",
"fonts"
]
}
],
ExecProcesses: []
}
//#endregion //#endregion
//#region ############################## 打印函数 ################################### //#region ############################## 打印函数 ###################################
@ -128,20 +82,20 @@ let IsSuccess = false;
* 在新行打印错误信息 * 在新行打印错误信息
*/ */
function PrintErrInNewLine(str) { function PrintErrInNewLine(str) {
process.stdout.write("\n");
var chunk = chalk["red"]("[错误]: "); var chunk = chalk["red"]("[错误]: ");
process.stdout.write(chunk); process.stdout.write(chunk);
process.stdout.write(str); process.stdout.write(str);
process.stdout.write("\n");
} }
/** /**
* 在新行打印成功信息 * 在新行打印成功信息
*/ */
function PrintSuccessInNewLine(str) { function PrintSuccessInNewLine(str) {
process.stdout.write("\n");
var chunk = chalk["green"]("[成功]: "); var chunk = chalk["green"]("[成功]: ");
process.stdout.write(chunk); process.stdout.write(chunk);
process.stdout.write(str); process.stdout.write(str);
process.stdout.write("\n");
} }
/** /**
@ -157,11 +111,8 @@ function PrintCloseNewLine(str) {
/** /**
* 在当前行打印一般信息打印此行信息之前会清除当前行 * 在当前行打印一般信息打印此行信息之前会清除当前行
*/ */
function PrintProcessLine(str) { function PrintGeneralInCurrentLine(str) {
var chunk = chalk["yellow"]("[上传进度]: "); process.stdout.write(`\r${str}`);
process.stdout.write(chunk);
process.stdout.write(`${str}`);
process.stdout.write("\n");
} }
//#endregion //#endregion
@ -171,59 +122,26 @@ function PrintProcessLine(str) {
/** /**
* 1-n. localServer 工作此处只展示信息 * 1-n. localServer 工作此处只展示信息
*/ */
function getOpEmoj(Op) {
switch (Op) {
case 0:
return "A";
case 1:
return "M";
case 2:
return "D";
default:
return "DIR";
}
}
let ws = null; let ws = null;
function MsgCb(MsgIt) { function MsgCb(MsgIt) {
if (MsgIt.Step <= 6) {
if (MsgIt.Type == 2) { if (MsgIt.Type == 2) {
PrintProcessLine(`(${MsgIt.Step}/6) ${MsgIt.Body}`); PrintGeneralInCurrentLine(MsgIt.Body);
if (parseFloat(MsgIt.Body) == 1) {
var chunk = chalk["green"]("[上传成功]");
process.stdout.write(chunk);
process.stdout.write("\n");
}
} else if (MsgIt.Type == 3) {
var it = JSON.parse(MsgIt.Body);
const f = (item) => {
PrintSuccessInNewLine(
`(${MsgIt.Step}/6) [${getOpEmoj(item.NextOp)}] ${item.FormatedPath}`
);
if (item.Children) {
item.Children.forEach((e) => {
f(e)
});
}
}
f(it)
} else { } else {
if (MsgIt.Step <= 6) {
PrintSuccessInNewLine(`(${MsgIt.Step}/6) ${MsgIt.Body}`); PrintSuccessInNewLine(`(${MsgIt.Step}/6) ${MsgIt.Body}`);
}
if (MsgIt.Step == 6) { if (MsgIt.Step == 6) {
if (MsgIt.Body == "发布完成!") { if (MsgIt.Body == "发布完成!") {
IsSuccess = true; IsSuccess = true;
ws.close(); ws.close();
} }
} }
} else if (MsgIt.Step == 7) { } else if (MsgIt == 7) {
PrintErrInNewLine(MsgIt.Body); PrintErrInNewLine(MsgIt.Body);
} else { } else {
PrintCloseNewLine("(关闭)" + MsgIt.Body); PrintCloseNewLine("(关闭)" + MsgIt.Body);
} }
}
} }
//#endregion //#endregion
async function connectWebSocket() { async function connectWebSocket() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -265,17 +183,10 @@ async function connectWebSocket() {
} }
(async function main() { (async function main() {
try { try {
// for(var i = 0;i<10;++i) {
// PrintGeneralInCurrentLine(`${i}`)
// }
// PrintSuccessInNewLine("11")
await connectWebSocket(); await connectWebSocket();
if (!IsSuccess) {
throw new Error("发布失败");
}
// console.log('WebSocket has closed'); // console.log('WebSocket has closed');
// The script will wait here until the WebSocket connection is closed // The script will wait here until the WebSocket connection is closed
} catch (err) { } catch (err) {
throw new Error(err); console.error("Failed to connect or an error occurred:", err);
} }
})(); })();

View file

@ -12,27 +12,26 @@ const options = ref({
tabSize: 2, tabSize: 2,
}) })
let IsSuccess = false
let Pipe = null let Pipe = null
const Msgs = ref([]) const Msgs = ref([])
const code = ref(` const code = ref(`
config = { config = {
// //
Name: "Test", Name: "FYMF",
RemotePwd: "t123", RemotePwd: "FYMF",
// //
RemoteUrl: "127.0.0.1:6819", RemoteUrl: "127.0.0.1:8007",
// sqlserver // sqlserver
IsDeployDb: true, IsDeployDb: true,
// //
IsDeployProject: true, IsDeployProject: false,
// //
LocalProjectAbsolutePath: LocalProjectAbsolutePath:
"D:/git/HMES-H7-HNFY/HMES-H7-HNFYMF/HMES-H7-HNFYMF.WEB", "D:/git/HMES-H7-HNFY/HMES-H7-HNFYMF/HMES-H7-HNFYMF.WEB",
//!! //!!
LocalRootPath: "D:/FileSyncTest/src", LocalRootPath: "D:/FileSyncTest/src",
// //
RemoteRootPath: "D:/FileSyncTest/dst", RemoteRootPath: "D:/FYMF",
// SqlServer, // SqlServer,
SrcDb: { SrcDb: {
//Host //Host
@ -56,7 +55,7 @@ config = {
ServerName: "127.0.0.1", ServerName: "127.0.0.1",
DatabaseName: "HMES_H7_HNFYMF", DatabaseName: "HMES_H7_HNFYMF",
User: "sa", User: "sa",
Password: "0", Password: "Yuanmo520...",
TrustServerCertificate: "True", TrustServerCertificate: "True",
}, },
//LocalRootPathRemoteRootPath( '/'), //LocalRootPathRemoteRootPath( '/'),
@ -87,52 +86,16 @@ config = {
}; };
`) `)
var CStatus = ref('None') var CStatus = ref('None')
function getOpEmoj(Op) {
switch (Op) {
case 0:
return "";
case 1:
return "Ⓜ️";
case 2:
return "❌";
default:
return "📁";
}
}
function publishCB(MsgIt) { function publishCB(MsgIt) {
console.log(MsgIt)
if (MsgIt.Type == 2) { if (MsgIt.Type == 2) {
Msgs.value[Msgs.value.length - 1] = MsgIt Msgs.value[Msgs.value.length - 1] = MsgIt
} else if (MsgIt.Type == 3) { } else {
var it = JSON.parse(MsgIt.Body);
/**
* This function appears to be intended for processing children elements, though the current implementation is incomplete.
*
* @param {Array} children - The array of child elements to be processed.
* @returns {void}
*/
const f = (item) => {
Msgs.value.push({
Step: MsgIt.Step,
Type: MsgIt.Type,
Body: `[${getOpEmoj(item.NextOp)}] ${item.FormatedPath}`
})
if (item.Children) {
item.Children.forEach((e) => {
f(e)
});
}
}
f(it)
}
else {
Msgs.value.push(MsgIt) Msgs.value.push(MsgIt)
} }
if (MsgIt.Step == 6) { if (MsgIt.Step == 6) {
if (MsgIt.Body == "发布完成!") { if (MsgIt.Body == "发布完成!") {
CStatus.value = 'Success' CStatus.value = 'Success'
IsSuccess = true
Pipe.ClosePipe() Pipe.ClosePipe()
dialogShow("正确:发布完成!") dialogShow("正确:发布完成!")
} }
@ -199,46 +162,6 @@ function dialogShow(msg) {
dMsg.value = msg dMsg.value = msg
document.getElementById('dialog').showModal() document.getElementById('dialog').showModal()
} }
function getColor(msg) {
if (msg.Step >= 7) {
if (IsSuccess) {
return "green"
}
return "red"
} else if (msg.Type == 2) {
return "yellow"
} else {
return "green"
}
}
function getTitle(msg) {
var x = getColor(msg)
switch (x) {
case "green":
return "[成功]"
break;
case "red":
return "[失败]"
break;
case "yellow":
return "[上传进度]"
break;
default:
break;
}
}
function getStep(msg) {
if (msg.Step > 6) {
return ""
}
return `(${msg.Step}/6)`
}
function getBody(msg) {
return msg.Body
}
</script> </script>
<template> <template>
@ -250,18 +173,13 @@ function getBody(msg) {
<MonacoEditor theme="vs-dark" :options="options" language="javascript" :width="800" :height="700" <MonacoEditor theme="vs-dark" :options="options" language="javascript" :width="800" :height="700"
v-model:value="code"></MonacoEditor> v-model:value="code"></MonacoEditor>
<div style="width: 1200px;height: 700px;background-color: #1e1e1e;overflow:auto;"> <div style="width: 800px;height: 700px;background-color: #1e1e1e;">
发布日志 发布日志
<p style="text-align: left;border: 1px solid gray;margin: 5px;" v-for="msg in Msgs"> <p style="text-align: left;border: 1px solid gray;margin: 5px;" v-for="msg in Msgs">
<span :style="{ width: '100px', color: msg.Step > 6 ? 'red' : 'green' }">[{{ msg.Step
<span :style="{ width: '100px', color: getColor(msg) }"> > 6 ? msg.Step > 7 ? "关闭" : "错误" : `${msg.Step}/${6}`}}]</span>
{{ getTitle(msg) }} <span style="margin-left: 5px ;">{{ msg.Body }}</span>
</span>
<span>
{{ getStep(msg) }}
</span>
{{ getBody(msg) }}
</p> </p>
</div> </div>
</div> </div>

View file

@ -7,7 +7,7 @@ class ConnectPipe {
} }
OpenPipe(config, MsgCb) { OpenPipe(config, MsgCb) {
var webSocUrl = `ws://${window.location.host}/websoc?Name=${config.Name}` var webSocUrl = `ws://${window.location.host}/websoc?Name=${config.Name}`
// var webSocUrl = `ws://127.0.0.1:6818/websoc?Name=${config.Name}`; // var webSocUrl = "ws://127.0.0.1:6818/websoc?Name=Test";
this.#websocket = new WebSocket(webSocUrl); this.#websocket = new WebSocket(webSocUrl);
this.#websocket.onopen = (event) => { this.#websocket.onopen = (event) => {
var starter = { var starter = {

View file

@ -59,7 +59,7 @@ button:focus-visible {
} }
#app { #app {
/* max-width: 1280px; */ max-width: 1280px;
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;