mirror of
https://github.com/standardnotes/app.git
synced 2026-01-16 23:01:30 +00:00
feat: add filepicker package
This commit is contained in:
parent
577da2ca84
commit
d4188a3fa2
45 changed files with 5848 additions and 25 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -14,6 +14,8 @@ codeqldb
|
|||
coverage
|
||||
lerna-debug.log
|
||||
|
||||
packages/**/dist
|
||||
|
||||
**/.pnp.*
|
||||
**/.yarn/*
|
||||
!.yarn/patches
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/ts-node-npm-10.8.2-f3c0c9eaee-1eede939be.zip
vendored
Normal file
BIN
.yarn/cache/ts-node-npm-10.8.2-f3c0c9eaee-1eede939be.zip
vendored
Normal file
Binary file not shown.
1
packages/encryption/.gitignore
vendored
1
packages/encryption/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
dist
|
||||
1
packages/features/.gitignore
vendored
1
packages/features/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
dist
|
||||
3
packages/filepicker/.eslintignore
Normal file
3
packages/filepicker/.eslintignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
dist
|
||||
example
|
||||
6
packages/filepicker/.eslintrc
Normal file
6
packages/filepicker/.eslintrc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "../../.eslintrc",
|
||||
"parserOptions": {
|
||||
"project": "./linter.tsconfig.json"
|
||||
}
|
||||
}
|
||||
344
packages/filepicker/CHANGELOG.md
Normal file
344
packages/filepicker/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.16.25](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.24...@standardnotes/filepicker@1.16.25) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.24](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.23...@standardnotes/filepicker@1.16.24) (2022-07-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add missing reflect-metadata package to all packages ([ce3a5bb](https://github.com/standardnotes/snjs/commit/ce3a5bbf3f1d2276ac4abc3eec3c6a44c8c3ba9b))
|
||||
|
||||
## [1.16.23](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.22...@standardnotes/filepicker@1.16.23) (2022-06-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.22](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.21...@standardnotes/filepicker@1.16.22) (2022-06-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.21](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.20...@standardnotes/filepicker@1.16.21) (2022-06-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.20](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.19...@standardnotes/filepicker@1.16.20) (2022-06-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.19](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.18...@standardnotes/filepicker@1.16.19) (2022-06-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.18](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.17...@standardnotes/filepicker@1.16.18) (2022-06-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.17](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.16...@standardnotes/filepicker@1.16.17) (2022-06-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.16](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.15...@standardnotes/filepicker@1.16.16) (2022-06-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.15](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.14...@standardnotes/filepicker@1.16.15) (2022-06-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.14](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.13...@standardnotes/filepicker@1.16.14) (2022-06-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.13](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.12...@standardnotes/filepicker@1.16.13) (2022-06-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.12](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.11...@standardnotes/filepicker@1.16.12) (2022-06-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.11](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.10...@standardnotes/filepicker@1.16.11) (2022-06-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.10](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.9...@standardnotes/filepicker@1.16.10) (2022-06-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.9](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.8...@standardnotes/filepicker@1.16.9) (2022-06-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.8](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.7...@standardnotes/filepicker@1.16.8) (2022-06-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.7](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.6...@standardnotes/filepicker@1.16.7) (2022-06-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove isLast dep from ordered byte chunker ([3385581](https://github.com/standardnotes/snjs/commit/33855817d8d96d100b7d4f423f59f00c55834b6f))
|
||||
|
||||
## [1.16.6](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.5...@standardnotes/filepicker@1.16.6) (2022-06-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.5](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.4...@standardnotes/filepicker@1.16.5) (2022-05-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.4](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.3...@standardnotes/filepicker@1.16.4) (2022-05-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.3](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.2...@standardnotes/filepicker@1.16.3) (2022-05-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.2](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.1...@standardnotes/filepicker@1.16.2) (2022-05-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.16.1](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.16.0...@standardnotes/filepicker@1.16.1) (2022-05-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
# [1.16.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.15.0...@standardnotes/filepicker@1.16.0) (2022-05-23)
|
||||
|
||||
### Features
|
||||
|
||||
* encrypted file cache ([#747](https://github.com/standardnotes/snjs/issues/747)) ([5b156a5](https://github.com/standardnotes/snjs/commit/5b156a5b4ee3365dac8e02653df129584a9dd4ef))
|
||||
|
||||
# [1.15.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.14.13...@standardnotes/filepicker@1.15.0) (2022-05-22)
|
||||
|
||||
### Features
|
||||
|
||||
* optional files navigation ([#745](https://github.com/standardnotes/snjs/issues/745)) ([8512166](https://github.com/standardnotes/snjs/commit/851216615478b57b11a570173f94ee598bec31c0))
|
||||
|
||||
## [1.14.13](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.14.12...@standardnotes/filepicker@1.14.13) (2022-05-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.14.12](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.14.11...@standardnotes/filepicker@1.14.12) (2022-05-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.14.11](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.14.10...@standardnotes/filepicker@1.14.11) (2022-05-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.14.10](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.14.9...@standardnotes/filepicker@1.14.10) (2022-05-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.14.9](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.14.8...@standardnotes/filepicker@1.14.9) (2022-05-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.14.8](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.14.7...@standardnotes/filepicker@1.14.8) (2022-05-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.14.7](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.14.6...@standardnotes/filepicker@1.14.7) (2022-05-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.14.6](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.14.5...@standardnotes/filepicker@1.14.6) (2022-05-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.14.5](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.14.4...@standardnotes/filepicker@1.14.5) (2022-05-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.14.4](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.14.3...@standardnotes/filepicker@1.14.4) (2022-05-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.14.3](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.14.2...@standardnotes/filepicker@1.14.3) (2022-05-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.14.2](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.14.1...@standardnotes/filepicker@1.14.2) (2022-05-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.14.1](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.14.0...@standardnotes/filepicker@1.14.1) (2022-05-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
# [1.14.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.13.7...@standardnotes/filepicker@1.14.0) (2022-05-12)
|
||||
|
||||
### Features
|
||||
|
||||
* file desktop backups ([#731](https://github.com/standardnotes/snjs/issues/731)) ([0dbce7d](https://github.com/standardnotes/snjs/commit/0dbce7dc9712fde848445b951079c81479c8bc11))
|
||||
|
||||
## [1.13.7](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.13.6...@standardnotes/filepicker@1.13.7) (2022-05-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.13.6](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.13.4...@standardnotes/filepicker@1.13.6) (2022-05-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.13.5](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.13.4...@standardnotes/filepicker@1.13.5) (2022-05-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.13.4](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.13.3...@standardnotes/filepicker@1.13.4) (2022-04-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.13.3](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.13.2...@standardnotes/filepicker@1.13.3) (2022-04-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* file size calculation to binary ([#709](https://github.com/standardnotes/snjs/issues/709)) ([5773bc7](https://github.com/standardnotes/snjs/commit/5773bc7a2d5f2d79b9d9633fafa660c4b13b42e0))
|
||||
|
||||
## [1.13.2](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.13.1...@standardnotes/filepicker@1.13.2) (2022-04-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.13.1](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.13.0...@standardnotes/filepicker@1.13.1) (2022-04-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
# [1.13.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.12.0...@standardnotes/filepicker@1.13.0) (2022-04-22)
|
||||
|
||||
### Features
|
||||
|
||||
* in memory file cache ([#705](https://github.com/standardnotes/snjs/issues/705)) ([fca294a](https://github.com/standardnotes/snjs/commit/fca294a84256e03272e3b1b29b3dc478cddf9c28))
|
||||
|
||||
# [1.12.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.11.0...@standardnotes/filepicker@1.12.0) (2022-04-21)
|
||||
|
||||
### Features
|
||||
|
||||
* add GB support to formatSizeAsReadableString ([#701](https://github.com/standardnotes/snjs/issues/701)) ([bafd52a](https://github.com/standardnotes/snjs/commit/bafd52a8e4d51229e37ec3f8bb6ea01cf2b7e584))
|
||||
|
||||
# [1.11.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.10.6...@standardnotes/filepicker@1.11.0) (2022-04-15)
|
||||
|
||||
### Features
|
||||
|
||||
* no merge payloads in payload manager ([#693](https://github.com/standardnotes/snjs/issues/693)) ([68a577c](https://github.com/standardnotes/snjs/commit/68a577cb887fd2d5556dc9ddec461f6ae665fcb6))
|
||||
|
||||
## [1.10.6](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.10.5...@standardnotes/filepicker@1.10.6) (2022-04-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* export byte_chunker from filepicker package ([#689](https://github.com/standardnotes/snjs/issues/689)) ([2541250](https://github.com/standardnotes/snjs/commit/2541250d7c01a0763c3162e7e68b28cb4c075322))
|
||||
|
||||
## [1.10.5](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.10.4...@standardnotes/filepicker@1.10.5) (2022-04-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.10.4](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.10.3...@standardnotes/filepicker@1.10.4) (2022-03-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.10.3](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.10.2...@standardnotes/filepicker@1.10.3) (2022-03-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* streaming reader error on abort selection ([#674](https://github.com/standardnotes/snjs/issues/674)) ([8c36554](https://github.com/standardnotes/snjs/commit/8c36554a95117ed6e42c75d0dc29e01cb8de7a54))
|
||||
|
||||
## [1.10.2](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.10.1...@standardnotes/filepicker@1.10.2) (2022-03-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* class file reader chunking ([63a8494](https://github.com/standardnotes/snjs/commit/63a84945f2935c2c2a23b1aa4ea26dcfd24f08d4))
|
||||
|
||||
## [1.10.1](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.10.0...@standardnotes/filepicker@1.10.1) (2022-03-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
# [1.10.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.9.0...@standardnotes/filepicker@1.10.0) (2022-03-11)
|
||||
|
||||
### Features
|
||||
|
||||
* remove ext property from files in favor of mimetype ([#650](https://github.com/standardnotes/snjs/issues/650)) ([d2e7e23](https://github.com/standardnotes/snjs/commit/d2e7e23ec117c505f2f38b9edea539ad3a6d70e2))
|
||||
|
||||
# [1.9.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.8.0...@standardnotes/filepicker@1.9.0) (2022-03-10)
|
||||
|
||||
### Features
|
||||
|
||||
* store file mimeType along with name & ext ([#648](https://github.com/standardnotes/snjs/issues/648)) ([05bf273](https://github.com/standardnotes/snjs/commit/05bf2737282f2d068e354c4d05fbe5390a19e613))
|
||||
|
||||
# [1.8.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.7.0...@standardnotes/filepicker@1.8.0) (2022-03-10)
|
||||
|
||||
### Features
|
||||
|
||||
* **filepicker:** multiple file selection ([#644](https://github.com/standardnotes/snjs/issues/644)) ([1bcdaf4](https://github.com/standardnotes/snjs/commit/1bcdaf4d2e05e1280ba8646683be71eebf95ee2d))
|
||||
|
||||
# [1.7.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.6.0...@standardnotes/filepicker@1.7.0) (2022-03-09)
|
||||
|
||||
### Features
|
||||
|
||||
* export file operation type ([#643](https://github.com/standardnotes/snjs/issues/643)) ([ff5f136](https://github.com/standardnotes/snjs/commit/ff5f136655a8089a47c7eaa04e1e13e58852c93f))
|
||||
|
||||
# [1.6.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.5.0...@standardnotes/filepicker@1.6.0) (2022-03-09)
|
||||
|
||||
### Features
|
||||
|
||||
* allow passing file or filehandle to reader as param ([#641](https://github.com/standardnotes/snjs/issues/641)) ([48b63a2](https://github.com/standardnotes/snjs/commit/48b63a270d647dd864edbcc8316146b4a32a634e))
|
||||
|
||||
# [1.5.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.4.0...@standardnotes/filepicker@1.5.0) (2022-03-09)
|
||||
|
||||
### Features
|
||||
|
||||
* add formatSizeToReadableString function ([#635](https://github.com/standardnotes/snjs/issues/635)) ([8688783](https://github.com/standardnotes/snjs/commit/8688783ac95073631e752cdb76011bca75a29794))
|
||||
|
||||
# [1.4.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.3.0...@standardnotes/filepicker@1.4.0) (2022-03-08)
|
||||
|
||||
### Features
|
||||
|
||||
* determine files host dynamically ([#637](https://github.com/standardnotes/snjs/issues/637)) ([8ae8d32](https://github.com/standardnotes/snjs/commit/8ae8d32a2469cc6b5b42bfc68ec63200d6bc49ed))
|
||||
|
||||
# [1.3.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.2.1...@standardnotes/filepicker@1.3.0) (2022-03-07)
|
||||
|
||||
### Features
|
||||
|
||||
* add renameFile function to itemManager ([#633](https://github.com/standardnotes/snjs/issues/633)) ([828f0d8](https://github.com/standardnotes/snjs/commit/828f0d8c79736b2ede1dd244e1e59569a88e6440))
|
||||
|
||||
## [1.2.1](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.2.0...@standardnotes/filepicker@1.2.1) (2022-03-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* store selected file variable ([52e9c49](https://github.com/standardnotes/snjs/commit/52e9c494868c809f9cc894c182056c92e2f23133))
|
||||
|
||||
# [1.2.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.1.3...@standardnotes/filepicker@1.2.0) (2022-03-03)
|
||||
|
||||
### Features
|
||||
|
||||
* split file picker selection and reading in two ([d5e98a1](https://github.com/standardnotes/snjs/commit/d5e98a15213c9976b629fe401d8ba5f31379f391))
|
||||
|
||||
## [1.1.3](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.1.1...@standardnotes/filepicker@1.1.3) (2022-02-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add pseudo change to get lerna to trigger ([41e6817](https://github.com/standardnotes/snjs/commit/41e6817bbf726b0932cdf16f58622328b9e42803))
|
||||
|
||||
## [1.1.2](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.1.1...@standardnotes/filepicker@1.1.2) (2022-02-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add pseudo change to get lerna to trigger ([41e6817](https://github.com/standardnotes/snjs/commit/41e6817bbf726b0932cdf16f58622328b9e42803))
|
||||
|
||||
## [1.1.1](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.1.0...@standardnotes/filepicker@1.1.1) (2022-02-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* variable reference ([960fce3](https://github.com/standardnotes/snjs/commit/960fce3d56f4a9204c373253cb75874766e6cf85))
|
||||
|
||||
# [1.1.0](https://github.com/standardnotes/snjs/compare/@standardnotes/filepicker@1.0.1...@standardnotes/filepicker@1.1.0) (2022-02-25)
|
||||
|
||||
### Features
|
||||
|
||||
* files improvements ([#612](https://github.com/standardnotes/snjs/issues/612)) ([27a29a9](https://github.com/standardnotes/snjs/commit/27a29a98fdf966ddcbe93df951db1358848f6aab))
|
||||
|
||||
## 1.0.1 (2022-02-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
5
packages/filepicker/example/.eslintrc
Normal file
5
packages/filepicker/example/.eslintrc
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"rules": {
|
||||
"no-console": ["off"]
|
||||
}
|
||||
}
|
||||
21
packages/filepicker/example/index.html
Normal file
21
packages/filepicker/example/index.html
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
background-color: gray;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<label>Classic File Picker</label>
|
||||
<button id="filePicker">Classic Picker</button>
|
||||
|
||||
<label>FileSystem API Picker</label>
|
||||
<button id="fileSystemUploadButton">FileSystem Upload File</button>
|
||||
<button id="downloadButton">Download File</button>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
42
packages/filepicker/example/package.json
Normal file
42
packages/filepicker/example/package.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "files-demo",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"files": [
|
||||
"dist/src"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"clean": "rm -fr dist",
|
||||
"prestart": "yarn clean",
|
||||
"start": "webpack-dev-server --config webpack.config.js",
|
||||
"watch": "webpack -w --config webpack.config.js",
|
||||
"prebuild": "yarn clean",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"lint": "eslint . --ext .ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.8",
|
||||
"@babel/preset-env": "^7.15.8",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@standardnotes/config": "^2.2.0",
|
||||
"@types/wicg-native-file-system": "^2020.6.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.2.3",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typescript": "^4.0.5",
|
||||
"typescript-eslint": "0.0.1-alpha.0",
|
||||
"webpack": "^5.59.1",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"webpack-dev-server": "^4.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/sncrypto-web": "^1.7.0",
|
||||
"@standardnotes/snjs": "^2.61.3",
|
||||
"regenerator-runtime": "^0.13.9"
|
||||
}
|
||||
}
|
||||
56
packages/filepicker/example/src/classic_file_api.ts
Normal file
56
packages/filepicker/example/src/classic_file_api.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { SNApplication, ContentType, FileItem, ClientDisplayableError } from '../../../snjs'
|
||||
import { ClassicFileReader, ClassicFileSaver } from '../../../filepicker'
|
||||
|
||||
export class ClassicFileApi {
|
||||
constructor(private application: SNApplication) {
|
||||
this.configureFilePicker()
|
||||
}
|
||||
|
||||
configureFilePicker(): void {
|
||||
const input = document.getElementById('filePicker') as HTMLInputElement
|
||||
input.onclick = () => {
|
||||
void this.openFilePicker()
|
||||
}
|
||||
console.log('Classic file picker ready.')
|
||||
}
|
||||
|
||||
async openFilePicker(): Promise<void> {
|
||||
const files = await ClassicFileReader.selectFiles()
|
||||
for (const file of files) {
|
||||
const operation = await this.application.files.beginNewFileUpload()
|
||||
if (operation instanceof ClientDisplayableError) {
|
||||
continue
|
||||
}
|
||||
const fileResult = await ClassicFileReader.readFile(file, 2_000_000, async (chunk, index, isLast) => {
|
||||
await this.application.files.pushBytesForUpload(operation, chunk, index, isLast)
|
||||
})
|
||||
const snFile = await this.application.files.finishUpload(operation, fileResult)
|
||||
|
||||
if (snFile instanceof ClientDisplayableError) {
|
||||
return
|
||||
}
|
||||
|
||||
const bytes = await this.downloadFileBytes(snFile.remoteIdentifier)
|
||||
|
||||
new ClassicFileSaver().saveFile(`${snFile.name}.${snFile.ext}`, bytes)
|
||||
}
|
||||
}
|
||||
|
||||
downloadFileBytes = async (remoteIdentifier: string): Promise<Uint8Array> => {
|
||||
console.log('Downloading file', remoteIdentifier)
|
||||
const file = this.application['itemManager']
|
||||
.getItems(ContentType.File)
|
||||
.find((file: FileItem) => file.remoteIdentifier === remoteIdentifier)
|
||||
|
||||
let receivedBytes = new Uint8Array()
|
||||
|
||||
await this.application.files.downloadFile(file, async (decryptedBytes: Uint8Array) => {
|
||||
console.log(`Downloaded ${decryptedBytes.length} bytes`)
|
||||
receivedBytes = new Uint8Array([...receivedBytes, ...decryptedBytes])
|
||||
})
|
||||
|
||||
console.log('Successfully downloaded and decrypted file!')
|
||||
|
||||
return receivedBytes
|
||||
}
|
||||
}
|
||||
68
packages/filepicker/example/src/file_system_api.ts
Normal file
68
packages/filepicker/example/src/file_system_api.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { StreamingFileReader, StreamingFileSaver } from '../../../filepicker'
|
||||
import { SNApplication, FileItem, ClientDisplayableError } from '../../../snjs'
|
||||
|
||||
export class FileSystemApi {
|
||||
private uploadedFiles: FileItem[] = []
|
||||
|
||||
constructor(private application: SNApplication) {
|
||||
this.configureFilePicker()
|
||||
this.configureDownloadButton()
|
||||
}
|
||||
|
||||
get downloadButton(): HTMLButtonElement {
|
||||
return document.getElementById('downloadButton') as HTMLButtonElement
|
||||
}
|
||||
|
||||
configureDownloadButton(): void {
|
||||
this.downloadButton.onclick = this.downloadFiles
|
||||
this.downloadButton.style.display = 'none'
|
||||
}
|
||||
|
||||
configureFilePicker(): void {
|
||||
const button = document.getElementById('fileSystemUploadButton') as HTMLButtonElement
|
||||
button.onclick = this.uploadFiles
|
||||
console.log('File picker ready.')
|
||||
}
|
||||
|
||||
uploadFiles = async (): Promise<void> => {
|
||||
const snFiles = []
|
||||
const selectedFiles = await StreamingFileReader.selectFiles()
|
||||
for (const file of selectedFiles) {
|
||||
const operation = await this.application.files.beginNewFileUpload()
|
||||
if (operation instanceof ClientDisplayableError) {
|
||||
continue
|
||||
}
|
||||
const fileResult = await StreamingFileReader.readFile(file, 2_000_000, async (chunk, index, isLast) => {
|
||||
await this.application.files.pushBytesForUpload(operation, chunk, index, isLast)
|
||||
})
|
||||
|
||||
const snFile = await this.application.files.finishUpload(operation, fileResult)
|
||||
|
||||
snFiles.push(snFile)
|
||||
}
|
||||
|
||||
this.downloadButton.style.display = ''
|
||||
|
||||
this.uploadedFiles = snFiles
|
||||
}
|
||||
|
||||
downloadFiles = async (): Promise<void> => {
|
||||
for (const snFile of this.uploadedFiles) {
|
||||
console.log('Downloading file', snFile.remoteIdentifier)
|
||||
|
||||
const saver = new StreamingFileSaver(snFile.name)
|
||||
await saver.selectFileToSaveTo()
|
||||
saver.loggingEnabled = true
|
||||
|
||||
await this.application.files.downloadFile(snFile, async (decryptedBytes: Uint8Array) => {
|
||||
console.log(`Pushing ${decryptedBytes.length} decrypted bytes to disk`)
|
||||
await saver.pushBytes(decryptedBytes)
|
||||
})
|
||||
|
||||
console.log('Closing file saver reader')
|
||||
await saver.finish()
|
||||
|
||||
console.log('Successfully downloaded and decrypted file!')
|
||||
}
|
||||
}
|
||||
}
|
||||
89
packages/filepicker/example/src/index.ts
Normal file
89
packages/filepicker/example/src/index.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { SNApplication, Environment, Platform, SNLog } from '../../../snjs'
|
||||
import WebDeviceInterface from './web_device_interface'
|
||||
import { SNWebCrypto } from '../../../sncrypto-web'
|
||||
import { ClassicFileApi } from './classic_file_api'
|
||||
import { FileSystemApi } from './file_system_api'
|
||||
|
||||
SNLog.onLog = console.log
|
||||
SNLog.onError = console.error
|
||||
|
||||
console.log('Clearing localStorage...')
|
||||
localStorage.clear()
|
||||
|
||||
/**
|
||||
* Important:
|
||||
* If reusing e2e docker servers, you must edit docker/auth.env ACCESS_TOKEN_AGE
|
||||
* and REFRESH_TOKEN_AGE and increase their ttl.
|
||||
*/
|
||||
|
||||
const host = 'http://localhost:3123'
|
||||
const mocksHost = 'http://localhost:3124'
|
||||
|
||||
const application = new SNApplication({
|
||||
environment: Environment.Web,
|
||||
platform: Platform.MacWeb,
|
||||
deviceInterface: new WebDeviceInterface(),
|
||||
crypto: new SNWebCrypto(),
|
||||
alertService: {
|
||||
confirm: async () => true,
|
||||
alert: async () => {
|
||||
alert()
|
||||
},
|
||||
blockingDialog: () => () => {
|
||||
confirm()
|
||||
},
|
||||
},
|
||||
identifier: `${Math.random()}`,
|
||||
defaultHost: host,
|
||||
appVersion: '1.0.0',
|
||||
})
|
||||
|
||||
console.log('Created application', application)
|
||||
|
||||
export async function publishMockedEvent(eventType: string, eventPayload: unknown): Promise<void> {
|
||||
await fetch(`${mocksHost}/events`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
eventType,
|
||||
eventPayload,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
console.log('Preparing for launch...')
|
||||
await application.prepareForLaunch({
|
||||
receiveChallenge: () => {
|
||||
console.warn('Ignoring challenge')
|
||||
},
|
||||
})
|
||||
await application.launch()
|
||||
console.log('Application launched...')
|
||||
|
||||
const email = String(Math.random())
|
||||
const password = String(Math.random())
|
||||
|
||||
console.log('Registering account...')
|
||||
await application.register(email, password)
|
||||
console.log(`Registered account ${email}/${password}. Be sure to edit docker/auth.env to increase session TTL.`)
|
||||
|
||||
console.log('Creating mock subscription...')
|
||||
await publishMockedEvent('SUBSCRIPTION_PURCHASED', {
|
||||
userEmail: email,
|
||||
subscriptionId: 1,
|
||||
subscriptionName: 'PLUS_PLAN',
|
||||
subscriptionExpiresAt: (new Date().getTime() + 3_600_000) * 1_000,
|
||||
timestamp: Date.now(),
|
||||
offline: false,
|
||||
})
|
||||
console.log('Successfully created mock subscription...')
|
||||
|
||||
new ClassicFileApi(application)
|
||||
new FileSystemApi(application)
|
||||
}
|
||||
|
||||
void run()
|
||||
138
packages/filepicker/example/src/web_device_interface.js
Normal file
138
packages/filepicker/example/src/web_device_interface.js
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
/* eslint-disable no-undef */
|
||||
const KEYCHAIN_STORAGE_KEY = 'keychain'
|
||||
|
||||
export default class WebDeviceInterface {
|
||||
async getRawStorageValue(key) {
|
||||
return localStorage.getItem(key)
|
||||
}
|
||||
|
||||
async getJsonParsedRawStorageValue(key) {
|
||||
const value = await this.getRawStorageValue(key)
|
||||
if (isNullOrUndefined(value)) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (e) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
async getAllRawStorageKeyValues() {
|
||||
const results = []
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
results.push({
|
||||
key: key,
|
||||
value: localStorage[key],
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
async setRawStorageValue(key, value) {
|
||||
localStorage.setItem(key, value)
|
||||
}
|
||||
|
||||
async removeRawStorageValue(key) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
|
||||
async removeAllRawStorageValues() {
|
||||
localStorage.clear()
|
||||
}
|
||||
|
||||
async openDatabase(_identifier) {
|
||||
return {}
|
||||
}
|
||||
|
||||
_getDatabaseKeyPrefix(identifier) {
|
||||
if (identifier) {
|
||||
return `${identifier}-item-`
|
||||
} else {
|
||||
return 'item-'
|
||||
}
|
||||
}
|
||||
|
||||
_keyForPayloadId(id, identifier) {
|
||||
return `${this._getDatabaseKeyPrefix(identifier)}${id}`
|
||||
}
|
||||
|
||||
async getAllRawDatabasePayloads(identifier) {
|
||||
const models = []
|
||||
for (const key in localStorage) {
|
||||
if (key.startsWith(this._getDatabaseKeyPrefix(identifier))) {
|
||||
models.push(JSON.parse(localStorage[key]))
|
||||
}
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
async saveRawDatabasePayload(payload, identifier) {
|
||||
localStorage.setItem(this._keyForPayloadId(payload.uuid, identifier), JSON.stringify(payload))
|
||||
}
|
||||
|
||||
async saveRawDatabasePayloads(payloads, identifier) {
|
||||
for (const payload of payloads) {
|
||||
await this.saveRawDatabasePayload(payload, identifier)
|
||||
}
|
||||
}
|
||||
|
||||
async removeRawDatabasePayloadWithId(id, identifier) {
|
||||
localStorage.removeItem(this._keyForPayloadId(id, identifier))
|
||||
}
|
||||
|
||||
async removeAllRawDatabasePayloads(identifier) {
|
||||
for (const key in localStorage) {
|
||||
if (key.startsWith(this._getDatabaseKeyPrefix(identifier))) {
|
||||
delete localStorage[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @keychain */
|
||||
async getNamespacedKeychainValue(identifier) {
|
||||
const keychain = await this.getRawKeychainValue(identifier)
|
||||
if (!keychain) {
|
||||
return
|
||||
}
|
||||
return keychain[identifier]
|
||||
}
|
||||
|
||||
async setNamespacedKeychainValue(value, identifier) {
|
||||
let keychain = await this.getRawKeychainValue()
|
||||
if (!keychain) {
|
||||
keychain = {}
|
||||
}
|
||||
localStorage.setItem(
|
||||
KEYCHAIN_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
...keychain,
|
||||
[identifier]: value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async clearNamespacedKeychainValue(identifier) {
|
||||
const keychain = await this.getRawKeychainValue()
|
||||
if (!keychain) {
|
||||
return
|
||||
}
|
||||
delete keychain[identifier]
|
||||
localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(keychain))
|
||||
}
|
||||
|
||||
/** Allows unit tests to set legacy keychain structure as it was <= 003 */
|
||||
// eslint-disable-next-line camelcase
|
||||
async setLegacyRawKeychainValue(value) {
|
||||
localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(value))
|
||||
}
|
||||
|
||||
async getRawKeychainValue() {
|
||||
const keychain = localStorage.getItem(KEYCHAIN_STORAGE_KEY)
|
||||
return JSON.parse(keychain)
|
||||
}
|
||||
|
||||
async clearRawKeychainValue() {
|
||||
localStorage.removeItem(KEYCHAIN_STORAGE_KEY)
|
||||
}
|
||||
}
|
||||
11
packages/filepicker/example/tsconfig.json
Normal file
11
packages/filepicker/example/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"target": "es2015",
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": ".",
|
||||
},
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
54
packages/filepicker/example/webpack.config.js
Normal file
54
packages/filepicker/example/webpack.config.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
module.exports = (env) => {
|
||||
return {
|
||||
entry: './src/index.ts',
|
||||
output: {
|
||||
filename: './dist/index.js',
|
||||
},
|
||||
mode: 'development',
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: './index.html',
|
||||
inject: true,
|
||||
templateParameters: {
|
||||
env: process.env,
|
||||
},
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
hot: 'only',
|
||||
static: './public',
|
||||
port: 3030,
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js'],
|
||||
fallback: {
|
||||
crypto: false,
|
||||
path: false,
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|tsx?)$/,
|
||||
exclude: /(node_modules)/,
|
||||
use: [
|
||||
'babel-loader',
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
4100
packages/filepicker/example/yarn.lock
Normal file
4100
packages/filepicker/example/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
14
packages/filepicker/jest.config.js
Normal file
14
packages/filepicker/jest.config.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const base = require('../../node_modules/@standardnotes/config/src/jest.json');
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: 'tsconfig.json',
|
||||
},
|
||||
},
|
||||
coveragePathIgnorePatterns: [
|
||||
"/example/"
|
||||
]
|
||||
};
|
||||
4
packages/filepicker/linter.tsconfig.json
Normal file
4
packages/filepicker/linter.tsconfig.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["dist", "example"]
|
||||
}
|
||||
42
packages/filepicker/package.json
Normal file
42
packages/filepicker/package.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "@standardnotes/filepicker",
|
||||
"version": "1.17.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
"description": "Web filepicker for Standard Notes projects",
|
||||
"main": "dist/index.js",
|
||||
"author": "Standard Notes",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"clean": "rm -fr dist",
|
||||
"prestart": "yarn clean",
|
||||
"start": "tsc -p tsconfig.json --watch",
|
||||
"prebuild": "yarn clean",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"test:unit": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/wicg-file-system-access": "^2020.9.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jest": "^27.5.1",
|
||||
"ts-jest": "^27.1.3",
|
||||
"ts-node": "^10.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.23.1",
|
||||
"@standardnotes/services": "^1.13.23",
|
||||
"@standardnotes/utils": "^1.6.12",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
}
|
||||
}
|
||||
78
packages/filepicker/src/Cache/FileMemoryCache.spec.ts
Normal file
78
packages/filepicker/src/Cache/FileMemoryCache.spec.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { EncryptedBytes } from './../TypedBytes'
|
||||
import { FileMemoryCache } from './FileMemoryCache'
|
||||
|
||||
describe('file memory cache', () => {
|
||||
const createBytes = (size: number): EncryptedBytes => {
|
||||
return { encryptedBytes: new TextEncoder().encode('a'.repeat(size)) }
|
||||
}
|
||||
|
||||
it('should add file', () => {
|
||||
const cache = new FileMemoryCache(5)
|
||||
const file = createBytes(1)
|
||||
cache.add('123', file)
|
||||
|
||||
expect(cache.get('123')).toEqual(file)
|
||||
})
|
||||
|
||||
it('should fail to add file if exceeds maximum', () => {
|
||||
const maxSize = 5
|
||||
const cache = new FileMemoryCache(maxSize)
|
||||
const file = createBytes(maxSize + 1)
|
||||
|
||||
expect(cache.add('123', file)).toEqual(false)
|
||||
})
|
||||
|
||||
it('should allow filling files up to limit', () => {
|
||||
const cache = new FileMemoryCache(5)
|
||||
|
||||
cache.add('1', createBytes(3))
|
||||
cache.add('2', createBytes(2))
|
||||
|
||||
expect(cache.get('1')).toBeTruthy()
|
||||
expect(cache.get('2')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should clear early files when adding new files above limit', () => {
|
||||
const cache = new FileMemoryCache(5)
|
||||
|
||||
cache.add('1', createBytes(3))
|
||||
cache.add('2', createBytes(2))
|
||||
cache.add('3', createBytes(5))
|
||||
|
||||
expect(cache.get('1')).toBeFalsy()
|
||||
expect(cache.get('2')).toBeFalsy()
|
||||
expect(cache.get('3')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should remove single file', () => {
|
||||
const cache = new FileMemoryCache(5)
|
||||
|
||||
cache.add('1', createBytes(3))
|
||||
cache.add('2', createBytes(2))
|
||||
|
||||
cache.remove('1')
|
||||
|
||||
expect(cache.get('1')).toBeFalsy()
|
||||
expect(cache.get('2')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should clear all files', () => {
|
||||
const cache = new FileMemoryCache(5)
|
||||
|
||||
cache.add('1', createBytes(3))
|
||||
cache.add('2', createBytes(2))
|
||||
cache.clear()
|
||||
|
||||
expect(cache.get('1')).toBeFalsy()
|
||||
expect(cache.get('2')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should return correct size', () => {
|
||||
const cache = new FileMemoryCache(20)
|
||||
|
||||
cache.add('1', createBytes(3))
|
||||
cache.add('2', createBytes(10))
|
||||
|
||||
expect(cache.size).toEqual(13)
|
||||
})
|
||||
})
|
||||
48
packages/filepicker/src/Cache/FileMemoryCache.ts
Normal file
48
packages/filepicker/src/Cache/FileMemoryCache.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { removeFromArray } from '@standardnotes/utils'
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { EncryptedBytes } from '../TypedBytes'
|
||||
|
||||
export class FileMemoryCache {
|
||||
private cache: Record<Uuid, EncryptedBytes> = {}
|
||||
private orderedQueue: Uuid[] = []
|
||||
|
||||
constructor(public readonly maxSize: number) {}
|
||||
|
||||
add(uuid: Uuid, data: EncryptedBytes): boolean {
|
||||
if (data.encryptedBytes.length > this.maxSize) {
|
||||
return false
|
||||
}
|
||||
|
||||
while (this.size + data.encryptedBytes.length > this.maxSize) {
|
||||
this.remove(this.orderedQueue[0])
|
||||
}
|
||||
|
||||
this.cache[uuid] = data
|
||||
|
||||
this.orderedQueue.push(uuid)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return Object.values(this.cache)
|
||||
.map((bytes) => bytes.encryptedBytes.length)
|
||||
.reduce((total, fileLength) => total + fileLength, 0)
|
||||
}
|
||||
|
||||
get(uuid: Uuid): EncryptedBytes | undefined {
|
||||
return this.cache[uuid]
|
||||
}
|
||||
|
||||
remove(uuid: Uuid): void {
|
||||
delete this.cache[uuid]
|
||||
|
||||
removeFromArray(this.orderedQueue, uuid)
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.cache = {}
|
||||
|
||||
this.orderedQueue = []
|
||||
}
|
||||
}
|
||||
70
packages/filepicker/src/Chunker/ByteChunker.spec.ts
Normal file
70
packages/filepicker/src/Chunker/ByteChunker.spec.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { ByteChunker } from './ByteChunker'
|
||||
|
||||
const chunkOfSize = (size: number) => {
|
||||
return new TextEncoder().encode('a'.repeat(size))
|
||||
}
|
||||
|
||||
describe('byte chunker', () => {
|
||||
it('should hold back small chunks until minimum size is met', async () => {
|
||||
let receivedBytes = new Uint8Array()
|
||||
let numChunks = 0
|
||||
const chunker = new ByteChunker(100, async (bytes) => {
|
||||
numChunks++
|
||||
receivedBytes = new Uint8Array([...receivedBytes, ...bytes])
|
||||
})
|
||||
|
||||
await chunker.addBytes(chunkOfSize(50), false)
|
||||
await chunker.addBytes(chunkOfSize(50), false)
|
||||
await chunker.addBytes(chunkOfSize(50), false)
|
||||
await chunker.addBytes(chunkOfSize(50), true)
|
||||
|
||||
expect(numChunks).toEqual(2)
|
||||
expect(receivedBytes.length).toEqual(200)
|
||||
})
|
||||
|
||||
it('should send back big chunks immediately', async () => {
|
||||
let receivedBytes = new Uint8Array()
|
||||
let numChunks = 0
|
||||
const chunker = new ByteChunker(100, async (bytes) => {
|
||||
numChunks++
|
||||
receivedBytes = new Uint8Array([...receivedBytes, ...bytes])
|
||||
})
|
||||
|
||||
await chunker.addBytes(chunkOfSize(150), false)
|
||||
await chunker.addBytes(chunkOfSize(150), false)
|
||||
await chunker.addBytes(chunkOfSize(150), false)
|
||||
await chunker.addBytes(chunkOfSize(50), true)
|
||||
|
||||
expect(numChunks).toEqual(4)
|
||||
expect(receivedBytes.length).toEqual(500)
|
||||
})
|
||||
|
||||
it('last chunk should be popped regardless of size', async () => {
|
||||
let receivedBytes = new Uint8Array()
|
||||
let numChunks = 0
|
||||
const chunker = new ByteChunker(100, async (bytes) => {
|
||||
numChunks++
|
||||
receivedBytes = new Uint8Array([...receivedBytes, ...bytes])
|
||||
})
|
||||
|
||||
await chunker.addBytes(chunkOfSize(50), false)
|
||||
await chunker.addBytes(chunkOfSize(25), true)
|
||||
|
||||
expect(numChunks).toEqual(1)
|
||||
expect(receivedBytes.length).toEqual(75)
|
||||
})
|
||||
|
||||
it('single chunk should be popped immediately', async () => {
|
||||
let receivedBytes = new Uint8Array()
|
||||
let numChunks = 0
|
||||
const chunker = new ByteChunker(100, async (bytes) => {
|
||||
numChunks++
|
||||
receivedBytes = new Uint8Array([...receivedBytes, ...bytes])
|
||||
})
|
||||
|
||||
await chunker.addBytes(chunkOfSize(50), true)
|
||||
|
||||
expect(numChunks).toEqual(1)
|
||||
expect(receivedBytes.length).toEqual(50)
|
||||
})
|
||||
})
|
||||
35
packages/filepicker/src/Chunker/ByteChunker.ts
Normal file
35
packages/filepicker/src/Chunker/ByteChunker.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { OnChunkCallback } from '../types'
|
||||
|
||||
export class ByteChunker {
|
||||
public loggingEnabled = false
|
||||
private bytes = new Uint8Array()
|
||||
private index = 1
|
||||
|
||||
constructor(private minimumChunkSize: number, private onChunk: OnChunkCallback) {}
|
||||
|
||||
private log(...args: any[]): void {
|
||||
if (!this.loggingEnabled) {
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(args)
|
||||
}
|
||||
|
||||
public async addBytes(bytes: Uint8Array, isLast: boolean): Promise<void> {
|
||||
this.bytes = new Uint8Array([...this.bytes, ...bytes])
|
||||
|
||||
this.log(`Chunker adding ${bytes.length}, total size ${this.bytes.length}`)
|
||||
|
||||
if (this.bytes.length >= this.minimumChunkSize || isLast) {
|
||||
await this.popBytes(isLast)
|
||||
}
|
||||
}
|
||||
|
||||
private async popBytes(isLast: boolean): Promise<void> {
|
||||
const maxIndex = Math.max(this.minimumChunkSize, this.bytes.length)
|
||||
const chunk = this.bytes.slice(0, maxIndex)
|
||||
this.bytes = new Uint8Array([...this.bytes.slice(maxIndex)])
|
||||
this.log(`Chunker popping ${chunk.length}, total size in queue ${this.bytes.length}`)
|
||||
await this.onChunk(chunk, this.index++, isLast)
|
||||
}
|
||||
}
|
||||
23
packages/filepicker/src/Chunker/OrderedByteChunker.spec.ts
Normal file
23
packages/filepicker/src/Chunker/OrderedByteChunker.spec.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { OrderedByteChunker } from './OrderedByteChunker'
|
||||
|
||||
const chunkOfSize = (size: number) => {
|
||||
return new TextEncoder().encode('a'.repeat(size))
|
||||
}
|
||||
|
||||
describe('ordered byte chunker', () => {
|
||||
it('should callback multiple times if added bytes matches multiple chunk sizes', async () => {
|
||||
const chunkSizes = [10, 10, 10]
|
||||
let receivedBytes = new Uint8Array()
|
||||
let numCallbacks = 0
|
||||
|
||||
const chunker = new OrderedByteChunker(chunkSizes, async (bytes) => {
|
||||
numCallbacks++
|
||||
receivedBytes = new Uint8Array([...receivedBytes, ...bytes])
|
||||
})
|
||||
|
||||
await chunker.addBytes(chunkOfSize(30))
|
||||
|
||||
expect(numCallbacks).toEqual(3)
|
||||
expect(receivedBytes.length).toEqual(30)
|
||||
})
|
||||
})
|
||||
40
packages/filepicker/src/Chunker/OrderedByteChunker.ts
Normal file
40
packages/filepicker/src/Chunker/OrderedByteChunker.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
export class OrderedByteChunker {
|
||||
private bytes = new Uint8Array()
|
||||
private index = 1
|
||||
private remainingChunks: number[] = []
|
||||
|
||||
constructor(
|
||||
private chunkSizes: number[],
|
||||
private onChunk: (chunk: Uint8Array, index: number, isLast: boolean) => Promise<void>,
|
||||
) {
|
||||
this.remainingChunks = chunkSizes.slice()
|
||||
}
|
||||
|
||||
private needsPop(): boolean {
|
||||
return this.remainingChunks.length > 0 && this.bytes.length >= this.remainingChunks[0]
|
||||
}
|
||||
|
||||
public async addBytes(bytes: Uint8Array): Promise<void> {
|
||||
this.bytes = new Uint8Array([...this.bytes, ...bytes])
|
||||
|
||||
if (this.needsPop()) {
|
||||
await this.popBytes()
|
||||
}
|
||||
}
|
||||
|
||||
private async popBytes(): Promise<void> {
|
||||
const readUntil = this.remainingChunks[0]
|
||||
|
||||
const chunk = this.bytes.slice(0, readUntil)
|
||||
|
||||
this.bytes = new Uint8Array([...this.bytes.slice(readUntil)])
|
||||
|
||||
this.remainingChunks.shift()
|
||||
|
||||
await this.onChunk(chunk, this.index++, this.index === this.chunkSizes.length - 1)
|
||||
|
||||
if (this.needsPop()) {
|
||||
await this.popBytes()
|
||||
}
|
||||
}
|
||||
}
|
||||
59
packages/filepicker/src/Classic/ClassicReader.ts
Normal file
59
packages/filepicker/src/Classic/ClassicReader.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { ByteChunker } from './../Chunker/ByteChunker'
|
||||
import { OnChunkCallback, FileSelectionResponse } from '../types'
|
||||
import { readFile as utilsReadFile } from '../utils'
|
||||
import { FileReaderInterface } from '../Interface/FileReader'
|
||||
|
||||
export const ClassicFileReader: FileReaderInterface = {
|
||||
selectFiles,
|
||||
readFile,
|
||||
available,
|
||||
maximumFileSize,
|
||||
}
|
||||
|
||||
function available(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
function maximumFileSize(): number {
|
||||
return 50 * 1_000_000
|
||||
}
|
||||
|
||||
function selectFiles(): Promise<File[]> {
|
||||
const input = document.createElement('input') as HTMLInputElement
|
||||
input.type = 'file'
|
||||
input.multiple = true
|
||||
|
||||
return new Promise((resolve) => {
|
||||
input.onchange = async (event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const files = []
|
||||
for (const file of target.files as FileList) {
|
||||
files.push(file)
|
||||
}
|
||||
resolve(files)
|
||||
}
|
||||
input.click()
|
||||
})
|
||||
}
|
||||
|
||||
async function readFile(
|
||||
file: File,
|
||||
minimumChunkSize: number,
|
||||
onChunk: OnChunkCallback,
|
||||
): Promise<FileSelectionResponse> {
|
||||
const buffer = await utilsReadFile(file)
|
||||
const chunker = new ByteChunker(minimumChunkSize, onChunk)
|
||||
const readSize = 2_000_000
|
||||
|
||||
for (let i = 0; i < buffer.length; i += readSize) {
|
||||
const chunkMax = i + readSize
|
||||
const chunk = buffer.slice(i, chunkMax)
|
||||
const isFinalChunk = chunkMax >= buffer.length
|
||||
await chunker.addBytes(chunk, isFinalChunk)
|
||||
}
|
||||
|
||||
return {
|
||||
name: file.name,
|
||||
mimeType: file.type,
|
||||
}
|
||||
}
|
||||
23
packages/filepicker/src/Classic/ClassicSaver.ts
Normal file
23
packages/filepicker/src/Classic/ClassicSaver.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { saveFile } from '../utils'
|
||||
|
||||
export class ClassicFileSaver {
|
||||
public loggingEnabled = false
|
||||
|
||||
private log(...args: any[]): void {
|
||||
if (!this.loggingEnabled) {
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(args)
|
||||
}
|
||||
|
||||
static maximumFileSize(): number {
|
||||
return 50 * 1_000_000
|
||||
}
|
||||
|
||||
saveFile(name: string, bytes: Uint8Array): void {
|
||||
this.log('Saving file to disk...')
|
||||
saveFile(name, bytes)
|
||||
this.log('Closing write stream')
|
||||
}
|
||||
}
|
||||
11
packages/filepicker/src/Interface/FileReader.ts
Normal file
11
packages/filepicker/src/Interface/FileReader.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { OnChunkCallback, FileSelectionResponse } from '../types'
|
||||
|
||||
export interface FileReaderInterface {
|
||||
selectFiles(): Promise<File[]>
|
||||
|
||||
readFile(file: File, minimumChunkSize: number, onChunk: OnChunkCallback): Promise<FileSelectionResponse>
|
||||
|
||||
available(): boolean
|
||||
|
||||
maximumFileSize(): number | undefined
|
||||
}
|
||||
112
packages/filepicker/src/Streaming/StreamingApi.ts
Normal file
112
packages/filepicker/src/Streaming/StreamingApi.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import {
|
||||
FileSystemApi,
|
||||
DirectoryHandle,
|
||||
FileHandleReadWrite,
|
||||
FileHandleRead,
|
||||
FileSystemNoSelection,
|
||||
FileSystemResult,
|
||||
} from '@standardnotes/services'
|
||||
|
||||
interface WebDirectoryHandle extends DirectoryHandle {
|
||||
nativeHandle: FileSystemDirectoryHandle
|
||||
}
|
||||
interface WebFileHandleReadWrite extends FileHandleReadWrite {
|
||||
nativeHandle: FileSystemFileHandle
|
||||
writableStream: FileSystemWritableFileStream
|
||||
}
|
||||
|
||||
interface WebFileHandleRead extends FileHandleRead {
|
||||
nativeHandle: FileSystemFileHandle
|
||||
}
|
||||
|
||||
export class StreamingFileApi implements FileSystemApi {
|
||||
async selectDirectory(): Promise<DirectoryHandle | FileSystemNoSelection> {
|
||||
try {
|
||||
const nativeHandle = await window.showDirectoryPicker()
|
||||
|
||||
return { nativeHandle }
|
||||
} catch (error) {
|
||||
return 'aborted'
|
||||
}
|
||||
}
|
||||
|
||||
async createFile(directory: WebDirectoryHandle, name: string): Promise<WebFileHandleReadWrite> {
|
||||
const nativeHandle = await directory.nativeHandle.getFileHandle(name, { create: true })
|
||||
const writableStream = await nativeHandle.createWritable()
|
||||
|
||||
return {
|
||||
nativeHandle,
|
||||
writableStream,
|
||||
}
|
||||
}
|
||||
|
||||
async createDirectory(
|
||||
parentDirectory: WebDirectoryHandle,
|
||||
name: string,
|
||||
): Promise<WebDirectoryHandle | FileSystemNoSelection> {
|
||||
const nativeHandle = await parentDirectory.nativeHandle.getDirectoryHandle(name, { create: true })
|
||||
return { nativeHandle }
|
||||
}
|
||||
|
||||
async saveBytes(file: WebFileHandleReadWrite, bytes: Uint8Array): Promise<'success' | 'failed'> {
|
||||
await file.writableStream.write(bytes)
|
||||
|
||||
return 'success'
|
||||
}
|
||||
|
||||
async saveString(file: WebFileHandleReadWrite, contents: string): Promise<'success' | 'failed'> {
|
||||
await file.writableStream.write(contents)
|
||||
|
||||
return 'success'
|
||||
}
|
||||
|
||||
async closeFileWriteStream(file: WebFileHandleReadWrite): Promise<'success' | 'failed'> {
|
||||
await file.writableStream.close()
|
||||
|
||||
return 'success'
|
||||
}
|
||||
|
||||
async selectFile(): Promise<WebFileHandleRead | FileSystemNoSelection> {
|
||||
try {
|
||||
const selection = await window.showOpenFilePicker()
|
||||
|
||||
const file = selection[0]
|
||||
|
||||
return {
|
||||
nativeHandle: file,
|
||||
}
|
||||
} catch (_) {
|
||||
return 'aborted'
|
||||
}
|
||||
}
|
||||
|
||||
async readFile(
|
||||
fileHandle: WebFileHandleRead,
|
||||
onBytes: (bytes: Uint8Array, isLast: boolean) => Promise<void>,
|
||||
): Promise<FileSystemResult> {
|
||||
const file = await fileHandle.nativeHandle.getFile()
|
||||
const stream = file.stream() as unknown as ReadableStream
|
||||
const reader = stream.getReader()
|
||||
|
||||
let previousChunk: Uint8Array
|
||||
|
||||
const processChunk = async (result: ReadableStreamDefaultReadResult<Uint8Array>): Promise<void> => {
|
||||
if (result.done) {
|
||||
await onBytes(previousChunk, true)
|
||||
return
|
||||
}
|
||||
|
||||
if (previousChunk) {
|
||||
await onBytes(previousChunk, false)
|
||||
}
|
||||
|
||||
previousChunk = result.value
|
||||
|
||||
return reader.read().then(processChunk)
|
||||
}
|
||||
|
||||
await reader.read().then(processChunk)
|
||||
|
||||
return 'success'
|
||||
}
|
||||
}
|
||||
75
packages/filepicker/src/Streaming/StreamingReader.ts
Normal file
75
packages/filepicker/src/Streaming/StreamingReader.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { FileReaderInterface } from './../Interface/FileReader'
|
||||
import { ByteChunker } from '../Chunker/ByteChunker'
|
||||
import { OnChunkCallback, FileSelectionResponse } from '../types'
|
||||
|
||||
interface StreamingFileReaderInterface {
|
||||
getFilesFromHandles(handles: FileSystemFileHandle[]): Promise<File[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* The File System Access API File Picker
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API
|
||||
*/
|
||||
export const StreamingFileReader: StreamingFileReaderInterface & FileReaderInterface = {
|
||||
getFilesFromHandles,
|
||||
selectFiles,
|
||||
readFile,
|
||||
available,
|
||||
maximumFileSize,
|
||||
}
|
||||
|
||||
function maximumFileSize(): number | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getFilesFromHandles(handles: FileSystemFileHandle[]): Promise<File[]> {
|
||||
return Promise.all(handles.map((handle) => handle.getFile()))
|
||||
}
|
||||
|
||||
async function selectFiles(): Promise<File[]> {
|
||||
let selectedFilesHandles: FileSystemFileHandle[]
|
||||
try {
|
||||
selectedFilesHandles = await window.showOpenFilePicker({ multiple: true })
|
||||
} catch (error) {
|
||||
selectedFilesHandles = []
|
||||
}
|
||||
return getFilesFromHandles(selectedFilesHandles)
|
||||
}
|
||||
|
||||
async function readFile(
|
||||
file: File,
|
||||
minimumChunkSize: number,
|
||||
onChunk: OnChunkCallback,
|
||||
): Promise<FileSelectionResponse> {
|
||||
const byteChunker = new ByteChunker(minimumChunkSize, onChunk)
|
||||
const stream = file.stream() as unknown as ReadableStream
|
||||
const reader = stream.getReader()
|
||||
|
||||
let previousChunk: Uint8Array
|
||||
|
||||
const processChunk = async (result: ReadableStreamDefaultReadResult<Uint8Array>): Promise<void> => {
|
||||
if (result.done) {
|
||||
await byteChunker.addBytes(previousChunk, true)
|
||||
return
|
||||
}
|
||||
|
||||
if (previousChunk) {
|
||||
await byteChunker.addBytes(previousChunk, false)
|
||||
}
|
||||
|
||||
previousChunk = result.value
|
||||
|
||||
return reader.read().then(processChunk)
|
||||
}
|
||||
|
||||
await reader.read().then(processChunk)
|
||||
|
||||
return {
|
||||
name: file.name,
|
||||
mimeType: file.type,
|
||||
}
|
||||
}
|
||||
|
||||
function available(): boolean {
|
||||
return window.showOpenFilePicker != undefined
|
||||
}
|
||||
49
packages/filepicker/src/Streaming/StreamingSaver.ts
Normal file
49
packages/filepicker/src/Streaming/StreamingSaver.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* The File System Access API File Picker
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API
|
||||
*/
|
||||
export class StreamingFileSaver {
|
||||
public loggingEnabled = false
|
||||
private writableStream!: FileSystemWritableFileStream
|
||||
|
||||
constructor(private name: string) {}
|
||||
|
||||
private log(...args: any[]): void {
|
||||
if (!this.loggingEnabled) {
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(args)
|
||||
}
|
||||
|
||||
static available(): boolean {
|
||||
return window.showSaveFilePicker != undefined
|
||||
}
|
||||
|
||||
/** This function must be called in response to a user interaction, otherwise, it will be rejected by the browser. */
|
||||
async selectFileToSaveTo(): Promise<void> {
|
||||
this.log('Showing save file picker')
|
||||
|
||||
const downloadHandle = await window.showSaveFilePicker({
|
||||
suggestedName: this.name,
|
||||
})
|
||||
|
||||
this.writableStream = await downloadHandle.createWritable()
|
||||
}
|
||||
|
||||
async pushBytes(bytes: Uint8Array): Promise<void> {
|
||||
if (!this.writableStream) {
|
||||
throw Error('Must call selectFileToSaveTo first')
|
||||
}
|
||||
this.log('Writing chunk to disk of size', bytes.length)
|
||||
await this.writableStream.write(bytes)
|
||||
}
|
||||
|
||||
async finish(): Promise<void> {
|
||||
if (!this.writableStream) {
|
||||
throw Error('Must call selectFileToSaveTo first')
|
||||
}
|
||||
this.log('Closing write stream')
|
||||
await this.writableStream.close()
|
||||
}
|
||||
}
|
||||
7
packages/filepicker/src/TypedBytes.ts
Normal file
7
packages/filepicker/src/TypedBytes.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export type EncryptedBytes = {
|
||||
encryptedBytes: Uint8Array
|
||||
}
|
||||
|
||||
export type DecryptedBytes = {
|
||||
decryptedBytes: Uint8Array
|
||||
}
|
||||
11
packages/filepicker/src/index.ts
Normal file
11
packages/filepicker/src/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export * from './types'
|
||||
export * from './Classic/ClassicReader'
|
||||
export * from './Classic/ClassicSaver'
|
||||
export * from './Streaming/StreamingReader'
|
||||
export * from './Streaming/StreamingSaver'
|
||||
export * from './Streaming/StreamingApi'
|
||||
export * from './utils'
|
||||
export * from './Chunker/ByteChunker'
|
||||
export * from './Chunker/OrderedByteChunker'
|
||||
export * from './Cache/FileMemoryCache'
|
||||
export * from './TypedBytes'
|
||||
6
packages/filepicker/src/types.ts
Normal file
6
packages/filepicker/src/types.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export type OnChunkCallback = (chunk: Uint8Array, index: number, isLast: boolean) => Promise<void>
|
||||
|
||||
export type FileSelectionResponse = {
|
||||
name: string
|
||||
mimeType: string
|
||||
}
|
||||
77
packages/filepicker/src/utils.spec.ts
Normal file
77
packages/filepicker/src/utils.spec.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { formatSizeToReadableString, parseFileName } from './utils'
|
||||
|
||||
describe('utils', () => {
|
||||
describe('parseFileName', () => {
|
||||
it('should parse regular filenames', () => {
|
||||
const fileName = 'test.txt'
|
||||
|
||||
const { name, ext } = parseFileName(fileName)
|
||||
|
||||
expect(name).toBe('test')
|
||||
expect(ext).toBe('txt')
|
||||
})
|
||||
|
||||
it('should parse filenames with multiple dots', () => {
|
||||
const fileName = 'Screen Shot 2022-03-06 at 12.13.32 PM.png'
|
||||
|
||||
const { name, ext } = parseFileName(fileName)
|
||||
|
||||
expect(name).toBe('Screen Shot 2022-03-06 at 12.13.32 PM')
|
||||
expect(ext).toBe('png')
|
||||
})
|
||||
|
||||
it('should parse filenames without extensions', () => {
|
||||
const fileName = 'extensionless'
|
||||
|
||||
const { name, ext } = parseFileName(fileName)
|
||||
|
||||
expect(name).toBe('extensionless')
|
||||
expect(ext).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatSizeToReadableString', () => {
|
||||
it('should show as bytes if less than 1KB', () => {
|
||||
const size = 1_023
|
||||
|
||||
const formattedSize = formatSizeToReadableString(size)
|
||||
|
||||
expect(formattedSize).toBe('1023 B')
|
||||
})
|
||||
|
||||
it('should format as KB', () => {
|
||||
const size = 1_024
|
||||
|
||||
const formattedSize = formatSizeToReadableString(size)
|
||||
|
||||
expect(formattedSize).toBe('1 KB')
|
||||
})
|
||||
|
||||
it('should format as MB', () => {
|
||||
const size = 1_048_576
|
||||
|
||||
const formattedSize = formatSizeToReadableString(size)
|
||||
|
||||
expect(formattedSize).toBe('1 MB')
|
||||
})
|
||||
|
||||
it('should format as GB', () => {
|
||||
const size = 1_073_741_824
|
||||
|
||||
const formattedSize = formatSizeToReadableString(size)
|
||||
|
||||
expect(formattedSize).toBe('1 GB')
|
||||
})
|
||||
|
||||
it('should only show fixed-point notation if calculated size is not an integer', () => {
|
||||
const size1 = 1_048_576
|
||||
const size2 = 1_572_864
|
||||
|
||||
const formattedSize1 = formatSizeToReadableString(size1)
|
||||
const formattedSize2 = formatSizeToReadableString(size2)
|
||||
|
||||
expect(formattedSize1).toBe('1 MB')
|
||||
expect(formattedSize2).toBe('1.50 MB')
|
||||
})
|
||||
})
|
||||
})
|
||||
56
packages/filepicker/src/utils.ts
Normal file
56
packages/filepicker/src/utils.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
export async function readFile(file: File): Promise<Uint8Array> {
|
||||
const reader = new FileReader()
|
||||
reader.readAsArrayBuffer(file)
|
||||
return new Promise((resolve) => {
|
||||
reader.onload = (readerEvent) => {
|
||||
const target = readerEvent.target as FileReader
|
||||
const content = target.result as ArrayBuffer
|
||||
resolve(new Uint8Array(content))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function parseFileName(fileName: string): {
|
||||
name: string
|
||||
ext: string
|
||||
} {
|
||||
const pattern = /(?:\.([^.]+))?$/
|
||||
const extMatches = pattern.exec(fileName)
|
||||
const ext = extMatches?.[1] || ''
|
||||
const name = fileName.includes('.') ? fileName.substring(0, fileName.lastIndexOf('.')) : fileName
|
||||
|
||||
return { name, ext }
|
||||
}
|
||||
|
||||
export function saveFile(name: string, bytes: Uint8Array): void {
|
||||
const link = document.createElement('a')
|
||||
const blob = new Blob([bytes], {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
})
|
||||
link.href = window.URL.createObjectURL(blob)
|
||||
link.setAttribute('download', name)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
window.URL.revokeObjectURL(link.href)
|
||||
}
|
||||
|
||||
const BYTES_IN_ONE_KILOBYTE = 1_024
|
||||
const BYTES_IN_ONE_MEGABYTE = 1_048_576
|
||||
const BYTES_IN_ONE_GIGABYTE = 1_073_741_824
|
||||
|
||||
export function formatSizeToReadableString(bytes: number): string {
|
||||
let size = bytes
|
||||
let unit = 'B'
|
||||
if (bytes >= BYTES_IN_ONE_GIGABYTE) {
|
||||
size = bytes / BYTES_IN_ONE_GIGABYTE
|
||||
unit = 'GB'
|
||||
} else if (bytes >= BYTES_IN_ONE_MEGABYTE) {
|
||||
size = bytes / BYTES_IN_ONE_MEGABYTE
|
||||
unit = 'MB'
|
||||
} else if (bytes >= BYTES_IN_ONE_KILOBYTE) {
|
||||
size = bytes / BYTES_IN_ONE_KILOBYTE
|
||||
unit = 'KB'
|
||||
}
|
||||
return `${Number.isInteger(size) ? size : size.toFixed(2)} ${unit}`
|
||||
}
|
||||
13
packages/filepicker/tsconfig.json
Normal file
13
packages/filepicker/tsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "../../node_modules/@standardnotes/config/src/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"references": [],
|
||||
"exclude": ["**/*.spec.ts", "dist", "example"]
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ module.exports = (async () => {
|
|||
} = await getDefaultConfig()
|
||||
|
||||
return {
|
||||
watchFolders: [__dirname, '../icons', '../styles', '../components', '../features', '../encryption'],
|
||||
watchFolders: [__dirname, '../icons', '../styles', '../components', '../features', '../encryption', '../filepicker'],
|
||||
transformer: {
|
||||
getTransformOptions: async () => ({
|
||||
transform: {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
"@standardnotes/components-meta": "workspace:*",
|
||||
"@standardnotes/encryption": "workspace:*",
|
||||
"@standardnotes/features": "workspace:*",
|
||||
"@standardnotes/filepicker": "^1.16.23",
|
||||
"@standardnotes/filepicker": "workspace:*",
|
||||
"@standardnotes/icons": "workspace:*",
|
||||
"@standardnotes/react-native-aes": "^1.4.3",
|
||||
"@standardnotes/react-native-textview": "1.1.0",
|
||||
|
|
|
|||
1
packages/web/.gitignore
vendored
1
packages/web/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
dist
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
"@reach/tooltip": "^0.16.2",
|
||||
"@reach/visually-hidden": "^0.16.0",
|
||||
"@standardnotes/encryption": "workspace:*",
|
||||
"@standardnotes/filepicker": "1.16.23",
|
||||
"@standardnotes/filepicker": "workspace:*",
|
||||
"@standardnotes/icons": "workspace:*",
|
||||
"@standardnotes/services": "^1.13.23",
|
||||
"@standardnotes/sncrypto-web": "1.10.1",
|
||||
|
|
|
|||
72
yarn.lock
72
yarn.lock
|
|
@ -6599,27 +6599,23 @@ __metadata:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@standardnotes/filepicker@npm:1.16.23, @standardnotes/filepicker@npm:^1.16.23":
|
||||
version: 1.16.23
|
||||
resolution: "@standardnotes/filepicker@npm:1.16.23"
|
||||
"@standardnotes/filepicker@^1.16.22, @standardnotes/filepicker@^1.16.23, @standardnotes/filepicker@workspace:*, @standardnotes/filepicker@workspace:packages/filepicker":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@standardnotes/filepicker@workspace:packages/filepicker"
|
||||
dependencies:
|
||||
"@standardnotes/common": ^1.23.1
|
||||
"@standardnotes/services": ^1.13.23
|
||||
"@standardnotes/utils": ^1.6.12
|
||||
checksum: 8b1eca3f4ee5d821bf0e7aaa859a0d6204b1083adf168db2d9deac9984d2d4b13d04cb018c8b545d26c2a8f2fc262d140e3e43bd4c91f0c9aebabee42bcd92f7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@standardnotes/filepicker@npm:^1.16.22":
|
||||
version: 1.16.22
|
||||
resolution: "@standardnotes/filepicker@npm:1.16.22"
|
||||
dependencies:
|
||||
"@standardnotes/common": ^1.23.1
|
||||
"@standardnotes/services": ^1.13.22
|
||||
"@standardnotes/utils": ^1.6.12
|
||||
checksum: df14eedefd9d9a5ebdff483b611eac2c78a6a17e2039b5893a9041b0abbf88acf85aa36444cbf5b3b0c52a1556a175d20ed15a2879117895d3669b597ee07d60
|
||||
languageName: node
|
||||
linkType: hard
|
||||
"@types/jest": ^27.4.1
|
||||
"@types/wicg-file-system-access": ^2020.9.5
|
||||
"@typescript-eslint/eslint-plugin": ^5.30.0
|
||||
eslint-plugin-prettier: ^4.2.1
|
||||
jest: ^27.5.1
|
||||
reflect-metadata: ^0.1.13
|
||||
ts-jest: ^27.1.3
|
||||
ts-node: ^10.5.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@standardnotes/files@npm:^1.3.22":
|
||||
version: 1.3.22
|
||||
|
|
@ -6936,7 +6932,7 @@ __metadata:
|
|||
"@standardnotes/config": ^2.4.3
|
||||
"@standardnotes/encryption": "workspace:*"
|
||||
"@standardnotes/features": "workspace:*"
|
||||
"@standardnotes/filepicker": ^1.16.23
|
||||
"@standardnotes/filepicker": "workspace:*"
|
||||
"@standardnotes/icons": "workspace:*"
|
||||
"@standardnotes/react-native-aes": ^1.4.3
|
||||
"@standardnotes/react-native-textview": 1.1.0
|
||||
|
|
@ -7431,7 +7427,7 @@ __metadata:
|
|||
"@reach/tooltip": ^0.16.2
|
||||
"@reach/visually-hidden": ^0.16.0
|
||||
"@standardnotes/encryption": "workspace:*"
|
||||
"@standardnotes/filepicker": 1.16.23
|
||||
"@standardnotes/filepicker": "workspace:*"
|
||||
"@standardnotes/icons": "workspace:*"
|
||||
"@standardnotes/services": ^1.13.23
|
||||
"@standardnotes/sncrypto-web": 1.10.1
|
||||
|
|
@ -36906,6 +36902,44 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ts-node@npm:^10.5.0":
|
||||
version: 10.8.2
|
||||
resolution: "ts-node@npm:10.8.2"
|
||||
dependencies:
|
||||
"@cspotcode/source-map-support": ^0.8.0
|
||||
"@tsconfig/node10": ^1.0.7
|
||||
"@tsconfig/node12": ^1.0.7
|
||||
"@tsconfig/node14": ^1.0.0
|
||||
"@tsconfig/node16": ^1.0.2
|
||||
acorn: ^8.4.1
|
||||
acorn-walk: ^8.1.1
|
||||
arg: ^4.1.0
|
||||
create-require: ^1.1.0
|
||||
diff: ^4.0.1
|
||||
make-error: ^1.1.1
|
||||
v8-compile-cache-lib: ^3.0.1
|
||||
yn: 3.1.1
|
||||
peerDependencies:
|
||||
"@swc/core": ">=1.2.50"
|
||||
"@swc/wasm": ">=1.2.50"
|
||||
"@types/node": "*"
|
||||
typescript: ">=2.7"
|
||||
peerDependenciesMeta:
|
||||
"@swc/core":
|
||||
optional: true
|
||||
"@swc/wasm":
|
||||
optional: true
|
||||
bin:
|
||||
ts-node: dist/bin.js
|
||||
ts-node-cwd: dist/bin-cwd.js
|
||||
ts-node-esm: dist/bin-esm.js
|
||||
ts-node-script: dist/bin-script.js
|
||||
ts-node-transpile-only: dist/bin-transpile.js
|
||||
ts-script: dist/bin-script-deprecated.js
|
||||
checksum: 1eede939beed9f4db35bcc88d78ef803815b99dcdbed1ecac728d861d74dc694918a7f0f437aa08d026193743a31e7e00e2ee34f875f909b5879981c1808e2a7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ts-node@npm:^10.7.0, ts-node@npm:^10.8.1":
|
||||
version: 10.8.1
|
||||
resolution: "ts-node@npm:10.8.1"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue