fix: filesystem error when creating long filename (#1240)

This commit is contained in:
Copilot authored and GitHub committed 2026-01-21 23:12:38 +08:00
1 parent af1c8b2719
commit bad7f1d06c
4 files changed
+303 -5

No files matched your search

+119
View File
@@ -9,6 +9,7 @@ import (
"path/filepath"
"strings"
"time"
"unicode/utf8"
)
func Dir(path string) string {
@@ -179,7 +180,65 @@ func IsExistsFile(path string) bool {
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 ""
@@ -190,6 +249,66 @@ func ReplaceInvalidFilename(path string) string {
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)