├── .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 | [](https://travis-ci.com/flowjs/ngx-flow)
4 | [](https://codeclimate.com/github/flowjs/ngx-flow/test_coverage)
5 | [](https://codeclimate.com/github/flowjs/ngx-flow/maintainability)
6 | [](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 | [](https://travis-ci.com/flowjs/ngx-flow)
6 | [](https://codeclimate.com/github/flowjs/ngx-flow/test_coverage)
7 | [](https://codeclimate.com/github/flowjs/ngx-flow/maintainability)
8 | [](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 |
--------------------------------------------------------------------------------