Compare commits

...
Author SHA1 Message Date
蘭 c392b72470 fix: Fix file decompression issue (#12684)
Refs #12675
2026-05-08 06:42:21 +00:00
王贺 50a70ef1e5 fix: skip large binary file history snapshots (#12683)
#### What this PR does / why we need it?

Refs https://github.com/1Panel-dev/1Panel/issues/12681

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-05-08 06:40:22 +00:00
ssongliu 75266ef659 fix: pause home carousel while editing memo (#12680)
Refs #12679
2026-05-08 02:18:20 +00:00
王贺 b3c593b852 fix openwrt procd init script selection (#12670) 2026-05-07 18:28:30 +08:00
ssongliu 73530bb8b9 fix: route terminal websocket by operate node (#12676) 2026-05-07 18:28:17 +08:00
王贺 1b6e70964b fix: preserve upload file permissions (#12672) 2026-05-07 18:26:39 +08:00
王贺 61d42b05e9 fix: optimize dashboard monitor chart (#12673) 2026-05-07 18:25:27 +08:00
11 changed files with 256 additions and 85 deletions
+13 -5
View File
@@ -411,7 +411,7 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
helper.BadRequest(c, errors.New("error paths in request"))
return
}
dir := path.Dir(paths[0])
dir := path.Clean(paths[0])
_, err = os.Stat(dir)
if err != nil && os.IsNotExist(err) {
@@ -452,8 +452,14 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
}
_ = os.Chown(dstDir, uid, gid)
}
dstDirMode := mode
dstFileMode := mode.Perm()
if dstDirInfo, err := os.Stat(dstDir); err == nil {
dstDirMode = dstDirInfo.Mode()
dstFileMode = dstDirInfo.Mode().Perm()
}
tmpFilename := dstFilename + ".tmp"
if err := c.SaveUploadedFile(file, tmpFilename); err != nil {
if err := c.SaveUploadedFile(file, tmpFilename, dstDirMode); err != nil {
_ = os.Remove(tmpFilename)
e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
failures[file.Filename] = e
@@ -476,7 +482,7 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
if statErr == nil {
_ = os.Chmod(dstFilename, dstInfo.Mode())
} else {
_ = os.Chmod(dstFilename, mode)
_ = os.Chmod(dstFilename, dstFileMode)
}
if uid != -1 && gid != -1 {
_ = os.Chown(dstFilename, uid, gid)
@@ -787,12 +793,13 @@ func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int,
return err
}
}
if dstDirInfo, err := os.Stat(dstDir); err == nil {
mode = dstDirInfo.Mode().Perm()
}
dstFileName := filepath.Join(dstDir, fileName)
dstInfo, statErr := os.Stat(dstFileName)
if statErr == nil {
mode = dstInfo.Mode()
} else {
mode = 0644
}
if overwrite {
_ = os.Remove(dstFileName)
@@ -814,6 +821,7 @@ func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int,
}
_ = os.Remove(chunkPath)
}
_ = os.Chmod(dstFileName, mode)
return nil
}
+158 -11
View File
@@ -40,7 +40,6 @@ import (
"golang.org/x/text/transform"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/files"
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
@@ -50,6 +49,8 @@ import (
type FileService struct {
}
const fileHistorySnapshotMaxSize = 10 * 1024 * 1024
type IFileService interface {
GetFileList(op request.FileOption) (response.FileInfo, error)
SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error)
@@ -476,7 +477,7 @@ func preflightCompressTool(compressType files.CompressType) error {
func preflightDecompressTool(decompressType files.CompressType) error {
switch decompressType {
case files.Tar, files.Zip, files.TarGz, files.Rar, files.X7z:
case files.Rar, files.X7z:
_, err := files.NewExtractShellArchiver(decompressType)
return err
default:
@@ -539,7 +540,7 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
if err := fo.CreateDir(c.Dst, constant.DirPerm); err != nil {
return err
}
if err := cmd.NewCommandMgr(cmd.WithContext(t.TaskCtx)).RunBashCf("cp -rfp '%s'/. '%s'", tempDst, c.Dst); err != nil {
if err := copyDecompressTree(t.TaskCtx, tempDst, c.Dst); err != nil {
return err
}
success = true
@@ -550,6 +551,124 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
return nil
}
func copyDecompressTree(ctx context.Context, srcDir, dstDir string) error {
entries, err := os.ReadDir(srcDir)
if err != nil {
return err
}
for _, entry := range entries {
if err := copyDecompressEntry(ctx, filepath.Join(srcDir, entry.Name()), filepath.Join(dstDir, entry.Name())); err != nil {
return err
}
}
return nil
}
func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr error) {
if err := ctx.Err(); err != nil {
return err
}
info, err := os.Lstat(srcPath)
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 {
if err := os.RemoveAll(dstPath); err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
return err
}
target, err := os.Readlink(srcPath)
if err != nil {
return err
}
if err := os.Symlink(target, dstPath); err != nil {
return err
}
return applyDecompressOwnership(srcPath, dstPath)
}
if info.IsDir() {
dstInfo, err := os.Lstat(dstPath)
keepExistingDir := err == nil && dstInfo.IsDir() && dstInfo.Mode()&os.ModeSymlink == 0
if err != nil && !os.IsNotExist(err) {
return err
}
if !keepExistingDir {
if err := os.RemoveAll(dstPath); err != nil {
return err
}
if err := os.MkdirAll(dstPath, info.Mode().Perm()); err != nil {
return err
}
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
return err
}
}
entries, err := os.ReadDir(srcPath)
if err != nil {
return err
}
for _, entry := range entries {
if err := copyDecompressEntry(ctx, filepath.Join(srcPath, entry.Name()), filepath.Join(dstPath, entry.Name())); err != nil {
return err
}
}
if keepExistingDir {
return nil
}
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
}
if err := os.RemoveAll(dstPath); err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
return err
}
srcFile, err := os.Open(srcPath)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, info.Mode().Perm())
if err != nil {
return err
}
defer func() {
if cerr := dstFile.Close(); cerr != nil && retErr == nil {
retErr = cerr
}
}()
if _, err := io.Copy(dstFile, srcFile); err != nil {
return err
}
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
return err
}
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
}
func applyDecompressOwnership(srcPath, dstPath string) error {
info, err := os.Lstat(srcPath)
if err != nil {
return err
}
stat, ok := info.Sys().(*unix.Stat_t)
if !ok {
return nil
}
if info.Mode()&os.ModeSymlink != 0 {
return os.Lchown(dstPath, int(stat.Uid), int(stat.Gid))
}
return os.Chown(dstPath, int(stat.Uid), int(stat.Gid))
}
func (f *FileService) GetContent(op request.FileContentReq) (response.FileInfo, error) {
info, err := files.NewFileInfo(files.FileOption{
Path: op.Path,
@@ -692,11 +811,11 @@ func (f *FileService) ChangeName(req request.FileRename) error {
}
fo := files.NewFileOp()
info, _ := files.NewFileInfo(files.FileOption{Path: req.OldName, Expand: false})
content, _ := os.ReadFile(req.OldName)
content, shouldRecordHistory := readEditableFileHistoryContent(req.OldName, info)
if err := fo.Rename(req.OldName, req.NewName); err != nil {
return err
}
if info != nil && !info.IsDir {
if shouldRecordHistory {
if histErr := historyService.RecordOperation(fileHistoryOpRename, req.OldName, content, info.FileMode, req.OldName, req.NewName); histErr != nil {
global.LOG.Warnf("record file rename history failed for %s: %v", req.OldName, histErr)
}
@@ -727,18 +846,18 @@ func (f *FileService) MvFile(m request.FileMove) error {
path string
content []byte
mode os.FileMode
isDir bool
record bool
}
snapshots := make([]moveSnapshot, 0, len(m.OldPaths))
for _, oldPath := range m.OldPaths {
content, _ := os.ReadFile(oldPath)
mode := os.FileMode(0640)
isDir := false
record := false
var content []byte
if info, err := files.NewFileInfo(files.FileOption{Path: oldPath, Expand: false}); err == nil {
mode = info.FileMode
isDir = info.IsDir
content, record = readEditableFileHistoryContent(oldPath, info)
}
snapshots = append(snapshots, moveSnapshot{path: oldPath, content: content, mode: mode, isDir: isDir})
snapshots = append(snapshots, moveSnapshot{path: oldPath, content: content, mode: mode, record: record})
}
var errs []error
if m.Type == "cut" {
@@ -754,7 +873,7 @@ func (f *FileService) MvFile(m request.FileMove) error {
return err
}
for _, snapshot := range snapshots {
if !snapshot.isDir {
if snapshot.record {
targetPath := buildHistoryMoveTargetPath(m.NewPath, m.Name, snapshot.path, len(m.OldPaths))
if histErr := historyService.RecordOperation(fileHistoryOpMove, snapshot.path, snapshot.content, snapshot.mode, snapshot.path, targetPath); histErr != nil {
global.LOG.Warnf("record file move history failed for %s: %v", snapshot.path, histErr)
@@ -790,6 +909,34 @@ func (f *FileService) MvFile(m request.FileMove) error {
return nil
}
func readEditableFileHistoryContent(filePath string, info *files.FileInfo) ([]byte, bool) {
if info == nil || info.IsDir || files.IsBlockDevice(info.FileMode) || info.Size > fileHistorySnapshotMaxSize {
return nil, false
}
file, err := os.Open(filePath)
if err != nil {
return nil, false
}
defer file.Close()
headBuf := make([]byte, 1024)
n, err := file.Read(headBuf)
if err != nil && err != io.EOF {
return nil, false
}
if n > 0 && files.DetectBinary(headBuf[:n]) {
return nil, false
}
if _, err := file.Seek(0, 0); err != nil {
return nil, false
}
content, err := io.ReadAll(io.LimitReader(file, fileHistorySnapshotMaxSize+1))
if err != nil || int64(len(content)) > fileHistorySnapshotMaxSize {
return nil, false
}
return content, true
}
func buildHistoryMoveTargetPath(dst, name, sourcePath string, sourceCount int) string {
if strings.TrimSpace(dst) == "" {
return sourcePath
+1 -1
View File
@@ -260,7 +260,7 @@ func SelectInitScript(keyword string) (string, error) {
keyword = strings.TrimSuffix(keyword, ".service") + ".openrc"
case "sysvinit":
if _, err := os.Stat("/etc/rc.common"); err == nil {
keyword = strings.TrimSuffix(keyword, ".service") + ".prod"
keyword = strings.TrimSuffix(keyword, ".service") + ".procd"
} else {
keyword = strings.TrimSuffix(keyword, ".service") + ".init"
}
+2 -2
View File
@@ -950,7 +950,7 @@ func (f FileOp) decompressWithSDK(ctx context.Context, srcFile string, dst strin
} else {
parentDir := path.Dir(filePath)
if !f.Stat(parentDir) {
if err := f.Fs.MkdirAll(parentDir, info.Mode()); err != nil {
if err := f.Fs.MkdirAll(parentDir, constant.DirPerm); err != nil {
return err
}
}
@@ -1116,7 +1116,7 @@ func (f FileOp) tryDecompressTarGz(ctx context.Context, srcFile string, dst stri
} else {
parentDir := filepath.Dir(filePath)
if !f.Stat(parentDir) {
if err := f.Fs.MkdirAll(parentDir, info.Mode()); err != nil {
if err := f.Fs.MkdirAll(parentDir, constant.DirPerm); err != nil {
return err
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ func Proxy() gin.HandlerFunc {
return
}
if c.Request.URL.Path == "/api/v2/hosts/terminal" {
if c.Request.URL.Path == "/api/v2/hosts/terminal" && (currentNode == "local" || len(currentNode) == 0) {
proxyLocalAgent(c)
return
}
+1 -1
View File
@@ -285,7 +285,7 @@ func SelectInitScript(keyword string) (string, error) {
keyword = strings.TrimSuffix(keyword, ".service") + ".openrc"
case "sysvinit":
if _, err := os.Stat("/etc/rc.common"); err == nil {
keyword = strings.TrimSuffix(keyword, ".service") + ".prod"
keyword = strings.TrimSuffix(keyword, ".service") + ".procd"
} else {
keyword = strings.TrimSuffix(keyword, ".service") + ".init"
}
+3 -2
View File
@@ -241,8 +241,9 @@ const initWebSocket = (endpoint_: string, args: string = '') => {
const protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
const host = href.split('//')[1].split('/')[0];
const endpoint = endpoint_.replace(/^\/+/, '');
let conn = `${protocol}://${host}/${endpoint}?cols=${term.value.cols}&rows=${term.value.rows}&${args}&operateNode=${globalStore.currentNode}`;
if (args.indexOf('&operateNode=') !== -1) {
let node = args.indexOf('id=') !== -1 ? 'local' : globalStore.currentNode;
let conn = `${protocol}://${host}/${endpoint}?cols=${term.value.cols}&rows=${term.value.rows}&${args}&operateNode=${node}`;
if (args.indexOf('operateNode=') !== -1) {
conn = `${protocol}://${host}/${endpoint}?cols=${term.value.cols}&rows=${term.value.rows}&${args}`;
}
terminalSocket.value = new WebSocket(conn);
@@ -141,6 +141,7 @@ function initChart() {
});
}
const grid = props.option.grid || { left: '7%', right: '7%', bottom: '20%' };
const option = {
title: [
{
@@ -205,9 +206,9 @@ function initChart() {
return res;
},
},
grid: props.option.grid || { left: '7%', right: '7%', bottom: '20%' },
grid,
legend: itemSelect || {
right: 10,
right: grid.right || 10,
itemWidth: 8,
textStyle: {
color: '#646A73',
@@ -228,7 +229,14 @@ function initChart() {
},
},
series: series,
dataZoom: [{ startValue: props?.option.xData[0], show: props.dataZoom }],
dataZoom: [
{
...(props.option.xData?.[0] ? { startValue: props.option.xData[0] } : {}),
left: grid.left,
right: grid.right,
show: props.dataZoom,
},
],
};
// 渲染数据
itemChart.setOption(option, true);
+58 -47
View File
@@ -118,7 +118,7 @@
</template>
<template #body>
<div style="position: relative; margin-top: 60px">
<div class="monitor-tags" v-if="chartOption === 'network'">
<div class="monitor-tags" :style="monitorTagsStyle" v-if="chartOption === 'network'">
<el-tag>
{{ $t('monitor.up') }}: {{ computeSizeFromKBs(currentChartInfo.netBytesSent) }}
</el-tag>
@@ -128,7 +128,7 @@
<el-tag>{{ $t('home.totalSend') }}: {{ computeSize(currentInfo.netBytesSent) }}</el-tag>
<el-tag>{{ $t('home.totalRecv') }}: {{ computeSize(currentInfo.netBytesRecv) }}</el-tag>
</div>
<div class="monitor-tags" v-if="chartOption === 'io'">
<div class="monitor-tags" :style="monitorTagsStyle" v-if="chartOption === 'io'">
<el-tag>{{ $t('monitor.read') }}: {{ currentChartInfo.ioReadBytes }} MB</el-tag>
<el-tag>{{ $t('monitor.write') }}: {{ currentChartInfo.ioWriteBytes }} MB</el-tag>
<el-tag>
@@ -168,6 +168,7 @@
height="368px"
indicator-position=""
arrow="never"
:autoplay="!showMemoCarousel || !memoEditing"
>
<el-carousel-item key="systemInfo">
<CardWithHeader :header="$t('home.systemInfo')">
@@ -468,6 +469,46 @@ const DASHBOARD_CACHE_TTL = {
netOptions: 60 * 60 * 1000,
ioOptions: 60 * 60 * 1000,
};
const monitorChartGrid = { left: 65, right: 65, bottom: '20%' };
const monitorTagsStyle = {
left: `${monitorChartGrid.left}px`,
right: `${monitorChartGrid.right}px`,
};
const monitorChartEmptyLength = 20;
const loadMonitorEmptyData = () => Array.from({ length: monitorChartEmptyLength }, () => null);
const loadMonitorEmptyTime = () => Array.from({ length: monitorChartEmptyLength }, () => '');
const loadMonitorChartData = (data: Array<number>) => (data.length === 0 ? loadMonitorEmptyData() : data);
const loadMonitorChartTime = (data: Array<string>) => (data.length === 0 ? loadMonitorEmptyTime() : data);
const loadIOChartOption = () => ({
xData: loadMonitorChartTime(timeIODatas.value),
yData: [
{
name: i18n.global.t('monitor.read'),
data: loadMonitorChartData(ioReadBytes.value),
},
{
name: i18n.global.t('monitor.write'),
data: loadMonitorChartData(ioWriteBytes.value),
},
],
grid: monitorChartGrid,
formatStr: 'MB',
});
const loadNetworkChartOption = () => ({
xData: loadMonitorChartTime(timeNetDatas.value),
yData: [
{
name: i18n.global.t('monitor.up'),
data: loadMonitorChartData(netBytesSents.value),
},
{
name: i18n.global.t('monitor.down'),
data: loadMonitorChartData(netBytesRecvs.value),
},
],
grid: monitorChartGrid,
formatStr: 'KB/s',
});
const statusRef = ref();
const appRef = ref();
@@ -604,16 +645,8 @@ const currentChartInfo = reactive({
const skipNextCurrentInfoDelta = ref(false);
const chartsOption = ref({
ioChart: {
xData: [],
yData: [],
formatStr: 'MB',
},
networkChart: {
xData: [],
yData: [],
formatStr: 'KB/s',
},
ioChart: loadIOChartOption(),
networkChart: loadNetworkChartOption(),
});
const updateCurrentInfo = (data: Dashboard.CurrentInfo) => {
@@ -688,14 +721,21 @@ const onLoadIOOptions = async (force?: boolean) => {
};
const onLoadBaseInfo = async (isInit: boolean, range: string) => {
let resetChartData = false;
if (range === 'all' || range === 'io') {
ioReadBytes.value = [];
ioWriteBytes.value = [];
timeIODatas.value = [];
} else if (range === 'all' || range === 'network') {
resetChartData = true;
}
if (range === 'all' || range === 'network') {
netBytesSents.value = [];
netBytesRecvs.value = [];
timeNetDatas.value = [];
resetChartData = true;
}
if (resetChartData) {
loadData();
}
const res = await loadBaseInfo(searchInfo.ioOption, searchInfo.netOption);
baseInfo.value = res.data;
@@ -920,35 +960,9 @@ const saveMemo = async () => {
const loadData = async () => {
if (chartOption.value === 'io') {
chartsOption.value['ioChart'] = {
xData: timeIODatas.value,
yData: [
{
name: i18n.global.t('monitor.read'),
data: ioReadBytes.value,
},
{
name: i18n.global.t('monitor.write'),
data: ioWriteBytes.value,
},
],
formatStr: 'MB',
};
chartsOption.value['ioChart'] = loadIOChartOption();
} else {
chartsOption.value['networkChart'] = {
xData: timeNetDatas.value,
yData: [
{
name: i18n.global.t('monitor.up'),
data: netBytesSents.value,
},
{
name: i18n.global.t('monitor.down'),
data: netBytesRecvs.value,
},
],
formatStr: 'KB/s',
};
chartsOption.value['networkChart'] = loadNetworkChartOption();
}
};
@@ -1208,12 +1222,9 @@ onBeforeUnmount(() => {
.monitor-tags {
position: absolute;
top: -10px;
left: 20px;
:deep(.el-tag) {
margin-right: 10px;
margin-bottom: 10px;
}
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.version {
@@ -205,15 +205,13 @@ const loadTaskInfo = async () => {
emitTaskChange();
if (!taskInfo.value || taskInfo.value.status !== 'Executing') {
stopTaskPolling();
currentTaskID.value = '';
taskInfo.value = null;
emitTaskChange();
resetDrawerState();
em('close', false);
}
} catch (error) {
stopTaskPolling();
currentTaskID.value = '';
taskInfo.value = null;
emitTaskChange();
resetDrawerState();
em('close', false);
}
};
@@ -187,15 +187,13 @@ const loadTaskInfo = async () => {
emitTaskChange();
if (!taskInfo.value || taskInfo.value.status !== 'Executing') {
stopTaskPolling();
currentTaskID.value = '';
taskInfo.value = null;
emitTaskChange();
resetDrawerState();
em('close', false);
}
} catch {
stopTaskPolling();
currentTaskID.value = '';
taskInfo.value = null;
emitTaskChange();
resetDrawerState();
em('close', false);
}
};