├── .codeclimate.yml ├── .editorconfig ├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── angular.json ├── browserslist ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── package-lock.json ├── package.json ├── prettier.config.js ├── projects ├── ngx-flow-demo │ ├── src │ │ ├── app │ │ │ ├── app.component.css │ │ │ ├── app.component.html │ │ │ └── app.component.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ └── styles.css │ ├── tsconfig.app.json │ └── tsconfig.spec.json └── ngx-flow │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── ng-package.prod.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── button.directive.spec.ts │ │ ├── button.directive.ts │ │ ├── drop.directive.spec.ts │ │ ├── drop.directive.ts │ │ ├── flow-constructor.ts │ │ ├── flow-injection-token.ts │ │ ├── flow.directive.spec.ts │ │ ├── flow.directive.ts │ │ ├── helpers │ │ │ ├── flow-file-to-transfer.ts │ │ │ └── tests │ │ │ │ ├── flow-file-mock-factory.ts │ │ │ │ ├── flow-mock.ts │ │ │ │ └── transfer-mock-factory.ts │ │ ├── ngx-flow.module.ts │ │ ├── src.directive.spec.ts │ │ ├── src.directive.ts │ │ ├── transfer.ts │ │ └── upload-state.ts │ ├── public-api.ts │ └── typings.d.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ └── tslint.json ├── server ├── README.md ├── app.js ├── flow-node.js ├── package-lock.json ├── package.json ├── public │ └── flow.js └── tmp │ └── .gitignore ├── tsconfig.json └── tslint.json /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | exclude_patterns: 2 | - /server/** 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 20 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 16 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm run build:lib 32 | - run: cd dist/ngx-flow 33 | - run: npm publish 34 | env: 35 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | node_modules/ 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .history/* 32 | 33 | # misc 34 | /.angular/cache 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'lts/*' 4 | sudo: required 5 | env: 6 | global: 7 | - CC_TEST_REPORTER_ID=3868b078bd767a7c54dbb77c6b7e427ef0da3421dae1b643bfd0b41106cd4774 8 | before_script: 9 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 10 | - chmod +x ./cc-test-reporter 11 | - ./cc-test-reporter before-build 12 | addons: 13 | chrome: stable 14 | cache: 15 | directories: 16 | - node_modules 17 | script: 18 | - npm run test:ci 19 | after_script: 20 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 HTML5 File upload 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NgxFlow 2 | 3 | [![Build Status](https://travis-ci.com/flowjs/ngx-flow.svg?branch=master)](https://travis-ci.com/flowjs/ngx-flow) 4 | [![Test Coverage](https://api.codeclimate.com/v1/badges/29153dcefffff1fe5a5c/test_coverage)](https://codeclimate.com/github/flowjs/ngx-flow/test_coverage) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/29153dcefffff1fe5a5c/maintainability)](https://codeclimate.com/github/flowjs/ngx-flow/maintainability) 6 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 7 | 8 | The purpose of this package is to create a wrapper for Angular for fileupload using [flow.js](https://github.com/flowjs/flow.js). 9 | 10 | ## Demo 11 | 12 | [https://stackblitz.com/edit/ngx-flow-example](https://stackblitz.com/edit/ngx-flow-example) 13 | 14 | You can also find example source code in the `src` folder. 15 | 16 | ## Roadmap 17 | 18 | - ✅ upload single file 19 | - ✅ upload multiple files 20 | - ✅ queue management 21 | - ✅ error handling 22 | - ✅ pause / resume upload 23 | - ✅ cancel upload, cancel all uploads 24 | - ✅ upload progress 25 | - ✅ file / directory restrictions 26 | - ✅ drag & drop 27 | - ✅ display uploaded image 28 | - ✅ tests 29 | - ✅ upload right after selecting file 30 | - ✅ run tests using TravisCI 31 | - ✅ demo using Stackblitz 32 | - ✅ support for server side rendering 33 | 34 | ## Compatibility 35 | 36 | | Angular | @flowjs/ngx-flow | 37 | | :-----: | :--------------: | 38 | | 19 | ^19.0.0 | 39 | | 18 | ^18.0.0 | 40 | | 17 | 0.8.1 | 41 | | 16 | 0.7.2 | 42 | | 15 | \- | 43 | | 14 | 0.6.0 | 44 | | 13 | 0.5.0 | 45 | | 12 | \- | 46 | | 6 -> 11 | 0.4.6 | 47 | 48 | 49 | ## Install 50 | 51 | `npm install @flowjs/flow.js @flowjs/ngx-flow` 52 | 53 | Import in your module: 54 | 55 | ```typescript 56 | import { NgxFlowModule, FlowInjectionToken } from '@flowjs/ngx-flow'; 57 | import Flow from '@flowjs/flow.js'; 58 | 59 | @NgModule({ 60 | imports: [NgxFlowModule], 61 | providers: [ 62 | { 63 | provide: FlowInjectionToken, 64 | useValue: Flow 65 | } 66 | ] 67 | }) 68 | export class AppModule 69 | ``` 70 | 71 | We use dependecy injection to provide flow.js library. 72 | 73 | ## How to use 74 | 75 | 1. Start up server. There is a server example taken from [flow.js](https://github.com/flowjs/flow.js) here in `server` directory. In this repo you can run it using `npm run server`. 76 | 77 | 1. First you need to initialize ngx-flow directive and export it as for example `flow` variable: 78 | 79 | ```html 80 | 81 | ``` 82 | 83 | 1. Now you can use either directive `flowButton` for select file dialog: 84 | 85 | ```html 86 | 87 | ``` 88 | 89 | Or `flowDrop` for drag&drop feature: 90 | 91 | ```html 92 |
93 | ``` 94 | 95 | For both you have to pass `[flow]=flow.flowJs` where `flow` is template variable exported in step 1. 96 | 97 | 1. You can than subscribe to observable of transfers: 98 | 99 | ```html 100 |
101 | ``` 102 | 103 | 1. After adding files you can begin upload using `upload()` method: 104 | 105 | ```html 106 | 107 | ``` 108 | 109 | ### How does `transfers$` data look like? 110 | 111 | Observable `flow.transfers$` emits state in form: 112 | 113 | ```typescript 114 | { 115 | totalProgress: 0.5, 116 | transfers: [ 117 | { 118 | name: "somefile.txt", 119 | flowFile: FlowFile, 120 | progress: number, 121 | error: boolean, 122 | paused: boolean, 123 | success: boolean, 124 | complete: boolean, 125 | currentSpeed: number, 126 | averageSpeed: number, 127 | size: number, 128 | timeRemaining: number, 129 | }, 130 | { 131 | name: "uploading.txt", 132 | flowFile: FlowFile, 133 | progress: 0.5, 134 | error: false, 135 | paused: false, 136 | success: false, 137 | complete: false, 138 | currentSpeed: number, 139 | averageSpeed: number, 140 | size: number, 141 | timeRemaining: number, 142 | }, 143 | { 144 | name: "failed-to-upload.txt", 145 | flowFile: FlowFile, 146 | progress: number, 147 | error: true, 148 | paused: false, 149 | success: false, 150 | complete: true, 151 | currentSpeed: number, 152 | averageSpeed: number, 153 | size: number, 154 | timeRemaining: number, 155 | } 156 | { 157 | name: "uploaded.txt", 158 | flowFile: FlowFile, 159 | progress: number, 160 | error: false, 161 | paused: false, 162 | success: true, 163 | complete: true, 164 | currentSpeed: number, 165 | averageSpeed: number, 166 | size: number, 167 | timeRemaining: number, 168 | } 169 | ], 170 | flow: { /* flow.js instance*/ } 171 | } 172 | ``` 173 | 174 | ## FAQ 175 | 176 | ### I need access to flow.js object 177 | 178 | You can find it under `flow` variable. 179 | 180 | ```html 181 |

Is flowjs supported by the browser? {{flow.flowJs?.support}}

182 | ``` 183 | 184 | ### I see flickering when upload is in progress 185 | 186 | Use `trackBy` for `ngFor`: 187 | 188 | ```html 189 |
190 | ``` 191 | 192 | ```typescript 193 | export class AppComponent { 194 | trackTransfer(transfer: Transfer) { 195 | return transfer.id; 196 | } 197 | } 198 | ``` 199 | 200 | ### I need just a single file 201 | 202 | Add `singleFile: true` to your flow config: 203 | 204 | ```html 205 | 206 | ``` 207 | 208 | ### I want to upload whole directory 209 | 210 | Add `flowDirectoryOnly="true"` to your button: 211 | 212 | ```html 213 | 214 | ``` 215 | 216 | ### I want to display image which is going to be uploaded 217 | 218 | Use directive `flowSrc`: 219 | 220 | ```html 221 |
222 | 223 |
224 | ``` 225 | 226 | ### How to trigger upload right after picking the file? 227 | 228 | Subscribe to `events$`. NgxFlow listens for these events: `filesSubmitted`, `fileRemoved`, `fileRetry`, `fileProgress`, `fileSuccess`, `fileError` of flow.js. Event `fileSubmitted` is fired when user drops or selects a file. 229 | 230 | ```typescript 231 | export class AppComponent implements AfterViewInit, OnDestroy { 232 | @ViewChild('flow') 233 | flow: FlowDirective; 234 | 235 | autoUploadSubscription: Subscription; 236 | 237 | ngAfterViewInit() { 238 | this.autoUploadSubscription = this.flow.events$.subscribe((event) => { 239 | if (event.type === 'filesSubmitted') { 240 | this.flow.upload(); 241 | } 242 | }); 243 | } 244 | 245 | ngOnDestroy() { 246 | this.autoUploadSubscription.unsubscribe(); 247 | } 248 | } 249 | ``` 250 | 251 | ### Development 252 | 253 | `npm run build:lib` - builds the library into dist folder 254 | 255 | After that you can publish to npm repository from `dist` folder: 256 | 257 | ``` 258 | cd dist/ngx-flow 259 | npm publish 260 | ``` 261 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": "87900679-5162-4f8c-a537-fe716f600189" 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "ngx-flow": { 10 | "projectType": "library", 11 | "root": "projects/ngx-flow", 12 | "sourceRoot": "projects/ngx-flow/src", 13 | "prefix": "lib", 14 | "architect": { 15 | "build": { 16 | "builder": "@angular-devkit/build-angular:ng-packagr", 17 | "options": { 18 | "project": "projects/ngx-flow/ng-package.json" 19 | }, 20 | "configurations": { 21 | "production": { 22 | "tsConfig": "projects/ngx-flow/tsconfig.lib.prod.json" 23 | }, 24 | "development": { 25 | "tsConfig": "projects/ngx-flow/tsconfig.lib.json" 26 | } 27 | }, 28 | "defaultConfiguration": "production" 29 | }, 30 | "test": { 31 | "builder": "@angular-devkit/build-angular:karma", 32 | "options": { 33 | "polyfills": [ 34 | "zone.js", 35 | "zone.js/testing" 36 | ], 37 | "tsConfig": "projects/ngx-flow/tsconfig.spec.json", 38 | "karmaConfig": "projects/ngx-flow/karma.conf.js" 39 | } 40 | } 41 | } 42 | }, 43 | "ngx-flow-demo": { 44 | "projectType": "application", 45 | "schematics": {}, 46 | "root": "projects/ngx-flow-demo", 47 | "sourceRoot": "projects/ngx-flow-demo/src", 48 | "prefix": "app", 49 | "architect": { 50 | "build": { 51 | "builder": "@angular-devkit/build-angular:application", 52 | "options": { 53 | "outputPath": "dist/ngx-flow-demo", 54 | "index": "projects/ngx-flow-demo/src/index.html", 55 | "browser": "projects/ngx-flow-demo/src/main.ts", 56 | "polyfills": [ 57 | "zone.js" 58 | ], 59 | "tsConfig": "projects/ngx-flow-demo/tsconfig.app.json", 60 | "assets": [ 61 | "projects/ngx-flow-demo/src/favicon.ico", 62 | "projects/ngx-flow-demo/src/assets" 63 | ], 64 | "styles": [ 65 | "projects/ngx-flow-demo/src/styles.css" 66 | ], 67 | "scripts": [] 68 | }, 69 | "configurations": { 70 | "production": { 71 | "budgets": [ 72 | { 73 | "type": "initial", 74 | "maximumWarning": "500kb", 75 | "maximumError": "1mb" 76 | }, 77 | { 78 | "type": "anyComponentStyle", 79 | "maximumWarning": "2kb", 80 | "maximumError": "4kb" 81 | } 82 | ], 83 | "outputHashing": "all" 84 | }, 85 | "development": { 86 | "optimization": false, 87 | "extractLicenses": false, 88 | "sourceMap": true 89 | } 90 | }, 91 | "defaultConfiguration": "production" 92 | }, 93 | "serve": { 94 | "builder": "@angular-devkit/build-angular:dev-server", 95 | "configurations": { 96 | "production": { 97 | "buildTarget": "ngx-flow-demo:build:production" 98 | }, 99 | "development": { 100 | "buildTarget": "ngx-flow-demo:build:development" 101 | } 102 | }, 103 | "defaultConfiguration": "development" 104 | }, 105 | "extract-i18n": { 106 | "builder": "@angular-devkit/build-angular:extract-i18n", 107 | "options": { 108 | "buildTarget": "ngx-flow-demo:build" 109 | } 110 | }, 111 | "test": { 112 | "builder": "@angular-devkit/build-angular:karma", 113 | "options": { 114 | "polyfills": [ 115 | "zone.js", 116 | "zone.js/testing" 117 | ], 118 | "tsConfig": "projects/ngx-flow-demo/tsconfig.spec.json", 119 | "assets": [ 120 | "projects/ngx-flow-demo/src/favicon.ico", 121 | "projects/ngx-flow-demo/src/assets" 122 | ], 123 | "styles": [ 124 | "projects/ngx-flow-demo/src/styles.css" 125 | ], 126 | "scripts": [] 127 | } 128 | } 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | defaults 8 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to ngx-flow-demo!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-flow", 3 | "version": "0.0.0", 4 | "author": { 5 | "name": "Martin Nuc", 6 | "email": "martin@nuc.cz" 7 | }, 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/flowjs/ngx-flow" 12 | }, 13 | "scripts": { 14 | "ng": "ng", 15 | "start": "ng serve", 16 | "build:lib": "ng build ngx-flow", 17 | "build:demo": "ng build ngx-flow-demo", 18 | "watch": "ng build --watch --configuration development", 19 | "test": "ng test", 20 | "test:ci": "ng test ngx-flow --watch=false --browsers ChromeHeadless --code-coverage", 21 | "server": "node server/app.js" 22 | }, 23 | "private": true, 24 | "dependencies": { 25 | "@angular/animations": "^19.0.0", 26 | "@angular/common": "^19.0.0", 27 | "@angular/compiler": "^19.0.0", 28 | "@angular/core": "^19.0.0", 29 | "@angular/forms": "^19.0.0", 30 | "@angular/platform-browser": "^19.0.0", 31 | "@angular/platform-browser-dynamic": "^19.0.0", 32 | "@angular/router": "^19.0.0", 33 | "@flowjs/flow.js": "^2.14.1", 34 | "rxjs": "^7.8.0", 35 | "tslib": "^2.3.0", 36 | "zone.js": "~0.15.0" 37 | }, 38 | "devDependencies": { 39 | "@angular-devkit/build-angular": "^19.0.0", 40 | "@angular/cli": "^19.0.0", 41 | "@angular/compiler-cli": "^19.0.0", 42 | "@types/flowjs": "^2.13.14", 43 | "@types/jasmine": "~5.1.0", 44 | "connect-multiparty": "^2.2.0", 45 | "jasmine-core": "~5.1.0", 46 | "karma": "~6.4.0", 47 | "karma-chrome-launcher": "~3.2.0", 48 | "karma-coverage": "~2.2.0", 49 | "karma-jasmine": "~5.1.0", 50 | "karma-jasmine-html-reporter": "~2.1.0", 51 | "ng-packagr": "^19.1.1", 52 | "typescript": "^5.5.4" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | singleQuote: true 4 | }; 5 | -------------------------------------------------------------------------------- /projects/ngx-flow-demo/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .transfers { 2 | display: flex; 3 | flex-wrap: wrap; 4 | } 5 | 6 | .transfer { 7 | color: white; 8 | background: lightsalmon; 9 | margin: 15px; 10 | padding: 5px; 11 | } 12 | 13 | .transfer--error { 14 | background: red; 15 | } 16 | 17 | .transfer--success { 18 | background: green; 19 | } 20 | 21 | .name { 22 | max-width: 300px; 23 | font-weight: bold; 24 | } 25 | 26 | img { 27 | max-width: 300px; 28 | } 29 | 30 | .drop-area { 31 | min-width: 80px; 32 | min-height: 80px; 33 | border: 1px solid darkblue; 34 | background: lightskyblue; 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | margin: 10px; 39 | max-width: 300px; 40 | } 41 | -------------------------------------------------------------------------------- /projects/ngx-flow-demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

ngx-flow example

2 |

3 | To see how files are being uploaded to the server you need to run this example along with a flowjs server. 4 |

5 | 6 |

7 | @if (flow?.flowJs?.support) { 8 | ✅ FlowJS is supported by your browser 9 | } 10 | @else { 11 | 🛑 FlowJS is NOT supported by your browser 12 | } 13 |

14 | 15 | 16 | 17 | 21 | 22 |
25 | Drop a file here 26 |
27 | 28 | 29 | 30 | Total progress: {{(flow.transfers$ | async)?.totalProgress | percent}} 31 | 32 |
33 | 34 | @for (transfer of (flow.transfers$ | async)?.transfers; track transfer.id) { 35 |
36 |
name: {{transfer.name}}
37 |
progress: {{transfer.progress | percent}}
38 |
size: {{transfer.size | number: '1.0'}} bytes
39 |
current speed: {{transfer.currentSpeed | number: '1.0'}} bytes/s
40 |
average speed: {{transfer.averageSpeed | number: '1.0'}} bytes/s
41 |
time ramining: {{transfer.timeRemaining}}s
42 |
paused: {{transfer.paused}}
43 |
success: {{transfer.success}}
44 |
complete: {{transfer.complete}}
45 |
error: {{transfer.error}}
46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 | } 54 | 55 |
56 | -------------------------------------------------------------------------------- /projects/ngx-flow-demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { AsyncPipe, PercentPipe, DecimalPipe, NgClass} from '@angular/common'; 2 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ViewChild } from '@angular/core'; 3 | import { FlowDirective, NgxFlowModule } from '@flowjs/ngx-flow'; 4 | import { Subscription } from 'rxjs'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | imports: [ 9 | NgClass, DecimalPipe, PercentPipe, AsyncPipe, 10 | NgxFlowModule 11 | ], 12 | templateUrl: './app.component.html', 13 | styleUrl: './app.component.css', 14 | changeDetection: ChangeDetectionStrategy.OnPush 15 | }) 16 | export class AppComponent { 17 | 18 | @ViewChild('flow', { static: false }) flow: FlowDirective | undefined; 19 | 20 | autoUploadSubscription: Subscription | undefined; 21 | 22 | constructor(private cd: ChangeDetectorRef) {} 23 | 24 | ngAfterViewInit() { 25 | this.autoUploadSubscription = this.flow?.events$.subscribe(event => { 26 | switch (event.type) { 27 | case 'filesSubmitted': 28 | return this.flow?.upload(); 29 | case 'newFlowJsInstance': 30 | this.cd.detectChanges(); 31 | } 32 | }); 33 | } 34 | 35 | ngOnDestroy() { 36 | this.autoUploadSubscription?.unsubscribe(); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /projects/ngx-flow-demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flowjs/ngx-flow/33f641d0c83c847fe982ec994ad4f31e4e716172/projects/ngx-flow-demo/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/ngx-flow-demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flowjs/ngx-flow/33f641d0c83c847fe982ec994ad4f31e4e716172/projects/ngx-flow-demo/src/favicon.ico -------------------------------------------------------------------------------- /projects/ngx-flow-demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgxFlowDemo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/ngx-flow-demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { AppComponent } from './app/app.component'; 3 | import Flow from '@flowjs/flow.js'; 4 | import { FlowInjectionToken } from '@flowjs/ngx-flow'; 5 | 6 | bootstrapApplication(AppComponent, { 7 | providers: [ 8 | { 9 | provide: FlowInjectionToken, 10 | useValue: Flow 11 | } 12 | ] 13 | }).catch((err) => console.error(err)); 14 | -------------------------------------------------------------------------------- /projects/ngx-flow-demo/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/ngx-flow-demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/ngx-flow-demo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/ngx-flow/README.md: -------------------------------------------------------------------------------- 1 | # NgxFlow 2 | 3 | The purpose of this package is to create a wrapper for Angular for fileupload using [flow.js](https://github.com/flowjs/flow.js). 4 | 5 | [![Build Status](https://travis-ci.com/flowjs/ngx-flow.svg?branch=master)](https://travis-ci.com/flowjs/ngx-flow) 6 | [![Test Coverage](https://api.codeclimate.com/v1/badges/29153dcefffff1fe5a5c/test_coverage)](https://codeclimate.com/github/flowjs/ngx-flow/test_coverage) 7 | [![Maintainability](https://api.codeclimate.com/v1/badges/29153dcefffff1fe5a5c/maintainability)](https://codeclimate.com/github/flowjs/ngx-flow/maintainability) 8 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 9 | 10 | 11 | ## Demo 12 | 13 | [https://stackblitz.com/edit/ngx-flow-example](https://stackblitz.com/edit/ngx-flow-example) 14 | 15 | You can also find example source code in the `src` folder. 16 | 17 | ## Roadmap 18 | 19 | - ✅ upload single file 20 | - ✅ upload multiple files 21 | - ✅ queue management 22 | - ✅ error handling 23 | - ✅ pause / resume upload 24 | - ✅ cancel upload, cancel all uploads 25 | - ✅ upload progress 26 | - ✅ file / directory restrictions 27 | - ✅ drag & drop 28 | - ✅ display uploaded image 29 | - ✅ tests 30 | - ✅ upload right after selecting file 31 | - ✅ run tests using TravisCI 32 | - ✅ demo using Stackblitz 33 | - ✅ support for server side rendering 34 | 35 | ## Install 36 | 37 | `npm install @flowjs/flow.js @flowjs/ngx-flow` 38 | 39 | Import in your module: 40 | 41 | ```typescript 42 | import { NgxFlowModule, FlowInjectionToken } from '@flowjs/ngx-flow'; 43 | import Flow from '@flowjs/flow.js'; 44 | 45 | @NgModule({ 46 | imports: [NgxFlowModule], 47 | providers: [ 48 | { 49 | provide: FlowInjectionToken, 50 | useValue: Flow 51 | } 52 | ] 53 | }) 54 | export class AppModule 55 | ``` 56 | 57 | We use dependecy injection to provide flow.js library. 58 | 59 | ## How to use 60 | 61 | 1. Start up server. There is a server example taken from [flow.js](https://github.com/flowjs/flow.js) here in `server` directory. In this repo you can run it using `npm run server`. 62 | 63 | 1. First you need to initialize ngx-flow directive and export it as for example `flow` variable: 64 | 65 | ```html 66 | 67 | ``` 68 | 69 | 1. Now you can use either directive `flowButton` for select file dialog: 70 | 71 | ```html 72 | 73 | ``` 74 | 75 | Or `flowDrop` for drag&drop feature: 76 | 77 | ```html 78 |
79 | ``` 80 | 81 | For both you have to pass `[flow]=flow.flowJs` where `flow` is template variable exported in step 1. 82 | 83 | 1. You can than subscribe to observable of transfers: 84 | 85 | ```html 86 |
87 | ``` 88 | 89 | 1. After adding files you can begin upload using `upload()` method: 90 | 91 | ```html 92 | 93 | ``` 94 | 95 | ### How does `transfers$` data look like? 96 | 97 | Observable `flow.transfers$` emits state in form: 98 | 99 | ```typescript 100 | { 101 | totalProgress: 0.5, 102 | transfers: [ 103 | { 104 | name: "somefile.txt", 105 | flowFile: FlowFile, 106 | progress: number, 107 | error: boolean, 108 | paused: boolean, 109 | success: boolean, 110 | complete: boolean, 111 | currentSpeed: number, 112 | averageSpeed: number, 113 | size: number, 114 | timeRemaining: number, 115 | }, 116 | { 117 | name: "uploading.txt", 118 | flowFile: FlowFile, 119 | progress: 0.5, 120 | error: false, 121 | paused: false, 122 | success: false, 123 | complete: false, 124 | currentSpeed: number, 125 | averageSpeed: number, 126 | size: number, 127 | timeRemaining: number, 128 | }, 129 | { 130 | name: "failed-to-upload.txt", 131 | flowFile: FlowFile, 132 | progress: number, 133 | error: true, 134 | paused: false, 135 | success: false, 136 | complete: true, 137 | currentSpeed: number, 138 | averageSpeed: number, 139 | size: number, 140 | timeRemaining: number, 141 | } 142 | { 143 | name: "uploaded.txt", 144 | flowFile: FlowFile, 145 | progress: number, 146 | error: false, 147 | paused: false, 148 | success: true, 149 | complete: true, 150 | currentSpeed: number, 151 | averageSpeed: number, 152 | size: number, 153 | timeRemaining: number, 154 | } 155 | ], 156 | flow: { /* flow.js instance*/ } 157 | } 158 | ``` 159 | 160 | ## FAQ 161 | 162 | ### I need access to flow.js object 163 | 164 | You can find it under `flow` variable. 165 | 166 | ```html 167 |

Is flowjs supported by the browser? {{flow.flowJs?.support}}

168 | ``` 169 | 170 | ### I see flickering when upload is in progress 171 | 172 | Use `trackBy` for `ngFor`: 173 | 174 | ```html 175 |
176 | ``` 177 | 178 | ```typescript 179 | export class AppComponent { 180 | trackTransfer(transfer: Transfer) { 181 | return transfer.id; 182 | } 183 | } 184 | ``` 185 | 186 | ### I need just a single file 187 | 188 | Add `singleFile: true` to your flow config: 189 | 190 | ```html 191 | 192 | ``` 193 | 194 | ### I want to upload whole directory 195 | 196 | Add `flowDirectoryOnly="true"` to your button: 197 | 198 | ```html 199 | 200 | ``` 201 | 202 | ### I want to display image which is going to be uploaded 203 | 204 | Use directive `flowSrc`: 205 | 206 | ```html 207 |
208 | 209 |
210 | ``` 211 | 212 | ### How to trigger upload right after picking the file? 213 | 214 | Subscribe to `events$`. NgxFlow listens for these events: `filesSubmitted`, `fileRemoved`, `fileRetry`, `fileProgress`, `fileSuccess`, `fileError` of flow.js. Event `fileSubmitted` is fired when user drops or selects a file. 215 | 216 | ```typescript 217 | export class AppComponent implements AfterViewInit, OnDestroy { 218 | @ViewChild('flow') 219 | flow: FlowDirective; 220 | 221 | autoUploadSubscription: Subscription; 222 | 223 | ngAfterViewInit() { 224 | this.autoUploadSubscription = this.flow.events$.subscribe((event) => { 225 | if (event.type === 'filesSubmitted') { 226 | this.flow.upload(); 227 | } 228 | }); 229 | } 230 | 231 | ngOnDestroy() { 232 | this.autoUploadSubscription.unsubscribe(); 233 | } 234 | } 235 | ``` 236 | 237 | ### Development 238 | 239 | `npm run build` - builds the library into dist folder 240 | 241 | After that you can publish to npm repository from `dist` folder: 242 | 243 | ``` 244 | cd dist/ngx-flow 245 | npm publish 246 | ``` 247 | -------------------------------------------------------------------------------- /projects/ngx-flow/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, '../../coverage/ngx-flow'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /projects/ngx-flow/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-flow", 4 | "allowedNonPeerDependencies": ["@types/flowjs"], 5 | "lib": { 6 | "entryFile": "src/public-api.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /projects/ngx-flow/ng-package.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-flow", 4 | "whitelistedNonPeerDependencies": ["@types/flowjs"], 5 | "lib": { 6 | "entryFile": "src/public_api.ts", 7 | "umdModuleIds": { 8 | "@flowjs/flow.js": "Flow" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /projects/ngx-flow/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flowjs/ngx-flow", 3 | "version": "0.5.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@flowjs/ngx-flow", 9 | "version": "0.5.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "tslib": "^2.2.0" 13 | }, 14 | "devDependencies": { 15 | "@types/flowjs": "2.13.3" 16 | }, 17 | "peerDependencies": { 18 | "@angular/common": "^14.0.0-", 19 | "@angular/core": "^14.0.0-", 20 | "@flowjs/flow.js": "^2.13.0" 21 | } 22 | }, 23 | "node_modules/@angular/common": { 24 | "version": "14.0.5", 25 | "resolved": "https://registry.npmjs.org/@angular/common/-/common-14.0.5.tgz", 26 | "integrity": "sha512-YFRPxx3yRLjk0gPL7tm/97mi8+Pjt3q6zWCjrLkAlDjniDvgmKNWIQ1h6crZQR0Cw7yNqK0QoFXQgTw0GJIWLQ==", 27 | "peer": true, 28 | "dependencies": { 29 | "tslib": "^2.3.0" 30 | }, 31 | "engines": { 32 | "node": "^14.15.0 || >=16.10.0" 33 | }, 34 | "peerDependencies": { 35 | "@angular/core": "14.0.5", 36 | "rxjs": "^6.5.3 || ^7.4.0" 37 | } 38 | }, 39 | "node_modules/@angular/core": { 40 | "version": "14.0.5", 41 | "resolved": "https://registry.npmjs.org/@angular/core/-/core-14.0.5.tgz", 42 | "integrity": "sha512-4MIfFM2nD+N0/Dk8xKfKvbdS/zYRhQgdnKT6ZIIV7Y/XCfn5QAIa4+vB5BEAZpuzSsZHLVdBQQ0TkaiONLfL2Q==", 43 | "peer": true, 44 | "dependencies": { 45 | "tslib": "^2.3.0" 46 | }, 47 | "engines": { 48 | "node": "^14.15.0 || >=16.10.0" 49 | }, 50 | "peerDependencies": { 51 | "rxjs": "^6.5.3 || ^7.4.0", 52 | "zone.js": "~0.11.4" 53 | } 54 | }, 55 | "node_modules/@flowjs/flow.js": { 56 | "version": "2.14.1", 57 | "resolved": "https://registry.npmjs.org/@flowjs/flow.js/-/flow.js-2.14.1.tgz", 58 | "integrity": "sha512-99DWlPnksOOS8uHfo+bhSjvs8d2MfLTB/22JBDC2ONwz/OCdP+gL/iiM4puMSTE2wH4A2/+J0eMc7pKwusXunw==", 59 | "peer": true 60 | }, 61 | "node_modules/@types/flowjs": { 62 | "version": "2.13.3", 63 | "resolved": "https://registry.npmjs.org/@types/flowjs/-/flowjs-2.13.3.tgz", 64 | "integrity": "sha512-VeWuL+Whk6lUSWX/g0LzLNyZywyTB5wZ2L6mPvD8/u5pgLF2HwyV7nZ1UArOifalJ5UE1CcJbPLKS+jc5+Z2ig==", 65 | "dev": true 66 | }, 67 | "node_modules/rxjs": { 68 | "version": "7.5.5", 69 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", 70 | "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", 71 | "peer": true, 72 | "dependencies": { 73 | "tslib": "^2.1.0" 74 | } 75 | }, 76 | "node_modules/tslib": { 77 | "version": "2.4.0", 78 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", 79 | "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" 80 | }, 81 | "node_modules/zone.js": { 82 | "version": "0.11.6", 83 | "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.6.tgz", 84 | "integrity": "sha512-umJqFtKyZlPli669gB1gOrRE9hxUUGkZr7mo878z+NEBJZZixJkKeVYfnoLa7g25SseUDc92OZrMKKHySyJrFg==", 85 | "peer": true, 86 | "dependencies": { 87 | "tslib": "^2.3.0" 88 | } 89 | } 90 | }, 91 | "dependencies": { 92 | "@angular/common": { 93 | "version": "14.0.5", 94 | "resolved": "https://registry.npmjs.org/@angular/common/-/common-14.0.5.tgz", 95 | "integrity": "sha512-YFRPxx3yRLjk0gPL7tm/97mi8+Pjt3q6zWCjrLkAlDjniDvgmKNWIQ1h6crZQR0Cw7yNqK0QoFXQgTw0GJIWLQ==", 96 | "peer": true, 97 | "requires": { 98 | "tslib": "^2.3.0" 99 | } 100 | }, 101 | "@angular/core": { 102 | "version": "14.0.5", 103 | "resolved": "https://registry.npmjs.org/@angular/core/-/core-14.0.5.tgz", 104 | "integrity": "sha512-4MIfFM2nD+N0/Dk8xKfKvbdS/zYRhQgdnKT6ZIIV7Y/XCfn5QAIa4+vB5BEAZpuzSsZHLVdBQQ0TkaiONLfL2Q==", 105 | "peer": true, 106 | "requires": { 107 | "tslib": "^2.3.0" 108 | } 109 | }, 110 | "@flowjs/flow.js": { 111 | "version": "2.14.1", 112 | "resolved": "https://registry.npmjs.org/@flowjs/flow.js/-/flow.js-2.14.1.tgz", 113 | "integrity": "sha512-99DWlPnksOOS8uHfo+bhSjvs8d2MfLTB/22JBDC2ONwz/OCdP+gL/iiM4puMSTE2wH4A2/+J0eMc7pKwusXunw==", 114 | "peer": true 115 | }, 116 | "@types/flowjs": { 117 | "version": "2.13.3", 118 | "resolved": "https://registry.npmjs.org/@types/flowjs/-/flowjs-2.13.3.tgz", 119 | "integrity": "sha512-VeWuL+Whk6lUSWX/g0LzLNyZywyTB5wZ2L6mPvD8/u5pgLF2HwyV7nZ1UArOifalJ5UE1CcJbPLKS+jc5+Z2ig==", 120 | "dev": true 121 | }, 122 | "rxjs": { 123 | "version": "7.5.5", 124 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", 125 | "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", 126 | "peer": true, 127 | "requires": { 128 | "tslib": "^2.1.0" 129 | } 130 | }, 131 | "tslib": { 132 | "version": "2.4.0", 133 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", 134 | "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" 135 | }, 136 | "zone.js": { 137 | "version": "0.11.6", 138 | "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.6.tgz", 139 | "integrity": "sha512-umJqFtKyZlPli669gB1gOrRE9hxUUGkZr7mo878z+NEBJZZixJkKeVYfnoLa7g25SseUDc92OZrMKKHySyJrFg==", 140 | "peer": true, 141 | "requires": { 142 | "tslib": "^2.3.0" 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /projects/ngx-flow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flowjs/ngx-flow", 3 | "version": "19.0.0", 4 | "author": { 5 | "name": "Martin Nuc", 6 | "email": "martin@nuc.cz" 7 | }, 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/flowjs/ngx-flow" 12 | }, 13 | "peerDependencies": { 14 | "@angular/common": "^19.0.0", 15 | "@angular/core": "^19.0.0", 16 | "@flowjs/flow.js": "^2.13.0" 17 | }, 18 | "dependencies": { 19 | "tslib": "^2.3.0" 20 | }, 21 | "devDependencies": { 22 | "@types/flowjs": "^2.14.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/button.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, DebugElement } from '@angular/core'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | import { ButtonDirective } from './button.directive'; 5 | 6 | @Component({ 7 | template: ``, 11 | standalone: false 12 | }) 13 | class TestComponent { 14 | flowJs: any; 15 | flowAttributes: any; 16 | } 17 | 18 | describe('Directive: Button', () => { 19 | let component: TestComponent; 20 | let fixture: ComponentFixture; 21 | let inputElement: DebugElement; 22 | 23 | beforeEach(() => { 24 | TestBed.configureTestingModule({ 25 | declarations: [TestComponent, ButtonDirective] 26 | }); 27 | fixture = TestBed.createComponent(TestComponent); 28 | component = fixture.componentInstance; 29 | inputElement = fixture.debugElement.query(By.css('input[type=file]')); 30 | }); 31 | 32 | it('should call assignBrowse when flow config changes', () => { 33 | component.flowJs = { 34 | opts: { 35 | singleFile: true 36 | }, 37 | assignBrowse: jasmine.createSpy() 38 | }; 39 | 40 | fixture.detectChanges(); 41 | expect(component.flowJs.assignBrowse).toHaveBeenCalledWith(inputElement.nativeElement, false, true, undefined); 42 | 43 | component.flowAttributes = { 44 | accept: 'images/*' 45 | }; 46 | fixture.detectChanges(); 47 | expect(component.flowJs.assignBrowse).toHaveBeenCalledWith(inputElement.nativeElement, false, true, { 48 | accept: 'images/*' 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/button.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, Input } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[flowButton]', 5 | standalone: false 6 | }) 7 | export class ButtonDirective { 8 | protected _directoryOnly = false; 9 | @Input() 10 | set flowDirectoryOnly(directoriesOnly: boolean) { 11 | this._directoryOnly = directoriesOnly; 12 | this.setup(); 13 | } 14 | 15 | protected _attributes?: object; 16 | @Input() 17 | set flowAttributes(attributes: object) { 18 | this._attributes = attributes; 19 | this.setup(); 20 | } 21 | 22 | protected _flow?: flowjs.Flow; 23 | @Input() 24 | set flow(flow: flowjs.Flow) { 25 | this._flow = flow; 26 | this.setup(); 27 | } 28 | 29 | setup() { 30 | if (!this._flow) { 31 | return; 32 | } 33 | this._flow.assignBrowse( 34 | this.el.nativeElement, 35 | this._directoryOnly, 36 | this._flow.opts.singleFile, 37 | this._attributes 38 | ); 39 | } 40 | 41 | constructor(protected el: ElementRef) {} 42 | } 43 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/drop.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { DropDirective } from './drop.directive'; 2 | import { Component, DebugElement, ViewChild, Renderer2 } from '@angular/core'; 3 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { By } from '@angular/platform-browser'; 5 | 6 | @Component({ 7 | template: `
`, 8 | standalone: false 9 | }) 10 | class TestComponent { 11 | flowJs: any; 12 | 13 | @ViewChild('flowDrop', { static: false }) 14 | flowDrop!: DropDirective; 15 | } 16 | describe('Directive: Drop', () => { 17 | let component: TestComponent; 18 | let fixture: ComponentFixture; 19 | let dropAreElement: DebugElement; 20 | let renderer: Renderer2; 21 | 22 | beforeEach(() => { 23 | TestBed.configureTestingModule({ 24 | declarations: [TestComponent, DropDirective], 25 | providers: [Renderer2], 26 | }); 27 | fixture = TestBed.createComponent(TestComponent); 28 | component = fixture.componentInstance; 29 | dropAreElement = fixture.debugElement.query(By.css('div')); 30 | }); 31 | 32 | it('should call assignDrop after setting up flow', () => { 33 | component.flowJs = { 34 | assignDrop: jasmine.createSpy(), 35 | unAssignDrop: jasmine.createSpy(), 36 | }; 37 | 38 | expect(component.flowJs.assignDrop).toHaveBeenCalledTimes(0); 39 | fixture.detectChanges(); 40 | expect(component.flowJs.assignDrop).toHaveBeenCalledWith( 41 | dropAreElement.nativeElement 42 | ); 43 | }); 44 | 45 | it('should call assignDrop after enable/disable', () => { 46 | component.flowJs = { 47 | assignDrop: jasmine.createSpy(), 48 | unAssignDrop: jasmine.createSpy(), 49 | }; 50 | fixture.detectChanges(); 51 | 52 | component.flowDrop.disable(); 53 | fixture.detectChanges(); 54 | expect(component.flowJs.unAssignDrop).toHaveBeenCalledWith( 55 | dropAreElement.nativeElement 56 | ); 57 | 58 | component.flowDrop.enable(); 59 | fixture.detectChanges(); 60 | expect(component.flowJs.assignDrop).toHaveBeenCalledWith( 61 | dropAreElement.nativeElement 62 | ); 63 | }); 64 | 65 | it('should attach drop and dragover listeners to body', () => { 66 | renderer = fixture.componentRef.injector.get(Renderer2); 67 | spyOn(renderer, 'listen').and.callThrough(); 68 | fixture.detectChanges(); 69 | // cannot use toHaveBeenCalledWith: https://github.com/jasmine/jasmine/issues/228 70 | expect((renderer.listen as any).calls.allArgs()).toEqual([ 71 | ['body', 'drop', jasmine.any(Function)], 72 | ['body', 'dragover', jasmine.any(Function)], 73 | ]); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/drop.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, ElementRef, Renderer2, OnInit } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[flowDrop]', 5 | exportAs: 'flowDrop', 6 | standalone: false 7 | }) 8 | export class DropDirective implements OnInit { 9 | protected flowJs?: flowjs.Flow; 10 | 11 | @Input() 12 | set flow(flow: flowjs.Flow) { 13 | this.flowJs = flow; 14 | if (!flow) { 15 | return; 16 | } 17 | this.enable(); 18 | } 19 | 20 | enable() { 21 | this.flowJs?.assignDrop(this.el.nativeElement); 22 | } 23 | 24 | disable() { 25 | this.flowJs?.unAssignDrop(this.el.nativeElement); 26 | } 27 | 28 | constructor(protected el: ElementRef, protected renderer: Renderer2) {} 29 | 30 | ngOnInit() { 31 | this.renderer.listen('body', 'drop', (event) => event.preventDefault()); 32 | this.renderer.listen('body', 'dragover', (event) => event.preventDefault()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/flow-constructor.ts: -------------------------------------------------------------------------------- 1 | export interface FlowConstructor { 2 | new (flowOptions: flowjs.FlowOptions): flowjs.Flow; 3 | } 4 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/flow-injection-token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { FlowConstructor } from './flow-constructor'; 3 | 4 | export const FlowInjectionToken = new InjectionToken('Flow'); 5 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/flow.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, PLATFORM_ID } from '@angular/core'; 2 | import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; 3 | import { first, skip } from 'rxjs/operators'; 4 | import { FlowInjectionToken } from './flow-injection-token'; 5 | import { FlowDirective, FlowChangeEvent, NgxFlowEvent } from './flow.directive'; 6 | import { trasnferMockFactory } from './helpers/tests/transfer-mock-factory'; 7 | import { flowFileMockFactory } from './helpers/tests/flow-file-mock-factory'; 8 | import { FlowMock } from './helpers/tests/flow-mock'; 9 | 10 | @Component({ 11 | template: ``, 12 | standalone: false 13 | }) 14 | class TestComponent { 15 | @ViewChild('flow', { static: true }) 16 | flow!: FlowDirective; 17 | 18 | config = { target: 'http://localhost:3000/upload' }; 19 | } 20 | 21 | describe('Directive: Flow integration tests', () => { 22 | let component: TestComponent; 23 | let fixture: ComponentFixture; 24 | 25 | beforeEach(() => { 26 | TestBed.configureTestingModule({ 27 | declarations: [TestComponent, FlowDirective], 28 | providers: [ 29 | { 30 | provide: FlowInjectionToken, 31 | useValue: FlowMock, 32 | }, 33 | ], 34 | }); 35 | fixture = TestBed.createComponent(TestComponent); 36 | component = fixture.componentInstance; 37 | }); 38 | 39 | it('should initialize flowjs and export flow directive as template reference variable', () => { 40 | fixture.detectChanges(); 41 | expect(component.flow instanceof FlowDirective).toBeTruthy(); 42 | expect(component.flow.flowJs).toBeDefined(); 43 | expect(component.flow.flowJs.opts.target).toBe('http://localhost:3000/upload'); 44 | }); 45 | 46 | it('should emit new flowjs instance when new config is provided', fakeAsync(() => { 47 | component.flow.transfers$ 48 | .pipe(first()) 49 | .subscribe((transfers) => expect(transfers.flow.opts.target).toBe('http://localhost:3000/upload')); 50 | fixture.detectChanges(); 51 | tick(); 52 | component.config = { target: 'http://localhost:4000/upload' }; 53 | fixture.detectChanges(); 54 | component.flow.transfers$ 55 | .pipe(first()) 56 | .subscribe((transfers) => expect(transfers.flow.opts.target).toBe('http://localhost:4000/upload')); 57 | })); 58 | 59 | it('should emit transfer when file is added', (done: DoneFn) => { 60 | fixture.detectChanges(); 61 | component.flow.flowJs.files = [flowFileMockFactory('file.txt')]; 62 | component.flow.transfers$ 63 | .pipe( 64 | skip(1), // skip initial emit with empty array 65 | first() 66 | ) 67 | .subscribe((transfers) => { 68 | expect(transfers.transfers.length).toBe(1); 69 | expect(transfers.transfers[0].name).toBe('file.txt'); 70 | done(); 71 | }); 72 | const flowMock = component.flow.flowJs as unknown as FlowMock; 73 | flowMock.flowJsEventEmitters['filesSubmitted'](); 74 | }); 75 | 76 | it('should emit transfers on pause/resume', (done: DoneFn) => { 77 | fixture.detectChanges(); 78 | component.flow.flowJs.files = []; 79 | component.flow.transfers$ 80 | .pipe( 81 | skip(1), // skip initial emit with empty array 82 | first() 83 | ) 84 | .subscribe((transfers) => { 85 | expect(transfers.transfers.length).toBe(0); 86 | done(); 87 | }); 88 | 89 | const transferMock = trasnferMockFactory('file.txt'); 90 | component.flow.pauseFile(transferMock); 91 | }); 92 | 93 | it('should trigger flowJs upload on upload', () => { 94 | fixture.detectChanges(); 95 | component.flow.flowJs.upload = jasmine.createSpy(); 96 | component.flow.upload(); 97 | expect(component.flow.flowJs.upload).toHaveBeenCalled(); 98 | }); 99 | 100 | it('should trigger flowJs cancel on cancel', () => { 101 | fixture.detectChanges(); 102 | component.flow.flowJs.cancel = jasmine.createSpy(); 103 | component.flow.cancel(); 104 | expect(component.flow.flowJs.cancel).toHaveBeenCalled(); 105 | }); 106 | 107 | it('should remove the file', () => { 108 | fixture.detectChanges(); 109 | const fileMock = trasnferMockFactory('file.txt'); 110 | component.flow.cancelFile(fileMock); 111 | expect(fileMock.flowFile.cancel).toHaveBeenCalled(); 112 | }); 113 | 114 | it('should pause file and emit event', (done) => { 115 | fixture.detectChanges(); 116 | const fileMock = trasnferMockFactory('file.txt'); 117 | component.flow.pauseOrResumeEvent$.pipe(first()).subscribe(() => { 118 | done(); 119 | }); 120 | 121 | component.flow.pauseFile(fileMock); 122 | expect(fileMock.flowFile.pause).toHaveBeenCalled(); 123 | }); 124 | 125 | it('should resume file and emit event', (done) => { 126 | fixture.detectChanges(); 127 | const fileMock = trasnferMockFactory('file.txt'); 128 | component.flow.pauseOrResumeEvent$.pipe(first()).subscribe(() => { 129 | done(); 130 | }); 131 | 132 | component.flow.resumeFile(fileMock); 133 | expect(fileMock.flowFile.resume).toHaveBeenCalled(); 134 | }); 135 | 136 | it('should tell us if there is something to upload', (done) => { 137 | fixture.detectChanges(); 138 | component.flow.flowJs.files = [ 139 | flowFileMockFactory('file.txt', { 140 | isComplete() { 141 | return false; 142 | }, 143 | }), 144 | ]; 145 | component.flow.somethingToUpload$ 146 | .pipe( 147 | skip(1), // skip initial emit with empty array 148 | first() 149 | ) 150 | .subscribe((somethingToUpload) => { 151 | expect(somethingToUpload).toBeTruthy(); 152 | done(); 153 | }); 154 | 155 | (component.flow.flowJs as any).flowJsEventEmitters['filesSubmitted'](); 156 | }); 157 | 158 | it('should tell us if there is nothing to upload after everything was uploaded', (done) => { 159 | fixture.detectChanges(); 160 | component.flow.flowJs.files = [ 161 | flowFileMockFactory('file.txt', { 162 | isComplete() { 163 | return true; 164 | }, 165 | }), 166 | ]; 167 | component.flow.somethingToUpload$.pipe(first()).subscribe((somethingToUpload) => { 168 | expect(somethingToUpload).toBeFalsy(); 169 | done(); 170 | }); 171 | }); 172 | 173 | it('should emit event when file is succesfully uploaded', (done) => { 174 | fixture.detectChanges(); 175 | function isFileSuccessEvent( 176 | event: FlowChangeEvent | NgxFlowEvent 177 | ): event is FlowChangeEvent<'fileSuccess'> { 178 | return event.type === 'fileSuccess'; 179 | } 180 | 181 | component.flow.events$.subscribe((event) => { 182 | if (!isFileSuccessEvent(event)) { 183 | return; 184 | } 185 | expect(event.event[0].name).toBe('file.txt'); 186 | expect(event.type).toBe('fileSuccess'); 187 | done(); 188 | }); 189 | const fileSuccessEvent: flowjs.FileSuccessCallbackArguments = [flowFileMockFactory('file.txt'), '', null as any]; 190 | (component.flow.flowJs as any).flowJsEventEmitters['fileSuccess'](fileSuccessEvent); 191 | }); 192 | }); 193 | 194 | describe('Directive: Flow SSR tests', () => { 195 | let component: TestComponent; 196 | let fixture: ComponentFixture; 197 | 198 | beforeEach(() => { 199 | TestBed.configureTestingModule({ 200 | declarations: [TestComponent, FlowDirective], 201 | providers: [ 202 | { 203 | provide: FlowInjectionToken, 204 | useValue: FlowMock, 205 | }, 206 | { 207 | provide: PLATFORM_ID, 208 | useValue: 'server', 209 | }, 210 | ], 211 | }); 212 | fixture = TestBed.createComponent(TestComponent); 213 | component = fixture.componentInstance; 214 | }); 215 | 216 | it('should not initialize flowjs when running on the server', () => { 217 | fixture.detectChanges(); 218 | expect(component.flow.flowJs).toBeUndefined(); 219 | }); 220 | }); 221 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/flow.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Inject, Input, PLATFORM_ID } from '@angular/core'; 2 | import { fromEvent, merge, Observable, ReplaySubject, Subject } from 'rxjs'; 3 | import { map, shareReplay, startWith, switchMap } from 'rxjs/operators'; 4 | import { FlowInjectionToken } from './flow-injection-token'; 5 | import { flowFile2Transfer } from './helpers/flow-file-to-transfer'; 6 | import { Transfer } from './transfer'; 7 | import { UploadState } from './upload-state'; 8 | import { isPlatformBrowser } from '@angular/common'; 9 | import { FlowConstructor } from './flow-constructor'; 10 | import { JQueryStyleEventEmitter } from 'rxjs/internal/observable/fromEvent'; 11 | 12 | export interface FlowChangeEvent { 13 | type: T; 14 | event: flowjs.FlowEventFromEventName; 15 | } 16 | 17 | export interface NgxFlowEvent { 18 | type: 'pauseOrResume' | 'newFlowJsInstance'; 19 | } 20 | 21 | @Directive({ 22 | selector: '[flowConfig]', 23 | exportAs: 'flow', 24 | standalone: false 25 | }) 26 | export class FlowDirective { 27 | @Input() 28 | set flowConfig(options: flowjs.FlowOptions) { 29 | if (isPlatformBrowser(this.platform)) { 30 | this.flowJs = new this.flowConstructor(options); 31 | this.flow$.next(this.flowJs); 32 | } 33 | } 34 | 35 | flowJs!: flowjs.Flow; 36 | 37 | protected flow$ = new ReplaySubject(1); 38 | 39 | pauseOrResumeEvent$ = new Subject(); 40 | 41 | events$ = this.flow$.pipe( 42 | switchMap((flow) => merge(this.flowEvents(flow), this.ngxFlowEvents())) 43 | ); 44 | 45 | transfers$: Observable = this.events$.pipe( 46 | map((_) => this.flowJs.files), 47 | map((files: flowjs.FlowFile[] = []) => ({ 48 | transfers: files.map((flowFile) => flowFile2Transfer(flowFile)), 49 | flow: this.flowJs, 50 | totalProgress: this.flowJs.progress(), 51 | })), 52 | shareReplay(1) 53 | ); 54 | 55 | somethingToUpload$ = this.transfers$.pipe( 56 | map( 57 | (state) => state.transfers.some((file) => !file.success), 58 | startWith(false) 59 | ) 60 | ); 61 | 62 | constructor( 63 | @Inject(FlowInjectionToken) protected flowConstructor: FlowConstructor, 64 | @Inject(PLATFORM_ID) protected platform: any 65 | ) {} 66 | 67 | private flowEvents( 68 | flow: flowjs.Flow 69 | ): Observable> { 70 | const events = [ 71 | this.listenForEvent(flow, 'fileSuccess'), 72 | this.listenForEvent(flow, 'fileProgress'), 73 | this.listenForEvent(flow, 'fileAdded'), 74 | this.listenForEvent(flow, 'filesAdded'), 75 | this.listenForEvent(flow, 'filesSubmitted'), 76 | this.listenForEvent(flow, 'fileRemoved'), 77 | this.listenForEvent(flow, 'fileRetry'), 78 | this.listenForEvent(flow, 'fileError'), 79 | this.listenForEvent(flow, 'uploadStart'), 80 | this.listenForEvent(flow, 'complete'), 81 | this.listenForEvent(flow, 'progress'), 82 | ]; 83 | return merge(...events); 84 | } 85 | 86 | private ngxFlowEvents(): Observable { 87 | const pauseOrResumeEvent$ = this.pauseOrResumeEvent$.pipe( 88 | map( 89 | (_) => 90 | ({ 91 | type: 'pauseOrResume', 92 | } as NgxFlowEvent) 93 | ) 94 | ); 95 | const newFlowInstanceEvent$ = this.flow$.pipe( 96 | map( 97 | (_) => 98 | ({ 99 | type: 'newFlowJsInstance', 100 | } as NgxFlowEvent) 101 | ) 102 | ); 103 | const events = [pauseOrResumeEvent$, newFlowInstanceEvent$]; 104 | return merge(...events); 105 | } 106 | 107 | upload(): void { 108 | this.flowJs.upload(); 109 | } 110 | 111 | cancel(): void { 112 | this.flowJs.cancel(); 113 | } 114 | 115 | cancelFile(file: Transfer): void { 116 | file.flowFile.cancel(); 117 | } 118 | 119 | pauseFile(file: Transfer): void { 120 | file.flowFile.pause(); 121 | this.pauseOrResumeEvent$.next(); 122 | } 123 | 124 | resumeFile(file: Transfer): void { 125 | file.flowFile.resume(); 126 | this.pauseOrResumeEvent$.next(); 127 | } 128 | 129 | protected listenForEvent>( 130 | flow: flowjs.Flow, 131 | eventName: T 132 | ): Observable<{ type: T; event: R }> { 133 | return fromEvent( 134 | flow as JQueryStyleEventEmitter, 135 | eventName 136 | ).pipe( 137 | map((args) => ({ 138 | type: eventName, 139 | event: args, 140 | })) 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/helpers/flow-file-to-transfer.ts: -------------------------------------------------------------------------------- 1 | import { Transfer } from '../transfer'; 2 | 3 | export function flowFile2Transfer(flowFile: flowjs.FlowFile): Transfer { 4 | return { 5 | id: flowFile.uniqueIdentifier, 6 | name: flowFile.name, 7 | progress: flowFile.progress(), 8 | averageSpeed: flowFile.averageSpeed, 9 | currentSpeed: flowFile.currentSpeed, 10 | size: flowFile.size, 11 | paused: flowFile.paused, 12 | error: flowFile.error, 13 | complete: flowFile.isComplete(), 14 | success: flowFile.isComplete() && !flowFile.error, 15 | timeRemaining: flowFile.timeRemaining(), 16 | flowFile 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/helpers/tests/flow-file-mock-factory.ts: -------------------------------------------------------------------------------- 1 | export function flowFileMockFactory(filename: string, overrides?: Partial): flowjs.FlowFile { 2 | const mocks = { 3 | flowObj: null as any, 4 | file: { 5 | name: filename, 6 | lastModified: 0, 7 | } as File, 8 | name: filename, 9 | relativePath: filename, 10 | size: 12345, 11 | uniqueIdentifier: 'id', 12 | averageSpeed: 0, 13 | currentSpeed: 0, 14 | chunks: [], 15 | paused: false, 16 | error: false, 17 | progress: jasmine.createSpy(), 18 | pause: jasmine.createSpy(), 19 | resume: jasmine.createSpy(), 20 | cancel: jasmine.createSpy(), 21 | retry: jasmine.createSpy(), 22 | bootstrap: jasmine.createSpy(), 23 | isUploading: jasmine.createSpy(), 24 | isComplete: jasmine.createSpy(), 25 | sizeUploaded: jasmine.createSpy(), 26 | timeRemaining: jasmine.createSpy(), 27 | getExtension: jasmine.createSpy(), 28 | getType: jasmine.createSpy(), 29 | } as flowjs.FlowFile; 30 | return Object.assign({}, mocks, overrides); 31 | } 32 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/helpers/tests/flow-mock.ts: -------------------------------------------------------------------------------- 1 | export class FlowMock { 2 | constructor(public opts: Partial) {} 3 | flowJsEventEmitters: Record = {}; 4 | addEventListener = jasmine.createSpy().and.callFake((eventName: string, cb: () => void) => { 5 | this.flowJsEventEmitters[eventName] = cb; 6 | }); 7 | removeEventListener = jasmine.createSpy().and.callFake((eventName: string) => { 8 | delete this.flowJsEventEmitters[eventName]; 9 | }); 10 | progress() { 11 | return 0; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/helpers/tests/transfer-mock-factory.ts: -------------------------------------------------------------------------------- 1 | import { Transfer } from '../../transfer'; 2 | import { flowFileMockFactory } from './flow-file-mock-factory'; 3 | 4 | export function trasnferMockFactory(filename: string): Transfer { 5 | return { 6 | flowFile: flowFileMockFactory(filename) 7 | } as Transfer; 8 | } 9 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/ngx-flow.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | // import Flow from '@flowjs/flow.js'; 3 | import { ButtonDirective } from './button.directive'; 4 | import { DropDirective } from './drop.directive'; 5 | import { FlowInjectionToken } from './flow-injection-token'; 6 | import { FlowDirective } from './flow.directive'; 7 | import { SrcDirective } from './src.directive'; 8 | 9 | const directives = [ 10 | ButtonDirective, 11 | SrcDirective, 12 | DropDirective, 13 | FlowDirective, 14 | ]; 15 | // export function flowFactory() { 16 | // return Flow; 17 | // } 18 | 19 | @NgModule({ 20 | imports: [], 21 | declarations: directives, 22 | // providers: [ 23 | // { 24 | // provide: FlowInjectionToken, 25 | // useFactory: flowFactory 26 | // } 27 | // ], 28 | exports: directives, 29 | }) 30 | export class NgxFlowModule {} 31 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/src.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, DebugElement } from '@angular/core'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | import { SrcDirective } from './src.directive'; 5 | 6 | @Component({ 7 | template: ``, 8 | standalone: false 9 | }) 10 | class TestComponent { 11 | transfer: any; 12 | } 13 | 14 | describe('Directive: Src', () => { 15 | let component: TestComponent; 16 | let fixture: ComponentFixture; 17 | let imgElement: DebugElement; 18 | 19 | beforeEach(() => { 20 | TestBed.configureTestingModule({ 21 | declarations: [TestComponent, SrcDirective] 22 | }); 23 | fixture = TestBed.createComponent(TestComponent); 24 | component = fixture.componentInstance; 25 | imgElement = fixture.debugElement.query(By.css('img')); 26 | }); 27 | 28 | it('should add src attribute with image data', (done: DoneFn) => { 29 | const blob = new Blob(['data-data-data'], { type: 'application/json' }); 30 | component.transfer = { 31 | flowFile: { 32 | file: blob 33 | } 34 | }; 35 | fixture.detectChanges(); 36 | 37 | // ToDo: find other way to test this. Problem is that element's attribute it not yet updated 38 | setTimeout(() => { 39 | expect(imgElement.nativeElement.getAttribute('src')).toBe('data:application/json;base64,ZGF0YS1kYXRhLWRhdGE='); 40 | done(); 41 | }, 1000); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/src.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, Input } from '@angular/core'; 2 | import { Transfer } from './transfer'; 3 | 4 | @Directive({ 5 | selector: '[flowSrc]', 6 | standalone: false 7 | }) 8 | export class SrcDirective { 9 | protected fileReader; 10 | 11 | @Input() 12 | set flowSrc(transfer: Transfer) { 13 | this.fileReader = new FileReader(); 14 | this.fileReader.readAsDataURL(transfer.flowFile.file); 15 | this.fileReader.onload = (event: any) => { 16 | const url = event.target.result; 17 | this.el.nativeElement.setAttribute('src', url); 18 | }; 19 | } 20 | 21 | constructor(private el: ElementRef) {} 22 | } 23 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/transfer.ts: -------------------------------------------------------------------------------- 1 | export interface Transfer { 2 | id: string; 3 | name: string; 4 | flowFile: flowjs.FlowFile; 5 | progress: number; 6 | error: boolean; 7 | paused: boolean; 8 | success: boolean; 9 | complete: boolean; 10 | currentSpeed: number; 11 | averageSpeed: number; 12 | size: number; 13 | timeRemaining: number; 14 | } 15 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/lib/upload-state.ts: -------------------------------------------------------------------------------- 1 | import { Transfer } from './transfer'; 2 | 3 | export interface UploadState { 4 | transfers: Transfer[]; 5 | totalProgress: number; 6 | flow: flowjs.Flow; 7 | } 8 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-flow 3 | */ 4 | export * from './lib/button.directive'; 5 | export * from './lib/drop.directive'; 6 | export * from './lib/src.directive'; 7 | export * from './lib/flow.directive'; 8 | export * from './lib/upload-state'; 9 | export * from './lib/transfer'; 10 | export * from './lib/flow-injection-token'; 11 | export * from './lib/ngx-flow.module'; 12 | export * from './lib/flow-constructor'; 13 | -------------------------------------------------------------------------------- /projects/ngx-flow/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@flowjs/flow.js'; 2 | -------------------------------------------------------------------------------- /projects/ngx-flow/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": ["flowjs"] 10 | }, 11 | "exclude": [ 12 | "**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/ngx-flow/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/ngx-flow/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": ["jasmine", "flowjs"] 7 | }, 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /projects/ngx-flow/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "flow", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "flow", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Sample code for Node.js 2 | 3 | This sample is written for [Node.js](http://nodejs.org/) and requires [Express](http://expressjs.com/) to make the sample code cleaner. 4 | 5 | To install and run: 6 | 7 | cd samples/Node.js 8 | npm install 9 | node app.js 10 | 11 | Then browse to [localhost:3000](http://localhost:3000). 12 | 13 | File chunks will be uploaded to samples/Node.js/tmp directory. 14 | 15 | ## Enabling Cross-domain Uploads 16 | 17 | If you would like to load the flow.js library from one domain and have your Node.js reside on another, you must allow 'Access-Control-Allow-Origin' from '*'. Please remember, there are some potential security risks with enabling this functionality. If you would still like to implement cross-domain uploads, open app.js and uncomment lines 24-31 and uncomment line 17. 18 | 19 | Then in public/index.html, on line 49, update the target with your server's address. For example: target:'http://www.example.com/upload' 20 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | process.env.TMPDIR = 'tmp'; // to avoid the EXDEV rename error, see http://stackoverflow.com/q/21071303/76173 2 | 3 | var express = require('express'); 4 | var multipart = require('connect-multiparty'); 5 | var multipartMiddleware = multipart(); 6 | var flow = require('./flow-node.js')('tmp'); 7 | var app = express(); 8 | 9 | // Configure access control allow origin header stuff 10 | var ACCESS_CONTROLL_ALLOW_ORIGIN = true; 11 | 12 | // Host most stuff in the public folder 13 | app.use(express.static(__dirname + '/public')); 14 | // app.use(express.static(__dirname + '/../../src')); 15 | 16 | // Handle uploads through Flow.js 17 | app.post('/upload', multipartMiddleware, function(req, res) { 18 | flow.post(req, function(status, filename, original_filename, identifier) { 19 | console.log('POST', status, original_filename, identifier); 20 | if (ACCESS_CONTROLL_ALLOW_ORIGIN) { 21 | res.header("Access-Control-Allow-Origin", "*"); 22 | } 23 | res.status(status === "done" || status === "partly_done" ? 200 : 400).send(); 24 | }); 25 | }); 26 | 27 | 28 | app.options('/upload', function(req, res){ 29 | console.log('OPTIONS'); 30 | if (ACCESS_CONTROLL_ALLOW_ORIGIN) { 31 | res.header("Access-Control-Allow-Origin", "*"); 32 | } 33 | res.status(200).send(); 34 | }); 35 | 36 | // Handle status checks on chunks through Flow.js 37 | app.get('/upload', function(req, res) { 38 | flow.get(req, function(status, filename, original_filename, identifier) { 39 | console.log('GET', status); 40 | if (ACCESS_CONTROLL_ALLOW_ORIGIN) { 41 | res.header("Access-Control-Allow-Origin", "*"); 42 | } 43 | 44 | if (status == 'found') { 45 | status = 204; 46 | } else { 47 | status = 204; 48 | } 49 | 50 | res.status(status).send(); 51 | }); 52 | }); 53 | 54 | app.get('/download/:identifier', function(req, res) { 55 | flow.write(req.params.identifier, res); 56 | }); 57 | 58 | app.listen(3000); 59 | -------------------------------------------------------------------------------- /server/flow-node.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | util = require('util'), 4 | Stream = require('stream').Stream; 5 | 6 | module.exports = flow = function(temporaryFolder) { 7 | var $ = this; 8 | $.temporaryFolder = temporaryFolder; 9 | $.maxFileSize = null; 10 | $.fileParameterName = 'file'; 11 | 12 | try { 13 | fs.mkdirSync($.temporaryFolder); 14 | } catch (e) {} 15 | 16 | function cleanIdentifier(identifier) { 17 | return identifier.replace(/[^0-9A-Za-z_-]/g, ''); 18 | } 19 | 20 | function getChunkFilename(chunkNumber, identifier) { 21 | // Clean up the identifier 22 | identifier = cleanIdentifier(identifier); 23 | // What would the file name be? 24 | return path.resolve($.temporaryFolder, './flow-' + identifier + '.' + chunkNumber); 25 | } 26 | 27 | function validateRequest(chunkNumber, chunkSize, totalSize, identifier, filename, fileSize) { 28 | // Clean up the identifier 29 | identifier = cleanIdentifier(identifier); 30 | 31 | // Check if the request is sane 32 | if (chunkNumber == 0 || chunkSize == 0 || totalSize == 0 || identifier.length == 0 || filename.length == 0) { 33 | return 'non_flow_request'; 34 | } 35 | var numberOfChunks = Math.max(Math.floor(totalSize / (chunkSize * 1.0)), 1); 36 | if (chunkNumber > numberOfChunks) { 37 | return 'invalid_flow_request1'; 38 | } 39 | 40 | // Is the file too big? 41 | if ($.maxFileSize && totalSize > $.maxFileSize) { 42 | return 'invalid_flow_request2'; 43 | } 44 | 45 | if (typeof(fileSize) != 'undefined') { 46 | if (chunkNumber < numberOfChunks && fileSize != chunkSize) { 47 | // The chunk in the POST request isn't the correct size 48 | return 'invalid_flow_request3'; 49 | } 50 | if (numberOfChunks > 1 && chunkNumber == numberOfChunks && fileSize != ((totalSize % chunkSize) + parseInt(chunkSize))) { 51 | // The chunks in the POST is the last one, and the fil is not the correct size 52 | return 'invalid_flow_request4'; 53 | } 54 | if (numberOfChunks == 1 && fileSize != totalSize) { 55 | // The file is only a single chunk, and the data size does not fit 56 | return 'invalid_flow_request5'; 57 | } 58 | } 59 | 60 | return 'valid'; 61 | } 62 | 63 | //'found', filename, original_filename, identifier 64 | //'not_found', null, null, null 65 | $.get = function(req, callback) { 66 | var chunkNumber = req.param('flowChunkNumber', 0); 67 | var chunkSize = req.param('flowChunkSize', 0); 68 | var totalSize = req.param('flowTotalSize', 0); 69 | var identifier = req.param('flowIdentifier', ""); 70 | var filename = req.param('flowFilename', ""); 71 | 72 | if (validateRequest(chunkNumber, chunkSize, totalSize, identifier, filename) == 'valid') { 73 | var chunkFilename = getChunkFilename(chunkNumber, identifier); 74 | fs.exists(chunkFilename, function(exists) { 75 | if (exists) { 76 | callback('found', chunkFilename, filename, identifier); 77 | } else { 78 | callback('not_found', null, null, null); 79 | } 80 | }); 81 | } else { 82 | callback('not_found', null, null, null); 83 | } 84 | }; 85 | 86 | //'partly_done', filename, original_filename, identifier 87 | //'done', filename, original_filename, identifier 88 | //'invalid_flow_request', null, null, null 89 | //'non_flow_request', null, null, null 90 | $.post = function(req, callback) { 91 | 92 | var fields = req.body; 93 | var files = req.files; 94 | 95 | var chunkNumber = fields['flowChunkNumber']; 96 | var chunkSize = fields['flowChunkSize']; 97 | var totalSize = fields['flowTotalSize']; 98 | var identifier = cleanIdentifier(fields['flowIdentifier']); 99 | var filename = fields['flowFilename']; 100 | 101 | if (!files[$.fileParameterName] || !files[$.fileParameterName].size) { 102 | callback('invalid_flow_request', null, null, null); 103 | return; 104 | } 105 | 106 | var original_filename = files[$.fileParameterName]['originalFilename']; 107 | var validation = validateRequest(chunkNumber, chunkSize, totalSize, identifier, filename, files[$.fileParameterName].size); 108 | if (validation == 'valid') { 109 | var chunkFilename = getChunkFilename(chunkNumber, identifier); 110 | 111 | // Save the chunk (TODO: OVERWRITE) 112 | fs.rename(files[$.fileParameterName].path, chunkFilename, function() { 113 | 114 | // Do we have all the chunks? 115 | var currentTestChunk = 1; 116 | var numberOfChunks = Math.max(Math.floor(totalSize / (chunkSize * 1.0)), 1); 117 | var testChunkExists = function() { 118 | fs.exists(getChunkFilename(currentTestChunk, identifier), function(exists) { 119 | if (exists) { 120 | currentTestChunk++; 121 | if (currentTestChunk > numberOfChunks) { 122 | callback('done', filename, original_filename, identifier); 123 | } else { 124 | // Recursion 125 | testChunkExists(); 126 | } 127 | } else { 128 | callback('partly_done', filename, original_filename, identifier); 129 | } 130 | }); 131 | }; 132 | testChunkExists(); 133 | }); 134 | } else { 135 | callback(validation, filename, original_filename, identifier); 136 | } 137 | }; 138 | 139 | // Pipe chunks directly in to an existsing WritableStream 140 | // r.write(identifier, response); 141 | // r.write(identifier, response, {end:false}); 142 | // 143 | // var stream = fs.createWriteStream(filename); 144 | // r.write(identifier, stream); 145 | // stream.on('data', function(data){...}); 146 | // stream.on('finish', function(){...}); 147 | $.write = function(identifier, writableStream, options) { 148 | options = options || {}; 149 | options.end = (typeof options['end'] == 'undefined' ? true : options['end']); 150 | 151 | // Iterate over each chunk 152 | var pipeChunk = function(number) { 153 | 154 | var chunkFilename = getChunkFilename(number, identifier); 155 | fs.exists(chunkFilename, function(exists) { 156 | 157 | if (exists) { 158 | // If the chunk with the current number exists, 159 | // then create a ReadStream from the file 160 | // and pipe it to the specified writableStream. 161 | var sourceStream = fs.createReadStream(chunkFilename); 162 | sourceStream.pipe(writableStream, { 163 | end: false 164 | }); 165 | sourceStream.on('end', function() { 166 | // When the chunk is fully streamed, 167 | // jump to the next one 168 | pipeChunk(number + 1); 169 | }); 170 | } else { 171 | // When all the chunks have been piped, end the stream 172 | if (options.end) writableStream.end(); 173 | if (options.onDone) options.onDone(); 174 | } 175 | }); 176 | }; 177 | pipeChunk(1); 178 | }; 179 | 180 | $.clean = function(identifier, options) { 181 | options = options || {}; 182 | 183 | // Iterate over each chunk 184 | var pipeChunkRm = function(number) { 185 | 186 | var chunkFilename = getChunkFilename(number, identifier); 187 | 188 | //console.log('removing pipeChunkRm ', number, 'chunkFilename', chunkFilename); 189 | fs.exists(chunkFilename, function(exists) { 190 | if (exists) { 191 | 192 | console.log('exist removing ', chunkFilename); 193 | fs.unlink(chunkFilename, function(err) { 194 | if (err && options.onError) options.onError(err); 195 | }); 196 | 197 | pipeChunkRm(number + 1); 198 | 199 | } else { 200 | 201 | if (options.onDone) options.onDone(); 202 | 203 | } 204 | }); 205 | }; 206 | pipeChunkRm(1); 207 | }; 208 | 209 | return $; 210 | }; 211 | -------------------------------------------------------------------------------- /server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "server", 8 | "dependencies": { 9 | "connect-multiparty": "^1.0.4", 10 | "express": "^4.3.1" 11 | } 12 | }, 13 | "node_modules/accepts": { 14 | "version": "1.3.8", 15 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 16 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 17 | "dependencies": { 18 | "mime-types": "~2.1.34", 19 | "negotiator": "0.6.3" 20 | }, 21 | "engines": { 22 | "node": ">= 0.6" 23 | } 24 | }, 25 | "node_modules/accepts/node_modules/mime-db": { 26 | "version": "1.52.0", 27 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 28 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 29 | "engines": { 30 | "node": ">= 0.6" 31 | } 32 | }, 33 | "node_modules/accepts/node_modules/mime-types": { 34 | "version": "2.1.35", 35 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 36 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 37 | "dependencies": { 38 | "mime-db": "1.52.0" 39 | }, 40 | "engines": { 41 | "node": ">= 0.6" 42 | } 43 | }, 44 | "node_modules/array-flatten": { 45 | "version": "1.1.1", 46 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 47 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 48 | }, 49 | "node_modules/body-parser": { 50 | "version": "1.20.1", 51 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", 52 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", 53 | "dependencies": { 54 | "bytes": "3.1.2", 55 | "content-type": "~1.0.4", 56 | "debug": "2.6.9", 57 | "depd": "2.0.0", 58 | "destroy": "1.2.0", 59 | "http-errors": "2.0.0", 60 | "iconv-lite": "0.4.24", 61 | "on-finished": "2.4.1", 62 | "qs": "6.11.0", 63 | "raw-body": "2.5.1", 64 | "type-is": "~1.6.18", 65 | "unpipe": "1.0.0" 66 | }, 67 | "engines": { 68 | "node": ">= 0.8", 69 | "npm": "1.2.8000 || >= 1.4.16" 70 | } 71 | }, 72 | "node_modules/body-parser/node_modules/ee-first": { 73 | "version": "1.1.1", 74 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 75 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 76 | }, 77 | "node_modules/body-parser/node_modules/mime-db": { 78 | "version": "1.52.0", 79 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 80 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 81 | "engines": { 82 | "node": ">= 0.6" 83 | } 84 | }, 85 | "node_modules/body-parser/node_modules/mime-types": { 86 | "version": "2.1.35", 87 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 88 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 89 | "dependencies": { 90 | "mime-db": "1.52.0" 91 | }, 92 | "engines": { 93 | "node": ">= 0.6" 94 | } 95 | }, 96 | "node_modules/body-parser/node_modules/on-finished": { 97 | "version": "2.4.1", 98 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 99 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 100 | "dependencies": { 101 | "ee-first": "1.1.1" 102 | }, 103 | "engines": { 104 | "node": ">= 0.8" 105 | } 106 | }, 107 | "node_modules/body-parser/node_modules/qs": { 108 | "version": "6.11.0", 109 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 110 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 111 | "dependencies": { 112 | "side-channel": "^1.0.4" 113 | }, 114 | "engines": { 115 | "node": ">=0.6" 116 | }, 117 | "funding": { 118 | "url": "https://github.com/sponsors/ljharb" 119 | } 120 | }, 121 | "node_modules/body-parser/node_modules/type-is": { 122 | "version": "1.6.18", 123 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 124 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 125 | "dependencies": { 126 | "media-typer": "0.3.0", 127 | "mime-types": "~2.1.24" 128 | }, 129 | "engines": { 130 | "node": ">= 0.6" 131 | } 132 | }, 133 | "node_modules/bytes": { 134 | "version": "3.1.2", 135 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 136 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 137 | "engines": { 138 | "node": ">= 0.8" 139 | } 140 | }, 141 | "node_modules/call-bind": { 142 | "version": "1.0.5", 143 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", 144 | "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", 145 | "dependencies": { 146 | "function-bind": "^1.1.2", 147 | "get-intrinsic": "^1.2.1", 148 | "set-function-length": "^1.1.1" 149 | }, 150 | "funding": { 151 | "url": "https://github.com/sponsors/ljharb" 152 | } 153 | }, 154 | "node_modules/connect-multiparty": { 155 | "version": "1.2.5", 156 | "resolved": "https://registry.npmjs.org/connect-multiparty/-/connect-multiparty-1.2.5.tgz", 157 | "integrity": "sha512-GqcNpxZbpRypIOcAH+dmodJ1u4s5p4QyMzRr7/gcUGkVVi44Hq0rhzPLwRE9h8BWo/UH5W+yg1jDzUV4Tz3PKA==", 158 | "dependencies": { 159 | "multiparty": "~3.3.2", 160 | "on-finished": "~2.1.0", 161 | "qs": "~2.2.4", 162 | "type-is": "~1.5.2" 163 | } 164 | }, 165 | "node_modules/content-disposition": { 166 | "version": "0.5.4", 167 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 168 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 169 | "dependencies": { 170 | "safe-buffer": "5.2.1" 171 | }, 172 | "engines": { 173 | "node": ">= 0.6" 174 | } 175 | }, 176 | "node_modules/content-type": { 177 | "version": "1.0.5", 178 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 179 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 180 | "engines": { 181 | "node": ">= 0.6" 182 | } 183 | }, 184 | "node_modules/cookie": { 185 | "version": "0.5.0", 186 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 187 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", 188 | "engines": { 189 | "node": ">= 0.6" 190 | } 191 | }, 192 | "node_modules/cookie-signature": { 193 | "version": "1.0.6", 194 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 195 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 196 | }, 197 | "node_modules/core-util-is": { 198 | "version": "1.0.2", 199 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 200 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 201 | }, 202 | "node_modules/debug": { 203 | "version": "2.6.9", 204 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 205 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 206 | "dependencies": { 207 | "ms": "2.0.0" 208 | } 209 | }, 210 | "node_modules/define-data-property": { 211 | "version": "1.1.1", 212 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", 213 | "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", 214 | "dependencies": { 215 | "get-intrinsic": "^1.2.1", 216 | "gopd": "^1.0.1", 217 | "has-property-descriptors": "^1.0.0" 218 | }, 219 | "engines": { 220 | "node": ">= 0.4" 221 | } 222 | }, 223 | "node_modules/depd": { 224 | "version": "2.0.0", 225 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 226 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 227 | "engines": { 228 | "node": ">= 0.8" 229 | } 230 | }, 231 | "node_modules/destroy": { 232 | "version": "1.2.0", 233 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 234 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 235 | "engines": { 236 | "node": ">= 0.8", 237 | "npm": "1.2.8000 || >= 1.4.16" 238 | } 239 | }, 240 | "node_modules/ee-first": { 241 | "version": "1.1.0", 242 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz", 243 | "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q=" 244 | }, 245 | "node_modules/encodeurl": { 246 | "version": "1.0.2", 247 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 248 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 249 | "engines": { 250 | "node": ">= 0.8" 251 | } 252 | }, 253 | "node_modules/escape-html": { 254 | "version": "1.0.3", 255 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 256 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 257 | }, 258 | "node_modules/etag": { 259 | "version": "1.8.1", 260 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 261 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 262 | "engines": { 263 | "node": ">= 0.6" 264 | } 265 | }, 266 | "node_modules/express": { 267 | "version": "4.18.2", 268 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", 269 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", 270 | "dependencies": { 271 | "accepts": "~1.3.8", 272 | "array-flatten": "1.1.1", 273 | "body-parser": "1.20.1", 274 | "content-disposition": "0.5.4", 275 | "content-type": "~1.0.4", 276 | "cookie": "0.5.0", 277 | "cookie-signature": "1.0.6", 278 | "debug": "2.6.9", 279 | "depd": "2.0.0", 280 | "encodeurl": "~1.0.2", 281 | "escape-html": "~1.0.3", 282 | "etag": "~1.8.1", 283 | "finalhandler": "1.2.0", 284 | "fresh": "0.5.2", 285 | "http-errors": "2.0.0", 286 | "merge-descriptors": "1.0.1", 287 | "methods": "~1.1.2", 288 | "on-finished": "2.4.1", 289 | "parseurl": "~1.3.3", 290 | "path-to-regexp": "0.1.7", 291 | "proxy-addr": "~2.0.7", 292 | "qs": "6.11.0", 293 | "range-parser": "~1.2.1", 294 | "safe-buffer": "5.2.1", 295 | "send": "0.18.0", 296 | "serve-static": "1.15.0", 297 | "setprototypeof": "1.2.0", 298 | "statuses": "2.0.1", 299 | "type-is": "~1.6.18", 300 | "utils-merge": "1.0.1", 301 | "vary": "~1.1.2" 302 | }, 303 | "engines": { 304 | "node": ">= 0.10.0" 305 | } 306 | }, 307 | "node_modules/express/node_modules/ee-first": { 308 | "version": "1.1.1", 309 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 310 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 311 | }, 312 | "node_modules/express/node_modules/mime-db": { 313 | "version": "1.52.0", 314 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 315 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 316 | "engines": { 317 | "node": ">= 0.6" 318 | } 319 | }, 320 | "node_modules/express/node_modules/mime-types": { 321 | "version": "2.1.35", 322 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 323 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 324 | "dependencies": { 325 | "mime-db": "1.52.0" 326 | }, 327 | "engines": { 328 | "node": ">= 0.6" 329 | } 330 | }, 331 | "node_modules/express/node_modules/on-finished": { 332 | "version": "2.4.1", 333 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 334 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 335 | "dependencies": { 336 | "ee-first": "1.1.1" 337 | }, 338 | "engines": { 339 | "node": ">= 0.8" 340 | } 341 | }, 342 | "node_modules/express/node_modules/qs": { 343 | "version": "6.11.0", 344 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 345 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 346 | "dependencies": { 347 | "side-channel": "^1.0.4" 348 | }, 349 | "engines": { 350 | "node": ">=0.6" 351 | }, 352 | "funding": { 353 | "url": "https://github.com/sponsors/ljharb" 354 | } 355 | }, 356 | "node_modules/express/node_modules/type-is": { 357 | "version": "1.6.18", 358 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 359 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 360 | "dependencies": { 361 | "media-typer": "0.3.0", 362 | "mime-types": "~2.1.24" 363 | }, 364 | "engines": { 365 | "node": ">= 0.6" 366 | } 367 | }, 368 | "node_modules/finalhandler": { 369 | "version": "1.2.0", 370 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 371 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 372 | "dependencies": { 373 | "debug": "2.6.9", 374 | "encodeurl": "~1.0.2", 375 | "escape-html": "~1.0.3", 376 | "on-finished": "2.4.1", 377 | "parseurl": "~1.3.3", 378 | "statuses": "2.0.1", 379 | "unpipe": "~1.0.0" 380 | }, 381 | "engines": { 382 | "node": ">= 0.8" 383 | } 384 | }, 385 | "node_modules/finalhandler/node_modules/ee-first": { 386 | "version": "1.1.1", 387 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 388 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 389 | }, 390 | "node_modules/finalhandler/node_modules/on-finished": { 391 | "version": "2.4.1", 392 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 393 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 394 | "dependencies": { 395 | "ee-first": "1.1.1" 396 | }, 397 | "engines": { 398 | "node": ">= 0.8" 399 | } 400 | }, 401 | "node_modules/forwarded": { 402 | "version": "0.2.0", 403 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 404 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 405 | "engines": { 406 | "node": ">= 0.6" 407 | } 408 | }, 409 | "node_modules/fresh": { 410 | "version": "0.5.2", 411 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 412 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 413 | "engines": { 414 | "node": ">= 0.6" 415 | } 416 | }, 417 | "node_modules/function-bind": { 418 | "version": "1.1.2", 419 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 420 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 421 | "funding": { 422 | "url": "https://github.com/sponsors/ljharb" 423 | } 424 | }, 425 | "node_modules/get-intrinsic": { 426 | "version": "1.2.2", 427 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", 428 | "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", 429 | "dependencies": { 430 | "function-bind": "^1.1.2", 431 | "has-proto": "^1.0.1", 432 | "has-symbols": "^1.0.3", 433 | "hasown": "^2.0.0" 434 | }, 435 | "funding": { 436 | "url": "https://github.com/sponsors/ljharb" 437 | } 438 | }, 439 | "node_modules/gopd": { 440 | "version": "1.0.1", 441 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 442 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 443 | "dependencies": { 444 | "get-intrinsic": "^1.1.3" 445 | }, 446 | "funding": { 447 | "url": "https://github.com/sponsors/ljharb" 448 | } 449 | }, 450 | "node_modules/has-property-descriptors": { 451 | "version": "1.0.1", 452 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", 453 | "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", 454 | "dependencies": { 455 | "get-intrinsic": "^1.2.2" 456 | }, 457 | "funding": { 458 | "url": "https://github.com/sponsors/ljharb" 459 | } 460 | }, 461 | "node_modules/has-proto": { 462 | "version": "1.0.1", 463 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", 464 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", 465 | "engines": { 466 | "node": ">= 0.4" 467 | }, 468 | "funding": { 469 | "url": "https://github.com/sponsors/ljharb" 470 | } 471 | }, 472 | "node_modules/has-symbols": { 473 | "version": "1.0.3", 474 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 475 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 476 | "engines": { 477 | "node": ">= 0.4" 478 | }, 479 | "funding": { 480 | "url": "https://github.com/sponsors/ljharb" 481 | } 482 | }, 483 | "node_modules/hasown": { 484 | "version": "2.0.0", 485 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", 486 | "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", 487 | "dependencies": { 488 | "function-bind": "^1.1.2" 489 | }, 490 | "engines": { 491 | "node": ">= 0.4" 492 | } 493 | }, 494 | "node_modules/http-errors": { 495 | "version": "2.0.0", 496 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 497 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 498 | "dependencies": { 499 | "depd": "2.0.0", 500 | "inherits": "2.0.4", 501 | "setprototypeof": "1.2.0", 502 | "statuses": "2.0.1", 503 | "toidentifier": "1.0.1" 504 | }, 505 | "engines": { 506 | "node": ">= 0.8" 507 | } 508 | }, 509 | "node_modules/iconv-lite": { 510 | "version": "0.4.24", 511 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 512 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 513 | "dependencies": { 514 | "safer-buffer": ">= 2.1.2 < 3" 515 | }, 516 | "engines": { 517 | "node": ">=0.10.0" 518 | } 519 | }, 520 | "node_modules/inherits": { 521 | "version": "2.0.4", 522 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 523 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 524 | }, 525 | "node_modules/ipaddr.js": { 526 | "version": "1.9.1", 527 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 528 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 529 | "engines": { 530 | "node": ">= 0.10" 531 | } 532 | }, 533 | "node_modules/isarray": { 534 | "version": "0.0.1", 535 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 536 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" 537 | }, 538 | "node_modules/media-typer": { 539 | "version": "0.3.0", 540 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 541 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", 542 | "engines": { 543 | "node": ">= 0.6" 544 | } 545 | }, 546 | "node_modules/merge-descriptors": { 547 | "version": "1.0.1", 548 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 549 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 550 | }, 551 | "node_modules/methods": { 552 | "version": "1.1.2", 553 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 554 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", 555 | "engines": { 556 | "node": ">= 0.6" 557 | } 558 | }, 559 | "node_modules/mime": { 560 | "version": "1.6.0", 561 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 562 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 563 | "bin": { 564 | "mime": "cli.js" 565 | }, 566 | "engines": { 567 | "node": ">=4" 568 | } 569 | }, 570 | "node_modules/mime-db": { 571 | "version": "1.12.0", 572 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz", 573 | "integrity": "sha1-PQxjGA9FjrENMlqqN9fFiuMS6dc=", 574 | "engines": { 575 | "node": ">= 0.6" 576 | } 577 | }, 578 | "node_modules/mime-types": { 579 | "version": "2.0.14", 580 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", 581 | "integrity": "sha1-MQ4VnbI+B3+Lsit0jav6SVcUCqY=", 582 | "dependencies": { 583 | "mime-db": "~1.12.0" 584 | }, 585 | "engines": { 586 | "node": ">= 0.6" 587 | } 588 | }, 589 | "node_modules/ms": { 590 | "version": "2.0.0", 591 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 592 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 593 | }, 594 | "node_modules/multiparty": { 595 | "version": "3.3.2", 596 | "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-3.3.2.tgz", 597 | "integrity": "sha1-Nd5oBNwZZD5SSfPT473GyM4wHT8=", 598 | "dependencies": { 599 | "readable-stream": "~1.1.9", 600 | "stream-counter": "~0.2.0" 601 | }, 602 | "engines": { 603 | "node": ">=0.8.0" 604 | } 605 | }, 606 | "node_modules/negotiator": { 607 | "version": "0.6.3", 608 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 609 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 610 | "engines": { 611 | "node": ">= 0.6" 612 | } 613 | }, 614 | "node_modules/object-inspect": { 615 | "version": "1.13.1", 616 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", 617 | "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", 618 | "funding": { 619 | "url": "https://github.com/sponsors/ljharb" 620 | } 621 | }, 622 | "node_modules/on-finished": { 623 | "version": "2.1.1", 624 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz", 625 | "integrity": "sha1-+CyhyeOk8yhrG5k4YQ5bhja9PLI=", 626 | "dependencies": { 627 | "ee-first": "1.1.0" 628 | }, 629 | "engines": { 630 | "node": ">= 0.8" 631 | } 632 | }, 633 | "node_modules/parseurl": { 634 | "version": "1.3.3", 635 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 636 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 637 | "engines": { 638 | "node": ">= 0.8" 639 | } 640 | }, 641 | "node_modules/path-to-regexp": { 642 | "version": "0.1.7", 643 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 644 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 645 | }, 646 | "node_modules/proxy-addr": { 647 | "version": "2.0.7", 648 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 649 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 650 | "dependencies": { 651 | "forwarded": "0.2.0", 652 | "ipaddr.js": "1.9.1" 653 | }, 654 | "engines": { 655 | "node": ">= 0.10" 656 | } 657 | }, 658 | "node_modules/qs": { 659 | "version": "2.2.5", 660 | "resolved": "https://registry.npmjs.org/qs/-/qs-2.2.5.tgz", 661 | "integrity": "sha1-EIirr53MCuWuRbcJ5sa1iIsjkjw=" 662 | }, 663 | "node_modules/range-parser": { 664 | "version": "1.2.1", 665 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 666 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 667 | "engines": { 668 | "node": ">= 0.6" 669 | } 670 | }, 671 | "node_modules/raw-body": { 672 | "version": "2.5.1", 673 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 674 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 675 | "dependencies": { 676 | "bytes": "3.1.2", 677 | "http-errors": "2.0.0", 678 | "iconv-lite": "0.4.24", 679 | "unpipe": "1.0.0" 680 | }, 681 | "engines": { 682 | "node": ">= 0.8" 683 | } 684 | }, 685 | "node_modules/readable-stream": { 686 | "version": "1.1.14", 687 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 688 | "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", 689 | "dependencies": { 690 | "core-util-is": "~1.0.0", 691 | "inherits": "~2.0.1", 692 | "isarray": "0.0.1", 693 | "string_decoder": "~0.10.x" 694 | } 695 | }, 696 | "node_modules/safe-buffer": { 697 | "version": "5.2.1", 698 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 699 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 700 | "funding": [ 701 | { 702 | "type": "github", 703 | "url": "https://github.com/sponsors/feross" 704 | }, 705 | { 706 | "type": "patreon", 707 | "url": "https://www.patreon.com/feross" 708 | }, 709 | { 710 | "type": "consulting", 711 | "url": "https://feross.org/support" 712 | } 713 | ] 714 | }, 715 | "node_modules/safer-buffer": { 716 | "version": "2.1.2", 717 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 718 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 719 | }, 720 | "node_modules/send": { 721 | "version": "0.18.0", 722 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 723 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 724 | "dependencies": { 725 | "debug": "2.6.9", 726 | "depd": "2.0.0", 727 | "destroy": "1.2.0", 728 | "encodeurl": "~1.0.2", 729 | "escape-html": "~1.0.3", 730 | "etag": "~1.8.1", 731 | "fresh": "0.5.2", 732 | "http-errors": "2.0.0", 733 | "mime": "1.6.0", 734 | "ms": "2.1.3", 735 | "on-finished": "2.4.1", 736 | "range-parser": "~1.2.1", 737 | "statuses": "2.0.1" 738 | }, 739 | "engines": { 740 | "node": ">= 0.8.0" 741 | } 742 | }, 743 | "node_modules/send/node_modules/ee-first": { 744 | "version": "1.1.1", 745 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 746 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 747 | }, 748 | "node_modules/send/node_modules/ms": { 749 | "version": "2.1.3", 750 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 751 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 752 | }, 753 | "node_modules/send/node_modules/on-finished": { 754 | "version": "2.4.1", 755 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 756 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 757 | "dependencies": { 758 | "ee-first": "1.1.1" 759 | }, 760 | "engines": { 761 | "node": ">= 0.8" 762 | } 763 | }, 764 | "node_modules/serve-static": { 765 | "version": "1.15.0", 766 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 767 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 768 | "dependencies": { 769 | "encodeurl": "~1.0.2", 770 | "escape-html": "~1.0.3", 771 | "parseurl": "~1.3.3", 772 | "send": "0.18.0" 773 | }, 774 | "engines": { 775 | "node": ">= 0.8.0" 776 | } 777 | }, 778 | "node_modules/set-function-length": { 779 | "version": "1.1.1", 780 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", 781 | "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", 782 | "dependencies": { 783 | "define-data-property": "^1.1.1", 784 | "get-intrinsic": "^1.2.1", 785 | "gopd": "^1.0.1", 786 | "has-property-descriptors": "^1.0.0" 787 | }, 788 | "engines": { 789 | "node": ">= 0.4" 790 | } 791 | }, 792 | "node_modules/setprototypeof": { 793 | "version": "1.2.0", 794 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 795 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 796 | }, 797 | "node_modules/side-channel": { 798 | "version": "1.0.4", 799 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 800 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 801 | "dependencies": { 802 | "call-bind": "^1.0.0", 803 | "get-intrinsic": "^1.0.2", 804 | "object-inspect": "^1.9.0" 805 | }, 806 | "funding": { 807 | "url": "https://github.com/sponsors/ljharb" 808 | } 809 | }, 810 | "node_modules/statuses": { 811 | "version": "2.0.1", 812 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 813 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 814 | "engines": { 815 | "node": ">= 0.8" 816 | } 817 | }, 818 | "node_modules/stream-counter": { 819 | "version": "0.2.0", 820 | "resolved": "https://registry.npmjs.org/stream-counter/-/stream-counter-0.2.0.tgz", 821 | "integrity": "sha1-3tJmVWMZyLDiIoErnPOyb6fZR94=", 822 | "dependencies": { 823 | "readable-stream": "~1.1.8" 824 | }, 825 | "engines": { 826 | "node": ">=0.8.0" 827 | } 828 | }, 829 | "node_modules/string_decoder": { 830 | "version": "0.10.31", 831 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 832 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" 833 | }, 834 | "node_modules/toidentifier": { 835 | "version": "1.0.1", 836 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 837 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 838 | "engines": { 839 | "node": ">=0.6" 840 | } 841 | }, 842 | "node_modules/type-is": { 843 | "version": "1.5.7", 844 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.5.7.tgz", 845 | "integrity": "sha1-uTaKWTzG730GReeLL0xky+zQXpA=", 846 | "dependencies": { 847 | "media-typer": "0.3.0", 848 | "mime-types": "~2.0.9" 849 | }, 850 | "engines": { 851 | "node": ">= 0.6" 852 | } 853 | }, 854 | "node_modules/unpipe": { 855 | "version": "1.0.0", 856 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 857 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 858 | "engines": { 859 | "node": ">= 0.8" 860 | } 861 | }, 862 | "node_modules/utils-merge": { 863 | "version": "1.0.1", 864 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 865 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", 866 | "engines": { 867 | "node": ">= 0.4.0" 868 | } 869 | }, 870 | "node_modules/vary": { 871 | "version": "1.1.2", 872 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 873 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", 874 | "engines": { 875 | "node": ">= 0.8" 876 | } 877 | } 878 | } 879 | } 880 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "express": "^4.3.1", 4 | "connect-multiparty": "^1.0.4" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /server/public/flow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | */ 4 | (function(window, document, undefined) {'use strict'; 5 | // ie10+ 6 | var ie10plus = window.navigator.msPointerEnabled; 7 | /** 8 | * Flow.js is a library providing multiple simultaneous, stable and 9 | * resumable uploads via the HTML5 File API. 10 | * @param [opts] 11 | * @param {number} [opts.chunkSize] 12 | * @param {bool} [opts.forceChunkSize] 13 | * @param {number} [opts.simultaneousUploads] 14 | * @param {bool} [opts.singleFile] 15 | * @param {string} [opts.fileParameterName] 16 | * @param {number} [opts.progressCallbacksInterval] 17 | * @param {number} [opts.speedSmoothingFactor] 18 | * @param {Object|Function} [opts.query] 19 | * @param {Object|Function} [opts.headers] 20 | * @param {bool} [opts.withCredentials] 21 | * @param {Function} [opts.preprocess] 22 | * @param {string} [opts.method] 23 | * @param {string|Function} [opts.testMethod] 24 | * @param {string|Function} [opts.uploadMethod] 25 | * @param {bool} [opts.prioritizeFirstAndLastChunk] 26 | * @param {bool} [opts.allowDuplicateUploads] 27 | * @param {string|Function} [opts.target] 28 | * @param {number} [opts.maxChunkRetries] 29 | * @param {number} [opts.chunkRetryInterval] 30 | * @param {Array.} [opts.permanentErrors] 31 | * @param {Array.} [opts.successStatuses] 32 | * @param {Function} [opts.initFileFn] 33 | * @param {Function} [opts.readFileFn] 34 | * @param {Function} [opts.generateUniqueIdentifier] 35 | * @constructor 36 | */ 37 | function Flow(opts) { 38 | /** 39 | * Supported by browser? 40 | * @type {boolean} 41 | */ 42 | this.support = ( 43 | typeof File !== 'undefined' && 44 | typeof Blob !== 'undefined' && 45 | typeof FileList !== 'undefined' && 46 | ( 47 | !!Blob.prototype.slice || !!Blob.prototype.webkitSlice || !!Blob.prototype.mozSlice || 48 | false 49 | ) // slicing files support 50 | ); 51 | 52 | if (!this.support) { 53 | return ; 54 | } 55 | 56 | /** 57 | * Check if directory upload is supported 58 | * @type {boolean} 59 | */ 60 | this.supportDirectory = ( 61 | /Chrome/.test(window.navigator.userAgent) || 62 | /Firefox/.test(window.navigator.userAgent) || 63 | /Edge/.test(window.navigator.userAgent) 64 | ); 65 | 66 | /** 67 | * List of FlowFile objects 68 | * @type {Array.} 69 | */ 70 | this.files = []; 71 | 72 | /** 73 | * Default options for flow.js 74 | * @type {Object} 75 | */ 76 | this.defaults = { 77 | chunkSize: 1024 * 1024, 78 | forceChunkSize: false, 79 | simultaneousUploads: 3, 80 | singleFile: false, 81 | fileParameterName: 'file', 82 | progressCallbacksInterval: 500, 83 | speedSmoothingFactor: 0.1, 84 | query: {}, 85 | headers: {}, 86 | withCredentials: false, 87 | preprocess: null, 88 | method: 'multipart', 89 | testMethod: 'GET', 90 | uploadMethod: 'POST', 91 | prioritizeFirstAndLastChunk: false, 92 | allowDuplicateUploads: false, 93 | target: '/', 94 | testChunks: true, 95 | generateUniqueIdentifier: null, 96 | maxChunkRetries: 0, 97 | chunkRetryInterval: null, 98 | permanentErrors: [404, 413, 415, 500, 501], 99 | successStatuses: [200, 201, 202], 100 | onDropStopPropagation: false, 101 | initFileFn: null, 102 | readFileFn: webAPIFileRead 103 | }; 104 | 105 | /** 106 | * Current options 107 | * @type {Object} 108 | */ 109 | this.opts = {}; 110 | 111 | /** 112 | * List of events: 113 | * key stands for event name 114 | * value array list of callbacks 115 | * @type {} 116 | */ 117 | this.events = {}; 118 | 119 | var $ = this; 120 | 121 | /** 122 | * On drop event 123 | * @function 124 | * @param {MouseEvent} event 125 | */ 126 | this.onDrop = function (event) { 127 | if ($.opts.onDropStopPropagation) { 128 | event.stopPropagation(); 129 | } 130 | event.preventDefault(); 131 | var dataTransfer = event.dataTransfer; 132 | if (dataTransfer.items && dataTransfer.items[0] && 133 | dataTransfer.items[0].webkitGetAsEntry) { 134 | $.webkitReadDataTransfer(event); 135 | } else { 136 | $.addFiles(dataTransfer.files, event); 137 | } 138 | }; 139 | 140 | /** 141 | * Prevent default 142 | * @function 143 | * @param {MouseEvent} event 144 | */ 145 | this.preventEvent = function (event) { 146 | event.preventDefault(); 147 | }; 148 | 149 | 150 | /** 151 | * Current options 152 | * @type {Object} 153 | */ 154 | this.opts = Flow.extend({}, this.defaults, opts || {}); 155 | 156 | } 157 | 158 | Flow.prototype = { 159 | /** 160 | * Set a callback for an event, possible events: 161 | * fileSuccess(file), fileProgress(file), fileAdded(file, event), 162 | * fileRemoved(file), fileRetry(file), fileError(file, message), 163 | * complete(), progress(), error(message, file), pause() 164 | * @function 165 | * @param {string} event 166 | * @param {Function} callback 167 | */ 168 | on: function (event, callback) { 169 | event = event.toLowerCase(); 170 | if (!this.events.hasOwnProperty(event)) { 171 | this.events[event] = []; 172 | } 173 | this.events[event].push(callback); 174 | }, 175 | 176 | /** 177 | * Remove event callback 178 | * @function 179 | * @param {string} [event] removes all events if not specified 180 | * @param {Function} [fn] removes all callbacks of event if not specified 181 | */ 182 | off: function (event, fn) { 183 | if (event !== undefined) { 184 | event = event.toLowerCase(); 185 | if (fn !== undefined) { 186 | if (this.events.hasOwnProperty(event)) { 187 | arrayRemove(this.events[event], fn); 188 | } 189 | } else { 190 | delete this.events[event]; 191 | } 192 | } else { 193 | this.events = {}; 194 | } 195 | }, 196 | 197 | /** 198 | * Fire an event 199 | * @function 200 | * @param {string} event event name 201 | * @param {...} args arguments of a callback 202 | * @return {bool} value is false if at least one of the event handlers which handled this event 203 | * returned false. Otherwise it returns true. 204 | */ 205 | fire: function (event, args) { 206 | // `arguments` is an object, not array, in FF, so: 207 | args = Array.prototype.slice.call(arguments); 208 | event = event.toLowerCase(); 209 | var preventDefault = false; 210 | if (this.events.hasOwnProperty(event)) { 211 | each(this.events[event], function (callback) { 212 | preventDefault = callback.apply(this, args.slice(1)) === false || preventDefault; 213 | }, this); 214 | } 215 | if (event != 'catchall') { 216 | args.unshift('catchAll'); 217 | preventDefault = this.fire.apply(this, args) === false || preventDefault; 218 | } 219 | return !preventDefault; 220 | }, 221 | 222 | /** 223 | * Read webkit dataTransfer object 224 | * @param event 225 | */ 226 | webkitReadDataTransfer: function (event) { 227 | var $ = this; 228 | var queue = event.dataTransfer.items.length; 229 | var files = []; 230 | each(event.dataTransfer.items, function (item) { 231 | var entry = item.webkitGetAsEntry(); 232 | if (!entry) { 233 | decrement(); 234 | return ; 235 | } 236 | if (entry.isFile) { 237 | // due to a bug in Chrome's File System API impl - #149735 238 | fileReadSuccess(item.getAsFile(), entry.fullPath); 239 | } else { 240 | readDirectory(entry.createReader()); 241 | } 242 | }); 243 | function readDirectory(reader) { 244 | reader.readEntries(function (entries) { 245 | if (entries.length) { 246 | queue += entries.length; 247 | each(entries, function(entry) { 248 | if (entry.isFile) { 249 | var fullPath = entry.fullPath; 250 | entry.file(function (file) { 251 | fileReadSuccess(file, fullPath); 252 | }, readError); 253 | } else if (entry.isDirectory) { 254 | readDirectory(entry.createReader()); 255 | } 256 | }); 257 | readDirectory(reader); 258 | } else { 259 | decrement(); 260 | } 261 | }, readError); 262 | } 263 | function fileReadSuccess(file, fullPath) { 264 | // relative path should not start with "/" 265 | file.relativePath = fullPath.substring(1); 266 | files.push(file); 267 | decrement(); 268 | } 269 | function readError(fileError) { 270 | throw fileError; 271 | } 272 | function decrement() { 273 | if (--queue == 0) { 274 | $.addFiles(files, event); 275 | } 276 | } 277 | }, 278 | 279 | /** 280 | * Generate unique identifier for a file 281 | * @function 282 | * @param {FlowFile} file 283 | * @returns {string} 284 | */ 285 | generateUniqueIdentifier: function (file) { 286 | var custom = this.opts.generateUniqueIdentifier; 287 | if (typeof custom === 'function') { 288 | return custom(file); 289 | } 290 | // Some confusion in different versions of Firefox 291 | var relativePath = file.relativePath || file.webkitRelativePath || file.fileName || file.name; 292 | return file.size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, ''); 293 | }, 294 | 295 | /** 296 | * Upload next chunk from the queue 297 | * @function 298 | * @returns {boolean} 299 | * @private 300 | */ 301 | uploadNextChunk: function (preventEvents) { 302 | // In some cases (such as videos) it's really handy to upload the first 303 | // and last chunk of a file quickly; this let's the server check the file's 304 | // metadata and determine if there's even a point in continuing. 305 | var found = false; 306 | if (this.opts.prioritizeFirstAndLastChunk) { 307 | each(this.files, function (file) { 308 | if (!file.paused && file.chunks.length && 309 | file.chunks[0].status() === 'pending') { 310 | file.chunks[0].send(); 311 | found = true; 312 | return false; 313 | } 314 | if (!file.paused && file.chunks.length > 1 && 315 | file.chunks[file.chunks.length - 1].status() === 'pending') { 316 | file.chunks[file.chunks.length - 1].send(); 317 | found = true; 318 | return false; 319 | } 320 | }); 321 | if (found) { 322 | return found; 323 | } 324 | } 325 | 326 | // Now, simply look for the next, best thing to upload 327 | each(this.files, function (file) { 328 | if (!file.paused) { 329 | each(file.chunks, function (chunk) { 330 | if (chunk.status() === 'pending') { 331 | chunk.send(); 332 | found = true; 333 | return false; 334 | } 335 | }); 336 | } 337 | if (found) { 338 | return false; 339 | } 340 | }); 341 | if (found) { 342 | return true; 343 | } 344 | 345 | // The are no more outstanding chunks to upload, check is everything is done 346 | var outstanding = false; 347 | each(this.files, function (file) { 348 | if (!file.isComplete()) { 349 | outstanding = true; 350 | return false; 351 | } 352 | }); 353 | if (!outstanding && !preventEvents) { 354 | // All chunks have been uploaded, complete 355 | async(function () { 356 | this.fire('complete'); 357 | }, this); 358 | } 359 | return false; 360 | }, 361 | 362 | 363 | /** 364 | * Assign a browse action to one or more DOM nodes. 365 | * @function 366 | * @param {Element|Array.} domNodes 367 | * @param {boolean} isDirectory Pass in true to allow directories to 368 | * @param {boolean} singleFile prevent multi file upload 369 | * @param {Object} attributes set custom attributes: 370 | * http://www.w3.org/TR/html-markup/input.file.html#input.file-attributes 371 | * eg: accept: 'image/*' 372 | * be selected (Chrome only). 373 | */ 374 | assignBrowse: function (domNodes, isDirectory, singleFile, attributes) { 375 | if (domNodes instanceof Element) { 376 | domNodes = [domNodes]; 377 | } 378 | 379 | each(domNodes, function (domNode) { 380 | var input; 381 | if (domNode.tagName === 'INPUT' && domNode.type === 'file') { 382 | input = domNode; 383 | } else { 384 | input = document.createElement('input'); 385 | input.setAttribute('type', 'file'); 386 | // display:none - not working in opera 12 387 | extend(input.style, { 388 | visibility: 'hidden', 389 | position: 'absolute', 390 | width: '1px', 391 | height: '1px' 392 | }); 393 | // for opera 12 browser, input must be assigned to a document 394 | domNode.appendChild(input); 395 | // https://developer.mozilla.org/en/using_files_from_web_applications) 396 | // event listener is executed two times 397 | // first one - original mouse click event 398 | // second - input.click(), input is inside domNode 399 | domNode.addEventListener('click', function() { 400 | input.click(); 401 | }, false); 402 | } 403 | if (!this.opts.singleFile && !singleFile) { 404 | input.setAttribute('multiple', 'multiple'); 405 | } 406 | if (isDirectory) { 407 | input.setAttribute('webkitdirectory', 'webkitdirectory'); 408 | } 409 | each(attributes, function (value, key) { 410 | input.setAttribute(key, value); 411 | }); 412 | // When new files are added, simply append them to the overall list 413 | var $ = this; 414 | input.addEventListener('change', function (e) { 415 | if (e.target.value) { 416 | $.addFiles(e.target.files, e); 417 | e.target.value = ''; 418 | } 419 | }, false); 420 | }, this); 421 | }, 422 | 423 | /** 424 | * Assign one or more DOM nodes as a drop target. 425 | * @function 426 | * @param {Element|Array.} domNodes 427 | */ 428 | assignDrop: function (domNodes) { 429 | if (typeof domNodes.length === 'undefined') { 430 | domNodes = [domNodes]; 431 | } 432 | each(domNodes, function (domNode) { 433 | domNode.addEventListener('dragover', this.preventEvent, false); 434 | domNode.addEventListener('dragenter', this.preventEvent, false); 435 | domNode.addEventListener('drop', this.onDrop, false); 436 | }, this); 437 | }, 438 | 439 | /** 440 | * Un-assign drop event from DOM nodes 441 | * @function 442 | * @param domNodes 443 | */ 444 | unAssignDrop: function (domNodes) { 445 | if (typeof domNodes.length === 'undefined') { 446 | domNodes = [domNodes]; 447 | } 448 | each(domNodes, function (domNode) { 449 | domNode.removeEventListener('dragover', this.preventEvent); 450 | domNode.removeEventListener('dragenter', this.preventEvent); 451 | domNode.removeEventListener('drop', this.onDrop); 452 | }, this); 453 | }, 454 | 455 | /** 456 | * Returns a boolean indicating whether or not the instance is currently 457 | * uploading anything. 458 | * @function 459 | * @returns {boolean} 460 | */ 461 | isUploading: function () { 462 | var uploading = false; 463 | each(this.files, function (file) { 464 | if (file.isUploading()) { 465 | uploading = true; 466 | return false; 467 | } 468 | }); 469 | return uploading; 470 | }, 471 | 472 | /** 473 | * should upload next chunk 474 | * @function 475 | * @returns {boolean|number} 476 | */ 477 | _shouldUploadNext: function () { 478 | var num = 0; 479 | var should = true; 480 | var simultaneousUploads = this.opts.simultaneousUploads; 481 | each(this.files, function (file) { 482 | each(file.chunks, function(chunk) { 483 | if (chunk.status() === 'uploading') { 484 | num++; 485 | if (num >= simultaneousUploads) { 486 | should = false; 487 | return false; 488 | } 489 | } 490 | }); 491 | }); 492 | // if should is true then return uploading chunks's length 493 | return should && num; 494 | }, 495 | 496 | /** 497 | * Start or resume uploading. 498 | * @function 499 | */ 500 | upload: function () { 501 | // Make sure we don't start too many uploads at once 502 | var ret = this._shouldUploadNext(); 503 | if (ret === false) { 504 | return; 505 | } 506 | // Kick off the queue 507 | this.fire('uploadStart'); 508 | var started = false; 509 | for (var num = 1; num <= this.opts.simultaneousUploads - ret; num++) { 510 | started = this.uploadNextChunk(true) || started; 511 | } 512 | if (!started) { 513 | async(function () { 514 | this.fire('complete'); 515 | }, this); 516 | } 517 | }, 518 | 519 | /** 520 | * Resume uploading. 521 | * @function 522 | */ 523 | resume: function () { 524 | each(this.files, function (file) { 525 | if (!file.isComplete()) { 526 | file.resume(); 527 | } 528 | }); 529 | }, 530 | 531 | /** 532 | * Pause uploading. 533 | * @function 534 | */ 535 | pause: function () { 536 | each(this.files, function (file) { 537 | file.pause(); 538 | }); 539 | }, 540 | 541 | /** 542 | * Cancel upload of all FlowFile objects and remove them from the list. 543 | * @function 544 | */ 545 | cancel: function () { 546 | for (var i = this.files.length - 1; i >= 0; i--) { 547 | this.files[i].cancel(); 548 | } 549 | }, 550 | 551 | /** 552 | * Returns a number between 0 and 1 indicating the current upload progress 553 | * of all files. 554 | * @function 555 | * @returns {number} 556 | */ 557 | progress: function () { 558 | var totalDone = 0; 559 | var totalSize = 0; 560 | // Resume all chunks currently being uploaded 561 | each(this.files, function (file) { 562 | totalDone += file.progress() * file.size; 563 | totalSize += file.size; 564 | }); 565 | return totalSize > 0 ? totalDone / totalSize : 0; 566 | }, 567 | 568 | /** 569 | * Add a HTML5 File object to the list of files. 570 | * @function 571 | * @param {File} file 572 | * @param {Event} [event] event is optional 573 | */ 574 | addFile: function (file, event) { 575 | this.addFiles([file], event); 576 | }, 577 | 578 | /** 579 | * Add a HTML5 File object to the list of files. 580 | * @function 581 | * @param {FileList|Array} fileList 582 | * @param {Event} [event] event is optional 583 | */ 584 | addFiles: function (fileList, event) { 585 | var files = []; 586 | each(fileList, function (file) { 587 | // https://github.com/flowjs/flow.js/issues/55 588 | if ((!ie10plus || ie10plus && file.size > 0) && !(file.size % 4096 === 0 && (file.name === '.' || file.fileName === '.'))) { 589 | var uniqueIdentifier = this.generateUniqueIdentifier(file); 590 | if (this.opts.allowDuplicateUploads || !this.getFromUniqueIdentifier(uniqueIdentifier)) { 591 | var f = new FlowFile(this, file, uniqueIdentifier); 592 | if (this.fire('fileAdded', f, event)) { 593 | files.push(f); 594 | } 595 | } 596 | } 597 | }, this); 598 | if (this.fire('filesAdded', files, event)) { 599 | each(files, function (file) { 600 | if (this.opts.singleFile && this.files.length > 0) { 601 | this.removeFile(this.files[0]); 602 | } 603 | this.files.push(file); 604 | }, this); 605 | this.fire('filesSubmitted', files, event); 606 | } 607 | }, 608 | 609 | 610 | /** 611 | * Cancel upload of a specific FlowFile object from the list. 612 | * @function 613 | * @param {FlowFile} file 614 | */ 615 | removeFile: function (file) { 616 | for (var i = this.files.length - 1; i >= 0; i--) { 617 | if (this.files[i] === file) { 618 | this.files.splice(i, 1); 619 | file.abort(); 620 | this.fire('fileRemoved', file); 621 | } 622 | } 623 | }, 624 | 625 | /** 626 | * Look up a FlowFile object by its unique identifier. 627 | * @function 628 | * @param {string} uniqueIdentifier 629 | * @returns {boolean|FlowFile} false if file was not found 630 | */ 631 | getFromUniqueIdentifier: function (uniqueIdentifier) { 632 | var ret = false; 633 | each(this.files, function (file) { 634 | if (file.uniqueIdentifier === uniqueIdentifier) { 635 | ret = file; 636 | } 637 | }); 638 | return ret; 639 | }, 640 | 641 | /** 642 | * Returns the total size of all files in bytes. 643 | * @function 644 | * @returns {number} 645 | */ 646 | getSize: function () { 647 | var totalSize = 0; 648 | each(this.files, function (file) { 649 | totalSize += file.size; 650 | }); 651 | return totalSize; 652 | }, 653 | 654 | /** 655 | * Returns the total size uploaded of all files in bytes. 656 | * @function 657 | * @returns {number} 658 | */ 659 | sizeUploaded: function () { 660 | var size = 0; 661 | each(this.files, function (file) { 662 | size += file.sizeUploaded(); 663 | }); 664 | return size; 665 | }, 666 | 667 | /** 668 | * Returns remaining time to upload all files in seconds. Accuracy is based on average speed. 669 | * If speed is zero, time remaining will be equal to positive infinity `Number.POSITIVE_INFINITY` 670 | * @function 671 | * @returns {number} 672 | */ 673 | timeRemaining: function () { 674 | var sizeDelta = 0; 675 | var averageSpeed = 0; 676 | each(this.files, function (file) { 677 | if (!file.paused && !file.error) { 678 | sizeDelta += file.size - file.sizeUploaded(); 679 | averageSpeed += file.averageSpeed; 680 | } 681 | }); 682 | if (sizeDelta && !averageSpeed) { 683 | return Number.POSITIVE_INFINITY; 684 | } 685 | if (!sizeDelta && !averageSpeed) { 686 | return 0; 687 | } 688 | return Math.floor(sizeDelta / averageSpeed); 689 | } 690 | }; 691 | 692 | 693 | 694 | 695 | 696 | 697 | /** 698 | * FlowFile class 699 | * @name FlowFile 700 | * @param {Flow} flowObj 701 | * @param {File} file 702 | * @param {string} uniqueIdentifier 703 | * @constructor 704 | */ 705 | function FlowFile(flowObj, file, uniqueIdentifier) { 706 | 707 | /** 708 | * Reference to parent Flow instance 709 | * @type {Flow} 710 | */ 711 | this.flowObj = flowObj; 712 | 713 | /** 714 | * Used to store the bytes read 715 | * @type {Blob|string} 716 | */ 717 | this.bytes = null; 718 | 719 | /** 720 | * Reference to file 721 | * @type {File} 722 | */ 723 | this.file = file; 724 | 725 | /** 726 | * File name. Some confusion in different versions of Firefox 727 | * @type {string} 728 | */ 729 | this.name = file.fileName || file.name; 730 | 731 | /** 732 | * File size 733 | * @type {number} 734 | */ 735 | this.size = file.size; 736 | 737 | /** 738 | * Relative file path 739 | * @type {string} 740 | */ 741 | this.relativePath = file.relativePath || file.webkitRelativePath || this.name; 742 | 743 | /** 744 | * File unique identifier 745 | * @type {string} 746 | */ 747 | this.uniqueIdentifier = (uniqueIdentifier === undefined ? flowObj.generateUniqueIdentifier(file) : uniqueIdentifier); 748 | 749 | /** 750 | * List of chunks 751 | * @type {Array.} 752 | */ 753 | this.chunks = []; 754 | 755 | /** 756 | * Indicated if file is paused 757 | * @type {boolean} 758 | */ 759 | this.paused = false; 760 | 761 | /** 762 | * Indicated if file has encountered an error 763 | * @type {boolean} 764 | */ 765 | this.error = false; 766 | 767 | /** 768 | * Average upload speed 769 | * @type {number} 770 | */ 771 | this.averageSpeed = 0; 772 | 773 | /** 774 | * Current upload speed 775 | * @type {number} 776 | */ 777 | this.currentSpeed = 0; 778 | 779 | /** 780 | * Date then progress was called last time 781 | * @type {number} 782 | * @private 783 | */ 784 | this._lastProgressCallback = Date.now(); 785 | 786 | /** 787 | * Previously uploaded file size 788 | * @type {number} 789 | * @private 790 | */ 791 | this._prevUploadedSize = 0; 792 | 793 | /** 794 | * Holds previous progress 795 | * @type {number} 796 | * @private 797 | */ 798 | this._prevProgress = 0; 799 | 800 | this.bootstrap(); 801 | } 802 | 803 | FlowFile.prototype = { 804 | /** 805 | * Update speed parameters 806 | * @link http://stackoverflow.com/questions/2779600/how-to-estimate-download-time-remaining-accurately 807 | * @function 808 | */ 809 | measureSpeed: function () { 810 | var timeSpan = Date.now() - this._lastProgressCallback; 811 | if (!timeSpan) { 812 | return ; 813 | } 814 | var smoothingFactor = this.flowObj.opts.speedSmoothingFactor; 815 | var uploaded = this.sizeUploaded(); 816 | // Prevent negative upload speed after file upload resume 817 | this.currentSpeed = Math.max((uploaded - this._prevUploadedSize) / timeSpan * 1000, 0); 818 | this.averageSpeed = smoothingFactor * this.currentSpeed + (1 - smoothingFactor) * this.averageSpeed; 819 | this._prevUploadedSize = uploaded; 820 | }, 821 | 822 | /** 823 | * For internal usage only. 824 | * Callback when something happens within the chunk. 825 | * @function 826 | * @param {FlowChunk} chunk 827 | * @param {string} event can be 'progress', 'success', 'error' or 'retry' 828 | * @param {string} [message] 829 | */ 830 | chunkEvent: function (chunk, event, message) { 831 | switch (event) { 832 | case 'progress': 833 | if (Date.now() - this._lastProgressCallback < 834 | this.flowObj.opts.progressCallbacksInterval) { 835 | break; 836 | } 837 | this.measureSpeed(); 838 | this.flowObj.fire('fileProgress', this, chunk); 839 | this.flowObj.fire('progress'); 840 | this._lastProgressCallback = Date.now(); 841 | break; 842 | case 'error': 843 | this.error = true; 844 | this.abort(true); 845 | this.flowObj.fire('fileError', this, message, chunk); 846 | this.flowObj.fire('error', message, this, chunk); 847 | break; 848 | case 'success': 849 | if (this.error) { 850 | return; 851 | } 852 | this.measureSpeed(); 853 | this.flowObj.fire('fileProgress', this, chunk); 854 | this.flowObj.fire('progress'); 855 | this._lastProgressCallback = Date.now(); 856 | if (this.isComplete()) { 857 | this.currentSpeed = 0; 858 | this.averageSpeed = 0; 859 | this.flowObj.fire('fileSuccess', this, message, chunk); 860 | } 861 | break; 862 | case 'retry': 863 | this.flowObj.fire('fileRetry', this, chunk); 864 | break; 865 | } 866 | }, 867 | 868 | /** 869 | * Pause file upload 870 | * @function 871 | */ 872 | pause: function() { 873 | this.paused = true; 874 | this.abort(); 875 | }, 876 | 877 | /** 878 | * Resume file upload 879 | * @function 880 | */ 881 | resume: function() { 882 | this.paused = false; 883 | this.flowObj.upload(); 884 | }, 885 | 886 | /** 887 | * Abort current upload 888 | * @function 889 | */ 890 | abort: function (reset) { 891 | this.currentSpeed = 0; 892 | this.averageSpeed = 0; 893 | var chunks = this.chunks; 894 | if (reset) { 895 | this.chunks = []; 896 | } 897 | each(chunks, function (c) { 898 | if (c.status() === 'uploading') { 899 | c.abort(); 900 | this.flowObj.uploadNextChunk(); 901 | } 902 | }, this); 903 | }, 904 | 905 | /** 906 | * Cancel current upload and remove from a list 907 | * @function 908 | */ 909 | cancel: function () { 910 | this.flowObj.removeFile(this); 911 | }, 912 | 913 | /** 914 | * Retry aborted file upload 915 | * @function 916 | */ 917 | retry: function () { 918 | this.bootstrap(); 919 | this.flowObj.upload(); 920 | }, 921 | 922 | /** 923 | * Clear current chunks and slice file again 924 | * @function 925 | */ 926 | bootstrap: function () { 927 | if (typeof this.flowObj.opts.initFileFn === "function") { 928 | this.flowObj.opts.initFileFn(this); 929 | } 930 | 931 | this.abort(true); 932 | this.error = false; 933 | // Rebuild stack of chunks from file 934 | this._prevProgress = 0; 935 | var round = this.flowObj.opts.forceChunkSize ? Math.ceil : Math.floor; 936 | var chunks = Math.max( 937 | round(this.size / this.flowObj.opts.chunkSize), 1 938 | ); 939 | for (var offset = 0; offset < chunks; offset++) { 940 | this.chunks.push( 941 | new FlowChunk(this.flowObj, this, offset) 942 | ); 943 | } 944 | }, 945 | 946 | /** 947 | * Get current upload progress status 948 | * @function 949 | * @returns {number} from 0 to 1 950 | */ 951 | progress: function () { 952 | if (this.error) { 953 | return 1; 954 | } 955 | if (this.chunks.length === 1) { 956 | this._prevProgress = Math.max(this._prevProgress, this.chunks[0].progress()); 957 | return this._prevProgress; 958 | } 959 | // Sum up progress across everything 960 | var bytesLoaded = 0; 961 | each(this.chunks, function (c) { 962 | // get chunk progress relative to entire file 963 | bytesLoaded += c.progress() * (c.endByte - c.startByte); 964 | }); 965 | var percent = bytesLoaded / this.size; 966 | // We don't want to lose percentages when an upload is paused 967 | this._prevProgress = Math.max(this._prevProgress, percent > 0.9999 ? 1 : percent); 968 | return this._prevProgress; 969 | }, 970 | 971 | /** 972 | * Indicates if file is being uploaded at the moment 973 | * @function 974 | * @returns {boolean} 975 | */ 976 | isUploading: function () { 977 | var uploading = false; 978 | each(this.chunks, function (chunk) { 979 | if (chunk.status() === 'uploading') { 980 | uploading = true; 981 | return false; 982 | } 983 | }); 984 | return uploading; 985 | }, 986 | 987 | /** 988 | * Indicates if file is has finished uploading and received a response 989 | * @function 990 | * @returns {boolean} 991 | */ 992 | isComplete: function () { 993 | var outstanding = false; 994 | each(this.chunks, function (chunk) { 995 | var status = chunk.status(); 996 | if (status === 'pending' || status === 'uploading' || status === 'reading' || chunk.preprocessState === 1 || chunk.readState === 1) { 997 | outstanding = true; 998 | return false; 999 | } 1000 | }); 1001 | return !outstanding; 1002 | }, 1003 | 1004 | /** 1005 | * Count total size uploaded 1006 | * @function 1007 | * @returns {number} 1008 | */ 1009 | sizeUploaded: function () { 1010 | var size = 0; 1011 | each(this.chunks, function (chunk) { 1012 | size += chunk.sizeUploaded(); 1013 | }); 1014 | return size; 1015 | }, 1016 | 1017 | /** 1018 | * Returns remaining time to finish upload file in seconds. Accuracy is based on average speed. 1019 | * If speed is zero, time remaining will be equal to positive infinity `Number.POSITIVE_INFINITY` 1020 | * @function 1021 | * @returns {number} 1022 | */ 1023 | timeRemaining: function () { 1024 | if (this.paused || this.error) { 1025 | return 0; 1026 | } 1027 | var delta = this.size - this.sizeUploaded(); 1028 | if (delta && !this.averageSpeed) { 1029 | return Number.POSITIVE_INFINITY; 1030 | } 1031 | if (!delta && !this.averageSpeed) { 1032 | return 0; 1033 | } 1034 | return Math.floor(delta / this.averageSpeed); 1035 | }, 1036 | 1037 | /** 1038 | * Get file type 1039 | * @function 1040 | * @returns {string} 1041 | */ 1042 | getType: function () { 1043 | return this.file.type && this.file.type.split('/')[1]; 1044 | }, 1045 | 1046 | /** 1047 | * Get file extension 1048 | * @function 1049 | * @returns {string} 1050 | */ 1051 | getExtension: function () { 1052 | return this.name.substr((~-this.name.lastIndexOf(".") >>> 0) + 2).toLowerCase(); 1053 | } 1054 | }; 1055 | 1056 | /** 1057 | * Default read function using the webAPI 1058 | * 1059 | * @function webAPIFileRead(fileObj, startByte, endByte, fileType, chunk) 1060 | * 1061 | */ 1062 | function webAPIFileRead(fileObj, startByte, endByte, fileType, chunk) { 1063 | var function_name = 'slice'; 1064 | 1065 | if (fileObj.file.slice) 1066 | function_name = 'slice'; 1067 | else if (fileObj.file.mozSlice) 1068 | function_name = 'mozSlice'; 1069 | else if (fileObj.file.webkitSlice) 1070 | function_name = 'webkitSlice'; 1071 | 1072 | chunk.readFinished(fileObj.file[function_name](startByte, endByte, fileType)); 1073 | } 1074 | 1075 | 1076 | /** 1077 | * Class for storing a single chunk 1078 | * @name FlowChunk 1079 | * @param {Flow} flowObj 1080 | * @param {FlowFile} fileObj 1081 | * @param {number} offset 1082 | * @constructor 1083 | */ 1084 | function FlowChunk(flowObj, fileObj, offset) { 1085 | 1086 | /** 1087 | * Reference to parent flow object 1088 | * @type {Flow} 1089 | */ 1090 | this.flowObj = flowObj; 1091 | 1092 | /** 1093 | * Reference to parent FlowFile object 1094 | * @type {FlowFile} 1095 | */ 1096 | this.fileObj = fileObj; 1097 | 1098 | /** 1099 | * File offset 1100 | * @type {number} 1101 | */ 1102 | this.offset = offset; 1103 | 1104 | /** 1105 | * Indicates if chunk existence was checked on the server 1106 | * @type {boolean} 1107 | */ 1108 | this.tested = false; 1109 | 1110 | /** 1111 | * Number of retries performed 1112 | * @type {number} 1113 | */ 1114 | this.retries = 0; 1115 | 1116 | /** 1117 | * Pending retry 1118 | * @type {boolean} 1119 | */ 1120 | this.pendingRetry = false; 1121 | 1122 | /** 1123 | * Preprocess state 1124 | * @type {number} 0 = unprocessed, 1 = processing, 2 = finished 1125 | */ 1126 | this.preprocessState = 0; 1127 | 1128 | /** 1129 | * Read state 1130 | * @type {number} 0 = not read, 1 = reading, 2 = finished 1131 | */ 1132 | this.readState = 0; 1133 | 1134 | 1135 | /** 1136 | * Bytes transferred from total request size 1137 | * @type {number} 1138 | */ 1139 | this.loaded = 0; 1140 | 1141 | /** 1142 | * Total request size 1143 | * @type {number} 1144 | */ 1145 | this.total = 0; 1146 | 1147 | /** 1148 | * Size of a chunk 1149 | * @type {number} 1150 | */ 1151 | this.chunkSize = this.flowObj.opts.chunkSize; 1152 | 1153 | /** 1154 | * Chunk start byte in a file 1155 | * @type {number} 1156 | */ 1157 | this.startByte = this.offset * this.chunkSize; 1158 | 1159 | /** 1160 | * Compute the endbyte in a file 1161 | * 1162 | */ 1163 | this.computeEndByte = function() { 1164 | var endByte = Math.min(this.fileObj.size, (this.offset + 1) * this.chunkSize); 1165 | if (this.fileObj.size - endByte < this.chunkSize && !this.flowObj.opts.forceChunkSize) { 1166 | // The last chunk will be bigger than the chunk size, 1167 | // but less than 2 * this.chunkSize 1168 | endByte = this.fileObj.size; 1169 | } 1170 | return endByte; 1171 | } 1172 | 1173 | /** 1174 | * Chunk end byte in a file 1175 | * @type {number} 1176 | */ 1177 | this.endByte = this.computeEndByte(); 1178 | 1179 | /** 1180 | * XMLHttpRequest 1181 | * @type {XMLHttpRequest} 1182 | */ 1183 | this.xhr = null; 1184 | 1185 | var $ = this; 1186 | 1187 | /** 1188 | * Send chunk event 1189 | * @param event 1190 | * @param {...} args arguments of a callback 1191 | */ 1192 | this.event = function (event, args) { 1193 | args = Array.prototype.slice.call(arguments); 1194 | args.unshift($); 1195 | $.fileObj.chunkEvent.apply($.fileObj, args); 1196 | }; 1197 | /** 1198 | * Catch progress event 1199 | * @param {ProgressEvent} event 1200 | */ 1201 | this.progressHandler = function(event) { 1202 | if (event.lengthComputable) { 1203 | $.loaded = event.loaded ; 1204 | $.total = event.total; 1205 | } 1206 | $.event('progress', event); 1207 | }; 1208 | 1209 | /** 1210 | * Catch test event 1211 | * @param {Event} event 1212 | */ 1213 | this.testHandler = function(event) { 1214 | var status = $.status(true); 1215 | if (status === 'error') { 1216 | $.event(status, $.message()); 1217 | $.flowObj.uploadNextChunk(); 1218 | } else if (status === 'success') { 1219 | $.tested = true; 1220 | $.event(status, $.message()); 1221 | $.flowObj.uploadNextChunk(); 1222 | } else if (!$.fileObj.paused) { 1223 | // Error might be caused by file pause method 1224 | // Chunks does not exist on the server side 1225 | $.tested = true; 1226 | $.send(); 1227 | } 1228 | }; 1229 | 1230 | /** 1231 | * Upload has stopped 1232 | * @param {Event} event 1233 | */ 1234 | this.doneHandler = function(event) { 1235 | var status = $.status(); 1236 | if (status === 'success' || status === 'error') { 1237 | delete this.data; 1238 | $.event(status, $.message()); 1239 | $.flowObj.uploadNextChunk(); 1240 | } else { 1241 | $.event('retry', $.message()); 1242 | $.pendingRetry = true; 1243 | $.abort(); 1244 | $.retries++; 1245 | var retryInterval = $.flowObj.opts.chunkRetryInterval; 1246 | if (retryInterval !== null) { 1247 | setTimeout(function () { 1248 | $.send(); 1249 | }, retryInterval); 1250 | } else { 1251 | $.send(); 1252 | } 1253 | } 1254 | }; 1255 | } 1256 | 1257 | FlowChunk.prototype = { 1258 | /** 1259 | * Get params for a request 1260 | * @function 1261 | */ 1262 | getParams: function () { 1263 | return { 1264 | flowChunkNumber: this.offset + 1, 1265 | flowChunkSize: this.flowObj.opts.chunkSize, 1266 | flowCurrentChunkSize: this.endByte - this.startByte, 1267 | flowTotalSize: this.fileObj.size, 1268 | flowIdentifier: this.fileObj.uniqueIdentifier, 1269 | flowFilename: this.fileObj.name, 1270 | flowRelativePath: this.fileObj.relativePath, 1271 | flowTotalChunks: this.fileObj.chunks.length 1272 | }; 1273 | }, 1274 | 1275 | /** 1276 | * Get target option with query params 1277 | * @function 1278 | * @param params 1279 | * @returns {string} 1280 | */ 1281 | getTarget: function(target, params){ 1282 | if(target.indexOf('?') < 0) { 1283 | target += '?'; 1284 | } else { 1285 | target += '&'; 1286 | } 1287 | return target + params.join('&'); 1288 | }, 1289 | 1290 | /** 1291 | * Makes a GET request without any data to see if the chunk has already 1292 | * been uploaded in a previous session 1293 | * @function 1294 | */ 1295 | test: function () { 1296 | // Set up request and listen for event 1297 | this.xhr = new XMLHttpRequest(); 1298 | this.xhr.addEventListener("load", this.testHandler, false); 1299 | this.xhr.addEventListener("error", this.testHandler, false); 1300 | var testMethod = evalOpts(this.flowObj.opts.testMethod, this.fileObj, this); 1301 | var data = this.prepareXhrRequest(testMethod, true); 1302 | this.xhr.send(data); 1303 | }, 1304 | 1305 | /** 1306 | * Finish preprocess state 1307 | * @function 1308 | */ 1309 | preprocessFinished: function () { 1310 | // Re-compute the endByte after the preprocess function to allow an 1311 | // implementer of preprocess to set the fileObj size 1312 | this.endByte = this.computeEndByte(); 1313 | 1314 | this.preprocessState = 2; 1315 | this.send(); 1316 | }, 1317 | 1318 | /** 1319 | * Finish read state 1320 | * @function 1321 | */ 1322 | readFinished: function (bytes) { 1323 | this.readState = 2; 1324 | this.bytes = bytes; 1325 | this.send(); 1326 | }, 1327 | 1328 | 1329 | /** 1330 | * Uploads the actual data in a POST call 1331 | * @function 1332 | */ 1333 | send: function () { 1334 | var preprocess = this.flowObj.opts.preprocess; 1335 | var read = this.flowObj.opts.readFileFn; 1336 | if (typeof preprocess === 'function') { 1337 | switch (this.preprocessState) { 1338 | case 0: 1339 | this.preprocessState = 1; 1340 | preprocess(this); 1341 | return; 1342 | case 1: 1343 | return; 1344 | } 1345 | } 1346 | switch (this.readState) { 1347 | case 0: 1348 | this.readState = 1; 1349 | read(this.fileObj, this.startByte, this.endByte, this.fileObj.file.type, this); 1350 | return; 1351 | case 1: 1352 | return; 1353 | } 1354 | if (this.flowObj.opts.testChunks && !this.tested) { 1355 | this.test(); 1356 | return; 1357 | } 1358 | 1359 | this.loaded = 0; 1360 | this.total = 0; 1361 | this.pendingRetry = false; 1362 | 1363 | // Set up request and listen for event 1364 | this.xhr = new XMLHttpRequest(); 1365 | this.xhr.upload.addEventListener('progress', this.progressHandler, false); 1366 | this.xhr.addEventListener("load", this.doneHandler, false); 1367 | this.xhr.addEventListener("error", this.doneHandler, false); 1368 | 1369 | var uploadMethod = evalOpts(this.flowObj.opts.uploadMethod, this.fileObj, this); 1370 | var data = this.prepareXhrRequest(uploadMethod, false, this.flowObj.opts.method, this.bytes); 1371 | this.xhr.send(data); 1372 | }, 1373 | 1374 | /** 1375 | * Abort current xhr request 1376 | * @function 1377 | */ 1378 | abort: function () { 1379 | // Abort and reset 1380 | var xhr = this.xhr; 1381 | this.xhr = null; 1382 | if (xhr) { 1383 | xhr.abort(); 1384 | } 1385 | }, 1386 | 1387 | /** 1388 | * Retrieve current chunk upload status 1389 | * @function 1390 | * @returns {string} 'pending', 'uploading', 'success', 'error' 1391 | */ 1392 | status: function (isTest) { 1393 | if (this.readState === 1) { 1394 | return 'reading'; 1395 | } else if (this.pendingRetry || this.preprocessState === 1) { 1396 | // if pending retry then that's effectively the same as actively uploading, 1397 | // there might just be a slight delay before the retry starts 1398 | return 'uploading'; 1399 | } else if (!this.xhr) { 1400 | return 'pending'; 1401 | } else if (this.xhr.readyState < 4) { 1402 | // Status is really 'OPENED', 'HEADERS_RECEIVED' 1403 | // or 'LOADING' - meaning that stuff is happening 1404 | return 'uploading'; 1405 | } else { 1406 | if (this.flowObj.opts.successStatuses.indexOf(this.xhr.status) > -1) { 1407 | // HTTP 200, perfect 1408 | // HTTP 202 Accepted - The request has been accepted for processing, but the processing has not been completed. 1409 | return 'success'; 1410 | } else if (this.flowObj.opts.permanentErrors.indexOf(this.xhr.status) > -1 || 1411 | !isTest && this.retries >= this.flowObj.opts.maxChunkRetries) { 1412 | // HTTP 413/415/500/501, permanent error 1413 | return 'error'; 1414 | } else { 1415 | // this should never happen, but we'll reset and queue a retry 1416 | // a likely case for this would be 503 service unavailable 1417 | this.abort(); 1418 | return 'pending'; 1419 | } 1420 | } 1421 | }, 1422 | 1423 | /** 1424 | * Get response from xhr request 1425 | * @function 1426 | * @returns {String} 1427 | */ 1428 | message: function () { 1429 | return this.xhr ? this.xhr.responseText : ''; 1430 | }, 1431 | 1432 | /** 1433 | * Get upload progress 1434 | * @function 1435 | * @returns {number} 1436 | */ 1437 | progress: function () { 1438 | if (this.pendingRetry) { 1439 | return 0; 1440 | } 1441 | var s = this.status(); 1442 | if (s === 'success' || s === 'error') { 1443 | return 1; 1444 | } else if (s === 'pending') { 1445 | return 0; 1446 | } else { 1447 | return this.total > 0 ? this.loaded / this.total : 0; 1448 | } 1449 | }, 1450 | 1451 | /** 1452 | * Count total size uploaded 1453 | * @function 1454 | * @returns {number} 1455 | */ 1456 | sizeUploaded: function () { 1457 | var size = this.endByte - this.startByte; 1458 | // can't return only chunk.loaded value, because it is bigger than chunk size 1459 | if (this.status() !== 'success') { 1460 | size = this.progress() * size; 1461 | } 1462 | return size; 1463 | }, 1464 | 1465 | /** 1466 | * Prepare Xhr request. Set query, headers and data 1467 | * @param {string} method GET or POST 1468 | * @param {bool} isTest is this a test request 1469 | * @param {string} [paramsMethod] octet or form 1470 | * @param {Blob} [blob] to send 1471 | * @returns {FormData|Blob|Null} data to send 1472 | */ 1473 | prepareXhrRequest: function(method, isTest, paramsMethod, blob) { 1474 | // Add data from the query options 1475 | var query = evalOpts(this.flowObj.opts.query, this.fileObj, this, isTest); 1476 | query = extend(query, this.getParams()); 1477 | 1478 | var target = evalOpts(this.flowObj.opts.target, this.fileObj, this, isTest); 1479 | var data = null; 1480 | if (method === 'GET' || paramsMethod === 'octet') { 1481 | // Add data from the query options 1482 | var params = []; 1483 | each(query, function (v, k) { 1484 | params.push([encodeURIComponent(k), encodeURIComponent(v)].join('=')); 1485 | }); 1486 | target = this.getTarget(target, params); 1487 | data = blob || null; 1488 | } else { 1489 | // Add data from the query options 1490 | data = new FormData(); 1491 | each(query, function (v, k) { 1492 | data.append(k, v); 1493 | }); 1494 | if (typeof blob !== "undefined") data.append(this.flowObj.opts.fileParameterName, blob, this.fileObj.file.name); 1495 | } 1496 | 1497 | this.xhr.open(method, target, true); 1498 | this.xhr.withCredentials = this.flowObj.opts.withCredentials; 1499 | 1500 | // Add data from header options 1501 | each(evalOpts(this.flowObj.opts.headers, this.fileObj, this, isTest), function (v, k) { 1502 | this.xhr.setRequestHeader(k, v); 1503 | }, this); 1504 | 1505 | return data; 1506 | } 1507 | }; 1508 | 1509 | /** 1510 | * Remove value from array 1511 | * @param array 1512 | * @param value 1513 | */ 1514 | function arrayRemove(array, value) { 1515 | var index = array.indexOf(value); 1516 | if (index > -1) { 1517 | array.splice(index, 1); 1518 | } 1519 | } 1520 | 1521 | /** 1522 | * If option is a function, evaluate it with given params 1523 | * @param {*} data 1524 | * @param {...} args arguments of a callback 1525 | * @returns {*} 1526 | */ 1527 | function evalOpts(data, args) { 1528 | if (typeof data === "function") { 1529 | // `arguments` is an object, not array, in FF, so: 1530 | args = Array.prototype.slice.call(arguments); 1531 | data = data.apply(null, args.slice(1)); 1532 | } 1533 | return data; 1534 | } 1535 | Flow.evalOpts = evalOpts; 1536 | 1537 | /** 1538 | * Execute function asynchronously 1539 | * @param fn 1540 | * @param context 1541 | */ 1542 | function async(fn, context) { 1543 | setTimeout(fn.bind(context), 0); 1544 | } 1545 | 1546 | /** 1547 | * Extends the destination object `dst` by copying all of the properties from 1548 | * the `src` object(s) to `dst`. You can specify multiple `src` objects. 1549 | * @function 1550 | * @param {Object} dst Destination object. 1551 | * @param {...Object} src Source object(s). 1552 | * @returns {Object} Reference to `dst`. 1553 | */ 1554 | function extend(dst, src) { 1555 | each(arguments, function(obj) { 1556 | if (obj !== dst) { 1557 | each(obj, function(value, key){ 1558 | dst[key] = value; 1559 | }); 1560 | } 1561 | }); 1562 | return dst; 1563 | } 1564 | Flow.extend = extend; 1565 | 1566 | /** 1567 | * Iterate each element of an object 1568 | * @function 1569 | * @param {Array|Object} obj object or an array to iterate 1570 | * @param {Function} callback first argument is a value and second is a key. 1571 | * @param {Object=} context Object to become context (`this`) for the iterator function. 1572 | */ 1573 | function each(obj, callback, context) { 1574 | if (!obj) { 1575 | return ; 1576 | } 1577 | var key; 1578 | // Is Array? 1579 | // Array.isArray won't work, not only arrays can be iterated by index https://github.com/flowjs/ng-flow/issues/236# 1580 | if (typeof(obj.length) !== 'undefined') { 1581 | for (key = 0; key < obj.length; key++) { 1582 | if (callback.call(context, obj[key], key) === false) { 1583 | return ; 1584 | } 1585 | } 1586 | } else { 1587 | for (key in obj) { 1588 | if (obj.hasOwnProperty(key) && callback.call(context, obj[key], key) === false) { 1589 | return ; 1590 | } 1591 | } 1592 | } 1593 | } 1594 | Flow.each = each; 1595 | 1596 | /** 1597 | * FlowFile constructor 1598 | * @type {FlowFile} 1599 | */ 1600 | Flow.FlowFile = FlowFile; 1601 | 1602 | /** 1603 | * FlowFile constructor 1604 | * @type {FlowChunk} 1605 | */ 1606 | Flow.FlowChunk = FlowChunk; 1607 | 1608 | /** 1609 | * Library version 1610 | * @type {string} 1611 | */ 1612 | Flow.version = '<%= version %>'; 1613 | 1614 | if ( typeof module === "object" && module && typeof module.exports === "object" ) { 1615 | // Expose Flow as module.exports in loaders that implement the Node 1616 | // module pattern (including browserify). Do not create the global, since 1617 | // the user will be storing it themselves locally, and globals are frowned 1618 | // upon in the Node module world. 1619 | module.exports = Flow; 1620 | } else { 1621 | // Otherwise expose Flow to the global object as usual 1622 | window.Flow = Flow; 1623 | 1624 | // Register as a named AMD module, since Flow can be concatenated with other 1625 | // files that may use define, but not via a proper concatenation script that 1626 | // understands anonymous AMD modules. A named AMD is safest and most robust 1627 | // way to register. Lowercase flow is used because AMD module names are 1628 | // derived from file names, and Flow is normally delivered in a lowercase 1629 | // file name. Do this after creating the global so that if an AMD module wants 1630 | // to call noConflict to hide this version of Flow, it will work. 1631 | if ( typeof define === "function" && define.amd ) { 1632 | define( "flow", [], function () { return Flow; } ); 1633 | } 1634 | } 1635 | })(window, document); 1636 | -------------------------------------------------------------------------------- /server/tmp/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !.gitignore -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "esModuleInterop": true, 9 | "strict": true, 10 | "noImplicitReturns": true, 11 | "noImplicitAny": false, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "paths": { 16 | "@flowjs/ngx-flow": [ 17 | "dist/ngx-flow" 18 | ] 19 | }, 20 | "experimentalDecorators": true, 21 | "moduleResolution": "node", 22 | "importHelpers": true, 23 | "target": "ES2022", 24 | "module": "es2020", 25 | "lib": [ 26 | "es2018", 27 | "dom" 28 | ], 29 | "useDefineForClassFields": false 30 | }, 31 | "angularCompilerOptions": { 32 | "enableI18nLegacyMessageIdFormat": false, 33 | "strictInjectionParameters": true, 34 | "strictInputAccessModifiers": true, 35 | "strictTemplates": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["node_modules/codelyzer"], 3 | "rules": { 4 | "arrow-return-shorthand": true, 5 | "callable-types": true, 6 | "class-name": true, 7 | "deprecation": { 8 | "severity": "warn" 9 | }, 10 | "forin": true, 11 | "import-blacklist": [true, "rxjs/Rx"], 12 | "interface-over-type-literal": true, 13 | "label-position": true, 14 | "member-access": false, 15 | "member-ordering": [ 16 | true, 17 | { 18 | "order": ["static-field", "instance-field", "static-method", "instance-method"] 19 | } 20 | ], 21 | "no-arg": true, 22 | "no-bitwise": true, 23 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 24 | "no-construct": true, 25 | "no-debugger": true, 26 | "no-duplicate-super": true, 27 | "no-empty": false, 28 | "no-empty-interface": true, 29 | "no-eval": true, 30 | "no-inferrable-types": [true, "ignore-params"], 31 | "no-misused-new": true, 32 | "no-non-null-assertion": true, 33 | "no-shadowed-variable": true, 34 | "no-string-literal": false, 35 | "no-string-throw": true, 36 | "no-switch-case-fall-through": true, 37 | "no-unnecessary-initializer": true, 38 | "no-unused-expression": true, 39 | "no-use-before-declare": true, 40 | "no-var-keyword": true, 41 | "object-literal-sort-keys": false, 42 | "prefer-const": true, 43 | "radix": true, 44 | "triple-equals": [true, "allow-null-check"], 45 | "unified-signatures": true, 46 | "variable-name": false, 47 | "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type"], 48 | "no-output-on-prefix": true, 49 | "no-inputs-metadata-property": true, 50 | "no-outputs-metadata-property": true, 51 | "no-host-metadata-property": true, 52 | "no-input-rename": true, 53 | "no-output-rename": true, 54 | "use-lifecycle-interface": true, 55 | "use-pipe-transform-interface": true, 56 | "component-class-suffix": true, 57 | "directive-class-suffix": true 58 | } 59 | } 60 | --------------------------------------------------------------------------------