mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 08:00:53 +00:00
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:
@@ -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
@@ -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
|
||||
|
||||
@@ -224,7 +224,7 @@ export namespace Container {
|
||||
}
|
||||
export interface ImageLoad {
|
||||
taskID: string;
|
||||
path: string;
|
||||
paths: Array<string>;
|
||||
}
|
||||
export interface ImageSave {
|
||||
taskID: string;
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user