Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions agent/app/service/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -1189,7 +1189,7 @@ func (u *ContainerService) ListContainerFiles(req dto.ContainerFileReq) ([]dto.C
ctx := context.Background()
stat, err := cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
if err != nil {
return nil, err
return nil, normalizeContainerFileError(err)
}
isDir := stat.Mode.IsDir()
isLink := stat.Mode&os.ModeSymlink != 0
Expand All @@ -1205,7 +1205,7 @@ func (u *ContainerService) ListContainerFiles(req dto.ContainerFileReq) ([]dto.C

output, err := runContainerCommand(cli, req.ContainerID, []string{"ls", "-1A", "--", req.Path})
if err != nil {
return nil, err
return nil, normalizeContainerFileError(err)
}
lines := strings.Split(strings.TrimSpace(output), "\n")
files := make([]dto.ContainerFileInfo, 0, len(lines))
Expand Down Expand Up @@ -1348,7 +1348,7 @@ func (u *ContainerService) GetContainerFileContent(req dto.ContainerFileReq) (*d

stat, err := cli.ContainerStatPath(context.Background(), req.ContainerID, req.Path)
if err != nil {
return nil, err
return nil, normalizeContainerFileError(err)
}
if stat.Mode.IsDir() {
return nil, fmt.Errorf("path %s is directory", req.Path)
Expand Down Expand Up @@ -1395,7 +1395,7 @@ func (u *ContainerService) GetContainerFileSize(req dto.ContainerFileReq) (int64

stat, err := cli.ContainerStatPath(context.Background(), req.ContainerID, req.Path)
if err != nil {
return 0, err
return 0, normalizeContainerFileError(err)
}
if !stat.Mode.IsDir() {
return stat.Size, nil
Expand Down Expand Up @@ -1428,7 +1428,7 @@ func (u *ContainerService) DownloadContainerFile(req dto.ContainerFileReq) (io.R
stat, err := cli.ContainerStatPath(ctx, req.ContainerID, req.Path)
if err != nil {
_ = cli.Close()
return nil, "", "", err
return nil, "", "", normalizeContainerFileError(err)
}

fileName := stat.Name
Expand Down Expand Up @@ -1474,6 +1474,17 @@ func (u *ContainerService) DownloadContainerFile(req dto.ContainerFileReq) (io.R
}, fileName, "application/octet-stream", nil
}

func normalizeContainerFileError(err error) error {
if err == nil {
return nil
}
message := strings.ToLower(err.Error())
if strings.Contains(message, "no such file or directory") || strings.Contains(message, "not found") {
return buserr.New("ErrPathNotFound")
}
return err
}

func runContainerCommand(cli *client.Client, containerID string, command []string) (string, error) {
raw, err := runContainerCommandRaw(cli, containerID, command)
if err != nil {
Expand Down
98 changes: 87 additions & 11 deletions frontend/src/views/container/container/file-browser/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,30 @@
@close="visible = false"
>
<template #content>
<el-form label-position="top">
<el-form label-position="top" @submit.prevent>
<el-form-item>
<div class="path-breadcrumb">
<div v-if="!pathEditing" class="path-breadcrumb" @click="enablePathEditing">
<el-breadcrumb separator="/">
<el-breadcrumb-item>
<el-link type="primary" @click="navigateToPath('/')">
<el-link type="primary" @click.stop="navigateToPath('/')">
<el-icon><HomeFilled /></el-icon>
</el-link>
</el-breadcrumb-item>
<el-breadcrumb-item v-for="item in pathSegments" :key="item.path">
<el-link type="primary" @click="navigateToPath(item.path)">
<el-link type="primary" @click.stop="navigateToPath(item.path)">
{{ item.name }}
</el-link>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<el-input
v-else
ref="pathInputRef"
v-model="pathInput"
class="path-breadcrumb path-input"
@blur="cancelPathEditing"
@keyup.enter.prevent="submitPathInput"
/>
<el-upload ref="uploadRef" :auto-upload="false" :show-file-list="false" :on-change="onUploadChange">
<el-button class="mt-2" :loading="uploading" type="primary" plain>
{{ $t('commons.button.upload') }}
Expand Down Expand Up @@ -103,7 +111,7 @@
</template>

<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, nextTick, ref } from 'vue';
import {
deleteContainerFile,
downloadContainerFile,
Expand All @@ -114,7 +122,7 @@ import {
} from '@/api/modules/container';
import { MsgError, MsgSuccess, MsgWarning } from '@/utils/message';
import i18n from '@/lang';
import { ElMessageBox, UploadFile, UploadInstance } from 'element-plus';
import { ElInput, ElMessageBox, UploadFile, UploadInstance } from 'element-plus';
import { Document, FolderOpened, HomeFilled } from '@element-plus/icons-vue';
import { computeSize2 } from '@/utils/util';

Expand All @@ -124,12 +132,15 @@ const containerID = ref('');
const filePath = ref('/');
const containerFiles = ref<any[]>([]);
const uploadRef = ref<UploadInstance>();
const pathInputRef = ref<InstanceType<typeof ElInput>>();
const uploading = ref(false);
const previewVisible = ref(false);
const previewTitle = ref('');
const previewContent = ref('');
const previewTruncated = ref(false);
const selectedRows = ref<any[]>([]);
const pathEditing = ref(false);
const pathInput = ref('/');
const pathSegments = computed(() => {
const parts = filePath.value.split('/').filter((item) => item);
return parts.map((name, index) => ({
Expand All @@ -149,16 +160,18 @@ const acceptParams = async (params: DrawerProps): Promise<void> => {
containerID.value = params.containerID;
title.value = params.title;
filePath.value = params.workingDir || '/';
pathInput.value = filePath.value;
pathEditing.value = false;
await loadContainerFiles();
};

const loadContainerFiles = async () => {
if (!containerID.value || !filePath.value) {
return;
const fetchContainerFiles = async (path: string) => {
if (!containerID.value || !path) {
return false;
}
await listContainerFiles({
return await listContainerFiles({
containerID: containerID.value,
path: filePath.value,
path,
})
.then((res) => {
containerFiles.value = (res.data || []).map((item) => ({
Expand All @@ -167,22 +180,80 @@ const loadContainerFiles = async () => {
sizeLoading: false,
}));
selectedRows.value = [];
return true;
})
.catch(() => {
containerFiles.value = [];
return false;
});
};

const loadContainerFiles = async () => {
await fetchContainerFiles(filePath.value);
};

const enterDir = async (path: string) => {
filePath.value = path;
await loadContainerFiles();
};

const navigateToPath = async (path: string) => {
filePath.value = path || '/';
pathInput.value = filePath.value;
pathEditing.value = false;
await loadContainerFiles();
};

const getParentPath = (path: string) => {
if (!path || path === '/') {
return '/';
}
const segments = path.split('/').filter((item) => item);
if (segments.length <= 1) {
return '/';
}
return '/' + segments.slice(0, -1).join('/');
};

const enablePathEditing = async () => {
pathInput.value = filePath.value || '/';
pathEditing.value = true;
await nextTick();
pathInputRef.value?.focus();
};

const cancelPathEditing = () => {
pathEditing.value = false;
pathInput.value = filePath.value || '/';
};

const submitPathInput = async () => {
let targetPath = (pathInput.value || '').trim();
if (!targetPath) {
targetPath = '/';
}
if (!targetPath.startsWith('/')) {
targetPath = '/' + targetPath;
}
let currentPath = targetPath;
while (true) {
const success = await fetchContainerFiles(currentPath);
if (success) {
filePath.value = currentPath;
pathInput.value = currentPath;
pathEditing.value = false;
return;
}
if (currentPath === '/') {
filePath.value = '/';
pathInput.value = '/';
pathEditing.value = false;
return;
}
currentPath = getParentPath(currentPath);
}
};

const onUploadChange = async (uploadFile: UploadFile) => {
if (!uploadFile.raw) {
return;
Expand Down Expand Up @@ -331,6 +402,11 @@ defineExpose({
border: 1px solid var(--el-border-color);
border-radius: 4px;
overflow-x: auto;
cursor: text;
}

.path-input {
overflow: visible;
}

.preview-content {
Expand Down
Loading