import { TagIndexService } from './tag-index.service' ; import { Injectable } from '@angular/core' ; import { WikiStorage } from './wiki-storage' ; import { LoggingService } from './logging-service' ; import { DropboxFileService } from './dropbox-get-file.service' ; import { IDropboxEntry } from './idropbox-entry' ; import { IDropboxAuth } from './idropbox-auth' ; import { DropboxListFilesService } from './dropbox-list-files.service' ; import { SyncState } from './sync-state' ; import { Storage } from '@ionic/storage' ; import { StoredFile } from './stored-file' ; import * as ignoreCase from 'ignore-case' ; import * as mergeText from 'plus.merge-text' ; import * as Throttle from 'promise-parallel-throttle' ; import { Task } from 'promise-parallel-throttle' ; import { Utils } from './utils' ; @ Injectable ( ) export class DropboxSyncService { private readonly syncInfoStorageKey : string = '.wiki-v4-sync-info' ; constructor ( private dropboxList , private dropboxFile , private loggingService , private tagIndexService , private wikiStorage , private storage ) { } syncFiles ( auth ) { const state = new SyncState ( this . loggingService ) ; state . makingProgress ( 'Initializing' ) ; state . promise = this . startSync ( state , auth ) ; return state ; } private async startSync ( state , auth ) { try { await this . doSyncStepsAsync ( state , auth ) ; } catch ( error ) { throw new Error ( `Error in step ${ state . syncProgress . label }: ${ Utils . serializeError ( error ) }` ) ; } } private async doSyncStepsAsync ( state , auth ) { await this . wait ( 2000 ) ; await this . dropboxList . initialize ( auth ) ; const localList = await this . getLocalSyncInfo ( ) ; state . localSyncInfo = localList ; state . makingProgress ( 'Get file info from dropbox...' ) ; const allRemoteFiles = await this . dropboxList . listFiles ( auth , true ) ; state . allRemoteFiles = allRemoteFiles ; state . remoteChangedFiles = this . dropboxList . getChangedFiles ( state . allRemoteFiles , state . localSyncInfo ) ; state . makingProgress ( 'Get local file info...' ) ; const allLocalFileNames = await this . wikiStorage . listFiles ( ) ; const promises = allLocalFileNames . map ( x => this . wikiStorage . getFileContents ( x ) ) ; const allLocalFiles = await Promise . all ( promises ) ; state . allLocalFiles = allLocalFiles ; state . localChangedFiles = state . allLocalFiles . filter ( x => { const fileInfoOnPreviousSync = state . localSyncInfo . find ( y => ignoreCase . equals ( y . name , x . fileName ) ) ; return ! fileInfoOnPreviousSync || fileInfoOnPreviousSync . checksum !== x . checksum ; } ) ; state . localDeletedFiles = state . localSyncInfo . filter ( x => ! allLocalFiles . find ( y => ignoreCase . equals ( x . name , y . fileName ) ) ) ; state . makingProgress ( 'Apply changes...' ) ; const filesToDownload = state . remoteChangedFiles . filter ( x => { const wasChangedLocally = state . localChangedFiles . find ( y => ignoreCase . equals ( x . name , y . fileName ) ) ; const deletedLocally = state . localDeletedFiles . find ( y => ignoreCase . equals ( x . name , y . name ) ) ; return ! wasChangedLocally && ! deletedLocally ; } ) ; const filesToUpload = state . localChangedFiles . filter ( x => { const remoteChange = state . remoteChangedFiles . find ( y => ignoreCase . equals ( y . name , x . fileName ) ) ; const wasChangedRemotely = ! ! remoteChange ; const wasDeletedRemotely = wasChangedRemotely && remoteChange . deleted ; return ! wasChangedRemotely || wasDeletedRemotely ; } ) ; const filesToUploadDeletion = state . localDeletedFiles . filter ( x => { const wasChangedRemotely = state . remoteChangedFiles . find ( y => ignoreCase . equals ( y . name , x . name ) ) ; return ! wasChangedRemotely ; } ) ; const filesToMerge = state . remoteChangedFiles . filter ( x => { if ( x . deleted ) { return false ; } if ( ! x . name . endsWith ( '.txt' ) ) { return false ; } const localChangedFile = state . localChangedFiles . find ( y => ignoreCase . equals ( x . name , y . fileName ) ) ; return ! ! localChangedFile ; } ) ; const downloadPromises = filesToDownload . filter ( x => ! x . deleted && x . isFile ) . map ( file => this . dropboxFile . download ( file , auth ) . then ( ( storedFile ) => this . wikiStorage . save ( storedFile . fileName , storedFile . contents ) ) . then ( ( ) => state . makingProgress ( ) ) . catch ( err => { this . loggingService . log ( 'download failed for file ' + file . name , err ) ; state . failedFiles . push ( file . name ) ; } ) ) ; const deletePromises = filesToDownload . filter ( x => x . deleted ) . map ( file => this . wikiStorage . delete ( file . name ) . then ( ( ) => state . makingProgress ( ) ) . catch ( err => { this . loggingService . log ( 'delete failed for file ' + file . name , err ) ; state . failedFiles . push ( file . name ) ; } ) ) ; const deleteRemotePromises = filesToUploadDeletion . map ( file => this . dropboxFile . delete ( file , auth ) . then ( ( ) => state . makingProgress ( ) ) . catch ( err => { this . loggingService . log ( 'remote delete failed for file ' + file . name , err ) ; state . failedFiles . push ( file . name ) ; } ) ) ; const mergePromises = filesToMerge . map ( file => this . mergeFile ( file , auth , state ) ) ; const uploadPromises = filesToUpload . map ( file => { return this . dropboxFile . upload ( file , auth ) . then ( ( ) => state . makingProgress ( ) ) . catch ( err => { this . loggingService . log ( 'upload failed for file ' + file . fileName , err ) ; state . failedFiles . push ( file . fileName ) ; } ) ; } ) ; this . loggingService . log ( 'downloads: ' + downloadPromises . length + '; delete local: ' + deletePromises . length + '; delete remote: ' + deleteRemotePromises . length + '; merges: ' + mergePromises . length + '; uploads: ' + uploadPromises . length ) ; const allPromises = [ ] . concat ( downloadPromises ) . concat ( deletePromises ) . concat ( deleteRemotePromises ) . concat ( mergePromises ) . concat ( uploadPromises ) ; state . setTotalSteps ( allPromises . length ) ; const allTasks = allPromises . map ( ( x ) => ( ) => x ) ; await Throttle . sync ( allTasks ) ; state . setTotalSteps ( 1 ) ; state . makingProgress ( 'Save sync info...' ) ; const newAllRemoteFiles = await this . dropboxList . listFiles ( auth ) ; const allSucceeded = newAllRemoteFiles . filter ( x => state . failedFiles . indexOf ( x . name ) === - 1 ) ; const checksumPromises = allSucceeded . filter ( x => x . isFile ) . map ( x => this . wikiStorage . getFileContents ( x . name ) . then ( file => { x . checksum = file . checksum ; return x ; } ) ) ; const newSyncState = await Promise . all ( checksumPromises ) ; state . makingProgress ( 'Sync finished' ) ; await this . saveLocalSyncInfo ( newSyncState ) ; await this . tagIndexService . buildIndex ( ) ; } private wait ( ms ) { return new Promise ( ( resolve ) => setTimeout ( ( ) => resolve ( ) , ms ) ) ; } private async mergeFile ( remote , auth , state ) { try { const lastKnownLocalState = state . localSyncInfo . find ( x => x . id === remote . id ) ; const downloadCurrent = this . dropboxFile . download ( remote , auth ) ; let downloadParentRevision ; if ( lastKnownLocalState ) { downloadParentRevision = this . dropboxFile . download ( lastKnownLocalState , auth , true ) ; } else { downloadParentRevision = downloadCurrent ; } const localFile = state . allLocalFiles . find ( x => ignoreCase . equals ( x . fileName , remote . name ) ) ; const resolved = await Promise . all ( [ downloadCurrent , downloadParentRevision ] ) ; const latestRemoteState = resolved [ 0 ] ; const parentStateOfLocalChange = resolved [ 1 ] ; const remoteContents = latestRemoteState . contents as string ; const localContents = localFile . contents as string ; const commonParent = parentStateOfLocalChange . contents ; let newContents = localContents ; try { if ( remoteContents !== localContents ) { const merged = mergeText . merge ( commonParent , remoteContents , localContents ) ; newContents = merged . toString ( ) ; } } catch ( err ) { } await this . wikiStorage . save ( localFile . fileName , newContents ) ; const newLocalFile = await this . wikiStorage . getFileContents ( localFile . fileName ) ; await this . dropboxFile . upload ( newLocalFile , auth ) ; state . makingProgress ( ) ; } catch ( err ) { this . loggingService . log ( 'merge failed for file ' + remote . name , err ) ; state . failedFiles . push ( remote . name ) ; } } /**
     * get local file with sync info state from the last sync
     */ private getLocalSyncInfo ( ) { return this . storage . get ( this . syncInfoStorageKey ) . then ( value => ( value || [ ] ) as IDropboxEntry [ ] ) ; } private saveLocalSyncInfo ( value ) { return this . storage . set ( this . syncInfoStorageKey , value ) ; } clearLocalSyncState ( ) { return this . storage . remove ( this . syncInfoStorageKey ) ; } }