Full application rewrite

This commit is contained in:
2025-01-04 01:49:48 +03:00
parent 70f32b799c
commit 754aecd69a
32 changed files with 1244 additions and 489 deletions

View File

@@ -0,0 +1,48 @@
package modes
import (
"fmt"
"io"
"log"
"os"
)
type Copy struct{}
func (c Copy) PlaceIt(sourcePath, targetPath string, mode os.FileMode) error {
targetFile, err := os.OpenFile(targetPath, os.O_TRUNC|os.O_RDWR|os.O_CREATE, mode)
if err != nil {
return fmt.Errorf("open target file: %w", err)
}
defer func() {
_ = targetFile.Close()
}()
sourceFile, err := os.OpenFile(sourcePath, os.O_RDONLY, os.ModePerm)
if err != nil {
return fmt.Errorf("open source file: %w", err)
}
defer func() {
_ = sourceFile.Close()
}()
copySize, err := io.Copy(targetFile, sourceFile)
if err != nil {
return fmt.Errorf("copy source file: %w", err)
}
stat, err := sourceFile.Stat()
if err != nil {
log.Println("stat source file failed:", err)
return nil
}
if stat.Size() != copySize {
log.Printf("copy source file size not equal target file size: source %d != %d copied\n", stat.Size(), copySize)
}
return nil
}

View File

@@ -0,0 +1,47 @@
package modes_test
import (
"fmt"
"os"
"testing"
. "github.com/derfenix/photocatalog/internal/organizer/modes"
)
func TestCopy_PlaceIt(t *testing.T) {
t.Parallel()
const testDataDir = "copy"
t.Cleanup(func() {
if err := os.RemoveAll(fmt.Sprintf("./testdata/%s/target/", testDataDir)); err != nil {
t.Errorf("error removing ./testdata/%s/target/: %v", testDataDir, err)
}
})
if err := os.Mkdir(fmt.Sprintf("./testdata/%s/target/", testDataDir), 0777); err != nil {
t.Errorf("error creating ./testdata/%s/target/: %v", testDataDir, err)
}
source := fmt.Sprintf("./testdata/%s/source/foo.txt", testDataDir)
target := fmt.Sprintf("./testdata/%s/target/foo.txt", testDataDir)
err := Copy{}.PlaceIt(source, target, 0644)
if err != nil {
t.Errorf("place file: %v", err)
}
targetData, err := os.ReadFile(target)
if err != nil {
t.Errorf("read target file: %v", err)
}
sourceData, err := os.ReadFile(source)
if err != nil {
t.Errorf("read source file: %v", err)
}
if string(targetData) != string(sourceData) {
t.Error("copy file contents missmatch")
}
}

View File

@@ -0,0 +1,21 @@
package modes
import (
"fmt"
"os"
)
type HardLink struct {
}
func (h HardLink) PlaceIt(sourcePath, targetPath string, mode os.FileMode) error {
if err := os.Link(sourcePath, targetPath); err != nil {
if os.IsExist(err) {
return nil
}
return fmt.Errorf("create hard link: %w", err)
}
return nil
}

View File

@@ -0,0 +1,47 @@
package modes_test
import (
"fmt"
"os"
"testing"
. "github.com/derfenix/photocatalog/internal/organizer/modes"
)
func TestHardLink_PlaceIt(t *testing.T) {
t.Parallel()
const testDataDir = "hardlink"
t.Cleanup(func() {
if err := os.RemoveAll(fmt.Sprintf("./testdata/%s/target/", testDataDir)); err != nil {
t.Errorf("error removing ./testdata/%s/target/: %v", testDataDir, err)
}
})
if err := os.Mkdir(fmt.Sprintf("./testdata/%s/target/", testDataDir), 0777); err != nil {
t.Errorf("error creating ./testdata/%s/target/: %v", testDataDir, err)
}
source := fmt.Sprintf("./testdata/%s/source/foo.txt", testDataDir)
target := fmt.Sprintf("./testdata/%s/target/foo.txt", testDataDir)
err := HardLink{}.PlaceIt(source, target, 0644)
if err != nil {
t.Errorf("place file: %v", err)
}
targetData, err := os.ReadFile(target)
if err != nil {
t.Errorf("read target file: %v", err)
}
sourceData, err := os.ReadFile(source)
if err != nil {
t.Errorf("read source file: %v", err)
}
if string(targetData) != string(sourceData) {
t.Error("copy file contents missmatch")
}
}

View File

@@ -0,0 +1,21 @@
package modes
import (
"fmt"
"os"
)
type Move struct {
}
func (m Move) PlaceIt(sourcePath, targetPath string, mode os.FileMode) error {
if err := os.Rename(sourcePath, targetPath); err != nil {
return fmt.Errorf("rename %s to %s: %w", sourcePath, targetPath, err)
}
if err := os.Chmod(targetPath, mode); err != nil {
return fmt.Errorf("chmod hard link: %w", err)
}
return nil
}

View File

@@ -0,0 +1,53 @@
package modes_test
import (
"fmt"
"os"
"testing"
. "github.com/derfenix/photocatalog/internal/organizer/modes"
)
func TestMove_PlaceIt(t *testing.T) {
t.Parallel()
const testDataDir = "move"
t.Cleanup(func() {
if err := os.RemoveAll(fmt.Sprintf("./testdata/%s/target/", testDataDir)); err != nil {
t.Errorf("error removing ./testdata/%s/target/: %v", testDataDir, err)
}
})
if err := os.Mkdir(fmt.Sprintf("./testdata/%s/target/", testDataDir), 0777); err != nil {
t.Errorf("error creating ./testdata/%s/target/: %v", testDataDir, err)
}
source := fmt.Sprintf("./testdata/%s/source/foo.txt", testDataDir)
target := fmt.Sprintf("./testdata/%s/target/foo.txt", testDataDir)
t.Cleanup(func() {
if err := (&Move{}).PlaceIt(target, source, 0644); err != nil {
t.Errorf("error placing back target: %v", err)
}
})
sourceData, err := os.ReadFile(source)
if err != nil {
t.Errorf("read source file: %v", err)
}
err = Move{}.PlaceIt(source, target, 0644)
if err != nil {
t.Errorf("place file: %v", err)
}
targetData, err := os.ReadFile(target)
if err != nil {
t.Errorf("read target file: %v", err)
}
if string(targetData) != string(sourceData) {
t.Error("copy file contents missmatch")
}
}

View File

@@ -0,0 +1,21 @@
package modes
import (
"fmt"
"os"
)
type SymLink struct {
}
func (s SymLink) PlaceIt(sourcePath, targetPath string, _ os.FileMode) error {
if err := os.Symlink(sourcePath, targetPath); err != nil {
if os.IsExist(err) {
return nil
}
return fmt.Errorf("create symlink: %w", err)
}
return nil
}

View File

@@ -0,0 +1,42 @@
package modes_test
import (
"fmt"
"os"
"testing"
. "github.com/derfenix/photocatalog/internal/organizer/modes"
)
func TestSymLink_PlaceIt(t *testing.T) {
t.Parallel()
const testDataDir = "symlink"
t.Cleanup(func() {
if err := os.RemoveAll(fmt.Sprintf("./testdata/%s/target/", testDataDir)); err != nil {
t.Errorf("error removing ./testdata/%s/target/: %v", testDataDir, err)
}
})
if err := os.Mkdir(fmt.Sprintf("./testdata/%s/target/", testDataDir), 0777); err != nil {
t.Errorf("error creating ./testdata/%s/target/: %v", testDataDir, err)
}
source := fmt.Sprintf("./testdata/%s/source/foo.txt", testDataDir)
target := fmt.Sprintf("./testdata/%s/target/foo.txt", testDataDir)
err := SymLink{}.PlaceIt(source, target, 0644)
if err != nil {
t.Errorf("place file: %v", err)
}
linkedFilePath, err := os.Readlink(target)
if err != nil {
t.Errorf("read target file: %v", err)
}
if linkedFilePath != source {
t.Errorf("linked file path is %s, want %s", linkedFilePath, source)
}
}

View File

@@ -0,0 +1,17 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Elit culpa hendrerit assum aliquam placerat cupiditat placerat excepteur doming ad incidunt magna eirmod, delenit nam at aliquip iure cupiditat qui congue consectetuer placerat mollit enim fugiat consequat aliquip delenit nam feugait vero aliquyam vel vel. Euismod adipiscing anim officia nibh molestie amet pariatur. Te te nulla pariatur ut option doming excepteur gubergren justo dolore sadipscing tempor stet. Tation nonummy nam nulla sint odio at sint feugait cum esse. Lorem adipiscing excepteur praesent nisi facilisis, clita invidunt molestie vulputate in tempor vulputate eiusmod tincidunt nobis. Iusto iure exerci.
Gubergren nobis ut illum dignissim at aliquam feugiat tation velit illum consequat hendrerit. Feugait facilisis nibh odio sint at adipiscing at nibh incidunt quod cupiditat ullamcorper dolore ex aute ullamco commodi rebum rebum sunt ea enim fugiat facilisi exercitation feugait eos blandit nihil molestie et facilisi aute incidunt nisl illum. Assum consequat assum at et voluptate nobis sed nobis sunt id eros consectetuer, culpa tation deserunt praesent aliquam erat stet zzril voluptate nibh, et fugiat commodi quod tation stet sadipscing lobortis officia quod ex imperdiet doming. Excepteur augue nobis.
Sanctus possim feugiat nostrud amet illum eros, eu ipsum dignissim ea tincidunt tincidunt accusam delenit placerat vel facilisi volutpat amet. Wisi dolore zzril pariatur odio cum incidunt esse duo liber aliquam voluptate eos autem est. Congue kasd vero hendrerit adipiscing rebum elitr, laboris mollit takimata nonummy id esse commodo excepteur non possim quis zzril. Consectetuer accumsan volutpat.
Eum nibh zzril vulputate consectetur mazim voluptate commodo sea augue tempor. Fugiat voluptua tempor nonumy. Congue eirmod voluptua nulla. Nonumy consectetuer voluptate.
Consequat eum tempor stet cupiditat quis in nobis. Veniam lorem sit veniam eu amet option no molestie dolores option cum reprehenderit assum tempor. Gubergren dolore wisi ut voluptua amet eum incidunt commodi eos justo quis lorem aliquyam velit praesent ullamco tempor anim volutpat velit velit aliquam cum facilisis iusto erat veniam option. Rebum nobis ea excepteur ex sea consequat blandit, excepteur et qui mazim lorem eos augue duo nulla deserunt ullamco sadipscing sed. Eos takimata justo incidunt sea vulputate blandit tempor aliqua reprehenderit aliquid sit aliqua takimata. Ullamco takimata elitr dolore ipsum takimata. Consequat aute aliquid.
Placerat sunt ut delenit aliquid enim hendrerit. Vel rebum et ad dolor luptatum veniam, volutpat soluta euismod facilisis lobortis placerat eos dignissim excepteur reprehenderit facer autem nobis officia facilisis, quis duo augue dolor autem placerat obcaecat ipsum volutpat pariatur. Ullamco elitr exerci at ad nostrud. Suscipit eu dolores sint molestie proident eleifend aute sadipscing anim. Iusto aliquid aliquam.
Iriure elitr sit nihil sanctus soluta autem et, nisi dolores erat option blandit soluta aliquip nihil placerat cum clita diam duis sunt sunt tation vel consequat deserunt doming gubergren euismod liber ad esse augue aliquyam ad facilisis. Anim dignissim fugiat. Aliquam dolores, obcaecat aliquam nobis quod euismod consectetur iusto wisi aute mollit eirmod eirmod nonumy, qui officia in eros hendrerit nonummy soluta option aliquip. Feugait laboris duis accumsan proident velit stet invidunt kasd consetetur nobis fugiat invidunt cupiditat et excepteur nonumy feugait iusto consectetur eos cillum. Eos at commodo facilisi. Facer facilisi exercitation.
Soluta takimata labore sunt erat cum option eu deserunt eu consectetur, soluta option cum euismod aliquid illum commodo obcaecat aliquyam erat eleifend blandit, mazim exercitation consequat duis vel imperdiet consetetur ullamco doming. In elit duis. Duo praesent lorem.
Commodi molestie reprehenderit ipsum enim, zzril vulputate in laboris voluptua nibh suscipit, autem facilisi invidunt commodi iusto mollit aliquip nam culpa consetetur dolore elitr, fugiat luptatum nisl amet elitr facilisis te quod ad kasd wisi reprehenderit invidunt. Facer ut quis nam dignissim. Option suscipit duis dolor consequat takimata te amet accumsan obcaecat autem congue ad obcaecat velit. Imperdiet id laborum sea. Hendrerit nisi eros.

View File

@@ -0,0 +1,17 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Elit culpa hendrerit assum aliquam placerat cupiditat placerat excepteur doming ad incidunt magna eirmod, delenit nam at aliquip iure cupiditat qui congue consectetuer placerat mollit enim fugiat consequat aliquip delenit nam feugait vero aliquyam vel vel. Euismod adipiscing anim officia nibh molestie amet pariatur. Te te nulla pariatur ut option doming excepteur gubergren justo dolore sadipscing tempor stet. Tation nonummy nam nulla sint odio at sint feugait cum esse. Lorem adipiscing excepteur praesent nisi facilisis, clita invidunt molestie vulputate in tempor vulputate eiusmod tincidunt nobis. Iusto iure exerci.
Gubergren nobis ut illum dignissim at aliquam feugiat tation velit illum consequat hendrerit. Feugait facilisis nibh odio sint at adipiscing at nibh incidunt quod cupiditat ullamcorper dolore ex aute ullamco commodi rebum rebum sunt ea enim fugiat facilisi exercitation feugait eos blandit nihil molestie et facilisi aute incidunt nisl illum. Assum consequat assum at et voluptate nobis sed nobis sunt id eros consectetuer, culpa tation deserunt praesent aliquam erat stet zzril voluptate nibh, et fugiat commodi quod tation stet sadipscing lobortis officia quod ex imperdiet doming. Excepteur augue nobis.
Sanctus possim feugiat nostrud amet illum eros, eu ipsum dignissim ea tincidunt tincidunt accusam delenit placerat vel facilisi volutpat amet. Wisi dolore zzril pariatur odio cum incidunt esse duo liber aliquam voluptate eos autem est. Congue kasd vero hendrerit adipiscing rebum elitr, laboris mollit takimata nonummy id esse commodo excepteur non possim quis zzril. Consectetuer accumsan volutpat.
Eum nibh zzril vulputate consectetur mazim voluptate commodo sea augue tempor. Fugiat voluptua tempor nonumy. Congue eirmod voluptua nulla. Nonumy consectetuer voluptate.
Consequat eum tempor stet cupiditat quis in nobis. Veniam lorem sit veniam eu amet option no molestie dolores option cum reprehenderit assum tempor. Gubergren dolore wisi ut voluptua amet eum incidunt commodi eos justo quis lorem aliquyam velit praesent ullamco tempor anim volutpat velit velit aliquam cum facilisis iusto erat veniam option. Rebum nobis ea excepteur ex sea consequat blandit, excepteur et qui mazim lorem eos augue duo nulla deserunt ullamco sadipscing sed. Eos takimata justo incidunt sea vulputate blandit tempor aliqua reprehenderit aliquid sit aliqua takimata. Ullamco takimata elitr dolore ipsum takimata. Consequat aute aliquid.
Placerat sunt ut delenit aliquid enim hendrerit. Vel rebum et ad dolor luptatum veniam, volutpat soluta euismod facilisis lobortis placerat eos dignissim excepteur reprehenderit facer autem nobis officia facilisis, quis duo augue dolor autem placerat obcaecat ipsum volutpat pariatur. Ullamco elitr exerci at ad nostrud. Suscipit eu dolores sint molestie proident eleifend aute sadipscing anim. Iusto aliquid aliquam.
Iriure elitr sit nihil sanctus soluta autem et, nisi dolores erat option blandit soluta aliquip nihil placerat cum clita diam duis sunt sunt tation vel consequat deserunt doming gubergren euismod liber ad esse augue aliquyam ad facilisis. Anim dignissim fugiat. Aliquam dolores, obcaecat aliquam nobis quod euismod consectetur iusto wisi aute mollit eirmod eirmod nonumy, qui officia in eros hendrerit nonummy soluta option aliquip. Feugait laboris duis accumsan proident velit stet invidunt kasd consetetur nobis fugiat invidunt cupiditat et excepteur nonumy feugait iusto consectetur eos cillum. Eos at commodo facilisi. Facer facilisi exercitation.
Soluta takimata labore sunt erat cum option eu deserunt eu consectetur, soluta option cum euismod aliquid illum commodo obcaecat aliquyam erat eleifend blandit, mazim exercitation consequat duis vel imperdiet consetetur ullamco doming. In elit duis. Duo praesent lorem.
Commodi molestie reprehenderit ipsum enim, zzril vulputate in laboris voluptua nibh suscipit, autem facilisi invidunt commodi iusto mollit aliquip nam culpa consetetur dolore elitr, fugiat luptatum nisl amet elitr facilisis te quod ad kasd wisi reprehenderit invidunt. Facer ut quis nam dignissim. Option suscipit duis dolor consequat takimata te amet accumsan obcaecat autem congue ad obcaecat velit. Imperdiet id laborum sea. Hendrerit nisi eros.

View File

@@ -0,0 +1,17 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Elit culpa hendrerit assum aliquam placerat cupiditat placerat excepteur doming ad incidunt magna eirmod, delenit nam at aliquip iure cupiditat qui congue consectetuer placerat mollit enim fugiat consequat aliquip delenit nam feugait vero aliquyam vel vel. Euismod adipiscing anim officia nibh molestie amet pariatur. Te te nulla pariatur ut option doming excepteur gubergren justo dolore sadipscing tempor stet. Tation nonummy nam nulla sint odio at sint feugait cum esse. Lorem adipiscing excepteur praesent nisi facilisis, clita invidunt molestie vulputate in tempor vulputate eiusmod tincidunt nobis. Iusto iure exerci.
Gubergren nobis ut illum dignissim at aliquam feugiat tation velit illum consequat hendrerit. Feugait facilisis nibh odio sint at adipiscing at nibh incidunt quod cupiditat ullamcorper dolore ex aute ullamco commodi rebum rebum sunt ea enim fugiat facilisi exercitation feugait eos blandit nihil molestie et facilisi aute incidunt nisl illum. Assum consequat assum at et voluptate nobis sed nobis sunt id eros consectetuer, culpa tation deserunt praesent aliquam erat stet zzril voluptate nibh, et fugiat commodi quod tation stet sadipscing lobortis officia quod ex imperdiet doming. Excepteur augue nobis.
Sanctus possim feugiat nostrud amet illum eros, eu ipsum dignissim ea tincidunt tincidunt accusam delenit placerat vel facilisi volutpat amet. Wisi dolore zzril pariatur odio cum incidunt esse duo liber aliquam voluptate eos autem est. Congue kasd vero hendrerit adipiscing rebum elitr, laboris mollit takimata nonummy id esse commodo excepteur non possim quis zzril. Consectetuer accumsan volutpat.
Eum nibh zzril vulputate consectetur mazim voluptate commodo sea augue tempor. Fugiat voluptua tempor nonumy. Congue eirmod voluptua nulla. Nonumy consectetuer voluptate.
Consequat eum tempor stet cupiditat quis in nobis. Veniam lorem sit veniam eu amet option no molestie dolores option cum reprehenderit assum tempor. Gubergren dolore wisi ut voluptua amet eum incidunt commodi eos justo quis lorem aliquyam velit praesent ullamco tempor anim volutpat velit velit aliquam cum facilisis iusto erat veniam option. Rebum nobis ea excepteur ex sea consequat blandit, excepteur et qui mazim lorem eos augue duo nulla deserunt ullamco sadipscing sed. Eos takimata justo incidunt sea vulputate blandit tempor aliqua reprehenderit aliquid sit aliqua takimata. Ullamco takimata elitr dolore ipsum takimata. Consequat aute aliquid.
Placerat sunt ut delenit aliquid enim hendrerit. Vel rebum et ad dolor luptatum veniam, volutpat soluta euismod facilisis lobortis placerat eos dignissim excepteur reprehenderit facer autem nobis officia facilisis, quis duo augue dolor autem placerat obcaecat ipsum volutpat pariatur. Ullamco elitr exerci at ad nostrud. Suscipit eu dolores sint molestie proident eleifend aute sadipscing anim. Iusto aliquid aliquam.
Iriure elitr sit nihil sanctus soluta autem et, nisi dolores erat option blandit soluta aliquip nihil placerat cum clita diam duis sunt sunt tation vel consequat deserunt doming gubergren euismod liber ad esse augue aliquyam ad facilisis. Anim dignissim fugiat. Aliquam dolores, obcaecat aliquam nobis quod euismod consectetur iusto wisi aute mollit eirmod eirmod nonumy, qui officia in eros hendrerit nonummy soluta option aliquip. Feugait laboris duis accumsan proident velit stet invidunt kasd consetetur nobis fugiat invidunt cupiditat et excepteur nonumy feugait iusto consectetur eos cillum. Eos at commodo facilisi. Facer facilisi exercitation.
Soluta takimata labore sunt erat cum option eu deserunt eu consectetur, soluta option cum euismod aliquid illum commodo obcaecat aliquyam erat eleifend blandit, mazim exercitation consequat duis vel imperdiet consetetur ullamco doming. In elit duis. Duo praesent lorem.
Commodi molestie reprehenderit ipsum enim, zzril vulputate in laboris voluptua nibh suscipit, autem facilisi invidunt commodi iusto mollit aliquip nam culpa consetetur dolore elitr, fugiat luptatum nisl amet elitr facilisis te quod ad kasd wisi reprehenderit invidunt. Facer ut quis nam dignissim. Option suscipit duis dolor consequat takimata te amet accumsan obcaecat autem congue ad obcaecat velit. Imperdiet id laborum sea. Hendrerit nisi eros.

View File

@@ -0,0 +1,17 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Elit culpa hendrerit assum aliquam placerat cupiditat placerat excepteur doming ad incidunt magna eirmod, delenit nam at aliquip iure cupiditat qui congue consectetuer placerat mollit enim fugiat consequat aliquip delenit nam feugait vero aliquyam vel vel. Euismod adipiscing anim officia nibh molestie amet pariatur. Te te nulla pariatur ut option doming excepteur gubergren justo dolore sadipscing tempor stet. Tation nonummy nam nulla sint odio at sint feugait cum esse. Lorem adipiscing excepteur praesent nisi facilisis, clita invidunt molestie vulputate in tempor vulputate eiusmod tincidunt nobis. Iusto iure exerci.
Gubergren nobis ut illum dignissim at aliquam feugiat tation velit illum consequat hendrerit. Feugait facilisis nibh odio sint at adipiscing at nibh incidunt quod cupiditat ullamcorper dolore ex aute ullamco commodi rebum rebum sunt ea enim fugiat facilisi exercitation feugait eos blandit nihil molestie et facilisi aute incidunt nisl illum. Assum consequat assum at et voluptate nobis sed nobis sunt id eros consectetuer, culpa tation deserunt praesent aliquam erat stet zzril voluptate nibh, et fugiat commodi quod tation stet sadipscing lobortis officia quod ex imperdiet doming. Excepteur augue nobis.
Sanctus possim feugiat nostrud amet illum eros, eu ipsum dignissim ea tincidunt tincidunt accusam delenit placerat vel facilisi volutpat amet. Wisi dolore zzril pariatur odio cum incidunt esse duo liber aliquam voluptate eos autem est. Congue kasd vero hendrerit adipiscing rebum elitr, laboris mollit takimata nonummy id esse commodo excepteur non possim quis zzril. Consectetuer accumsan volutpat.
Eum nibh zzril vulputate consectetur mazim voluptate commodo sea augue tempor. Fugiat voluptua tempor nonumy. Congue eirmod voluptua nulla. Nonumy consectetuer voluptate.
Consequat eum tempor stet cupiditat quis in nobis. Veniam lorem sit veniam eu amet option no molestie dolores option cum reprehenderit assum tempor. Gubergren dolore wisi ut voluptua amet eum incidunt commodi eos justo quis lorem aliquyam velit praesent ullamco tempor anim volutpat velit velit aliquam cum facilisis iusto erat veniam option. Rebum nobis ea excepteur ex sea consequat blandit, excepteur et qui mazim lorem eos augue duo nulla deserunt ullamco sadipscing sed. Eos takimata justo incidunt sea vulputate blandit tempor aliqua reprehenderit aliquid sit aliqua takimata. Ullamco takimata elitr dolore ipsum takimata. Consequat aute aliquid.
Placerat sunt ut delenit aliquid enim hendrerit. Vel rebum et ad dolor luptatum veniam, volutpat soluta euismod facilisis lobortis placerat eos dignissim excepteur reprehenderit facer autem nobis officia facilisis, quis duo augue dolor autem placerat obcaecat ipsum volutpat pariatur. Ullamco elitr exerci at ad nostrud. Suscipit eu dolores sint molestie proident eleifend aute sadipscing anim. Iusto aliquid aliquam.
Iriure elitr sit nihil sanctus soluta autem et, nisi dolores erat option blandit soluta aliquip nihil placerat cum clita diam duis sunt sunt tation vel consequat deserunt doming gubergren euismod liber ad esse augue aliquyam ad facilisis. Anim dignissim fugiat. Aliquam dolores, obcaecat aliquam nobis quod euismod consectetur iusto wisi aute mollit eirmod eirmod nonumy, qui officia in eros hendrerit nonummy soluta option aliquip. Feugait laboris duis accumsan proident velit stet invidunt kasd consetetur nobis fugiat invidunt cupiditat et excepteur nonumy feugait iusto consectetur eos cillum. Eos at commodo facilisi. Facer facilisi exercitation.
Soluta takimata labore sunt erat cum option eu deserunt eu consectetur, soluta option cum euismod aliquid illum commodo obcaecat aliquyam erat eleifend blandit, mazim exercitation consequat duis vel imperdiet consetetur ullamco doming. In elit duis. Duo praesent lorem.
Commodi molestie reprehenderit ipsum enim, zzril vulputate in laboris voluptua nibh suscipit, autem facilisi invidunt commodi iusto mollit aliquip nam culpa consetetur dolore elitr, fugiat luptatum nisl amet elitr facilisis te quod ad kasd wisi reprehenderit invidunt. Facer ut quis nam dignissim. Option suscipit duis dolor consequat takimata te amet accumsan obcaecat autem congue ad obcaecat velit. Imperdiet id laborum sea. Hendrerit nisi eros.

View File

@@ -0,0 +1,311 @@
package organizer
import (
"context"
"fmt"
"io"
"io/fs"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/fsnotify/fsnotify"
"github.com/derfenix/photocatalog/internal/metadata"
)
type MetaExtractor interface {
Extract(_ string, data io.Reader) (metadata.Metadata, error)
}
type Mode interface {
PlaceIt(sourcePath, targetPath string, mode os.FileMode) error
}
func NewOrganizer(mode Mode, source, target string) *Organizer {
return &Organizer{
mode: mode,
sourceDir: source,
targetDir: target,
dirMode: 0777,
fileMode: 0644,
metaExtractors: map[string]MetaExtractor{
"": &metadata.Default{},
"jpg": metadata.Exif{},
"jpeg": metadata.Exif{},
"tiff": metadata.Exif{},
},
}
}
type Organizer struct {
mode Mode
sourceDir string
targetDir string
overwrite bool
dirMode os.FileMode
fileMode os.FileMode
errLogger func(error)
metaExtractors map[string]MetaExtractor
}
func (o *Organizer) WithOverwrite() *Organizer {
o.overwrite = true
return o
}
func (o *Organizer) WithDirMode(mode os.FileMode) *Organizer {
o.dirMode = mode
return o
}
func (o *Organizer) WithFileMode(mode os.FileMode) *Organizer {
o.fileMode = mode
return o
}
func (o *Organizer) WithErrLogger(f func(error)) *Organizer {
o.errLogger = f
return o
}
func (o *Organizer) logErr(err error) {
if o.errLogger != nil {
o.errLogger(err)
}
}
func (o *Organizer) Watch(ctx context.Context, wg *sync.WaitGroup) error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("new watcher: %w", err)
}
if err := watcher.Add(o.sourceDir); err != nil {
return fmt.Errorf("add source dir to watcher: %w", err)
}
// Add all subfolders to the watcher.
err = filepath.WalkDir(o.sourceDir, func(path string, d fs.DirEntry, err error) error {
if path == o.sourceDir {
return nil
}
if d.IsDir() {
if err := watcher.Add(path); err != nil {
o.logErr(fmt.Errorf("add the directory %s to watcher: %w", path, err))
}
}
return nil
})
if err != nil {
return fmt.Errorf("add subdirs to watcher: %w", err)
}
wg.Add(2)
go func() {
defer wg.Done()
<-ctx.Done()
if err := watcher.Close(); err != nil {
o.logErr(fmt.Errorf("close watcher: %w", err))
}
}()
go func() {
defer wg.Done()
for {
select {
case event := <-watcher.Events:
if event.Op == fsnotify.Write {
stat, err := os.Stat(event.Name)
if err != nil {
o.logErr(fmt.Errorf("stat %s: %w", event.Name, err))
continue
}
// Add new directories to the watcher.
if stat.IsDir() {
if err := watcher.Add(event.Name); err != nil {
o.logErr(fmt.Errorf("watch dir: %w", err))
}
continue
}
if err := o.processFile(event.Name); err != nil {
o.logErr(fmt.Errorf("process file %s: %w", event.Name, err))
}
}
case <-ctx.Done():
return
}
}
}()
return nil
}
func (o *Organizer) FullSync(ctx context.Context) error {
err := filepath.WalkDir(o.sourceDir, func(path string, info fs.DirEntry, err error) error {
if ctx.Err() != nil {
return ctx.Err()
}
if info.IsDir() {
return nil
}
if err := o.processFile(path); err != nil {
return err
}
return nil
})
if err != nil {
return fmt.Errorf("walking source dir: %w", err)
}
return nil
}
func (o *Organizer) getMetaForPath(fp string) (metadata.Metadata, error) {
file, err := os.OpenFile(fp, os.O_RDONLY, os.ModePerm)
if err != nil {
return metadata.Metadata{}, fmt.Errorf("open file: %w", err)
}
defer func() {
_ = file.Close()
}()
meta, err := o.getMetadata(fp, file)
if err != nil {
return metadata.Metadata{}, fmt.Errorf("get metadata: %w", err)
}
return meta, nil
}
func (o *Organizer) getMetadata(fp string, data io.Reader) (metadata.Metadata, error) {
ext := strings.ToLower(filepath.Ext(fp))
if strings.HasPrefix(ext, ".") {
ext = ext[1:]
}
extractor, ok := o.metaExtractors[ext]
if !ok {
extractor = o.metaExtractors[""]
}
meta, err := extractor.Extract(fp, data)
if err != nil || meta.Created.IsZero() {
// Fallback to default extractor.
extractor = o.metaExtractors[""]
meta, err = extractor.Extract(fp, data)
if err != nil {
return metadata.Metadata{}, fmt.Errorf("extract metadata: %w", err)
}
}
return meta, nil
}
func (o *Organizer) processFile(sourcePath string) error {
meta, err := o.getMetaForPath(sourcePath)
if err != nil {
return err
}
targetPath, err := o.BuildTargetPath(sourcePath, meta)
if err != nil {
return fmt.Errorf("build target path: %w", err)
}
if pathExists(targetPath) && !o.overwrite {
return nil
}
if err := o.ensureTargetPath(filepath.Dir(targetPath)); err != nil {
return fmt.Errorf("ensure target path: %w", err)
}
if err := o.mode.PlaceIt(sourcePath, targetPath, o.fileMode); err != nil {
return fmt.Errorf("place file to new path: %w", err)
}
log.Printf("File %s placed at %s", sourcePath, targetPath)
return nil
}
func (o *Organizer) BuildTargetPath(sourcePath string, meta metadata.Metadata) (string, error) {
sourcePath, err := filepath.Rel(o.sourceDir, sourcePath)
if err != nil {
return "", fmt.Errorf("get a relative path: %w", err)
}
target := filepath.Join(
o.targetDir,
strconv.Itoa(meta.Created.Year()),
strconv.Itoa(int(meta.Created.Month())),
sourcePath,
)
return target, nil
}
func (o *Organizer) ensureTargetPath(targetPath string) error {
if pathExists(targetPath) {
return nil
}
relPath, err := filepath.Rel(o.targetDir, targetPath)
if err != nil {
return fmt.Errorf("get a relative path: %w", err)
}
dir := o.targetDir
for _, part := range strings.Split(relPath, string(filepath.Separator)) {
dir = filepath.Join(dir, part)
if err := os.Mkdir(dir, o.dirMode); err != nil && !os.IsExist(err) {
return fmt.Errorf("create target directory path at %s: %w", dir, err)
}
}
return nil
}
func pathExists(path string) bool {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false
}
return true
}
return true
}

View File

@@ -0,0 +1,152 @@
package organizer
import (
"bytes"
"context"
"fmt"
"io/fs"
"os"
"path/filepath"
"sync"
"testing"
"time"
"github.com/derfenix/photocatalog/internal/metadata"
"github.com/derfenix/photocatalog/internal/organizer/modes"
)
func TestOrganizer_FullSync(t *testing.T) {
t.Parallel()
source := "./testdata/fullsync/source"
target := "./testdata/fullsync/target"
t.Cleanup(func() {
if err := os.RemoveAll(target); err != nil {
t.Fatal(err)
}
})
if err := os.Mkdir(target, 0777); err != nil {
t.Fatalf("create target dir %s failed: %v", target, err)
}
ctx := context.Background()
org := NewOrganizer(modes.HardLink{}, source, target)
if err := org.FullSync(ctx); err != nil {
t.Fatalf("full sync failed: %v", err)
}
err := filepath.WalkDir(source, func(path string, d fs.DirEntry, err error) error {
if d.IsDir() {
return nil
}
sourceFile, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read source file %s: %v", path, err)
}
meta, err := (&metadata.Default{}).Extract(path, nil)
if err != nil {
return fmt.Errorf("extract metadata from %s: %v", path, err)
}
targetPath, err := org.BuildTargetPath(path, meta)
if err != nil {
return fmt.Errorf("build target file %s: %v", path, err)
}
targetFile, err := os.ReadFile(targetPath)
if err != nil {
return fmt.Errorf("read target file %s: %v", targetPath, err)
}
if !bytes.Equal(sourceFile, targetFile) {
return fmt.Errorf("target file content missmatch")
}
return nil
})
if err != nil {
t.Fatalf("walk dir failed: %v", err)
}
}
func TestOrganizer_Watch(t *testing.T) {
t.Parallel()
source := "./testdata/watcher/source"
target := "./testdata/watcher/target"
t.Cleanup(func() {
if err := os.RemoveAll(target); err != nil {
t.Fatal(err)
}
})
if err := os.Mkdir(target, 0777); err != nil {
t.Fatalf("create target dir %s failed: %v", target, err)
}
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
wg := sync.WaitGroup{}
org := NewOrganizer(&modes.HardLink{}, source, target)
if err := org.Watch(ctx, &wg); err != nil {
t.Fatalf("watch failed: %v", err)
}
nonEmpty, err := checkEmpty(t, target)
if err != nil {
t.Fatalf("check empty failed: %v", err)
}
if nonEmpty {
t.Fatal("target dir should not be empty")
}
err = os.WriteFile(filepath.Join(source, "20241108_160834.txt"), []byte("test"), 0777)
if err != nil {
t.Fatalf("file write failed: %v", err)
}
time.Sleep(time.Millisecond)
nonEmpty, err = checkEmpty(t, target)
if err != nil {
t.Fatalf("check empty failed: %v", err)
}
if !nonEmpty {
t.Fatal("target dir should not be empty")
}
cancel()
wg.Wait()
}
func checkEmpty(t *testing.T, target string) (bool, error) {
t.Helper()
var nonEmpty bool
err := filepath.WalkDir(target, func(path string, d fs.DirEntry, err error) error {
if path == target {
return nil
}
nonEmpty = true
return nil
})
if err != nil {
t.Fatalf("walk dir failed: %v", err)
}
return nonEmpty, err
}

View File

@@ -0,0 +1,17 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Elit culpa hendrerit assum aliquam placerat cupiditat placerat excepteur doming ad incidunt magna eirmod, delenit nam at aliquip iure cupiditat qui congue consectetuer placerat mollit enim fugiat consequat aliquip delenit nam feugait vero aliquyam vel vel. Euismod adipiscing anim officia nibh molestie amet pariatur. Te te nulla pariatur ut option doming excepteur gubergren justo dolore sadipscing tempor stet. Tation nonummy nam nulla sint odio at sint feugait cum esse. Lorem adipiscing excepteur praesent nisi facilisis, clita invidunt molestie vulputate in tempor vulputate eiusmod tincidunt nobis. Iusto iure exerci.
Gubergren nobis ut illum dignissim at aliquam feugiat tation velit illum consequat hendrerit. Feugait facilisis nibh odio sint at adipiscing at nibh incidunt quod cupiditat ullamcorper dolore ex aute ullamco commodi rebum rebum sunt ea enim fugiat facilisi exercitation feugait eos blandit nihil molestie et facilisi aute incidunt nisl illum. Assum consequat assum at et voluptate nobis sed nobis sunt id eros consectetuer, culpa tation deserunt praesent aliquam erat stet zzril voluptate nibh, et fugiat commodi quod tation stet sadipscing lobortis officia quod ex imperdiet doming. Excepteur augue nobis.
Sanctus possim feugiat nostrud amet illum eros, eu ipsum dignissim ea tincidunt tincidunt accusam delenit placerat vel facilisi volutpat amet. Wisi dolore zzril pariatur odio cum incidunt esse duo liber aliquam voluptate eos autem est. Congue kasd vero hendrerit adipiscing rebum elitr, laboris mollit takimata nonummy id esse commodo excepteur non possim quis zzril. Consectetuer accumsan volutpat.
Eum nibh zzril vulputate consectetur mazim voluptate commodo sea augue tempor. Fugiat voluptua tempor nonumy. Congue eirmod voluptua nulla. Nonumy consectetuer voluptate.
Consequat eum tempor stet cupiditat quis in nobis. Veniam lorem sit veniam eu amet option no molestie dolores option cum reprehenderit assum tempor. Gubergren dolore wisi ut voluptua amet eum incidunt commodi eos justo quis lorem aliquyam velit praesent ullamco tempor anim volutpat velit velit aliquam cum facilisis iusto erat veniam option. Rebum nobis ea excepteur ex sea consequat blandit, excepteur et qui mazim lorem eos augue duo nulla deserunt ullamco sadipscing sed. Eos takimata justo incidunt sea vulputate blandit tempor aliqua reprehenderit aliquid sit aliqua takimata. Ullamco takimata elitr dolore ipsum takimata. Consequat aute aliquid.
Placerat sunt ut delenit aliquid enim hendrerit. Vel rebum et ad dolor luptatum veniam, volutpat soluta euismod facilisis lobortis placerat eos dignissim excepteur reprehenderit facer autem nobis officia facilisis, quis duo augue dolor autem placerat obcaecat ipsum volutpat pariatur. Ullamco elitr exerci at ad nostrud. Suscipit eu dolores sint molestie proident eleifend aute sadipscing anim. Iusto aliquid aliquam.
Iriure elitr sit nihil sanctus soluta autem et, nisi dolores erat option blandit soluta aliquip nihil placerat cum clita diam duis sunt sunt tation vel consequat deserunt doming gubergren euismod liber ad esse augue aliquyam ad facilisis. Anim dignissim fugiat. Aliquam dolores, obcaecat aliquam nobis quod euismod consectetur iusto wisi aute mollit eirmod eirmod nonumy, qui officia in eros hendrerit nonummy soluta option aliquip. Feugait laboris duis accumsan proident velit stet invidunt kasd consetetur nobis fugiat invidunt cupiditat et excepteur nonumy feugait iusto consectetur eos cillum. Eos at commodo facilisi. Facer facilisi exercitation.
Soluta takimata labore sunt erat cum option eu deserunt eu consectetur, soluta option cum euismod aliquid illum commodo obcaecat aliquyam erat eleifend blandit, mazim exercitation consequat duis vel imperdiet consetetur ullamco doming. In elit duis. Duo praesent lorem.
Commodi molestie reprehenderit ipsum enim, zzril vulputate in laboris voluptua nibh suscipit, autem facilisi invidunt commodi iusto mollit aliquip nam culpa consetetur dolore elitr, fugiat luptatum nisl amet elitr facilisis te quod ad kasd wisi reprehenderit invidunt. Facer ut quis nam dignissim. Option suscipit duis dolor consequat takimata te amet accumsan obcaecat autem congue ad obcaecat velit. Imperdiet id laborum sea. Hendrerit nisi eros.

View File

@@ -0,0 +1 @@
test