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)
158 lines
3.8 KiB
Go
158 lines
3.8 KiB
Go
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
|
||
}
|