Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ListableURI interface & Initial Implementation thereof #1233

Merged
merged 19 commits into from
Aug 31, 2020
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ type Driver interface {
// This should refer to a filesystem resource as external data will not be writable.
FileWriterForURI(URI) (URIWriteCloser, error)

// ListerForURI converts a URI to a listable URI, if it is possible to do so.
ListerForURI(URI) (ListableURI, error)

// CanvasForObject returns the canvas that is associated with a given CanvasObject.
CanvasForObject(CanvasObject) Canvas
// AbsolutePositionForObject returns the position of a given CanvasObject relative to the top/left of a canvas.
Expand Down
48 changes: 48 additions & 0 deletions internal/driver/glfw/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package glfw

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"

"fyne.io/fyne"
"fyne.io/fyne/storage"
Expand All @@ -13,6 +15,52 @@ type file struct {
path string
}

type directory struct {
fyne.URI
}

// Declare conformity to the ListableURI interface
var _ fyne.ListableURI = (*directory)(nil)

func (d *directory) List() ([]fyne.URI, error) {
if d.Scheme() != "file" {
return nil, fmt.Errorf("unsupported URL protocol")
}

path := d.String()[len(d.Scheme())+3 : len(d.String())]
files, err := ioutil.ReadDir(path)
if err != nil {
return nil, err
}

urilist := []fyne.URI{}

for _, f := range files {
uri := storage.NewURI("file://" + filepath.Join(path, f.Name()))
urilist = append(urilist, uri)
}

return urilist, nil
}

func (d *gLDriver) ListerForURI(uri fyne.URI) (fyne.ListableURI, error) {
if uri.Scheme() != "file" {
return nil, fmt.Errorf("unsupported URL protocol")
}

path := uri.String()[len(uri.Scheme())+3 : len(uri.String())]
s, err := os.Stat(path)
if err != nil {
return nil, err
}

if !s.IsDir() {
return nil, fmt.Errorf("Path '%s' is not a directory, cannot convert to listable URI", path)
}

return &directory{URI: uri}, nil
}

func (d *gLDriver) FileReaderForURI(uri fyne.URI) (fyne.URIReadCloser, error) {
return openFile(uri, false)
}
Expand Down
57 changes: 57 additions & 0 deletions internal/driver/glfw/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,60 @@ func testURI(file string) (fyne.URI, string) {

return uri, path
}

func Test_ListableURI(t *testing.T) {
tempdir, err := ioutil.TempDir("", "file_test")
if err != nil {
t.Fatal(err)
}

defer os.RemoveAll(tempdir)

content := []byte("Test Content!")

err = ioutil.WriteFile(filepath.Join(tempdir, "aaa"), content, 0666)
if err != nil {
t.Fatal(err)
}

err = ioutil.WriteFile(filepath.Join(tempdir, "bbb"), content, 0666)
if err != nil {
t.Fatal(err)
}

err = ioutil.WriteFile(filepath.Join(tempdir, "ccc"), content, 0666)
if err != nil {
t.Fatal(err)
}

uri := storage.NewURI("file://" + tempdir)

luri, err := NewGLDriver().ListerForURI(uri)
if err != nil {
t.Fatal(err)
}

listing, err := luri.List()
if err != nil {
t.Fatal(err)
}

if len(listing) != 3 {
t.Errorf("Expected directory listing to contain exactly 3 items")
}

listingStrings := []string{}
for _, v := range listing {
t.Logf("stringify URI %v to %s", v, v.String())
listingStrings = append(listingStrings, v.String())
}

expect := []string{
"file://" + filepath.Join(tempdir, "aaa"),
"file://" + filepath.Join(tempdir, "bbb"),
"file://" + filepath.Join(tempdir, "ccc"),
}

assert.ElementsMatch(t, listingStrings, expect)

}
5 changes: 5 additions & 0 deletions internal/driver/gomobile/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gomobile

import (
"errors"
"fmt"
"io"

"github.com/fyne-io/mobile/app"
Expand Down Expand Up @@ -34,6 +35,10 @@ func (d *mobileDriver) FileReaderForURI(u fyne.URI) (fyne.URIReadCloser, error)
return file, err
}

func (d *mobileDriver) ListerForURI(uri fyne.URI) (fyne.ListableURI, error) {
return nil, fmt.Errorf("mobile driver does support creating listable URIs yet")
}

func (d *mobileDriver) FileWriterForURI(u fyne.URI) (fyne.URIWriteCloser, error) {
return nil, errors.New("file writing is not supported on mobile")
}
Expand Down
6 changes: 6 additions & 0 deletions storage/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ func OpenFileFromURI(uri fyne.URI) (fyne.URIReadCloser, error) {
func SaveFileToURI(uri fyne.URI) (fyne.URIWriteCloser, error) {
return fyne.CurrentApp().Driver().FileWriterForURI(uri)
}

// ListerForURI will attempt to use the application's driver to convert a
// standard URI into a listable URI.
func ListerForURI(uri fyne.URI) (fyne.ListableURI, error) {
return fyne.CurrentApp().Driver().ListerForURI(uri)
}
52 changes: 52 additions & 0 deletions storage/uri.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"mime"
"path/filepath"
"regexp"
"strings"
"unicode/utf8"

Expand Down Expand Up @@ -57,3 +58,54 @@ func (u *uri) Scheme() string {
func (u *uri) String() string {
return u.raw
}

// Parent gets the parent of a URI by splitting it along '/' separators and
// removing the last item.
func Parent(u fyne.URI) (fyne.URI, error) {
s := u.String()

// trim trailing slash
if s[len(s)-1] == '/' {
s = s[0 : len(s)-1]
}

// trim the scheme (and +1 for the :)
s = s[len(u.Scheme())+1 : len(s)]

// Completely empty URI with just a scheme
if len(s) == 0 {
return nil, URIRootError
}

// trim leading forward slashes
trimmed := 0
for s[0] == '/' {
s = s[1:len(s)]
trimmed++

// if all we have left is an empty string, than this URI
// pointed to a UNIX-style root
if len(s) == 0 {
return nil, URIRootError
}
}

// handle Windows drive letters
r := regexp.MustCompile("[A-Za-z][:]")
components := strings.Split(s, "/")
if len(components) == 1 && r.MatchString(components[0]) && trimmed <= 2 {
// trimmed <= 2 makes sure we handle UNIX-style paths on
// Windows correctly
return nil, URIRootError
}

parent := u.Scheme() + "://"
if trimmed > 2 && len(components) > 1 {
// Because we trimmed all the leading '/' characters, for UNIX
// style paths we want to insert one back in. Presumably we
// trimmed two instances of / for the scheme.
parent = parent + "/"
}
parent = parent + strings.Join(components[0:len(components)-1], "/") + "/"
return NewURI(parent), nil
}
16 changes: 16 additions & 0 deletions storage/uri_root_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package storage

// Declare conformance with Error interface
var _ error = URIRootError

type uriRootError string

// URIRootError should be thrown by fyne.URI implementations when the caller
// attempts to take the parent of the root. This way, downstream code that
// wants to programmatically walk up a URIs parent's will know when to stop
// iterating.
const URIRootError uriRootError = uriRootError("Cannot take the parent of the root element in a URI")

func (e uriRootError) Error() string {
return string(URIRootError)
}
30 changes: 30 additions & 0 deletions storage/uri_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,36 @@ func TestURI_Name(t *testing.T) {
assert.Equal(t, "directory", storage.NewURI("file://C:/directory/").Name())
}

func TestURI_Parent(t *testing.T) {
// note the trailing slashes are significant, as they tend to belie a
// directory

parent, err := storage.Parent(storage.NewURI("file:///foo/bar/baz"))
assert.Nil(t, err)
assert.Equal(t, "file:///foo/bar/", parent.String())

parent, err = storage.Parent(storage.NewURI("file:///foo/bar/baz/"))
assert.Nil(t, err)
assert.Equal(t, "file:///foo/bar/", parent.String())

parent, err = storage.Parent(storage.NewURI("file://C:/foo/bar/baz/"))
assert.Nil(t, err)
assert.Equal(t, "file://C:/foo/bar/", parent.String())

parent, err = storage.Parent(storage.NewURI("file:///"))
assert.Equal(t, storage.URIRootError, err)

// This should cause an error, since this is a Windows-style path and
// thus we can't get the parent of a drive letter.
parent, err = storage.Parent(storage.NewURI("file://C:/"))
assert.Equal(t, storage.URIRootError, err)

// Windows supports UNIX-style paths. /C:/ is also a valid path.
parent, err = storage.Parent(storage.NewURI("file:///C:/"))
assert.Nil(t, err)
assert.Equal(t, "file:///", parent.String())
}
charlesdaniels marked this conversation as resolved.
Show resolved Hide resolved

func TestURI_Extension(t *testing.T) {
assert.Equal(t, ".txt", storage.NewURI("file:///text.txt").Extension())
assert.Equal(t, ".a", storage.NewURI("file:///library.a").Extension())
Expand Down
50 changes: 50 additions & 0 deletions test/testfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package test
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"

Expand Down Expand Up @@ -55,3 +56,52 @@ func (d *testDriver) FileReaderForURI(uri fyne.URI) (fyne.URIReadCloser, error)
func (d *testDriver) FileWriterForURI(uri fyne.URI) (fyne.URIWriteCloser, error) {
return openFile(uri, true)
}

// lazily copied over from the glfw driver, should be good enough for testing
// purposes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be revisited? We probably don't need a full copy of an implementation for testing, and could probably get by with a simple type that saves its parameters and returns predetermined results, both of which can be asserted against.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I agree with this. Copying implementation into tests is a bit of a smell - as you're not really testing that code but potentially copying bugs into the test code


type directory struct {
fyne.URI
}

// Declare conformity to the ListableURI interface
var _ fyne.ListableURI = (*directory)(nil)

func (d *directory) List() ([]fyne.URI, error) {
if d.Scheme() != "file" {
return nil, fmt.Errorf("unsupported URL protocol")
}

path := d.String()[len(d.Scheme())+3 : len(d.String())]
files, err := ioutil.ReadDir(path)
if err != nil {
return nil, err
}

urilist := []fyne.URI{}

for _, f := range files {
uri := storage.NewURI("file://" + filepath.Join(path, f.Name()))
urilist = append(urilist, uri)
}

return urilist, nil
}

func (d *testDriver) ListerForURI(uri fyne.URI) (fyne.ListableURI, error) {
if uri.Scheme() != "file" {
return nil, fmt.Errorf("unsupported URL protocol")
}

path := uri.String()[len(uri.Scheme())+3 : len(uri.String())]
s, err := os.Stat(path)
if err != nil {
return nil, err
}

if !s.IsDir() {
return nil, fmt.Errorf("Path '%s' is not a directory, cannot convert to listable URI", path)
}

return &directory{URI: uri}, nil
}
9 changes: 9 additions & 0 deletions uri.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,12 @@ type URI interface {
MimeType() string
Scheme() string
}

// ListableURI represents a URI that can have child items, most commonly a
// directory on disk in the native filesystem.
type ListableURI interface {
URI

// List returns a list of child URIs of this URI.
List() ([]URI, error)
}