// Copyright 2023 The Ebitengine Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package file import ( "errors" "fmt" "io" "io/fs" "syscall/js" "time" ) type FileEntryFS struct { rootEntry js.Value } func NewFileEntryFS(root js.Value) *FileEntryFS { return &FileEntryFS{ rootEntry: root, } } func (f *FileEntryFS) Open(name string) (fs.File, error) { if !fs.ValidPath(name) { return nil, &fs.PathError{ Op: "open", Path: name, Err: fs.ErrNotExist, } } if name == "." { return &dir{entry: f.rootEntry}, 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() 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) f.rootEntry.Call("getDirectory", name, nil, cbSuccess, cbFailure) if entry := <-chEntry; entry.Truthy() { return &dir{entry: entry}, nil } return nil, &fs.PathError{ Op: "open", Path: name, Err: fs.ErrNotExist, } } type file struct { entry js.Value file js.Value offset int64 uint8Array js.Value } func getFile(entry js.Value) js.Value { ch := make(chan js.Value, 1) cb := js.FuncOf(func(this js.Value, args []js.Value) any { ch <- args[0] return nil }) defer cb.Release() entry.Call("file", cb) return <-ch } func (f *file) ensureFile() js.Value { if f.file.Truthy() { return f.file } f.file = getFile(f.entry) return f.file } func (f *file) Stat() (fs.FileInfo, error) { return &fileInfo{ entry: f.entry, file: f.ensureFile(), }, nil } func (f *file) Read(buf []byte) (int, error) { if !f.uint8Array.Truthy() { chArrayBuffer := make(chan js.Value, 1) cbThen := js.FuncOf(func(this js.Value, args []js.Value) any { chArrayBuffer <- args[0] return nil }) defer cbThen.Release() chError := make(chan js.Value, 1) cbCatch := js.FuncOf(func(this js.Value, args []js.Value) any { chError <- args[0] return nil }) defer cbCatch.Release() f.ensureFile().Call("arrayBuffer").Call("then", cbThen).Call("catch", cbCatch) select { case ab := <-chArrayBuffer: f.uint8Array = js.Global().Get("Uint8Array").New(ab) case err := <-chError: return 0, fmt.Errorf("%s", err.Call("toString").String()) } } size := int64(f.uint8Array.Get("byteLength").Float()) if f.offset >= size { return 0, io.EOF } if len(buf) == 0 { return 0, nil } slice := f.uint8Array.Call("subarray", f.offset, f.offset+int64(len(buf))) n := slice.Get("byteLength").Int() js.CopyBytesToGo(buf[:n], slice) f.offset += int64(n) if f.offset >= size { return n, io.EOF } return n, nil } func (f *file) Close() error { return nil } type dir struct { entry js.Value entries []js.Value offset int } func (d *dir) Stat() (fs.FileInfo, error) { return &fileInfo{ entry: d.entry, }, nil } func (d *dir) Read(buf []byte) (int, error) { return 0, &fs.PathError{ Op: "read", Path: d.entry.Get("name").String(), Err: errors.New("is a directory"), } } func (d *dir) Close() error { return nil } func (d *dir) ReadDir(count int) ([]fs.DirEntry, error) { if d.entries == 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) 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.entries = append(d.entries, ent) } rec.Value.Call("call") return nil }) defer cb.Release() reader := d.entry.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 } n := len(d.entries) - d.offset if n == 0 { if count <= 0 { return nil, nil } return nil, io.EOF } if count > 0 && n > count { n = count } ents := make([]fs.DirEntry, n) for i := range ents { fi := &fileInfo{ entry: d.entries[d.offset+i], } if fi.entry.Get("isFile").Bool() { fi.file = getFile(fi.entry) } ents[i] = fs.FileInfoToDirEntry(fi) } d.offset += n return ents, nil } type fileInfo struct { entry js.Value file js.Value } func (f *fileInfo) Name() string { return f.entry.Get("name").String() } func (f *fileInfo) Size() int64 { if !f.file.Truthy() { return 0 } return int64(f.file.Get("size").Float()) } func (f *fileInfo) Mode() fs.FileMode { if !f.file.Truthy() { return 0555 | fs.ModeDir } return 0444 } func (f *fileInfo) ModTime() time.Time { if !f.file.Truthy() { return time.Time{} } return time.UnixMilli(int64(f.file.Get("lastModified").Float())) } func (f *fileInfo) IsDir() bool { if !f.file.Truthy() { return true } return false } func (f *fileInfo) Sys() any { return nil }