store.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. //go:build wasm
  2. // +build wasm
  3. package indexeddb
  4. import (
  5. "context"
  6. "errors"
  7. "path"
  8. "time"
  9. "github.com/hack-pad/go-indexeddb/idb"
  10. "github.com/hack-pad/hackpadfs"
  11. "github.com/hack-pad/hackpadfs/indexeddb/idbblob"
  12. "github.com/hack-pad/hackpadfs/keyvalue"
  13. "github.com/hack-pad/hackpadfs/keyvalue/blob"
  14. "github.com/hack-pad/safejs"
  15. )
  16. var (
  17. _ interface {
  18. keyvalue.Store
  19. keyvalue.TransactionStore
  20. } = &store{}
  21. )
  22. type store struct {
  23. db *idb.Database
  24. options Options
  25. }
  26. func newStore(db *idb.Database, options Options) *store {
  27. return &store{db: db, options: options}
  28. }
  29. func (s *store) Get(ctx context.Context, path string) (keyvalue.FileRecord, error) {
  30. txn, err := s.Transaction(keyvalue.TransactionOptions{})
  31. if err != nil {
  32. return nil, err
  33. }
  34. txn.Get(path)
  35. ops, err := txn.Commit(ctx)
  36. err = getFirstCommitError(ops, err)
  37. if err != nil {
  38. return nil, err
  39. }
  40. return ops[0].Record, nil
  41. }
  42. func getFirstCommitError(ops []keyvalue.OpResult, err error) error {
  43. if err == nil || errors.Is(err, errAborted) {
  44. for _, op := range ops {
  45. if op.Err != nil {
  46. return op.Err
  47. }
  48. }
  49. }
  50. return err
  51. }
  52. func (s *store) getFile(files *idb.ObjectStore, path string) (*getFileRequest, error) {
  53. jsPath, err := safejs.ValueOf(path)
  54. if err != nil {
  55. return nil, err
  56. }
  57. req, err := files.Get(safejs.Unsafe(jsPath))
  58. if err != nil {
  59. return nil, err
  60. }
  61. return newGetFileRequest(s, path, req), nil
  62. }
  63. type getFileRequest struct {
  64. *idb.Request
  65. store *store
  66. path string
  67. }
  68. func newGetFileRequest(s *store, path string, req *idb.Request) *getFileRequest {
  69. return &getFileRequest{
  70. Request: req,
  71. store: s,
  72. path: path,
  73. }
  74. }
  75. func (g *getFileRequest) Result() (keyvalue.FileRecord, error) {
  76. result, err := g.Request.Result()
  77. return g.parseResult(safejs.Safe(result), err)
  78. }
  79. func (g *getFileRequest) parseResult(result safejs.Value, err error) (keyvalue.FileRecord, error) {
  80. if result.IsUndefined() {
  81. return nil, hackpadfs.ErrNotExist
  82. }
  83. if err != nil {
  84. return nil, err
  85. }
  86. jsInitialSize, err := result.Get("Size")
  87. if err != nil {
  88. return nil, err
  89. }
  90. initialSize, err := jsInitialSize.Int()
  91. if err != nil {
  92. return nil, err
  93. }
  94. jsModTime, err := result.Get("ModTime")
  95. if err != nil {
  96. return nil, err
  97. }
  98. intModTime, err := jsModTime.Int()
  99. if err != nil {
  100. return nil, err
  101. }
  102. modTime := time.Unix(0, int64(intModTime))
  103. mode, err := getMode(result)
  104. if err != nil {
  105. return nil, err
  106. }
  107. var getData func() (blob.Blob, error)
  108. var getDirNames func() ([]string, error)
  109. if mode.IsDir() {
  110. getDirNames = g.store.getDirNames(g.path)
  111. } else {
  112. getData = g.store.getFileData(g.path)
  113. }
  114. return keyvalue.NewBaseFileRecord(int64(initialSize), modTime, mode, nil, getData, getDirNames), nil
  115. }
  116. func (s *store) getFileData(path string) func() (blob.Blob, error) {
  117. return func() (blob.Blob, error) {
  118. txn, err := s.db.TransactionWithOptions(idb.TransactionOptions{
  119. Mode: idb.TransactionReadOnly,
  120. Durability: s.options.TransactionDurability,
  121. }, contentsStore)
  122. if err != nil {
  123. return nil, err
  124. }
  125. files, err := txn.ObjectStore(contentsStore)
  126. if err != nil {
  127. return nil, err
  128. }
  129. jsPath, err := safejs.ValueOf(path)
  130. if err != nil {
  131. return nil, err
  132. }
  133. req, err := files.Get(safejs.Unsafe(jsPath))
  134. if err != nil {
  135. return nil, err
  136. }
  137. value, err := req.Await(context.Background())
  138. if value.IsUndefined() {
  139. return nil, hackpadfs.ErrNotExist
  140. }
  141. if err != nil {
  142. return nil, err
  143. }
  144. return idbblob.New(value)
  145. }
  146. }
  147. func (s *store) getDirNames(name string) func() ([]string, error) {
  148. return func() (_ []string, err error) {
  149. txn, err := s.db.TransactionWithOptions(idb.TransactionOptions{
  150. Mode: idb.TransactionReadOnly,
  151. Durability: s.options.TransactionDurability,
  152. }, infoStore)
  153. if err != nil {
  154. return nil, err
  155. }
  156. files, err := txn.ObjectStore(infoStore)
  157. if err != nil {
  158. return nil, err
  159. }
  160. parentIndex, err := files.Index(parentKey)
  161. if err != nil {
  162. return nil, err
  163. }
  164. jsName, err := safejs.ValueOf(name)
  165. if err != nil {
  166. return nil, err
  167. }
  168. keyRange, err := idb.NewKeyRangeOnly(safejs.Unsafe(jsName))
  169. if err != nil {
  170. return nil, err
  171. }
  172. keysReq, err := parentIndex.GetAllKeysRange(keyRange, 0)
  173. if err != nil {
  174. return nil, err
  175. }
  176. jsKeys, err := keysReq.Await(context.Background())
  177. var keys []string
  178. if err == nil {
  179. for _, jsKey := range jsKeys {
  180. keys = append(keys, path.Base(jsKey.String()))
  181. }
  182. }
  183. return keys, err
  184. }
  185. }
  186. func getMode(fileRecord safejs.Value) (hackpadfs.FileMode, error) {
  187. mode, err := fileRecord.Get("Mode")
  188. if err != nil {
  189. return 0, err
  190. }
  191. intMode, err := mode.Int()
  192. return hackpadfs.FileMode(intMode), err
  193. }
  194. const rootPath = "."
  195. var errAborted = idb.NewDOMException("AbortError")
  196. func (s *store) Set(ctx context.Context, name string, record keyvalue.FileRecord) error {
  197. var data blob.Blob
  198. if record != nil && record.Mode().IsRegular() { // i.e. "should not delete" AND "is a regular file"
  199. var err error
  200. data, err = record.Data()
  201. if err != nil {
  202. return err
  203. }
  204. }
  205. txn, err := s.Transaction(keyvalue.TransactionOptions{
  206. Mode: keyvalue.TransactionReadWrite,
  207. })
  208. if err != nil {
  209. return err
  210. }
  211. txn.Set(name, record, data)
  212. ops, err := txn.Commit(ctx)
  213. return getFirstCommitError(ops, err)
  214. }
  215. func deleteRecord(infos, contents *idb.ObjectStore, name string) (*idb.AckRequest, error) {
  216. jsName, err := safejs.ValueOf(name)
  217. if err != nil {
  218. return nil, err
  219. }
  220. _, err = infos.Delete(safejs.Unsafe(jsName))
  221. if err != nil {
  222. return nil, err
  223. }
  224. return contents.Delete(safejs.Unsafe(jsName))
  225. }
  226. func setFileContents(contents *idb.ObjectStore, name string, data blob.Blob) error {
  227. jsName, err := safejs.ValueOf(name)
  228. if err != nil {
  229. return err
  230. }
  231. _, err = contents.PutKey(safejs.Unsafe(jsName), idbblob.FromBlob(data).JSValue())
  232. return err
  233. }
  234. // 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().
  235. func validateAndSetFileMeta(ctx context.Context, infos *idb.ObjectStore, name string, record keyvalue.FileRecord, data blob.Blob) (*idb.Request, *parentDirExistsReq, error) {
  236. var size int64
  237. if data == nil {
  238. size = record.Size()
  239. } else {
  240. size = int64(data.Len())
  241. }
  242. fileInfo := map[string]interface{}{
  243. "ModTime": record.ModTime().UnixNano(),
  244. "Mode": uint32(record.Mode()),
  245. "Size": size,
  246. }
  247. if name != rootPath {
  248. fileInfo[parentKey] = path.Dir(name)
  249. }
  250. parentExistsReq, err := requireParentDirectoryExists(ctx, infos, name)
  251. if err != nil {
  252. return nil, nil, err
  253. }
  254. jsName, err := safejs.ValueOf(name)
  255. if err != nil {
  256. return nil, nil, err
  257. }
  258. jsFileInfo, err := safejs.ValueOf(fileInfo)
  259. if err != nil {
  260. return nil, nil, err
  261. }
  262. req, err := infos.PutKey(safejs.Unsafe(jsName), safejs.Unsafe(jsFileInfo))
  263. return req, parentExistsReq, err
  264. }
  265. type parentDirExistsReq struct {
  266. *idb.Request
  267. notExists bool
  268. }
  269. func (p *parentDirExistsReq) Err() error {
  270. if p.notExists {
  271. return hackpadfs.ErrNotDir
  272. }
  273. return p.Request.Err()
  274. }
  275. // requireParentDirectoryExists returns an async err chan. Async error is nil if directory exists.
  276. func requireParentDirectoryExists(ctx context.Context, infos *idb.ObjectStore, name string) (*parentDirExistsReq, error) {
  277. dir := path.Dir(name)
  278. if dir == "" || dir == rootPath {
  279. return nil, nil
  280. }
  281. jsDir, err := safejs.ValueOf(dir)
  282. if err != nil {
  283. return nil, err
  284. }
  285. req, err := infos.Get(safejs.Unsafe(jsDir))
  286. if err != nil {
  287. return nil, err
  288. }
  289. parentReq := &parentDirExistsReq{
  290. Request: req,
  291. }
  292. listenErr := req.ListenSuccess(ctx, func() {
  293. result, err := req.Result()
  294. if err != nil || result.IsUndefined() {
  295. parentReq.notExists = result.IsUndefined()
  296. if txn, err := infos.Transaction(); err == nil {
  297. _ = txn.Abort()
  298. }
  299. return
  300. }
  301. mode, err := getMode(safejs.Safe(result))
  302. if err == nil && !mode.IsDir() {
  303. if txn, err := infos.Transaction(); err == nil {
  304. _ = txn.Abort()
  305. }
  306. parentReq.notExists = true
  307. return
  308. }
  309. })
  310. return parentReq, listenErr
  311. }
  312. func (s *store) Transaction(options keyvalue.TransactionOptions) (keyvalue.Transaction, error) {
  313. mode := idb.TransactionReadOnly
  314. stores := []string{infoStore}
  315. if options.Mode == keyvalue.TransactionReadWrite {
  316. mode = idb.TransactionReadWrite
  317. stores = append(stores, contentsStore)
  318. }
  319. ctx, cancel := context.WithCancel(context.Background())
  320. txn, err := s.db.TransactionWithOptions(idb.TransactionOptions{
  321. Mode: mode,
  322. Durability: s.options.TransactionDurability,
  323. }, stores[0], stores[1:]...)
  324. return &transaction{
  325. ctx: ctx,
  326. abort: cancel,
  327. store: s,
  328. txn: txn,
  329. results: make(map[keyvalue.OpID]keyvalue.OpResult),
  330. }, err
  331. }