| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- //go:build wasm
- // +build wasm
- package indexeddb
- import (
- "context"
- "errors"
- "path"
- "time"
- "github.com/hack-pad/go-indexeddb/idb"
- "github.com/hack-pad/hackpadfs"
- "github.com/hack-pad/hackpadfs/indexeddb/idbblob"
- "github.com/hack-pad/hackpadfs/keyvalue"
- "github.com/hack-pad/hackpadfs/keyvalue/blob"
- "github.com/hack-pad/safejs"
- )
- var (
- _ interface {
- keyvalue.Store
- keyvalue.TransactionStore
- } = &store{}
- )
- type store struct {
- db *idb.Database
- options Options
- }
- func newStore(db *idb.Database, options Options) *store {
- return &store{db: db, options: options}
- }
- func (s *store) Get(ctx context.Context, path string) (keyvalue.FileRecord, error) {
- txn, err := s.Transaction(keyvalue.TransactionOptions{})
- if err != nil {
- return nil, err
- }
- txn.Get(path)
- ops, err := txn.Commit(ctx)
- err = getFirstCommitError(ops, err)
- if err != nil {
- return nil, err
- }
- return ops[0].Record, nil
- }
- func getFirstCommitError(ops []keyvalue.OpResult, err error) error {
- if err == nil || errors.Is(err, errAborted) {
- for _, op := range ops {
- if op.Err != nil {
- return op.Err
- }
- }
- }
- return err
- }
- func (s *store) getFile(files *idb.ObjectStore, path string) (*getFileRequest, error) {
- jsPath, err := safejs.ValueOf(path)
- if err != nil {
- return nil, err
- }
- req, err := files.Get(safejs.Unsafe(jsPath))
- if err != nil {
- return nil, err
- }
- return newGetFileRequest(s, path, req), nil
- }
- type getFileRequest struct {
- *idb.Request
- store *store
- path string
- }
- func newGetFileRequest(s *store, path string, req *idb.Request) *getFileRequest {
- return &getFileRequest{
- Request: req,
- store: s,
- path: path,
- }
- }
- func (g *getFileRequest) Result() (keyvalue.FileRecord, error) {
- result, err := g.Request.Result()
- return g.parseResult(safejs.Safe(result), err)
- }
- func (g *getFileRequest) parseResult(result safejs.Value, err error) (keyvalue.FileRecord, error) {
- if result.IsUndefined() {
- return nil, hackpadfs.ErrNotExist
- }
- if err != nil {
- return nil, err
- }
- jsInitialSize, err := result.Get("Size")
- if err != nil {
- return nil, err
- }
- initialSize, err := jsInitialSize.Int()
- if err != nil {
- return nil, err
- }
- jsModTime, err := result.Get("ModTime")
- if err != nil {
- return nil, err
- }
- intModTime, err := jsModTime.Int()
- if err != nil {
- return nil, err
- }
- modTime := time.Unix(0, int64(intModTime))
- mode, err := getMode(result)
- if err != nil {
- return nil, err
- }
- var getData func() (blob.Blob, error)
- var getDirNames func() ([]string, error)
- if mode.IsDir() {
- getDirNames = g.store.getDirNames(g.path)
- } else {
- getData = g.store.getFileData(g.path)
- }
- return keyvalue.NewBaseFileRecord(int64(initialSize), modTime, mode, nil, getData, getDirNames), nil
- }
- func (s *store) getFileData(path string) func() (blob.Blob, error) {
- return func() (blob.Blob, error) {
- txn, err := s.db.TransactionWithOptions(idb.TransactionOptions{
- Mode: idb.TransactionReadOnly,
- Durability: s.options.TransactionDurability,
- }, contentsStore)
- if err != nil {
- return nil, err
- }
- files, err := txn.ObjectStore(contentsStore)
- if err != nil {
- return nil, err
- }
- jsPath, err := safejs.ValueOf(path)
- if err != nil {
- return nil, err
- }
- req, err := files.Get(safejs.Unsafe(jsPath))
- if err != nil {
- return nil, err
- }
- value, err := req.Await(context.Background())
- if value.IsUndefined() {
- return nil, hackpadfs.ErrNotExist
- }
- if err != nil {
- return nil, err
- }
- return idbblob.New(value)
- }
- }
- func (s *store) getDirNames(name string) func() ([]string, error) {
- return func() (_ []string, err error) {
- txn, err := s.db.TransactionWithOptions(idb.TransactionOptions{
- Mode: idb.TransactionReadOnly,
- Durability: s.options.TransactionDurability,
- }, infoStore)
- if err != nil {
- return nil, err
- }
- files, err := txn.ObjectStore(infoStore)
- if err != nil {
- return nil, err
- }
- parentIndex, err := files.Index(parentKey)
- if err != nil {
- return nil, err
- }
- jsName, err := safejs.ValueOf(name)
- if err != nil {
- return nil, err
- }
- keyRange, err := idb.NewKeyRangeOnly(safejs.Unsafe(jsName))
- if err != nil {
- return nil, err
- }
- keysReq, err := parentIndex.GetAllKeysRange(keyRange, 0)
- if err != nil {
- return nil, err
- }
- jsKeys, err := keysReq.Await(context.Background())
- var keys []string
- if err == nil {
- for _, jsKey := range jsKeys {
- keys = append(keys, path.Base(jsKey.String()))
- }
- }
- return keys, err
- }
- }
- func getMode(fileRecord safejs.Value) (hackpadfs.FileMode, error) {
- mode, err := fileRecord.Get("Mode")
- if err != nil {
- return 0, err
- }
- intMode, err := mode.Int()
- return hackpadfs.FileMode(intMode), err
- }
- const rootPath = "."
- var errAborted = idb.NewDOMException("AbortError")
- func (s *store) Set(ctx context.Context, name string, record keyvalue.FileRecord) error {
- var data blob.Blob
- if record != nil && record.Mode().IsRegular() { // i.e. "should not delete" AND "is a regular file"
- var err error
- data, err = record.Data()
- if err != nil {
- return err
- }
- }
- txn, err := s.Transaction(keyvalue.TransactionOptions{
- Mode: keyvalue.TransactionReadWrite,
- })
- if err != nil {
- return err
- }
- txn.Set(name, record, data)
- ops, err := txn.Commit(ctx)
- return getFirstCommitError(ops, err)
- }
- func deleteRecord(infos, contents *idb.ObjectStore, name string) (*idb.AckRequest, error) {
- jsName, err := safejs.ValueOf(name)
- if err != nil {
- return nil, err
- }
- _, err = infos.Delete(safejs.Unsafe(jsName))
- if err != nil {
- return nil, err
- }
- return contents.Delete(safejs.Unsafe(jsName))
- }
- func setFileContents(contents *idb.ObjectStore, name string, data blob.Blob) error {
- jsName, err := safejs.ValueOf(name)
- if err != nil {
- return err
- }
- _, err = contents.PutKey(safejs.Unsafe(jsName), idbblob.FromBlob(data).JSValue())
- return err
- }
- // validateAndSetFileMeta verifies the file by 'name' has a parent directory, then updates the file metadata. If not nil, 'data' is used to detect size instead of record.Size().
- func validateAndSetFileMeta(ctx context.Context, infos *idb.ObjectStore, name string, record keyvalue.FileRecord, data blob.Blob) (*idb.Request, *parentDirExistsReq, error) {
- var size int64
- if data == nil {
- size = record.Size()
- } else {
- size = int64(data.Len())
- }
- fileInfo := map[string]interface{}{
- "ModTime": record.ModTime().UnixNano(),
- "Mode": uint32(record.Mode()),
- "Size": size,
- }
- if name != rootPath {
- fileInfo[parentKey] = path.Dir(name)
- }
- parentExistsReq, err := requireParentDirectoryExists(ctx, infos, name)
- if err != nil {
- return nil, nil, err
- }
- jsName, err := safejs.ValueOf(name)
- if err != nil {
- return nil, nil, err
- }
- jsFileInfo, err := safejs.ValueOf(fileInfo)
- if err != nil {
- return nil, nil, err
- }
- req, err := infos.PutKey(safejs.Unsafe(jsName), safejs.Unsafe(jsFileInfo))
- return req, parentExistsReq, err
- }
- type parentDirExistsReq struct {
- *idb.Request
- notExists bool
- }
- func (p *parentDirExistsReq) Err() error {
- if p.notExists {
- return hackpadfs.ErrNotDir
- }
- return p.Request.Err()
- }
- // requireParentDirectoryExists returns an async err chan. Async error is nil if directory exists.
- func requireParentDirectoryExists(ctx context.Context, infos *idb.ObjectStore, name string) (*parentDirExistsReq, error) {
- dir := path.Dir(name)
- if dir == "" || dir == rootPath {
- return nil, nil
- }
- jsDir, err := safejs.ValueOf(dir)
- if err != nil {
- return nil, err
- }
- req, err := infos.Get(safejs.Unsafe(jsDir))
- if err != nil {
- return nil, err
- }
- parentReq := &parentDirExistsReq{
- Request: req,
- }
- listenErr := req.ListenSuccess(ctx, func() {
- result, err := req.Result()
- if err != nil || result.IsUndefined() {
- parentReq.notExists = result.IsUndefined()
- if txn, err := infos.Transaction(); err == nil {
- _ = txn.Abort()
- }
- return
- }
- mode, err := getMode(safejs.Safe(result))
- if err == nil && !mode.IsDir() {
- if txn, err := infos.Transaction(); err == nil {
- _ = txn.Abort()
- }
- parentReq.notExists = true
- return
- }
- })
- return parentReq, listenErr
- }
- func (s *store) Transaction(options keyvalue.TransactionOptions) (keyvalue.Transaction, error) {
- mode := idb.TransactionReadOnly
- stores := []string{infoStore}
- if options.Mode == keyvalue.TransactionReadWrite {
- mode = idb.TransactionReadWrite
- stores = append(stores, contentsStore)
- }
- ctx, cancel := context.WithCancel(context.Background())
- txn, err := s.db.TransactionWithOptions(idb.TransactionOptions{
- Mode: mode,
- Durability: s.options.TransactionDurability,
- }, stores[0], stores[1:]...)
- return &transaction{
- ctx: ctx,
- abort: cancel,
- store: s,
- txn: txn,
- results: make(map[keyvalue.OpID]keyvalue.OpResult),
- }, err
- }
|