Skip to content
Open
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
46 changes: 30 additions & 16 deletions agent/app/service/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"os"
Expand All @@ -28,6 +27,7 @@ import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/moby/go-archive"
)

Expand Down Expand Up @@ -325,18 +325,16 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
}

go func() {
client, err := docker.NewDockerClient()
if err != nil {
taskItem.Log("Failed to create Docker client: " + err.Error())
return
}
defer client.Close()

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)
client, err := docker.NewDockerClient()
if err != nil {
return err
}
defer client.Close()
file, err := os.Open(currentPath)
if err != nil {
return err
Expand All @@ -347,21 +345,37 @@ func (u *ImageService) ImageLoad(req dto.ImageLoad) error {
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
return consumeImageLoadResponse(res.Body, taskItem.Log)
}, nil)
}
_ = taskItem.Execute()
}()
return nil
}

// Docker may report load failures in a successful HTTP response's JSON stream.
func consumeImageLoadResponse(reader io.Reader, log func(string)) error {
decoder := json.NewDecoder(reader)
for {
var message jsonmessage.JSONMessage
if err := decoder.Decode(&message); err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
if message.Error != nil && message.Error.Message != "" {
return message.Error
}
if message.ErrorMessage != "" {
return errors.New(message.ErrorMessage)
}
if text := strings.TrimSpace(message.Stream); text != "" {
log(text)
}
}
}

func (u *ImageService) ImageSave(req dto.ImageSave) error {
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskExport, task.TaskScopeImage, req.TaskID, 1)
if err != nil {
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/api/modules/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,13 @@ export const imagePull = (params: Container.ImagePull) => {
export const imagePush = (params: Container.ImagePush) => {
return http.post<string>(`/containers/image/push`, params);
};
export const imageLoad = (params: Container.ImageLoad) => {
return http.post(`/containers/image/load`, params, TimeoutEnum.T_10M);
export const imageLoad = (params: Container.ImageLoad, currentNode?: string) => {
return http.post(
`/containers/image/load`,
params,
TimeoutEnum.T_10M,
currentNode ? { CurrentNode: currentNode } : undefined,
);
};
export const imageSave = (params: Container.ImageSave) => {
return http.post(`/containers/image/save`, params, TimeoutEnum.T_10M);
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/lang/modules/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1878,6 +1878,11 @@ const message = {
imageName: 'Image name',
pull: 'Pull',
path: 'Path',
imageUploadDrop: 'Drop an image file here, or click to upload',
imageUploadHelper:
'Any file format is allowed. Docker validates the contents. Uploaded files are retained for retrying imports or manual deletion.',
imageUploadEmpty: 'The file is empty. Select a valid image archive.',
imageImportSubmitted: 'Image import task submitted. Check the task log for the result.',
importImage: 'Import',
buildArgs: 'Build Arguments',
imageBuild: 'Build',
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/lang/modules/es-es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1902,6 +1902,12 @@ const message = {
imageName: 'Nombre de la imagen',
pull: 'Descargar',
path: 'Ruta',
imageUploadDrop: 'Arrastre un archivo de imagen aquí o haga clic para subirlo',
imageUploadHelper:
'Se permite cualquier formato de archivo. Docker valida el contenido. Los archivos subidos se conservan para reintentar la importación o eliminarlos manualmente.',
imageUploadEmpty: 'El archivo está vacío. Seleccione un archivo de imagen válido.',
imageImportSubmitted:
'Tarea de importación de imagen enviada. Consulte el registro de la tarea para ver el resultado.',
importImage: 'Importar',
buildArgs: 'Argumentos de Construcción',
imageBuild: 'Construir',
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/lang/modules/fa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1862,6 +1862,11 @@ const message = {
imageName: 'نام تصویر',
pull: 'دریافت',
path: 'مسیر',
imageUploadDrop: 'فایل ایمیج را اینجا بکشید یا برای بارگذاری کلیک کنید',
imageUploadHelper:
'همه قالب‌های فایل مجاز هستند. Docker محتوای فایل را اعتبارسنجی می‌کند. فایل‌های بارگذاری‌شده برای تلاش مجدد جهت وارد کردن یا حذف دستی نگه داشته می‌شوند.',
imageUploadEmpty: 'فایل خالی است. یک فایل ایمیج معتبر انتخاب کنید.',
imageImportSubmitted: 'وظیفه وارد کردن ایمیج ثبت شد. برای مشاهده نتیجه، گزارش وظیفه را بررسی کنید.',
importImage: 'واردات',
buildArgs: 'آرگومان‌های ساخت',
imageBuild: 'ساخت',
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/lang/modules/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1859,6 +1859,11 @@ const message = {
imageName: '画像名',
pull: '引く',
path: 'パス',
imageUploadDrop: 'イメージファイルをここにドラッグするか、クリックしてアップロードしてください',
imageUploadHelper:
'ファイル形式に制限はありません。Docker が内容を検証します。アップロードしたファイルは保持されるため、インポートを再試行したり、手動で削除したりできます。',
imageUploadEmpty: 'ファイルが空です。有効なイメージファイルを選択してください。',
imageImportSubmitted: 'イメージのインポートタスクを送信しました。結果はタスクログで確認してください。',
importImage: '輸入',
buildArgs: 'ビルド引数',
imageBuild: '建てる',
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/lang/modules/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1841,6 +1841,11 @@ const message = {
imageName: '이미지 이름',
pull: '풀',
path: '경로',
imageUploadDrop: '이미지 파일을 여기로 끌어 놓거나 클릭하여 업로드하세요',
imageUploadHelper:
'파일 형식에는 제한이 없습니다. Docker가 내용을 검증합니다. 업로드한 파일은 보관되므로 가져오기를 다시 시도하거나 수동으로 삭제할 수 있습니다.',
imageUploadEmpty: '빈 파일입니다. 유효한 이미지 파일을 선택하세요.',
imageImportSubmitted: '이미지 가져오기 작업이 제출되었습니다. 작업 로그에서 결과를 확인하세요.',
importImage: '가져오기',
buildArgs: '빌드 인수',
imageBuild: '이미지 빌드',
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/lang/modules/lo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1831,6 +1831,11 @@ const message = {
imageName: 'ຊື່ Image',
pull: 'ດຶງຂໍ້ມູນ (Pull)',
path: 'ເສັ້ນທາງ',
imageUploadDrop: 'ລາກໄຟລ໌ອິມເມຈມາວາງບ່ອນນີ້ ຫຼື ຄລິກເພື່ອອັບໂຫຼດ',
imageUploadHelper:
'ບໍ່ຈຳກັດຮູບແບບໄຟລ໌. Docker ຈະກວດສອບເນື້ອຫາ. ໄຟລ໌ທີ່ອັບໂຫຼດຈະຖືກເກັບໄວ້ ເພື່ອໃຫ້ສາມາດລອງນຳເຂົ້າອີກຄັ້ງ ຫຼື ລຶບດ້ວຍຕົນເອງ.',
imageUploadEmpty: 'ໄຟລ໌ຫວ່າງເປົ່າ. ກະລຸນາເລືອກໄຟລ໌ອິມເມຈທີ່ຖືກຕ້ອງ.',
imageImportSubmitted: 'ສົ່ງວຽກນຳເຂົ້າອິມເມຈແລ້ວ. ກະລຸນາເບິ່ງບັນທຶກວຽກເພື່ອກວດສອບຜົນ.',
importImage: 'ນຳເຂົ້າ',
buildArgs: 'Build Arguments',
imageBuild: 'ສ້າງ (Build)',
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/lang/modules/ms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1891,6 +1891,11 @@ const message = {
imageName: 'Nama imej',
pull: 'Tarik',
path: 'Laluan',
imageUploadDrop: 'Seret fail imej ke sini atau klik untuk memuat naik',
imageUploadHelper:
'Semua format fail dibenarkan. Docker mengesahkan kandungannya. Fail yang dimuat naik disimpan untuk mencuba semula import atau dipadam secara manual.',
imageUploadEmpty: 'Fail kosong. Pilih arkib imej yang sah.',
imageImportSubmitted: 'Tugas import imej telah dihantar. Semak log tugas untuk melihat hasilnya.',
importImage: 'Import',
buildArgs: 'Argumen Binaan',
imageBuild: 'Bina',
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/lang/modules/pt-br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1893,6 +1893,11 @@ const message = {
imageName: 'Nome da imagem',
pull: 'Puxar',
path: 'Caminho',
imageUploadDrop: 'Arraste um arquivo de imagem para cá ou clique para enviar',
imageUploadHelper:
'Qualquer formato de arquivo é permitido. O Docker valida o conteúdo. Os arquivos enviados são mantidos para novas tentativas de importação ou exclusão manual.',
imageUploadEmpty: 'O arquivo está vazio. Selecione um arquivo de imagem válido.',
imageImportSubmitted: 'Tarefa de importação de imagem enviada. Consulte o log da tarefa para ver o resultado.',
importImage: 'Importar',
buildArgs: 'Argumentos de Build',
imageBuild: 'Construção de imagem',
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/lang/modules/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1883,6 +1883,11 @@ const message = {
imageName: 'Имя образа',
pull: 'Загрузить',
path: 'Путь',
imageUploadDrop: 'Перетащите файл образа сюда или нажмите для загрузки',
imageUploadHelper:
'Допускаются файлы любого формата. Docker проверяет содержимое. Загруженные файлы сохраняются для повторного импорта или удаления вручную.',
imageUploadEmpty: 'Файл пуст. Выберите корректный архив образа.',
imageImportSubmitted: 'Задача импорта образа отправлена. Результат можно посмотреть в журнале задачи.',
importImage: 'Импорт',
buildArgs: 'Аргументы Сборки',
imageBuild: 'Сборка',
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/lang/modules/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1892,6 +1892,11 @@ const message = {
imageName: 'İmaj adı',
pull: 'Çek',
path: 'Yol',
imageUploadDrop: 'Bir imaj dosyasını buraya sürükleyin veya yüklemek için tıklayın',
imageUploadHelper:
'Tüm dosya biçimlerine izin verilir. Docker içeriği doğrular. Yüklenen dosyalar, içe aktarmayı yeniden denemek veya elle silmek için saklanır.',
imageUploadEmpty: 'Dosya boş. Geçerli bir imaj arşivi seçin.',
imageImportSubmitted: 'İmajı içe aktarma görevi gönderildi. Sonuç için görev günlüğünü kontrol edin.',
importImage: 'İçe aktar',
buildArgs: 'Derleme Argümanları',
imageBuild: 'Oluştur',
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/lang/modules/zh-Hant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1781,6 +1781,10 @@ const message = {
delInsecureHelper: '刪除授信需要重新啟動 docker 服務,是否刪除?',
pull: '拉取',
path: '路徑',
imageUploadDrop: '拖曳映像檔案至此處,或點擊上傳',
imageUploadHelper: '不限制檔案格式,由 Docker 驗證內容。上傳的檔案會保留,可重試匯入或手動刪除。',
imageUploadEmpty: '不能上傳空檔案,請選擇有效的映像檔案。',
imageImportSubmitted: '映像匯入任務已提交,請查看任務日誌',
importImage: '匯入映像',
imageBuild: '構建映像',
buildArgs: '構建參數',
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/lang/modules/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1801,6 +1801,10 @@ const message = {
delInsecureHelper: '删除授信需要重启 docker 服务,是否删除?',
pull: '拉取',
path: '路径',
imageUploadDrop: '拖拽镜像文件到此处,或点击上传',
imageUploadHelper: '不限制文件格式,由 Docker 校验内容。上传的文件会保留,可重试导入或手动删除。',
imageUploadEmpty: '不能上传空文件,请选择有效的镜像文件。',
imageImportSubmitted: '镜像导入任务已提交,请查看任务日志',
importImage: '导入镜像',
imageBuild: '构建镜像',
buildArgs: '构建参数',
Expand Down
57 changes: 57 additions & 0 deletions frontend/src/utils/chunk-upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { chunkUploadFileData, type FileUploadRequestConfig } from '@/api/modules/files';

export const CHUNK_SIZE = 5 * 1024 * 1024;
const MAX_CHUNK_RETRIES = 3;

const shouldRetryChunkUpload = (error: unknown) => {
const item = error as {
code?: string | number;
data?: { retryable?: boolean };
response?: { status?: number };
};
if (item?.code === 'ERR_CANCELED') {
return false;
}
if (typeof item?.data?.retryable === 'boolean') {
return item.data.retryable;
}
const status = item?.response?.status ?? (typeof item?.code === 'number' ? item.code : undefined);
return !status || status === 408 || status === 429 || status >= 500;
};

const waitForChunkRetry = (attempt: number, signal: AbortSignal) => {
return new Promise<void>((resolve) => {
if (signal.aborted) {
resolve();
return;
}
const timer = window.setTimeout(done, 500 * 2 ** attempt);
function done() {
window.clearTimeout(timer);
signal.removeEventListener('abort', done);
resolve();
}
signal.addEventListener('abort', done, { once: true });
});
};

export const uploadChunkWithRetry = async (
formData: FormData,
config: FileUploadRequestConfig,
signal: AbortSignal,
) => {
let retryCount = 0;
while (true) {
signal.throwIfAborted();
try {
await chunkUploadFileData(formData, config);
return;
} catch (error) {
if (signal.aborted || retryCount >= MAX_CHUNK_RETRIES || !shouldRetryChunkUpload(error)) {
throw error;
}
await waitForChunkRetry(retryCount, signal);
retryCount++;
}
}
};
Loading
Loading