mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-04 15:49:49 +00:00
336 lines
9.1 KiB
Go
336 lines
9.1 KiB
Go
package util
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
syspath "path"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
func Dir(path string) string {
|
|
dir := syspath.Dir(path)
|
|
if dir == "." {
|
|
return ""
|
|
}
|
|
return dir
|
|
}
|
|
|
|
func Filepath(path string, originName string, customName string) string {
|
|
if customName == "" {
|
|
customName = originName
|
|
}
|
|
return syspath.Join(path, customName)
|
|
}
|
|
|
|
// SafeRemove remove file safely, ignoring errors if the path does not exist.
|
|
func SafeRemove(name string) error {
|
|
if err := os.Remove(name); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CheckDuplicateAndRename rename duplicate file, add suffix (1) (2) ...
|
|
// if file name is a.txt, rename to a (1).txt
|
|
// if directory name is a, rename to a (1)
|
|
// return new name
|
|
func CheckDuplicateAndRename(path string) (string, error) {
|
|
dir := syspath.Dir(path)
|
|
name := syspath.Base(path)
|
|
|
|
// if file not exists, return directly
|
|
_, err := os.Stat(path)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return name, nil
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
ext := syspath.Ext(name)
|
|
var nameTpl string
|
|
// Special case: if the extension is the entire filename (like .gitignore),
|
|
// or if index of last dot is 0 (starts with dot), treat it as no extension
|
|
if ext == "" || ext == name || (len(ext) > 0 && strings.LastIndex(name, ".") == 0) {
|
|
// No extension or hidden file without extension
|
|
nameTpl = name + " (%d)"
|
|
} else {
|
|
// Has extension
|
|
nameWithoutExt := name[:len(name)-len(ext)]
|
|
nameTpl = nameWithoutExt + " (%d)" + ext
|
|
}
|
|
for i := 1; ; i++ {
|
|
newName := fmt.Sprintf(nameTpl, i)
|
|
newPath := syspath.Join(dir, newName)
|
|
if _, err := os.Stat(newPath); os.IsNotExist(err) {
|
|
return newName, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// CopyDir Copy all files to the target directory, if the file already exists, it will be overwritten.
|
|
// Remove target file if the source file is not exist.
|
|
func CopyDir(source string, target string, excludeDir ...string) error {
|
|
if err := os.MkdirAll(target, 0755); err != nil {
|
|
return err
|
|
}
|
|
if err := filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() {
|
|
if len(excludeDir) > 0 {
|
|
for _, dir := range excludeDir {
|
|
if info.IsDir() && info.Name() == dir {
|
|
return filepath.SkipDir
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
relPath, err := filepath.Rel(source, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
targetPath := filepath.Join(target, relPath)
|
|
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
|
return err
|
|
}
|
|
if err := copyForce(path, targetPath); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := filepath.Walk(target, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() {
|
|
if len(excludeDir) > 0 {
|
|
for _, dir := range excludeDir {
|
|
if info.IsDir() && info.Name() == dir {
|
|
return filepath.SkipDir
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
relPath, err := filepath.Rel(target, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
targetPath := filepath.Join(target, relPath)
|
|
sourcePath := filepath.Join(source, relPath)
|
|
// if source file is not exist, remove target file
|
|
if _, err := os.Stat(sourcePath); os.IsNotExist(err) {
|
|
if err := SafeRemove(targetPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// copy file, if the target file already exists, it will be overwritten.
|
|
func copyForce(source string, target string) error {
|
|
sourceFile, err := os.Open(source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer sourceFile.Close()
|
|
|
|
targetFile, err := os.Create(target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer targetFile.Close()
|
|
|
|
_, err = io.Copy(targetFile, sourceFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func CreateDirIfNotExist(dir string) error {
|
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
|
return os.MkdirAll(dir, 0o777)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsExistsFile check file exists and is a file
|
|
func IsExistsFile(path string) bool {
|
|
info, err := os.Stat(path)
|
|
// if file exists and is a file
|
|
if err == nil && !info.IsDir() {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
const (
|
|
// MaxFilenameLength is the maximum length in bytes for a filename
|
|
MaxFilenameLength = 100
|
|
// maxExtensionLength is the maximum length in bytes for a file extension
|
|
// to be treated as a valid extension. Extensions longer than this are
|
|
// treated as part of the filename to avoid edge cases.
|
|
maxExtensionLength = 20
|
|
)
|
|
|
|
// SafeFilename sanitizes a filename by replacing invalid characters and truncating to a safe length.
|
|
// It performs two operations:
|
|
// 1. Replaces invalid path characters (platform-specific) with underscores
|
|
// 2. Truncates filename to MaxFilenameLength bytes while preserving the file extension
|
|
// The function handles UTF-8 multi-byte characters correctly by truncating at valid boundaries.
|
|
func SafeFilename(filename string) string {
|
|
if filename == "" {
|
|
return ""
|
|
}
|
|
|
|
// Step 1: Replace invalid characters
|
|
for _, char := range invalidPathChars {
|
|
filename = strings.ReplaceAll(filename, char, "_")
|
|
}
|
|
|
|
// Step 2: Truncate if needed
|
|
if len(filename) <= MaxFilenameLength {
|
|
return filename
|
|
}
|
|
|
|
// Find the extension (last dot in filename)
|
|
ext := ""
|
|
lastDot := strings.LastIndex(filename, ".")
|
|
|
|
// Only treat as extension if:
|
|
// 1. There is a dot
|
|
// 2. The dot is not at the start (not a hidden file like .gitignore)
|
|
// 3. The extension is reasonable length (< maxExtensionLength bytes) to avoid edge cases
|
|
if lastDot > 0 && lastDot < len(filename)-1 && len(filename)-lastDot < maxExtensionLength {
|
|
ext = filename[lastDot:]
|
|
filename = filename[:lastDot]
|
|
}
|
|
|
|
// Calculate how much space we have for the base name
|
|
availableLength := MaxFilenameLength - len(ext)
|
|
|
|
// Ensure we have at least some space for the base name
|
|
if availableLength < 1 {
|
|
// Extension itself is too long or no room, just truncate everything at byte boundary
|
|
return truncateAtValidUTF8Boundary(filename+ext, MaxFilenameLength)
|
|
}
|
|
|
|
// Truncate the base name at a valid UTF-8 boundary
|
|
truncatedBase := truncateAtValidUTF8Boundary(filename, availableLength)
|
|
|
|
return truncatedBase + ext
|
|
}
|
|
|
|
// ReplaceInvalidFilename replace invalid path characters
|
|
// Deprecated: Use SafeFilename instead which also handles length truncation
|
|
func ReplaceInvalidFilename(path string) string {
|
|
if path == "" {
|
|
return ""
|
|
}
|
|
for _, char := range invalidPathChars {
|
|
path = strings.ReplaceAll(path, char, "_")
|
|
}
|
|
return path
|
|
}
|
|
|
|
// TruncateFilename truncates a filename to a maximum byte length while preserving the extension.
|
|
// Deprecated: Use SafeFilename instead which also handles invalid character replacement
|
|
func TruncateFilename(filename string, maxLength int) string {
|
|
// If already short enough, return as-is
|
|
if len(filename) <= maxLength {
|
|
return filename
|
|
}
|
|
|
|
// Find the extension (last dot in filename)
|
|
ext := ""
|
|
lastDot := strings.LastIndex(filename, ".")
|
|
|
|
// Only treat as extension if:
|
|
// 1. There is a dot
|
|
// 2. The dot is not at the start (not a hidden file like .gitignore)
|
|
// 3. The extension is reasonable length (< maxExtensionLength bytes) to avoid edge cases
|
|
if lastDot > 0 && lastDot < len(filename)-1 && len(filename)-lastDot < maxExtensionLength {
|
|
ext = filename[lastDot:]
|
|
filename = filename[:lastDot]
|
|
}
|
|
|
|
// Calculate how much space we have for the base name
|
|
availableLength := maxLength - len(ext)
|
|
|
|
// Ensure we have at least some space for the base name
|
|
if availableLength < 1 {
|
|
// Extension itself is too long or no room, just truncate everything at byte boundary
|
|
return truncateAtValidUTF8Boundary(filename+ext, maxLength)
|
|
}
|
|
|
|
// Truncate the base name at a valid UTF-8 boundary
|
|
truncatedBase := truncateAtValidUTF8Boundary(filename, availableLength)
|
|
|
|
return truncatedBase + ext
|
|
}
|
|
|
|
// truncateAtValidUTF8Boundary truncates a string to at most maxBytes,
|
|
// ensuring we don't cut in the middle of a UTF-8 character
|
|
func truncateAtValidUTF8Boundary(s string, maxBytes int) string {
|
|
if len(s) <= maxBytes {
|
|
return s
|
|
}
|
|
|
|
// Truncate at byte position
|
|
truncated := s[:maxBytes]
|
|
|
|
// Find the last valid UTF-8 character boundary
|
|
// Walk backwards to find where the last complete character ends
|
|
for len(truncated) > 0 {
|
|
// Check if this is a valid UTF-8 string
|
|
if utf8.ValidString(truncated) {
|
|
return truncated
|
|
}
|
|
// Remove one byte and try again
|
|
truncated = truncated[:len(truncated)-1]
|
|
}
|
|
|
|
return truncated
|
|
}
|
|
|
|
// ReplacePathPlaceholders replaces date placeholders in a path with actual values
|
|
// Supported placeholders:
|
|
// - %year% - Current year (e.g., 2025)
|
|
// - %month% - Current month (01-12)
|
|
// - %day% - Current day (01-31)
|
|
// - %date% - Full date format (2025-01-01)
|
|
func ReplacePathPlaceholders(path string) string {
|
|
if path == "" {
|
|
return ""
|
|
}
|
|
|
|
now := time.Now()
|
|
year := fmt.Sprintf("%d", now.Year())
|
|
month := fmt.Sprintf("%02d", now.Month())
|
|
day := fmt.Sprintf("%02d", now.Day())
|
|
date := fmt.Sprintf("%s-%s-%s", year, month, day)
|
|
|
|
path = strings.ReplaceAll(path, "%year%", year)
|
|
path = strings.ReplaceAll(path, "%month%", month)
|
|
path = strings.ReplaceAll(path, "%day%", day)
|
|
path = strings.ReplaceAll(path, "%date%", date)
|
|
|
|
return path
|
|
}
|