diff --git a/internal/file/file_js.go b/internal/file/file_js.go index ddb3c3506..ddb3fc21f 100644 --- a/internal/file/file_js.go +++ b/internal/file/file_js.go @@ -24,13 +24,24 @@ import ( ) type FileEntryFS struct { - rootEntry js.Value + rootEntries []js.Value } -func NewFileEntryFS(root js.Value) *FileEntryFS { - return &FileEntryFS{ - rootEntry: root, +func NewFileEntryFS(rootEntries []js.Value) (*FileEntryFS, error) { + // Check all the full paths are the same. + var fullpath string + for _, ent := range rootEntries { + if fullpath == "" { + fullpath = ent.Get("fullPath").String() + continue + } + if fullpath != ent.Get("fullPath").String() { + return nil, errors.New("file: all the full paths must be the same") + } } + return &FileEntryFS{ + rootEntries: rootEntries, + }, nil } func (f *FileEntryFS) Open(name string) (fs.File, error) { @@ -43,33 +54,55 @@ func (f *FileEntryFS) Open(name string) (fs.File, error) { } if name == "." { - return &dir{dirEntry: f.rootEntry}, nil + var dirName string + for _, ent := range f.rootEntries { + if dirName == "" { + dirName = ent.Get("name").String() + continue + } + if dirName != ent.Get("name").String() { + return nil, &fs.PathError{ + Op: "open", + Path: name, + Err: errors.New("invalid directory"), + } + } + } + return &dir{ + name: dirName, + dirEntries: f.rootEntries, + }, nil } - var chEntry chan js.Value - cbSuccess := js.FuncOf(func(this js.Value, args []js.Value) any { - chEntry <- args[0] - close(chEntry) - return nil - }) - defer cbSuccess.Release() + for _, ent := range f.rootEntries { + var chEntry chan js.Value + cbSuccess := js.FuncOf(func(this js.Value, args []js.Value) any { + chEntry <- args[0] + close(chEntry) + return nil + }) + defer cbSuccess.Release() - cbFailure := js.FuncOf(func(this js.Value, args []js.Value) any { - close(chEntry) - return nil - }) - defer cbFailure.Release() + cbFailure := js.FuncOf(func(this js.Value, args []js.Value) any { + close(chEntry) + return nil + }) + defer cbFailure.Release() - chEntry = make(chan js.Value) - f.rootEntry.Call("getFile", name, nil, cbSuccess, cbFailure) - if entry := <-chEntry; entry.Truthy() { - return &file{entry: entry}, nil - } + chEntry = make(chan js.Value) + ent.Call("getFile", name, nil, cbSuccess, cbFailure) + if entry := <-chEntry; entry.Truthy() { + return &file{entry: entry}, nil + } - chEntry = make(chan js.Value) - f.rootEntry.Call("getDirectory", name, nil, cbSuccess, cbFailure) - if entry := <-chEntry; entry.Truthy() { - return &dir{dirEntry: entry}, nil + chEntry = make(chan js.Value) + ent.Call("getDirectory", name, nil, cbSuccess, cbFailure) + if entry := <-chEntry; entry.Truthy() { + return &dir{ + name: entry.Get("name").String(), + dirEntries: []js.Value{entry}, + }, nil + } } return nil, &fs.PathError{ @@ -163,21 +196,22 @@ func (f *file) Close() error { } type dir struct { - dirEntry js.Value + name string + dirEntries []js.Value fileEntries []js.Value offset int } func (d *dir) Stat() (fs.FileInfo, error) { return &fileInfo{ - name: d.dirEntry.Get("name").String(), + name: d.name, }, nil } func (d *dir) Read(buf []byte) (int, error) { return 0, &fs.PathError{ Op: "read", - Path: d.dirEntry.Get("name").String(), + Path: d.name, Err: errors.New("is a directory"), } } @@ -188,39 +222,48 @@ func (d *dir) Close() error { func (d *dir) ReadDir(count int) ([]fs.DirEntry, error) { if d.fileEntries == nil { - ch := make(chan struct{}) - var rec js.Func - cb := js.FuncOf(func(this js.Value, args []js.Value) any { - entries := args[0] - if entries.Length() == 0 { - close(ch) + names := map[string]struct{}{} + for _, dirEntry := range d.dirEntries { + ch := make(chan struct{}) + var rec js.Func + cb := js.FuncOf(func(this js.Value, args []js.Value) any { + entries := args[0] + if entries.Length() == 0 { + close(ch) + return nil + } + for i := 0; i < entries.Length(); i++ { + ent := entries.Index(i) + name := ent.Get("name").String() + // A name can be empty when this directory is a root directory. + if name == "" { + continue + } + // Avoid entry duplications. Entry duplications happen when multiple files are dropped on Chrome. + if _, ok := names[name]; ok { + continue + } + if !ent.Get("isFile").Bool() && !ent.Get("isDirectory").Bool() { + continue + } + d.fileEntries = append(d.fileEntries, ent) + names[name] = struct{}{} + } + rec.Value.Call("call") return nil - } - for i := 0; i < entries.Length(); i++ { - ent := entries.Index(i) - // A name can be empty when this directory is a root directory. - if ent.Get("name").String() == "" { - continue - } - if !ent.Get("isFile").Bool() && !ent.Get("isDirectory").Bool() { - continue - } - d.fileEntries = append(d.fileEntries, ent) - } + }) + defer cb.Release() + + reader := dirEntry.Call("createReader") + rec = js.FuncOf(func(this js.Value, args []js.Value) any { + reader.Call("readEntries", cb) + return nil + }) + defer rec.Release() + rec.Value.Call("call") - return nil - }) - defer cb.Release() - - reader := d.dirEntry.Call("createReader") - rec = js.FuncOf(func(this js.Value, args []js.Value) any { - reader.Call("readEntries", cb) - return nil - }) - defer rec.Release() - - rec.Value.Call("call") - <-ch + <-ch + } } n := len(d.fileEntries) - d.offset diff --git a/internal/ui/ui_js.go b/internal/ui/ui_js.go index 1b846c585..f3c90da44 100644 --- a/internal/ui/ui_js.go +++ b/internal/ui/ui_js.go @@ -707,14 +707,21 @@ func (u *UserInterface) appendDroppedFiles(data js.Value) { defer u.dropFileM.Unlock() items := data.Get("items") + var entries []js.Value for i := 0; i < items.Length(); i++ { kind := items.Index(i).Get("kind").String() switch kind { case "file": - fs := items.Index(i).Call("webkitGetAsEntry").Get("filesystem").Get("root") - u.inputState.DroppedFiles = file.NewFileEntryFS(fs) + entries = append(entries, items.Index(i).Call("webkitGetAsEntry").Get("filesystem").Get("root")) + } + } + if len(entries) > 0 { + fs, err := file.NewFileEntryFS(entries) + if err != nil { + u.setError(err) return } + u.inputState.DroppedFiles = fs } }