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
}
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 <source_dir> <dest_dir> [-dry-run]")
fmt.Println("Usage: photorg [-dry-run] <source_dir> <dest_dir>")
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 {
if moveOp.DropSource {
// Файлы идентичны, удаляем исходный в корзину
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 {