Compare commits
6 Commits
6d9e14c262
...
master
Author | SHA1 | Date | |
---|---|---|---|
f3837cbf23
|
|||
bb61c0c727
|
|||
0d82e0d6a1
|
|||
951dff84fa
|
|||
4b5c4a577a
|
|||
87188deda5
|
15
README.md
Normal file
15
README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# filemover
|
||||||
|
|
||||||
|
Следит за директорией.
|
||||||
|
Перемещает новые файлы в другую директорию,
|
||||||
|
переименовывет файлы в формат 000123.ext
|
||||||
|
|
||||||
|
Использование:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
filemover <watch_dir> <dest_dir>
|
||||||
|
```
|
||||||
|
|
||||||
|
Я написал эту небольшую программу, когда мне нужно было сканировать
|
||||||
|
много старых фотографий и документов, чтобы не следить за правильными
|
||||||
|
именами файлов.
|
5
go.mod
5
go.mod
@@ -4,4 +4,7 @@ go 1.24.3
|
|||||||
|
|
||||||
require github.com/fsnotify/fsnotify v1.9.0
|
require github.com/fsnotify/fsnotify v1.9.0
|
||||||
|
|
||||||
require golang.org/x/sys v0.13.0 // indirect
|
require (
|
||||||
|
github.com/adrg/xdg v0.5.3
|
||||||
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
|
)
|
||||||
|
14
go.sum
14
go.sum
@@ -1,4 +1,14 @@
|
|||||||
|
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||||
|
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||||
|
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
96
main.go
96
main.go
@@ -2,7 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -12,15 +14,39 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg"
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
)
|
)
|
||||||
|
|
||||||
const fileQueueLen = 100
|
const fileQueueLen = 100
|
||||||
|
const fileProcessingDelay = 500 * time.Millisecond
|
||||||
|
const moveAttempts = 100
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
watchDir := "/home/av/temp/inbox"
|
// Проверка аргументов командной строки
|
||||||
destDir := "/home/av/temp/dest"
|
if len(os.Args) < 3 {
|
||||||
os.MkdirAll(destDir, 0755)
|
log.Fatalf("Usage: %s <watch_dir> <dest_dir>", os.Args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
watchDir := os.Args[1]
|
||||||
|
destDir := os.Args[2]
|
||||||
|
|
||||||
|
// Проверка существования watchDir
|
||||||
|
if _, err := os.Stat(watchDir); os.IsNotExist(err) {
|
||||||
|
log.Fatalf("Watch directory does not exist: %s", watchDir)
|
||||||
|
} else if err != nil {
|
||||||
|
log.Fatalf("Error accessing watch directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создание destDir если не существует
|
||||||
|
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||||
|
log.Fatalf("Failed to create destination directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
counterFile, err := xdg.DataFile("filemover/counter")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Application data dir not accessible, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Контекст для graceful shutdown
|
// Контекст для graceful shutdown
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@@ -36,20 +62,20 @@ func main() {
|
|||||||
// Запуск единственного обработчика
|
// Запуск единственного обработчика
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
counter := loadCounter(destDir)
|
counter := loadCounter(counterFile)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case file, ok := <-tasks:
|
case file, ok := <-tasks:
|
||||||
if !ok {
|
if !ok {
|
||||||
// Канал закрыт, завершаем работу
|
// Канал закрыт, завершаем работу
|
||||||
saveCounter(destDir, counter)
|
saveCounter(counterFile, counter)
|
||||||
log.Println("Worker stopped")
|
log.Println("Worker stopped")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
processFile(file, destDir, &counter)
|
processFile(file, destDir, counterFile, &counter)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// Получен сигнал завершения
|
// Получен сигнал завершения
|
||||||
saveCounter(destDir, counter)
|
saveCounter(counterFile, counter)
|
||||||
log.Println("Worker stopped by context")
|
log.Println("Worker stopped by context")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -117,7 +143,7 @@ mainLoop:
|
|||||||
log.Println("Shutdown complete")
|
log.Println("Shutdown complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
func processFile(filePath, destDir string, counter *int) {
|
func processFile(filePath, destDir, counterPath string, counter *int) {
|
||||||
// Проверка что это файл
|
// Проверка что это файл
|
||||||
info, err := os.Stat(filePath)
|
info, err := os.Stat(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -129,24 +155,47 @@ func processFile(filePath, destDir string, counter *int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ожидание завершения записи
|
// Ожидание завершения записи
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(fileProcessingDelay)
|
||||||
|
|
||||||
// Копирование
|
attempt := 0
|
||||||
newName := fmt.Sprintf("%03d", *counter)
|
|
||||||
destPath := filepath.Join(destDir, newName)
|
for {
|
||||||
if err := os.Rename(filePath, destPath); err != nil {
|
if attempt >= moveAttempts {
|
||||||
log.Printf("Moving failed: %v", err)
|
log.Printf("Moving failed after %d attempts, see messages", attempt)
|
||||||
return
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
*counter++
|
||||||
|
saveCounter(counterPath, *counter)
|
||||||
|
|
||||||
|
newName := fmt.Sprintf("%05d%s", *counter, filepath.Ext(filePath))
|
||||||
|
destPath := filepath.Join(destDir, newName)
|
||||||
|
|
||||||
|
_, err := os.Stat(destPath)
|
||||||
|
if err == nil {
|
||||||
|
log.Printf("Moving failed, file already exists: %s", destPath)
|
||||||
|
attempt++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
log.Printf("Moving failed: %v", err)
|
||||||
|
attempt++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(filePath, destPath); err != nil {
|
||||||
|
log.Printf("Moving failed: %v", err)
|
||||||
|
attempt++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Moved: %s -> %s", filePath, destPath)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновление счетчика
|
|
||||||
*counter++
|
|
||||||
saveCounter(destDir, *counter)
|
|
||||||
log.Printf("Moved: %s -> %s", filePath, destPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadCounter(dir string) int {
|
func loadCounter(counterPath string) int {
|
||||||
counterPath := filepath.Join(dir, "counter.txt")
|
|
||||||
data, err := os.ReadFile(counterPath)
|
data, err := os.ReadFile(counterPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
@@ -162,8 +211,7 @@ func loadCounter(dir string) int {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveCounter(dir string, count int) {
|
func saveCounter(counterPath string, count int) {
|
||||||
counterPath := filepath.Join(dir, "counter.txt")
|
|
||||||
if err := os.WriteFile(counterPath, []byte(strconv.Itoa(count)), 0644); err != nil {
|
if err := os.WriteFile(counterPath, []byte(strconv.Itoa(count)), 0644); err != nil {
|
||||||
log.Printf("Failed to save counter: %v", err)
|
log.Printf("Failed to save counter: %v", err)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user