Rewrite moving algo
This commit is contained in:
112
main.go
112
main.go
@@ -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 {
|
||||||
|
Reference in New Issue
Block a user