feat: Support batch import for Docker images (#11640)

* feat: Support batch import for Docker images

* perf: Avoid duplicate Docker client creation in ImageLoad

* style: Remove unnecessary comments in FileList component
This commit is contained in:
Scars
2026-01-19 10:05:17 +08:00
committed by GitHub
parent 3af07a42d6
commit de402ea724
5 changed files with 98 additions and 38 deletions
+2 -2
View File
@@ -21,8 +21,8 @@ type ImageInfo struct {
}
type ImageLoad struct {
TaskID string `json:"taskID"`
Path string `json:"path" validate:"required"`
TaskID string `json:"taskID"`
Paths []string `json:"paths" validate:"required,dive,required"`
}
type ImageBuild struct {
+30 -22
View File
@@ -321,36 +321,44 @@ func (u *ImageService) ImagePull(req dto.ImagePull) error {
}
func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
taskItem, err := task.NewTaskWithOps(req.Path, task.TaskImport, task.TaskScopeImage, req.TaskID, 1)
taskItem, err := task.NewTaskWithOps(strings.Join(req.Paths, ","), task.TaskImport, task.TaskScopeImage, req.TaskID, 1)
if err != nil {
return fmt.Errorf("new task for image import failed, err: %v", err)
}
taskItem.AddSubTask(i18n.GetWithName("TaskImport", req.Path), func(t *task.Task) error {
file, err := os.Open(req.Path)
if err != nil {
return err
}
defer file.Close()
go func() {
client, err := docker.NewDockerClient()
if err != nil {
return err
taskItem.Log("Failed to create Docker client: " + err.Error())
return
}
defer client.Close()
res, err := client.ImageLoad(context.TODO(), file)
if err != nil {
return err
for _, itemPath := range req.Paths {
currentPath := itemPath
itemName := path.Base(currentPath)
taskItem.AddSubTask(i18n.GetWithName("TaskImport", itemName), func(t *task.Task) error {
taskItem.Logf("----------------- %s -----------------", itemName)
file, err := os.Open(currentPath)
if err != nil {
return err
}
defer file.Close()
res, err := client.ImageLoad(context.TODO(), file)
if err != nil {
return err
}
defer res.Body.Close()
content, err := io.ReadAll(res.Body)
if err != nil {
return err
}
if strings.Contains(string(content), "Error") {
return errors.New(string(content))
}
return nil
}, nil)
}
defer res.Body.Close()
content, err := io.ReadAll(res.Body)
if err != nil {
return err
}
if strings.Contains(string(content), "Error") {
return errors.New(string(content))
}
return nil
}, nil)
go func() {
_ = taskItem.Execute()
}()
return nil
+1 -1
View File
@@ -224,7 +224,7 @@ export namespace Container {
}
export interface ImageLoad {
taskID: string;
path: string;
paths: Array<string>;
}
export interface ImageSave {
taskID: string;
+57 -9
View File
@@ -105,7 +105,7 @@
</el-table>
</div>
<div class="file-list-bottom">
<div v-if="selectRow?.path">
<div v-if="!isMultiple && selectRow?.path">
{{ $t('file.currentSelect') }}
<el-tooltip :content="selectRow.path" placement="top">
<el-tag type="success">
@@ -115,6 +115,23 @@
</el-tag>
</el-tooltip>
</div>
<div v-if="isMultiple && selectRows.length > 0">
{{ $t('file.currentSelect') }}
<el-tag
v-for="(item, index) in selectRows"
:key="item.path"
type="success"
closable
class="mr-1 mb-1"
@close="removeSelected(index)"
>
<el-tooltip :content="item.path" placement="top">
<div class="path">
<span>{{ item.path }}</span>
</div>
</el-tooltip>
</el-tag>
</div>
</div>
<template #footer>
@@ -142,6 +159,8 @@ const loading = ref(false);
const paths = ref<string[]>([]);
const req = reactive({ path: '/', expand: true, page: 1, pageSize: 300, showHidden: true });
const selectRow = ref({ path: '', name: '' });
const selectRows = ref<Array<{ path: string; name: string }>>([]);
const isMultiple = ref(false);
const rowRefs = ref();
const open = ref(false);
const newFolder = ref();
@@ -160,16 +179,20 @@ const form = reactive({
});
interface DialogProps {
path: string;
path?: string;
dir: boolean;
isAll: boolean;
disabled: boolean;
isAll?: boolean;
disabled?: boolean;
multiple?: boolean;
}
const acceptParams = (props: DialogProps): void => {
form.path = props.path || '/';
form.dir = props.dir;
form.isAll = props.isAll;
form.disabled = props.disabled;
form.isAll = props.isAll || false;
form.disabled = props.disabled || false;
isMultiple.value = props.multiple || false;
selectRows.value = [];
selectRow.value = { path: '', name: '' };
openPage();
req.path = form.path;
oldUrl.value = form.path;
@@ -178,8 +201,15 @@ const acceptParams = (props: DialogProps): void => {
};
const selectFile = () => {
if (selectRow.value) {
em('choose', selectRow.value.path);
if (isMultiple.value) {
em(
'choose',
selectRows.value.map((r) => r.path),
);
} else {
if (selectRow.value) {
em('choose', selectRow.value.path);
}
}
handleClose();
};
@@ -187,6 +217,7 @@ const selectFile = () => {
const handleClose = () => {
open.value = false;
selectRow.value = { path: '', name: '' };
selectRows.value = [];
};
const openPage = () => {
@@ -221,8 +252,21 @@ const openDir = async (row: File.File, column: any, event: any) => {
});
return;
}
// Handle file click
const fullPath = (req.path === '/' ? req.path : req.path + '/') + row.name;
if (isMultiple.value) {
// Multiple mode: toggle selection
const index = selectRows.value.findIndex((r) => r.path === fullPath);
if (index > -1) {
selectRows.value.splice(index, 1);
} else {
selectRows.value.push({ path: fullPath, name: row.name });
}
return;
}
// Single mode: original behavior
if (form.isAll || !form.dir) {
selectRow.value.path = (req.path === '/' ? req.path : req.path + '/') + row.name;
selectRow.value.path = fullPath;
return;
}
selectRow.value.path = '';
@@ -419,6 +463,10 @@ onUpdated(() => {
search(req);
});
const removeSelected = (index: number) => {
selectRows.value.splice(index, 1);
};
defineExpose({
acceptParams,
});
@@ -4,7 +4,7 @@
<el-form-item :label="$t('container.path')" :rules="Rules.requiredInput" prop="path">
<el-input v-model="form.path">
<template #prepend>
<el-button icon="Folder" @click="fileRef.acceptParams({ dir: false })" />
<el-button icon="Folder" @click="fileRef.acceptParams({ dir: false, multiple: true })" />
</template>
</el-input>
</el-form-item>
@@ -42,12 +42,14 @@ const taskLogRef = ref();
const loadVisible = ref(false);
const form = reactive({
path: '',
paths: [] as string[],
taskID: '',
});
const acceptParams = () => {
loadVisible.value = true;
form.path = '';
form.paths = [];
};
const handleClose = () => {
loadVisible.value = false;
@@ -64,7 +66,7 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
if (!valid) return;
loading.value = true;
form.taskID = newUUID();
await imageLoad(form)
await imageLoad({ paths: form.paths, taskID: form.taskID })
.then(() => {
loading.value = false;
loadVisible.value = false;
@@ -82,8 +84,10 @@ const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const loadLoadDir = async (path: string) => {
form.path = path;
const loadLoadDir = async (paths: string | string[]) => {
const newPaths = Array.isArray(paths) ? paths : [paths];
form.paths = [...new Set([...form.paths, ...newPaths])];
form.path = form.paths.join('; ');
};
defineExpose({