Rewrite moving algo

This commit is contained in:
2025-07-23 17:37:59 +03:00
parent 6c010d23c7
commit b787e81823

114
main.go
View File

@@ -22,10 +22,10 @@ type Config struct {
DryRun bool DryRun bool
} }
type FileInfo struct { type MoveOperation struct {
Path string SourcePath string
DateTaken *time.Time DestPath string
Hash string DropSource bool
} }
func main() { func main() {
@@ -37,7 +37,7 @@ func main() {
// Получаем позиционные аргументы // Получаем позиционные аргументы
args := flag.Args() args := flag.Args()
if len(args) != 2 { if len(args) != 2 {
fmt.Println("Usage: photorg <source_dir> <dest_dir> [-dry-run]") fmt.Println("Usage: photorg [-dry-run] <source_dir> <dest_dir>")
fmt.Println("\nArguments:") fmt.Println("\nArguments:")
fmt.Println(" source_dir Source directory to scan for photos") fmt.Println(" source_dir Source directory to scan for photos")
fmt.Println(" dest_dir Destination directory to organize photos") fmt.Println(" dest_dir Destination directory to organize photos")
@@ -93,38 +93,37 @@ func validateDirectories(sourceDir, destDir string) error {
} }
func organizePhotos(config Config) error { func organizePhotos(config Config) error {
return filepath.Walk(config.SourceDir, func(path string, info os.FileInfo, err error) error { return filepath.Walk(config.SourceDir, func(sourcePath string, sourceInfo os.FileInfo, err error) error {
if err != nil { if err != nil {
log.Printf("Error accessing %s: %v", path, err) log.Printf("Error accessing %s: %v", sourcePath, err)
return nil return nil
} }
if info.IsDir() { if sourceInfo.IsDir() {
return nil return nil
} }
// Проверяем, является ли файл изображением или видео // Проверяем, является ли файл изображением или видео
if !isMediaFile(path) { if !isMediaFile(sourcePath) {
return nil return nil
} }
fileInfo, err := analyzeFile(path) captureTime := extractDateFromMetadata(sourcePath)
moveOp, err := planMovement(config.DestDir, sourcePath, captureTime)
if err != nil { if err != nil {
log.Printf("Error analyzing file %s: %v", path, err) return err
return nil
} }
destPath := generateDestinationPath(config.DestDir, fileInfo)
if config.DryRun { if config.DryRun {
fmt.Printf("[DRY RUN] Would move: %s -> %s\n", path, destPath) fmt.Printf("[DRY RUN] Would move: %s -> %s\n", moveOp.SourcePath, moveOp.DestPath)
return nil return nil
} }
if err := moveFile(fileInfo, destPath); err != nil { if err := moveFile(moveOp); err != nil {
log.Printf("Error moving file %s: %v", path, err) log.Printf("Error moving file %s: %v", moveOp.SourcePath, err)
} else { } else {
fmt.Printf("Moved: %s -> %s\n", path, destPath) fmt.Printf("Moved: %s -> %s\n", moveOp.SourcePath, moveOp.DestPath)
} }
return nil return nil
@@ -144,24 +143,6 @@ func isMediaFile(path string) bool {
return slices.Contains(mediaExtensions, ext) return slices.Contains(mediaExtensions, ext)
} }
func analyzeFile(path string) (*FileInfo, error) {
hash, err := calculateFileHash(path)
if err == nil {
return nil, err
}
// Пытаемся извлечь дату из метаданных
dateTaken := extractDateFromMetadata(path)
fileInfo := &FileInfo{
Path: path,
Hash: hash,
DateTaken: dateTaken,
}
return fileInfo, nil
}
func extractDateFromMetadata(path string) *time.Time { func extractDateFromMetadata(path string) *time.Time {
// Сначала пытаемся использовать простой EXIF парсер // Сначала пытаемся использовать простой EXIF парсер
if date := extractDateFromEXIF(path); date != nil { if date := extractDateFromEXIF(path); date != nil {
@@ -260,43 +241,51 @@ func extractDateFromExifTool(path string) *time.Time {
return nil return nil
} }
func generateDestinationPath(destDir string, fileInfo *FileInfo) string { func planMovement(destDir, sourcePath string, captureTime *time.Time) (*MoveOperation, error) {
var targetDir string var targetDir string
if fileInfo.DateTaken != nil { if captureTime != nil {
year := fileInfo.DateTaken.Format("2006") year := captureTime.Format("2006")
date := fileInfo.DateTaken.Format("2006-01-02") date := captureTime.Format("2006-01-02")
targetDir = filepath.Join(destDir, year, date) targetDir = filepath.Join(destDir, year, date)
} else { } else {
targetDir = filepath.Join(destDir, "Unsorted") targetDir = filepath.Join(destDir, "Unsorted")
} }
fileName := filepath.Base(fileInfo.Path) fileName := filepath.Base(sourcePath)
targetPath := filepath.Join(targetDir, fileName) destPath := filepath.Join(targetDir, fileName)
// Проверяем, нужно ли добавить суффикс для избежания конфликтов moveOp := &MoveOperation{
return resolveFileConflict(targetPath, fileInfo.Hash) SourcePath: sourcePath,
} DestPath: destPath,
}
func resolveFileConflict(targetPath, newFileHash string) string { if _, err := os.Stat(destPath); os.IsNotExist(err) {
if _, err := os.Stat(targetPath); os.IsNotExist(err) { // Файл не существует, можно перемещать без изменения имени
return targetPath return moveOp, nil
} }
// Файл существует, проверяем хеш // Файл существует, проверяем хеш
existingHash, err := calculateFileHash(targetPath) destHash, err := calculateFileHash(destPath)
if err != nil { if err != nil {
log.Printf("Error calculating hash for existing file %s: %v", targetPath, err) return nil, fmt.Errorf("cannot calc hash for file %s: %v", destPath, err)
return generateUniqueFileName(targetPath)
} }
if existingHash == newFileHash { sourceHash, err := calculateFileHash(sourcePath)
// Файлы идентичны, можно удалить исходный if err != nil {
return targetPath return nil, fmt.Errorf("cannot calc hash for file %s: %v", sourcePath, err)
} }
// Файлы разные, нужно создать уникальное имя if destHash == sourceHash {
return generateUniqueFileName(targetPath) // Файлы идентичны, можно будет удалить исходный
moveOp.DropSource = true
return moveOp, nil
}
// Файл существует, но содержимое не идентично: нужно добавить суффикс
moveOp.DestPath = generateUniqueFileName(destPath)
return moveOp, nil
} }
func generateUniqueFileName(basePath string) string { func generateUniqueFileName(basePath string) string {
@@ -349,24 +338,21 @@ func parseDateTime(dateStr string) *time.Time {
return nil return nil
} }
func moveFile(fileInfo *FileInfo, destPath string) error { func moveFile(moveOp *MoveOperation) error {
// Создаем директорию назначения, если она не существует // Создаем директорию назначения, если она не существует
destDir := filepath.Dir(destPath) destDir := filepath.Dir(moveOp.DestPath)
if err := os.MkdirAll(destDir, 0755); err != nil { if err := os.MkdirAll(destDir, 0755); err != nil {
return fmt.Errorf("failed to create destination directory: %v", err) return fmt.Errorf("failed to create destination directory: %v", err)
} }
// Проверяем, нужно ли удалить исходный файл (если файлы идентичны) // Проверяем, нужно ли удалить исходный файл (если файлы идентичны)
if _, err := os.Stat(destPath); err == nil { if moveOp.DropSource {
existingHash, err := calculateFileHash(destPath)
if err == nil && existingHash == fileInfo.Hash {
// Файлы идентичны, удаляем исходный в корзину // Файлы идентичны, удаляем исходный в корзину
return moveToTrash(fileInfo.Path) return moveToTrash(moveOp.SourcePath)
}
} }
// Перемещаем файл // Перемещаем файл
return os.Rename(fileInfo.Path, destPath) return os.Rename(moveOp.SourcePath, moveOp.DestPath)
} }
func moveToTrash(path string) error { func moveToTrash(path string) error {