feat: add filepicker package

This commit is contained in:
Karol Sójko 2022-07-05 19:28:22 +02:00
parent 577da2ca84
commit d4188a3fa2
No known key found for this signature in database
GPG key ID: A50543BF560BDEB0
45 changed files with 5848 additions and 25 deletions

2
.gitignore vendored
View file

@ -14,6 +14,8 @@ codeqldb
coverage
lerna-debug.log
packages/**/dist
**/.pnp.*
**/.yarn/*
!.yarn/patches

Binary file not shown.

View file

@ -1 +0,0 @@
dist

View file

@ -1 +0,0 @@
dist

View file

@ -0,0 +1,3 @@
node_modules
dist
example

View file

@ -0,0 +1,6 @@
{
"extends": "../../.eslintrc",
"parserOptions": {
"project": "./linter.tsconfig.json"
}
}

View 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

View file

@ -0,0 +1,5 @@
{
"rules": {
"no-console": ["off"]
}
}

View 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>

View 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"
}
}

View 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
}
}

View 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!')
}
}
}

View 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()

View 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)
}
}

View file

@ -0,0 +1,11 @@
{
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"target": "es2015",
"module": "es2015",
"moduleResolution": "node",
"baseUrl": ".",
},
"exclude": ["dist", "node_modules"]
}

View 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,
},
},
],
},
],
},
};
};

File diff suppressed because it is too large Load diff

View 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/"
]
};

View file

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["dist", "example"]
}

View 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"
}
}

View 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)
})
})

View 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 = []
}
}

View 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)
})
})

View 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)
}
}

View 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)
})
})

View 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()
}
}
}

View 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,
}
}

View 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')
}
}

View 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
}

View 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'
}
}

View 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
}

View 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()
}
}

View file

@ -0,0 +1,7 @@
export type EncryptedBytes = {
encryptedBytes: Uint8Array
}
export type DecryptedBytes = {
decryptedBytes: Uint8Array
}

View 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'

View file

@ -0,0 +1,6 @@
export type OnChunkCallback = (chunk: Uint8Array, index: number, isLast: boolean) => Promise<void>
export type FileSelectionResponse = {
name: string
mimeType: string
}

View 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')
})
})
})

View 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}`
}

View 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"]
}

View file

@ -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: {

View file

@ -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",

View file

@ -1 +0,0 @@
dist

View file

@ -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",

View file

@ -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"