├── .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 | 7 |
v{{version}}
8 | 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 |
2 | 9 |
10 | 19 | 20 |
21 |
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 |
4 | 5 | 16 | 17 | 18 |
19 | 20 | 21 |
25 |
26 | 27 |
28 |
29 | 35 |
36 | 37 | 38 | 39 |
40 | 43 | {{activeRoute?.name}} 44 |
45 | 46 |
47 | 48 |
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 | 7 | 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/default-remote.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/delete-local.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/delete-remote.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/deleted.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/directory-archive-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/directory-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/directory.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/downloaded.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/downloading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/extract.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/extracting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/file-archive-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/file-archive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/file-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 13 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/queue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/sort-asc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 14 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/sort-desc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 14 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/angular/src/assets/icons/stopped.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 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 | SeedSync 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 | --------------------------------------------------------------------------------