Rewrite moving algo
This commit is contained in:
116
main.go
116
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 <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 {
|
||||
// Файлы идентичны, удаляем исходный в корзину
|
||||
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 {
|
||||
|
Reference in New Issue
Block a user