├── .github
└── workflows
│ └── master.yml
├── .gitignore
├── LICENSE.txt
├── Makefile
├── README.md
├── doc
├── CodingGuidelines.md
├── DeveloperReadme.md
├── assets
│ ├── logo.ai
│ └── logo_with_name.ai
├── images
│ ├── install_1.png
│ ├── install_2.png
│ └── logo_with_name.png
└── licenses
│ └── flaticon_license.pdf
└── src
├── angular
├── .angular-cli.json
├── .editorconfig
├── .gitignore
├── e2e
│ ├── app.e2e-spec.ts
│ ├── app.po.ts
│ └── tsconfig.e2e.json
├── karma.conf.js
├── package.json
├── protractor.conf.js
├── src
│ ├── app
│ │ ├── app.module.ts
│ │ ├── common
│ │ │ ├── _common.scss
│ │ │ ├── cached-reuse-strategy.ts
│ │ │ ├── capitalize.pipe.ts
│ │ │ ├── click-stop-propagation.directive.ts
│ │ │ ├── eta.pipe.ts
│ │ │ ├── file-size.pipe.ts
│ │ │ ├── localization.ts
│ │ │ └── storage-keys.ts
│ │ ├── pages
│ │ │ ├── about
│ │ │ │ ├── about-page.component.html
│ │ │ │ ├── about-page.component.scss
│ │ │ │ └── about-page.component.ts
│ │ │ ├── autoqueue
│ │ │ │ ├── autoqueue-page.component.html
│ │ │ │ ├── autoqueue-page.component.scss
│ │ │ │ └── autoqueue-page.component.ts
│ │ │ ├── files
│ │ │ │ ├── file-list.component.html
│ │ │ │ ├── file-list.component.scss
│ │ │ │ ├── file-list.component.ts
│ │ │ │ ├── file-options.component.html
│ │ │ │ ├── file-options.component.scss
│ │ │ │ ├── file-options.component.ts
│ │ │ │ ├── file.component.html
│ │ │ │ ├── file.component.scss
│ │ │ │ ├── file.component.ts
│ │ │ │ ├── files-page.component.html
│ │ │ │ └── files-page.component.ts
│ │ │ ├── logs
│ │ │ │ ├── logs-page.component.html
│ │ │ │ ├── logs-page.component.scss
│ │ │ │ └── logs-page.component.ts
│ │ │ ├── main
│ │ │ │ ├── app.component.html
│ │ │ │ ├── app.component.scss
│ │ │ │ ├── app.component.ts
│ │ │ │ ├── header.component.html
│ │ │ │ ├── header.component.scss
│ │ │ │ ├── header.component.ts
│ │ │ │ ├── sidebar.component.html
│ │ │ │ ├── sidebar.component.scss
│ │ │ │ └── sidebar.component.ts
│ │ │ └── settings
│ │ │ │ ├── option.component.html
│ │ │ │ ├── option.component.scss
│ │ │ │ ├── option.component.ts
│ │ │ │ ├── options-list.ts
│ │ │ │ ├── settings-page.component.html
│ │ │ │ ├── settings-page.component.scss
│ │ │ │ └── settings-page.component.ts
│ │ ├── routes.ts
│ │ ├── services
│ │ │ ├── autoqueue
│ │ │ │ ├── autoqueue-pattern.ts
│ │ │ │ └── autoqueue.service.ts
│ │ │ ├── base
│ │ │ │ ├── base-stream.service.ts
│ │ │ │ ├── base-web.service.ts
│ │ │ │ └── stream-service.registry.ts
│ │ │ ├── files
│ │ │ │ ├── mock-model-files.ts
│ │ │ │ ├── model-file.service.ts
│ │ │ │ ├── model-file.ts
│ │ │ │ ├── screenshot-model-files.ts
│ │ │ │ ├── view-file-filter.service.ts
│ │ │ │ ├── view-file-options.service.ts
│ │ │ │ ├── view-file-options.ts
│ │ │ │ ├── view-file-sort.service.ts
│ │ │ │ ├── view-file.service.ts
│ │ │ │ └── view-file.ts
│ │ │ ├── logs
│ │ │ │ ├── log-record.ts
│ │ │ │ └── log.service.ts
│ │ │ ├── server
│ │ │ │ ├── server-command.service.ts
│ │ │ │ ├── server-status.service.ts
│ │ │ │ └── server-status.ts
│ │ │ ├── settings
│ │ │ │ ├── config.service.ts
│ │ │ │ └── config.ts
│ │ │ └── utils
│ │ │ │ ├── connected.service.ts
│ │ │ │ ├── dom.service.ts
│ │ │ │ ├── logger.service.ts
│ │ │ │ ├── notification.service.ts
│ │ │ │ ├── notification.ts
│ │ │ │ ├── rest.service.ts
│ │ │ │ └── version-check.service.ts
│ │ └── tests
│ │ │ ├── mocks
│ │ │ ├── mock-event-source.ts
│ │ │ ├── mock-model-file.service.ts
│ │ │ ├── mock-rest.service.ts
│ │ │ ├── mock-storage.service.ts
│ │ │ ├── mock-stream-service.registry.ts
│ │ │ ├── mock-view-file-options.service.ts
│ │ │ └── mock-view-file.service.ts
│ │ │ └── unittests
│ │ │ └── services
│ │ │ ├── autoqueue
│ │ │ └── autoqueue.service.spec.ts
│ │ │ ├── base
│ │ │ ├── base-stream.service.spec.ts
│ │ │ ├── base-web.service.spec.ts
│ │ │ └── stream-service.registry.spec.ts
│ │ │ ├── files
│ │ │ ├── model-file.service.spec.ts
│ │ │ ├── model-file.spec.ts
│ │ │ ├── view-file-filter.service.spec.ts
│ │ │ ├── view-file-options.service.spec.ts
│ │ │ ├── view-file-sort.service.spec.ts
│ │ │ └── view-file.service.spec.ts
│ │ │ ├── logs
│ │ │ ├── log-record.spec.ts
│ │ │ └── log.service.spec.ts
│ │ │ ├── server
│ │ │ ├── server-command.service.spec.ts
│ │ │ ├── server-status.service.spec.ts
│ │ │ └── server-status.spec.ts
│ │ │ ├── settings
│ │ │ ├── config.service.spec.ts
│ │ │ └── config.spec.ts
│ │ │ └── utils
│ │ │ ├── connected.service.spec.ts
│ │ │ ├── dom.service.spec.ts
│ │ │ ├── notification.service.spec.ts
│ │ │ ├── rest.service.spec.ts
│ │ │ └── version-check.service.spec.ts
│ ├── assets
│ │ ├── .gitkeep
│ │ ├── favicon.png
│ │ ├── icons
│ │ │ ├── about.svg
│ │ │ ├── autoqueue.svg
│ │ │ ├── dashboard.svg
│ │ │ ├── default-remote.svg
│ │ │ ├── delete-local.svg
│ │ │ ├── delete-remote.svg
│ │ │ ├── deleted.svg
│ │ │ ├── directory-archive-light.svg
│ │ │ ├── directory-archive.svg
│ │ │ ├── directory-light.svg
│ │ │ ├── directory.svg
│ │ │ ├── downloaded.svg
│ │ │ ├── downloading.svg
│ │ │ ├── extract.svg
│ │ │ ├── extracted.svg
│ │ │ ├── extracting.svg
│ │ │ ├── file-archive-light.svg
│ │ │ ├── file-archive.svg
│ │ │ ├── file-light.svg
│ │ │ ├── file.svg
│ │ │ ├── hamburger.svg
│ │ │ ├── logs.svg
│ │ │ ├── queue.svg
│ │ │ ├── queued.svg
│ │ │ ├── refresh.svg
│ │ │ ├── search.svg
│ │ │ ├── settings.svg
│ │ │ ├── sort-asc.svg
│ │ │ ├── sort-desc.svg
│ │ │ ├── states.svg
│ │ │ ├── stop.svg
│ │ │ └── stopped.svg
│ │ └── logo.png
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── index.html
│ ├── main.ts
│ ├── polyfills.ts
│ ├── styles.scss
│ ├── test.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.spec.json
│ └── typings.d.ts
├── tsconfig.json
└── tslint.json
├── debian
├── changelog
├── compat
├── config
├── control
├── postinst
├── postrm
├── rules
├── seedsync.service
├── source
│ └── format
└── templates
├── docker
├── build
│ ├── deb
│ │ ├── Dockerfile
│ │ └── Dockerfile.dockerignore
│ └── docker-image
│ │ ├── Dockerfile
│ │ ├── Dockerfile.dockerignore
│ │ ├── run_as_user
│ │ ├── scp
│ │ ├── setup_default_config.sh
│ │ └── ssh
├── stage
│ ├── deb
│ │ ├── Dockerfile
│ │ ├── compose-ubu1604.yml
│ │ ├── compose-ubu1804.yml
│ │ ├── compose-ubu2004.yml
│ │ ├── compose.yml
│ │ ├── entrypoint.sh
│ │ ├── expect_seedsync.exp
│ │ ├── id_rsa
│ │ ├── id_rsa.pub
│ │ ├── install_seedsync.sh
│ │ └── ubuntu-systemd
│ │ │ ├── ubuntu-16.04-systemd
│ │ │ ├── Dockerfile
│ │ │ └── setup
│ │ │ ├── ubuntu-18.04-systemd
│ │ │ ├── Dockerfile
│ │ │ └── setup
│ │ │ └── ubuntu-20.04-systemd
│ │ │ ├── Dockerfile
│ │ │ └── setup
│ └── docker-image
│ │ └── compose.yml
├── test
│ ├── angular
│ │ ├── Dockerfile
│ │ └── compose.yml
│ ├── e2e
│ │ ├── Dockerfile
│ │ ├── chrome
│ │ │ └── Dockerfile
│ │ ├── compose-dev.yml
│ │ ├── compose.yml
│ │ ├── configure
│ │ │ ├── Dockerfile
│ │ │ └── setup_seedsync.sh
│ │ ├── parse_seedsync_status.py
│ │ ├── remote
│ │ │ ├── Dockerfile
│ │ │ ├── files
│ │ │ │ ├── clients.jpg
│ │ │ │ ├── crispycat
│ │ │ │ │ └── cat.mp4
│ │ │ │ ├── documentation.png
│ │ │ │ ├── goose
│ │ │ │ │ └── goose.mp4
│ │ │ │ ├── illusion.jpg
│ │ │ │ ├── joke
│ │ │ │ │ └── joke.png
│ │ │ │ ├── testing.gif
│ │ │ │ ├── áßç déÀ.mp4
│ │ │ │ └── üæÒ
│ │ │ │ │ └── µ®© ÷úƤ.png
│ │ │ └── id_rsa.pub
│ │ ├── run_tests.sh
│ │ └── urls.ts
│ └── python
│ │ ├── Dockerfile
│ │ ├── compose.yml
│ │ └── entrypoint.sh
└── wait-for-it.sh
├── e2e
├── .gitignore
├── README.md
├── conf.ts
├── package.json
├── tests
│ ├── about.page.spec.ts
│ ├── about.page.ts
│ ├── app.spec.ts
│ ├── app.ts
│ ├── autoqueue.page.spec.ts
│ ├── autoqueue.page.ts
│ ├── dashboard.page.spec.ts
│ ├── dashboard.page.ts
│ ├── settings.page.spec.ts
│ └── settings.page.ts
├── tsconfig.json
└── urls.ts
├── pyinstaller_hooks
└── hook-patoolib.py
└── python
├── __init__.py
├── common
├── __init__.py
├── app_process.py
├── config.py
├── constants.py
├── context.py
├── error.py
├── job.py
├── localization.py
├── multiprocessing_logger.py
├── persist.py
├── status.py
└── types.py
├── controller
├── __init__.py
├── auto_queue.py
├── controller.py
├── controller_job.py
├── controller_persist.py
├── delete
│ ├── __init__.py
│ └── delete_process.py
├── extract
│ ├── __init__.py
│ ├── dispatch.py
│ ├── extract.py
│ └── extract_process.py
├── model_builder.py
└── scan
│ ├── __init__.py
│ ├── active_scanner.py
│ ├── local_scanner.py
│ ├── remote_scanner.py
│ └── scanner_process.py
├── docs
├── faq.md
├── images
│ ├── favicon.png
│ └── logo.png
├── index.md
├── install.md
└── usage.md
├── lftp
├── __init__.py
├── job_status.py
├── job_status_parser.py
└── lftp.py
├── mkdocs.yml
├── model
├── __init__.py
├── diff.py
├── file.py
└── model.py
├── poetry.lock
├── pyproject.toml
├── scan_fs.py
├── seedsync.py
├── ssh
├── __init__.py
└── sshcp.py
├── system
├── __init__.py
├── file.py
└── scanner.py
├── tests
├── __init__.py
├── integration
│ ├── __init__.py
│ ├── test_controller
│ │ ├── __init__.py
│ │ ├── test_controller.py
│ │ └── test_extract
│ │ │ ├── __init__.py
│ │ │ └── test_extract.py
│ ├── test_lftp
│ │ ├── __init__.py
│ │ └── test_lftp.py
│ └── test_web
│ │ ├── __init__.py
│ │ ├── test_handler
│ │ ├── __init__.py
│ │ ├── test_auto_queue.py
│ │ ├── test_config.py
│ │ ├── test_controller.py
│ │ ├── test_server.py
│ │ ├── test_status.py
│ │ ├── test_stream_log.py
│ │ ├── test_stream_model.py
│ │ └── test_stream_status.py
│ │ └── test_web_app.py
├── unittests
│ ├── __init__.py
│ ├── test_common
│ │ ├── __init__.py
│ │ ├── test_app_process.py
│ │ ├── test_config.py
│ │ ├── test_job.py
│ │ ├── test_multiprocessing_logger.py
│ │ ├── test_persist.py
│ │ └── test_status.py
│ ├── test_controller
│ │ ├── __init__.py
│ │ ├── test_auto_queue.py
│ │ ├── test_controller_persist.py
│ │ ├── test_extract
│ │ │ ├── __init__.py
│ │ │ ├── test_dispatch.py
│ │ │ └── test_extract_process.py
│ │ ├── test_model_builder.py
│ │ └── test_scan
│ │ │ ├── __init__.py
│ │ │ ├── test_remote_scanner.py
│ │ │ └── test_scanner_process.py
│ ├── test_lftp
│ │ ├── __init__.py
│ │ ├── test_job_status.py
│ │ ├── test_job_status_parser.py
│ │ └── test_lftp.py
│ ├── test_model
│ │ ├── __init__.py
│ │ ├── test_diff.py
│ │ ├── test_file.py
│ │ └── test_model.py
│ ├── test_seedsync.py
│ ├── test_ssh
│ │ ├── __init__.py
│ │ └── test_sshcp.py
│ ├── test_system
│ │ ├── __init__.py
│ │ ├── test_file.py
│ │ └── test_scanner.py
│ └── test_web
│ │ ├── __init__.py
│ │ ├── test_handler
│ │ └── test_stream_log.py
│ │ └── test_serialize
│ │ ├── __init__.py
│ │ ├── test_serialize.py
│ │ ├── test_serialize_auto_queue.py
│ │ ├── test_serialize_config.py
│ │ ├── test_serialize_log_record.py
│ │ ├── test_serialize_model.py
│ │ └── test_serialize_status.py
└── utils.py
└── web
├── __init__.py
├── handler
├── __init__.py
├── auto_queue.py
├── config.py
├── controller.py
├── server.py
├── status.py
├── stream_log.py
├── stream_model.py
└── stream_status.py
├── serialize
├── __init__.py
├── serialize.py
├── serialize_auto_queue.py
├── serialize_config.py
├── serialize_log_record.py
├── serialize_model.py
└── serialize_status.py
├── utils.py
├── web_app.py
├── web_app_builder.py
└── web_app_job.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.pyc
3 | /build
4 | .venv
5 | package-lock.json
6 | src/python/build
7 | src/python/site
--------------------------------------------------------------------------------
/doc/CodingGuidelines.md:
--------------------------------------------------------------------------------
1 | # Coding Guidelines
2 |
3 | ## Python
4 | 1. Try not to throw exceptions in constructors.
5 | Delay exceptions until after the web service is up and running.
6 | This allows us to notify the user about the error.
7 |
8 | 2. Try to keep constructors short and passive.
9 | Try not to start any threads or processes in constructors.
10 |
11 | 3. Do not rely on timing constraints in tests.
12 | That is, don't use `time.sleep()` to wait for something to happen.
13 | Actually wait for the condition and use a watchdog timer to check for failure.
14 |
15 | ## Angular
16 | 1. Keep constructor of Immutable.Record blank.
17 | Any pre-processing that is needed to convert a JS object to Record should be put in a factory function.
18 | This ensures that the Record object can be easily constructed for tests without having to know the JS object translations.
19 |
--------------------------------------------------------------------------------
/doc/assets/logo.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/doc/assets/logo.ai
--------------------------------------------------------------------------------
/doc/assets/logo_with_name.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/doc/assets/logo_with_name.ai
--------------------------------------------------------------------------------
/doc/images/install_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/doc/images/install_1.png
--------------------------------------------------------------------------------
/doc/images/install_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/doc/images/install_2.png
--------------------------------------------------------------------------------
/doc/images/logo_with_name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/doc/images/logo_with_name.png
--------------------------------------------------------------------------------
/doc/licenses/flaticon_license.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/doc/licenses/flaticon_license.pdf
--------------------------------------------------------------------------------
/src/angular/.angular-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "project": {
4 | "name": "seedsync"
5 | },
6 | "apps": [
7 | {
8 | "root": "src",
9 | "outDir": "dist",
10 | "assets": [
11 | "assets"
12 | ],
13 | "index": "index.html",
14 | "main": "main.ts",
15 | "polyfills": "polyfills.ts",
16 | "test": "test.ts",
17 | "tsconfig": "tsconfig.app.json",
18 | "testTsconfig": "tsconfig.spec.json",
19 | "prefix": "app",
20 | "styles": [
21 | "../node_modules/bootstrap/dist/css/bootstrap.min.css",
22 | "../node_modules/font-awesome/scss/font-awesome.scss",
23 | "styles.scss"
24 | ],
25 | "scripts": [
26 | "../node_modules/jquery/dist/jquery.min.js",
27 | "../node_modules/popper.js/dist/umd/popper.min.js",
28 | "../node_modules/bootstrap/dist/js/bootstrap.min.js"
29 | ],
30 | "environmentSource": "environments/environment.ts",
31 | "environments": {
32 | "dev": "environments/environment.ts",
33 | "prod": "environments/environment.prod.ts"
34 | }
35 | }
36 | ],
37 | "e2e": {
38 | "protractor": {
39 | "config": "./protractor.conf.js"
40 | }
41 | },
42 | "lint": [
43 | {
44 | "project": "src/tsconfig.app.json",
45 | "exclude": "**/node_modules/**"
46 | },
47 | {
48 | "project": "src/tsconfig.spec.json",
49 | "exclude": "**/node_modules/**"
50 | },
51 | {
52 | "project": "e2e/tsconfig.e2e.json",
53 | "exclude": "**/node_modules/**"
54 | }
55 | ],
56 | "test": {
57 | "karma": {
58 | "config": "./karma.conf.js"
59 | }
60 | },
61 | "defaults": {
62 | "styleExt": "scss",
63 | "component": {}
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/angular/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 4
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/src/angular/.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 |
8 | # dependencies
9 | /node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
27 | # misc
28 | /.sass-cache
29 | /connect.lock
30 | /coverage
31 | /libpeerconnection.log
32 | npm-debug.log
33 | testem.log
34 | /typings
35 | yarn-error.log
36 |
37 | # e2e
38 | /e2e/*.js
39 | /e2e/*.map
40 |
41 | # System Files
42 | .DS_Store
43 | Thumbs.db
44 |
--------------------------------------------------------------------------------
/src/angular/e2e/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { AppPage } from './app.po';
2 |
3 | describe('seedsync 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 app!');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/src/angular/e2e/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 |
--------------------------------------------------------------------------------
/src/angular/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": [
9 | "jasmine",
10 | "jasminewd2",
11 | "node"
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/angular/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/cli'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular/cli/plugins/karma'),
14 | require('karma-mocha-reporter')
15 | ],
16 | client: {
17 | clearContext: false, // leave Jasmine Spec Runner output visible in browser
18 | captureConsole: false
19 | },
20 | coverageIstanbulReporter: {
21 | reports: ['html', 'lcovonly'],
22 | fixWebpackSourcePaths: true
23 | },
24 | angularCli: {
25 | environment: 'dev'
26 | },
27 | reporters: ['mocha', 'kjhtml'],
28 | port: 9876,
29 | colors: true,
30 | logLevel: config.LOG_INFO,
31 | autoWatch: true,
32 | browsers: ['Chrome'],
33 | singleRun: false,
34 |
35 | customLaunchers: {
36 | ChromeHeadless: {
37 | base: 'Chrome',
38 | flags: [
39 | '--headless',
40 | '--disable-gpu',
41 | // Without a remote debugging port, Google Chrome exits immediately.
42 | '--remote-debugging-port=9222',
43 | '--no-sandbox'
44 | ]
45 | }
46 | },
47 | mochaReporter: {
48 | output: 'full'
49 | }
50 | });
51 | };
52 |
--------------------------------------------------------------------------------
/src/angular/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "seedsync",
3 | "version": "0.8.6",
4 | "license": "Apache 2.0",
5 | "scripts": {
6 | "ng": "ng",
7 | "start": "ng serve",
8 | "build": "ng build",
9 | "test": "ng test",
10 | "lint": "ng lint",
11 | "e2e": "ng e2e"
12 | },
13 | "private": true,
14 | "dependencies": {
15 | "@angular/animations": "^4.2.4",
16 | "@angular/common": "^4.2.4",
17 | "@angular/compiler": "^4.2.4",
18 | "@angular/core": "^4.2.4",
19 | "@angular/forms": "^4.2.4",
20 | "@angular/http": "^4.2.4",
21 | "@angular/platform-browser": "^4.2.4",
22 | "@angular/platform-browser-dynamic": "^4.2.4",
23 | "@angular/router": "^4.2.4",
24 | "@types/eventsource": "^1.0.2",
25 | "angular-webstorage-service": "^1.0.2",
26 | "bootstrap": "^4.2.1",
27 | "compare-versions": "^3.4.0",
28 | "core-js": "^2.4.1",
29 | "css-element-queries": "^1.1.1",
30 | "font-awesome": "^4.7.0",
31 | "immutable": "^3.8.2",
32 | "jquery": "^3.2.1",
33 | "ngx-modialog": "^3.0.4",
34 | "popper.js": "^1.14.6",
35 | "rxjs": "^5.4.2",
36 | "zone.js": "^0.8.14"
37 | },
38 | "devDependencies": {
39 | "@angular/cli": "1.3.2",
40 | "@angular/compiler-cli": "^4.2.4",
41 | "@angular/language-service": "^4.2.4",
42 | "@types/jasmine": "~2.5.53",
43 | "@types/jasminewd2": "~2.0.2",
44 | "@types/node": "^13.13.0",
45 | "codelyzer": "~3.1.1",
46 | "jasmine-core": "~2.6.2",
47 | "jasmine-spec-reporter": "~4.1.0",
48 | "karma": "~1.7.0",
49 | "karma-chrome-launcher": "~2.1.1",
50 | "karma-cli": "~1.0.1",
51 | "karma-coverage-istanbul-reporter": "^1.2.1",
52 | "karma-jasmine": "~1.1.0",
53 | "karma-jasmine-html-reporter": "^0.2.2",
54 | "karma-mocha-reporter": "^2.2.5",
55 | "node-sass": "^4.5.3",
56 | "protractor": "~5.1.2",
57 | "ts-node": "~3.2.0",
58 | "tslint": "~5.3.2",
59 | "typescript": "^3.2.2"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/angular/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 | './e2e/**/*.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: 'e2e/tsconfig.e2e.json'
25 | });
26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/src/angular/src/app/common/_common.scss:
--------------------------------------------------------------------------------
1 | $primary-color: #337BB7;
2 | $primary-dark-color: #2e6da4;
3 | $primary-light-color: #D7E7F4;
4 | $primary-lighter-color: #F6F6F6;
5 |
6 | $secondary-color: #79DFB6;
7 | $secondary-light-color: #C5F0DE;
8 | $secondary-dark-color: #32AD7B;
9 | $secondary-darker-color: #077F4F;
10 |
11 | $header-color: #DDDDDD;
12 | $header-dark-color: #D3D3D3;
13 |
14 | $logo-color: #118247;
15 | $logo-font: 'Arial Black', Gadget, sans-serif;
16 |
17 | $small-max-width: 600px;
18 | $medium-min-width: 601px;
19 | $medium-max-width: 992px;
20 | $large-min-width: 993px;
21 |
22 | $sidebar-width: 170px;
23 |
24 | $zindex-sidebar: 300;
25 | $zindex-top-header: 200;
26 | $zindex-file-options: 201;
27 | $zindex-file-search: 100;
28 |
29 | %button {
30 | background-color: $primary-color;
31 | color: white;
32 | border: 1px solid $primary-dark-color;
33 | border-radius: 4px;
34 | display: flex;
35 | flex-direction: column;
36 | align-items: center;
37 | justify-content: center;
38 | cursor: default;
39 | user-select: none;
40 |
41 | &:active {
42 | background-color: #286090;
43 | }
44 |
45 | &[disabled] {
46 | opacity: .65;
47 | background-color: $primary-color;
48 | }
49 |
50 | &.selected {
51 | background-color: $secondary-color;
52 | border-color: $secondary-darker-color;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/angular/src/app/common/cached-reuse-strategy.ts:
--------------------------------------------------------------------------------
1 | import {ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy} from "@angular/router";
2 |
3 | /**
4 | * CachedReuseStrategy caches Components so that they are not
5 | * recreated after navigating away.
6 | * Source: https://www.softwarearchitekt.at/post/2016/12/02/
7 | * sticky-routes-in-angular-2-3-with-routereusestrategy.aspx
8 | */
9 | export class CachedReuseStrategy implements RouteReuseStrategy {
10 |
11 | handlers: {[key: string]: DetachedRouteHandle} = {};
12 |
13 | // noinspection JSUnusedLocalSymbols
14 | shouldDetach(route: ActivatedRouteSnapshot): boolean {
15 | return true;
16 | }
17 |
18 | store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
19 | this.handlers[route.routeConfig.path] = handle;
20 | }
21 |
22 | shouldAttach(route: ActivatedRouteSnapshot): boolean {
23 | return !!route.routeConfig && !!this.handlers[route.routeConfig.path];
24 | }
25 |
26 | retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
27 | if (!route.routeConfig) { return null; }
28 | return this.handlers[route.routeConfig.path];
29 | }
30 |
31 | shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
32 | return future.routeConfig === curr.routeConfig;
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/angular/src/app/common/capitalize.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from "@angular/core";
2 |
3 | @Pipe({name: "capitalize"})
4 | export class CapitalizePipe implements PipeTransform {
5 |
6 | transform(value: any) {
7 | if (value) {
8 | return value.charAt(0).toUpperCase() + value.slice(1);
9 | }
10 | return value;
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/angular/src/app/common/click-stop-propagation.directive.ts:
--------------------------------------------------------------------------------
1 | import {Directive, HostListener} from "@angular/core";
2 |
3 | @Directive({
4 | selector: "[appClickStopPropagation]"
5 | })
6 | export class ClickStopPropagationDirective {
7 | @HostListener("click", ["$event"])
8 | public onClick(event: any): void {
9 | event.stopPropagation();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/angular/src/app/common/eta.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from "@angular/core";
2 |
3 | /*
4 | * Convert seconds to an eta in the form "Xh Ym Zs"
5 | */
6 | @Pipe({name: "eta"})
7 | export class EtaPipe implements PipeTransform {
8 |
9 | private units = {
10 | "h": 3600,
11 | "m": 60,
12 | "s": 1
13 | };
14 |
15 | transform(seconds: number = 0): string {
16 | if ( isNaN( parseFloat( String(seconds) )) || ! isFinite( seconds ) ) { return "?"; }
17 | if (seconds === 0) { return "0s"; }
18 |
19 | let out = "";
20 |
21 | for (const key of Object.keys(this.units)) {
22 | const unit = this.units[key];
23 | if (seconds >= unit) {
24 | const unitMultiplicity = Math.floor(seconds / unit);
25 | seconds -= unitMultiplicity * unit;
26 | out += Number(unitMultiplicity) + key;
27 | }
28 | }
29 | return out;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/angular/src/app/common/file-size.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from "@angular/core";
2 |
3 | /*
4 | * Convert bytes into largest possible unit.
5 | * Takes an precision argument that defaults to 2.
6 | * Usage:
7 | * bytes | fileSize:precision
8 | * Example:
9 | * {{ 1024 | fileSize}}
10 | * formats to: 1 KB
11 | * Source: https://gist.github.com/JonCatmull/ecdf9441aaa37
12 | * 336d9ae2c7f9cb7289a#file-file-size-pipe-ts
13 | */
14 | @Pipe({name: "fileSize"})
15 | export class FileSizePipe implements PipeTransform {
16 |
17 | private units = [
18 | "B",
19 | "KB",
20 | "MB",
21 | "GB",
22 | "TB",
23 | "PB"
24 | ];
25 |
26 | transform(bytes: number = 0, precision: number = 2 ): string {
27 | if ( isNaN( parseFloat( String(bytes) )) || ! isFinite( bytes ) ) { return "?"; }
28 |
29 | let unit = 0;
30 |
31 | while ( bytes >= 1024 ) {
32 | bytes /= 1024;
33 | unit ++;
34 | }
35 |
36 | return Number(bytes.toPrecision( + precision )) + " " + this.units[ unit ];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/angular/src/app/common/localization.ts:
--------------------------------------------------------------------------------
1 | export class Localization {
2 | static Error = class {
3 | public static readonly SERVER_DISCONNECTED = "Lost connection to the SeedSync service.";
4 | };
5 |
6 | static Notification = class {
7 | public static readonly CONFIG_RESTART = "Restart the app to apply new settings.";
8 | public static readonly CONFIG_VALUE_BLANK =
9 | (section: string, option: string) => `Setting ${section}.${option} cannot be blank.`
10 |
11 | public static readonly AUTOQUEUE_PATTERN_EMPTY = "Cannot add an empty autoqueue pattern.";
12 |
13 | public static readonly STATUS_CONNECTION_WAITING = "Waiting for SeedSync service to respond...";
14 | public static readonly STATUS_REMOTE_SCAN_WAITING = "Waiting for remote server to respond...";
15 | public static readonly STATUS_REMOTE_SERVER_ERROR = (error: string) =>
16 | `Lost connection to remote server. Retrying automatically. \
17 | ${error ? "
" + error : ""}`
18 |
19 | public static readonly NEW_VERSION_AVAILABLE = (url: string) =>
20 | `A new version of SeedSync is available! \
21 | Click here to grab the latest version.`
22 | };
23 |
24 | static Modal = class {
25 | public static readonly DELETE_LOCAL_TITLE = "Delete Local File";
26 | public static readonly DELETE_LOCAL_MESSAGE =
27 | (name: string) => `Are you sure you want to delete ${name} from the local server?`
28 |
29 | public static readonly DELETE_REMOTE_TITLE = "Delete Remote File";
30 | public static readonly DELETE_REMOTE_MESSAGE =
31 | (name: string) => `Are you sure you want to delete ${name} from the remote server?`
32 | };
33 |
34 | static Log = class {
35 | public static readonly CONNECTED = "Connected to service";
36 | public static readonly DISCONNECTED = "Lost connection to service";
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/src/angular/src/app/common/storage-keys.ts:
--------------------------------------------------------------------------------
1 | export class StorageKeys {
2 | public static readonly VIEW_OPTION_SHOW_DETAILS = "view-option-show-details";
3 | public static readonly VIEW_OPTION_SORT_METHOD = "view-option-sort-method";
4 | public static readonly VIEW_OPTION_PIN = "view-option-pin";
5 | }
6 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/about/about-page.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 |
SeedSync
6 |
7 |
v{{version}}
8 |
Copyright © 2017-2020 Inderpreet Singh
9 |
10 | Source available on
11 |
Github
12 |
13 |
14 | Icons by Freepik, Yannick, Dave Gandy, Google and Smashicons from
15 |
Flaticon
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/about/about-page.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../common/common';
2 |
3 | #about {
4 | display: flex;
5 | justify-content: center;
6 |
7 | #wrapper {
8 | margin-top: 50px;
9 | text-align: center;
10 |
11 | #banner {
12 | display: flex;
13 | align-items: center;
14 |
15 | img {
16 | max-width: 80px;
17 | max-height: 80px;
18 | }
19 | span {
20 | margin-left: 10px;
21 | color: $logo-color;
22 | font-family: $logo-font;
23 | font-size: 300%;
24 | cursor: default;
25 | user-select: none;
26 | }
27 | }
28 |
29 | #version {
30 | font-size: 150%;
31 | }
32 |
33 | #icons {
34 | font-size: 80%;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/about/about-page.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from "@angular/core";
2 |
3 | declare function require(moduleName: string): any;
4 | const { version: appVersion } = require('../../../../package.json');
5 |
6 | @Component({
7 | selector: "app-about-page",
8 | templateUrl: "./about-page.component.html",
9 | styleUrls: ["./about-page.component.scss"],
10 | providers: [],
11 | })
12 |
13 | export class AboutPageComponent {
14 |
15 | public version: string;
16 |
17 | constructor() {
18 | this.version = appVersion;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/autoqueue/autoqueue-page.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Files matching these patterns will be automatically queued.
6 | Wildcards (*) are supported.
7 | Add/Remove these patterns below.
8 |
9 |
10 |
11 | Patterns are disabled. All files will be auto-queued.
12 | To restrict which files are auto-queued, enable patterns in Settings.
13 |
14 |
15 |
16 | Auto-Queue is disabled.
17 | Enable AutoQueue in Settings to queue files automatically.
18 |
19 |
20 |
21 |
23 |
26 | −
27 |
28 |
{{pattern.pattern}}
29 |
30 |
31 |
34 | +
35 |
36 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/autoqueue/autoqueue-page.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../common/common';
2 |
3 | #autoqueue {
4 | padding: 15px;
5 |
6 | #description {
7 | font-size: 100%;
8 | }
9 |
10 | #controls {
11 | &[disabled] {
12 | opacity: .65;
13 | }
14 |
15 | .pattern,
16 | #add-pattern {
17 | display: flex;
18 | flex-direction: row;
19 | flex-wrap: nowrap;
20 | align-items: center;
21 | margin: 10px;
22 |
23 | .button {
24 | @extend %button;
25 |
26 | flex-grow: 0;
27 | flex-direction: row;
28 | padding: 8px;
29 | margin-right: 12px;
30 | height: 35px;
31 | width: 35px;
32 |
33 | span {
34 | font-size: 220%;
35 | font-weight: 900;
36 | }
37 | }
38 | }
39 |
40 | .pattern {
41 | .text {
42 | font-size: 100%;
43 | font-family: monospace;
44 | white-space: pre-wrap;
45 |
46 | /* break up long text */
47 | overflow-wrap: normal;
48 | hyphens: auto;
49 | word-break: break-all;
50 | }
51 |
52 | .button {
53 | background-color: red;
54 | border-color: darkred;
55 |
56 | &:active {
57 | background-color: darkred;
58 | }
59 |
60 | &[disabled] {
61 | opacity: .65;
62 | background-color: red;
63 | }
64 | }
65 | }
66 |
67 | #add-pattern {
68 | .button {
69 | background-color: green;
70 | border-color: darkgreen;
71 |
72 | &:active {
73 | background-color: darkgreen;
74 | }
75 |
76 | &[disabled] {
77 | opacity: .65;
78 | background-color: green;
79 | }
80 | }
81 |
82 | label {
83 | margin-bottom: 0;
84 | }
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/files/file-list.component.html:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/files/file-list.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../common/common';
2 |
3 | #file-list #header {
4 | display: none;
5 | }
6 |
7 | /* striped rows */
8 | #file-list > div:nth-child(even){
9 | background-color: $primary-lighter-color;
10 | }
11 |
12 | /* list separator */
13 | #file-list > div {border-bottom: 1px solid #ddd;}
14 | #file-list > div:last-child{border-bottom: none;}
15 |
16 |
17 | /* Medium and large screens */
18 | @media only screen and (min-width: $medium-min-width) {
19 | #file-list #header {
20 | display: flex;
21 | }
22 |
23 | /* width */
24 | /* NOTE: make sure this is in-sync with ".file .content" */
25 | #header .status {width: 100px; min-width: 100px;}
26 | #header .name {flex-grow: 1;}
27 | #header .speed {width: 100px; min-width: 100px;}
28 | #header .eta {width: 100px; min-width: 100px;}
29 | #header .size {width: 30%; min-width: 30%;}
30 |
31 | /* header and content elements */
32 | #header > div, .content > div {
33 | padding: 8px 8px;
34 | text-align: center;
35 | vertical-align: top;
36 | }
37 | #header .name, .content .name {text-align: left;}
38 |
39 | /* header color */
40 | #header div {
41 | font-weight: bold;
42 | color: #fff;
43 | background-color: #000;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/files/files-page.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/files/files-page.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from "@angular/core";
2 |
3 | @Component({
4 | selector: "app-files-page",
5 | templateUrl: "./files-page.component.html"
6 | })
7 |
8 | export class FilesPageComponent {
9 | }
10 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/logs/logs-page.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 | {{record.time | date: 'yyyy/MM/dd HH:mm:ss'}} -
11 | {{record.level}} -
12 | {{record.loggerName}} -
13 | {{record.message}}
14 |
15 |
16 | {{record.exceptionTraceback}}
17 |
18 |
19 |
20 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
43 |
44 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/logs/logs-page.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../common/common';
2 |
3 | #logs {
4 | padding: 5px 15px;
5 | font-family: monospace;
6 | font-size: 70%;
7 |
8 | display: flex;
9 | flex-direction: column;
10 | justify-content: center;
11 |
12 | .log-marker {
13 | width: 100%;
14 | height: 2px;
15 | }
16 |
17 | .btn-scroll {
18 | @extend %button;
19 | position: sticky;
20 | display: none;
21 |
22 | &.visible {
23 | display: inherit;
24 | }
25 | }
26 |
27 | #btn-scroll-top {
28 | top: 0;
29 | }
30 | #btn-scroll-bottom {
31 | bottom: 0;
32 | }
33 |
34 | p.record {
35 | margin: 0;
36 |
37 | /* break up long text */
38 | overflow-wrap: normal;
39 | hyphens: auto;
40 | word-break: break-all;
41 |
42 | &.debug {
43 | color: darkgray;
44 | }
45 |
46 | &.info {
47 | color: black;
48 | }
49 |
50 | &.warning {
51 | // copied from bootstrap alert-warning
52 | color: #8a6d3b;
53 | background-color: #fcf8e3;
54 | border-color: #faebcc;
55 | }
56 |
57 | &.error, &.critical {
58 | // copied from bootstrap alert-danger
59 | color: #a94442;
60 | background-color: #f2dede;
61 | border-color: #ebccd1;
62 | }
63 |
64 | span.traceback {
65 | display: block;
66 | white-space: pre-line;
67 | margin-left: 30px;
68 |
69 | /* break up long text */
70 | overflow-wrap: normal;
71 | hyphens: auto;
72 | word-break: break-all;
73 | }
74 | }
75 |
76 | .connected {
77 | height: 12px;
78 | width: 100%;
79 | text-align: center;
80 | border-bottom: 1px solid #e3e3e3;
81 | margin-bottom: 15px;
82 |
83 | span {
84 | background-color: #f5f5f5;
85 | font-size: 10px;
86 | padding: 5px;
87 | }
88 | }
89 | }
90 |
91 | /* Medium and large screens */
92 | @media only screen and (min-width: $medium-min-width) {
93 | #logs {
94 | font-size: 95%;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/main/app.component.html:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
26 |
27 |
28 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/main/app.component.ts:
--------------------------------------------------------------------------------
1 | import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from "@angular/core";
2 | import {NavigationEnd, Router} from "@angular/router";
3 | import {ROUTE_INFOS, RouteInfo} from "../../routes";
4 |
5 | import {ElementQueries, ResizeSensor} from "css-element-queries";
6 | import {DomService} from "../../services/utils/dom.service";
7 |
8 | @Component({
9 | selector: "app-root",
10 | templateUrl: "./app.component.html",
11 | styleUrls: ["./app.component.scss"]
12 | })
13 | export class AppComponent implements OnInit, AfterViewInit {
14 | @ViewChild("topHeader") topHeader: ElementRef;
15 |
16 | showSidebar = false;
17 | activeRoute: RouteInfo;
18 |
19 | constructor(private router: Router,
20 | private _domService: DomService) {
21 | // Navigation listener
22 | // Close the sidebar
23 | // Store the active route
24 | router.events.subscribe(() => {
25 | this.showSidebar = false;
26 | this.activeRoute = ROUTE_INFOS.find(value => "/" + value.path === router.url);
27 | });
28 | }
29 |
30 | ngOnInit() {
31 | // Scroll to top on route changes
32 | this.router.events.subscribe((evt) => {
33 | if (!(evt instanceof NavigationEnd)) {
34 | return;
35 | }
36 | window.scrollTo(0, 0);
37 | });
38 | }
39 |
40 | ngAfterViewInit() {
41 | ElementQueries.listen();
42 | ElementQueries.init();
43 | // noinspection TsLint
44 | new ResizeSensor(this.topHeader.nativeElement, () => {
45 | this._domService.setHeaderHeight(this.topHeader.nativeElement.clientHeight);
46 | });
47 | }
48 |
49 | title = "app";
50 | }
51 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/main/header.component.html:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/main/header.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../common/common';
2 |
3 | #header {
4 | .alert {
5 | padding: 5px;
6 | margin-bottom: 0;
7 | border-radius: 0;
8 |
9 | button.close {
10 | top: 5px;
11 | right: 5px;
12 | cursor: default;
13 | user-select: none;
14 | outline: none;
15 | opacity: .2;
16 | font-size: 160%;
17 | padding: 0;
18 |
19 | &:active {
20 | color: red;
21 | }
22 |
23 | &:hover {
24 | color: inherit;
25 | opacity: .2;
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/main/sidebar.component.html:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/main/sidebar.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../common/common';
2 |
3 | #sidebar {
4 | .button {
5 | background-color: inherit;
6 | color: inherit;
7 | text-decoration: none;
8 | font-weight: bolder;
9 | display: block;
10 | width: 100%;
11 | padding: 8px 16px;
12 | text-align: left;
13 | border: none;
14 | outline: none;
15 | white-space: normal;
16 | float: none;
17 | cursor: default;
18 | user-select: none;
19 |
20 | &:active {
21 | background-color: $primary-color;
22 | }
23 |
24 | &.selected {
25 | background-color: $secondary-color;
26 | color: white;
27 | border-color: #6ac19e;
28 |
29 | img {
30 | filter: invert(1.0);
31 | }
32 | }
33 |
34 | &[disabled] {
35 | opacity: .65;
36 | background-color: inherit;
37 | }
38 |
39 | img {
40 | width: 18px;
41 | height: 18px;
42 | margin-right: 4px;
43 | margin-bottom: 2px;
44 | }
45 | }
46 |
47 | hr {
48 | margin-top: 3px;
49 | margin-bottom: 3px;
50 | border: 1px solid $header-dark-color;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/main/sidebar.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnInit} from "@angular/core";
2 |
3 | import {ROUTE_INFOS} from "../../routes";
4 | import {ServerCommandService} from "../../services/server/server-command.service";
5 | import {LoggerService} from "../../services/utils/logger.service";
6 | import {ConnectedService} from "../../services/utils/connected.service";
7 | import {StreamServiceRegistry} from "../../services/base/stream-service.registry";
8 |
9 | @Component({
10 | selector: "app-sidebar",
11 | templateUrl: "./sidebar.component.html",
12 | styleUrls: ["./sidebar.component.scss"]
13 | })
14 |
15 | export class SidebarComponent implements OnInit {
16 | routeInfos = ROUTE_INFOS;
17 |
18 | public commandsEnabled: boolean;
19 |
20 | private _connectedService: ConnectedService;
21 |
22 | constructor(private _logger: LoggerService,
23 | _streamServiceRegistry: StreamServiceRegistry,
24 | private _commandService: ServerCommandService) {
25 | this._connectedService = _streamServiceRegistry.connectedService;
26 | this.commandsEnabled = false;
27 | }
28 |
29 | // noinspection JSUnusedGlobalSymbols
30 | ngOnInit() {
31 | this._connectedService.connected.subscribe({
32 | next: (connected: boolean) => {
33 | this.commandsEnabled = connected;
34 | }
35 | });
36 | }
37 |
38 | onCommandRestart() {
39 | this._commandService.restart().subscribe({
40 | next: reaction => {
41 | if (reaction.success) {
42 | this._logger.info(reaction.data);
43 | } else {
44 | this._logger.error(reaction.errorMessage);
45 | }
46 | }
47 | });
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/settings/option.component.html:
--------------------------------------------------------------------------------
1 | '">
3 |
4 |
6 |
7 |
8 |
19 |
20 |
21 |
32 |
33 |
34 |
45 |
46 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/settings/option.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../common/common';
2 |
3 | .form-group {
4 | margin-bottom: 0;
5 | margin-left: 20px;
6 | margin-right: 20px;
7 | }
8 |
9 | .error {
10 | background-color: #f2dede;
11 | color: #a94442;
12 | border: 1px solid #a94442;
13 | }
14 |
15 | label {
16 | width: 100%;
17 | display: flex;
18 | flex-direction: row;
19 | flex-wrap: wrap;
20 | align-items: center;
21 | }
22 |
23 | span.description {
24 | color: darkgrey;
25 | font-size: 80%;
26 | line-height: initial;
27 | width: 100%;
28 | white-space: pre-line; // renders newline as
29 | }
30 |
31 | input[type="checkbox"] {
32 | width: auto;
33 | margin-right: 10px;
34 | box-shadow: none;
35 | height: 20px;
36 |
37 | &:focus {
38 | box-shadow: none;
39 | }
40 |
41 | & ~ .description {
42 | margin-left: 23px;
43 | }
44 | }
45 |
46 | input[type="text"] {
47 | width: 100%;
48 | }
49 |
--------------------------------------------------------------------------------
/src/angular/src/app/pages/settings/option.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input, Output, ChangeDetectionStrategy, EventEmitter, OnInit} from "@angular/core";
2 | import {Subject} from "rxjs/Subject";
3 |
4 | @Component({
5 | selector: "app-option",
6 | providers: [],
7 | templateUrl: "./option.component.html",
8 | styleUrls: ["./option.component.scss"],
9 | changeDetection: ChangeDetectionStrategy.OnPush
10 | })
11 |
12 | export class OptionComponent implements OnInit {
13 | @Input() type: OptionType;
14 | @Input() label: string;
15 | @Input() value: any;
16 | @Input() description: string;
17 |
18 | @Output() changeEvent = new EventEmitter();
19 |
20 | // expose to template
21 | public OptionType = OptionType;
22 |
23 | private readonly DEBOUNCE_TIME_MS: number = 1000;
24 |
25 | private newValue = new Subject();
26 |
27 | // noinspection JSUnusedGlobalSymbols
28 | ngOnInit(): void {
29 | // Debounce
30 | // References:
31 | // https://angular.io/tutorial/toh-pt6#fix-the-herosearchcomponent-class
32 | // https://stackoverflow.com/a/41965515
33 | this.newValue
34 | .debounceTime(this.DEBOUNCE_TIME_MS)
35 | .distinctUntilChanged()
36 | .subscribe({next: val => this.changeEvent.emit(val)});
37 | }
38 |
39 | onChange(value: any) {
40 | this.newValue.next(value);
41 | }
42 | }
43 |
44 | export enum OptionType {
45 | Text,
46 | Checkbox,
47 | Password
48 | }
49 |
--------------------------------------------------------------------------------
/src/angular/src/app/routes.ts:
--------------------------------------------------------------------------------
1 | import {Routes} from "@angular/router";
2 |
3 | import * as Immutable from "immutable";
4 |
5 | import {FilesPageComponent} from "./pages/files/files-page.component";
6 | import {SettingsPageComponent} from "./pages/settings/settings-page.component";
7 | import {AutoQueuePageComponent} from "./pages/autoqueue/autoqueue-page.component";
8 | import {LogsPageComponent} from "./pages/logs/logs-page.component";
9 | import {AboutPageComponent} from "./pages/about/about-page.component";
10 |
11 | export interface RouteInfo {
12 | path: string;
13 | name: string;
14 | icon: string;
15 | component: any;
16 | }
17 |
18 | export const ROUTE_INFOS: Immutable.List = Immutable.List([
19 | {
20 | path: "dashboard",
21 | name: "Dashboard",
22 | icon: "assets/icons/dashboard.svg",
23 | component: FilesPageComponent
24 | },
25 | {
26 | path: "settings",
27 | name: "Settings",
28 | icon: "assets/icons/settings.svg",
29 | component: SettingsPageComponent
30 | },
31 | {
32 | path: "autoqueue",
33 | name: "AutoQueue",
34 | icon: "assets/icons/autoqueue.svg",
35 | component: AutoQueuePageComponent
36 | },
37 | {
38 | path: "logs",
39 | name: "Logs",
40 | icon: "assets/icons/logs.svg",
41 | component: LogsPageComponent
42 | },
43 | {
44 | path: "about",
45 | name: "About",
46 | icon: "assets/icons/about.svg",
47 | component: AboutPageComponent
48 | }
49 | ]);
50 |
51 | export const ROUTES: Routes = [
52 | {
53 | path: "",
54 | redirectTo: "/dashboard",
55 | pathMatch: "full"
56 | },
57 | {
58 | path: "dashboard",
59 | component: FilesPageComponent
60 | },
61 | {
62 | path: "settings",
63 | component: SettingsPageComponent
64 | },
65 | {
66 | path: "autoqueue",
67 | component: AutoQueuePageComponent
68 | },
69 | {
70 | path: "logs",
71 | component: LogsPageComponent
72 | },
73 | {
74 | path: "about",
75 | component: AboutPageComponent
76 | }
77 | ];
78 |
--------------------------------------------------------------------------------
/src/angular/src/app/services/autoqueue/autoqueue-pattern.ts:
--------------------------------------------------------------------------------
1 | import {Record} from "immutable";
2 |
3 | interface IAutoQueuePattern {
4 | pattern: string;
5 | }
6 | const DefaultAutoQueuePattern: IAutoQueuePattern = {
7 | pattern: null
8 | };
9 | const AutoQueuePatternRecord = Record(DefaultAutoQueuePattern);
10 |
11 |
12 | export class AutoQueuePattern extends AutoQueuePatternRecord implements IAutoQueuePattern {
13 | pattern: string;
14 |
15 | constructor(props) {
16 | super(props);
17 | }
18 | }
19 |
20 | /**
21 | * ServerStatus as serialized by the backend.
22 | * Note: naming convention matches that used in JSON
23 | */
24 | export interface AutoQueuePatternJson {
25 | pattern: string;
26 | }
27 |
--------------------------------------------------------------------------------
/src/angular/src/app/services/base/base-stream.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from "@angular/core";
2 |
3 | import {IStreamService} from "./stream-service.registry";
4 |
5 |
6 | /**
7 | * BaseStreamService represents a web services that fetches data
8 | * from a SSE stream. This class provides utilities to register
9 | * for event notifications from a multiplexed stream.
10 | *
11 | * Note: services derived from this class SHOULD NOT be created
12 | * directly. They need to be added to StreamServiceRegistry
13 | * and fetched from an instance of that registry class.
14 | */
15 | @Injectable()
16 | export abstract class BaseStreamService implements IStreamService {
17 |
18 | private _eventNames: string[] = [];
19 |
20 |
21 | constructor() {}
22 |
23 | getEventNames(): string[] {
24 | return this._eventNames;
25 | }
26 |
27 | notifyConnected() {
28 | this.onConnected();
29 | }
30 |
31 | notifyDisconnected() {
32 | this.onDisconnected();
33 | }
34 |
35 | notifyEvent(eventName: string, data: string) {
36 | this.onEvent(eventName, data);
37 | }
38 |
39 | protected registerEventName(eventName: string) {
40 | this._eventNames.push(eventName);
41 | }
42 |
43 | /**
44 | * Callback for a new event
45 | * @param {string} eventName
46 | * @param {string} data
47 | */
48 | protected abstract onEvent(eventName: string, data: string);
49 |
50 | /**
51 | * Callback for connected
52 | */
53 | protected abstract onConnected();
54 |
55 | /**
56 | * Callback for disconnected
57 | */
58 | protected abstract onDisconnected();
59 | }
60 |
--------------------------------------------------------------------------------
/src/angular/src/app/services/base/base-web.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from "@angular/core";
2 |
3 | import {StreamServiceRegistry} from "./stream-service.registry";
4 | import {ConnectedService} from "../utils/connected.service";
5 |
6 |
7 | /**
8 | * BaseWebService provides utility to be notified when connection to
9 | * the backend server is lost and regained. Non-streaming web services
10 | * can use these notifications to re-issue get requests.
11 | */
12 | @Injectable()
13 | export abstract class BaseWebService {
14 |
15 | private _connectedService: ConnectedService;
16 |
17 | /**
18 | * Call this method to finish initialization
19 | */
20 | public onInit() {
21 | this._connectedService.connected.subscribe({
22 | next: connected => {
23 | if(connected) {
24 | this.onConnected();
25 | } else {
26 | this.onDisconnected();
27 | }
28 | }
29 | });
30 | }
31 |
32 | constructor(_streamServiceProvider: StreamServiceRegistry) {
33 | this._connectedService = _streamServiceProvider.connectedService;
34 | }
35 |
36 |
37 | /**
38 | * Callback for connected
39 | */
40 | protected abstract onConnected(): void;
41 |
42 | /**
43 | * Callback for disconnected
44 | */
45 | protected abstract onDisconnected(): void;
46 | }
47 |
--------------------------------------------------------------------------------
/src/angular/src/app/services/files/view-file-options.ts:
--------------------------------------------------------------------------------
1 | import {Record} from "immutable";
2 |
3 | import {ViewFile} from "./view-file";
4 |
5 | /**
6 | * View file options
7 | * Describes display related options for view files
8 | */
9 | interface IViewFileOptions {
10 | // Show additional details about the view file
11 | showDetails: boolean;
12 |
13 | // Method to use to sort the view file list
14 | sortMethod: ViewFileOptions.SortMethod;
15 |
16 | // Status filter setting
17 | selectedStatusFilter: ViewFile.Status;
18 |
19 | // Name filter setting
20 | nameFilter: string;
21 |
22 | // Track filter pin status
23 | pinFilter: boolean;
24 | }
25 |
26 |
27 | // Boiler plate code to set up an immutable class
28 | const DefaultViewFileOptions: IViewFileOptions = {
29 | showDetails: null,
30 | sortMethod: null,
31 | selectedStatusFilter: null,
32 | nameFilter: null,
33 | pinFilter: null,
34 | };
35 | const ViewFileOptionsRecord = Record(DefaultViewFileOptions);
36 |
37 |
38 | /**
39 | * Immutable class that implements the interface
40 | */
41 | export class ViewFileOptions extends ViewFileOptionsRecord implements IViewFileOptions {
42 | showDetails: boolean;
43 | sortMethod: ViewFileOptions.SortMethod;
44 | selectedStatusFilter: ViewFile.Status;
45 | nameFilter: string;
46 | pinFilter: boolean;
47 |
48 | constructor(props) {
49 | super(props);
50 | }
51 | }
52 |
53 | export module ViewFileOptions {
54 | export enum SortMethod {
55 | STATUS,
56 | NAME_ASC,
57 | NAME_DESC
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/angular/src/app/services/logs/log-record.ts:
--------------------------------------------------------------------------------
1 | import {Record} from "immutable";
2 |
3 |
4 | /**
5 | * LogRecord immutable
6 | */
7 | interface ILogRecord {
8 | time: Date;
9 | level: LogRecord.Level;
10 | loggerName: string;
11 | message: string;
12 | exceptionTraceback: string;
13 | }
14 | const DefaultLogRecord: ILogRecord = {
15 | time: null,
16 | level: null,
17 | loggerName: null,
18 | message: null,
19 | exceptionTraceback: null,
20 | };
21 | const LogRecordRecord = Record(DefaultLogRecord);
22 | export class LogRecord extends LogRecordRecord implements ILogRecord {
23 | time: Date;
24 | level: LogRecord.Level;
25 | loggerName: string;
26 | message: string;
27 | exceptionTraceback: string;
28 |
29 | constructor(props) {
30 | super(props);
31 | }
32 | }
33 |
34 |
35 | export module LogRecord {
36 | export function fromJson(json: LogRecordJson): LogRecord {
37 | return new LogRecord({
38 | // str -> number, then sec -> ms
39 | time: new Date(1000 * +json.time),
40 | level: LogRecord.Level[json.level_name],
41 | loggerName: json.logger_name,
42 | message: json.message,
43 | exceptionTraceback: json.exc_tb
44 | });
45 | }
46 |
47 | export enum Level {
48 | DEBUG = "DEBUG",
49 | INFO = "INFO",
50 | WARNING = "WARNING",
51 | ERROR = "ERROR",
52 | CRITICAL = "CRITICAL",
53 | }
54 | }
55 |
56 |
57 | /**
58 | * LogRecord as serialized by the backend.
59 | * Note: naming convention matches that used in JSON
60 | */
61 | export interface LogRecordJson {
62 | time: number;
63 | level_name: string;
64 | logger_name: string;
65 | message: string;
66 | exc_tb: string;
67 | }
68 |
--------------------------------------------------------------------------------
/src/angular/src/app/services/logs/log.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from "@angular/core";
2 | import {Observable} from "rxjs/Observable";
3 | import {ReplaySubject} from "rxjs/ReplaySubject";
4 |
5 | import {BaseStreamService} from "../base/base-stream.service";
6 | import {LogRecord} from "./log-record";
7 |
8 |
9 | @Injectable()
10 | export class LogService extends BaseStreamService {
11 |
12 | private _logs: ReplaySubject = new ReplaySubject();
13 |
14 | constructor() {
15 | super();
16 | this.registerEventName("log-record");
17 | }
18 |
19 | /**
20 | * Logs is a hot observable (i.e. no caching)
21 | * @returns {Observable}
22 | */
23 | get logs(): Observable {
24 | return this._logs.asObservable();
25 | }
26 |
27 | protected onEvent(eventName: string, data: string) {
28 | this._logs.next(LogRecord.fromJson(JSON.parse(data)));
29 | }
30 |
31 | protected onConnected() {
32 | // nothing to do
33 | }
34 |
35 | protected onDisconnected() {
36 | // nothing to do
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/src/angular/src/app/services/server/server-command.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from "@angular/core";
2 | import {Observable} from "rxjs/Observable";
3 |
4 | import {BaseWebService} from "../base/base-web.service";
5 | import {StreamServiceRegistry} from "../base/stream-service.registry";
6 | import {RestService, WebReaction} from "../utils/rest.service";
7 |
8 |
9 | /**
10 | * ServerCommandService handles sending commands to the backend server
11 | */
12 | @Injectable()
13 | export class ServerCommandService extends BaseWebService {
14 | private readonly RESTART_URL = "/server/command/restart";
15 |
16 | constructor(_streamServiceProvider: StreamServiceRegistry,
17 | private _restService: RestService) {
18 | super(_streamServiceProvider);
19 | }
20 |
21 | /**
22 | * Send a restart command to the server
23 | * @returns {Observable}
24 | */
25 | public restart(): Observable {
26 | return this._restService.sendRequest(this.RESTART_URL);
27 | }
28 |
29 | protected onConnected() {
30 | // Nothing to do
31 | }
32 |
33 | protected onDisconnected() {
34 | // Nothing to do
35 | }
36 | }
37 |
38 | /**
39 | * ConfigService factory and provider
40 | */
41 | export let serverCommandServiceFactory = (
42 | _streamServiceRegistry: StreamServiceRegistry,
43 | _restService: RestService
44 | ) => {
45 | const serverCommandService = new ServerCommandService(_streamServiceRegistry, _restService);
46 | serverCommandService.onInit();
47 | return serverCommandService;
48 | };
49 |
50 | // noinspection JSUnusedGlobalSymbols
51 | export let ServerCommandServiceProvider = {
52 | provide: ServerCommandService,
53 | useFactory: serverCommandServiceFactory,
54 | deps: [StreamServiceRegistry, RestService]
55 | };
56 |
--------------------------------------------------------------------------------
/src/angular/src/app/services/server/server-status.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from "@angular/core";
2 | import {Observable} from "rxjs/Observable";
3 | import {BehaviorSubject} from "rxjs/Rx";
4 |
5 | import {Localization} from "../../common/localization";
6 | import {ServerStatus, ServerStatusJson} from "./server-status";
7 | import {BaseStreamService} from "../base/base-stream.service";
8 |
9 |
10 | @Injectable()
11 | export class ServerStatusService extends BaseStreamService {
12 |
13 | private _status: BehaviorSubject =
14 | new BehaviorSubject(new ServerStatus({
15 | server: {
16 | up: false,
17 | errorMessage: Localization.Notification.STATUS_CONNECTION_WAITING
18 | }
19 | }));
20 |
21 | constructor() {
22 | super();
23 | this.registerEventName("status");
24 | }
25 |
26 | get status(): Observable {
27 | return this._status.asObservable();
28 | }
29 |
30 | protected onEvent(eventName: string, data: string) {
31 | this.parseStatus(data);
32 | }
33 |
34 | protected onConnected() {
35 | // nothing to do
36 | }
37 |
38 | protected onDisconnected() {
39 | // Notify the clients
40 | this._status.next(new ServerStatus({
41 | server: {
42 | up: false,
43 | errorMessage: Localization.Error.SERVER_DISCONNECTED
44 | }
45 | }));
46 | }
47 |
48 | /**
49 | * Parse an event and notify subscribers
50 | * @param {string} data
51 | */
52 | private parseStatus(data: string) {
53 | const statusJson: ServerStatusJson = JSON.parse(data);
54 | const status = ServerStatus.fromJson(statusJson);
55 | this._status.next(status);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/angular/src/app/services/utils/connected.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from "@angular/core";
2 | import {Observable} from "rxjs/Observable";
3 | import {BehaviorSubject} from "rxjs/Rx";
4 |
5 | import {LoggerService} from "./logger.service";
6 | import {BaseStreamService} from "../base/base-stream.service";
7 | import {RestService} from "./rest.service";
8 |
9 |
10 | /**
11 | * ConnectedService exposes the connection status to clients
12 | * as an Observable
13 | */
14 | @Injectable()
15 | export class ConnectedService extends BaseStreamService {
16 |
17 | // For clients
18 | private _connectedSubject: BehaviorSubject = new BehaviorSubject(false);
19 |
20 | constructor() {
21 | super();
22 | // No events to register
23 | }
24 |
25 | get connected(): Observable {
26 | return this._connectedSubject.asObservable();
27 | }
28 |
29 | protected onEvent(eventName: string, data: string) {
30 | // Nothing to do
31 | }
32 |
33 | protected onConnected() {
34 | if(this._connectedSubject.getValue() === false) {
35 | this._connectedSubject.next(true);
36 | }
37 | }
38 |
39 | protected onDisconnected() {
40 | if(this._connectedSubject.getValue() === true) {
41 | this._connectedSubject.next(false);
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/angular/src/app/services/utils/dom.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from "@angular/core";
2 | import {Observable} from "rxjs/Observable";
3 | import {BehaviorSubject} from "rxjs/Rx";
4 |
5 |
6 | /**
7 | * DomService facilitates inter-component communication related
8 | * to DOM updates
9 | */
10 | @Injectable()
11 | export class DomService {
12 | private _headerHeight: BehaviorSubject = new BehaviorSubject(0);
13 |
14 | get headerHeight(): Observable{
15 | return this._headerHeight.asObservable();
16 | }
17 |
18 | public setHeaderHeight(height: number) {
19 | if(height !== this._headerHeight.getValue()) {
20 | this._headerHeight.next(height);
21 | }
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/angular/src/app/services/utils/logger.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from "@angular/core";
2 |
3 | @Injectable()
4 | export class LoggerService {
5 |
6 | public level: LoggerService.Level;
7 |
8 | constructor() {
9 | this.level = LoggerService.Level.DEBUG;
10 | }
11 |
12 | get debug() {
13 | if (this.level >= LoggerService.Level.DEBUG) {
14 | return console.debug.bind(console);
15 | } else {
16 | return () => {};
17 | }
18 | }
19 |
20 | get info() {
21 | if (this.level >= LoggerService.Level.INFO) {
22 | return console.log.bind(console);
23 | } else {
24 | return () => {};
25 | }
26 | }
27 |
28 | // noinspection JSUnusedGlobalSymbols
29 | get warn() {
30 | if (this.level >= LoggerService.Level.WARN) {
31 | return console.warn.bind(console);
32 | } else {
33 | return () => {};
34 | }
35 | }
36 |
37 | get error() {
38 | if (this.level >= LoggerService.Level.ERROR) {
39 | return console.error.bind(console);
40 | } else {
41 | return () => {};
42 | }
43 | }
44 | }
45 |
46 | export module LoggerService {
47 | export enum Level {
48 | ERROR,
49 | WARN,
50 | INFO,
51 | DEBUG,
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/angular/src/app/services/utils/notification.ts:
--------------------------------------------------------------------------------
1 | import {Record} from "immutable";
2 |
3 | interface INotification {
4 | level: Notification.Level;
5 | text: string;
6 | timestamp: number;
7 | dismissible: boolean;
8 | }
9 | const DefaultNotification: INotification = {
10 | level: null,
11 | text: null,
12 | timestamp: null,
13 | dismissible: false,
14 | };
15 | const NotificationRecord = Record(DefaultNotification);
16 |
17 |
18 | export class Notification extends NotificationRecord implements INotification {
19 | level: Notification.Level;
20 | text: string;
21 | timestamp: number;
22 | dismissible: boolean;
23 |
24 | constructor(props) {
25 | props.timestamp = Date.now();
26 |
27 | super(props);
28 | }
29 | }
30 |
31 |
32 | export module Notification {
33 | export enum Level {
34 | SUCCESS = "success",
35 | INFO = "info",
36 | WARNING = "warning",
37 | DANGER = "danger",
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/angular/src/app/tests/mocks/mock-event-source.ts:
--------------------------------------------------------------------------------
1 | declare let spyOn: any;
2 |
3 | export class MockEventSource {
4 | url: string;
5 | onopen: (event: Event) => any;
6 | onerror: (event: Event) => any;
7 |
8 | eventListeners: Map = new Map();
9 |
10 | constructor(url: string) {
11 | this.url = url;
12 | }
13 |
14 | addEventListener(type: string, listener: EventListener) {
15 | this.eventListeners.set(type, listener);
16 | }
17 |
18 | close() {}
19 | }
20 |
21 | export function createMockEventSource(url: string): MockEventSource {
22 | let mockEventSource = new MockEventSource(url);
23 | spyOn(mockEventSource, 'addEventListener').and.callThrough();
24 | spyOn(mockEventSource, 'close').and.callThrough();
25 | return mockEventSource;
26 | }
27 |
--------------------------------------------------------------------------------
/src/angular/src/app/tests/mocks/mock-model-file.service.ts:
--------------------------------------------------------------------------------
1 | import {Subject} from "rxjs/Subject";
2 | import {Observable} from "rxjs/Observable";
3 |
4 | import * as Immutable from "immutable";
5 |
6 | import {ModelFile} from "../../services/files/model-file";
7 |
8 |
9 | export class MockModelFileService {
10 |
11 | _files = new Subject>();
12 |
13 | get files(): Observable> {
14 | return this._files.asObservable();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/angular/src/app/tests/mocks/mock-rest.service.ts:
--------------------------------------------------------------------------------
1 | import {Observable} from "rxjs/Observable";
2 |
3 | import {WebReaction} from "../../services/utils/rest.service";
4 |
5 | export class MockRestService {
6 | public sendRequest(url: string): Observable {
7 | return null;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/angular/src/app/tests/mocks/mock-storage.service.ts:
--------------------------------------------------------------------------------
1 | export class MockStorageService {
2 | // noinspection JSUnusedLocalSymbols
3 | public get(key: string): any {}
4 |
5 | // noinspection JSUnusedLocalSymbols
6 | set(key: string, value: any): void {}
7 |
8 | // noinspection JSUnusedLocalSymbols
9 | remove(key: string): void {}
10 | }
11 |
--------------------------------------------------------------------------------
/src/angular/src/app/tests/mocks/mock-stream-service.registry.ts:
--------------------------------------------------------------------------------
1 | import {TestBed} from "@angular/core/testing";
2 |
3 | import {ConnectedService} from "../../services/utils/connected.service";
4 | import {MockModelFileService} from "./mock-model-file.service";
5 |
6 |
7 | export class MockStreamServiceRegistry {
8 | // Real connected service
9 | connectedService = TestBed.get(ConnectedService);
10 |
11 | // Fake model file service
12 | modelFileService = new MockModelFileService();
13 |
14 | connect() {
15 | this.connectedService.notifyConnected();
16 | }
17 |
18 | disconnect() {
19 | this.connectedService.notifyDisconnected();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/angular/src/app/tests/mocks/mock-view-file-options.service.ts:
--------------------------------------------------------------------------------
1 | import {Subject} from "rxjs/Subject";
2 | import {Observable} from "rxjs/Observable";
3 |
4 | import {ViewFileOptions} from "../../services/files/view-file-options";
5 |
6 |
7 | export class MockViewFileOptionsService {
8 |
9 | _options = new Subject();
10 |
11 | get options(): Observable {
12 | return this._options.asObservable();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/angular/src/app/tests/mocks/mock-view-file.service.ts:
--------------------------------------------------------------------------------
1 | import {Subject} from "rxjs/Subject";
2 | import {Observable} from "rxjs/Observable";
3 |
4 | import * as Immutable from "immutable";
5 |
6 | import {ViewFile} from "../../services/files/view-file";
7 | import {ViewFileComparator, ViewFileFilterCriteria} from "../../services/files/view-file.service";
8 |
9 |
10 | export class MockViewFileService {
11 |
12 | _files = new Subject>();
13 | _filteredFiles = new Subject>();
14 |
15 | get files(): Observable> {
16 | return this._files.asObservable();
17 | }
18 |
19 | get filteredFiles(): Observable> {
20 | return this._filteredFiles.asObservable();
21 | }
22 |
23 | // noinspection JSUnusedLocalSymbols
24 | public setFilterCriteria(criteria: ViewFileFilterCriteria) {}
25 |
26 | // noinspection JSUnusedLocalSymbols
27 | public setComparator(comparator: ViewFileComparator) {}
28 | }
29 |
--------------------------------------------------------------------------------
/src/angular/src/app/tests/unittests/services/base/base-web.service.spec.ts:
--------------------------------------------------------------------------------
1 | import {TestBed} from "@angular/core/testing";
2 |
3 | import {BaseWebService} from "../../../../services/base/base-web.service";
4 | import {StreamServiceRegistry} from "../../../../services/base/stream-service.registry";
5 | import {MockStreamServiceRegistry} from "../../../mocks/mock-stream-service.registry";
6 | import {LoggerService} from "../../../../services/utils/logger.service";
7 | import {ConnectedService} from "../../../../services/utils/connected.service";
8 |
9 |
10 | // noinspection JSUnusedLocalSymbols
11 | const DoNothing = {next: reaction => {}};
12 |
13 |
14 | class TestBaseWebService extends BaseWebService {
15 | public onConnected(): void {}
16 |
17 | public onDisconnected(): void {}
18 | }
19 |
20 | describe("Testing base web service", () => {
21 | let baseWebService: TestBaseWebService;
22 |
23 | let mockRegistry: MockStreamServiceRegistry;
24 |
25 | beforeEach(() => {
26 | TestBed.configureTestingModule({
27 | providers: [
28 | TestBaseWebService,
29 | LoggerService,
30 | ConnectedService,
31 | {provide: StreamServiceRegistry, useClass: MockStreamServiceRegistry}
32 | ]
33 | });
34 |
35 | mockRegistry = TestBed.get(StreamServiceRegistry);
36 |
37 | baseWebService = TestBed.get(TestBaseWebService);
38 | spyOn(baseWebService, "onConnected");
39 | spyOn(baseWebService, "onDisconnected");
40 |
41 | // Initialize base web service
42 | baseWebService.onInit();
43 | });
44 |
45 | it("should create an instance", () => {
46 | expect(baseWebService).toBeDefined();
47 | });
48 |
49 | it("should forward the connected notification", () => {
50 | mockRegistry.connectedService.notifyConnected();
51 | expect(baseWebService.onConnected).toHaveBeenCalledTimes(1);
52 | });
53 |
54 | it("should forward the disconnected notification", () => {
55 | mockRegistry.connectedService.notifyDisconnected();
56 | expect(baseWebService.onDisconnected).toHaveBeenCalledTimes(1);
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/src/angular/src/app/tests/unittests/services/utils/dom.service.spec.ts:
--------------------------------------------------------------------------------
1 | import {fakeAsync, TestBed, tick} from "@angular/core/testing";
2 |
3 | import {DomService} from "../../../../services/utils/dom.service";
4 |
5 |
6 |
7 | describe("Testing view file options service", () => {
8 | let domService: DomService;
9 |
10 | beforeEach(() => {
11 | TestBed.configureTestingModule({
12 | providers: [
13 | DomService,
14 | ]
15 | });
16 |
17 | domService = TestBed.get(DomService);
18 | });
19 |
20 | it("should create an instance", () => {
21 | expect(domService).toBeDefined();
22 | });
23 |
24 | it("should forward updates to headerHeight", fakeAsync(() => {
25 | let count = 0;
26 | let headerHeight = null;
27 | domService.headerHeight.subscribe({
28 | next: height => {
29 | headerHeight = height;
30 | count++;
31 | }
32 | });
33 | tick();
34 | expect(count).toBe(1);
35 |
36 | domService.setHeaderHeight(10);
37 | tick();
38 | expect(headerHeight).toBe(10);
39 | expect(count).toBe(2);
40 |
41 | domService.setHeaderHeight(20);
42 | tick();
43 | expect(headerHeight).toBe(20);
44 | expect(count).toBe(3);
45 |
46 | // Setting same value shouldn't trigger an update
47 | domService.setHeaderHeight(20);
48 | tick();
49 | expect(headerHeight).toBe(20);
50 | expect(count).toBe(3);
51 | }));
52 | });
53 |
--------------------------------------------------------------------------------
/src/angular/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/angular/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/angular/src/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/angular/src/assets/favicon.png
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/about.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
48 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/default-remote.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
45 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/delete-local.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/delete-remote.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
44 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/deleted.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/directory-archive-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
47 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/directory-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
43 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/directory.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
44 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/downloaded.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
44 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/downloading.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
44 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/extract.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
45 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/extracting.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
43 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/file-archive-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
49 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/file-archive.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
52 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/file-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
44 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/file.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
43 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/hamburger.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
54 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/queue.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
51 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
51 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/sort-asc.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
54 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/sort-desc.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
54 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/stop.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
51 |
--------------------------------------------------------------------------------
/src/angular/src/assets/icons/stopped.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
48 |
--------------------------------------------------------------------------------
/src/angular/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/angular/src/assets/logo.png
--------------------------------------------------------------------------------
/src/angular/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | import {LoggerService} from "../app/services/utils/logger.service"
2 |
3 | export const environment = {
4 | production: true,
5 | logger: {
6 | level: LoggerService.Level.WARN
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/src/angular/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `.angular-cli.json`.
5 |
6 | import {LoggerService} from "../app/services/utils/logger.service"
7 |
8 | export const environment = {
9 | production: false,
10 | logger: {
11 | level: LoggerService.Level.DEBUG
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/angular/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SeedSync
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/angular/src/main.ts:
--------------------------------------------------------------------------------
1 | import {enableProdMode} from '@angular/core';
2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
3 |
4 | import {AppModule} from './app/app.module';
5 | import {environment} from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule);
12 |
--------------------------------------------------------------------------------
/src/angular/src/styles.scss:
--------------------------------------------------------------------------------
1 | @import 'app/common/common';
2 |
3 | html {
4 | -ms-text-size-adjust: 100%;
5 | -webkit-text-size-adjust: 100%;
6 | -moz-text-size-adjust: 100%;
7 | }
8 |
9 | html, body {
10 | font-family: Verdana,sans-serif;
11 | font-size: 15px;
12 | line-height: 1.5;
13 | margin: 0;
14 | }
15 |
16 | /* show the input search cancel button */
17 | input[type=search]::-webkit-search-cancel-button {
18 | -webkit-appearance: searchfield-cancel-button;
19 | }
20 |
21 | div {
22 | /*border: 1px solid black;*/
23 | /* make padding and border inside box */
24 | box-sizing: border-box;
25 | }
26 |
27 | // Bootstrap modifications
28 | .modal-body {
29 | /* break up long text */
30 | overflow-wrap: normal;
31 | hyphens: auto;
32 | word-break: break-word;
33 | }
34 |
--------------------------------------------------------------------------------
/src/angular/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/long-stack-trace-zone';
4 | import 'zone.js/dist/proxy.js';
5 | import 'zone.js/dist/sync-test';
6 | import 'zone.js/dist/jasmine-patch';
7 | import 'zone.js/dist/async-test';
8 | import 'zone.js/dist/fake-async-test';
9 | import {getTestBed} from '@angular/core/testing';
10 | import {
11 | BrowserDynamicTestingModule,
12 | platformBrowserDynamicTesting
13 | } from '@angular/platform-browser-dynamic/testing';
14 |
15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
16 | declare const __karma__: any;
17 | declare const require: any;
18 |
19 | // Prevent Karma from running prematurely.
20 | __karma__.loaded = function () {};
21 |
22 | // First, initialize the Angular testing environment.
23 | getTestBed().initTestEnvironment(
24 | BrowserDynamicTestingModule,
25 | platformBrowserDynamicTesting()
26 | );
27 | // Then we find all the tests.
28 | const context = require.context('./', true, /\.spec\.ts$/);
29 | // And load the modules.
30 | context.keys().map(context);
31 | // Finally, start Karma to run the tests.
32 | __karma__.start();
33 |
--------------------------------------------------------------------------------
/src/angular/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "types": []
8 | },
9 | "exclude": [
10 | "test.ts",
11 | "**/*.spec.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/src/angular/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": [
9 | "jasmine",
10 | "node"
11 | ]
12 | },
13 | "files": [
14 | "test.ts"
15 | ],
16 | "include": [
17 | "**/*.spec.ts",
18 | "**/*.d.ts"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/src/angular/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /* SystemJS module definition */
2 | declare var module: NodeModule;
3 |
4 | interface NodeModule {
5 | id: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/angular/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "outDir": "./dist/out-tsc",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "target": "es5",
11 | "typeRoots": [
12 | "node_modules/@types"
13 | ],
14 | "lib": [
15 | "es2017",
16 | "dom"
17 | ]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/debian/compat:
--------------------------------------------------------------------------------
1 | 10
--------------------------------------------------------------------------------
/src/debian/config:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 |
3 | # Source debconf library.
4 | . /usr/share/debconf/confmodule
5 |
6 | # Ask for username
7 | db_input high seedsync/username || true
8 | db_go || true
--------------------------------------------------------------------------------
/src/debian/control:
--------------------------------------------------------------------------------
1 | Source: seedsync
2 | Section: utils
3 | Priority: extra
4 | Maintainer: Inderpreet Singh
5 | Build-Depends: debhelper (>= 8.0.0), dh-systemd (>= 1.5)
6 | Standards-Version: 4.0.0
7 |
8 | Package: seedsync
9 | Architecture: amd64
10 | Depends: ${shlibs:Depends}, ${misc:Depends}, lftp, openssh-client
11 | Pre-Depends: debconf (>= 0.2.17)
12 | Description: fully GUI-configurable, lftp-based file transfer and management program
13 | seedsync is a lftp-based file transfer program to keep your local server
14 | synchronized with your remote seedbox. It features a web-based GUI to
15 | fully configure lftp settings as well as view the transfer status.
16 | It additionally allows you to extract archives and delete files on both
17 | the local and remote server, all from the web-based GUI.
--------------------------------------------------------------------------------
/src/debian/postinst:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | OVERRIDE_DIR=/etc/systemd/system/seedsync.service.d
4 | OVERRIDE_FILE=override.conf
5 |
6 |
7 | #!/bin/sh -e
8 |
9 | # Source debconf library.
10 | . /usr/share/debconf/confmodule
11 |
12 | db_get seedsync/username
13 | USER=$RET
14 |
15 | if [ -z "${USER}" ]; then
16 | echo "Skipping user override"
17 | else
18 | echo "Setting user to ${USER}"
19 |
20 | if [ "$RET" = "root" ]; then
21 | rm -rf ${OVERRIDE_DIR}/${OVERRIDE_FILE}
22 | else
23 | mkdir -p ${OVERRIDE_DIR}
24 | echo [Service]\\nUser=${USER}\\nEnvironment=\"HOME=/home/${USER}\" > ${OVERRIDE_DIR}/${OVERRIDE_FILE}
25 | fi
26 | fi
27 |
28 | #DEBHELPER#
29 |
--------------------------------------------------------------------------------
/src/debian/postrm:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | if [ "$1" = "purge" ]; then
3 | rm -rf /etc/systemd/system/seedsync.service.d
4 | fi
5 |
6 | #DEBHELPER#
7 |
--------------------------------------------------------------------------------
/src/debian/rules:
--------------------------------------------------------------------------------
1 | #!/usr/bin/make -f
2 |
3 | export DESTROOT=$(CURDIR)/debian/seedsync
4 |
5 | %:
6 | dh $@ --with=systemd
7 |
8 | override_dh_auto_build:
9 | dh_auto_build
10 |
11 | override_dh_auto_install:
12 | dh_auto_install
13 | mkdir -p $(DESTROOT)/usr/lib
14 | cp -rf seedsync $(DESTROOT)/usr/lib/
15 |
16 | override_dh_shlibdeps:
17 | dh_shlibdeps -l$(CURDIR)/seedsync
18 |
--------------------------------------------------------------------------------
/src/debian/seedsync.service:
--------------------------------------------------------------------------------
1 | # Service unit for seedsync
2 | # Note: User should be overridden in
3 | # /etc/systemd/system/seedsync.service.d/override.conf
4 |
5 | [Unit]
6 | Description=Job that runs the SeedSync daemon
7 | Requires=local-fs.target network-online.target
8 | After=local-fs.target network-online.target
9 |
10 | [Service]
11 | Type=simple
12 | User=root
13 | Environment="HOME=/root"
14 | ExecStartPre=/bin/mkdir -p ${HOME}/.seedsync
15 | ExecStartPre=/bin/mkdir -p ${HOME}/.seedsync/log
16 | ExecStart=/usr/lib/seedsync/seedsync --logdir ${HOME}/.seedsync/log -c ${HOME}/.seedsync
17 |
18 | [Install]
19 | WantedBy=multi-user.target
20 |
--------------------------------------------------------------------------------
/src/debian/source/format:
--------------------------------------------------------------------------------
1 | 3.0 (native)
--------------------------------------------------------------------------------
/src/debian/templates:
--------------------------------------------------------------------------------
1 | Template: seedsync/username
2 | Type: string
3 | Default: root
4 | Description: User for SeedSync service
5 | The service will run under this user.
6 | All transferred files will be owned by this user.
--------------------------------------------------------------------------------
/src/docker/build/deb/Dockerfile.dockerignore:
--------------------------------------------------------------------------------
1 | **/*.pyc
2 | **/__pycache__
3 | **/node_modules
4 | **/.venv
5 | .git
6 | .idea
7 | build
8 | src/angular/dist
9 | src/python/build
10 |
--------------------------------------------------------------------------------
/src/docker/build/docker-image/Dockerfile.dockerignore:
--------------------------------------------------------------------------------
1 | **/*.pyc
2 | **/__pycache__
3 | **/node_modules
4 | **/.venv
5 | .git
6 | .idea
7 | build
8 | src/angular/dist
9 | src/python/tests
10 | src/python/build
11 |
--------------------------------------------------------------------------------
/src/docker/build/docker-image/run_as_user:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # run a command as (non-existent) user, using libnss-wrapper
3 |
4 | U=`id -u`
5 | G=`id -g`
6 |
7 | HOME_DIR=/home/seedsync
8 | PASSWD=/var/tmp/passwd
9 | GROUP=/var/tmp/group
10 |
11 | if [ ! -d "$HOME_DIR" ]; then
12 | mkdir "$HOME_DIR"
13 | fi
14 | if [ ! -f "$PASSWD" ]; then
15 | echo "user::$U:$G::$HOME_DIR:" > "$PASSWD"
16 | fi
17 | if [ ! -f "$GROUP" ]; then
18 | echo "user::$G:" > "$GROUP"
19 | fi
20 |
21 | LD_PRELOAD=libnss_wrapper.so NSS_WRAPPER_PASSWD="$PASSWD" NSS_WRAPPER_GROUP="$GROUP" "$@"
22 |
--------------------------------------------------------------------------------
/src/docker/build/docker-image/scp:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | SCP=/usr/bin/scp
3 | /usr/local/bin/run_as_user "$SCP" "$@"
4 |
--------------------------------------------------------------------------------
/src/docker/build/docker-image/setup_default_config.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # exit on first error
4 | set -e
5 |
6 | CONFIG_DIR="/config"
7 | SETTINGS_FILE="${CONFIG_DIR}/settings.cfg"
8 | SCRIPT_PATH="/app/python/seedsync.py"
9 |
10 | replace_setting() {
11 | NAME=$1
12 | OLD_VALUE=$2
13 | NEW_VALUE=$3
14 |
15 | echo "Replacing ${NAME} from ${OLD_VALUE} to ${NEW_VALUE}"
16 | sed -i "s/${NAME} = ${OLD_VALUE}/${NAME} = ${NEW_VALUE}/" ${SETTINGS_FILE} && \
17 | grep -q "${NAME} = ${NEW_VALUE}" ${SETTINGS_FILE}
18 | }
19 |
20 | # Generate default config
21 | python ${SCRIPT_PATH} \
22 | -c ${CONFIG_DIR} \
23 | --html / \
24 | --scanfs / \
25 | --exit > /dev/null 2>&1 > /dev/null || true
26 |
27 | cat ${SETTINGS_FILE}
28 |
29 |
30 | # Replace default values
31 | replace_setting 'local_path' '' '\/downloads\/'
32 |
33 | echo
34 | echo
35 | echo "Done configuring seedsync"
36 | cat ${SETTINGS_FILE}
37 |
--------------------------------------------------------------------------------
/src/docker/build/docker-image/ssh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | SSH=/usr/bin/ssh
3 | /usr/local/bin/run_as_user "$SSH" "$@"
4 |
--------------------------------------------------------------------------------
/src/docker/stage/deb/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG BASE_IMAGE=ubuntu:16.04
2 |
3 | FROM $BASE_IMAGE
4 |
5 | # Install dependencies
6 | RUN apt-get update && apt-get install -y \
7 | sudo \
8 | libssl-dev \
9 | libexpat1 \
10 | expect \
11 | lftp \
12 | openssh-client
13 |
14 | # Create non-root user
15 | RUN useradd --create-home -s /bin/bash user && \
16 | echo "user:user" | chpasswd && adduser user sudo
17 |
18 | # Create directory for downloaded files
19 | RUN mkdir /downloads && \
20 | chown user:user /downloads
21 |
22 | USER user
23 |
24 | # Add ssh keys for user, as user
25 | ADD --chown=user:user src/docker/stage/deb/id_rsa.pub /home/user/.ssh/
26 | ADD --chown=user:user src/docker/stage/deb/id_rsa /home/user/.ssh/
27 | RUN chmod 600 /home/user/.ssh/id_rsa
28 |
29 | USER root
30 |
31 |
32 | # Let user run sudo without password
33 | RUN echo "user ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers
34 |
35 | WORKDIR /scripts
36 |
37 | ADD src/docker/stage/deb/install_seedsync.sh /scripts/
38 | ADD src/docker/stage/deb/expect_seedsync.exp /scripts/
39 | ADD src/docker/stage/deb/entrypoint.sh /scripts/
40 |
41 | ENTRYPOINT ["/scripts/entrypoint.sh"]
42 | CMD ["/lib/systemd/systemd --log-target=journal 3>&1"]
43 |
44 | EXPOSE 8800
45 |
--------------------------------------------------------------------------------
/src/docker/stage/deb/compose-ubu1604.yml:
--------------------------------------------------------------------------------
1 | version: "3.4"
2 | services:
3 | myapp:
4 | image: seedsync/stage/deb/ubu1604
5 | container_name: seedsync_stage_deb_ubu1604
6 | build:
7 | args:
8 | - BASE_IMAGE=ubuntu-systemd:16.04
9 |
--------------------------------------------------------------------------------
/src/docker/stage/deb/compose-ubu1804.yml:
--------------------------------------------------------------------------------
1 | version: "3.4"
2 | services:
3 | myapp:
4 | image: seedsync/stage/deb/ubu1804
5 | container_name: seedsync_stage_deb_ubu1804
6 | build:
7 | args:
8 | - BASE_IMAGE=ubuntu-systemd:18.04
9 |
--------------------------------------------------------------------------------
/src/docker/stage/deb/compose-ubu2004.yml:
--------------------------------------------------------------------------------
1 | version: "3.4"
2 | services:
3 | myapp:
4 | image: seedsync/stage/deb/ubu2004
5 | container_name: seedsync_stage_deb_ubu2004
6 | build:
7 | args:
8 | - BASE_IMAGE=ubuntu-systemd:20.04
9 |
--------------------------------------------------------------------------------
/src/docker/stage/deb/compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.4"
2 | services:
3 |
4 | myapp:
5 | image: seedsync/stage/deb
6 | container_name: seedsync_stage_deb
7 | build:
8 | context: ../../../..
9 | dockerfile: src/docker/stage/deb/Dockerfile
10 | tty: true
11 | tmpfs:
12 | - /run
13 | - /run/lock
14 | security_opt:
15 | - seccomp:unconfined
16 | volumes:
17 | - type: bind
18 | source: ${SEEDSYNC_DEB}
19 | target: /install/seedsync.deb
20 | read_only: true
21 |
22 | - type: bind
23 | source: /sys/fs/cgroup
24 | target: /sys/fs/cgroup
25 | read_only: true
26 |
--------------------------------------------------------------------------------
/src/docker/stage/deb/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # exit on first error
4 | set -e
5 |
6 | echo "Running entrypoint"
7 |
8 | echo "Installing SeedSync"
9 | ./expect_seedsync.exp
10 |
11 | echo "Continuing docker CMD"
12 | echo "$@"
13 | exec $@
14 |
--------------------------------------------------------------------------------
/src/docker/stage/deb/expect_seedsync.exp:
--------------------------------------------------------------------------------
1 | #!/usr/bin/expect
2 | set timeout 10
3 | spawn "./install_seedsync.sh"
4 |
5 | expect {
6 | timeout {error "ERROR: missing user prompt"; exit 1}
7 | "User for SeedSync service"
8 | }
9 | send "user\n"
10 |
11 | expect {
12 | timeout {error "ERROR: timeout"; exit 1}
13 | eof
14 | }
15 |
--------------------------------------------------------------------------------
/src/docker/stage/deb/id_rsa:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEowIBAAKCAQEA4Pn1J1LJKi9/xE1XaZZuSzdfwPQykqbnevCx1iSq8cWA8rdu
3 | ZcR9sY7dyMc6xQ8YQ08nZnRyvMfxdmvIDiymy7/ugxS7CwCoX9/CMLw11y9RyT6t
4 | hrtvdqSFoQDPydWlgmYyq28FYRFWpKgmWgo9xgE1LywVxOGvI7nxr2UPq/bM2T8C
5 | y4DIEcVe6jU1NR5okVg4UJeBl6ToAOQ5qjOSPgHtLQi1vqHtTi5BDkC1AkBv2FI1
6 | H007Z1O4kgQUhBd3WoeDFTbvQ+3FBfjRZRLgFXR7/QrCa6RenI9qnnWnp+oRzaqR
7 | NRk7VG1tC9/En3Kh91t3pVj3wXU9lygXhTs66wIDAQABAoIBABcoY53Knb5j6Ujx
8 | lR/fRjcj2g1olZQW7hjvkb6zQ41jgSR60ThUg4O1awrxxxDlvt+e1DVtoynfgvFn
9 | os4itoCenxSLG73EMZC83aZamUgvLMIEW6RUwuJ5iO/Lv5fNEB5eGrUe1nTpbfvA
10 | +0GlcDpjgW/7n7oGaRrKVyBwzK4spfYMLIuP8j6sLlUOyXi6gLjsOHA+W6GeDp7L
11 | GbK2cq80L2xA/wduaxh7QLacPlnpabJaIGCF+Wq7O6DddBg02bi7AZCtFRBs6eLI
12 | XCB4SAA7eT/qwoukX/apSXYwWGy5xKQPbZr8V4BkhCXvJDzE62YAV8cLezdyPdTL
13 | DOE3NxECgYEA+EexLMo0b+tNsBJJCk55qXZv1MSreda1JqT9aTLdaIhNVlw1ErBq
14 | ngfsytvgmUYEN/g//NStUKTVtRkvzR6sluNVTLdSJskbKPtWBP0ipaM1AvlW4l7R
15 | PdAzvMFQswMjHYElxassEmj7iM+NFvpdZdW0z2rlw04cOvhs2k/fVF0CgYEA5/jE
16 | w0TAeUsvd4sl+xq1sTzfr1mSeg4irE2kw/NFQsTaDvGZJc8+HtFv8O1ZUSkn9/qk
17 | SypHf97G78+dN59d5rR+P/Eb5OJFWo3aKytu4drt+qtbvc4sxvkW/ORwEfmfXDVK
18 | q7jolmk0i9/oVyA4N4ruAjB1sxZRmYohQcCM1+cCgYAAllLR80x6c0kEwJZRouvg
19 | vbn3+9sX960IAV3kEM27QI9GRAOQHsCxzPz/YdO/KQ47f6fPFkWuqiUjP4MAbjEk
20 | TjdWbhyQoOsihq2mZ17cm201q5dMA8Nk7QgiSybAtaIwoKyRMh1xkbP+l9cSldcA
21 | taeu0ebnNlkUvp+rSIMTtQKBgFal9dl6tOqZywE8WNOTBotN0cAOFUjCPvFdj04i
22 | cJygK1OpqysUXn/ke4vjHJnUZbmbRgNNp6d775NkWbWNMeYbRY1c4q58VquckQHP
23 | F3wF6x7XI02i1db89DlCmxobxAsNXPcH+tk0MwyMdp0Uy+rzWjQ3Jb/fdluD3ShS
24 | ZEnBAoGBAMmwVdUc2r+93TpJUYtD+NKZ8uAF7BiPdEQzCS8VDk73xGT5hnE5GLcW
25 | vtBqwnLOCaRYEi0GvWk5LizfxClrtzhRsiD2n9dJYWVHxc7YDuD9Soe8w3/VELLf
26 | ABAiTRuLbr5dweLwjY8KDsVmZ8yWOz8ejrAlGtT8Gnqf/2Uo6UNN
27 | -----END RSA PRIVATE KEY-----
--------------------------------------------------------------------------------
/src/docker/stage/deb/id_rsa.pub:
--------------------------------------------------------------------------------
1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDg+fUnUskqL3/ETVdplm5LN1/A9DKSpud68LHWJKrxxYDyt25lxH2xjt3IxzrFDxhDTydmdHK8x/F2a8gOLKbLv+6DFLsLAKhf38IwvDXXL1HJPq2Gu292pIWhAM/J1aWCZjKrbwVhEVakqCZaCj3GATUvLBXE4a8jufGvZQ+r9szZPwLLgMgRxV7qNTU1HmiRWDhQl4GXpOgA5DmqM5I+Ae0tCLW+oe1OLkEOQLUCQG/YUjUfTTtnU7iSBBSEF3dah4MVNu9D7cUF+NFlEuAVdHv9CsJrpF6cj2qedaen6hHNqpE1GTtUbW0L38SfcqH3W3elWPfBdT2XKBeFOzrr user@572e13b2bdf3
--------------------------------------------------------------------------------
/src/docker/stage/deb/install_seedsync.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | dpkg -i /install/seedsync.deb
3 |
--------------------------------------------------------------------------------
/src/docker/stage/deb/ubuntu-systemd/ubuntu-16.04-systemd/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copied from:
2 | # https://github.com/solita/docker-systemd/blob/master/Dockerfile
3 | # https://hub.docker.com/r/solita/ubuntu-systemd/
4 |
5 | FROM ubuntu:16.04
6 |
7 | ENV container docker
8 |
9 | # Don't start any optional services except for the few we need.
10 | RUN find /etc/systemd/system \
11 | /lib/systemd/system \
12 | -path '*.wants/*' \
13 | -not -name '*journald*' \
14 | -not -name '*systemd-tmpfiles*' \
15 | -not -name '*systemd-user-sessions*' \
16 | -exec rm \{} \;
17 |
18 | RUN systemctl set-default multi-user.target
19 |
20 | COPY setup /sbin/
21 |
22 | STOPSIGNAL SIGRTMIN+3
23 |
24 | # Workaround for docker/docker#27202, technique based on comments from docker/docker#9212
25 | CMD ["/bin/bash", "-c", "exec /sbin/init --log-target=journal 3>&1"]
26 |
--------------------------------------------------------------------------------
/src/docker/stage/deb/ubuntu-systemd/ubuntu-16.04-systemd/setup:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | if nsenter --mount=/host/proc/1/ns/mnt -- mount | grep /sys/fs/cgroup/systemd >/dev/null 2>&1; then
5 | echo 'The systemd cgroup hierarchy is already mounted at /sys/fs/cgroup/systemd.'
6 | else
7 | if [ -d /host/sys/fs/cgroup/systemd ]; then
8 | echo 'The mount point for the systemd cgroup hierarchy already exists at /sys/fs/cgroup/systemd.'
9 | else
10 | echo 'Creating the mount point for the systemd cgroup hierarchy at /sys/fs/cgroup/systemd.'
11 | mkdir -p /host/sys/fs/cgroup/systemd
12 | fi
13 |
14 | echo 'Mounting the systemd cgroup hierarchy.'
15 | nsenter --mount=/host/proc/1/ns/mnt -- mount -t cgroup cgroup -o none,name=systemd /sys/fs/cgroup/systemd
16 | fi
17 | echo 'Your Docker host is now configured for running systemd containers!'
18 |
--------------------------------------------------------------------------------
/src/docker/stage/deb/ubuntu-systemd/ubuntu-18.04-systemd/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copied from:
2 | # https://github.com/solita/docker-systemd/blob/master/Dockerfile
3 | # https://hub.docker.com/r/solita/ubuntu-systemd/
4 | FROM ubuntu:18.04
5 |
6 | RUN apt-get update && apt-get install -y systemd
7 |
8 | ENV container docker
9 |
10 | # Don't start any optional services except for the few we need.
11 | RUN find /etc/systemd/system \
12 | /lib/systemd/system \
13 | -path '*.wants/*' \
14 | -not -name '*journald*' \
15 | -not -name '*systemd-tmpfiles*' \
16 | -not -name '*systemd-user-sessions*' \
17 | -exec rm \{} \;
18 |
19 | RUN systemctl set-default multi-user.target
20 |
21 | COPY setup /sbin/
22 |
23 | STOPSIGNAL SIGRTMIN+3
24 |
25 | # Workaround for docker/docker#27202, technique based on comments from docker/docker#9212
26 | CMD ["/bin/bash", "-c", "exec /sbin/init --log-target=journal 3>&1"]
27 |
--------------------------------------------------------------------------------
/src/docker/stage/deb/ubuntu-systemd/ubuntu-18.04-systemd/setup:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | if nsenter --mount=/host/proc/1/ns/mnt -- mount | grep /sys/fs/cgroup/systemd >/dev/null 2>&1; then
5 | echo 'The systemd cgroup hierarchy is already mounted at /sys/fs/cgroup/systemd.'
6 | else
7 | if [ -d /host/sys/fs/cgroup/systemd ]; then
8 | echo 'The mount point for the systemd cgroup hierarchy already exists at /sys/fs/cgroup/systemd.'
9 | else
10 | echo 'Creating the mount point for the systemd cgroup hierarchy at /sys/fs/cgroup/systemd.'
11 | mkdir -p /host/sys/fs/cgroup/systemd
12 | fi
13 |
14 | echo 'Mounting the systemd cgroup hierarchy.'
15 | nsenter --mount=/host/proc/1/ns/mnt -- mount -t cgroup cgroup -o none,name=systemd /sys/fs/cgroup/systemd
16 | fi
17 | echo 'Your Docker host is now configured for running systemd containers!'
18 |
--------------------------------------------------------------------------------
/src/docker/stage/deb/ubuntu-systemd/ubuntu-20.04-systemd/Dockerfile:
--------------------------------------------------------------------------------
1 | # Copied from:
2 | # https://github.com/solita/docker-systemd/blob/master/Dockerfile
3 | # https://hub.docker.com/r/solita/ubuntu-systemd/
4 | FROM ubuntu:20.04
5 |
6 | RUN apt-get update && apt-get install -y systemd
7 |
8 | ENV container docker
9 |
10 | # Don't start any optional services except for the few we need.
11 | RUN find /etc/systemd/system \
12 | /lib/systemd/system \
13 | -path '*.wants/*' \
14 | -not -name '*journald*' \
15 | -not -name '*systemd-tmpfiles*' \
16 | -not -name '*systemd-user-sessions*' \
17 | -exec rm \{} \;
18 |
19 | RUN systemctl set-default multi-user.target
20 |
21 | COPY setup /sbin/
22 |
23 | STOPSIGNAL SIGRTMIN+3
24 |
25 | # Workaround for docker/docker#27202, technique based on comments from docker/docker#9212
26 | CMD ["/bin/bash", "-c", "exec /lib/systemd/systemd --log-target=journal 3>&1"]
27 |
--------------------------------------------------------------------------------
/src/docker/stage/deb/ubuntu-systemd/ubuntu-20.04-systemd/setup:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -eu
3 |
4 | if nsenter --mount=/host/proc/1/ns/mnt -- mount | grep /sys/fs/cgroup/systemd >/dev/null 2>&1; then
5 | echo 'The systemd cgroup hierarchy is already mounted at /sys/fs/cgroup/systemd.'
6 | else
7 | if [ -d /host/sys/fs/cgroup/systemd ]; then
8 | echo 'The mount point for the systemd cgroup hierarchy already exists at /sys/fs/cgroup/systemd.'
9 | else
10 | echo 'Creating the mount point for the systemd cgroup hierarchy at /sys/fs/cgroup/systemd.'
11 | mkdir -p /host/sys/fs/cgroup/systemd
12 | fi
13 |
14 | echo 'Mounting the systemd cgroup hierarchy.'
15 | nsenter --mount=/host/proc/1/ns/mnt -- mount -t cgroup cgroup -o none,name=systemd /sys/fs/cgroup/systemd
16 | fi
17 | echo 'Your Docker host is now configured for running systemd containers!'
18 |
--------------------------------------------------------------------------------
/src/docker/stage/docker-image/compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.4"
2 | services:
3 | myapp:
4 | image: ${STAGING_REGISTRY}/seedsync:${STAGING_VERSION}
5 | container_name: seedsync_test_e2e_myapp
6 |
--------------------------------------------------------------------------------
/src/docker/test/angular/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM seedsync/build/angular/env as seedsync_test_angular
2 |
3 | RUN apt-get update
4 | RUN wget -nv -O /tmp/chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \
5 | dpkg -i /tmp/chrome.deb; apt-get -fy install > /dev/null
6 |
7 | COPY \
8 | src/angular/tsconfig.json \
9 | src/angular/tslint.json \
10 | src/angular/karma.conf.js \
11 | src/angular/.angular-cli.json \
12 | /app/
13 |
14 | RUN ls -l /app/
15 |
16 | # ng src needs to be mounted on /app/src
17 | WORKDIR /app/src
18 |
19 | CMD ["/app/node_modules/@angular/cli/bin/ng", "test", \
20 | "--browsers", "ChromeHeadless", \
21 | "--single-run"]
22 |
--------------------------------------------------------------------------------
/src/docker/test/angular/compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.4"
2 | services:
3 | tests:
4 | image: seedsync/test/angular
5 | container_name: seedsync_test_angular
6 | tty: true
7 | build:
8 | context: ../../../../
9 | dockerfile: src/docker/test/angular/Dockerfile
10 | target: seedsync_test_angular
11 | volumes:
12 | - type: bind
13 | source: ../../../angular/src
14 | target: /app/src
15 | read_only: true
16 |
--------------------------------------------------------------------------------
/src/docker/test/e2e/Dockerfile:
--------------------------------------------------------------------------------
1 | # Creates environment for e2e tests
2 | FROM node:12.16 as seedsync_test_e2e_env
3 |
4 | COPY src/e2e/package*.json /app/
5 | WORKDIR /app
6 | RUN npm install
7 |
8 |
9 | # Builds and runs e2e tests
10 | FROM seedsync_test_e2e_env as seedsync_test_e2e
11 |
12 | COPY \
13 | src/e2e/conf.ts \
14 | src/e2e/tsconfig.json \
15 | /app/
16 | COPY src/e2e/tests /app/tests
17 | COPY \
18 | src/docker/test/e2e/urls.ts \
19 | src/docker/test/e2e/run_tests.sh \
20 | src/docker/test/e2e/parse_seedsync_status.py \
21 | /app/
22 |
23 | WORKDIR /app
24 |
25 | RUN node_modules/typescript/bin/tsc --outDir ./tmp
26 |
27 | CMD ["/app/run_tests.sh"]
28 |
--------------------------------------------------------------------------------
/src/docker/test/e2e/chrome/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM yukinying/chrome-headless-browser-selenium:latest
2 |
3 | USER root
4 | RUN apt-get update && apt-get install -y libxi6 libgconf-2-4
5 | USER headless
6 |
--------------------------------------------------------------------------------
/src/docker/test/e2e/compose-dev.yml:
--------------------------------------------------------------------------------
1 | version: "3.4"
2 | services:
3 | tests:
4 | command: /bin/true
5 |
6 | myapp:
7 | ports:
8 | - target: 8800
9 | published: 8800
10 | protocol: tcp
11 | mode: host
12 |
13 | chrome:
14 | ports:
15 | - target: 4444
16 | published: 4444
17 | protocol: tcp
18 | mode: host
19 |
--------------------------------------------------------------------------------
/src/docker/test/e2e/compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.4"
2 | services:
3 | tests:
4 | image: seedsync/test/e2e
5 | container_name: seedsync_test_e2e
6 | build:
7 | context: ../../../../
8 | dockerfile: src/docker/test/e2e/Dockerfile
9 | target: seedsync_test_e2e
10 | depends_on:
11 | - chrome
12 | - remote
13 |
14 | chrome:
15 | image: seedsync/test/e2e/chrome
16 | container_name: seedsync_test_e2e_chrome
17 | build:
18 | context: ../../../../
19 | dockerfile: src/docker/test/e2e/chrome/Dockerfile
20 | shm_size: 1024M
21 | cap_add:
22 | - SYS_ADMIN
23 |
24 | remote:
25 | image: seedsync/test/e2e/remote
26 | container_name: seedsync_test_e2e_remote
27 | build:
28 | context: ../../../../
29 | dockerfile: src/docker/test/e2e/remote/Dockerfile
30 |
31 | configure:
32 | image: seedsync/test/e2e/configure
33 | container_name: seedsync_test_e2e_configure
34 | build:
35 | context: ../../../../
36 | dockerfile: src/docker/test/e2e/configure/Dockerfile
37 | depends_on:
38 | - myapp
39 |
--------------------------------------------------------------------------------
/src/docker/test/e2e/configure/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.11.6
2 |
3 | RUN apk add --no-cache curl bash
4 |
5 | WORKDIR /
6 | ADD src/docker/wait-for-it.sh /
7 | ADD src/docker/test/e2e/configure/setup_seedsync.sh /
8 | CMD ["/setup_seedsync.sh"]
9 |
--------------------------------------------------------------------------------
/src/docker/test/e2e/configure/setup_seedsync.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ./wait-for-it.sh myapp:8800 -- echo "Seedsync app is up (before configuring)"
4 | curl -sS "http://myapp:8800/server/config/set/general/debug/true"; echo
5 | curl -sS "http://myapp:8800/server/config/set/general/verbose/true"; echo
6 | curl -sS "http://myapp:8800/server/config/set/lftp/local_path/%252Fdownloads"; echo
7 | curl -sS "http://myapp:8800/server/config/set/lftp/remote_address/remote"; echo
8 | curl -sS "http://myapp:8800/server/config/set/lftp/remote_username/remoteuser"; echo
9 | curl -sS "http://myapp:8800/server/config/set/lftp/remote_password/remotepass"; echo
10 | curl -sS "http://myapp:8800/server/config/set/lftp/remote_port/1234"; echo
11 | curl -sS "http://myapp:8800/server/config/set/lftp/remote_path/%252Fhome%252Fremoteuser%252Ffiles"; echo
12 | curl -sS "http://myapp:8800/server/config/set/autoqueue/patterns_only/true"; echo
13 |
14 | curl -sS "http://myapp:8800/server/command/restart"; echo
15 |
16 | ./wait-for-it.sh myapp:8800 -- echo "Seedsync app is up (after configuring)"
17 |
18 | echo
19 | echo "Done configuring SeedSync app"
20 |
--------------------------------------------------------------------------------
/src/docker/test/e2e/parse_seedsync_status.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import json
3 | try:
4 | print(json.load(sys.stdin)['server']['up'])
5 | except:
6 | print('False')
7 |
--------------------------------------------------------------------------------
/src/docker/test/e2e/remote/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:18.04
2 |
3 | # Install dependencies
4 | RUN apt-get update && apt-get install -y \
5 | python3.7
6 |
7 | # Switch to Python 3.7
8 | RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.7 1
9 | RUN update-alternatives --set python /usr/bin/python3.7
10 |
11 | # Create non-root user
12 | RUN useradd --create-home -s /bin/bash remoteuser && \
13 | echo "remoteuser:remotepass" | chpasswd
14 |
15 |
16 | # Add install image's user's key to authorized
17 | USER remoteuser
18 | ADD --chown=remoteuser:remoteuser src/docker/test/e2e/remote/id_rsa.pub /home/remoteuser/user_id_rsa.pub
19 | RUN mkdir -p /home/remoteuser/.ssh && \
20 | cat /home/remoteuser/user_id_rsa.pub >> /home/remoteuser/.ssh/authorized_keys
21 | USER root
22 |
23 | # Copy over data
24 | ADD --chown=remoteuser:remoteuser src/docker/test/e2e/remote/files /home/remoteuser/files
25 |
26 | # Install and run ssh server
27 | RUN apt-get update && apt-get install -y openssh-server
28 |
29 | # Change port
30 | RUN sed -i '/Port 22/c\Port 1234' /etc/ssh/sshd_config
31 | EXPOSE 1234
32 |
33 | RUN mkdir /var/run/sshd
34 |
35 | CMD ["/usr/sbin/sshd", "-D"]
36 |
--------------------------------------------------------------------------------
/src/docker/test/e2e/remote/files/clients.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/docker/test/e2e/remote/files/clients.jpg
--------------------------------------------------------------------------------
/src/docker/test/e2e/remote/files/crispycat/cat.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/docker/test/e2e/remote/files/crispycat/cat.mp4
--------------------------------------------------------------------------------
/src/docker/test/e2e/remote/files/documentation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/docker/test/e2e/remote/files/documentation.png
--------------------------------------------------------------------------------
/src/docker/test/e2e/remote/files/goose/goose.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/docker/test/e2e/remote/files/goose/goose.mp4
--------------------------------------------------------------------------------
/src/docker/test/e2e/remote/files/illusion.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/docker/test/e2e/remote/files/illusion.jpg
--------------------------------------------------------------------------------
/src/docker/test/e2e/remote/files/joke/joke.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/docker/test/e2e/remote/files/joke/joke.png
--------------------------------------------------------------------------------
/src/docker/test/e2e/remote/files/testing.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/docker/test/e2e/remote/files/testing.gif
--------------------------------------------------------------------------------
/src/docker/test/e2e/remote/files/áßç déÀ.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/docker/test/e2e/remote/files/áßç déÀ.mp4
--------------------------------------------------------------------------------
/src/docker/test/e2e/remote/files/üæÒ/µ®© ÷úƤ.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/docker/test/e2e/remote/files/üæÒ/µ®© ÷úƤ.png
--------------------------------------------------------------------------------
/src/docker/test/e2e/remote/id_rsa.pub:
--------------------------------------------------------------------------------
1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDg+fUnUskqL3/ETVdplm5LN1/A9DKSpud68LHWJKrxxYDyt25lxH2xjt3IxzrFDxhDTydmdHK8x/F2a8gOLKbLv+6DFLsLAKhf38IwvDXXL1HJPq2Gu292pIWhAM/J1aWCZjKrbwVhEVakqCZaCj3GATUvLBXE4a8jufGvZQ+r9szZPwLLgMgRxV7qNTU1HmiRWDhQl4GXpOgA5DmqM5I+Ae0tCLW+oe1OLkEOQLUCQG/YUjUfTTtnU7iSBBSEF3dah4MVNu9D7cUF+NFlEuAVdHv9CsJrpF6cj2qedaen6hHNqpE1GTtUbW0L38SfcqH3W3elWPfBdT2XKBeFOzrr user@572e13b2bdf3
--------------------------------------------------------------------------------
/src/docker/test/e2e/run_tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | red=`tput setaf 1`
4 | green=`tput setaf 2`
5 | reset=`tput sgr0`
6 |
7 | END=$((SECONDS+10))
8 | while [ ${SECONDS} -lt ${END} ];
9 | do
10 | SERVER_UP=$(
11 | curl -s myapp:8800/server/status | \
12 | python ./parse_seedsync_status.py
13 | )
14 | if [[ "${SERVER_UP}" == 'True' ]]; then
15 | break
16 | fi
17 | echo "E2E Test is waiting for Seedsync server to come up..."
18 | sleep 1
19 | done
20 |
21 |
22 | if [[ "${SERVER_UP}" == 'True' ]]; then
23 | echo "${green}E2E Test detected that Seedsync server is UP${reset}"
24 | node_modules/protractor/bin/protractor tmp/conf.js
25 | else
26 | echo "${red}E2E Test failed to detect Seedsync server${reset}"
27 | exit 1
28 | fi
29 |
--------------------------------------------------------------------------------
/src/docker/test/e2e/urls.ts:
--------------------------------------------------------------------------------
1 | export class Urls {
2 | static readonly APP_BASE_URL = "http://myapp:8800/";
3 | static readonly SELENIUM_ADDRESS = "http://chrome:4444/wd/hub";
4 | }
--------------------------------------------------------------------------------
/src/docker/test/python/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM seedsync/run/python/devenv as seedsync_test_python
2 |
3 | RUN ls -l /app/python
4 |
5 | # Install dependencies
6 | RUN apt-get install -y software-properties-common && \
7 | apt-add-repository non-free && \
8 | apt-get update && \
9 | apt-get install -y \
10 | openssh-server \
11 | rar
12 |
13 | ADD src/docker/test/python/entrypoint.sh /app/
14 |
15 | # setup sshd
16 | RUN mkdir /var/run/sshd
17 | RUN ssh-keygen -t rsa -N "" -f /root/.ssh/id_rsa && \
18 | cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys
19 | # Disable the known hosts prompt
20 | RUN echo "StrictHostKeyChecking no\nUserKnownHostsFile /dev/null\nLogLevel=quiet" > /root/.ssh/config
21 |
22 | # create the seedsynctest user, add root's public key to seedsynctest
23 | RUN useradd --create-home -s /bin/bash seedsynctest && \
24 | echo "seedsynctest:seedsyncpass" | chpasswd
25 | RUN usermod -a -G root seedsynctest
26 | RUN cp /root/.ssh/id_rsa.pub /home/seedsynctest/ && \
27 | chown seedsynctest:seedsynctest /home/seedsynctest/id_rsa.pub
28 | USER seedsynctest
29 | RUN mkdir -p /home/seedsynctest/.ssh && \
30 | cat /home/seedsynctest/id_rsa.pub >> /home/seedsynctest/.ssh/authorized_keys
31 | USER root
32 |
33 | EXPOSE 22
34 |
35 | # src needs to be mounted on /src/
36 | WORKDIR /src/
37 | ENV PYTHONPATH=/src
38 |
39 | ENTRYPOINT ["/app/entrypoint.sh"]
40 | CMD ["pytest", "-v"]
41 |
--------------------------------------------------------------------------------
/src/docker/test/python/compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.4"
2 | services:
3 | tests:
4 | image: seedsync/test/python
5 | container_name: seedsync_test_python
6 | build:
7 | context: ../../../../
8 | dockerfile: src/docker/test/python/Dockerfile
9 | target: seedsync_test_python
10 | volumes:
11 | - type: bind
12 | source: ../../../python
13 | target: /src
14 | read_only: true
15 |
--------------------------------------------------------------------------------
/src/docker/test/python/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # exit on first error
4 | set -e
5 |
6 | echo "Running sshd"
7 | /usr/sbin/sshd -D &
8 |
9 | echo "Continuing entrypoint"
10 | echo "$@"
11 | exec $@
12 |
--------------------------------------------------------------------------------
/src/e2e/.gitignore:
--------------------------------------------------------------------------------
1 | *.js
2 | node_modules
3 | tmp/
4 |
--------------------------------------------------------------------------------
/src/e2e/README.md:
--------------------------------------------------------------------------------
1 | ### To run e2e tests in dev mode:
2 |
3 | 1. Install dependencies
4 |
5 | ```bash
6 | cd src/e2e
7 | npm install
8 | ```
9 |
10 | 2. Choose which dev image to run: deb install or docker image
11 |
12 | - deb install
13 |
14 | ```bash
15 | make run-tests-e2e SEEDSYNC_VERSION=latest SEEDSYNC_ARCH= DEV=1
16 | ```
17 |
18 | - docker image
19 |
20 | ```bash
21 | make run-tests-e2e SEEDSYNC_DEB=`readlink -f build/*.deb` SEEDSYNC_OS= DEV=1
22 | ```
23 |
24 |
25 |
26 | 3. Compile and run the tests
27 |
28 | ```bash
29 | cd src/e2e/
30 | rm -rf tmp && \
31 | ./node_modules/typescript/bin/tsc && \
32 | ./node_modules/protractor/bin/protractor tmp/conf.js
33 | ```
34 |
35 |
36 |
37 | ### About
38 |
39 | The dev end-to-end tests use the following docker images:
40 | 1. myapp: Installs and runs the seedsync deb package
41 | 2. chrome: Runs the selenium server
42 | 3. remote: Runs a remote SSH server
43 |
44 | The automated e2e tests additionally have:
45 | 4. tests: Runs the e2e tests
46 |
47 | Notes:
48 | 1. In dev mode, the app is visible at [http://localhost:8800](http://localhost:8800)
49 | However the url used in test is still [http://myapp:8800](http://myapp:8800) as
50 | that's how the selenium server accesses it.
51 |
52 | 2. The app requires a fully configured settings.cfg.
53 | This is done automatically in during the start of the docker image that runs the app.
54 |
55 |
--------------------------------------------------------------------------------
/src/e2e/conf.ts:
--------------------------------------------------------------------------------
1 | // Because this file imports from protractor, you'll need to have it as a
2 | // project dependency. Please see the reference config: lib/config.ts for more
3 | // information.
4 | //
5 | // Why you might want to create your config with typescript:
6 | // Editors like Microsoft Visual Studio Code will have autocomplete and
7 | // description hints.
8 | //
9 | // To run this example, first transpile it to javascript with `npm run tsc`,
10 | // then run `protractor conf.js`.
11 | import {Config} from 'protractor';
12 |
13 | import {Urls} from "./urls";
14 |
15 | let SpecReporter = require('jasmine-spec-reporter').SpecReporter;
16 |
17 | export let config: Config = {
18 | framework: 'jasmine',
19 | capabilities: {
20 | browserName: 'chrome',
21 | chromeOptions: { args: [
22 | '--headless',
23 | '--disable-gpu',
24 | '--no-sandbox',
25 | '--disable-extensions',
26 | '--disable-dev-shm-usage'
27 | ] },
28 | },
29 | specs: ['tests/**/*.spec.js'],
30 | seleniumAddress: Urls.SELENIUM_ADDRESS,
31 |
32 | // You could set no globals to true to avoid jQuery '$' and protractor '$'
33 | // collisions on the global namespace.
34 | noGlobals: true,
35 |
36 | allScriptsTimeout: 5000,
37 |
38 | // Options to be passed to Jasmine-node.
39 | jasmineNodeOpts: {
40 | showColors: true,
41 | defaultTimeoutInterval: 3000,
42 | print: function() {}
43 | },
44 |
45 | onPrepare: function () {
46 | jasmine.getEnv().addReporter(new SpecReporter({
47 | spec: {
48 | displayStacktrace: true
49 | }
50 | }));
51 | }
52 | };
53 |
--------------------------------------------------------------------------------
/src/e2e/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "e2e",
3 | "version": "1.0.0",
4 | "description": "end to end tests",
5 | "author": "",
6 | "dependencies": {
7 | "@types/jasminewd2": "^2.0.6",
8 | "@types/node": "^14.0.1",
9 | "jasmine": "^3.3.1",
10 | "jasmine-spec-reporter": "^5.0.2",
11 | "protractor": "^7.0.0",
12 | "typescript": "^3.9.2"
13 | },
14 | "devDependencies": {
15 | "@types/jasmine": "^3.5.10",
16 | "@types/jasminewd2": "^2.0.6",
17 | "ts-node": "^8.10.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/e2e/tests/about.page.spec.ts:
--------------------------------------------------------------------------------
1 | import {AboutPage} from "./about.page";
2 |
3 | describe('Testing about page', () => {
4 | let page: AboutPage;
5 |
6 | beforeEach(() => {
7 | page = new AboutPage();
8 | page.navigateTo();
9 | });
10 |
11 | it('should have right top title', () => {
12 | expect(page.getTopTitle()).toEqual("About");
13 | });
14 |
15 | it('should have the right version', () => {
16 | expect(page.getVersion()).toEqual("v0.8.6");
17 | });
18 | });
--------------------------------------------------------------------------------
/src/e2e/tests/about.page.ts:
--------------------------------------------------------------------------------
1 | import {browser, by, element} from 'protractor';
2 | import {promise} from "selenium-webdriver";
3 | import Promise = promise.Promise;
4 |
5 | import {Urls} from "../urls";
6 | import {App} from "./app";
7 |
8 | export class AboutPage extends App {
9 | navigateTo() {
10 | return browser.get(Urls.APP_BASE_URL + "about");
11 | }
12 |
13 | getVersion(): Promise {
14 | return element(by.css("#version")).getText();
15 | }
16 | }
--------------------------------------------------------------------------------
/src/e2e/tests/app.spec.ts:
--------------------------------------------------------------------------------
1 | import {App} from "./app";
2 |
3 | describe('Testing top-level app', () => {
4 | let app: App;
5 |
6 | beforeEach(() => {
7 | app = new App();
8 | app.navigateTo();
9 | });
10 |
11 | it('should have right title', () => {
12 | expect(app.getTitle()).toEqual("SeedSync");
13 | });
14 |
15 | it('should have all the sidebar items', () => {
16 | expect(app.getSidebarItems()).toEqual(
17 | [
18 | "Dashboard",
19 | "Settings",
20 | "AutoQueue",
21 | "Logs",
22 | "About",
23 | "Restart"
24 | ]
25 | );
26 | });
27 |
28 | it('should default to the dashboard page', () => {
29 | expect(app.getTopTitle()).toEqual("Dashboard");
30 | });
31 | });
--------------------------------------------------------------------------------
/src/e2e/tests/app.ts:
--------------------------------------------------------------------------------
1 | import {browser, by, element} from 'protractor';
2 | import {promise} from "selenium-webdriver";
3 | import Promise = promise.Promise;
4 |
5 | import {Urls} from "../urls";
6 |
7 | export class App {
8 | navigateTo() {
9 | return browser.get(Urls.APP_BASE_URL);
10 | }
11 |
12 | getTitle(): Promise {
13 | return browser.getTitle();
14 | }
15 |
16 | getSidebarItems(): Promise> {
17 | return element.all(by.css("#sidebar a span.text")).map(function (elm) {
18 | return browser.executeScript("return arguments[0].innerHTML;", elm);
19 | });
20 | }
21 |
22 | getTopTitle(): Promise {
23 | return browser.executeScript("return arguments[0].innerHTML;", element(by.css("#title")));
24 | }
25 | }
--------------------------------------------------------------------------------
/src/e2e/tests/autoqueue.page.ts:
--------------------------------------------------------------------------------
1 | import {browser, by, element} from 'protractor';
2 | import {promise} from "selenium-webdriver";
3 | import Promise = promise.Promise;
4 |
5 | import {Urls} from "../urls";
6 | import {App} from "./app";
7 |
8 | export class AutoQueuePage extends App {
9 | navigateTo() {
10 | return browser.get(Urls.APP_BASE_URL + "autoqueue");
11 | }
12 |
13 | getPatterns(): Promise> {
14 | return element.all(by.css("#autoqueue .pattern span.text")).map(function (elm) {
15 | return browser.executeScript("return arguments[0].innerHTML;", elm);
16 | });
17 | }
18 |
19 | addPattern(pattern: string) {
20 | let input = element(by.css("#add-pattern input"));
21 | input.sendKeys(pattern);
22 | let button = element(by.css("#add-pattern .button"));
23 | button.click();
24 | }
25 |
26 | removePattern(index: number) {
27 | let button = element.all(by.css("#autoqueue .pattern")).get(index).element(by.css(".button"));
28 | button.click();
29 | }
30 | }
--------------------------------------------------------------------------------
/src/e2e/tests/settings.page.spec.ts:
--------------------------------------------------------------------------------
1 | import {SettingsPage} from "./settings.page";
2 |
3 | describe('Testing settings page', () => {
4 | let page: SettingsPage;
5 |
6 | beforeEach(() => {
7 | page = new SettingsPage();
8 | page.navigateTo();
9 | });
10 |
11 | it('should have right top title', () => {
12 | expect(page.getTopTitle()).toEqual("Settings");
13 | });
14 | });
--------------------------------------------------------------------------------
/src/e2e/tests/settings.page.ts:
--------------------------------------------------------------------------------
1 | import {browser, by, element} from 'protractor';
2 | import {promise} from "selenium-webdriver";
3 | import Promise = promise.Promise;
4 |
5 | import {Urls} from "../urls";
6 | import {App} from "./app";
7 |
8 | export class SettingsPage extends App {
9 | navigateTo() {
10 | return browser.get(Urls.APP_BASE_URL + "settings");
11 | }
12 | }
--------------------------------------------------------------------------------
/src/e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | // Source: https://github.com/angular/protractor/tree/master/exampleTypescript
2 | {
3 | "compilerOptions": {
4 | "target": "es6",
5 | "module": "commonjs",
6 | "moduleResolution": "node",
7 | "inlineSourceMap": true,
8 | "declaration": false,
9 | "noImplicitAny": false,
10 | "outDir": "tmp"
11 | },
12 | "exclude": [
13 | "node_modules"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/src/e2e/urls.ts:
--------------------------------------------------------------------------------
1 | export class Urls {
2 | static readonly APP_BASE_URL = "http://myapp:8800/";
3 | static readonly SELENIUM_ADDRESS = "http://localhost:4444/wd/hub";
4 | }
--------------------------------------------------------------------------------
/src/python/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/__init__.py
--------------------------------------------------------------------------------
/src/python/common/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from .types import overrides
4 | from .job import Job
5 | from .context import Context, Args
6 | from .error import AppError, ServiceExit, ServiceRestart
7 | from .constants import Constants
8 | from .config import Config, ConfigError
9 | from .persist import Persist, PersistError, Serializable
10 | from .localization import Localization
11 | from .multiprocessing_logger import MultiprocessingLogger
12 | from .status import Status, IStatusListener, StatusComponent, IStatusComponentListener
13 | from .app_process import AppProcess, AppOneShotProcess
14 |
--------------------------------------------------------------------------------
/src/python/common/constants.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 |
4 | class Constants:
5 | """
6 | POD class to hold shared constants
7 | :return:
8 | """
9 | SERVICE_NAME = "seedsync"
10 | MAIN_THREAD_SLEEP_INTERVAL_IN_SECS = 0.5
11 | MAX_LOG_SIZE_IN_BYTES = 10*1024*1024 # 10 MB
12 | LOG_BACKUP_COUNT = 10
13 | WEB_ACCESS_LOG_NAME = 'web_access'
14 | MIN_PERSIST_TO_FILE_INTERVAL_IN_SECS = 30
15 | JSON_PRETTY_PRINT_INDENT = 4
16 | LFTP_TEMP_FILE_SUFFIX = ".lftp"
17 |
--------------------------------------------------------------------------------
/src/python/common/error.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 |
4 | class AppError(Exception):
5 | """
6 | Exception indicating an error
7 | """
8 | pass
9 |
10 |
11 | class ServiceExit(AppError):
12 | """
13 | Custom exception which is used to trigger the clean exit
14 | of all running threads and the main program.
15 | """
16 | pass
17 |
18 |
19 | class ServiceRestart(AppError):
20 | """
21 | Exception indicating a restart is requested
22 | """
23 | pass
24 |
--------------------------------------------------------------------------------
/src/python/common/localization.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 |
4 | class Localization:
5 | class Error:
6 | MISSING_FILE = "The file '{}' doesn't exist."
7 | REMOTE_SERVER_SCAN = "An error occurred while scanning the remote server: '{}'."
8 | REMOTE_SERVER_INSTALL = "An error occurred while installing scanner script to remote server: '{}'."
9 | LOCAL_SERVER_SCAN = "An error occurred while scanning the local system."
10 | SETTINGS_INCOMPLETE = "The settings are not fully configured."
11 |
--------------------------------------------------------------------------------
/src/python/common/persist.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import os
4 | from abc import ABC, abstractmethod
5 | from typing import Type, TypeVar
6 |
7 | from .error import AppError
8 | from .localization import Localization
9 |
10 |
11 | # Source: https://stackoverflow.com/a/39205612/8571324
12 | T_Persist = TypeVar('T_Persist', bound='Persist')
13 | T_Serializable = TypeVar('T_Serializable', bound='Serializable')
14 |
15 |
16 | class Serializable(ABC):
17 | """
18 | Defines a class that is serializable to string.
19 | The string representation must be human readable (i.e. not pickle)
20 | """
21 | @classmethod
22 | @abstractmethod
23 | def from_str(cls: Type[T_Serializable], content: str) -> T_Serializable:
24 | pass
25 |
26 | @abstractmethod
27 | def to_str(self) -> str:
28 | pass
29 |
30 |
31 | class PersistError(AppError):
32 | """
33 | Exception indicating persist loading/saving error
34 | """
35 | pass
36 |
37 |
38 | class Persist(Serializable):
39 | """
40 | Defines state that should be persisted between runs
41 | Provides utility methods to persist/load content to/from file
42 | Concrete implementations need to implement the from_str() and
43 | to_str() functionality
44 | """
45 | @classmethod
46 | def from_file(cls: Type[T_Persist], file_path: str) -> T_Persist:
47 | if not os.path.isfile(file_path):
48 | raise AppError(Localization.Error.MISSING_FILE.format(file_path))
49 | with open(file_path, "r") as f:
50 | return cls.from_str(f.read())
51 |
52 | def to_file(self, file_path: str):
53 | with open(file_path, "w") as f:
54 | f.write(self.to_str())
55 |
56 | @classmethod
57 | @abstractmethod
58 | def from_str(cls: Type[T_Persist], content: str) -> T_Persist:
59 | pass
60 |
61 | @abstractmethod
62 | def to_str(self) -> str:
63 | pass
64 |
--------------------------------------------------------------------------------
/src/python/common/types.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import inspect
4 |
5 |
6 | def overrides(interface_class):
7 | """
8 | Decorator to check that decorated method is a valid override
9 | Source: https://stackoverflow.com/a/8313042
10 | :param interface_class: The super class
11 | :return:
12 | """
13 | assert(inspect.isclass(interface_class)), "Overrides parameter must be a class type"
14 |
15 | def overrider(method):
16 | assert(method.__name__ in dir(interface_class)), "Method does not override super class"
17 | return method
18 | return overrider
19 |
--------------------------------------------------------------------------------
/src/python/controller/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from .controller import Controller
4 | from .controller_job import ControllerJob
5 | from .controller_persist import ControllerPersist
6 | from .model_builder import ModelBuilder
7 | from .auto_queue import AutoQueue, AutoQueuePersist, IAutoQueuePersistListener, AutoQueuePattern
8 | from .scan import IScanner, ScannerResult, ScannerProcess, ScannerError
9 |
--------------------------------------------------------------------------------
/src/python/controller/controller_job.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 |
4 | # my libs
5 | from common import overrides, Job, Context
6 | from .controller import Controller
7 | from .auto_queue import AutoQueue
8 |
9 |
10 | class ControllerJob(Job):
11 | """
12 | The controller service
13 | Handles querying and downloading of files
14 | """
15 | def __init__(self,
16 | context: Context,
17 | controller: Controller,
18 | auto_queue: AutoQueue):
19 | super().__init__(name=self.__class__.__name__, context=context)
20 | self.__controller = controller
21 | self.__auto_queue = auto_queue
22 |
23 | @overrides(Job)
24 | def setup(self):
25 | self.__controller.start()
26 |
27 | @overrides(Job)
28 | def execute(self):
29 | self.__controller.process()
30 | self.__auto_queue.process()
31 |
32 | @overrides(Job)
33 | def cleanup(self):
34 | self.__controller.exit()
35 |
--------------------------------------------------------------------------------
/src/python/controller/controller_persist.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import json
4 |
5 | from common import overrides, Constants, Persist, PersistError
6 |
7 |
8 | class ControllerPersist(Persist):
9 | """
10 | Persisting state for controller
11 | """
12 |
13 | # Keys
14 | __KEY_DOWNLOADED_FILE_NAMES = "downloaded"
15 | __KEY_EXTRACTED_FILE_NAMES = "extracted"
16 |
17 | def __init__(self):
18 | self.downloaded_file_names = set()
19 | self.extracted_file_names = set()
20 |
21 | @classmethod
22 | @overrides(Persist)
23 | def from_str(cls: "ControllerPersist", content: str) -> "ControllerPersist":
24 | persist = ControllerPersist()
25 | try:
26 | dct = json.loads(content)
27 | persist.downloaded_file_names = set(dct[ControllerPersist.__KEY_DOWNLOADED_FILE_NAMES])
28 | persist.extracted_file_names = set(dct[ControllerPersist.__KEY_EXTRACTED_FILE_NAMES])
29 | return persist
30 | except (json.decoder.JSONDecodeError, KeyError) as e:
31 | raise PersistError("Error parsing AutoQueuePersist - {}: {}".format(
32 | type(e).__name__, str(e))
33 | )
34 |
35 | @overrides(Persist)
36 | def to_str(self) -> str:
37 | dct = dict()
38 | dct[ControllerPersist.__KEY_DOWNLOADED_FILE_NAMES] = list(self.downloaded_file_names)
39 | dct[ControllerPersist.__KEY_EXTRACTED_FILE_NAMES] = list(self.extracted_file_names)
40 | return json.dumps(dct, indent=Constants.JSON_PRETTY_PRINT_INDENT)
41 |
--------------------------------------------------------------------------------
/src/python/controller/delete/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from .delete_process import DeleteLocalProcess, DeleteRemoteProcess
4 |
--------------------------------------------------------------------------------
/src/python/controller/delete/delete_process.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import os
4 | import shutil
5 | from typing import Optional
6 |
7 | from common import AppOneShotProcess
8 | from ssh import Sshcp, SshcpError
9 |
10 |
11 | class DeleteLocalProcess(AppOneShotProcess):
12 | def __init__(self, local_path: str, file_name: str):
13 | super().__init__(name=self.__class__.__name__)
14 | self.__local_path = local_path
15 | self.__file_name = file_name
16 |
17 | def run_once(self):
18 | file_path = os.path.join(self.__local_path, self.__file_name)
19 | self.logger.debug("Deleting local file {}".format(self.__file_name))
20 | if not os.path.exists(file_path):
21 | self.logger.error("Failed to delete non-existing file: {}".format(file_path))
22 | else:
23 | if os.path.isfile(file_path):
24 | os.remove(file_path)
25 | else:
26 | shutil.rmtree(file_path, ignore_errors=True)
27 |
28 |
29 | class DeleteRemoteProcess(AppOneShotProcess):
30 | def __init__(self,
31 | remote_address: str,
32 | remote_username: str,
33 | remote_password: Optional[str],
34 | remote_port: int,
35 | remote_path: str,
36 | file_name: str):
37 | super().__init__(name=self.__class__.__name__)
38 | self.__remote_path = remote_path
39 | self.__file_name = file_name
40 | self.__ssh = Sshcp(host=remote_address,
41 | port=remote_port,
42 | user=remote_username,
43 | password=remote_password)
44 |
45 | def run_once(self):
46 | self.__ssh.set_base_logger(self.logger)
47 | file_path = os.path.join(self.__remote_path, self.__file_name)
48 | self.logger.debug("Deleting remote file {}".format(self.__file_name))
49 | try:
50 | out = self.__ssh.shell("rm -rf '{}'".format(file_path))
51 | self.logger.debug("Remote delete output: {}".format(out.decode()))
52 | except SshcpError:
53 | self.logger.exception("Exception while deleting remote file")
54 |
--------------------------------------------------------------------------------
/src/python/controller/extract/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from .extract import Extract, ExtractError
4 | from .dispatch import ExtractDispatch, ExtractDispatchError, ExtractListener, ExtractStatus
5 | from .extract_process import ExtractProcess, ExtractStatusResult, ExtractCompletedResult
6 |
--------------------------------------------------------------------------------
/src/python/controller/scan/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from .scanner_process import IScanner, ScannerResult, ScannerProcess, ScannerError
4 | from .active_scanner import ActiveScanner
5 | from .local_scanner import LocalScanner
6 | from .remote_scanner import RemoteScanner
7 |
--------------------------------------------------------------------------------
/src/python/controller/scan/active_scanner.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import logging
4 | from typing import List
5 | import multiprocessing
6 | import queue
7 |
8 | from .scanner_process import IScanner
9 | from common import overrides
10 | from system import SystemScanner, SystemScannerError, SystemFile
11 |
12 |
13 | class ActiveScanner(IScanner):
14 | """
15 | Scanner implementation to scan the active files only
16 | A caller sets the names of the active files that need to be scanned.
17 | A multiprocessing.Queue is used to store the names because the set and scan
18 | methods are called by different processes.
19 | """
20 | def __init__(self, local_path: str):
21 | self.__scanner = SystemScanner(local_path)
22 | self.__active_files_queue = multiprocessing.Queue()
23 | self.__active_files = [] # latest state
24 | self.logger = logging.getLogger(self.__class__.__name__)
25 |
26 | @overrides(IScanner)
27 | def set_base_logger(self, base_logger: logging.Logger):
28 | self.logger = base_logger.getChild(self.__class__.__name__)
29 |
30 | def set_active_files(self, file_names: List[str]):
31 | """
32 | Set the list of active file names. Only these files will be scanned.
33 | :param file_names:
34 | :return:
35 | """
36 | self.__active_files_queue.put(file_names)
37 |
38 | @overrides(IScanner)
39 | def scan(self) -> List[SystemFile]:
40 | # Grab the latest list of active files, if any
41 | try:
42 | while True:
43 | self.__active_files = self.__active_files_queue.get(block=False)
44 | except queue.Empty:
45 | pass
46 |
47 | # Do the scan
48 | # self.logger.debug("Scanning files: {}".format(str(self.__active_files)))
49 | result = []
50 | for file_name in self.__active_files:
51 | try:
52 | result.append(self.__scanner.scan_single(file_name))
53 | except SystemScannerError as ex:
54 | # Ignore errors here, file may have been deleted
55 | self.logger.warning(str(ex))
56 | return result
57 |
--------------------------------------------------------------------------------
/src/python/controller/scan/local_scanner.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import logging
4 | from typing import List
5 |
6 | from .scanner_process import IScanner, ScannerError
7 | from common import overrides, Localization, Constants
8 | from system import SystemScanner, SystemFile, SystemScannerError
9 |
10 |
11 | class LocalScanner(IScanner):
12 | """
13 | Scanner implementation to scan the local filesystem
14 | """
15 | def __init__(self, local_path: str, use_temp_file: bool):
16 | self.__scanner = SystemScanner(local_path)
17 | if use_temp_file:
18 | self.__scanner.set_lftp_temp_suffix(Constants.LFTP_TEMP_FILE_SUFFIX)
19 | self.logger = logging.getLogger("LocalScanner")
20 |
21 | @overrides(IScanner)
22 | def set_base_logger(self, base_logger: logging.Logger):
23 | self.logger = base_logger.getChild("LocalScanner")
24 |
25 | @overrides(IScanner)
26 | def scan(self) -> List[SystemFile]:
27 | try:
28 | result = self.__scanner.scan()
29 | except SystemScannerError:
30 | self.logger.exception("Caught SystemScannerError")
31 | raise ScannerError(Localization.Error.LOCAL_SERVER_SCAN, recoverable=False)
32 | return result
33 |
--------------------------------------------------------------------------------
/src/python/docs/faq.md:
--------------------------------------------------------------------------------
1 | # Frequently Asked Questions (FAQ)
2 |
3 | ## General
4 |
5 | ### How do I restart SeedSync Debian Service?
6 |
7 | SeedSync can be restarted from the web GUI. If that fails, you can restart the service from command-line:
8 |
9 | :::bash
10 | sudo service seedsync restart
11 |
12 |
13 | ### How can I save my settings across updates when using the Docker image?
14 |
15 | To maintain state across updates, you can store the settings in the host machine.
16 | Add the following option when starting the container.
17 |
18 | :::bash
19 | -v :/config
20 |
21 | where `` refers to the location on host machine where you wish to store the application
22 | state.
23 |
24 |
25 | ## Security
26 |
27 | ### Does SeedSync collect any data?
28 |
29 | No, SeedSync does not collect any data.
30 |
31 |
32 | ## Troubleshooting
33 |
34 | ### SeedSync can't seem to connect to my remote server?
35 |
36 | Make sure your remote server address was entered correctly.
37 | If using password-based login, make sure the password is correct.
38 | Check the logs for details about the exact failure.
39 |
40 | ### I am getting some errors about locale?
41 |
42 | On some servers you may see errors in the log like so:
43 | `Unpickling error: unpickling stack underflow b'bash: warning: setlocale: LC_ALL: cannot change locale`
44 |
45 | This means your remote server requires that the locale matches with the Seedsync app.
46 | We can fix this my changing the locale for Seedsync.
47 | For Seedsync docker, try adding the following options to the `docker run` command:
48 | ```
49 | -e LC_ALL=en_US.UTF-8
50 | -e LANG=en_US.UTF-8
51 | ```
52 |
53 | See [this issue](https://github.com/ipsingh06/seedsync/issues/66) for more details.
54 |
--------------------------------------------------------------------------------
/src/python/docs/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/docs/images/favicon.png
--------------------------------------------------------------------------------
/src/python/docs/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/docs/images/logo.png
--------------------------------------------------------------------------------
/src/python/docs/index.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Documentation
6 |
7 | Welcome to SeedSync documentation!
8 |
9 | On the left navigation you will find useful links.
10 |
11 |
12 | External links:
13 |
14 | * Github: [ipsingh06/seedsync](https://github.com/ipsingh06/seedsync)
15 | * Docker Hub: [ipsingh06/seedsync](https://hub.docker.com/repository/docker/ipsingh06/seedsync)
--------------------------------------------------------------------------------
/src/python/docs/usage.md:
--------------------------------------------------------------------------------
1 | # Usage
2 |
3 | ## Dashboard
4 |
5 | The Dashboard page shows all the files and directories on the remote server and the local machine.
6 | Here you can manually queue files to be transferred, extract archives and delete files.
7 |
8 | ## AutoQueue
9 |
10 | AutoQueue queues all newly discovered files on the remote server.
11 | You can also restrict AutoQueue to pattern-based matches (see this option in the Settings page).
12 | When pattern restriction is enabled, the AutoQueue page is where you can add or remove patterns.
13 | Any files or directories on the remote server that match a pattern will be automatically queued for transfer.
14 |
15 |
--------------------------------------------------------------------------------
/src/python/lftp/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from .lftp import Lftp, LftpError
4 | from .job_status import LftpJobStatus
5 | from .job_status_parser import LftpJobStatusParser, LftpJobStatusParserError
6 |
--------------------------------------------------------------------------------
/src/python/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: SeedSync
2 | theme:
3 | name: material
4 | language: en
5 | palette:
6 | scheme: preference
7 | primary: teal
8 | accent: teal
9 | font: false
10 | logo: images/logo.png
11 | favicon: images/favicon.png
12 | markdown_extensions:
13 | - admonition
14 | - codehilite:
15 | guess_lang: false
16 | - toc:
17 | permalink: true
18 | repo_name: ipsingh06/seedsync
19 | repo_url: https://github.com/ipsingh06/seedsync
20 | nav:
21 | - Home: index.md
22 | - Installation: install.md
23 | - Usage: usage.md
24 | - FAQ: faq.md
25 |
--------------------------------------------------------------------------------
/src/python/model/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from .model import Model, IModelListener, ModelError
4 | from .file import ModelFile
5 | from .diff import ModelDiff, ModelDiffUtil
6 |
--------------------------------------------------------------------------------
/src/python/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool]
2 | [tool.poetry]
3 | name = "seedsync"
4 | version = "0.0.0"
5 | description = ""
6 | authors = []
7 |
8 | [tool.poetry.dependencies]
9 | python = "~3.8"
10 | bottle = "*"
11 | mkdocs = "*"
12 | mkdocs-material = "*"
13 | parameterized = "*"
14 | paste = "*"
15 | patool = "*"
16 | pexpect = "*"
17 | pytz = "*"
18 | requests = "*"
19 | tblib = "*"
20 | timeout-decorator = "*"
21 |
22 | [tool.poetry.dev-dependencies]
23 | pyinstaller = "*"
24 | testfixtures = "*"
25 | webtest = "*"
26 | pytest = "^6.2.1"
27 |
--------------------------------------------------------------------------------
/src/python/scan_fs.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import pickle
4 | import sys
5 | import argparse
6 |
7 | # my libs
8 | from system import SystemScanner, SystemFile, SystemScannerError
9 |
10 |
11 | if __name__ == "__main__":
12 | if sys.hexversion < 0x03050000:
13 | sys.exit("Python 3.5 or newer is required to run this program.")
14 |
15 | parser = argparse.ArgumentParser(description="File size scanner")
16 | parser.add_argument("path", help="Path of the root directory to scan")
17 | parser.add_argument("-e", "--exclude-hidden", action="store_true", default=False,
18 | help="Exclude hidden files")
19 | parser.add_argument("-H", "--human-readable", action="store_true", default=False,
20 | help="Human readable output")
21 | args = parser.parse_args()
22 |
23 | scanner = SystemScanner(args.path)
24 | if args.exclude_hidden:
25 | scanner.add_exclude_prefix(".")
26 | try:
27 | root_files = scanner.scan()
28 | except SystemScannerError as e:
29 | sys.exit("SystemScannerError: {}".format(str(e)))
30 | if args.human_readable:
31 | def print_file(file: SystemFile, level: int):
32 | sys.stdout.write(" "*level)
33 | sys.stdout.write("{} {} {}\n".format(
34 | file.name,
35 | "d" if file.is_dir else "f",
36 | file.size
37 | ))
38 | for child in file.children:
39 | print_file(child, level+1)
40 | for root_file in root_files:
41 | print_file(root_file, 0)
42 | else:
43 | bytes_out = pickle.dumps(root_files)
44 | sys.stdout.buffer.write(bytes_out)
45 |
--------------------------------------------------------------------------------
/src/python/ssh/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from .sshcp import Sshcp, SshcpError
4 |
--------------------------------------------------------------------------------
/src/python/system/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from .scanner import SystemScanner, SystemScannerError
4 | from .file import SystemFile
5 |
--------------------------------------------------------------------------------
/src/python/system/file.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from typing import List
4 | from datetime import datetime
5 |
6 |
7 | class SystemFile:
8 | """
9 | Represents a system file or directory
10 | """
11 | def __init__(self,
12 | name: str,
13 | size: int,
14 | is_dir: bool = False,
15 | time_created: datetime = None,
16 | time_modified: datetime = None):
17 | if size < 0:
18 | raise ValueError("File size must be greater than zero")
19 | self.__name = name
20 | self.__size = size # in bytes
21 | self.__is_dir = is_dir
22 | self.__timestamp_created = time_created
23 | self.__timestamp_modified = time_modified
24 | self.__children = []
25 |
26 | def __eq__(self, other):
27 | return self.__dict__ == other.__dict__
28 |
29 | def __repr__(self):
30 | return str(self.__dict__)
31 |
32 | @property
33 | def name(self) -> str: return self.__name
34 |
35 | @property
36 | def size(self) -> int: return self.__size
37 |
38 | @property
39 | def is_dir(self) -> bool: return self.__is_dir
40 |
41 | @property
42 | def timestamp_created(self) -> datetime: return self.__timestamp_created
43 |
44 | @property
45 | def timestamp_modified(self) -> datetime: return self.__timestamp_modified
46 |
47 | @property
48 | def children(self) -> List["SystemFile"]: return self.__children
49 |
50 | def add_child(self, file: "SystemFile"):
51 | if not self.__is_dir:
52 | raise TypeError("Cannot add children to a file")
53 | self.__children.append(file)
54 |
--------------------------------------------------------------------------------
/src/python/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/integration/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/integration/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/integration/test_controller/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/integration/test_controller/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/integration/test_controller/test_extract/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/integration/test_controller/test_extract/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/integration/test_lftp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/integration/test_lftp/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/integration/test_web/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/integration/test_web/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/integration/test_web/test_handler/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/integration/test_web/test_handler/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/integration/test_web/test_handler/test_server.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from tests.integration.test_web.test_web_app import BaseTestWebApp
4 |
5 |
6 | class TestServerHandler(BaseTestWebApp):
7 | def test_restart(self):
8 | self.assertFalse(self.web_app_builder.server_handler.is_restart_requested())
9 | print(self.test_app.get("/server/command/restart"))
10 | self.assertTrue(self.web_app_builder.server_handler.is_restart_requested())
11 | print(self.test_app.get("/server/command/restart"))
12 | self.assertTrue(self.web_app_builder.server_handler.is_restart_requested())
13 |
--------------------------------------------------------------------------------
/src/python/tests/integration/test_web/test_handler/test_status.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import json
4 |
5 | from tests.integration.test_web.test_web_app import BaseTestWebApp
6 |
7 |
8 | class TestStatusHandler(BaseTestWebApp):
9 | def test_status(self):
10 | resp = self.test_app.get("/server/status")
11 | self.assertEqual(200, resp.status_int)
12 | json_dict = json.loads(str(resp.html))
13 | self.assertEqual(True, json_dict["server"]["up"])
14 |
--------------------------------------------------------------------------------
/src/python/tests/integration/test_web/test_handler/test_stream_log.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import logging
4 | from unittest.mock import patch
5 | from threading import Timer
6 |
7 | from tests.integration.test_web.test_web_app import BaseTestWebApp
8 |
9 |
10 | class TestLogStreamHandler(BaseTestWebApp):
11 | @patch("web.handler.stream_log.SerializeLogRecord")
12 | def test_stream_log_serializes_record(self, mock_serialize_log_record_cls):
13 | # Schedule server stop
14 | Timer(0.5, self.web_app.stop).start()
15 |
16 | # Schedule status update
17 | def issue_logs():
18 | self.context.logger.debug("Debug msg")
19 | self.context.logger.info("Info msg")
20 | self.context.logger.warning("Warning msg")
21 | self.context.logger.error("Error msg")
22 | Timer(0.3, issue_logs).start()
23 |
24 | # Setup mock serialize instance
25 | mock_serialize = mock_serialize_log_record_cls.return_value
26 | mock_serialize.record.return_value = "\n"
27 |
28 | self.test_app.get("/server/stream")
29 | self.assertEqual(4, len(mock_serialize.record.call_args_list))
30 | call1, call2, call3, call4 = mock_serialize.record.call_args_list
31 | record1 = call1[0][0]
32 | self.assertEqual("Debug msg", record1.msg)
33 | self.assertEqual(logging.DEBUG, record1.levelno)
34 | record2 = call2[0][0]
35 | self.assertEqual("Info msg", record2.msg)
36 | self.assertEqual(logging.INFO, record2.levelno)
37 | record3 = call3[0][0]
38 | self.assertEqual("Warning msg", record3.msg)
39 | self.assertEqual(logging.WARNING, record3.levelno)
40 | record4 = call4[0][0]
41 | self.assertEqual("Error msg", record4.msg)
42 | self.assertEqual(logging.ERROR, record4.levelno)
43 |
--------------------------------------------------------------------------------
/src/python/tests/integration/test_web/test_web_app.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import unittest
4 | from unittest.mock import MagicMock
5 | import logging
6 | import sys
7 |
8 | from webtest import TestApp
9 |
10 | from common import overrides, Status, Config
11 | from controller import AutoQueuePersist
12 | from web import WebAppBuilder
13 |
14 |
15 | class BaseTestWebApp(unittest.TestCase):
16 | """
17 | Base class for testing web app
18 | Sets up the web app with mocks
19 | """
20 | @overrides(unittest.TestCase)
21 | def setUp(self):
22 | self.context = MagicMock()
23 | self.controller = MagicMock()
24 |
25 | # Mock the base logger
26 | logger = logging.getLogger()
27 | handler = logging.StreamHandler(sys.stdout)
28 | logger.addHandler(handler)
29 | logger.setLevel(logging.DEBUG)
30 | formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s")
31 | handler.setFormatter(formatter)
32 | self.context.logger = logger
33 |
34 | # Model files
35 | self.model_files = []
36 |
37 | # Real status
38 | self.context.status = Status()
39 |
40 | # Real config
41 | self.context.config = Config()
42 |
43 | # Real auto-queue persist
44 | self.auto_queue_persist = AutoQueuePersist()
45 |
46 | # Capture the model listener
47 | def capture_listener(listener):
48 | self.model_listener = listener
49 | return self.model_files
50 | self.model_listener = None
51 | self.controller.get_model_files_and_add_listener = MagicMock()
52 | self.controller.get_model_files_and_add_listener.side_effect = capture_listener
53 | self.controller.remove_model_listener = MagicMock()
54 |
55 | # noinspection PyTypeChecker
56 | self.web_app_builder = WebAppBuilder(self.context,
57 | self.controller,
58 | self.auto_queue_persist)
59 | self.web_app = self.web_app_builder.build()
60 | self.test_app = TestApp(self.web_app)
61 |
62 |
63 | class TestWebApp(BaseTestWebApp):
64 | def test_process(self):
65 | self.web_app.process()
66 |
--------------------------------------------------------------------------------
/src/python/tests/unittests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/unittests/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/unittests/test_common/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/unittests/test_common/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/unittests/test_common/test_job.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import unittest
4 | from unittest.mock import MagicMock
5 | import time
6 |
7 |
8 | from common import Job
9 |
10 |
11 | class DummyError(Exception):
12 | pass
13 |
14 |
15 | class DummyFailingJob(Job):
16 | def setup(self):
17 | # noinspection PyAttributeOutsideInit
18 | self.cleanup_run = False
19 |
20 | def execute(self):
21 | raise DummyError()
22 |
23 | def cleanup(self):
24 | # noinspection PyAttributeOutsideInit
25 | self.cleanup_run = True
26 |
27 |
28 | class TestJob(unittest.TestCase):
29 | def test_exception_propagates(self):
30 | context = MagicMock()
31 | # noinspection PyTypeChecker
32 | job = DummyFailingJob("DummyFailingJob", context)
33 | job.start()
34 | time.sleep(0.2)
35 | with self.assertRaises(DummyError):
36 | job.propagate_exception()
37 | job.terminate()
38 | job.join()
39 |
40 | def test_cleanup_executes_on_execute_error(self):
41 | context = MagicMock()
42 | # noinspection PyTypeChecker
43 | job = DummyFailingJob("DummyFailingJob", context)
44 | job.start()
45 | time.sleep(0.2)
46 | job.terminate()
47 | job.join()
48 | self.assertTrue(job.cleanup_run)
49 |
--------------------------------------------------------------------------------
/src/python/tests/unittests/test_controller/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/unittests/test_controller/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/unittests/test_controller/test_extract/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/unittests/test_controller/test_extract/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/unittests/test_controller/test_scan/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/unittests/test_controller/test_scan/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/unittests/test_lftp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/unittests/test_lftp/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/unittests/test_model/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/unittests/test_model/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/unittests/test_ssh/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/unittests/test_ssh/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/unittests/test_system/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/unittests/test_system/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/unittests/test_web/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/unittests/test_web/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/unittests/test_web/test_serialize/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ipsingh06/seedsync/ff2a1039935beccbbf7ec76134b41d2e91137742/src/python/tests/unittests/test_web/test_serialize/__init__.py
--------------------------------------------------------------------------------
/src/python/tests/unittests/test_web/test_serialize/test_serialize.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import unittest
4 |
5 | from web.serialize import Serialize
6 |
7 |
8 | class DummySerialize(Serialize):
9 | def dummy(self):
10 | return self._sse_pack(event="event", data="data")
11 |
12 |
13 | def parse_stream(serialized_str: str):
14 | parsed = dict()
15 | for line in serialized_str.split("\n"):
16 | if line:
17 | key, value = line.split(":", maxsplit=1)
18 | parsed[key.strip()] = value.strip()
19 | return parsed
20 |
--------------------------------------------------------------------------------
/src/python/tests/unittests/test_web/test_serialize/test_serialize_auto_queue.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import unittest
4 | import json
5 |
6 | from controller import AutoQueuePattern
7 | from web.serialize import SerializeAutoQueue
8 |
9 |
10 | class TestSerializeConfig(unittest.TestCase):
11 | def test_is_list(self):
12 | patterns = [
13 | AutoQueuePattern(pattern="one"),
14 | AutoQueuePattern(pattern="two"),
15 | AutoQueuePattern(pattern="three")
16 | ]
17 | out = SerializeAutoQueue.patterns(patterns)
18 | out_list = json.loads(out)
19 | self.assertIsInstance(out_list, list)
20 | self.assertEqual(3, len(out_list))
21 |
22 | def test_patterns(self):
23 | patterns = [
24 | AutoQueuePattern(pattern="one"),
25 | AutoQueuePattern(pattern="tw o"),
26 | AutoQueuePattern(pattern="th'ree"),
27 | AutoQueuePattern(pattern="fo\"ur"),
28 | AutoQueuePattern(pattern="fi=ve")
29 | ]
30 | out = SerializeAutoQueue.patterns(patterns)
31 | out_list = json.loads(out)
32 | self.assertEqual(5, len(out_list))
33 | self.assertEqual([
34 | {"pattern": "one"},
35 | {"pattern": "tw o"},
36 | {"pattern": "th'ree"},
37 | {"pattern": "fo\"ur"},
38 | {"pattern": "fi=ve"},
39 | ], out_list)
40 |
--------------------------------------------------------------------------------
/src/python/tests/utils.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import os
4 |
5 |
6 | class TestUtils:
7 | @staticmethod
8 | def chmod_from_to(from_path: str, to_path: str, mode: int):
9 | """
10 | Chmod from_path and all its parents up to and including to_path
11 | :param from_path:
12 | :param to_path:
13 | :param mode:
14 | :return:
15 | """
16 | path = from_path
17 | try:
18 | os.chmod(path, mode)
19 | except PermissionError:
20 | pass
21 | while path != "/" and path != to_path:
22 | path = os.path.dirname(path)
23 | try:
24 | os.chmod(path, mode)
25 | except PermissionError:
26 | pass
27 |
--------------------------------------------------------------------------------
/src/python/web/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from .web_app import WebApp
4 | from .web_app_job import WebAppJob
5 | from .web_app_builder import WebAppBuilder
6 |
--------------------------------------------------------------------------------
/src/python/web/handler/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/python/web/handler/config.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from bottle import HTTPResponse
4 | from urllib.parse import unquote
5 |
6 | from common import overrides, Config, ConfigError
7 | from ..web_app import IHandler, WebApp
8 | from ..serialize import SerializeConfig
9 |
10 |
11 | class ConfigHandler(IHandler):
12 | def __init__(self, config: Config):
13 | self.__config = config
14 |
15 | @overrides(IHandler)
16 | def add_routes(self, web_app: WebApp):
17 | web_app.add_handler("/server/config/get", self.__handle_get_config)
18 | # The regex allows slashes in values
19 | web_app.add_handler("/server/config/set///", self.__handle_set_config)
20 |
21 | def __handle_get_config(self):
22 | out_json = SerializeConfig.config(self.__config)
23 | return HTTPResponse(body=out_json)
24 |
25 | def __handle_set_config(self, section: str, key: str, value: str):
26 | # value is double encoded
27 | value = unquote(value)
28 |
29 | if not self.__config.has_section(section):
30 | return HTTPResponse(body="There is no section '{}' in config".format(section), status=400)
31 | inner_config = getattr(self.__config, section)
32 | if not inner_config.has_property(key):
33 | return HTTPResponse(body="Section '{}' in config has no option '{}'".format(section, key), status=400)
34 | try:
35 | inner_config.set_property(key, value)
36 | return HTTPResponse(body="{}.{} set to {}".format(section, key, value))
37 | except ConfigError as e:
38 | return HTTPResponse(body=str(e), status=400)
39 |
--------------------------------------------------------------------------------
/src/python/web/handler/server.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from bottle import HTTPResponse
4 |
5 | from common import Context, overrides
6 | from ..web_app import IHandler, WebApp
7 |
8 |
9 | class ServerHandler(IHandler):
10 | def __init__(self, context: Context):
11 | self.logger = context.logger.getChild("ServerActionHandler")
12 | self.__request_restart = False
13 |
14 | @overrides(IHandler)
15 | def add_routes(self, web_app: WebApp):
16 | web_app.add_handler("/server/command/restart", self.__handle_action_restart)
17 |
18 | def is_restart_requested(self):
19 | """
20 | Returns true is a restart is requested
21 | :return:
22 | """
23 | return self.__request_restart
24 |
25 | def __handle_action_restart(self):
26 | """
27 | Request a server restart
28 | :return:
29 | """
30 | self.logger.info("Received a restart action")
31 | self.__request_restart = True
32 | return HTTPResponse(body="Requested restart")
33 |
--------------------------------------------------------------------------------
/src/python/web/handler/status.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from bottle import HTTPResponse
4 |
5 | from common import Status, overrides
6 | from ..web_app import IHandler, WebApp
7 | from ..serialize import SerializeStatusJson
8 |
9 |
10 | class StatusHandler(IHandler):
11 | def __init__(self, status: Status):
12 | self.__status = status
13 |
14 | @overrides(IHandler)
15 | def add_routes(self, web_app: WebApp):
16 | web_app.add_handler("/server/status", self.__handle_get_status)
17 |
18 | def __handle_get_status(self):
19 | out_json = SerializeStatusJson.status(self.__status)
20 | return HTTPResponse(body=out_json)
21 |
--------------------------------------------------------------------------------
/src/python/web/handler/stream_status.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from typing import Optional
4 |
5 | from ..web_app import IStreamHandler
6 | from ..serialize import SerializeStatus
7 | from ..utils import StreamQueue
8 | from common import overrides, Status, IStatusListener
9 |
10 |
11 | class StatusListener(IStatusListener, StreamQueue[Status]):
12 | """
13 | Status listener used by status streams to listen to status updates
14 | """
15 | def __init__(self, status: Status):
16 | super().__init__()
17 | self.__status = status
18 |
19 | @overrides(IStatusListener)
20 | def notify(self):
21 | self.put(self.__status.copy())
22 |
23 |
24 | class StatusStreamHandler(IStreamHandler):
25 | def __init__(self, status: Status):
26 | self.status = status
27 | self.serialize = SerializeStatus()
28 | self.status_listener = StatusListener(status)
29 | self.first_run = True
30 |
31 | @overrides(IStreamHandler)
32 | def setup(self):
33 | self.status.add_listener(self.status_listener)
34 |
35 | @overrides(IStreamHandler)
36 | def get_value(self) -> Optional[str]:
37 | if self.first_run:
38 | self.first_run = False
39 | status = self.status.copy()
40 | return self.serialize.status(status)
41 | else:
42 | status = self.status_listener.get_next_event()
43 | if status is not None:
44 | return self.serialize.status(status)
45 | else:
46 | return None
47 |
48 | @overrides(IStreamHandler)
49 | def cleanup(self):
50 | if self.status_listener:
51 | self.status.remove_listener(self.status_listener)
52 |
--------------------------------------------------------------------------------
/src/python/web/serialize/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from .serialize import Serialize
4 | from .serialize_model import SerializeModel
5 | from .serialize_status import SerializeStatus, SerializeStatusJson
6 | from .serialize_config import SerializeConfig
7 | from .serialize_auto_queue import SerializeAutoQueue
8 | from .serialize_log_record import SerializeLogRecord
9 |
--------------------------------------------------------------------------------
/src/python/web/serialize/serialize.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from abc import ABC
4 |
5 |
6 | class Serialize(ABC):
7 | """
8 | Base class for SSE serialization
9 | """
10 | # noinspection PyMethodMayBeStatic
11 | def _sse_pack(self, event: str, data: str) -> str:
12 | """Pack data in SSE format"""
13 | buffer = ""
14 | buffer += "event: %s\n" % event
15 | buffer += "data: %s\n" % data
16 | buffer += "\n"
17 | return buffer
18 |
--------------------------------------------------------------------------------
/src/python/web/serialize/serialize_auto_queue.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import json
4 | from typing import List
5 |
6 | from controller import AutoQueuePattern
7 |
8 |
9 | class SerializeAutoQueue:
10 | __KEY_PATTERN = "pattern"
11 |
12 | @staticmethod
13 | def patterns(patterns: List[AutoQueuePattern]) -> str:
14 | patterns_list = []
15 | for pattern in patterns:
16 | patterns_list.append({
17 | SerializeAutoQueue.__KEY_PATTERN: pattern.pattern
18 | })
19 |
20 | return json.dumps(patterns_list)
21 |
--------------------------------------------------------------------------------
/src/python/web/serialize/serialize_config.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import json
4 | import collections
5 |
6 | from common import Config
7 |
8 |
9 | class SerializeConfig:
10 | @staticmethod
11 | def config(config: Config) -> str:
12 | config_dict = config.as_dict()
13 |
14 | # Make the section names lower case
15 | keys = list(config_dict.keys())
16 | config_dict_lowercase = collections.OrderedDict()
17 | for key in keys:
18 | config_dict_lowercase[key.lower()] = config_dict[key]
19 |
20 | return json.dumps(config_dict_lowercase)
21 |
--------------------------------------------------------------------------------
/src/python/web/serialize/serialize_log_record.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | import json
4 | import logging
5 |
6 | from .serialize import Serialize
7 |
8 |
9 | class SerializeLogRecord(Serialize):
10 | """
11 | This class defines the serialization interface between python backend
12 | and the EventSource client frontend for the log stream.
13 | """
14 | # Event keys
15 | __EVENT_RECORD = "log-record"
16 |
17 | # Data keys
18 | __KEY_TIME = "time"
19 | __KEY_LEVEL_NAME = "level_name"
20 | __KEY_LOGGER_NAME = "logger_name"
21 | __KEY_MESSAGE = "message"
22 | __KEY_EXCEPTION_TRACEBACK = "exc_tb"
23 |
24 | def __init__(self):
25 | super().__init__()
26 | # logging formatter to generate exception traceback
27 | self.__log_formatter = logging.Formatter()
28 |
29 | def record(self, record: logging.LogRecord) -> str:
30 | json_dict = dict()
31 | json_dict[SerializeLogRecord.__KEY_TIME] = str(record.created)
32 | json_dict[SerializeLogRecord.__KEY_LEVEL_NAME] = record.levelname
33 | json_dict[SerializeLogRecord.__KEY_LOGGER_NAME] = record.name
34 | json_dict[SerializeLogRecord.__KEY_MESSAGE] = record.msg
35 | exc_text = None
36 | if record.exc_text:
37 | exc_text = record.exc_text
38 | elif record.exc_info:
39 | exc_text = self.__log_formatter.formatException(record.exc_info)
40 | json_dict[SerializeLogRecord.__KEY_EXCEPTION_TRACEBACK] = exc_text
41 |
42 | record_json = json.dumps(json_dict)
43 | return self._sse_pack(event=SerializeLogRecord.__EVENT_RECORD, data=record_json)
44 |
--------------------------------------------------------------------------------
/src/python/web/utils.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from queue import Queue, Empty
4 | from typing import TypeVar, Generic, Optional
5 |
6 |
7 | T = TypeVar('T')
8 |
9 |
10 | class StreamQueue(Generic[T]):
11 | """
12 | A queue that transfers events from one thread to another.
13 | Useful for web streams that wait for listener events from other threads.
14 | The producer thread calls put() to insert events. The consumer stream
15 | calls get_next_event() to receive event in its own thread.
16 | """
17 | def __init__(self):
18 | self.__queue = Queue()
19 |
20 | def put(self, event: T):
21 | self.__queue.put(event)
22 |
23 | def get_next_event(self) -> Optional[T]:
24 | """
25 | Returns the next event if there is one, otherwise returns None
26 | :return:
27 | """
28 | try:
29 | return self.__queue.get(block=False)
30 | except Empty:
31 | return None
32 |
--------------------------------------------------------------------------------
/src/python/web/web_app_builder.py:
--------------------------------------------------------------------------------
1 | # Copyright 2017, Inderpreet Singh, All rights reserved.
2 |
3 | from common import Context
4 | from controller import Controller, AutoQueuePersist
5 | from .web_app import WebApp
6 | from .handler.stream_model import ModelStreamHandler
7 | from .handler.stream_status import StatusStreamHandler
8 | from .handler.controller import ControllerHandler
9 | from .handler.server import ServerHandler
10 | from .handler.config import ConfigHandler
11 | from .handler.auto_queue import AutoQueueHandler
12 | from .handler.stream_log import LogStreamHandler
13 | from .handler.status import StatusHandler
14 |
15 |
16 | class WebAppBuilder:
17 | """
18 | Helper class to build WebApp with all the extensions
19 | """
20 | def __init__(self,
21 | context: Context,
22 | controller: Controller,
23 | auto_queue_persist: AutoQueuePersist):
24 | self.__context = context
25 | self.__controller = controller
26 |
27 | self.controller_handler = ControllerHandler(controller)
28 | self.server_handler = ServerHandler(context)
29 | self.config_handler = ConfigHandler(context.config)
30 | self.auto_queue_handler = AutoQueueHandler(auto_queue_persist)
31 | self.status_handler = StatusHandler(context.status)
32 |
33 | def build(self) -> WebApp:
34 | web_app = WebApp(context=self.__context,
35 | controller=self.__controller)
36 |
37 | StatusStreamHandler.register(web_app=web_app,
38 | status=self.__context.status)
39 |
40 | LogStreamHandler.register(web_app=web_app,
41 | logger=self.__context.logger)
42 |
43 | ModelStreamHandler.register(web_app=web_app,
44 | controller=self.__controller)
45 |
46 | self.controller_handler.add_routes(web_app)
47 | self.server_handler.add_routes(web_app)
48 | self.config_handler.add_routes(web_app)
49 | self.auto_queue_handler.add_routes(web_app)
50 | self.status_handler.add_routes(web_app)
51 |
52 | web_app.add_default_routes()
53 |
54 | return web_app
55 |
--------------------------------------------------------------------------------