Complete file browser with symlink support and full folder / file names (#412)

* Backend: Code clean-up

Signed-off-by: Michael Mayer <michael@liquidbytes.net>

* File Browser: Show complete, original file and folder names #408

Signed-off-by: Michael Mayer <michael@liquidbytes.net>

* File Browser: Follow symlinks #201 #403 #407

Warning: Following symlinks can make folder lists non-deterministic
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-07-16 15:43:23 +02:00 committed by GitHub
parent 44fcc3e531
commit 6847b8b5f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 98 additions and 31 deletions

View file

@ -91,7 +91,7 @@
<div> <div>
<h3 class="body-2 mb-2" :title="model.Name"> <h3 class="body-2 mb-2" :title="model.Name">
<button @click.exact="openFile(index)"> <button @click.exact="openFile(index)">
{{ model.baseName(19) }} {{ model.baseName() }}
</button> </button>
</h3> </h3>
<div class="caption" title="Info"> <div class="caption" title="Info">
@ -103,13 +103,10 @@
<div> <div>
<h3 class="body-2 mb-2" :title="model.Title"> <h3 class="body-2 mb-2" :title="model.Title">
<button @click.exact="openFile(index)"> <button @click.exact="openFile(index)">
{{ model.Title }} {{ model.baseName() }}
</button> </button>
</h3> </h3>
<div class="caption" title="Path" v-if="model.Title !== model.Path"> <div class="caption" title="Path">
/{{ model.Path }}
</div>
<div class="caption" title="Path" v-else>
<translate key="Folder">Folder</translate> <translate key="Folder">Folder</translate>
</div> </div>
</div> </div>

View file

@ -13,7 +13,7 @@ func TestGetFoldersOriginals(t *testing.T) {
t.Run("flat", func(t *testing.T) { t.Run("flat", func(t *testing.T) {
app, router, conf := NewApiTest() app, router, conf := NewApiTest()
_ = conf.CreateDirectories() _ = conf.CreateDirectories()
expected, err := fs.Dirs(conf.OriginalsPath(), false) expected, err := fs.Dirs(conf.OriginalsPath(), false, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -56,7 +56,7 @@ func TestGetFoldersOriginals(t *testing.T) {
t.Run("recursive", func(t *testing.T) { t.Run("recursive", func(t *testing.T) {
app, router, conf := NewApiTest() app, router, conf := NewApiTest()
_ = conf.CreateDirectories() _ = conf.CreateDirectories()
expected, err := fs.Dirs(conf.OriginalsPath(), true) expected, err := fs.Dirs(conf.OriginalsPath(), true, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -96,7 +96,7 @@ func TestGetFoldersImport(t *testing.T) {
t.Run("flat", func(t *testing.T) { t.Run("flat", func(t *testing.T) {
app, router, conf := NewApiTest() app, router, conf := NewApiTest()
_ = conf.CreateDirectories() _ = conf.CreateDirectories()
expected, err := fs.Dirs(conf.ImportPath(), false) expected, err := fs.Dirs(conf.ImportPath(), false, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -140,7 +140,7 @@ func TestGetFoldersImport(t *testing.T) {
t.Run("recursive", func(t *testing.T) { t.Run("recursive", func(t *testing.T) {
app, router, conf := NewApiTest() app, router, conf := NewApiTest()
_ = conf.CreateDirectories() _ = conf.CreateDirectories()
expected, err := fs.Dirs(conf.ImportPath(), true) expected, err := fs.Dirs(conf.ImportPath(), true, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View file

@ -176,10 +176,10 @@ func TestLocation_Assign(t *testing.T) {
lat := -21.976301666666668 lat := -21.976301666666668
lng := 49.148046666666666 lng := 49.148046666666666
id := s2.Token(lat, lng) id := s2.Token(lat, lng)
log.Printf("ID: %s", id) // log.Printf("ID: %s", id)
o, err := places.FindLocation(id) o, err := places.FindLocation(id)
log.Printf("Output: %+v", o) // log.Printf("Output: %+v", o)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View file

@ -9,7 +9,7 @@ import (
// FoldersByPath returns a slice of folders in a given directory incl sub directories in recursive mode. // FoldersByPath returns a slice of folders in a given directory incl sub directories in recursive mode.
func FoldersByPath(rootName, rootPath, path string, recursive bool) (folders entity.Folders, err error) { func FoldersByPath(rootName, rootPath, path string, recursive bool) (folders entity.Folders, err error) {
dirs, err := fs.Dirs(filepath.Join(rootPath, path), recursive) dirs, err := fs.Dirs(filepath.Join(rootPath, path), recursive, true)
if err != nil { if err != nil {
return folders, err return folders, err

View file

@ -88,25 +88,51 @@ var ImportPaths = []string{
"~/Import", "~/Import",
} }
func Dirs(root string, recursive bool) (result []string, err error) { // Dirs returns a slice of directories in a path, optional recursively and with symlinks.
//
// Warning: Following symlinks can make the result non-deterministic and hard to test!
func Dirs(root string, recursive bool, followLinks bool) (result []string, err error) {
result = []string{} result = []string{}
ignore := NewIgnoreList(".ppignore", true, false) ignore := NewIgnoreList(".ppignore", true, false)
mutex := sync.Mutex{} mutex := sync.Mutex{}
err = fastwalk.Walk(root, func(fileName string, info os.FileMode) error { symlinks := make(map[string]bool)
if info.IsDir() { symlinksMutex := sync.Mutex{}
appendResult := func(fileName string) {
fileName = strings.Replace(fileName, root, "", 1)
mutex.Lock()
defer mutex.Unlock()
result = append(result, fileName)
}
err = fastwalk.Walk(root, func(fileName string, typ os.FileMode) error {
if typ.IsDir() || typ == os.ModeSymlink && followLinks {
if ignore.Ignore(fileName) { if ignore.Ignore(fileName) {
return filepath.SkipDir return filepath.SkipDir
} }
if fileName != root { if fileName != root {
mutex.Lock()
fileName = strings.Replace(fileName, root, "", 1)
result = append(result, fileName)
mutex.Unlock()
if !recursive { if !recursive {
appendResult(fileName)
return filepath.SkipDir return filepath.SkipDir
} else if typ != os.ModeSymlink {
appendResult(fileName)
return nil
} else if resolved, err := filepath.EvalSymlinks(fileName); err == nil {
symlinksMutex.Lock()
defer symlinksMutex.Unlock()
if _, ok := symlinks[resolved]; ok {
return filepath.SkipDir
} else {
symlinks[resolved] = true
appendResult(fileName)
}
return fastwalk.ErrTraverseLink
} }
} }
} }

View file

@ -9,27 +9,70 @@ import (
func TestDirs(t *testing.T) { func TestDirs(t *testing.T) {
t.Run("recursive", func(t *testing.T) { t.Run("recursive", func(t *testing.T) {
result, err := Dirs("testdata", true) result, err := Dirs("testdata", true, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
expected := []string{"/directory", "/directory/subdirectory", "/linked"} assert.Contains(t, result, "/directory")
assert.Contains(t, result, "/directory/subdirectory")
assert.Contains(t, result, "/linked")
assert.Contains(t, result, "/linked/photoprism")
assert.Contains(t, result, "/linked/photoprism/sub")
})
assert.Equal(t, expected, result) t.Run("recursive no-symlinks", func(t *testing.T) {
result, err := Dirs("testdata", true, false)
if err != nil {
t.Fatal(err)
}
assert.Contains(t, result, "/directory")
assert.Contains(t, result, "/directory/subdirectory")
assert.Contains(t, result, "/linked")
}) })
t.Run("non-recursive", func(t *testing.T) { t.Run("non-recursive", func(t *testing.T) {
result, err := Dirs("testdata", false) result, err := Dirs("testdata", false, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
expected := []string{"/directory", "/linked"} assert.Contains(t, result, "/directory")
assert.Contains(t, result, "/linked")
})
assert.Equal(t, expected, result) t.Run("non-recursive no-symlinks", func(t *testing.T) {
result, err := Dirs("testdata/directory/subdirectory", false, false)
if err != nil {
t.Fatal(err)
}
assert.Contains(t, result, "/bar")
})
t.Run("non-recursive symlinks", func(t *testing.T) {
result, err := Dirs("testdata/linked", false, true)
if err != nil {
t.Fatal(err)
}
assert.Contains(t, result, "/photoprism")
assert.Contains(t, result, "/self")
})
t.Run("no-result", func(t *testing.T) {
result, err := Dirs("testdata/linked", false, false)
if err != nil {
t.Fatal(err)
}
assert.Empty(t, result)
}) })
} }

View file

@ -94,6 +94,7 @@ func TestIgnoreList_Ignored(t *testing.T) {
expectIgnored := []string{ expectIgnored := []string{
"testdata/directory/bar.txt", "testdata/directory/bar.txt",
"testdata/directory/baz.xml", "testdata/directory/baz.xml",
"testdata/directory/subdirectory/bar",
"testdata/directory/subdirectory/example.txt", "testdata/directory/subdirectory/example.txt",
"testdata/directory/subdirectory/foo.txt", "testdata/directory/subdirectory/foo.txt",
} }

View file

@ -0,0 +1 @@
# Hello World!

1
pkg/fs/testdata/linked/photoprism vendored Symbolic link
View file

@ -0,0 +1 @@
../.photoprism

View file

@ -1,7 +1,6 @@
package txt package txt
import ( import (
"fmt"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -116,7 +115,6 @@ func Time(s string) (result time.Time) {
time.UTC) time.UTC)
} else if found := DatePathRegexp.Find(b); len(found) > 0 { // Is it a date path like "2020/01/03"? } else if found := DatePathRegexp.Find(b); len(found) > 0 { // Is it a date path like "2020/01/03"?
n := DateIntRegexp.FindAll(found, -1) n := DateIntRegexp.FindAll(found, -1)
fmt.Println(n)
if len(n) < 2 || len(n) > 3 { if len(n) < 2 || len(n) > 3 {
return result return result