Stefan Huber

init

node_modules/
.vscode/
# bsync-client
Allows downloading of files referenced by a http/https URI inside a couchdb/pouchdb record. The plugin works for cordova and electron projects.
# Usage
### In cordova projects
### In electron projects
Within the main process bsync needs to be integrated an initiated.
import {Bsync} from 'bsync';
Bsync.init(ipcMain, filePath);
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
'use strict';
var rxjs = require('rxjs');
var http = require('http');
var https = require('https');
var fs = require('fs');
var NodeFileHandler = (function () {
function NodeFileHandler() {
}
NodeFileHandler.prototype.selectProtocol = function (url) {
if (url.search(/^http:\/\//) === 0) {
return http;
}
else if (url.search(/^https:\/\//) === 0) {
return https;
}
else {
return null;
}
};
NodeFileHandler.prototype.download = function (source, target) {
var handler = this.selectProtocol(source);
return rxjs.Observable.create(function (subscriber) {
if (!handler) {
subscriber.error("No handler for source: " + source);
return;
}
// file already exists and is not empty
if (fs.existsSync(target) && (fs.statSync(target)['size'] > 0)) {
subscriber.complete();
return;
}
var file = fs.createWriteStream(target, { 'flags': 'a' });
handler.get(source, function (response) {
var size = response.headers['content-length']; // in bytes
var prog = 0; // already downloaded
var progCounts = 100; // how many progress events should be triggerd (1-100 %)
var nextProg = (1 / progCounts);
response.on('data', function (chunk) {
prog += chunk.length;
file.write(chunk, 'binary');
if ((prog / size) > nextProg) {
subscriber.next(prog / size);
nextProg += (1 / progCounts);
}
});
response.on('end', function () {
file.end();
subscriber.complete();
});
}).on('error', function (error) {
fs.unlink(target);
subscriber.error("Error while downloading: " + error);
});
});
};
return NodeFileHandler;
}());
var Bsync = (function () {
function Bsync() {
}
Bsync.configIpcMain = function (ipcMain, downloadDir) {
var nodeFileHander = new NodeFileHandler();
ipcMain.on('bsync-download', function (event, args) {
nodeFileHander.download(args.source, downloadDir + args.target)
.subscribe(function (progress) { event.sender.send('bsync-download-progress', progress); }, function (error) { event.sender.send('bsync-download-error', error); }, function () { event.sender.send('bsync-download-complete'); });
});
};
return Bsync;
}());
module.exports = Bsync;
//# sourceMappingURL=node-build.js.map
{"version":3,"file":null,"sources":["../src/file-handler/node-file-handler.ts","../src/node-main.ts"],"sourcesContent":["import { Observable, Subscriber } from 'rxjs';\nimport { FileHandler } from '../api/file-handler';\nimport * as http from 'http';\nimport * as https from 'https';\nimport * as fs from 'fs';\n\nexport class NodeFileHandler implements FileHandler {\n\n selectProtocol(url:string) : any {\n if (url.search(/^http:\\/\\//) === 0) {\n return http;\n } else if (url.search(/^https:\\/\\//) === 0) {\n return https;\n } else {\n return null;\n }\n }\n\n download(source:string, target:string) : Observable<number> {\n\n let handler = this.selectProtocol(source);\n\n return Observable.create((subscriber:Subscriber<number>) => {\n \n if (!handler) {\n subscriber.error(\"No handler for source: \" + source);\n return;\n }\n\n // file already exists and is not empty\n if (fs.existsSync(target) && (fs.statSync(target)['size'] > 0)) {\n subscriber.complete();\n return;\n }\n\n let file = fs.createWriteStream(target, {'flags': 'a'});\n\n handler.get(source, (response) => {\n let size = response.headers['content-length']; // in bytes\n let prog = 0; // already downloaded\n let progCounts = 100; // how many progress events should be triggerd (1-100 %)\n let nextProg = (1/progCounts);\n \n response.on('data', (chunk) => {\n prog += chunk.length;\n file.write(chunk, 'binary');\n\n if ((prog / size) > nextProg) {\n subscriber.next(prog / size);\n nextProg += (1 / progCounts);\n } \n });\n\n response.on('end', () => {\n file.end();\n subscriber.complete();\n });\n \n }).on('error', (error) => {\n fs.unlink(target);\n subscriber.error(\"Error while downloading: \" + error);\n });\n\n });\n\n }\n\n}","import { NodeFileHandler } from './file-handler/node-file-handler';\n\nexport default class Bsync {\n\n static configIpcMain(ipcMain: any, downloadDir:string) {\n let nodeFileHander = new NodeFileHandler();\n\n ipcMain.on('bsync-download', (event, args) => {\n nodeFileHander.download(args.source, downloadDir + args.target)\n .subscribe(\n (progress:number) => { event.sender.send('bsync-download-progress', progress); } ,\n (error:any) => { event.sender.send('bsync-download-error', error); } ,\n () => { event.sender.send('bsync-download-complete'); }\n );\n });\n }\n\n}"],"names":["Observable","fs.existsSync","fs.statSync","fs.createWriteStream","fs.unlink"],"mappings":"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"}
\ No newline at end of file
// Karma configuration
// Generated on Tue Jan 03 2017 13:04:16 GMT+0100 (CET)
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
'./node_modules/rxjs/bundles/Rx.min.js' ,
'./node_modules/pouchdb/dist/pouchdb.min.js' ,
'./.tmp/browser-test.js'
],
// list of files to exclude
exclude: [
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Chrome'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity
})
}
{
"name": "bsync-client",
"version": "1.0.0",
"description": "",
"main": "dist/browser-build.js",
"scripts": {
"build": "npm run build:node && npm run build:browser",
"build:node": "rollup -c ./rollup.config.node.js",
"build:browser": "rollup --config ./rollup.config.browser.js",
"pretest": "scripts/before-test.sh",
"posttest": "scripts/after-test.sh",
"test": "npm run test:node && npm run test:browser",
"test:node": "rollup --config ./rollup.config.node-test.js && jasmine",
"test:browser": "rollup --config ./rollup.config.browser-test.js && karma start",
"test:cordova": "npm run build:browser && scripts/prepare-cordova-test.sh"
},
"author": "",
"license": "ISC",
"dependencies": {
"rxjs": "^5.0.2"
},
"devDependencies": {
"@types/jasmine": "^2.5.40",
"cordova": "^6.4.0",
"jasmine": "^2.5.2",
"karma": "^1.3.0",
"karma-chrome-launcher": "^2.0.0",
"karma-jasmine": "^1.1.0",
"pouchdb": "^6.1.0",
"pouchdb-upsert": "^2.0.2",
"rollup": "^0.39.2",
"rollup-plugin-commonjs": "^7.0.0",
"rollup-plugin-ignore": "^1.0.3",
"rollup-plugin-node-builtins": "^2.0.0",
"rollup-plugin-node-globals": "^1.1.0",
"rollup-plugin-node-resolve": "^2.0.0",
"rollup-plugin-typescript": "^0.8.1"
}
}
<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
id="bsync-client"
version="1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<name>bsync client plugin</name>
<description>Cordova plugin for syncing files referenced by pouchdb records</description>
<author>Stefan Huber</author>
<keywords>pouchdb, file sync</keywords>
<license>Apache 2.0 License</license>
<asset src="dist/browser-build.js" target="bsync.js"></asset>
<dependency id="cordova-plugin-file-transfer" url="https://github.com/apache/cordova-plugin-file-transfer" commit="master" />
</plugin>
\ No newline at end of file
import typescript from 'rollup-plugin-typescript';
import globals from 'rollup-plugin-node-globals';
import builtins from 'rollup-plugin-node-builtins';
export default {
entry: './spec/browser-test.ts',
dest: './.tmp/browser-test.js',
format: 'umd',
globals: {
'rxjs' : 'Rx'
},
plugins: [
typescript() ,
globals(),
builtins()
]
};
\ No newline at end of file
import typescript from 'rollup-plugin-typescript';
import builtins from 'rollup-plugin-node-builtins';
import globals from 'rollup-plugin-node-globals';
export default {
moduleName : 'bsync',
entry: './src/browser-main.ts',
dest: './dist/browser-build.js',
format: 'cjs',
sourceMap: true ,
plugins: [
typescript(),
// globals(),
// builtins()
]
};
\ No newline at end of file
import typescript from 'rollup-plugin-typescript';
export default {
entry: './spec/node-test.ts',
dest: './.tmp/node-test-build.spec.js',
format: 'cjs',
plugins: [
typescript()
]
};
\ No newline at end of file
import typescript from 'rollup-plugin-typescript';
export default {
entry: './src/node-main.ts',
dest: './dist/node-build.js',
format: 'cjs',
sourceMap: true ,
plugins: [
typescript()
]
};
\ No newline at end of file
#!/bin/bash
curl -X DELETE http://admin:admin@127.0.0.1:5984/pouch_test_db
rm -R ./.tmp
\ No newline at end of file
#!/bin/bash
curl -X DELETE http://admin:admin@127.0.0.1:5984/pouch_test_db
curl -X PUT http://admin:admin@127.0.0.1:5984/pouch_test_db
#!/bin/bash
COMMAND=${1:-emulate}
PLATFORM=${2:-android}
echo "cordova $COMMAND $PLATFORM"
cd ..
rm -r ./bysnc-client-test-app
./bsync-client/node_modules/.bin/cordova create bysnc-client-test-app
cd ./bysnc-client-test-app
../bsync-client/node_modules/.bin/cordova platform add $PLATFORM
../bsync-client/node_modules/.bin/cordova plugin add ../bsync-client
../bsync-client/node_modules/.bin/cordova plugin add ../bsync-client/spec/cordova
../bsync-client/node_modules/.bin/cordova plugin add cordova-plugin-test-framework
sed -i 's/index\.html/cdvtests\/index\.html/g' ./config.xml
if [ $COMMAND == "run" ]; then
../bsync-client/node_modules/.bin/cordova run $PLATFORM
else
../bsync-client/node_modules/.bin/cordova emulate $PLATFORM
fi
import PouchDB from 'pouchdb';
import {FileReplicator} from '../src/file-replicator';
import {ServiceLocator, ENV_UNKNOWN} from '../src/service-locator';
import {TestFileHandler} from './file-handler/test-file-handler';
ServiceLocator.addFileHandler(ENV_UNKNOWN, new TestFileHandler());
import '../src/browser-main';
declare var emit:any;
const dbUrl = 'http://admin:admin@localhost:5984/pouch_test_db';
const testDocs = [
{ 'type' : 'asset', 'source' : 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/FullMoon2010.jpg/292px-FullMoon2010.jpg' } ,
{ 'type' : 'asset', 'source' : 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/2b/Jupiter_and_its_shrunken_Great_Red_Spot.jpg/260px-Jupiter_and_its_shrunken_Great_Red_Spot.jpg' } ,
{ 'type' : 'asset', 'source' : 'https://upload.wikimedia.org/wikipedia/commons/c/c7/Saturn_during_Equinox.jpg' }
];
describe("Integration tests with couchdb", () => {
let index = 1;
let remoteDb = new PouchDB(dbUrl);
let localDb;
beforeAll((done) => {
ServiceLocator.getFileReplicator().retryTimeout = 100;
remoteDb.bulkDocs(testDocs).then(() => {
done();
});
});
beforeEach(() => {
localDb = new PouchDB('testdb-' + index);
index++;
localDb.put({
_id : "_design/index_type",
views : {
type : {
map : function(doc) {
if (doc.type) { emit(doc.type); }
}.toString()
}
}
});
});
it("Should successfully download several files", (done) => {
TestFileHandler.setErrorRate(0);
ServiceLocator.getFileReplicator().init();
let index = 0;
localDb.replicate.from(dbUrl)
.on('file-replicator-complete', event => {
index++;
})
.on('complete', () => {
expect(index).toEqual(3);
done();
});
});
it("Should trigger errors, but successfully download with retries", (done) => {
TestFileHandler.setErrorRate(0.8);
ServiceLocator.getFileReplicator().init();
let errors = 0;
localDb.replicate.from(dbUrl)
.on('file-replicator-error', event => {
errors++;
})
.on('complete', () => {
expect(errors).toBeGreaterThanOrEqual(1);
done();
});
});
});
exports.defineAutoTests = function() {
console.log(bsync);
describe("Cordova tests", () => {
/*
let downloader = new CordovaDownloader();
it("should download sample image from https source and store with new name", (done) => {
let source = "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/FullMoon2010.jpg/800px-FullMoon2010.jpg";
let target = "cdvfile://full-moon.jpg";
let lastProgress = 0;
downloader.download(source, target)
.subscribe(
(progress: number) => {
expect(progress).toBeGreaterThan(lastProgress);
lastProgress = progress;
} ,
(error:any) => {} ,
() => {
expect(lastProgress).toEqual(1);
// expect(fs.existsSync(target)).toBeTruthy();
done();
}
);
});
*/
});
};
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
xmlns:android="http://schemas.android.com/apk/res/android"
id="bsync-client-test"
version="1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<name>bsync cordova test</name>
<license>Apache 2.0 License</license>
<js-module src="cordova-test.spec.js" name="tests">
</js-module>
</plugin>
\ No newline at end of file
import { Observable, Subscriber } from 'rxjs';
import { FileHandler } from '../../src/api/file-handler';
export class TestFileHandler implements FileHandler {
protected static errorRate:number = 0;
static setErrorRate(rate:number) {
TestFileHandler.errorRate = rate;
}
download(source:string, target:string) : Observable<number> {
return Observable.create((subscriber:Subscriber<number>) => {
let random = Math.random();
let error:boolean = random < TestFileHandler.errorRate;
let counter = 1;
if (error) {
subscriber.error("random error triggered");
return;
}
let interval = setInterval(() => {
if (counter < 4) {
subscriber.next(counter * 25);
} else {
subscriber.complete();
clearInterval(interval);
}
++counter;
}, 10);
});
}
}
\ No newline at end of file
export * from './test/file-handler/node-file-handler';
export * from './test/file-replicator';
\ No newline at end of file
{
"spec_dir": ".tmp",
"spec_files": [
"**/*[sS]pec.js"
],
"helpers": [
"helpers/**/*.js"
],
"stopSpecOnExpectationFailure": false,
"random": true
}
import { NodeFileHandler } from '../../../src/file-handler/node-file-handler';
import * as http from 'http';
import * as https from 'https';
import * as fs from 'fs';
describe("Node Downloader", () => {
let nodeFileHandler = new NodeFileHandler();
it("should retrieve right handler", () => {
let httpHandler = nodeFileHandler.selectProtocol("http://someurl.com/image.jpg");
expect(httpHandler).toEqual(http);
let httpsHandler = nodeFileHandler.selectProtocol("https://someurl.com/image.jpg");
expect(httpsHandler).toEqual(https);
let nullHandler = nodeFileHandler.selectProtocol("notsupported://blub");
expect(nullHandler).toBeNull();
});
it("should download sample image from https source and store with new name", (done) => {
let source = "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/FullMoon2010.jpg/800px-FullMoon2010.jpg";
let target = ".tmp/full-moon.jpg";
let lastProgress = 0;
nodeFileHandler.download(source, target)
.subscribe(
(progress: number) => {
expect(progress).toBeGreaterThan(lastProgress);
lastProgress = progress;
} ,
(error:any) => {} ,
() => {
expect(lastProgress).toEqual(1);
expect(fs.existsSync(target)).toBeTruthy();
done();
}
);
});
it('should not download if file with same name exists and bytesize > 0', (done) => {
let source = "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/FullMoon2010.jpg/800px-FullMoon2010.jpg";
let target = ".tmp/full-moon-sensless.jpg";
let lastProgress = 0;
let file = fs.createWriteStream(target, {'flags': 'a'});
let dummyData = "some sensless information to fill bytes...";
file.write(new Buffer(dummyData));
file.end(() => {
nodeFileHandler.download(source, target)
.subscribe(
() => { fail("progress should not be called"); } ,
() => {} ,
() => {
expect(fs.existsSync(target)).toBeTruthy();
done();
}
);
});
});
});
\ No newline at end of file
import { FileReplicator } from '../../src/file-replicator';
describe("File Replicator", () => {
let fileReplicator = new FileReplicator();
let change = {
docs : [
{ language: 'de', type : "asset", source : "http://someplace.com/icon.jpg" , target : "icon.jpg" } ,
{ language: 'de', type : "asset", source : "http://sampleuri.com/image.png" } ,
{ language: 'en', type : "asset", source : "https://secureasset.com/asset.mp3" , target : "music.mp3" }
]
};
beforeEach(() => {
fileReplicator.init();
});
it("should contain several assets", () => {
fileReplicator.pushChanges(change);
expect(fileReplicator.files.length).toEqual(3);
});
it("should get correct asset names", () => {
let files = fileReplicator.prepareFiles(change.docs);
expect(files[0].target).toEqual("icon.jpg");
expect(files[1].target).toEqual("bsync_707608502");
expect(files[2].target).toEqual("music.mp3");
});
it("should only get items with language=de", () => {
fileReplicator.itemValidator = (item:any) => {
if (item && item.language === "de") {
return true;
}
return false;
};
let files = fileReplicator.prepareFiles(change.docs);
expect(files.length).toEqual(2);
expect(files[0].target).toEqual("icon.jpg");
expect(files[1].target).toEqual("bsync_707608502");
fileReplicator.itemValidator = null;
});
});
\ No newline at end of file
import { Observable } from 'rxjs/Observable';
export interface FileHandler {
/**
* The donwload works as following:
* - if the file already exists and is not empty: trigger complete
* - if the download is in progress: trigger next (0-1 progress for percent of download)
* - if the download enters any error condition, trigger error and an already downloaded part of the file
*/
download(source:string, target:string) : Observable<number>;
}
\ No newline at end of file
export interface File {
source :string;
target :string;
}
\ No newline at end of file
import { EventEmitter } from 'events';
import { ServiceLocator } from './service-locator';
export * from './service-locator';
export function loadBsyncPlugin (PouchDB) {
let pouchReplicate = PouchDB.replicate;
PouchDB.plugin((PouchDB) => {
PouchDB.replicate = function() {
let eventEmitter = new EventEmitter();
let emitter = pouchReplicate.apply(this, arguments);
let replicator = ServiceLocator.getFileReplicator();
let db = arguments[1];
replicator.once('final', event => {
eventEmitter.emit('complete');
eventEmitter.removeAllListeners();
});
replicator.on('error', event => {
eventEmitter.emit('file-replicator-error', event);
});
replicator.on('complete', event => {
eventEmitter.emit('file-replicator-complete', event);
});
replicator.on('progress', event => {
eventEmitter.emit('file-replicator-progress', event);
});
emitter.once('change', info => {
eventEmitter.emit('change', info);
});
emitter.once('complete', info => {
db.query('index_type/type',{
include_docs : true,
key : replicator.itemValue
}).then((res) => {
let docs = { docs : [] };
for (let r of res.rows) {
docs.docs.push(r.doc);
}
replicator.pushChanges(docs);
replicator.start();
}).catch(error => {
eventEmitter.emit('error', error);
});
});
emitter.once('error', (error) => {
eventEmitter.emit('error', error);
});
return eventEmitter;
};
});
};
if (typeof window !== 'undefined' && window['PouchDB']) {
loadBsyncPlugin(window['PouchDB']);
}
export const CONFIG_ITEM_KEY = "itemKey";
export const CONFIG_ITEM_VALUE = "itemValue";
export const CONFIG_ITEM_SOURCE_ATTRIBUTE = "itemSourceAttribute";
export const CONFIG_ITEM_TARGET_ATTRIBUTE = "itemTargetAttribute";
export const CONFIG_ITEM_VALIDATOR = "itemValidator";
export const CONFIG_RETRY_TIMEOUT = "retryTimeout";
export const CONFIG_FILE_HANDLER = "fileHandler";
export class Config {
protected config:any = {};
hasConfig(key:string) {
if (this.config[key]) {
return true;
}
return false;
}
getConfig(key:string) {
return this.config[key];
}
setConfig(key:string, value:any) {
this.config[key] = value;
}
}
\ No newline at end of file
import { FileHandler } from '../api/file-handler';
declare var Rx;
export class CordovaDownloader implements FileHandler {
download(source:string, target:string) : Rx.Observable<number> {
return Rx.Observable.create((subscriber:Rx.Subscriber<number>) => {
if (!window['FileTransfer']) {
subscriber.error("Cordova FileTransfer object undefined");
}
let fileTransfer = new window['FileTransfer']();
fileTransfer.onprogress = (progress:ProgressEvent) => {
subscriber.next(progress.total / progress.loaded);
};
fileTransfer.download(
source ,
target ,
(entry:any) => {
subscriber.complete();
} ,
(error:any) => {
subscriber.error(error);
},
true
);
});
}
}
\ No newline at end of file
import { Observable, Subscriber } from 'rxjs';
import { FileHandler } from '../api/file-handler';
export class ElectronFileHandler implements FileHandler {
constructor (private ipcRenderer:any) {
}
download(source:string, target:string) : Observable<number> {
return Observable.create((subscriber:Subscriber<number>) => {
this.ipcRenderer.once('bsync-download-complete', () => {
this.ipcRenderer.removeAllListeners('bsync-download-progress');
this.ipcRenderer.removeAllListeners('bsync-download-error');
subscriber.complete();
});
this.ipcRenderer.on('bsync-download-progress', (progress:number) => {
subscriber.next(progress);
});
this.ipcRenderer.once('bsync-download-error', (error:any) => {
this.ipcRenderer.removeAllListeners('bsync-download-progress');
this.ipcRenderer.removeAllListeners('bsync-download-complete');
subscriber.error(error);
});
this.ipcRenderer.send('bsync-download', {
source : source ,
target : target
});
});
}
}
\ No newline at end of file
import { Observable, Subscriber } from 'rxjs';
import { FileHandler } from '../api/file-handler';
import * as http from 'http';
import * as https from 'https';
import * as fs from 'fs';
export class NodeFileHandler implements FileHandler {
selectProtocol(url:string) : any {
if (url.search(/^http:\/\//) === 0) {
return http;
} else if (url.search(/^https:\/\//) === 0) {
return https;
} else {
return null;
}
}
download(source:string, target:string) : Observable<number> {
let handler = this.selectProtocol(source);
return Observable.create((subscriber:Subscriber<number>) => {
if (!handler) {
subscriber.error("No handler for source: " + source);
return;
}
// file already exists and is not empty
if (fs.existsSync(target) && (fs.statSync(target)['size'] > 0)) {
subscriber.complete();
return;
}
let file = fs.createWriteStream(target, {'flags': 'a'});
handler.get(source, (response) => {
let size = response.headers['content-length']; // in bytes
let prog = 0; // already downloaded
let progCounts = 100; // how many progress events should be triggerd (1-100 %)
let nextProg = (1/progCounts);
response.on('data', (chunk) => {
prog += chunk.length;
file.write(chunk, 'binary');
if ((prog / size) > nextProg) {
subscriber.next(prog / size);
nextProg += (1 / progCounts);
}
});
response.on('end', () => {
file.end();
subscriber.complete();
});
}).on('error', (error) => {
fs.unlink(target);
subscriber.error("Error while downloading: " + error);
});
});
}
}
\ No newline at end of file
import {FileHandler} from './api/file-handler';
import {File} from './api/file';
import {Util} from './util';
import {EventEmitter} from 'events';
export class FileReplicator extends EventEmitter {
constructor() {
super();
}
protected _files:Array<File> = [];
protected _itemValidator: (item:any) => boolean = null;
protected _fileHandler:FileHandler = null;
protected _retryTimeout:number = 0;
protected _itemKey = "type";
protected _itemValue = "asset";
protected _itemSourceAttribute = "source";
protected _itemTargetAttribute = "target";
get files(): Array<File> {
return this._files;
}
set fileHandler (handler:FileHandler) {
this._fileHandler = handler;
}
set retryTimeout (timeout:number) {
this._retryTimeout = timeout;
}
set itemValidator(validator:(item:any) => boolean) {
this._itemValidator = validator;
}
set itemKey(key:string) {
this._itemKey = key;
}
set itemValue(value:string) {
this._itemValue = value;
}
set itemSourceAttribute(sourceAttribute:string) {
this._itemSourceAttribute = sourceAttribute;
}
set itemTargetAttribute(targetAttribute:string) {
this._itemTargetAttribute = targetAttribute;
}
get itemKey() {
return this._itemKey;
}
get itemValue() {
return this._itemValue;
}
get itemSourceAttribute() {
return this._itemSourceAttribute;
}
get itemTargetAttribute() {
return this._itemTargetAttribute;
}
init(files: Array<File> = []) {
this._files = files;
}
/**
* change from pouchdb replicate
*/
pushChanges(change:any) {
let items:Array<any> = [];
if (change && change.docs && change.docs.length > 0) {
for (let item of change.docs) {
if (item[this._itemKey] && item[this._itemKey] === this._itemValue) {
items.push(item);
}
}
}
let files = this.prepareFiles(items);
for (let file of files) {
this._files.push(file);
}
}
downloadFiles(files:Array<File>, fileHandler:FileHandler, index:number = 0) {
if (index >= files.length) {
return;
}
this.emit('start', { progress: 0, index : index, length : files.length });
fileHandler
.download(files[index].source, files[index].target)
.subscribe(
progress => {
this.emit('progress', { progress : progress, index : index, length : files.length })
} ,
error => {
this.emit('error', { progress : 0, index : index, length : files.length, error: error });
} ,
() => {
this.emit('complete', { progress : 100 , index : index, length : files.length });
this.downloadFiles(files, fileHandler, index+1);
}
);
}
prepareFiles(items: Array<any>) : Array<File> {
let output = [];
for (let item of items) {
if (item[this._itemSourceAttribute] && (!this._itemValidator || this._itemValidator(item))) {
let file = { source : item[this._itemSourceAttribute] , target : '' };
if (item[this._itemTargetAttribute]) {
file.target = item[this._itemTargetAttribute];
} else {
file.target = Util.getNameHash(file.source);
}
output.push(file);
}
}
return output;
}
start() {
this.on('complete', (event:any) => {
if ((event.index + 1) >= event.length) {
this.replicationFinalized(event.index);
}
});
this.on('error', (event:any) => {
this.replicationFinalized(event.index);
});
this.downloadFiles(this._files, this._fileHandler);
}
replicationFinalized(lastIndex:number) {
if (lastIndex+1 >= this._files.length) { // all finished
this._files = [];
this.emit('final');
} else if (this._retryTimeout > 0) { // restart after last success
this._files.splice(0,lastIndex);
setTimeout(() => {
this.downloadFiles(this._files, this._fileHandler);
}, this._retryTimeout);
}
}
}
\ No newline at end of file
import { NodeFileHandler } from './file-handler/node-file-handler';
export default class Bsync {
static configIpcMain(ipcMain: any, downloadDir:string) {
let nodeFileHander = new NodeFileHandler();
ipcMain.on('bsync-download', (event, args) => {
nodeFileHander.download(args.source, downloadDir + args.target)
.subscribe(
(progress:number) => { event.sender.send('bsync-download-progress', progress); } ,
(error:any) => { event.sender.send('bsync-download-error', error); } ,
() => { event.sender.send('bsync-download-complete'); }
);
});
}
}
\ No newline at end of file
import {FileHandler} from './api/file-handler';
import {ElectronFileHandler} from './file-handler/electron-file-handler';
import {FileReplicator} from './file-replicator';
import {
Config,
CONFIG_RETRY_TIMEOUT,
CONFIG_ITEM_KEY,
CONFIG_ITEM_VALUE,
CONFIG_ITEM_TARGET_ATTRIBUTE,
CONFIG_ITEM_SOURCE_ATTRIBUTE
} from './config';
export const ENV_ELECTRON = "electron";
export const ENV_CORDOVA = "cordova";
export const ENV_UNKNOWN = "unknown";
export class ServiceLocator {
protected static fileHandlers:any = {};
protected static fileReplicator: FileReplicator;
protected static config: Config;
static addFileHandler(environment:string, fileHandler:FileHandler) {
ServiceLocator.fileHandlers[environment] = fileHandler;
}
static getConfig() : Config {
if (!ServiceLocator.config) {
ServiceLocator.config = new Config();
}
return ServiceLocator.config;
}
static getEnvironment() {
if (typeof window['require'] === 'function' && window['require']('electron')) {
return ENV_ELECTRON;
}
if (typeof window['FileTransfer'] === 'function') {
return ENV_CORDOVA;
}
return ENV_UNKNOWN;
}
static getFileHandler() : FileHandler {
let environment = ServiceLocator.getEnvironment();
if (ServiceLocator.fileHandlers[environment]) {
return ServiceLocator.fileHandlers[environment];
}
if (environment === ENV_ELECTRON) {
return new ElectronFileHandler(window['require']('electron').ipcRenderer);
}
return null;
}
static getFileReplicator() : FileReplicator {
if (!ServiceLocator.fileReplicator) {
ServiceLocator.fileReplicator = new FileReplicator();
ServiceLocator.fileReplicator.fileHandler = ServiceLocator.getFileHandler();
if (ServiceLocator.getConfig().hasConfig(CONFIG_RETRY_TIMEOUT)) {
ServiceLocator.fileReplicator.retryTimeout = ServiceLocator.getConfig().getConfig(CONFIG_RETRY_TIMEOUT);
}
if (ServiceLocator.getConfig().hasConfig(CONFIG_ITEM_KEY)) {
ServiceLocator.fileReplicator.itemKey = ServiceLocator.getConfig().getConfig(CONFIG_ITEM_KEY);
}
if (ServiceLocator.getConfig().hasConfig(CONFIG_ITEM_VALUE)) {
ServiceLocator.fileReplicator.itemValue = ServiceLocator.getConfig().getConfig(CONFIG_ITEM_VALUE);
}
if (ServiceLocator.getConfig().hasConfig(CONFIG_ITEM_SOURCE_ATTRIBUTE)) {
ServiceLocator.fileReplicator.itemSourceAttribute = ServiceLocator.getConfig().getConfig(CONFIG_ITEM_SOURCE_ATTRIBUTE);
}
if (ServiceLocator.getConfig().hasConfig(CONFIG_ITEM_TARGET_ATTRIBUTE)) {
ServiceLocator.fileReplicator.itemTargetAttribute = ServiceLocator.getConfig().getConfig(CONFIG_ITEM_TARGET_ATTRIBUTE);
}
}
return ServiceLocator.fileReplicator;
}
}
\ No newline at end of file
export class Util {
static getNameHash(path:string) {
for(var r=0,i=0;i<path.length;i++) {
r=(r<<5)-r+path.charCodeAt(i),r&=r;
}
return "bsync_" + Math.abs(r);
}
}
\ No newline at end of file