├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yaml │ └── feature_request.md └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── commitlint.config.js ├── docs └── class-diagram.svg ├── package.json ├── release.config.js ├── src ├── AbstractFolderEntry.ts ├── ArrayBufferBlob.ts ├── AutoBindingEmitter.ts ├── BranchEntry.ts ├── IEntry.ts ├── IFileEntry.ts ├── IFolderEntry.ts ├── IFolderMembership.ts ├── IMatrixFiles.ts ├── IPendingEntry.ts ├── MatrixFiles.ts ├── PendingBranchEntry.ts ├── TreeSpaceEntry.ts ├── TreeSpaceMembership.ts ├── index.ts └── log.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'matrix-org', 4 | ], 5 | extends: [ 6 | 'plugin:matrix-org/typescript', 7 | ], 8 | env: { 9 | browser: true, 10 | node: true, 11 | }, 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | project: "./tsconfig.json", 15 | }, 16 | rules: { 17 | '@typescript-eslint/no-floating-promises': 'error', 18 | '@typescript-eslint/no-misused-promises': 'error', 19 | '@typescript-eslint/promise-function-async': 'error', 20 | '@typescript-eslint/await-thenable': 'error', 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yaml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Matrix Community Support 4 | url: "https://matrix.to/#/#matrix:matrix.org" 5 | about: General support questions can be asked here. 6 | - name: Matrix Security Policy 7 | url: https://www.matrix.org/security-disclosure-policy/ 8 | about: Learn more about our security disclosure policy. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | lint: 10 | name: 'Lint' 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - name: Setup node 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version-file: '.nvmrc' 19 | - name: Cache deps 20 | id: cache 21 | uses: actions/cache@v3 22 | with: 23 | path: ./node_modules 24 | key: modules-${{ hashFiles('yarn.lock') }} 25 | - name: Install deps 26 | if: steps.cache.outputs.cache-hit != 'true' 27 | run: yarn install 28 | - name: Lint 29 | run: yarn lint 30 | 31 | build: 32 | name: 'Build' 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v3 37 | - name: Setup node 38 | uses: actions/setup-node@v3 39 | with: 40 | node-version-file: '.nvmrc' 41 | - name: Cache deps 42 | id: cache 43 | uses: actions/cache@v3 44 | with: 45 | path: ./node_modules 46 | key: modules-${{ hashFiles('yarn.lock') }} 47 | - name: Install deps 48 | if: steps.cache.outputs.cache-hit != 'true' 49 | run: yarn install 50 | - name: Build 51 | run: yarn build 52 | 53 | release: 54 | name: Semantic release 55 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 56 | needs: [lint, build] 57 | runs-on: ubuntu-latest 58 | steps: 59 | - name: Checkout 60 | uses: actions/checkout@v3 61 | with: 62 | persist-credentials: false 63 | - name: Setup node 64 | uses: actions/setup-node@v3 65 | with: 66 | node-version-file: '.nvmrc' 67 | - name: Cache deps 68 | id: cache 69 | uses: actions/cache@v3 70 | with: 71 | path: ./node_modules 72 | key: modules-${{ hashFiles('yarn.lock') }} 73 | - name: Install deps 74 | if: steps.cache.outputs.cache-hit != 'true' 75 | run: yarn install 76 | - name: Release 77 | run: yarn release 78 | env: 79 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 80 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | 75 | # FuseBox cache 76 | .fusebox/ 77 | 78 | dist 79 | .DS_Store 80 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [3.1.0](https://github.com/matrix-org/matrix-files-sdk/compare/v3.0.2...v3.1.0) (2023-11-29) 2 | 3 | 4 | ### Features 5 | 6 | * update matrix-js-sdk from v24 to v30 ([4f55110](https://github.com/matrix-org/matrix-files-sdk/commit/4f551100265a0f768ab0efdee712bb6548bb695f)) 7 | 8 | ## [3.0.2](https://github.com/matrix-org/matrix-files-sdk/compare/v3.0.1...v3.0.2) (2023-03-30) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **deps:** bump matrix-js-sdk from 22.0.0 to 24.0.0 ([#16](https://github.com/matrix-org/matrix-files-sdk/issues/16)) ([d433cb9](https://github.com/matrix-org/matrix-files-sdk/commit/d433cb90208928ab1aac9562d3d16c541136879d)) 14 | 15 | ## [3.0.1](https://github.com/matrix-org/matrix-files-sdk/compare/v3.0.0...v3.0.1) (2022-12-09) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **deps:** update qs to non-vulnerable version ([393e815](https://github.com/matrix-org/matrix-files-sdk/commit/393e815f508908e8d1cc3755bbb698e56c1998fa)) 21 | 22 | # [3.0.0](https://github.com/matrix-org/matrix-files-sdk/compare/v2.5.0...v3.0.0) (2022-12-08) 23 | 24 | 25 | ### Features 26 | 27 | * upgrade to matrix-js-sdk v22 ([165d3ac](https://github.com/matrix-org/matrix-files-sdk/commit/165d3ac578a8891c62e4bc8084e56b982b4a9ba8)) 28 | 29 | 30 | ### BREAKING CHANGES 31 | 32 | * requires Node.js >= 16 33 | 34 | # [2.5.0](https://github.com/matrix-org/matrix-files-sdk/compare/v2.4.2...v2.5.0) (2022-11-14) 35 | 36 | 37 | ### Features 38 | 39 | * upgrade js-sdk to known secure version ([ca396ee](https://github.com/matrix-org/matrix-files-sdk/commit/ca396eecb9d000b666fa72fadb55c0b7faad03f6)) 40 | 41 | ## [2.4.2](https://github.com/matrix-org/matrix-files-sdk/compare/v2.4.1...v2.4.2) (2022-05-18) 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * use release version of matrix-js-sdk not RC ([10e4fb9](https://github.com/matrix-org/matrix-files-sdk/commit/10e4fb9ac6d373153d38b2c5135461b183dc5796)) 47 | 48 | ## [2.4.1](https://github.com/matrix-org/matrix-files-sdk/compare/v2.4.0...v2.4.1) (2022-02-28) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * emit modified event when room/folder is left or deleted ([baac1bd](https://github.com/matrix-org/matrix-files-sdk/commit/baac1bd45f110a21017b6e49e3d739bf2dfc4224)) 54 | 55 | # [2.4.0](https://github.com/matrix-org/matrix-files-sdk/compare/v2.3.2...v2.4.0) (2022-02-24) 56 | 57 | 58 | ### Bug Fixes 59 | 60 | * support waiting for sent/real event from PendingBranchEvent ([c172413](https://github.com/matrix-org/matrix-files-sdk/commit/c172413377a2e653d503737fc0b82784c3aa22d2)) 61 | 62 | 63 | ### Features 64 | 65 | * improve trace logging to include entity ID ([22d2699](https://github.com/matrix-org/matrix-files-sdk/commit/22d2699d9a6523b13b3f82d65325d0e74059ed35)) 66 | * keep tracking of pending IFileEntries and return them in IFolder.getChildren() ([2372d1c](https://github.com/matrix-org/matrix-files-sdk/commit/2372d1c6ddb20ef1a39ab2f30dedaea1c8200d39)) 67 | * return ID from IFileEntry.addVersion() and .copyAsVersion() ([39ea089](https://github.com/matrix-org/matrix-files-sdk/commit/39ea089fcb9e4f3d81cbe1347011bbbe516573dc)) 68 | 69 | ## [2.3.2](https://github.com/matrix-org/matrix-files-sdk/compare/v2.3.1...v2.3.2) (2022-02-23) 70 | 71 | 72 | ### Bug Fixes 73 | 74 | * more event logging ([5c3abeb](https://github.com/matrix-org/matrix-files-sdk/commit/5c3abebf86221c936ff55cce12f8fcf3e03d6691)) 75 | 76 | ## [2.3.1](https://github.com/matrix-org/matrix-files-sdk/compare/v2.3.0...v2.3.1) (2022-02-21) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * added trace logging of event emitter ([f7e28a3](https://github.com/matrix-org/matrix-files-sdk/commit/f7e28a3cd6d719267266a3672fa00a86eb07cb24)) 82 | 83 | # [2.3.0](https://github.com/matrix-org/matrix-files-sdk/compare/v2.2.0...v2.3.0) (2022-02-21) 84 | 85 | 86 | ### Features 87 | 88 | * improve trace logging to include entity ID ([64a2c8b](https://github.com/matrix-org/matrix-files-sdk/commit/64a2c8be2cafc7fd8193adacd86e19ec2e3c6d04)) 89 | 90 | # [2.2.0](https://github.com/matrix-org/matrix-files-sdk/compare/v2.1.1...v2.2.0) (2022-02-18) 91 | 92 | 93 | ### Features 94 | 95 | * add logging support via log4js ([a52e43a](https://github.com/matrix-org/matrix-files-sdk/commit/a52e43ace6a60cde1da2cbfcae5046d52c68978f)) 96 | 97 | ## [2.1.1](https://github.com/matrix-org/matrix-files-sdk/compare/v2.1.0...v2.1.1) (2022-02-14) 98 | 99 | 100 | ### Bug Fixes 101 | 102 | * top-level is writable for folders ([30743b2](https://github.com/matrix-org/matrix-files-sdk/commit/30743b22c4bee729a4fc5463267970e0910beaf5)) 103 | 104 | # [2.1.0](https://github.com/matrix-org/matrix-files-sdk/compare/v2.0.5...v2.1.0) (2022-02-14) 105 | 106 | 107 | ### Features 108 | 109 | * add IEntry.writable and IFolderMembership.canWrite ([7debecc](https://github.com/matrix-org/matrix-files-sdk/commit/7debeccf3ff640169737c3f66a6d2ab308982efa)) 110 | 111 | ## [2.0.5](https://github.com/matrix-org/matrix-files-sdk/compare/v2.0.4...v2.0.5) (2022-02-11) 112 | 113 | 114 | ### Bug Fixes 115 | 116 | * export IMatrixFiles ([05a8000](https://github.com/matrix-org/matrix-files-sdk/commit/05a800049f511f9eaace4c16bff19ad1df3f5d96)) 117 | 118 | ## [2.0.4](https://github.com/matrix-org/matrix-files-sdk/compare/v2.0.3...v2.0.4) (2022-02-11) 119 | 120 | 121 | ### Bug Fixes 122 | 123 | * correctly handle case of unknown last modified date on folder ([67eb982](https://github.com/matrix-org/matrix-files-sdk/commit/67eb982335361cd040cb1d4fd4c4a8c116d4bc8b)) 124 | 125 | ## [2.0.3](https://github.com/matrix-org/matrix-files-sdk/compare/v2.0.2...v2.0.3) (2022-02-07) 126 | 127 | 128 | ### Bug Fixes 129 | 130 | * get version history more reliably ([7294e19](https://github.com/matrix-org/matrix-files-sdk/commit/7294e19d0b944077862abe4ea485c650a10ccfea)) 131 | 132 | ## [2.0.2](https://github.com/matrix-org/matrix-files-sdk/compare/v2.0.1...v2.0.2) (2022-02-07) 133 | 134 | 135 | ### Bug Fixes 136 | 137 | * emit modified event when child room changes name ([5f69f88](https://github.com/matrix-org/matrix-files-sdk/commit/5f69f88efb65f60d9f36aa764aab552a2cef9117)) 138 | 139 | ## [2.0.1](https://github.com/matrix-org/matrix-files-sdk/compare/v2.0.0...v2.0.1) (2022-02-07) 140 | 141 | 142 | ### Bug Fixes 143 | 144 | * include class diagram in npm ([96a3d45](https://github.com/matrix-org/matrix-files-sdk/commit/96a3d456ee29805d44d44abbe3aa3dcb30888c55)) 145 | 146 | # [2.0.0](https://github.com/matrix-org/matrix-files-sdk/compare/v1.5.0...v2.0.0) (2022-02-06) 147 | 148 | 149 | ### Features 150 | 151 | * removed all deprecated functions/properties ([b7bd1a6](https://github.com/matrix-org/matrix-files-sdk/commit/b7bd1a657bd1552ce9693df92dbf267a05c5dbea)) 152 | 153 | 154 | ### BREAKING CHANGES 155 | 156 | * removed all deprecated functions/properties 157 | 158 | # [1.5.0](https://github.com/matrix-org/matrix-files-sdk/compare/v1.4.0...v1.5.0) (2022-02-06) 159 | 160 | 161 | ### Features 162 | 163 | * cleanup interfaces exposed by SDK and deprecate old names ([#3](https://github.com/matrix-org/matrix-files-sdk/issues/3)) ([2188799](https://github.com/matrix-org/matrix-files-sdk/commit/21887995a3940ba8e7cdf03b635076dba762445b)) 164 | 165 | # [1.4.0](https://github.com/matrix-org/matrix-files-sdk/compare/v1.3.0...v1.4.0) (2022-02-05) 166 | 167 | 168 | ### Features 169 | 170 | * add inline documentation for public Interface classes ([f160a27](https://github.com/matrix-org/matrix-files-sdk/commit/f160a273419133447433b99be3d3e78e1e9f9be2)) 171 | 172 | # [1.3.0](https://github.com/matrix-org/matrix-files-sdk/compare/v1.2.0...v1.3.0) (2022-02-05) 173 | 174 | 175 | ### Features 176 | 177 | * expose IMatrixFiles interface ([ad2a85e](https://github.com/matrix-org/matrix-files-sdk/commit/ad2a85e80cc479b86eba82d02441ed4d1b7f9d30)) 178 | 179 | # [1.2.0](https://github.com/matrix-org/matrix-files-sdk/compare/v1.1.4...v1.2.0) (2022-01-31) 180 | 181 | 182 | ### Features 183 | 184 | * optimise moveTo() to rename() where possible ([6f48822](https://github.com/matrix-org/matrix-files-sdk/commit/6f48822265bece993e870b30390c857be949e7c9)) 185 | 186 | ## [1.1.4](https://github.com/matrix-org/matrix-files-sdk/compare/v1.1.3...v1.1.4) (2022-01-29) 187 | 188 | 189 | ### Bug Fixes 190 | 191 | * **deps:** revert workaround for matrix-encrypt-attachment not working from npmjs ([2a5b3ef](https://github.com/matrix-org/matrix-files-sdk/commit/2a5b3ef2fa410325a5311e639b3b717a49bdfed2)) 192 | 193 | ## [1.1.3](https://github.com/matrix-org/matrix-files-sdk/compare/v1.1.2...v1.1.3) (2022-01-17) 194 | 195 | 196 | ### Bug Fixes 197 | 198 | * use release version of matrix-js-sdk instead of rc ([0345dbf](https://github.com/matrix-org/matrix-files-sdk/commit/0345dbf0fbfb1903a0d8cdec0dbb6fdd6757c2b8)) 199 | 200 | ## [1.1.2](https://github.com/matrix-org/matrix-files-sdk/compare/v1.1.1...v1.1.2) (2022-01-17) 201 | 202 | 203 | ### Bug Fixes 204 | 205 | * actually return the stored mimetype in getBlob() ([417edb8](https://github.com/matrix-org/matrix-files-sdk/commit/417edb8fd63ecfb203183b1c0784ed1e471eb608)) 206 | 207 | ## [1.1.1](https://github.com/matrix-org/matrix-files-sdk/compare/v1.1.0...v1.1.1) (2022-01-17) 208 | 209 | 210 | ### Bug Fixes 211 | 212 | * export ArrayBufferBlob type ([7bbbac2](https://github.com/matrix-org/matrix-files-sdk/commit/7bbbac2cfd2a0847def628552cb6b897e429a413)) 213 | 214 | # [1.1.0](https://github.com/matrix-org/matrix-files-sdk/compare/v1.0.3...v1.1.0) (2022-01-17) 215 | 216 | 217 | ### Features 218 | 219 | * return raw mimetype and rely on SDK consumer to take care of any XSS issues ([2d9ab63](https://github.com/matrix-org/matrix-files-sdk/commit/2d9ab6387f2abf9ff88c77cbd867c95ffc7feb4a)) 220 | 221 | ## [1.0.3](https://github.com/matrix-org/matrix-files-sdk/compare/v1.0.2...v1.0.3) (2022-01-17) 222 | 223 | 224 | ### Bug Fixes 225 | 226 | * correct return type of IEntry.getParent() ([48e282a](https://github.com/matrix-org/matrix-files-sdk/commit/48e282a4ce83fbbb2bcef892e07caf8522986e43)) 227 | 228 | ## [1.0.2](https://github.com/matrix-org/matrix-files-sdk/compare/v1.0.1...v1.0.2) (2022-01-16) 229 | 230 | 231 | ### Bug Fixes 232 | 233 | * reduce npm package size and exclude source/build files ([69577a4](https://github.com/matrix-org/matrix-files-sdk/commit/69577a428eae33ec430273b7e0519674b8ac426e)) 234 | 235 | ## [1.0.1](https://github.com/matrix-org/matrix-files-sdk/compare/v1.0.0...v1.0.1) (2022-01-15) 236 | 237 | 238 | ### Bug Fixes 239 | 240 | * fixed up changelog ([c8e4f0e](https://github.com/matrix-org/matrix-files-sdk/commit/c8e4f0e1b5bd95d6c8428aab6fb1002736373eae)) 241 | 242 | # 1.0.0 (2022-01-15) 243 | 244 | 245 | ### Features 246 | 247 | * first iteration of file system like abstraction over MatrixClient ([12185f8](https://github.com/matrix-org/matrix-files-sdk/commit/12185f8d34c937141a1a21343421655a655b7726)) 248 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing code to matrix-files-sdk 2 | 3 | Everyone is welcome to contribute code to this project, provided that they are 4 | willing to license their contributions under the same license as the project 5 | itself. We follow a simple 'inbound=outbound' model for contributions: the act 6 | of submitting an 'inbound' contribution means that the contributor agrees to 7 | license the code under the same terms as the project's overall 'outbound' 8 | license - in this case, Apache Software License v2 (see [LICENSE](./LICENSE)). 9 | 10 | ## How to contribute 11 | 12 | The preferred and easiest way to contribute changes to the project is to fork 13 | it on github, and then create a pull request to ask us to pull your changes 14 | into our repo (https://help.github.com/articles/using-pull-requests/) 15 | 16 | We use the main branch as an unstable/development branch - users looking for 17 | a stable branch should use the release branches or a given release instead. 18 | 19 | The workflow is that contributors should fork the main branch to 20 | make a 'feature' branch for a particular contribution, and then make a pull 21 | request to merge this back into the matrix.org 'official' main branch. We 22 | use GitHub's pull request workflow to review the contribution, and either ask 23 | you to make any refinements needed or merge it and make them ourselves. The 24 | changes will then land on master when we next do a release. 25 | 26 | We use continuous integration, and all pull requests get automatically tested 27 | by it: if your change breaks the build, then the PR will show that there are 28 | failed checks, so please check back after a few minutes. 29 | 30 | ## Code style 31 | 32 | This project aims to target TypeScript with published versions having JS-compatible 33 | code. All files should be written in TypeScript. 34 | 35 | Members should not be exported as a default export in general - it causes problems 36 | with the architecture of the SDK (index file becomes less clear) and could 37 | introduce naming problems (as default exports get aliased upon import). In 38 | general, avoid using `export default`. 39 | 40 | The remaining code-style for this project is not formally documented, but 41 | contributors are encouraged to read the 42 | [code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md) 43 | and follow the principles set out there. 44 | 45 | Please ensure your changes match the cosmetic style of the existing project, 46 | and **never** mix cosmetic and functional changes in the same commit, as it 47 | makes it horribly hard to review otherwise. 48 | 49 | ## Sign off 50 | 51 | In order to have a concrete record that your contribution is intentional 52 | and you agree to license it under the same terms as the project's license, we've 53 | adopted the same lightweight approach that the Linux Kernel 54 | (https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker 55 | (https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other 56 | projects use: the DCO (Developer Certificate of Origin: 57 | http://developercertificate.org/). This is a simple declaration that you wrote 58 | the contribution or otherwise have the right to contribute it to Matrix: 59 | 60 | ``` 61 | Developer Certificate of Origin 62 | Version 1.1 63 | 64 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 65 | 660 York Street, Suite 102, 66 | San Francisco, CA 94110 USA 67 | 68 | Everyone is permitted to copy and distribute verbatim copies of this 69 | license document, but changing it is not allowed. 70 | 71 | Developer's Certificate of Origin 1.1 72 | 73 | By making a contribution to this project, I certify that: 74 | 75 | (a) The contribution was created in whole or in part by me and I 76 | have the right to submit it under the open source license 77 | indicated in the file; or 78 | 79 | (b) The contribution is based upon previous work that, to the best 80 | of my knowledge, is covered under an appropriate open source 81 | license and I have the right under that license to submit that 82 | work with modifications, whether created in whole or in part 83 | by me, under the same open source license (unless I am 84 | permitted to submit under a different license), as indicated 85 | in the file; or 86 | 87 | (c) The contribution was provided directly to me by some other 88 | person who certified (a), (b) or (c) and I have not modified 89 | it. 90 | 91 | (d) I understand and agree that this project and the contribution 92 | are public and that a record of the contribution (including all 93 | personal information I submit with it, including my sign-off) is 94 | maintained indefinitely and may be redistributed consistent with 95 | this project or the open source license(s) involved. 96 | ``` 97 | 98 | If you agree to this for your contribution, then all that's needed is to 99 | include the line in your commit or pull request comment: 100 | 101 | ``` 102 | Signed-off-by: Your Name 103 | ``` 104 | 105 | We accept contributions under a legally identifiable name, such as your name on 106 | government documentation or common-law names (names claimed by legitimate usage 107 | or repute). Unfortunately, we cannot accept anonymous contributions at this 108 | time. 109 | 110 | Git allows you to add this signoff automatically when using the `-s` flag to 111 | `git commit`, which uses the name and email set in your `user.name` and 112 | `user.email` git configs. 113 | 114 | If you forgot to sign off your commits before making your pull request and are 115 | on Git 2.17+ you can mass signoff using rebase: 116 | 117 | ``` 118 | git rebase --signoff origin/main 119 | ``` 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Matrix Files SDK 2 | 3 | Provides a file system like abstraction around MSC3089 tree & branch spaces over [Matrix](https://matrix.org). 4 | 5 | Targets LTS versions of Node.js (currently >=12) and browsers. 6 | 7 | ## Installation 8 | 9 | ```sh 10 | npm install matrix-files-sdk 11 | ``` 12 | 13 | or for `yarn`: 14 | 15 | ```sh 16 | yarn add matrix-files-sdk 17 | ``` 18 | 19 | You must install `matrix-js-sdk` in your project as well. 20 | 21 | ## Usage 22 | 23 | For a more complete example of the SDK in use see [vector-im/files-sdk-demo](https://github.com/vector-im/files-sdk-demo). 24 | 25 | ![Class Diagram](./docs/class-diagram.svg) 26 | 27 | ### Initialisation 28 | 29 | The SDK acts as a wrapper around `matrix-js-sdk` so you need to create an instance of `MatrixClient` first. 30 | 31 | The main entry point is via the `MatrixFiles` class: 32 | 33 | ```ts 34 | import { createClient } from 'matrix-js-sdk'; 35 | import { MatrixFiles } from 'matrix-files-sdk'; 36 | 37 | const client = createClient({}); 38 | 39 | const files = new MatrixFiles(client); 40 | 41 | // do initial sync of required state: 42 | await files.sync(); 43 | ``` 44 | 45 | ### Navigating the hierarchy 46 | 47 | You can use the `IFolder.getChildren()` function to discovery entries in the hierarchy: 48 | 49 | ```ts 50 | import { IEntry } from 'matrix-files-sdk'; 51 | 52 | ... 53 | 54 | const entries: IEntry[] = await folder.getChildren(); 55 | 56 | for (const e: entries) { 57 | console.log(`${e.name} => ${e.isFolder ? 'folder' : 'file' }`); 58 | } 59 | 60 | ``` 61 | 62 | Entries are identified by an ID (`IEntry.id` of type `MatrixFilesID`) which are standard Matrix room and event IDs. 63 | 64 | The ID can be used on an `IFolder` with `getChildById()` and `getDescendantById()`. 65 | 66 | Furthermore the `MatrixFiles.resolvePath()` function can be used to resolve an entry by name from the root of the hierarchy: 67 | 68 | ```ts 69 | const entry = await files.resolvePath(['My workspace', 'documents', 'file.pdf']); 70 | ``` 71 | 72 | ### Common operations on entries 73 | 74 | Deleting (redacting) a file: 75 | 76 | ```ts 77 | await anEntry.delete(); 78 | ``` 79 | 80 | Renaming on a folder: 81 | 82 | ```ts 83 | await aFolder.rename('new name'); 84 | ``` 85 | 86 | and a file: 87 | 88 | ```ts 89 | await aFile.rename('new name.pdf'); 90 | ``` 91 | 92 | Moving within the hierarchy: 93 | 94 | ```ts 95 | const file = await files.resolvePath(['old folder', 'file.pdf']); 96 | 97 | const newFolderId = await files.addFolder('new folder'); 98 | const newFolder = await files.getDescendantById(newFolderId); 99 | 100 | await file.moveTo(newFolder, 'file.pdf'); 101 | ``` 102 | 103 | ### Observing changes 104 | 105 | Files, folders and `MatrixFiles` all implement the `EventEmitter` pattern: 106 | 107 | ```ts 108 | const file = await files.resolvePath(['old folder', 'file.pdf']); 109 | 110 | file.on('modified', () => { console.log('file modified'); }); 111 | ``` 112 | 113 | ### Logging 114 | 115 | Logging is available via [log4js](https://www.npmjs.com/package/log4js) under the `MatrixFilesSDK` log category. 116 | 117 | For example, to enable trace level logging from the SDK: 118 | 119 | ```ts 120 | import log4js from 'log4js'; 121 | 122 | log4js.configure({ 123 | appenders: { 124 | console: { 125 | type: 'console', 126 | layout: { type: 'coloured' }, 127 | }, 128 | }, 129 | categories: { 130 | default: { 131 | appenders: ['console'], 132 | level: 'debug', 133 | }, 134 | MatrixFilesSDK: { 135 | appenders: ['console'], 136 | level: 'trace', 137 | }, 138 | }, 139 | }); 140 | ``` 141 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a Vulnerability 2 | 3 | **If you've found a security vulnerability, please report it to security@matrix.org** 4 | 5 | For more information on our security disclosure policy, visit https://www.matrix.org/security-disclosure-policy/ 6 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/class-diagram.svg: -------------------------------------------------------------------------------- 1 | 2 | nomnoml 3 | #.interface: fill=lightblue 4 | #.enumeration: fill=lightgreen 5 | [<interface>IEntry|id;parent;path;name;isFolder|getCreatedByUserId();getCreationDate();getLastModifiedDate();delete();rename();copyTo();moveTo()] 6 | [EventEmitter]<:--[IEntry] 7 | [EventEmitter]<:--[IEntry] 8 | [<interface>IFileEntry|version;locked;encryptionStatus|copyAsVersion();addVersion();getBlob();getSize();setLocked();getVersionHistory()] 9 | [IEntry]<:--[IFileEntry] 10 | [EventEmitter]<:--[IFileEntry] 11 | [EventEmitter]<:--[IFileEntry] 12 | [IFileEntry]<:--[BranchEntry] 13 | [<interface>IFolderEntry|members;ownMembership|getChildren();getChildByName();getChildById();getDescendantById();addFolder();addFile();getMembership();inviteMember();setMemberRole();removeMember()] 14 | [IEntry]<:--[IFolderEntry] 15 | [EventEmitter]<:--[IFolderEntry] 16 | [EventEmitter]<:--[IFolderEntry] 17 | [IFolderEntry]<:--[AbstractFolderEntry] 18 | [IFolderEntry]<:--[TreeSpaceEntry] 19 | [IFolderEntry]<:--[MatrixFiles] 20 | [<interface>IFolderMembership|userId;role;since;canInvite;canRemove;canManageRoles|] 21 | [EventEmitter]<:--[IFolderMembership] 22 | [EventEmitter]<:--[IFolderMembership] 23 | [IFolderMembership]<:--[TreeSpaceMembership] 24 | [<interface>IMatrixFiles||resolvePath();sync();logout();deactivate();getPendingInvites();acceptInvite()] 25 | [EventEmitter]<:--[IMatrixFiles] 26 | [EventEmitter]<:--[IMatrixFiles] 27 | [IMatrixFiles]<:--[MatrixFiles] 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | IEntry 56 | 57 | id 58 | parent 59 | path 60 | name 61 | isFolder 62 | 63 | getCreatedByUserId() 64 | getCreationDate() 65 | getLastModifiedDate() 66 | delete() 67 | rename() 68 | copyTo() 69 | moveTo() 70 | 71 | IFileEntry 72 | 73 | version 74 | locked 75 | encryptionStatus 76 | 77 | copyAsVersion() 78 | addVersion() 79 | getBlob() 80 | getSize() 81 | setLocked() 82 | getVersionHistory() 83 | 84 | IFolderEntry 85 | 86 | members 87 | ownMembership 88 | 89 | getChildren() 90 | getChildByName() 91 | getChildById() 92 | getDescendantById() 93 | addFolder() 94 | addFile() 95 | getMembership() 96 | inviteMember() 97 | setMemberRole() 98 | removeMember() 99 | 100 | IFolderMembership 101 | 102 | userId 103 | role 104 | since 105 | canInvite 106 | canRemove 107 | canManageRoles 108 | 109 | 110 | IMatrixFiles 111 | 112 | 113 | resolvePath() 114 | sync() 115 | logout() 116 | deactivate() 117 | getPendingInvites() 118 | acceptInvite() 119 | 120 | EventEmitter 121 | 122 | BranchEntry 123 | 124 | AbstractFolderEntry 125 | 126 | TreeSpaceEntry 127 | 128 | MatrixFiles 129 | 130 | TreeSpaceMembership 131 | 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matrix-files-sdk", 3 | "version": "3.1.0", 4 | "description": "JS/TS SDK for working with files and folders in Matrix", 5 | "author": "The Matrix.org Foundation C.I.C.", 6 | "license": "Apache-2.0", 7 | "homepage": "https://github.com/matrix-org/matrix-files-sdk", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/matrix-org/matrix-files-sdk" 11 | }, 12 | "keywords": [ 13 | "matrix-org" 14 | ], 15 | "bugs": { 16 | "url": "https://github.com/matrix-org/matrix-files-sdk/issues" 17 | }, 18 | "files": [ 19 | "dist", 20 | "docs", 21 | "README.md", 22 | "CHANGELOG.md", 23 | "SECURITY.md", 24 | "package.json", 25 | "yarn.lock", 26 | "LICENSE" 27 | ], 28 | "main": "dist/index.js", 29 | "scripts": { 30 | "prepublishOnly": "yarn build", 31 | "clean": "rimraf dist", 32 | "build": "yarn clean && tsc --skipLibCheck", 33 | "build:watch": "tsc --skipLibCheck -w", 34 | "lint": "yarn lint:types && yarn lint:style", 35 | "lint:style": "eslint src", 36 | "lint:types": "tsc --noEmit --skipLibCheck", 37 | "docs:class-diagram": "npx tsuml2 --glob './src/I*.ts' --propertyTypes false --modifiers false -o ./docs/class-diagram.svg", 38 | "test": "yarn lint", 39 | "release": "yarn semantic-release" 40 | }, 41 | "dependencies": { 42 | "@log4js-node/log4js-api": "^1.0.2", 43 | "axios": "^0.24.0", 44 | "events": "^3.3.0", 45 | "matrix-encrypt-attachment": "^1.0.3", 46 | "p-retry": "^4.5.0" 47 | }, 48 | "devDependencies": { 49 | "@commitlint/cli": "^16.0.2", 50 | "@commitlint/config-conventional": "^16.0.0", 51 | "@semantic-release/changelog": "^6.0.1", 52 | "@semantic-release/git": "^10.0.1", 53 | "@types/node": "^14.0.0", 54 | "@typescript-eslint/eslint-plugin": "^5.3.0", 55 | "@typescript-eslint/parser": "^5.3.0", 56 | "eslint": "^7.24.0", 57 | "eslint-config-google": "^0.14.0", 58 | "eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org", 59 | "husky": "^7.0.4", 60 | "matrix-js-sdk": "^30.1.0", 61 | "semantic-release": "^19.0.2", 62 | "typescript": "^4.4.4" 63 | }, 64 | "peerDependencies": { 65 | "matrix-js-sdk": "^30.1.0" 66 | }, 67 | "engines": { 68 | "node": ">=16.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: ['main'], 3 | plugins: [ 4 | '@semantic-release/commit-analyzer', 5 | '@semantic-release/release-notes-generator', 6 | '@semantic-release/changelog', 7 | '@semantic-release/npm', 8 | ['@semantic-release/git', { 9 | assets: ['package.json', 'CHANGELOG.md'], 10 | message: 'chore(release): ${nextRelease.version}\n\n${nextRelease.notes}', 11 | }], 12 | '@semantic-release/github', 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /src/AbstractFolderEntry.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import type { MatrixClient } from 'matrix-js-sdk/lib'; 18 | import type { IFolderEntry, IEntry, FolderRole, MatrixFilesID } from '.'; 19 | import { AutoBindingEmitter } from './AutoBindingEmitter'; 20 | import { IFolderMembership } from './IFolderMembership'; 21 | import { ArrayBufferBlob } from './ArrayBufferBlob'; 22 | 23 | export abstract class AbstractFolderEntry extends AutoBindingEmitter implements IFolderEntry { 24 | constructor(client: MatrixClient, public parent: IFolderEntry | undefined, logName?: string) { 25 | super(client, logName); 26 | } 27 | abstract addFile(name: string, file: ArrayBufferBlob): Promise; 28 | abstract id: string; 29 | abstract name: string; 30 | abstract getCreationDate(): Promise; 31 | abstract getLastModifiedDate(): Promise; 32 | abstract delete(): Promise; 33 | abstract rename(newName: string): Promise; 34 | abstract copyTo(newParent: IFolderEntry, newName: string): Promise; 35 | abstract moveTo(newParent: IFolderEntry, newName: string): Promise; 36 | abstract getCreatedByUserId(): Promise; 37 | abstract members: IFolderMembership[]; 38 | abstract ownMembership: IFolderMembership; 39 | abstract getMembership(userId: string): IFolderMembership; 40 | abstract inviteMember(userId: string, role: FolderRole): Promise; 41 | abstract setMemberRole(userId: string, role: FolderRole): Promise; 42 | abstract removeMember(userId: string): Promise; 43 | abstract writable: boolean; 44 | 45 | isFolder = true; 46 | 47 | get path(): string[] { 48 | if (this.parent) { 49 | return [...this.parent.path, this.name]; 50 | } 51 | return []; 52 | } 53 | 54 | abstract getChildren(): Promise; 55 | 56 | async getChildByName(name: string) { 57 | return (await this.getChildren()).find(x => x.name === name); 58 | } 59 | 60 | async getChildById(id: MatrixFilesID) { 61 | return (await this.getChildren()).find(x => x.id === id); 62 | } 63 | 64 | abstract addChildFolder(name: string): Promise; 65 | 66 | async addFolder(_name: string | string[]): Promise { 67 | const name = typeof _name === 'string' ? [_name] : _name; 68 | const first = name[0]; 69 | const existingChild = await this.getChildByName(first); 70 | let childId = (existingChild)?.id; 71 | if (!childId) { 72 | childId = await this.addChildFolder(first); 73 | } else if (!existingChild?.isFolder) { 74 | throw new Error(`Not a folder: ${this.path} => ${first}`); 75 | } 76 | 77 | const folder = await this.getChildById(childId) as IFolderEntry | undefined; 78 | 79 | if (!folder) { 80 | throw new Error('New folder not found'); 81 | } 82 | 83 | const remaining = name.slice(1); 84 | if (remaining.length > 0) { 85 | return folder.addFolder(remaining); 86 | } else { 87 | return folder.id; 88 | } 89 | } 90 | 91 | async getDescendentById(id: string, maxDepth?: number): Promise { 92 | return this.getDescendantById(id, maxDepth); 93 | } 94 | 95 | async getDescendantById(id: string, maxDepth?: number): Promise { 96 | const children = await this.getChildren(); 97 | // breadth first search 98 | for (const c of children) { 99 | if (c.id === id) { 100 | return c; 101 | } 102 | } 103 | if (typeof maxDepth !== 'number' || maxDepth > 0) { 104 | for (const c of children) { 105 | if (c.isFolder) { 106 | const f = c as IFolderEntry; 107 | const found = await f.getDescendantById( 108 | id, 109 | typeof maxDepth !== 'number' ? undefined : maxDepth - 1, 110 | ); 111 | if (found) { 112 | return found; 113 | } 114 | } 115 | } 116 | } 117 | return undefined; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/ArrayBufferBlob.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * Represents a file stored on a Matrix media repository. 19 | */ 20 | export interface ArrayBufferBlob { 21 | /** 22 | * The content of the file in binary form. 23 | */ 24 | data: ArrayBuffer; 25 | 26 | /** 27 | * The size of the file in bytes. 28 | */ 29 | size: number; 30 | 31 | /** 32 | * The mimetype that was given when the file was stored. 33 | */ 34 | mimetype: string; 35 | } 36 | -------------------------------------------------------------------------------- /src/AutoBindingEmitter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import EventEmitter from 'events'; 18 | import type { MatrixClient } from 'matrix-js-sdk/lib'; 19 | 20 | import { log } from './log'; 21 | 22 | /** 23 | * Wrapper to automatically bind and unbind to a shared MatrixClient depending on if anyone in turn has bound to us 24 | */ 25 | export abstract class AutoBindingEmitter extends EventEmitter { 26 | constructor(private matrixClient: MatrixClient, private logName?: string | (() => string)) { 27 | super(); 28 | } 29 | 30 | protected trace(type: string, message?: string) { 31 | if (log.isTraceEnabled()) { 32 | const logName = typeof this.logName === 'function' ? this.logName() : this.logName; 33 | log.trace(`${logName}.${type}()${message ? ` ${message}` : ''}`); 34 | } 35 | } 36 | 37 | private eventHandlers: EventHandlers = {}; 38 | 39 | /** 40 | * 41 | * @param eventHandlers The event handlers to be bound on the underlying MatrixClient 42 | */ 43 | setEventHandlers(eventHandlers: EventHandlers) { 44 | this.eventHandlers = eventHandlers; 45 | for (const e in this.eventHandlers) { 46 | this.eventHandlers[e] = this.eventHandlers[e].bind(this); 47 | } 48 | } 49 | 50 | bound = false; 51 | 52 | private autobind() { 53 | if (this.eventNames().length > 0) { 54 | if (!this.bound) { 55 | // eslint-disable-next-line max-len 56 | this.trace('autobind', `Binding ${Object.values(this.eventHandlers).length} handlers: ${Object.keys(this.eventHandlers)}`); 57 | for (const e in this.eventHandlers) { 58 | this.matrixClient.on(e as any, this.eventHandlers[e]); 59 | } 60 | this.bound = true; 61 | } 62 | } else { 63 | if (this.bound) { 64 | // eslint-disable-next-line max-len 65 | this.trace('autobind', `Unbinding ${Object.values(this.eventHandlers).length} handlers: ${Object.keys(this.eventHandlers)}`); 66 | for (const e in this.eventHandlers) { 67 | this.matrixClient.off(e as any, this.eventHandlers[e]); 68 | } 69 | this.bound = false; 70 | } 71 | } 72 | } 73 | 74 | addListener(event: string | symbol, listener: (...args: any[]) => void): this { 75 | this.trace('addListener', event.toString()); 76 | super.addListener(event, listener); 77 | this.autobind(); 78 | return this; 79 | } 80 | 81 | on(event: string | symbol, listener: (...args: any[]) => void): this { 82 | this.trace('on', event.toString()); 83 | super.on(event, listener); 84 | this.autobind(); 85 | return this; 86 | } 87 | 88 | once(event: string | symbol, listener: (...args: any[]) => void): this { 89 | this.trace('once', event.toString()); 90 | super.once(event, listener); 91 | this.autobind(); 92 | return this; 93 | } 94 | 95 | removeListener(event: string | symbol, listener: (...args: any[]) => void): this { 96 | this.trace('removeListener', event.toString()); 97 | super.removeListener(event, listener); 98 | this.autobind(); 99 | return this; 100 | } 101 | 102 | off(event: string | symbol, listener: (...args: any[]) => void): this { 103 | this.trace('off', event.toString()); 104 | super.off(event, listener); 105 | this.autobind(); 106 | return this; 107 | } 108 | 109 | removeAllListeners(event?: string | symbol): this { 110 | this.trace('removeAllListeners', event?.toString()); 111 | super.removeAllListeners(event); 112 | this.autobind(); 113 | return this; 114 | } 115 | 116 | prependListener(event: string | symbol, listener: (...args: any[]) => void): this { 117 | this.trace('prependListener', event.toString()); 118 | super.prependListener(event, listener); 119 | this.autobind(); 120 | return this; 121 | } 122 | 123 | prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this { 124 | this.trace('prependOnceListener', event.toString()); 125 | super.prependOnceListener(event, listener); 126 | this.autobind(); 127 | return this; 128 | } 129 | 130 | emit(event: string | symbol, ...args: any[]): boolean { 131 | this.trace('emit', event.toString()); 132 | return super.emit(event, ...args); 133 | } 134 | } 135 | 136 | export type EventHandlers = Record void>; 137 | -------------------------------------------------------------------------------- /src/BranchEntry.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import type { MSC3089Branch } from 'matrix-js-sdk/lib/models/MSC3089Branch'; 18 | import type { MatrixEvent, Room } from 'matrix-js-sdk/lib'; 19 | import { encryptAttachment, decryptAttachment } from 'matrix-encrypt-attachment'; 20 | import type { MatrixFilesID, FileEncryptionStatus, IFileEntry, IFolderEntry, MatrixFiles, TreeSpaceEntry } from '.'; 21 | import { ArrayBufferBlob } from './ArrayBufferBlob'; 22 | import { AutoBindingEmitter } from './AutoBindingEmitter'; 23 | import axios from 'axios'; 24 | import { PendingBranchEntry } from './PendingBranchEntry'; 25 | 26 | export class BranchEntry extends AutoBindingEmitter implements IFileEntry { 27 | constructor(private files: MatrixFiles, public parent: TreeSpaceEntry, public branch: MSC3089Branch) { 28 | super(files.client, `BranchEntry(${branch.id})`); 29 | super.setEventHandlers({ 'Room.timeline': this.timelineChanged }); 30 | } 31 | 32 | get id(): string { 33 | return this.branch.id; 34 | } 35 | 36 | get version(): number { 37 | return this.branch.version; 38 | } 39 | 40 | async getCreatedByUserId() { 41 | return (await this.branch.getFileEvent()).event.sender ?? ''; 42 | } 43 | 44 | async getVersionHistory(): Promise { 45 | const versions: BranchEntry[] = []; 46 | versions.push(this); // start with ourselves 47 | 48 | let childEvent: MatrixEvent | undefined; 49 | let parentEvent = await this.branch.getFileEvent(); 50 | do { 51 | const replacingEventId = parentEvent.getRelation()?.event_id; 52 | 53 | if (replacingEventId) { 54 | childEvent = this.parent.treespace.room.findEventById(replacingEventId); 55 | if (childEvent) { 56 | const childId = childEvent.getId(); 57 | const childBranch = childId ? this.parent.treespace.getFile(childId) : undefined; 58 | if (childBranch) { 59 | versions.push(new BranchEntry(this.files, this.parent, childBranch!)); 60 | parentEvent = childEvent; 61 | continue; 62 | } 63 | } 64 | } 65 | break; 66 | } while (childEvent); 67 | 68 | return versions; 69 | } 70 | 71 | isFolder = false; 72 | 73 | get name(): string { 74 | return this.branch.getName(); 75 | } 76 | 77 | get path() { 78 | return [...this.parent.path, this.name]; 79 | } 80 | 81 | get writable() { 82 | return this.parent?.writable ?? false; 83 | } 84 | 85 | async getCreationDate() { 86 | const history = await this.branch.getVersionHistory(); 87 | return new Date(history[history.length - 1].indexEvent.getTs()); 88 | } 89 | 90 | async getLastModifiedDate() { 91 | return new Date((await this.branch.getFileEvent()).getTs()); 92 | } 93 | 94 | async delete() { 95 | this.trace('delete', this.path.join('/')); 96 | return this.branch.delete(); 97 | } 98 | 99 | async rename(name: string) { 100 | this.trace('rename', `${this.path.join('/')} to ${name}`); 101 | return this.branch.setName(name); 102 | } 103 | 104 | async copyAsVersion(fileTo: IFileEntry) { 105 | this.trace('copyAsVersion', `${this.path.join('/')} to ${fileTo.path.join('/')}`); 106 | const blob = await this.getBlob(); 107 | return fileTo.addVersion(blob); 108 | } 109 | 110 | async copyTo(resolvedParent: IFolderEntry, fileName: string): Promise { 111 | this.trace('copyTo', `${this.path.join('/')} to ${resolvedParent.path.join('/')}/${fileName}`); 112 | 113 | const versions = await this.getVersionHistory(); 114 | // process versions in order that they were created 115 | versions.reverse(); 116 | 117 | // make first version 118 | const firstVersion = versions.shift(); 119 | const downloadedFile = await firstVersion!.getBlob(); 120 | const newFileId = await resolvedParent.addFile(fileName, downloadedFile); 121 | 122 | const newFile = await resolvedParent.getChildById(newFileId) as IFileEntry | undefined; 123 | if (!newFile) { 124 | throw new Error('New file not found'); 125 | } 126 | 127 | // make subsequent versions if needed 128 | while (versions.length > 0) { 129 | const v = versions.shift(); 130 | if (v) { 131 | const downloadedVersion = await v.getBlob(); 132 | await newFile.addVersion(downloadedVersion, v.name); 133 | } 134 | } 135 | 136 | return newFileId; 137 | } 138 | 139 | async moveTo(resolvedParent: IFolderEntry, fileName: string): Promise { 140 | this.trace('moveTo', `${this.path.join('/')} to ${resolvedParent.path.join('/')}/${fileName}`); 141 | 142 | // simple rename? 143 | if (resolvedParent.id === this.parent.id) { 144 | await this.rename(fileName); 145 | return this.id; 146 | } 147 | 148 | const newFile = await this.copyTo(resolvedParent, fileName); 149 | await this.delete(); 150 | return newFile; 151 | } 152 | 153 | async addVersion(file: ArrayBufferBlob, newName?: string): Promise { 154 | const name = newName ?? this.name; 155 | this.trace('addVersion', `${this.path.join('/')} with name ${name}`); 156 | 157 | // because adding a file/version is not atomic we add a placeholder pending entry to prevent concurrent adds: 158 | const newEntry = new PendingBranchEntry( 159 | this.files, 160 | this.parent, 161 | `(pending_version_for_${this.id}@${Date.now()})`, 162 | name, 163 | this, 164 | file, 165 | this.files.client.isRoomEncrypted(this.id) ? 'decrypted' : 'encryptionNotEnabled', 166 | this.files.client.getUserId()!, 167 | { 168 | ...this.branch.indexEvent.getContent(), 169 | version: this.branch.version + 1, 170 | }, 171 | ); 172 | 173 | this.files.addPendingEntry(newEntry); 174 | 175 | const { 176 | mimetype, 177 | size, 178 | data, 179 | } = file; 180 | const encrypted = await encryptAttachment(data); 181 | 182 | const { event_id: id } = await this.branch.createNewVersion( 183 | name, 184 | Buffer.from(encrypted.data), 185 | encrypted.info, 186 | { 187 | info: { 188 | mimetype, 189 | size, 190 | }, 191 | }, 192 | ); 193 | 194 | // TODO: ideally we would return the new IFileEntry 195 | 196 | newEntry.setSent(id); 197 | 198 | return id; 199 | } 200 | 201 | async getBlob(): Promise { 202 | const file = await this.branch.getFileInfo(); 203 | 204 | // we use axios for consistent binary handling across browsers and Node.js 205 | const response = await axios.get(file.httpUrl, { responseType: 'arraybuffer' }); 206 | const data = await decryptAttachment(response.data, file.info); 207 | 208 | const { info } = (await this.branch.getFileEvent()).getOriginalContent(); 209 | 210 | return { 211 | data, 212 | mimetype: info?.mimetype ?? 'application/octet-stream', 213 | size: data.byteLength, 214 | }; 215 | } 216 | 217 | private getLoadedFileEvent(): MatrixEvent | undefined { 218 | const room = this.branch.directory.room; 219 | return room.getUnfilteredTimelineSet().findEventById(this.id); 220 | } 221 | 222 | async getSize(): Promise { 223 | const event = await this.branch.getFileEvent(); 224 | const size = event.getOriginalContent().info?.size; 225 | return typeof size === 'number' ? size : -1; 226 | } 227 | 228 | get locked(): boolean { 229 | return this.branch.isLocked(); 230 | } 231 | 232 | async setLocked(locked: boolean): Promise { 233 | return this.branch.setLocked(locked); 234 | } 235 | 236 | timelineChanged(e: MatrixEvent, r: Room) { 237 | this.trace('event(timelineChanged)', `room ${r.roomId} type ${e.getType()}`); 238 | if (r.roomId === this.parent.id && e.replacingEventId() === this.id) { 239 | this.emit('modified', this, e); 240 | this.parent.emit('modified', this, e); 241 | } 242 | } 243 | 244 | get encryptionStatus(): FileEncryptionStatus { 245 | const e = this.getLoadedFileEvent(); 246 | if (!e || e.isDecryptionFailure()) { 247 | return 'decryptionFailed'; 248 | } 249 | if (e.isBeingDecrypted()) { 250 | return 'decryptionPending'; 251 | } 252 | if (e.getClearContent()) { 253 | return 'decrypted'; 254 | } 255 | return e.isEncrypted() ? 'encrypted' : 'encryptionNotEnabled'; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/IEntry.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import type EventEmitter from 'events'; 18 | import type { IFolderEntry } from '.'; 19 | 20 | export type MatrixFilesID = string; 21 | 22 | /** 23 | * Represents an abstract entry in a Matrix Files SDK hierarchy. 24 | */ 25 | export interface IEntry extends EventEmitter { 26 | /** 27 | * The identifier for this entry. It corresponds to a {@link matrix-js-sdk#Room} or {@link matrix-js-sdk#Event}. 28 | */ 29 | id: MatrixFilesID; 30 | 31 | /** 32 | * The parent of this entry or undefined if it is at the top-level. 33 | */ 34 | parent: IFolderEntry | undefined; 35 | 36 | /** 37 | * All parts of the path including the file/leaf name. This is the same path that would be resolved by {@link IMatrixFiles.resolvePath}. 38 | */ 39 | path: string[]; 40 | 41 | /** 42 | * The name of this entry. 43 | */ 44 | name: string; 45 | 46 | /** 47 | * `true` if this entry is an {@link IFolder} 48 | */ 49 | isFolder: boolean; 50 | 51 | /** 52 | * `true` if this user can write to the entry. 53 | */ 54 | writable: boolean; 55 | 56 | /** 57 | * @returns The Matrix user ID that created the entry or `undefined` if not available/known. 58 | */ 59 | getCreatedByUserId(): Promise; 60 | 61 | /** 62 | * @returns The date/time that the entry was created or `undefined` if not available/known. 63 | */ 64 | getCreationDate(): Promise; 65 | 66 | /** 67 | * @returns The date/time that the entry was last modified at or `undefined` if not available/known. 68 | */ 69 | getLastModifiedDate(): Promise; 70 | 71 | /** 72 | * Delete/redact the entry. 73 | */ 74 | delete(): Promise; 75 | 76 | /** 77 | * Rename an entry. 78 | * 79 | * @param newName The new name for the entry. 80 | */ 81 | rename(newName: string): Promise; 82 | 83 | /** 84 | * Copy the entry to a new parent in the hierarchy. The history of the entry will *not* be preserved. Folders will be deep copied. 85 | * 86 | * @param newParent The destination folder that should contain the copy. 87 | * @param newName The new name for the entry in the destination folder. 88 | */ 89 | copyTo(newParent: IFolderEntry, newName: string): Promise; 90 | 91 | /** 92 | * Move the entry to a new parent in the hierarchy. The history of the entry will be preserved. 93 | * 94 | * If the @newParent is the same as the current parent then this should be equivalent to {@link rename}. 95 | * 96 | * @param newParent The destination folder that should be the new parent. 97 | * @param newName The new name for the entry in the destination folder. 98 | */ 99 | moveTo(newParent: IFolderEntry, newName: string): Promise; 100 | } 101 | -------------------------------------------------------------------------------- /src/IFileEntry.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import type EventEmitter from 'events'; 18 | import type { IEntry, ArrayBufferBlob, MatrixFilesID } from '.'; 19 | 20 | /** 21 | * Helper enum to represent the encryption status of the file. 22 | */ 23 | export type FileEncryptionStatus = 24 | 'encryptionNotEnabled' | 25 | 'encrypted' | 26 | 'decryptionPending' | 27 | 'decrypted' | 28 | 'decryptionFailed'; 29 | 30 | /** 31 | * Represents a file stored in the Matrix Files SDK hierarchy. 32 | */ 33 | export interface IFileEntry extends IEntry, EventEmitter { 34 | /** 35 | * The revision number of this file. Revisions start at `1` and increment. 36 | */ 37 | version: number; 38 | 39 | /** 40 | * Copy this file as a new version on another file. 41 | * 42 | * @param fileTo The entry to add a version to. 43 | */ 44 | 45 | copyAsVersion(fileTo: IFileEntry): Promise; 46 | 47 | /** 48 | * Add a new version/revision to this file. 49 | * 50 | * @param file The contents of the new version. 51 | * @param newName The new name for the file, or `undefined` if no change. 52 | */ 53 | addVersion(file: ArrayBufferBlob, newName?: string): Promise; 54 | 55 | /** 56 | * @returns The contents of the file in binary form. 57 | */ 58 | getBlob(): Promise; 59 | 60 | /** 61 | * @returns The size of this file in bytes. 62 | */ 63 | getSize(): Promise; 64 | 65 | /** 66 | * The current lock disposition for this file. 67 | */ 68 | locked: boolean; 69 | 70 | /** 71 | * Set the lock status of this file. 72 | * 73 | * @param locked The lock status to set. 74 | */ 75 | setLocked(locked: boolean): Promise; 76 | 77 | /** 78 | * Get versions of this file. 79 | * 80 | * @returns Array of versions including this version. 81 | */ 82 | getVersionHistory(): Promise; 83 | 84 | /** 85 | * The disposition of this file with regards to Matrix end-to-end encryption. 86 | */ 87 | encryptionStatus: FileEncryptionStatus; 88 | } 89 | -------------------------------------------------------------------------------- /src/IFolderEntry.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import type EventEmitter from 'events'; 18 | import type { IEntry, FolderRole, MatrixFilesID, ArrayBufferBlob, IFolderMembership } from '.'; 19 | 20 | /** 21 | * Represents a file stored in the Matrix Files SDK hierarchy. 22 | */ 23 | export interface IFolderEntry extends IEntry, EventEmitter { 24 | /** 25 | * @returns The immediate descendants of this folder. 26 | */ 27 | getChildren(): Promise; 28 | 29 | /** 30 | * Find an immediate descendant entry by name. 31 | * 32 | * @param name The name of the child to locate. 33 | * @returns The entry if found, otherwise `undefined`. 34 | */ 35 | getChildByName(name: string): Promise; 36 | 37 | /** 38 | * Find an immediate descendant entry by {@link MatrixFilesID}`. 39 | * 40 | * @param name The ID of the child to locate. 41 | * @returns The entry if found, otherwise `undefined`. 42 | */ 43 | getChildById(id: MatrixFilesID): Promise; 44 | 45 | /** 46 | * Find a descendant entry by {@link MatrixFilesID}`. 47 | * 48 | * @param name The ID of the descendant to locate. 49 | * @returns The entry if found, otherwise `undefined`. 50 | */ 51 | getDescendantById(id: MatrixFilesID, maxSearchDepth?: number): Promise; 52 | 53 | /** 54 | * Add a (sub) folder to this folder. 55 | * 56 | * @param name Name of the sub folder to add. 57 | * @returns The ID of the new folder. 58 | */ 59 | addFolder(name: string | string[]): Promise; 60 | 61 | /** 62 | * Add a file to this folder. 63 | * 64 | * @param name Name of the file to add. 65 | * @param file The file contents to add. 66 | * @returns The ID of the new folder. 67 | */ 68 | addFile(name: string, file: ArrayBufferBlob): Promise; 69 | 70 | /** 71 | * Current members of the folder. 72 | */ 73 | members: IFolderMembership[]; 74 | 75 | /** 76 | * Get the membership for a user ID. Throws an exception if the user is not a member. 77 | * 78 | * @param userId The user ID of the member to return. 79 | * @returns The membership representation for the user. 80 | */ 81 | getMembership(userId: string): IFolderMembership; 82 | 83 | /** 84 | * Invite another user to access this folder. The user will be invited recursively to all sub-folders. 85 | * 86 | * @param userId The Matrix user ID to be invited. 87 | * @param role The role for the new member. 88 | * @returns The membership representation of the newly invited user. 89 | */ 90 | inviteMember(userId: string, role: FolderRole): Promise; 91 | 92 | /** 93 | * Change the role/power level of an existing member. 94 | * 95 | * @param userId The Matrix user ID of the member. 96 | * @param role The new role for the member. 97 | * @returns The updated membership representation 98 | */ 99 | setMemberRole(userId: string, role: FolderRole): Promise; 100 | 101 | /** 102 | * Remove an existing member from a folder. 103 | * 104 | * @param userId The Matrix user ID of the member to remove. 105 | */ 106 | removeMember(userId: string): Promise; 107 | 108 | /** 109 | * The representation of the authenticated user on this folder. 110 | */ 111 | ownMembership: IFolderMembership; 112 | } 113 | -------------------------------------------------------------------------------- /src/IFolderMembership.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import type EventEmitter from 'events'; 18 | import type { FolderRole } from '.'; 19 | 20 | /** 21 | * Represents the membership of a folder. This is an abstraction on top of Matrix room membership. 22 | */ 23 | export interface IFolderMembership extends EventEmitter { 24 | /** 25 | * The Matrix user ID for this member. 26 | */ 27 | userId: string; 28 | 29 | /** 30 | * The MSC3089 role of this member. 31 | */ 32 | role: FolderRole; 33 | 34 | /** 35 | * The date/time that the member joined (or was invited?). 36 | */ 37 | since: Date; 38 | 39 | /** 40 | * `true` if the member has power to invite other members to the folder. 41 | */ 42 | canInvite: boolean; 43 | 44 | /** 45 | * `true` if the member has power to remove another member from the folder. 46 | */ 47 | canRemove: boolean; 48 | 49 | /** 50 | * `true` if the member has power to remove change the power of another member of the folder. 51 | */ 52 | canManageRoles: boolean; 53 | 54 | /** 55 | * `true` if the member has power to write or change the content of the folder. 56 | */ 57 | canWrite: boolean; 58 | } 59 | -------------------------------------------------------------------------------- /src/IMatrixFiles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import type EventEmitter from 'events'; 18 | import type { Room } from 'matrix-js-sdk/lib'; 19 | import type { IEntry } from '.'; 20 | 21 | /** 22 | * Represents the main Matrix Files SDK entry point. 23 | */ 24 | export interface IMatrixFiles extends EventEmitter { 25 | /** 26 | * Resolve a path in the hierarchy to an {@link IEntry}. 27 | * 28 | * @param path The entry path to resolve. 29 | * @returns The matching entry or undefined if not found. 30 | */ 31 | resolvePath(path: string[]): Promise; 32 | 33 | /** 34 | * Perform a {@link matrix-js-sdk#MatrixClient.startClient} and sync with necessary configuration to work with Matrix Files SDK. 35 | * 36 | * @returns Promise that resolves when initial sync has been completed. 37 | */ 38 | sync(): Promise; 39 | 40 | /** 41 | * Convenience function for stopping and logging out of {@link matrix-js-sdk#MatrixClient}. 42 | */ 43 | logout(): Promise; 44 | 45 | /** 46 | * Convenience function for deactivating the user account on the home server and stopping the {@link matrix-js-sdk#MatrixClient}. 47 | * @param erase @see{@link matrix-js-sdk#MatrixClient.deactivateAccount} 48 | */ 49 | deactivate(erase?: boolean): Promise; 50 | 51 | /** 52 | * Get any pending folder (room) invitations. 53 | * 54 | * @returns The array of pending invites represented as {@link matrix-js-sdk#Room} objects. 55 | */ 56 | getPendingInvites(): Promise ; 57 | 58 | /** 59 | * Accept a pending invitation to join a folder (room). 60 | * 61 | * @param invite The invitation to be accepted represented as {@link matrix-js-sdk#Room} object. 62 | */ 63 | acceptInvite(invite: Room): Promise ; 64 | } 65 | -------------------------------------------------------------------------------- /src/IPendingEntry.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import type { IEntry } from '.'; 18 | 19 | /** 20 | * Represents an abstract entry in a Matrix Files SDK hierarchy. 21 | */ 22 | export interface IPendingEntry extends IEntry { 23 | replacesEntry: IEntry | undefined; 24 | } 25 | -------------------------------------------------------------------------------- /src/MatrixFiles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { IStartClientOpts, MatrixClient, MatrixEvent, Room, RoomState } from 'matrix-js-sdk/lib'; 18 | import { ClientEvent, PendingEventOrdering } from 'matrix-js-sdk/lib/client'; 19 | import { simpleRetryOperation } from 'matrix-js-sdk/lib/utils'; 20 | import { EventType, UNSTABLE_MSC3089_TREE_SUBTYPE } from 'matrix-js-sdk/lib/@types/event'; 21 | import { SyncState } from 'matrix-js-sdk/lib/sync'; 22 | import type { IFolderEntry, IEntry, FolderRole, MatrixFilesID, ArrayBufferBlob, IFolderMembership } from '.'; 23 | import { AbstractFolderEntry } from './AbstractFolderEntry'; 24 | import { TreeSpaceEntry } from './TreeSpaceEntry'; 25 | import promiseRetry from 'p-retry'; 26 | import { IMatrixFiles } from './IMatrixFiles'; 27 | import { IPendingEntry } from './IPendingEntry'; 28 | 29 | export class MatrixFiles extends AbstractFolderEntry implements IMatrixFiles { 30 | constructor(public client: MatrixClient) { 31 | super(client, undefined, 'MatrixFiles'); 32 | super.setEventHandlers({ 33 | 'Room': this.newRoom, 34 | 'RoomState.events': this.roomState, 35 | 'Room.myMembership': this.roomMyMembership, 36 | }); 37 | } 38 | 39 | get id(): MatrixFilesID { 40 | return this.client.getUserId() ?? ''; 41 | } 42 | 43 | isFolder = true; 44 | 45 | getClient() { 46 | return this.client; 47 | } 48 | 49 | get path() { 50 | return []; 51 | } 52 | 53 | get name() { 54 | return ''; 55 | } 56 | 57 | // top-level is writable, but only for folders 58 | writable = true; 59 | 60 | async getCreatedByUserId() { 61 | return ''; 62 | } 63 | 64 | async getChildren(): Promise { 65 | return this.client.getVisibleRooms() 66 | .map(r => this.client.unstableGetFileTreeSpace(r.roomId)) 67 | .filter(r => !!r && !r.room.currentState.getStateEvents(EventType.SpaceParent)?.length) 68 | .map(r => new TreeSpaceEntry(this, this, r!)); 69 | } 70 | 71 | async addChildFolder(name: string): Promise { 72 | const w = await this.client.unstableCreateFileTree(name); 73 | return w.id; 74 | } 75 | 76 | async getCreationDate() { 77 | return undefined; 78 | } 79 | 80 | async getLastModifiedDate() { 81 | return undefined; 82 | } 83 | 84 | async delete() { 85 | this.trace('delete', this.path.join('/')); 86 | throw new Error('Cannot remove root'); 87 | } 88 | 89 | async rename(name: string) { 90 | this.trace('rename', `${this.path.join('/')} to ${name}`); 91 | throw new Error('Cannot rename root'); 92 | } 93 | 94 | async copyTo(resolvedParent: IFolderEntry, fileName: string): Promise { 95 | this.trace('copyTo', `${this.path.join('/')} to ${resolvedParent.path.join('/')}/${fileName}`); 96 | throw new Error('Cannot copy root'); 97 | } 98 | 99 | async moveTo(resolvedParent: IFolderEntry, fileName: string): Promise { 100 | this.trace('moveTo', `${this.path.join('/')} to ${resolvedParent.path.join('/')}/${fileName}`); 101 | throw new Error('Cannot move root'); 102 | } 103 | 104 | async addFile(name: string, file: ArrayBufferBlob): Promise { 105 | this.trace('addFile', `${this.path.join('/')} with name ${name}`); 106 | throw new Error('Cannot add file at root'); 107 | } 108 | 109 | // eslint-disable-next-line @typescript-eslint/naming-convention 110 | private async _resolvePath(path: string[]): Promise { 111 | if (path.length === 0) { 112 | return this; 113 | } 114 | 115 | // eslint-disable-next-line @typescript-eslint/no-this-alias 116 | let next: IFolderEntry | undefined = this; 117 | const paths = path.slice(); // make a clone as we are modifying the array 118 | 119 | while (next) { 120 | const matching = (await next.getChildren()).find(x => x.name === paths[0]); 121 | if (!matching) { 122 | next = undefined; 123 | } else { 124 | if (paths.length === 1) { 125 | return matching; 126 | } 127 | 128 | if (!matching.isFolder) { 129 | next = undefined; 130 | } else { 131 | paths.shift(); 132 | next = matching as IFolderEntry; 133 | } 134 | } 135 | } 136 | 137 | // not found 138 | return undefined; 139 | } 140 | 141 | async resolvePath(path: string[]): Promise { 142 | const result = await this._resolvePath(path); 143 | const status = result ? (result.isFolder ? 'folder' : 'file') : 'not found'; 144 | this.trace('resolvePath', `${path.join('/')} was ${status}`); 145 | return result; 146 | } 147 | 148 | async sync(): Promise { 149 | this.trace('sync'); 150 | const opts: IStartClientOpts = { 151 | pendingEventOrdering: PendingEventOrdering.Detached, 152 | lazyLoadMembers: true, 153 | initialSyncLimit: 0, 154 | // clientWellKnownPollPeriod: 2 * 60 * 60, // 2 hours 155 | }; 156 | 157 | // We delay the ready state until after the first sync has completed 158 | const readyPromise = new Promise ((resolve) => { 159 | const fn = (newState: SyncState, oldState: SyncState | null) => { 160 | if (newState === SyncState.Syncing && oldState === SyncState.Prepared) { 161 | resolve(); 162 | this.client.off(ClientEvent.Sync, fn.bind(this)); 163 | } 164 | }; 165 | this.client.on(ClientEvent.Sync, fn.bind(this)); 166 | }); 167 | return this.client.startClient(opts).then(async () => readyPromise); 168 | } 169 | 170 | async logout(): Promise { 171 | this.trace('logout'); 172 | await this.client.logout(); 173 | this.client.stopClient(); 174 | } 175 | 176 | async deactivate(erase = true): Promise { 177 | this.trace('deactivate'); 178 | this.client.stopClient(); 179 | await this.client.deactivateAccount(undefined, erase); 180 | } 181 | 182 | async getPendingInvites(): Promise { 183 | return this.client.getRooms().filter(r => r.getMyMembership() === 'invite'); 184 | } 185 | 186 | async acceptInvite(invite: Room): Promise { 187 | await this.client.joinRoom(invite.roomId); 188 | } 189 | 190 | /** 191 | * Accepts all pending invites. Note that this can cause stacked requests if called 192 | * multiple times. Joins will be re-tried forever, or until the runtime is interrupted. 193 | * @returns {Promise} Resolves when complete. 194 | */ 195 | async acceptAllInvites(): Promise { 196 | const invites = this.client.getRooms().filter(r => r.getMyMembership() === 'invite'); 197 | return Promise.all(invites.map(async (r) => this.retryJoin(r))).then(); // .then() to coerce types 198 | } 199 | 200 | private async retryJoin(room: Room): Promise { 201 | return simpleRetryOperation(async () => { 202 | const domain = this.client.getDomain(); 203 | const opts = domain ? { viaServers: [domain] } : {}; 204 | return this.client.joinRoom(room.roomId, opts).catch(e => { 205 | if (e?.errcode === 'M_FORBIDDEN') { 206 | throw new promiseRetry.AbortError(e); 207 | } 208 | throw e; 209 | }); 210 | }); 211 | } 212 | 213 | get members(): IFolderMembership[] { 214 | throw new Error('Function not available on root.'); 215 | } 216 | 217 | async inviteMember(userId: string, role: FolderRole): Promise { 218 | throw new Error('Function not available on root.'); 219 | } 220 | 221 | async removeMember(userId: string): Promise { 222 | throw new Error('Function not available on root.'); 223 | } 224 | 225 | async setMemberLevel(userId: string, role: FolderRole): Promise { 226 | throw new Error('Function not available on root.'); 227 | } 228 | 229 | getMembership(userId: string): IFolderMembership { 230 | throw new Error('Function not available on root.'); 231 | } 232 | 233 | get ownMembership(): IFolderMembership { 234 | throw new Error('Function not available on root.'); 235 | } 236 | 237 | async setMemberRole(userId: string, role: FolderRole): Promise { 238 | throw new Error('Function not available on root.'); 239 | } 240 | 241 | private newRoom(r: Room) { 242 | if (r.getMyMembership() === 'invite' && r.getType() === UNSTABLE_MSC3089_TREE_SUBTYPE.unstable) { 243 | this.emit('invite', this, r); 244 | } else { 245 | // TODO: maybe we don't need this as we are monitoring roomstate? 246 | // this might not be a top-level folder, but we can't tell at this point 247 | this.emit('modified', this, r); 248 | } 249 | } 250 | 251 | private roomState(e: MatrixEvent, s: RoomState) { 252 | // TODO: don't emit events that are for sub folders 253 | this.emit('modified', this, e); 254 | } 255 | 256 | private roomMyMembership(r: Room, membership: string, prevMembership: string) { 257 | // TODO: don't emit events that are for sub folders? 258 | if (membership === 'leave') { 259 | this.emit('modified', this, r); 260 | } 261 | } 262 | 263 | private pendingEntries: (IPendingEntry | IEntry)[] = []; 264 | 265 | public addPendingEntry(entry: IPendingEntry | IEntry): void { 266 | if ('replacesEntry' in entry && entry.replacesEntry) { 267 | this.trace( 268 | 'addPendingEntry', 269 | // eslint-disable-next-line max-len 270 | `${entry.id} with parent ${entry.parent?.id} replaces ${entry.replacesEntry.id} for ${entry.path.join('/')}`, 271 | ); 272 | this.clearPendingEntry(entry.replacesEntry); 273 | } else { 274 | this.trace('addPendingEntry', `${entry.id} with parent ${entry.parent?.id} for ${entry.path.join('/')}`); 275 | } 276 | this.pendingEntries.push(entry); 277 | } 278 | 279 | public clearPendingEntry(entry: IPendingEntry | IEntry): void { 280 | this.trace('clearPendingEntry', entry.id); 281 | this.pendingEntries = this.pendingEntries.filter(x => x.id !== entry.id); 282 | } 283 | 284 | public getPendingEntries(parent: IFolderEntry): (IPendingEntry | IEntry)[] { 285 | const entries = this.pendingEntries.filter(x => x.parent?.id === parent.id); 286 | this.trace('getPendingEntries', `${parent.id} = ${entries.length} of ${this.pendingEntries.length}`); 287 | return entries; 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/PendingBranchEntry.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { encryptAttachment } from 'matrix-encrypt-attachment'; 18 | import { 19 | FileType, IContent, IEncryptedFile, ISendEventResponse, RelationType, UNSTABLE_MSC3089_BRANCH, 20 | } from 'matrix-js-sdk/lib'; 21 | import type { MatrixFilesID, FileEncryptionStatus, IFileEntry, IFolderEntry, MatrixFiles, TreeSpaceEntry } from '.'; 22 | import { ArrayBufferBlob } from './ArrayBufferBlob'; 23 | import { AutoBindingEmitter } from './AutoBindingEmitter'; 24 | import { IPendingEntry } from './IPendingEntry'; 25 | 26 | export class PendingBranchEntry extends AutoBindingEmitter implements IFileEntry, IPendingEntry { 27 | private date: Date = new Date(); 28 | constructor( 29 | private files: MatrixFiles, 30 | public parent: TreeSpaceEntry, 31 | private privateId: string, 32 | public name: string, 33 | public replacesEntry: IFileEntry | undefined, 34 | private blob: ArrayBufferBlob, 35 | public encryptionStatus: FileEncryptionStatus, 36 | private createdByUserId: string, 37 | private indexEventContent: IContent, 38 | ) { 39 | super(files.client, () => `PendingBranchEntry(${this.id})`); 40 | this.sentPromise = new Promise((resolve) => { 41 | this.resolve = resolve; 42 | }); 43 | this.version = indexEventContent.version; 44 | } 45 | 46 | public version: number; 47 | 48 | private sentPromise: Promise; 49 | private sent = false; 50 | private resolve!: (id: MatrixFilesID) => void; 51 | 52 | get id() { 53 | return this.privateId; 54 | } 55 | 56 | setSent(id: string) { 57 | if (this.sent) { 58 | throw new Error('Event has already been marked as sent'); 59 | } 60 | this.trace('setSent', `Real ID is ${id}`); 61 | this.privateId = id; 62 | this.sent = true; 63 | this.resolve(id); 64 | } 65 | 66 | async getCreatedByUserId() { 67 | return this.createdByUserId; 68 | } 69 | 70 | async getVersionHistory(): Promise { 71 | return [this]; 72 | } 73 | 74 | isFolder = false; 75 | 76 | get path() { 77 | return [...this.parent.path, this.name]; 78 | } 79 | 80 | get writable() { 81 | return this.parent?.writable ?? false; 82 | } 83 | 84 | async getCreationDate() { 85 | return this.date; 86 | } 87 | 88 | async getLastModifiedDate() { 89 | return this.date; 90 | } 91 | 92 | private async waitForRealEntry() { 93 | this.trace('waitForRealEntry'); 94 | await this.sentPromise; 95 | return this.files.getDescendantById(this.id); 96 | } 97 | 98 | async delete() { 99 | this.trace('delete', this.path.join('/')); 100 | const real = await this.waitForRealEntry(); 101 | if (!real) { 102 | throw new Error('Unable to delete pending branch'); 103 | } 104 | return real.delete(); 105 | } 106 | 107 | async rename(name: string) { 108 | this.trace('rename', `${this.path.join('/')} to ${name}`); 109 | const real = await this.waitForRealEntry(); 110 | if (!real) { 111 | throw new Error('Unable to rename pending branch'); 112 | } 113 | return real.rename(name); 114 | } 115 | 116 | async copyAsVersion(fileTo: IFileEntry): Promise { 117 | this.trace('copyAsVersion', `${this.path.join('/')} to ${fileTo.path.join('/')}`); 118 | const real = await this.waitForRealEntry(); 119 | if (!real) { 120 | throw new Error('Unable to copy pending branch'); 121 | } 122 | return (real as IFileEntry).copyAsVersion(fileTo); 123 | } 124 | 125 | async copyTo(resolvedParent: IFolderEntry, fileName: string): Promise { 126 | this.trace('copyTo', `${this.path.join('/')} to ${resolvedParent.path.join('/')}/${fileName}`); 127 | const real = await this.waitForRealEntry(); 128 | if (!real) { 129 | throw new Error('Unable to copy pending branch'); 130 | } 131 | return this.copyTo(resolvedParent, fileName); 132 | } 133 | 134 | async moveTo(resolvedParent: IFolderEntry, fileName: string): Promise { 135 | this.trace('moveTo', `${this.path.join('/')} to ${resolvedParent.path.join('/')}/${fileName}`); 136 | const real = await this.waitForRealEntry(); 137 | if (!real) { 138 | throw new Error('Unable to move pending branch'); 139 | } 140 | return this.moveTo(resolvedParent, fileName); 141 | } 142 | 143 | private async createNewVersion( 144 | name: string, 145 | encryptedContents: FileType, 146 | info: Partial, 147 | additionalContent?: IContent, 148 | ): Promise { 149 | const fileEventResponse = await this.parent.treespace.createFile(name, encryptedContents, info, { 150 | ...(additionalContent ?? {}), 151 | 'm.new_content': true, 152 | 'm.relates_to': { 153 | 'rel_type': RelationType.Replace, 154 | 'event_id': this.id, 155 | }, 156 | }); 157 | 158 | // Update the version of the new event 159 | await this.files.client.sendStateEvent(this.parent.id, UNSTABLE_MSC3089_BRANCH.name, { 160 | active: true, 161 | name: name, 162 | version: this.version + 1, 163 | }, fileEventResponse['event_id']); 164 | 165 | // Deprecate ourselves 166 | await this.files.client.sendStateEvent(this.parent.id, UNSTABLE_MSC3089_BRANCH.name, { 167 | ...this.indexEventContent, 168 | active: false, 169 | }, this.id); 170 | 171 | return fileEventResponse; 172 | } 173 | 174 | async addVersion(file: ArrayBufferBlob, newName?: string): Promise { 175 | const name = newName ?? this.name; 176 | this.trace('addVersion', `${this.path.join('/')} with name ${name}`); 177 | 178 | if (!this.sent) { 179 | this.trace('addVersion', `Current version ${this.version} with ID ${this.id} not sent so waiting`); 180 | await this.sentPromise; 181 | this.trace('addVersion', `Current version ${this.version} now sent with ID ${this.id}`); 182 | } 183 | 184 | // because adding a file/version is not atomic we add a placeholder pending entry to prevent concurrent adds: 185 | const newEntry = new PendingBranchEntry( 186 | this.files, 187 | this.parent, 188 | `(pending_version_for_${this.id}@${Date.now()})`, 189 | name, 190 | this, 191 | file, 192 | this.files.client.isRoomEncrypted(this.id) ? 'decrypted' : 'encryptionNotEnabled', 193 | this.files.client.getUserId()!, 194 | { 195 | ...this.indexEventContent, 196 | version: this.version + 1, 197 | }, 198 | ); 199 | 200 | this.files.addPendingEntry(newEntry); 201 | 202 | const { 203 | mimetype, 204 | size, 205 | data, 206 | } = file; 207 | const encrypted = await encryptAttachment(data); 208 | 209 | const { event_id: id } = await this.createNewVersion( 210 | name, 211 | Buffer.from(encrypted.data), 212 | encrypted.info, 213 | { 214 | info: { 215 | mimetype, 216 | size, 217 | }, 218 | }, 219 | ); 220 | 221 | // set the real ID when known 222 | newEntry.setSent(id); 223 | 224 | return id; 225 | } 226 | 227 | async getBlob(): Promise { 228 | return this.blob; 229 | } 230 | 231 | async getSize(): Promise { 232 | return this.blob.size; 233 | } 234 | 235 | locked = false; 236 | 237 | async setLocked(locked: boolean): Promise { 238 | const real = await this.waitForRealEntry(); 239 | if (!real) { 240 | throw new Error('Unable to lock pending branch'); 241 | } 242 | return (real as IFileEntry).setLocked(locked); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/TreeSpaceEntry.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { EventType, UNSTABLE_MSC3089_BRANCH } from 'matrix-js-sdk/lib/@types/event'; 18 | import type { MSC3089TreeSpace } from 'matrix-js-sdk/lib/models/MSC3089TreeSpace'; 19 | import type { MatrixEvent, Room, RoomMember, RoomState } from 'matrix-js-sdk/lib'; 20 | import { encryptAttachment } from 'matrix-encrypt-attachment'; 21 | import { 22 | IFolderEntry, IFileEntry, IEntry, MatrixFiles, FolderRole, 23 | MatrixFilesID, IFolderMembership, ArrayBufferBlob, 24 | } from '.'; 25 | import { BranchEntry } from './BranchEntry'; 26 | import { AbstractFolderEntry } from './AbstractFolderEntry'; 27 | import { TreeSpaceMembership } from './TreeSpaceMembership'; 28 | import { PendingBranchEntry } from './PendingBranchEntry'; 29 | 30 | export class TreeSpaceEntry extends AbstractFolderEntry { 31 | constructor(private files: MatrixFiles, parent: IFolderEntry, public treespace: MSC3089TreeSpace) { 32 | super(files.client, parent, `TreeSpaceEntry(${treespace.id})`); 33 | super.setEventHandlers({ 34 | 'Room.name': this.nameChanged, 35 | 'Room.timeline': this.timelineChanged, 36 | 'RoomState.events': this.roomState, 37 | }); 38 | } 39 | 40 | get id(): string { 41 | return this.treespace.roomId; 42 | } 43 | 44 | isFolder = true; 45 | 46 | get writable() { 47 | return this.ownMembership.canWrite; 48 | } 49 | 50 | get name(): string { 51 | return this.treespace.room.name; 52 | } 53 | 54 | async getCreatedByUserId(): Promise { 55 | return this.createEvent?.sender?.userId; 56 | } 57 | 58 | async getChildren(): Promise { 59 | let children: IEntry[] = await Promise.resolve([ 60 | ...this.treespace.getDirectories().map(d => new TreeSpaceEntry(this.files, this, d)), 61 | ...this.treespace.listFiles().map(f => new BranchEntry(this.files, this, f)), 62 | ]); 63 | const pending = this.files.getPendingEntries(this); 64 | 65 | // adding pending if not already present 66 | pending.forEach((p) => { 67 | if (!children.find(c => c.id === p.id)) { 68 | children.push(p); 69 | // if this pending entry replaces a previous one then remove the previous one from the list 70 | if ('replacesEntry' in p) { 71 | if (p.replacesEntry) { 72 | children = children.filter(x => x.id !== p.replacesEntry?.id); 73 | } 74 | } 75 | } else { 76 | this.files.clearPendingEntry(p); 77 | } 78 | }); 79 | 80 | return children; 81 | } 82 | 83 | async getChildByName(name: string): Promise { 84 | return (await this.getChildren()).find(x => x.name === name); 85 | } 86 | 87 | async getChildById(id: MatrixFilesID): Promise { 88 | return (await this.getChildren()).find(x => x.id === id); 89 | } 90 | 91 | async addChildFolder(name: string): Promise { 92 | this.trace(`addChildFolder() ${this.path.join('/')} with name ${name}`); 93 | const d = await this.treespace.createDirectory(name); 94 | this.files.addPendingEntry(new TreeSpaceEntry(this.files, this, d)); 95 | return d.id; 96 | } 97 | 98 | private get createEvent() { 99 | return this.treespace.room.currentState.getStateEvents(EventType.RoomCreate, ''); 100 | } 101 | 102 | async getCreationDate() { 103 | const createEvent = this.createEvent; 104 | return createEvent ? new Date(createEvent.getTs()) : undefined; 105 | } 106 | 107 | async getLastModifiedDate() { 108 | const ts = this.treespace.room.getLastActiveTimestamp(); 109 | return ts > 0 ? new Date(ts) : undefined; 110 | } 111 | 112 | async delete() { 113 | this.trace('delete', this.path.join('/')); 114 | await this.treespace.delete(); 115 | this.emitModified(this.treespace.room); 116 | } 117 | 118 | async rename(name: string) { 119 | this.trace('rename', `${this.path.join('/')} to ${name}`); 120 | return this.treespace.setName(name); 121 | } 122 | 123 | async copyTo(resolvedParent: IFolderEntry, fileName: string): Promise { 124 | this.trace('copyTo', `${this.path.join('/')} to ${resolvedParent.path.join('/')}/${fileName}`); 125 | // TODO: Implement this 126 | throw new Error('Not implemented'); 127 | } 128 | 129 | async moveTo(resolvedParent: IFolderEntry, fileName: string): Promise { 130 | this.trace('moveTo', `${this.path.join('/')} to ${resolvedParent.path.join('/')}/${fileName}`); 131 | 132 | // simple rename? 133 | if (resolvedParent.id === this.parent?.id) { 134 | await this.rename(fileName); 135 | return this.id; 136 | } 137 | 138 | // TODO: Implement this 139 | throw new Error('Not implemented'); 140 | } 141 | 142 | async addFile(name: string, file: ArrayBufferBlob): Promise { 143 | this.trace('addFile', `${this.path.join('/')} with name ${name}`); 144 | const existing = await this.getChildByName(name); 145 | if (existing && existing.isFolder) { 146 | throw new Error('A folder with that name already exists'); 147 | } 148 | if (existing) { 149 | this.trace('addFile', `Found existing entry for file: ${existing.id}`); 150 | const existingFile = existing as IFileEntry; 151 | await existingFile.addVersion(file); 152 | return existingFile.id; 153 | } 154 | 155 | // because adding a file/version is not atomic we add a placeholder pending entry to prevent concurrent adds: 156 | const newEntry = new PendingBranchEntry( 157 | this.files, 158 | this, 159 | `(pending_file_for_${this.id}@${Date.now()})`, 160 | name, 161 | undefined, 162 | file, 163 | this.files.client.isRoomEncrypted(this.id) ? 'decrypted' : 'encryptionNotEnabled', 164 | this.files.client.getUserId()!, 165 | { // TODO: this is a hack and really we should get directory.createFile() to return the index event content for caching 166 | name, 167 | active: true, 168 | version: 1, 169 | }, 170 | ); 171 | 172 | this.files.addPendingEntry(newEntry); 173 | 174 | const directory = this.treespace; 175 | 176 | const { 177 | mimetype, 178 | size, 179 | data, 180 | } = file; 181 | 182 | const encrypted = await encryptAttachment(data); 183 | 184 | const { 185 | event_id: id, 186 | } = await directory.createFile(name, Buffer.from(encrypted.data), encrypted.info, { 187 | info: { 188 | mimetype, 189 | size, 190 | }, 191 | }); 192 | 193 | newEntry.setSent(id); 194 | 195 | return id; 196 | } 197 | 198 | private mapMember(m: RoomMember) { 199 | return new TreeSpaceMembership(this.files, this.treespace, m); 200 | } 201 | 202 | get members(): IFolderMembership[] { 203 | const ms: RoomMember[] = this.treespace.room.getMembers(); 204 | return ms.map(m => this.mapMember(m)); 205 | } 206 | 207 | getMembership(userId: string): IFolderMembership { 208 | const m: RoomMember | null = this.treespace.room.getMember(userId); 209 | if (!m) { 210 | throw new Error('Not a member'); 211 | } 212 | return this.mapMember(m); 213 | } 214 | 215 | async inviteMember(userId: string, role: FolderRole): Promise { 216 | await this.treespace.invite(userId); 217 | return this.getMembership(userId); 218 | } 219 | 220 | async removeMember(userId: string): Promise { 221 | await this.files.client.kick(this.treespace.roomId, userId); 222 | } 223 | 224 | async setMemberRole(userId: string, role: FolderRole): Promise { 225 | await this.treespace.setPermissions(userId, role); 226 | return this.getMembership(userId); 227 | } 228 | 229 | get ownMembership(): IFolderMembership { 230 | return this.getMembership(this.files.getClient().getUserId()!); 231 | } 232 | 233 | private emitModified(e: MatrixEvent | Room) { 234 | this.emit('modified', e); 235 | const parent = this.parent; 236 | if (parent) { 237 | parent.emit('modified', this, e); 238 | } 239 | } 240 | 241 | nameChanged(r: Room) { 242 | this.trace('event(nameChanged)', `room ${r.roomId} to ${r.name}`); 243 | if (r.roomId === this.id) { 244 | this.emitModified(r); 245 | } else { 246 | // see if the room is a child folder/space of us 247 | const parents = r.currentState.getStateEvents(EventType.SpaceParent); 248 | if (parents.length > 0 && parents[0].getStateKey() === this.id) { 249 | this.emitModified(r); 250 | } 251 | } 252 | } 253 | 254 | timelineChanged(e: MatrixEvent, r: Room) { 255 | this.trace('event(timelineChanged)', `room ${r.roomId} type ${e.getType()}`); 256 | if (r.roomId === this.id && e.getType() === UNSTABLE_MSC3089_BRANCH.name) { 257 | // look for new branch entry 258 | this.emitModified(e); 259 | } 260 | } 261 | 262 | roomState(e: MatrixEvent, s: RoomState) { 263 | this.trace('event(roomState)', `room ${s.roomId} type ${e.getType()}`); 264 | // new child: 265 | if (s.roomId === this.id && e.getType() === EventType.SpaceChild) { 266 | this.emitModified(e); 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/TreeSpaceMembership.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import type { RoomMember } from 'matrix-js-sdk/lib'; 18 | import { EventType } from 'matrix-js-sdk/lib/@types/event'; 19 | import { MSC3089TreeSpace, TreePermissions } from 'matrix-js-sdk/lib/models/MSC3089TreeSpace'; 20 | import EventEmitter from 'events'; 21 | import { IFolderMembership, MatrixFiles } from '.'; 22 | 23 | export class TreeSpaceMembership extends EventEmitter implements IFolderMembership { 24 | constructor(private files: MatrixFiles, private treespace: MSC3089TreeSpace, private roomMember: RoomMember) { 25 | super(); 26 | } 27 | 28 | get userId() { 29 | return this.roomMember.userId; 30 | } 31 | 32 | get role() { 33 | return this.treespace.getPermissions(this.roomMember.userId); 34 | } 35 | 36 | since = new Date(); // FIXME implement this 37 | 38 | get canInvite() { 39 | return this.treespace.room.canInvite(this.files.getClient().getUserId()!); 40 | } 41 | 42 | get canRemove() { 43 | return this.treespace.room.currentState.getStateEvents( 44 | EventType.RoomPowerLevels, '')?.getContent().kick <= (this.roomMember.powerLevel ?? 0); 45 | } 46 | 47 | get canManageRoles() { 48 | return this.treespace.room.currentState.maySendStateEvent( 49 | EventType.RoomPowerLevels, this.files.getClient().getUserId()!); 50 | } 51 | 52 | get canWrite() { 53 | return [TreePermissions.Editor, TreePermissions.Owner].includes(this.treespace.getPermissions(this.userId)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021-2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | export { TreePermissions as FolderRole } from 'matrix-js-sdk/lib/models/MSC3089TreeSpace'; 18 | 19 | export * from './IEntry'; 20 | export * from './IFileEntry'; 21 | export * from './IFolderEntry'; 22 | export * from './IFolderMembership'; 23 | export * from './MatrixFiles'; 24 | export * from './TreeSpaceEntry'; 25 | export * from './BranchEntry'; 26 | export * from './ArrayBufferBlob'; 27 | export * from './IMatrixFiles'; 28 | -------------------------------------------------------------------------------- /src/log.ts: -------------------------------------------------------------------------------- 1 | import { getLogger } from '@log4js-node/log4js-api'; 2 | 3 | export const log = getLogger('MatrixFilesSDK'); 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "lib": ["ES2021", "DOM"], 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "noImplicitAny": true, 11 | "noImplicitThis": true, 12 | "esModuleInterop": true, 13 | "strictNullChecks": true, 14 | "noImplicitReturns": true, 15 | "preserveConstEnums": true, 16 | "suppressImplicitAnyIndexErrors": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "outDir": "./dist", 19 | "rootDir": "./src" 20 | } 21 | } 22 | --------------------------------------------------------------------------------