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