feat: add registry config, image upload/download, and OCI format support

Backend:
- Add registry_address configuration API (GET/POST)
- Add tar image upload with OCI and Docker format support
- Add image download with streaming optimization
- Fix blob download using c.Send (Fiber v3 SendStream bug)
- Add registry_address prefix stripping for all OCI v2 endpoints
- Add AGENTS.md for project documentation

Frontend:
- Add settings store with Snackbar notifications
- Add image upload dialog with progress bar
- Add download state tracking with multi-stage feedback
- Replace alert() with MUI Snackbar messages
- Display image names without registry_address prefix

🤖 Generated with [Qoder](https://qoder.com)
This commit is contained in:
loveuer
2025-11-10 16:28:58 +08:00
parent 29088a6b54
commit 9780a2b028
35 changed files with 3065 additions and 91 deletions

157
pkg/tool/file.go Normal file
View File

@@ -0,0 +1,157 @@
package tool
import (
"crypto/md5"
"fmt"
"io"
"os"
"path/filepath"
)
// FileMD5 calculate file md5
// - if file not exist, return ""
// - if _path is dir, return ""
func FileMD5(_path string) string {
// 检查文件是否存在
fileInfo, err := os.Stat(_path)
if err != nil {
if os.IsNotExist(err) {
return ""
}
// 其他错误也返回空字符串
return ""
}
// 检查是否是目录
if fileInfo.IsDir() {
return ""
}
// 打开文件
file, err := os.Open(_path)
if err != nil {
return ""
}
defer file.Close()
// 创建MD5哈希计算器
hash := md5.New()
// 将文件内容复制到哈希计算器
if _, err := io.Copy(hash, file); err != nil {
return ""
}
// 计算并返回MD5哈希值十六进制字符串
return fmt.Sprintf("%x", hash.Sum(nil))
}
// CopyFile copies a file from src to dst.
// Returns an error if source/destination are the same, source isn't a regular file,
// or any step in the copy process fails.
func CopyFile(src, dst string) error {
// Open source file
srcFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open source: %w", err)
}
defer srcFile.Close()
// Get source file metadata
srcInfo, err := srcFile.Stat()
if err != nil {
return fmt.Errorf("failed to get source info: %w", err)
}
// Verify source is a regular file
if !srcInfo.Mode().IsRegular() {
return fmt.Errorf("source is not a regular file")
}
// Check if source and destination are the same file
if same, err := sameFile(src, dst, srcInfo); same {
return fmt.Errorf("source and destination are the same file")
} else if err != nil {
return err
}
// Create destination directory structure
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return fmt.Errorf("failed to create destination directory: %w", err)
}
// Create destination file with source permissions
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode())
if err != nil {
return fmt.Errorf("failed to create destination: %w", err)
}
// Copy contents and handle destination close errors
_, err = io.Copy(dstFile, srcFile)
if closeErr := dstFile.Close(); closeErr != nil && err == nil {
err = fmt.Errorf("failed to close destination: %w", closeErr)
}
if err != nil {
return fmt.Errorf("copy failed: %w", err)
}
return nil
}
// sameFile checks if src and dst refer to the same file using device/inode numbers
func sameFile(src, dst string, srcInfo os.FileInfo) (bool, error) {
dstInfo, err := os.Stat(dst)
if os.IsNotExist(err) {
return false, nil // Destination doesn't exist
}
if err != nil {
return false, err // Other errors
}
return os.SameFile(srcInfo, dstInfo), nil
}
func CopyDir(src, dst string) error {
// todo: copy src dir to dst dir recursively
// if dst is not exist, create it
// if file exist, overwrite it
srcInfo, err := os.Stat(src)
if err != nil {
return fmt.Errorf("stat src dir failed: %w", err)
}
if !srcInfo.IsDir() {
return fmt.Errorf("source is not a directory")
}
// Create destination directory if it does not exist
if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil {
return fmt.Errorf("failed to create destination directory: %w", err)
}
entries, err := os.ReadDir(src)
if err != nil {
return fmt.Errorf("failed to read source directory: %w", err)
}
for _, entry := range entries {
srcPath := filepath.Join(src, entry.Name())
dstPath := filepath.Join(dst, entry.Name())
info, err := entry.Info()
if err != nil {
return fmt.Errorf("failed to get info for %s: %w", srcPath, err)
}
if info.IsDir() {
// Recursively copy subdirectory
if err := CopyDir(srcPath, dstPath); err != nil {
return err
}
} else {
// Copy file, overwrite if exists
if err := CopyFile(srcPath, dstPath); err != nil {
return err
}
}
}
return nil
}