2021-11-10 11:07:09 +02:00
import { App , debounce , Modal , Notice , Plugin , PluginSettingTab , Setting , TFile , addIcon , TFolder , normalizePath , TAbstractFile , Editor , MarkdownView } from "obsidian" ;
2021-10-12 16:50:13 +02:00
import { PouchDB } from "./pouchdb-browser-webpack/dist/pouchdb-browser" ;
import { DIFF_DELETE , DIFF_EQUAL , DIFF_INSERT , diff_match_patch } from "diff-match-patch" ;
2021-10-15 10:58:42 +02:00
import xxhash from "xxhash-wasm" ;
2021-10-12 16:50:13 +02:00
2021-10-14 12:27:08 +02:00
// docs should be encoded as base64, so 1 char -> 1 bytes
// and cloudant limitation is 1MB , we use 900kb;
// const MAX_DOC_SIZE = 921600;
2021-10-17 15:10:03 +02:00
const MAX_DOC_SIZE = 1000 ; // for .md file, but if delimiters exists. use that before.
2021-10-15 10:58:42 +02:00
const MAX_DOC_SIZE_BIN = 102400 ; // 100kb
2021-10-18 08:07:44 +02:00
const VER = 10 ;
const RECENT_MOFIDIED_DOCS_QTY = 30 ;
2021-11-10 11:07:09 +02:00
const LEAF_WAIT_TIMEOUT = 90000 ; // in synchronization, waiting missing leaf time out.
2021-10-18 08:07:44 +02:00
const LOG_LEVEL = {
VERBOSE : 1 ,
INFO : 10 ,
NOTICE : 100 ,
URGENT : 1000 ,
} as const ;
type LOG_LEVEL = typeof LOG_LEVEL [ keyof typeof LOG_LEVEL ] ;
2021-10-14 12:27:08 +02:00
2021-10-21 11:48:42 +02:00
const VERSIONINFO_DOCID = "obsydian_livesync_version" ;
2021-10-26 11:08:01 +02:00
const MILSTONE_DOCID = "_local/obsydian_livesync_milestone" ;
const NODEINFO_DOCID = "_local/obsydian_livesync_nodeinfo" ;
2021-10-21 11:48:42 +02:00
2021-10-12 16:50:13 +02:00
interface ObsidianLiveSyncSettings {
couchDB_URI : string ;
couchDB_USER : string ;
couchDB_PASSWORD : string ;
2021-11-15 05:25:18 +02:00
couchDB_DBNAME : string ;
2021-10-14 12:27:08 +02:00
liveSync : boolean ;
2021-10-12 16:50:13 +02:00
syncOnSave : boolean ;
syncOnStart : boolean ;
2021-11-10 11:07:09 +02:00
syncOnFileOpen : boolean ;
2021-10-15 05:30:06 +02:00
savingDelay : number ;
lessInformationInLog : boolean ;
2021-10-15 10:58:42 +02:00
gcDelay : number ;
2021-10-17 15:10:03 +02:00
versionUpFlash : string ;
minimumChunkSize : number ;
2021-10-18 08:07:44 +02:00
longLineThreshold : number ;
showVerboseLog : boolean ;
2021-10-19 10:53:54 +02:00
suspendFileWatching : boolean ;
2021-11-04 12:12:43 +02:00
trashInsteadDelete : boolean ;
2021-11-10 11:07:09 +02:00
periodicReplication : boolean ;
periodicReplicationInterval : number ;
2021-11-12 12:11:52 +02:00
encrypt : boolean ;
passphrase : string ;
workingEncrypt : boolean ;
workingPassphrase : string ;
2021-11-15 05:25:18 +02:00
doNotDeleteFolder : boolean ;
2021-10-12 16:50:13 +02:00
}
const DEFAULT_SETTINGS : ObsidianLiveSyncSettings = {
couchDB_URI : "" ,
couchDB_USER : "" ,
couchDB_PASSWORD : "" ,
2021-11-15 05:25:18 +02:00
couchDB_DBNAME : "" ,
2021-10-14 12:27:08 +02:00
liveSync : false ,
2021-10-12 16:50:13 +02:00
syncOnSave : false ,
syncOnStart : false ,
2021-10-15 05:30:06 +02:00
savingDelay : 200 ,
lessInformationInLog : false ,
2021-10-19 10:53:54 +02:00
gcDelay : 300 ,
2021-10-17 15:10:03 +02:00
versionUpFlash : "" ,
minimumChunkSize : 20 ,
longLineThreshold : 250 ,
2021-10-18 08:07:44 +02:00
showVerboseLog : false ,
2021-10-19 10:53:54 +02:00
suspendFileWatching : false ,
2021-11-04 12:12:43 +02:00
trashInsteadDelete : false ,
2021-11-10 11:07:09 +02:00
periodicReplication : false ,
periodicReplicationInterval : 60 ,
syncOnFileOpen : false ,
2021-11-12 12:11:52 +02:00
encrypt : false ,
passphrase : "" ,
workingEncrypt : false ,
workingPassphrase : "" ,
2021-11-15 05:25:18 +02:00
doNotDeleteFolder : false ,
2021-10-12 16:50:13 +02:00
} ;
2021-10-14 12:27:08 +02:00
interface Entry {
2021-10-12 16:50:13 +02:00
_id : string ;
data : string ;
_rev? : string ;
ctime : number ;
mtime : number ;
size : number ;
_deleted? : boolean ;
2021-10-18 08:07:44 +02:00
_conflicts? : string [ ] ;
2021-10-14 12:27:08 +02:00
type ? : "notes" ;
2021-10-12 16:50:13 +02:00
}
2021-10-14 12:27:08 +02:00
interface NewEntry {
_id : string ;
children : string [ ] ;
_rev? : string ;
ctime : number ;
mtime : number ;
size : number ;
_deleted? : boolean ;
2021-10-18 08:07:44 +02:00
_conflicts? : string [ ] ;
2021-10-14 12:27:08 +02:00
NewNote : true ;
type : "newnote" ;
2021-10-12 16:50:13 +02:00
}
2021-10-17 15:10:03 +02:00
interface PlainEntry {
_id : string ;
children : string [ ] ;
_rev? : string ;
ctime : number ;
mtime : number ;
size : number ;
_deleted? : boolean ;
NewNote : true ;
2021-10-18 08:07:44 +02:00
_conflicts? : string [ ] ;
2021-10-17 15:10:03 +02:00
type : "plain" ;
}
2021-10-14 12:27:08 +02:00
type LoadedEntry = Entry & {
children : string [ ] ;
2021-10-18 08:07:44 +02:00
datatype : "plain" | "newnote" ;
2021-10-14 12:27:08 +02:00
} ;
interface EntryLeaf {
_id : string ;
data : string ;
_deleted? : boolean ;
type : "leaf" ;
2021-10-17 15:10:03 +02:00
_rev? : string ;
2021-10-12 16:50:13 +02:00
}
2021-10-15 10:58:42 +02:00
2021-10-21 11:48:42 +02:00
interface EntryVersionInfo {
_id : typeof VERSIONINFO_DOCID ;
_rev? : string ;
type : "versioninfo" ;
version : number ;
_deleted? : boolean ;
}
2021-10-26 11:08:01 +02:00
interface EntryMilestoneInfo {
_id : typeof MILSTONE_DOCID ;
_rev? : string ;
type : "milestoneinfo" ;
_deleted? : boolean ;
created : number ;
accepted_nodes : string [ ] ;
locked : boolean ;
}
interface EntryNodeInfo {
_id : typeof NODEINFO_DOCID ;
_rev? : string ;
_deleted? : boolean ;
type : "nodeinfo" ;
nodeid : string ;
}
2021-10-19 10:53:54 +02:00
type EntryBody = Entry | NewEntry | PlainEntry ;
2021-10-26 11:08:01 +02:00
type EntryDoc = EntryBody | LoadedEntry | EntryLeaf | EntryVersionInfo | EntryMilestoneInfo | EntryNodeInfo ;
2021-10-19 10:53:54 +02:00
2021-10-12 16:50:13 +02:00
type diff_result_leaf = {
rev : string ;
data : string ;
ctime : number ;
mtime : number ;
} ;
type dmp_result = Array < [ number , string ] > ;
type diff_result = {
left : diff_result_leaf ;
right : diff_result_leaf ;
diff : dmp_result ;
} ;
type diff_check_result = boolean | diff_result ;
2021-10-14 12:27:08 +02:00
type Credential = {
username : string ;
password : string ;
} ;
2021-10-18 08:07:44 +02:00
type EntryDocResponse = EntryDoc & PouchDB . Core . IdMeta & PouchDB . Core . GetMeta ;
2021-10-12 16:50:13 +02:00
//-->Functions.
2021-11-10 11:07:09 +02:00
function arrayBufferToBase64Old ( buffer : ArrayBuffer ) {
2021-10-12 16:50:13 +02:00
var binary = "" ;
var bytes = new Uint8Array ( buffer ) ;
var len = bytes . byteLength ;
for ( var i = 0 ; i < len ; i ++ ) {
binary += String . fromCharCode ( bytes [ i ] ) ;
}
return window . btoa ( binary ) ;
}
2021-11-10 11:07:09 +02:00
// Ten times faster.
function arrayBufferToBase64 ( buffer : ArrayBuffer ) : Promise < string > {
return new Promise ( ( res ) = > {
var blob = new Blob ( [ buffer ] , { type : "application/octet-binary" } ) ;
var reader = new FileReader ( ) ;
reader . onload = function ( evt ) {
var dataurl = evt . target . result . toString ( ) ;
res ( dataurl . substr ( dataurl . indexOf ( "," ) + 1 ) ) ;
} ;
reader . readAsDataURL ( blob ) ;
} ) ;
}
2021-10-12 16:50:13 +02:00
function base64ToArrayBuffer ( base64 : string ) : ArrayBuffer {
try {
var binary_string = window . atob ( base64 ) ;
var len = binary_string . length ;
var bytes = new Uint8Array ( len ) ;
for ( var i = 0 ; i < len ; i ++ ) {
bytes [ i ] = binary_string . charCodeAt ( i ) ;
}
return bytes . buffer ;
} catch ( ex ) {
2021-10-21 11:48:42 +02:00
try {
return new Uint16Array (
[ ] . map . call ( base64 , function ( c : string ) {
return c . charCodeAt ( 0 ) ;
} )
) . buffer ;
} catch ( ex2 ) {
return null ;
}
2021-10-12 16:50:13 +02:00
}
}
function base64ToString ( base64 : string ) : string {
try {
var binary_string = window . atob ( base64 ) ;
var len = binary_string . length ;
var bytes = new Uint8Array ( len ) ;
for ( var i = 0 ; i < len ; i ++ ) {
bytes [ i ] = binary_string . charCodeAt ( i ) ;
}
return new TextDecoder ( ) . decode ( bytes ) ;
} catch ( ex ) {
2021-10-20 11:43:53 +02:00
return base64 ;
2021-10-12 16:50:13 +02:00
}
}
const escapeStringToHTML = ( str : string ) = > {
if ( ! str ) return ;
return str . replace ( /[<>&"'`]/g , ( match ) = > {
const escape : any = {
"<" : "<" ,
">" : ">" ,
"&" : "&" ,
'"' : """ ,
"'" : "'" ,
"`" : "`" ,
} ;
return escape [ match ] ;
} ) ;
} ;
2021-10-26 11:08:01 +02:00
function resolveWithIgnoreKnownError < T > ( p : Promise < T > , def : T ) : Promise < T > {
return new Promise ( ( res , rej ) = > {
p . then ( res ) . catch ( ( ex ) = > ( ex . status && ex . status == 404 ? res ( def ) : rej ( ex ) ) ) ;
} ) ;
}
2021-10-13 14:38:44 +02:00
const isValidRemoteCouchDBURI = ( uri : string ) : boolean = > {
if ( uri . startsWith ( "https://" ) ) return true ;
if ( uri . startsWith ( "http://" ) ) return true ;
return false ;
} ;
const connectRemoteCouchDB = async ( uri : string , auth : { username : string ; password : string } ) : Promise < false | { db : PouchDB.Database ; info : any } > = > {
2021-11-08 10:52:07 +02:00
if ( ! isValidRemoteCouchDBURI ( uri ) ) return false ;
2021-10-13 14:38:44 +02:00
let db = new PouchDB ( uri , {
auth ,
} ) ;
try {
let info = await db . info ( ) ;
return { db : db , info : info } ;
} catch ( ex ) {
2021-11-08 10:52:07 +02:00
Logger ( ex , LOG_LEVEL . VERBOSE ) ;
2021-10-19 10:53:54 +02:00
return false ;
2021-10-13 14:38:44 +02:00
}
} ;
2021-10-21 11:48:42 +02:00
// check the version of remote.
// if remote is higher than current(or specified) version, return false.
const checkRemoteVersion = async ( db : PouchDB.Database , migrate : ( from : number , to : number ) = > Promise < boolean > , barrier : number = VER ) : Promise < boolean > = > {
try {
let versionInfo = ( await db . get ( VERSIONINFO_DOCID ) ) as EntryVersionInfo ;
if ( versionInfo . type != "versioninfo" ) {
return false ;
}
2021-10-14 12:27:08 +02:00
2021-10-21 11:48:42 +02:00
let version = versionInfo . version ;
if ( version < barrier ) {
try {
let versionUpResult = await migrate ( version , barrier ) ;
if ( versionUpResult ) {
await bumpRemoteVersion ( db ) ;
return true ;
}
} catch ( ex ) {
throw ex ;
}
}
if ( version == barrier ) return true ;
return false ;
} catch ( ex ) {
if ( ex . status && ex . status == 404 ) {
if ( await bumpRemoteVersion ( db ) ) {
return true ;
}
return false ;
}
throw ex ;
}
} ;
const bumpRemoteVersion = async ( db : PouchDB.Database , barrier : number = VER ) : Promise < boolean > = > {
let vi : EntryVersionInfo = {
_id : VERSIONINFO_DOCID ,
version : barrier ,
type : "versioninfo" ,
} ;
2021-10-26 11:08:01 +02:00
let versionInfo = ( await resolveWithIgnoreKnownError ( db . get ( VERSIONINFO_DOCID ) , vi ) ) as EntryVersionInfo ;
if ( versionInfo . type != "versioninfo" ) {
return false ;
2021-10-21 11:48:42 +02:00
}
2021-10-26 11:08:01 +02:00
vi . _rev = versionInfo . _rev ;
2021-10-21 11:48:42 +02:00
await db . put ( vi ) ;
return true ;
} ;
2021-10-26 11:08:01 +02:00
function isValidPath ( filename : string ) : boolean {
let regex = /[\u0000-\u001f]|[\\"':?<>|*$]/g ;
let x = filename . replace ( regex , "_" ) ;
let win = /(\\|\/)(COM\d|LPT\d|CON|PRN|AUX|NUL|CLOCK$)($|\.)/gi ;
let sx = ( x = x . replace ( win , "/_" ) ) ;
return sx == filename ;
}
2021-10-21 11:48:42 +02:00
// Default Logger.
let Logger : ( message : any , levlel? : LOG_LEVEL ) = > Promise < void > = async ( message , _ ) = > {
let timestamp = new Date ( ) . toLocaleString ( ) ;
let messagecontent = typeof message == "string" ? message : JSON.stringify ( message , null , 2 ) ;
let newmessage = timestamp + "->" + messagecontent ;
console . log ( newmessage ) ;
} ;
2021-10-12 16:50:13 +02:00
2021-11-04 12:12:43 +02:00
type DatabaseConnectingStatus = "NOT_CONNECTED" | "PAUSED" | "CONNECTED" | "COMPLETED" | "CLOSED" | "ERRORED" ;
2021-11-12 12:11:52 +02:00
// --> Encryption.
//NOTE: I have to split source.
type encodedData = [ encryptedData : string , iv : string , salt : string ] ;
type KeyBuffer = {
index : string ;
key : CryptoKey ;
salt : Uint8Array ;
} ;
let KeyBuffs : KeyBuffer [ ] = [ ] ;
const KEY_RECYCLE_COUNT = 100 ;
let recycleCount = KEY_RECYCLE_COUNT ;
async function getKeyForEncrypt ( passphrase : string ) : Promise < [ CryptoKey , Uint8Array ] > {
// For performance, the plugin reuses the key KEY_RECYCLE_COUNT times.
let f = KeyBuffs . find ( ( e ) = > e . index == passphrase ) ;
if ( f ) {
recycleCount -- ;
if ( recycleCount > 0 ) {
return [ f . key , f . salt ] ;
}
KeyBuffs . remove ( f ) ;
recycleCount = KEY_RECYCLE_COUNT ;
}
let xpassphrase = new TextEncoder ( ) . encode ( passphrase ) ;
let digest = await crypto . subtle . digest ( { name : "SHA-256" } , xpassphrase ) ;
let keyMaterial = await crypto . subtle . importKey ( "raw" , digest , { name : "PBKDF2" } , false , [ "deriveKey" ] ) ;
const salt = crypto . getRandomValues ( new Uint8Array ( 16 ) ) ;
let key = await crypto . subtle . deriveKey (
{
name : "PBKDF2" ,
salt ,
iterations : 100000 ,
hash : "SHA-256" ,
} ,
keyMaterial ,
{ name : "AES-GCM" , length : 256 } ,
false ,
[ "encrypt" ]
) ;
KeyBuffs . push ( {
index : passphrase ,
key ,
salt ,
} ) ;
while ( KeyBuffs . length > 50 ) {
KeyBuffs . shift ( ) ;
}
return [ key , salt ] ;
}
let decKeyBuffs : KeyBuffer [ ] = [ ] ;
async function getKeyForDecryption ( passphrase : string , salt : Uint8Array ) : Promise < [ CryptoKey , Uint8Array ] > {
let bufKey = passphrase + uint8ArrayToHexString ( salt ) ;
let f = decKeyBuffs . find ( ( e ) = > e . index == bufKey ) ;
if ( f ) {
return [ f . key , f . salt ] ;
}
let xpassphrase = new TextEncoder ( ) . encode ( passphrase ) ;
let digest = await crypto . subtle . digest ( { name : "SHA-256" } , xpassphrase ) ;
let keyMaterial = await crypto . subtle . importKey ( "raw" , digest , { name : "PBKDF2" } , false , [ "deriveKey" ] ) ;
let key = await crypto . subtle . deriveKey (
{
name : "PBKDF2" ,
salt ,
iterations : 100000 ,
hash : "SHA-256" ,
} ,
keyMaterial ,
{ name : "AES-GCM" , length : 256 } ,
false ,
[ "decrypt" ]
) ;
decKeyBuffs . push ( {
index : bufKey ,
key ,
salt ,
} ) ;
while ( decKeyBuffs . length > 50 ) {
decKeyBuffs . shift ( ) ;
}
return [ key , salt ] ;
}
let semiStaticFieldBuffer : Uint8Array = null ;
function getSemiStaticField ( reset? : boolean ) {
// return fixed field of iv.
if ( semiStaticFieldBuffer != null && ! reset ) {
return semiStaticFieldBuffer ;
}
semiStaticFieldBuffer = crypto . getRandomValues ( new Uint8Array ( 12 ) ) ;
return semiStaticFieldBuffer ;
}
let nonceBuffer : Uint32Array = new Uint32Array ( 1 ) ;
function getNonce() {
// This is nonce, so do not send same thing.
nonceBuffer [ 0 ] ++ ;
if ( nonceBuffer [ 0 ] > 10000 ) {
// reset semi-static field.
getSemiStaticField ( true ) ;
}
return nonceBuffer ;
}
function uint8ArrayToHexString ( src : Uint8Array ) : string {
return Array . from ( src )
. map ( ( e : number ) : string = > ` 00 ${ e . toString ( 16 ) } ` . slice ( - 2 ) )
. join ( "" ) ;
}
function hexStringToUint8Array ( src : string ) : Uint8Array {
const srcArr = [ . . . src ] ;
const arr = srcArr . reduce ( ( acc , _ , i ) = > ( i % 2 ? acc : [ . . . acc , srcArr . slice ( i , i + 2 ) . join ( "" ) ] ) , [ ] ) . map ( ( e ) = > parseInt ( e , 16 ) ) ;
return Uint8Array . from ( arr ) ;
}
async function encrypt ( input : string , passphrase : string ) {
let key : CryptoKey ;
let salt : Uint8Array ;
[ key , salt ] = await getKeyForEncrypt ( passphrase ) ;
// Create initial vector with semifixed part and incremental part
// I think it's not good against related-key attacks.
const fixedPart = getSemiStaticField ( ) ;
const invocationPart = getNonce ( ) ;
const iv = Uint8Array . from ( [ . . . fixedPart , . . . new Uint8Array ( invocationPart . buffer ) ] ) ;
const plainStringified : string = JSON . stringify ( input ) ;
const plainStringBuffer : Uint8Array = new TextEncoder ( ) . encode ( plainStringified ) ;
const encryptedDataArrayBuffer = await crypto . subtle . encrypt ( { name : "AES-GCM" , iv } , key , plainStringBuffer ) ;
const encryptedData = window . btoa ( Array . from ( new Uint8Array ( encryptedDataArrayBuffer ) , ( char ) = > String . fromCharCode ( char ) ) . join ( "" ) ) ;
//return data with iv and salt.
const response : encodedData = [ encryptedData , uint8ArrayToHexString ( iv ) , uint8ArrayToHexString ( salt ) ] ;
const ret = JSON . stringify ( response ) ;
return ret ;
}
async function decrypt ( encryptedResult : string , passphrase : string ) : Promise < string > {
try {
let [ encryptedData , ivString , salt ] : encodedData = JSON . parse ( encryptedResult ) ;
let [ key , _ ] = await getKeyForDecryption ( passphrase , hexStringToUint8Array ( salt ) ) ;
let iv = hexStringToUint8Array ( ivString ) ;
// decode base 64, it should increase speed and i should with in MAX_DOC_SIZE_BIN, so it won't OOM.
let encryptedDataBin = window . atob ( encryptedData ) ;
let encryptedDataArrayBuffer = Uint8Array . from ( encryptedDataBin . split ( "" ) , ( char ) = > char . charCodeAt ( 0 ) ) ;
let plainStringBuffer : ArrayBuffer = await crypto . subtle . decrypt ( { name : "AES-GCM" , iv } , key , encryptedDataArrayBuffer ) ;
let plainStringified = new TextDecoder ( ) . decode ( plainStringBuffer ) ;
let plain = JSON . parse ( plainStringified ) ;
return plain ;
} catch ( ex ) {
Logger ( "Couldn't decode! You should wrong the passphrases" , LOG_LEVEL . VERBOSE ) ;
Logger ( ex , LOG_LEVEL . VERBOSE ) ;
throw ex ;
}
}
async function testCrypt() {
let src = "supercalifragilisticexpialidocious" ;
let encoded = await encrypt ( src , "passwordTest" ) ;
let decrypted = await decrypt ( encoded , "passwordTest" ) ;
if ( src != decrypted ) {
Logger ( "WARNING! Your device would not support encryption." , LOG_LEVEL . VERBOSE ) ;
return false ;
} else {
Logger ( "CRYPT LOGIC OK" , LOG_LEVEL . VERBOSE ) ;
return true ;
}
}
// <-- Encryption
2021-10-21 11:48:42 +02:00
//<--Functions
2021-10-14 12:27:08 +02:00
class LocalPouchDB {
auth : Credential ;
dbname : string ;
2021-10-21 11:48:42 +02:00
settings : ObsidianLiveSyncSettings ;
2021-10-14 12:27:08 +02:00
localDatabase : PouchDB.Database < EntryDoc > ;
2021-10-26 11:08:01 +02:00
nodeid : string = "" ;
2021-11-10 11:07:09 +02:00
isReady : boolean = false ;
2021-10-14 12:27:08 +02:00
2021-10-17 15:10:03 +02:00
recentModifiedDocs : string [ ] = [ ] ;
2021-10-15 10:58:42 +02:00
h32 : ( input : string , seed? : number ) = > string ;
h64 : ( input : string , seedHigh? : number , seedLow? : number ) = > string ;
2021-11-12 12:11:52 +02:00
h32Raw : ( input : Uint8Array , seed? : number ) = > number ;
2021-10-18 08:07:44 +02:00
hashCache : {
[ key : string ] : string ;
} = { } ;
hashCacheRev : {
[ key : string ] : string ;
} = { } ;
2021-10-19 10:53:54 +02:00
corruptedEntries : { [ key : string ] : EntryDoc } = { } ;
2021-10-26 11:08:01 +02:00
remoteLocked = false ;
remoteLockedAndDeviceNotAccepted = false ;
2021-10-19 10:53:54 +02:00
2021-10-21 11:48:42 +02:00
constructor ( settings : ObsidianLiveSyncSettings , dbname : string ) {
2021-10-14 12:27:08 +02:00
this . auth = {
username : "" ,
password : "" ,
} ;
this . dbname = dbname ;
2021-10-21 11:48:42 +02:00
this . settings = settings ;
2021-10-14 12:27:08 +02:00
2021-11-10 11:07:09 +02:00
// this.initializeDatabase();
2021-10-14 12:27:08 +02:00
}
close() {
2021-11-10 11:07:09 +02:00
this . isReady = false ;
if ( this . changeHandler != null ) {
this . changeHandler . cancel ( ) ;
this . changeHandler . removeAllListeners ( ) ;
}
if ( this . localDatabase != null ) {
this . localDatabase . close ( ) ;
}
2021-10-14 12:27:08 +02:00
}
status() {
if ( this . syncHandler == null ) {
return "connected" ;
}
return "disabled" ;
}
2021-10-18 08:07:44 +02:00
disposeHashCache() {
this . hashCache = { } ;
this . hashCacheRev = { } ;
}
updateRecentModifiedDocs ( id : string , rev : string , deleted : boolean ) {
2021-10-17 15:10:03 +02:00
let idrev = id + rev ;
2021-10-18 08:07:44 +02:00
if ( deleted ) {
this . recentModifiedDocs = this . recentModifiedDocs . filter ( ( e ) = > e != idrev ) ;
} else {
this . recentModifiedDocs . push ( idrev ) ;
this . recentModifiedDocs = this . recentModifiedDocs . slice ( 0 - RECENT_MOFIDIED_DOCS_QTY ) ;
2021-10-17 15:10:03 +02:00
}
}
isSelfModified ( id : string , rev : string ) : boolean {
let idrev = id + rev ;
return this . recentModifiedDocs . indexOf ( idrev ) !== - 1 ;
}
2021-10-21 11:48:42 +02:00
2021-10-19 10:53:54 +02:00
changeHandler : PouchDB.Core.Changes < { } > = null ;
2021-10-15 10:58:42 +02:00
async initializeDatabase() {
2021-11-10 11:07:09 +02:00
await this . prepareHashFunctions ( ) ;
2021-10-14 12:27:08 +02:00
if ( this . localDatabase != null ) this . localDatabase . close ( ) ;
2021-10-19 10:53:54 +02:00
if ( this . changeHandler != null ) {
this . changeHandler . cancel ( ) ;
}
2021-10-14 12:27:08 +02:00
this . localDatabase = null ;
this . localDatabase = new PouchDB < EntryDoc > ( this . dbname + "-livesync" , {
auto_compaction : true ,
revs_limit : 100 ,
deterministic_revs : true ,
} ) ;
2021-11-10 11:07:09 +02:00
Logger ( "Database Info" ) ;
Logger ( await this . localDatabase . info ( ) , LOG_LEVEL . VERBOSE ) ;
2021-10-26 11:08:01 +02:00
// initialize local node information.
let nodeinfo : EntryNodeInfo = await resolveWithIgnoreKnownError < EntryNodeInfo > ( this . localDatabase . get ( NODEINFO_DOCID ) , {
_id : NODEINFO_DOCID ,
type : "nodeinfo" ,
nodeid : "" ,
} ) ;
if ( nodeinfo . nodeid == "" ) {
nodeinfo . nodeid = Math . random ( ) . toString ( 36 ) . slice ( - 10 ) ;
await this . localDatabase . put ( nodeinfo ) ;
}
2021-11-10 11:07:09 +02:00
this . localDatabase . on ( "close" , ( ) = > {
this . isReady = false ;
} ) ;
2021-10-26 11:08:01 +02:00
this . nodeid = nodeinfo . nodeid ;
2021-10-19 10:53:54 +02:00
// Traceing the leaf id
let changes = this . localDatabase
. changes ( {
since : "now" ,
live : true ,
filter : ( doc ) = > doc . type == "leaf" ,
} )
. on ( "change" , ( e ) = > {
if ( e . deleted ) return ;
this . leafArrived ( e . id ) ;
2021-11-04 12:12:43 +02:00
this . docSeq = ` ${ e . seq } ` ;
2021-10-19 10:53:54 +02:00
} ) ;
this . changeHandler = changes ;
2021-11-10 11:07:09 +02:00
this . isReady = true ;
2021-10-15 10:58:42 +02:00
}
2021-10-19 10:53:54 +02:00
2021-10-18 08:07:44 +02:00
async prepareHashFunctions() {
2021-10-15 10:58:42 +02:00
if ( this . h32 != null ) return ;
2021-11-12 12:11:52 +02:00
const { h32 , h64 , h32Raw } = await xxhash ( ) ;
2021-10-15 10:58:42 +02:00
this . h32 = h32 ;
this . h64 = h64 ;
2021-11-12 12:11:52 +02:00
this . h32Raw = h32Raw ;
2021-10-14 12:27:08 +02:00
}
2021-10-19 10:53:54 +02:00
// leaf waiting
leafArrivedCallbacks : { [ key : string ] : ( ( ) = > void ) [ ] } = { } ;
leafArrived ( id : string ) {
if ( typeof this . leafArrivedCallbacks [ id ] !== "undefined" ) {
for ( let func of this . leafArrivedCallbacks [ id ] ) {
func ( ) ;
}
delete this . leafArrivedCallbacks [ id ] ;
}
}
// wait
waitForLeafReady ( id : string ) : Promise < boolean > {
2021-10-20 16:03:07 +02:00
return new Promise ( ( res , rej ) = > {
2021-10-19 10:53:54 +02:00
// Set timeout.
2021-10-20 16:03:07 +02:00
let timer = setTimeout ( ( ) = > rej ( false ) , LEAF_WAIT_TIMEOUT ) ;
2021-10-19 10:53:54 +02:00
if ( typeof this . leafArrivedCallbacks [ id ] == "undefined" ) {
this . leafArrivedCallbacks [ id ] = [ ] ;
}
this . leafArrivedCallbacks [ id ] . push ( ( ) = > {
clearTimeout ( timer ) ;
res ( true ) ;
} ) ;
} ) ;
}
async getDBLeaf ( id : string ) : Promise < string > {
// when in cache, use that.
if ( this . hashCacheRev [ id ] ) {
return this . hashCacheRev [ id ] ;
}
try {
let w = await this . localDatabase . get ( id ) ;
if ( w . type == "leaf" ) {
2021-11-12 12:11:52 +02:00
if ( id . startsWith ( "h:+" ) ) {
try {
w . data = await decrypt ( w . data , this . settings . passphrase ) ;
} catch ( e ) {
Logger ( "The element of the document has been encrypted, but decryption failed." , LOG_LEVEL . NOTICE ) ;
throw e ;
}
}
2021-10-19 10:53:54 +02:00
this . hashCache [ w . data ] = id ;
this . hashCacheRev [ id ] = w . data ;
return w . data ;
}
throw new Error ( ` retrive leaf, but it was not leaf. ` ) ;
} catch ( ex ) {
if ( ex . status && ex . status == 404 ) {
// just leaf is not ready.
// wait for on
if ( ( await this . waitForLeafReady ( id ) ) === false ) {
throw new Error ( ` time out (waiting leaf) ` ) ;
}
try {
// retrive again.
let w = await this . localDatabase . get ( id ) ;
if ( w . type == "leaf" ) {
2021-11-12 12:11:52 +02:00
if ( id . startsWith ( "h:+" ) ) {
try {
w . data = await decrypt ( w . data , this . settings . passphrase ) ;
} catch ( e ) {
Logger ( "The element of the document has been encrypted, but decryption failed." , LOG_LEVEL . NOTICE ) ;
throw e ;
}
}
2021-10-19 10:53:54 +02:00
this . hashCache [ w . data ] = id ;
this . hashCacheRev [ id ] = w . data ;
return w . data ;
}
throw new Error ( ` retrive leaf, but it was not leaf. ` ) ;
} catch ( ex ) {
if ( ex . status && ex . status == 404 ) {
throw new Error ( "leaf is not found" ) ;
}
2021-10-21 11:48:42 +02:00
Logger ( ` Something went wrong on retriving leaf ` ) ;
2021-10-19 10:53:54 +02:00
throw ex ;
}
} else {
2021-10-21 11:48:42 +02:00
Logger ( ` Something went wrong on retriving leaf ` ) ;
2021-10-19 10:53:54 +02:00
throw ex ;
}
}
}
2021-11-04 12:12:43 +02:00
async getDBEntryMeta ( id : string , opt? : PouchDB.Core.GetOptions ) : Promise < false | LoadedEntry > {
try {
let obj : EntryDocResponse = null ;
if ( opt ) {
obj = await this . localDatabase . get ( id , opt ) ;
} else {
obj = await this . localDatabase . get ( id ) ;
}
if ( obj . type && obj . type == "leaf" ) {
//do nothing for leaf;
return false ;
}
// retrieve metadata only
if ( ! obj . type || ( obj . type && obj . type == "notes" ) || obj . type == "newnote" || obj . type == "plain" ) {
let note = obj as Entry ;
let doc : LoadedEntry & PouchDB . Core . IdMeta & PouchDB . Core . GetMeta = {
data : "" ,
_id : note._id ,
ctime : note.ctime ,
mtime : note.mtime ,
size : note.size ,
_deleted : obj._deleted ,
_rev : obj._rev ,
_conflicts : obj._conflicts ,
children : [ ] ,
datatype : "newnote" ,
} ;
2021-11-10 11:07:09 +02:00
return doc ;
2021-11-04 12:12:43 +02:00
}
} catch ( ex ) {
if ( ex . status && ex . status == 404 ) {
return false ;
}
throw ex ;
}
return false ;
}
2021-11-10 11:07:09 +02:00
async getDBEntry ( id : string , opt? : PouchDB.Core.GetOptions , dump = false ) : Promise < false | LoadedEntry > {
2021-10-14 12:27:08 +02:00
try {
2021-10-18 08:07:44 +02:00
let obj : EntryDocResponse = null ;
2021-10-14 12:27:08 +02:00
if ( opt ) {
obj = await this . localDatabase . get ( id , opt ) ;
} else {
obj = await this . localDatabase . get ( id ) ;
}
if ( obj . type && obj . type == "leaf" ) {
//do nothing for leaf;
return false ;
}
//Check it out and fix docs to regular case
if ( ! obj . type || ( obj . type && obj . type == "notes" ) ) {
let note = obj as Entry ;
2021-10-18 08:07:44 +02:00
let doc : LoadedEntry & PouchDB . Core . IdMeta & PouchDB . Core . GetMeta = {
2021-10-14 12:27:08 +02:00
data : note.data ,
_id : note._id ,
ctime : note.ctime ,
mtime : note.mtime ,
size : note.size ,
_deleted : obj._deleted ,
_rev : obj._rev ,
2021-10-18 08:07:44 +02:00
_conflicts : obj._conflicts ,
2021-10-14 12:27:08 +02:00
children : [ ] ,
2021-10-17 15:10:03 +02:00
datatype : "newnote" ,
2021-10-14 12:27:08 +02:00
} ;
2021-10-19 10:53:54 +02:00
if ( typeof this . corruptedEntries [ doc . _id ] != "undefined" ) {
delete this . corruptedEntries [ doc . _id ] ;
}
2021-11-10 11:07:09 +02:00
if ( dump ) {
Logger ( ` Simple doc ` ) ;
Logger ( doc ) ;
}
2021-10-14 12:27:08 +02:00
return doc ;
// simple note
}
2021-10-17 15:10:03 +02:00
if ( obj . type == "newnote" || obj . type == "plain" ) {
2021-10-14 12:27:08 +02:00
// search childrens
try {
2021-11-10 11:07:09 +02:00
if ( dump ) {
Logger ( ` Enhanced doc ` ) ;
Logger ( obj ) ;
}
2021-11-12 12:11:52 +02:00
let childrens : string [ ] ;
2021-10-19 10:53:54 +02:00
try {
childrens = await Promise . all ( obj . children . map ( ( e ) = > this . getDBLeaf ( e ) ) ) ;
2021-11-10 11:07:09 +02:00
if ( dump ) {
Logger ( ` childrens: ` ) ;
Logger ( childrens ) ;
}
2021-10-19 10:53:54 +02:00
} catch ( ex ) {
2021-10-21 11:48:42 +02:00
Logger ( ` Something went wrong on reading elements of ${ obj . _id } from database. ` , LOG_LEVEL . NOTICE ) ;
2021-10-19 10:53:54 +02:00
this . corruptedEntries [ obj . _id ] = obj ;
return false ;
2021-10-14 12:27:08 +02:00
}
2021-10-15 10:58:42 +02:00
let data = childrens . join ( "" ) ;
2021-10-18 08:07:44 +02:00
let doc : LoadedEntry & PouchDB . Core . IdMeta & PouchDB . Core . GetMeta = {
2021-10-14 12:27:08 +02:00
data : data ,
_id : obj._id ,
ctime : obj.ctime ,
mtime : obj.mtime ,
size : obj.size ,
_deleted : obj._deleted ,
_rev : obj._rev ,
children : obj.children ,
2021-10-18 08:07:44 +02:00
datatype : obj.type ,
_conflicts : obj._conflicts ,
2021-10-14 12:27:08 +02:00
} ;
2021-11-10 11:07:09 +02:00
if ( dump ) {
Logger ( ` therefore: ` ) ;
Logger ( doc ) ;
}
2021-10-19 10:53:54 +02:00
if ( typeof this . corruptedEntries [ doc . _id ] != "undefined" ) {
delete this . corruptedEntries [ doc . _id ] ;
}
2021-10-14 12:27:08 +02:00
return doc ;
} catch ( ex ) {
if ( ex . status && ex . status == 404 ) {
2021-10-21 11:48:42 +02:00
Logger ( ` Missing document content!, could not read ${ obj . _id } from database. ` , LOG_LEVEL . NOTICE ) ;
2021-10-15 10:58:42 +02:00
return false ;
2021-10-14 12:27:08 +02:00
}
2021-10-21 11:48:42 +02:00
Logger ( ` Something went wrong on reading ${ obj . _id } from database. ` , LOG_LEVEL . NOTICE ) ;
Logger ( ex ) ;
2021-10-14 12:27:08 +02:00
}
}
} catch ( ex ) {
if ( ex . status && ex . status == 404 ) {
return false ;
}
throw ex ;
}
return false ;
}
2021-10-18 08:07:44 +02:00
async deleteDBEntry ( id : string , opt? : PouchDB.Core.GetOptions ) : Promise < boolean > {
2021-10-14 12:27:08 +02:00
try {
2021-10-18 08:07:44 +02:00
let obj : EntryDocResponse = null ;
2021-10-14 12:27:08 +02:00
if ( opt ) {
obj = await this . localDatabase . get ( id , opt ) ;
} else {
obj = await this . localDatabase . get ( id ) ;
}
2021-10-14 18:26:15 +02:00
2021-10-14 12:27:08 +02:00
if ( obj . type && obj . type == "leaf" ) {
//do nothing for leaf;
return false ;
}
//Check it out and fix docs to regular case
if ( ! obj . type || ( obj . type && obj . type == "notes" ) ) {
2021-10-15 05:30:06 +02:00
obj . _deleted = true ;
2021-10-14 18:26:15 +02:00
let r = await this . localDatabase . put ( obj ) ;
2021-10-18 08:07:44 +02:00
this . updateRecentModifiedDocs ( r . id , r . rev , true ) ;
2021-10-19 10:53:54 +02:00
if ( typeof this . corruptedEntries [ obj . _id ] != "undefined" ) {
delete this . corruptedEntries [ obj . _id ] ;
}
2021-10-14 12:27:08 +02:00
return true ;
// simple note
}
2021-10-17 15:10:03 +02:00
if ( obj . type == "newnote" || obj . type == "plain" ) {
2021-10-14 12:27:08 +02:00
obj . _deleted = true ;
2021-10-17 15:10:03 +02:00
let r = await this . localDatabase . put ( obj ) ;
2021-10-21 11:48:42 +02:00
Logger ( ` entry removed: ${ obj . _id } - ${ r . rev } ` ) ;
2021-10-18 08:07:44 +02:00
this . updateRecentModifiedDocs ( r . id , r . rev , true ) ;
2021-10-19 10:53:54 +02:00
if ( typeof this . corruptedEntries [ obj . _id ] != "undefined" ) {
delete this . corruptedEntries [ obj . _id ] ;
}
2021-10-14 12:27:08 +02:00
return true ;
}
} catch ( ex ) {
if ( ex . status && ex . status == 404 ) {
return false ;
}
throw ex ;
}
}
2021-10-26 11:08:01 +02:00
async deleteDBEntryPrefix ( prefix : string ) : Promise < boolean > {
// delete database entries by prefix.
// it called from folder deletion.
let c = 0 ;
let readCount = 0 ;
let delDocs : string [ ] = [ ] ;
do {
let result = await this . localDatabase . allDocs ( { include_docs : false , skip : c , limit : 100 , conflicts : true } ) ;
readCount = result . rows . length ;
if ( readCount > 0 ) {
//there are some result
for ( let v of result . rows ) {
// let doc = v.doc;
if ( v . id . startsWith ( prefix ) || v . id . startsWith ( "/" + prefix ) ) {
delDocs . push ( v . id ) ;
2021-11-12 12:11:52 +02:00
// console.log("!" + v.id);
2021-10-26 11:08:01 +02:00
} else {
if ( ! v . id . startsWith ( "h:" ) ) {
2021-11-09 08:05:12 +02:00
// console.log("?" + v.id);
2021-10-26 11:08:01 +02:00
}
}
}
}
c += readCount ;
} while ( readCount != 0 ) ;
// items collected.
//bulk docs to delete?
let deleteCount = 0 ;
let notfound = 0 ;
for ( let v of delDocs ) {
try {
let item = await this . localDatabase . get ( v ) ;
item . _deleted = true ;
await this . localDatabase . put ( item ) ;
this . updateRecentModifiedDocs ( item . _id , item . _rev , true ) ;
deleteCount ++ ;
} catch ( ex ) {
if ( ex . status && ex . status == 404 ) {
notfound ++ ;
// NO OP. It should be timing problem.
} else {
throw ex ;
}
}
}
Logger ( ` deleteDBEntryPrefix:deleted ${ deleteCount } items, skipped ${ notfound } ` ) ;
return true ;
}
2021-11-10 11:07:09 +02:00
isPlainText ( filename : string ) : boolean {
if ( filename . endsWith ( ".md" ) ) return true ;
if ( filename . endsWith ( ".txt" ) ) return true ;
if ( filename . endsWith ( ".svg" ) ) return true ;
if ( filename . endsWith ( ".html" ) ) return true ;
if ( filename . endsWith ( ".csv" ) ) return true ;
if ( filename . endsWith ( ".css" ) ) return true ;
if ( filename . endsWith ( ".js" ) ) return true ;
if ( filename . endsWith ( ".xml" ) ) return true ;
return false ;
}
2021-10-14 12:27:08 +02:00
async putDBEntry ( note : LoadedEntry ) {
let leftData = note . data ;
2021-10-15 10:58:42 +02:00
let savenNotes = [ ] ;
let processed = 0 ;
let made = 0 ;
let skiped = 0 ;
2021-10-17 15:10:03 +02:00
let pieceSize = MAX_DOC_SIZE_BIN ;
let plainSplit = false ;
2021-10-18 08:07:44 +02:00
let cacheUsed = 0 ;
2021-11-12 12:11:52 +02:00
let userpasswordHash = this . h32Raw ( new TextEncoder ( ) . encode ( this . settings . passphrase ) ) ;
2021-11-10 11:07:09 +02:00
if ( this . isPlainText ( note . _id ) ) {
2021-10-17 15:10:03 +02:00
pieceSize = MAX_DOC_SIZE ;
plainSplit = true ;
2021-10-15 10:58:42 +02:00
}
2021-11-10 11:07:09 +02:00
let newLeafs : EntryLeaf [ ] = [ ] ;
2021-10-14 12:27:08 +02:00
do {
2021-10-15 10:58:42 +02:00
// To keep low bandwith and database size,
// Dedup pieces on database.
2021-10-18 08:07:44 +02:00
// from 0.1.10, for best performance. we use markdown delimiters
2021-10-17 15:10:03 +02:00
// 1. \n[^\n]{longLineThreshold}[^\n]*\n -> long sentence shuld break.
// 2. \n\n shold break
// 3. \r\n\r\n should break
// 4. \n# should break.
2021-10-18 08:07:44 +02:00
let cPieceSize = pieceSize ;
2021-10-17 15:10:03 +02:00
if ( plainSplit ) {
2021-11-10 11:07:09 +02:00
let minimumChunkSize = this . settings . minimumChunkSize ;
if ( minimumChunkSize < 10 ) minimumChunkSize = 10 ;
let longLineThreshold = this . settings . longLineThreshold ;
if ( longLineThreshold < 100 ) longLineThreshold = 100 ;
2021-10-17 15:10:03 +02:00
cPieceSize = 0 ;
// lookup for next splittion .
// we're standing on "\n"
// debugger
do {
let n1 = leftData . indexOf ( "\n" , cPieceSize + 1 ) ;
let n2 = leftData . indexOf ( "\n\n" , cPieceSize + 1 ) ;
let n3 = leftData . indexOf ( "\r\n\r\n" , cPieceSize + 1 ) ;
let n4 = leftData . indexOf ( "\n#" , cPieceSize + 1 ) ;
if ( n1 == - 1 && n2 == - 1 && n3 == - 1 && n4 == - 1 ) {
cPieceSize = MAX_DOC_SIZE ;
break ;
}
if ( n1 > longLineThreshold ) {
// long sentence is an established piece
2021-11-15 05:25:18 +02:00
cPieceSize = n1 ;
2021-10-17 15:10:03 +02:00
} else {
// cPieceSize = Math.min.apply([n2, n3, n4].filter((e) => e > 1));
// ^ heavy.
2021-11-15 05:25:18 +02:00
if ( n1 > 0 && cPieceSize < n1 ) cPieceSize = n1 ;
2021-10-17 15:10:03 +02:00
if ( n2 > 0 && cPieceSize < n2 ) cPieceSize = n2 + 1 ;
if ( n3 > 0 && cPieceSize < n3 ) cPieceSize = n3 + 3 ;
2021-11-15 05:25:18 +02:00
// Choose shorter, empty line and \n#
if ( n4 > 0 && cPieceSize > n4 ) cPieceSize = n4 + 0 ;
2021-10-17 15:10:03 +02:00
cPieceSize ++ ;
}
2021-10-18 08:07:44 +02:00
} while ( cPieceSize < minimumChunkSize ) ;
2021-10-17 15:10:03 +02:00
}
2021-11-10 11:07:09 +02:00
// piece size determined.
2021-10-17 15:10:03 +02:00
let piece = leftData . substring ( 0 , cPieceSize ) ;
leftData = leftData . substring ( cPieceSize ) ;
2021-10-15 10:58:42 +02:00
processed ++ ;
2021-10-18 08:07:44 +02:00
let leafid = "" ;
2021-11-10 11:07:09 +02:00
// Get hash of piece.
2021-10-18 08:07:44 +02:00
let hashedPiece : string = "" ;
2021-10-15 10:58:42 +02:00
let hashQ : number = 0 ; // if hash collided, **IF**, count it up.
let tryNextHash = false ;
let needMake = true ;
2021-10-18 08:07:44 +02:00
if ( typeof this . hashCache [ piece ] !== "undefined" ) {
hashedPiece = "" ;
leafid = this . hashCache [ piece ] ;
needMake = false ;
skiped ++ ;
cacheUsed ++ ;
} else {
2021-11-12 12:11:52 +02:00
if ( this . settings . encrypt ) {
// When encryption has been enabled, make hash to be different between each passphrase to avoid inferring password.
hashedPiece = "+" + ( this . h32Raw ( new TextEncoder ( ) . encode ( piece ) ) ^ userpasswordHash ) . toString ( 16 ) ;
} else {
hashedPiece = this . h32 ( piece ) ;
}
2021-10-18 08:07:44 +02:00
leafid = "h:" + hashedPiece ;
do {
let nleafid = leafid ;
try {
nleafid = ` ${ leafid } ${ hashQ } ` ;
let pieceData = await this . localDatabase . get < EntryLeaf > ( nleafid ) ;
2021-11-12 12:11:52 +02:00
//try decode
if ( pieceData . _id . startsWith ( "h:+" ) ) {
try {
pieceData . data = await decrypt ( pieceData . data , this . settings . passphrase ) ;
} catch ( e ) {
Logger ( "Decode failed !" ) ;
throw e ;
}
}
2021-10-18 08:07:44 +02:00
if ( pieceData . type == "leaf" && pieceData . data == piece ) {
leafid = nleafid ;
needMake = false ;
tryNextHash = false ;
this . hashCache [ piece ] = leafid ;
this . hashCacheRev [ leafid ] = piece ;
} else if ( pieceData . type == "leaf" ) {
2021-10-21 11:48:42 +02:00
Logger ( "hash:collision!!" ) ;
2021-10-18 08:07:44 +02:00
hashQ ++ ;
tryNextHash = true ;
} else {
leafid = nleafid ;
tryNextHash = false ;
}
} catch ( ex ) {
if ( ex . status && ex . status == 404 ) {
//not found, we can use it.
leafid = nleafid ;
needMake = true ;
2021-11-12 12:11:52 +02:00
tryNextHash = false ;
2021-10-18 08:07:44 +02:00
} else {
needMake = false ;
2021-11-12 12:11:52 +02:00
tryNextHash = false ;
2021-10-18 08:07:44 +02:00
throw ex ;
}
2021-10-15 10:58:42 +02:00
}
2021-10-18 08:07:44 +02:00
} while ( tryNextHash ) ;
if ( needMake ) {
//have to make
2021-11-12 12:11:52 +02:00
let savePiece = piece ;
if ( this . settings . encrypt ) {
let passphrase = this . settings . passphrase ;
savePiece = await encrypt ( piece , passphrase ) ;
}
2021-10-18 08:07:44 +02:00
let d : EntryLeaf = {
_id : leafid ,
2021-11-12 12:11:52 +02:00
data : savePiece ,
2021-10-18 08:07:44 +02:00
type : "leaf" ,
} ;
2021-11-10 11:07:09 +02:00
newLeafs . push ( d ) ;
this . hashCache [ piece ] = leafid ;
this . hashCacheRev [ leafid ] = piece ;
made ++ ;
2021-10-15 10:58:42 +02:00
} else {
2021-10-18 08:07:44 +02:00
skiped ++ ;
2021-10-15 10:58:42 +02:00
}
}
2021-10-14 12:27:08 +02:00
savenNotes . push ( leafid ) ;
} while ( leftData != "" ) ;
2021-11-10 11:07:09 +02:00
let saved = true ;
if ( newLeafs . length > 0 ) {
try {
let result = await this . localDatabase . bulkDocs ( newLeafs ) ;
for ( let item of result ) {
if ( ( item as any ) . ok ) {
this . updateRecentModifiedDocs ( item . id , item . rev , false ) ;
Logger ( ` save ok:id: ${ item . id } rev: ${ item . rev } ` , LOG_LEVEL . VERBOSE ) ;
} else {
2021-11-12 12:11:52 +02:00
if ( ( item as any ) . status && ( item as any ) . status == 409 ) {
// conflicted, but it would be ok in childrens.
} else {
Logger ( ` save failed:id: ${ item . id } rev: ${ item . rev } ` , LOG_LEVEL . NOTICE ) ;
Logger ( item ) ;
this . disposeHashCache ( ) ;
saved = false ;
}
2021-11-10 11:07:09 +02:00
}
}
} catch ( ex ) {
Logger ( "ERROR ON SAVING LEAVES " ) ;
Logger ( ex ) ;
saved = false ;
2021-10-14 12:27:08 +02:00
}
}
2021-11-10 11:07:09 +02:00
if ( saved ) {
Logger ( ` note content saven, pieces: ${ processed } new: ${ made } , skip: ${ skiped } , cache: ${ cacheUsed } ` ) ;
let newDoc : PlainEntry | NewEntry = {
NewNote : true ,
children : savenNotes ,
_id : note._id ,
ctime : note.ctime ,
mtime : note.mtime ,
size : note.size ,
type : plainSplit ? "plain" : "newnote" ,
} ;
// Here for upsert logic,
try {
let old = await this . localDatabase . get ( newDoc . _id ) ;
if ( ! old . type || old . type == "notes" || old . type == "newnote" || old . type == "plain" ) {
// simple use rev for new doc
newDoc . _rev = old . _rev ;
}
} catch ( ex ) {
if ( ex . status && ex . status == 404 ) {
// NO OP/
} else {
throw ex ;
}
}
let r = await this . localDatabase . put ( newDoc ) ;
this . updateRecentModifiedDocs ( r . id , r . rev , newDoc . _deleted ) ;
if ( typeof this . corruptedEntries [ note . _id ] != "undefined" ) {
delete this . corruptedEntries [ note . _id ] ;
}
Logger ( ` note saven: ${ newDoc . _id } : ${ r . rev } ` ) ;
} else {
Logger ( ` note coud not saved: ${ note . _id } ` ) ;
2021-10-19 10:53:54 +02:00
}
2021-10-14 12:27:08 +02:00
}
syncHandler : PouchDB.Replication.Sync < { } > = null ;
2021-11-04 12:12:43 +02:00
syncStatus : DatabaseConnectingStatus = "NOT_CONNECTED" ;
docArrived : number = 0 ;
docSent : number = 0 ;
docSeq : string = "" ;
updateInfo : ( ) = > void = ( ) = > {
console . log ( "default updinfo" ) ;
} ;
2021-10-21 11:48:42 +02:00
async migrate ( from : number , to : number ) : Promise < boolean > {
Logger ( ` Database updated from ${ from } to ${ to } ` , LOG_LEVEL . NOTICE ) ;
// no op now,
return true ;
}
2021-11-12 12:11:52 +02:00
replicateAllToServer ( setting : ObsidianLiveSyncSettings , showingNotice? : boolean ) {
2021-10-26 11:08:01 +02:00
return new Promise ( async ( res , rej ) = > {
this . closeReplication ( ) ;
Logger ( "send all data to server" , LOG_LEVEL . NOTICE ) ;
2021-11-12 12:11:52 +02:00
let notice : Notice = null ;
if ( showingNotice ) {
notice = new Notice ( "Initializing" , 0 ) ;
}
2021-11-04 12:12:43 +02:00
this . syncStatus = "CLOSED" ;
this . updateInfo ( ) ;
2021-11-15 05:25:18 +02:00
let uri = setting . couchDB_URI + ( setting . couchDB_DBNAME == "" ? "" : "/" + setting . couchDB_DBNAME ) ;
2021-10-26 11:08:01 +02:00
let auth : Credential = {
username : setting.couchDB_USER ,
password : setting.couchDB_PASSWORD ,
} ;
let dbret = await connectRemoteCouchDB ( uri , auth ) ;
if ( dbret === false ) {
Logger ( ` could not connect to ${ uri } ` , LOG_LEVEL . NOTICE ) ;
2021-11-12 12:11:52 +02:00
if ( notice != null ) notice . hide ( ) ;
2021-10-26 11:08:01 +02:00
return rej ( ` could not connect to ${ uri } ` ) ;
}
let syncOptionBase : PouchDB.Replication.SyncOptions = {
batch_size : 250 ,
batches_limit : 40 ,
} ;
let db = dbret . db ;
2021-11-12 12:11:52 +02:00
let totalCount = ( await this . localDatabase . info ( ) ) . doc_count ;
2021-10-26 11:08:01 +02:00
//replicate once
let replicate = this . localDatabase . replicate . to ( db , syncOptionBase ) ;
replicate
2021-11-04 12:12:43 +02:00
. on ( "active" , ( ) = > {
this . syncStatus = "CONNECTED" ;
this . updateInfo ( ) ;
2021-11-12 12:11:52 +02:00
if ( notice ) {
notice . setMessage ( "CONNECTED" ) ;
}
2021-11-04 12:12:43 +02:00
} )
2021-10-26 11:08:01 +02:00
. on ( "change" , async ( e ) = > {
// no op.
2021-11-12 12:11:52 +02:00
this . docSent += e . docs . length ;
2021-11-04 12:12:43 +02:00
this . updateInfo ( ) ;
2021-11-12 12:11:52 +02:00
notice . setMessage ( ` SENDING: ${ e . docs_written } / ${ totalCount } ` ) ;
2021-11-10 11:07:09 +02:00
Logger ( ` replicateAllToServer: sending..: ${ e . docs . length } ` ) ;
2021-10-26 11:08:01 +02:00
} )
. on ( "complete" , async ( info ) = > {
2021-11-04 12:12:43 +02:00
this . syncStatus = "COMPLETED" ;
this . updateInfo ( ) ;
2021-11-10 11:07:09 +02:00
Logger ( "replicateAllToServer: Completed" , LOG_LEVEL . NOTICE ) ;
2021-10-26 11:08:01 +02:00
replicate . cancel ( ) ;
replicate . removeAllListeners ( ) ;
2021-11-12 12:11:52 +02:00
if ( notice != null ) notice . hide ( ) ;
2021-10-26 11:08:01 +02:00
res ( true ) ;
} )
. on ( "error" , ( e ) = > {
2021-11-04 12:12:43 +02:00
this . syncStatus = "ERRORED" ;
this . updateInfo ( ) ;
2021-11-10 11:07:09 +02:00
Logger ( "replicateAllToServer: Pulling Replication error" , LOG_LEVEL . INFO ) ;
2021-10-26 11:08:01 +02:00
Logger ( e ) ;
2021-11-10 11:07:09 +02:00
replicate . cancel ( ) ;
replicate . removeAllListeners ( ) ;
2021-11-12 12:11:52 +02:00
if ( notice != null ) notice . hide ( ) ;
2021-10-26 11:08:01 +02:00
rej ( e ) ;
} ) ;
} ) ;
}
2021-11-10 11:07:09 +02:00
2021-10-15 05:30:06 +02:00
async openReplication ( setting : ObsidianLiveSyncSettings , keepAlive : boolean , showResult : boolean , callback : ( e : PouchDB.Core.ExistingDocument < { } > [ ] ) = > Promise < void > ) {
2021-11-10 11:07:09 +02:00
if ( ! this . isReady ) {
Logger ( "Database is not ready." ) ;
return false ;
}
2021-10-17 15:10:03 +02:00
if ( setting . versionUpFlash != "" ) {
new Notice ( "Open settings and check message, please." ) ;
return ;
}
2021-11-15 05:25:18 +02:00
let uri = setting . couchDB_URI + ( setting . couchDB_DBNAME == "" ? "" : "/" + setting . couchDB_DBNAME ) ;
2021-10-14 12:27:08 +02:00
let auth : Credential = {
username : setting.couchDB_USER ,
password : setting.couchDB_PASSWORD ,
} ;
if ( this . syncHandler != null ) {
2021-10-21 11:48:42 +02:00
Logger ( "Another replication running." ) ;
2021-10-14 12:27:08 +02:00
return false ;
}
let dbret = await connectRemoteCouchDB ( uri , auth ) ;
if ( dbret === false ) {
2021-10-21 11:48:42 +02:00
Logger ( ` could not connect to ${ uri } ` , LOG_LEVEL . NOTICE ) ;
2021-10-14 12:27:08 +02:00
return ;
}
2021-10-21 11:48:42 +02:00
if ( ! ( await checkRemoteVersion ( dbret . db , this . migrate . bind ( this ) , VER ) ) ) {
2021-11-05 09:38:45 +02:00
Logger ( "Remote database is newer or corrupted, make sure to latest version of self-hosted-livesync installed" , LOG_LEVEL . NOTICE ) ;
2021-10-21 11:48:42 +02:00
return ;
}
2021-10-26 11:08:01 +02:00
let defMilestonePoint : EntryMilestoneInfo = {
_id : MILSTONE_DOCID ,
type : "milestoneinfo" ,
created : ( new Date ( ) as any ) / 1 ,
locked : false ,
accepted_nodes : [ this . nodeid ] ,
} ;
let remoteMilestone : EntryMilestoneInfo = await resolveWithIgnoreKnownError ( dbret . db . get ( MILSTONE_DOCID ) , defMilestonePoint ) ;
this . remoteLocked = remoteMilestone . locked ;
this . remoteLockedAndDeviceNotAccepted = remoteMilestone . locked && remoteMilestone . accepted_nodes . indexOf ( this . nodeid ) == - 1 ;
if ( remoteMilestone . locked && remoteMilestone . accepted_nodes . indexOf ( this . nodeid ) == - 1 ) {
Logger ( "Remote database marked as 'Auto Sync Locked'. And this devide does not marked as resolved device. see settings dialog." , LOG_LEVEL . NOTICE ) ;
return ;
}
if ( typeof remoteMilestone . _rev == "undefined" ) {
await dbret . db . put ( remoteMilestone ) ;
}
2021-10-18 08:07:44 +02:00
let syncOptionBase : PouchDB.Replication.SyncOptions = {
batch_size : 250 ,
batches_limit : 40 ,
} ;
let syncOption : PouchDB.Replication.SyncOptions = keepAlive ? { live : true , retry : true , heartbeat : 30000 , . . . syncOptionBase } : { . . . syncOptionBase } ;
2021-11-12 12:11:52 +02:00
let notice : Notice = null ;
if ( showResult ) {
notice = new Notice ( "Replicating" , 0 ) ;
}
2021-10-14 12:27:08 +02:00
let db = dbret . db ;
//replicate once
2021-11-04 12:12:43 +02:00
this . syncStatus = "CONNECTED" ;
2021-11-10 11:07:09 +02:00
Logger ( "Pull before replicate." ) ;
Logger ( await this . localDatabase . info ( ) , LOG_LEVEL . VERBOSE ) ;
Logger ( await db . info ( ) , LOG_LEVEL . VERBOSE ) ;
2021-10-18 08:07:44 +02:00
let replicate = this . localDatabase . replicate . from ( db , syncOptionBase ) ;
2021-10-15 05:30:06 +02:00
replicate
2021-11-04 12:12:43 +02:00
. on ( "active" , ( ) = > {
this . syncStatus = "CONNECTED" ;
this . updateInfo ( ) ;
2021-11-10 11:07:09 +02:00
Logger ( "Replication pull activated." ) ;
2021-11-04 12:12:43 +02:00
} )
2021-10-15 05:30:06 +02:00
. on ( "change" , async ( e ) = > {
2021-10-19 10:53:54 +02:00
// when in first run, replication will send us tombstone data
// and in normal cases, all leavs should sent before the entry that contains these item.
// so skip to completed all, we should treat all changes.
2021-10-15 05:30:06 +02:00
try {
callback ( e . docs ) ;
2021-11-12 12:11:52 +02:00
this . docArrived += e . docs . length ;
2021-11-04 12:12:43 +02:00
this . updateInfo ( ) ;
2021-10-21 11:48:42 +02:00
Logger ( ` pulled ${ e . docs . length } doc(s) ` ) ;
2021-11-12 12:11:52 +02:00
if ( notice != null ) {
notice . setMessage ( ` Replication pulled: ${ e . docs_read } ` ) ;
}
2021-10-15 05:30:06 +02:00
} catch ( ex ) {
2021-10-21 11:48:42 +02:00
Logger ( "Replication callback error" ) ;
Logger ( ex ) ;
2021-10-15 05:30:06 +02:00
}
} )
2021-10-14 12:27:08 +02:00
. on ( "complete" , async ( info ) = > {
2021-11-04 12:12:43 +02:00
this . syncStatus = "COMPLETED" ;
this . updateInfo ( ) ;
2021-10-15 05:30:06 +02:00
replicate . cancel ( ) ;
2021-10-19 10:53:54 +02:00
replicate . removeAllListeners ( ) ;
2021-10-18 08:07:44 +02:00
this . syncHandler = null ;
2021-10-17 15:10:03 +02:00
if ( this . syncHandler != null ) {
2021-10-19 10:53:54 +02:00
this . syncHandler . cancel ( ) ;
2021-10-17 15:10:03 +02:00
this . syncHandler . removeAllListeners ( ) ;
}
2021-11-10 11:07:09 +02:00
Logger ( "Replication pull completed." ) ;
2021-10-14 12:27:08 +02:00
this . syncHandler = this . localDatabase . sync ( db , syncOption ) ;
this . syncHandler
2021-10-17 15:10:03 +02:00
. on ( "active" , ( ) = > {
2021-11-04 12:12:43 +02:00
this . syncStatus = "CONNECTED" ;
this . updateInfo ( ) ;
2021-10-21 11:48:42 +02:00
Logger ( "Replication activated" ) ;
2021-10-17 15:10:03 +02:00
} )
2021-10-14 12:27:08 +02:00
. on ( "change" , async ( e ) = > {
try {
2021-11-12 12:11:52 +02:00
if ( e . direction == "pull" ) {
callback ( e . change . docs ) ;
Logger ( ` replicated ${ e . change . docs_read } doc(s) ` ) ;
this . docArrived += e . change . docs . length ;
} else {
this . docSent += e . change . docs . length ;
}
if ( notice != null ) {
notice . setMessage ( ` ↑ ${ e . change . docs_written } ↓ ${ e . change . docs_read } ` ) ;
}
2021-11-04 12:12:43 +02:00
this . updateInfo ( ) ;
2021-10-14 12:27:08 +02:00
} catch ( ex ) {
2021-10-21 11:48:42 +02:00
Logger ( "Replication callback error" ) ;
Logger ( ex ) ;
2021-10-14 12:27:08 +02:00
}
} )
. on ( "complete" , ( e ) = > {
2021-11-04 12:12:43 +02:00
this . syncStatus = "COMPLETED" ;
this . updateInfo ( ) ;
2021-10-21 11:48:42 +02:00
Logger ( "Replication completed" , showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO ) ;
2021-10-14 12:27:08 +02:00
this . syncHandler = null ;
2021-11-12 12:11:52 +02:00
if ( notice != null ) notice . hide ( ) ;
2021-10-14 12:27:08 +02:00
} )
. on ( "denied" , ( e ) = > {
2021-11-04 12:12:43 +02:00
this . syncStatus = "ERRORED" ;
this . updateInfo ( ) ;
2021-11-12 12:11:52 +02:00
if ( notice != null ) notice . hide ( ) ;
2021-10-21 11:48:42 +02:00
Logger ( "Replication denied" , LOG_LEVEL . NOTICE ) ;
// Logger(e);
2021-10-14 12:27:08 +02:00
} )
. on ( "error" , ( e ) = > {
2021-11-04 12:12:43 +02:00
this . syncStatus = "ERRORED" ;
this . updateInfo ( ) ;
2021-11-12 12:11:52 +02:00
if ( notice != null ) notice . hide ( ) ;
2021-10-21 11:48:42 +02:00
Logger ( "Replication error" , LOG_LEVEL . NOTICE ) ;
// Logger(e);
2021-10-14 12:27:08 +02:00
} )
. on ( "paused" , ( e ) = > {
2021-11-04 12:12:43 +02:00
this . syncStatus = "PAUSED" ;
this . updateInfo ( ) ;
2021-11-12 12:11:52 +02:00
if ( notice != null ) notice . hide ( ) ;
2021-10-21 11:48:42 +02:00
Logger ( "replication paused" , LOG_LEVEL . VERBOSE ) ;
// Logger(e);
2021-10-14 12:27:08 +02:00
} ) ;
} )
2021-10-18 08:07:44 +02:00
. on ( "error" , ( e ) = > {
2021-11-04 12:12:43 +02:00
this . syncStatus = "ERRORED" ;
this . updateInfo ( ) ;
2021-11-10 11:07:09 +02:00
Logger ( "Pulling Replication error" , LOG_LEVEL . INFO ) ;
replicate . cancel ( ) ;
replicate . removeAllListeners ( ) ;
this . syncHandler . cancel ( ) ;
this . syncHandler . removeAllListeners ( ) ;
this . syncHandler = null ;
2021-11-12 12:11:52 +02:00
if ( notice != null ) notice . hide ( ) ;
2021-11-10 11:07:09 +02:00
// debugger;
2021-10-21 11:48:42 +02:00
Logger ( e ) ;
2021-10-14 12:27:08 +02:00
} ) ;
}
closeReplication() {
if ( this . syncHandler == null ) {
return ;
}
2021-11-04 12:12:43 +02:00
this . syncStatus = "CLOSED" ;
this . updateInfo ( ) ;
2021-10-14 12:27:08 +02:00
this . syncHandler . cancel ( ) ;
this . syncHandler . removeAllListeners ( ) ;
this . syncHandler = null ;
2021-10-21 11:48:42 +02:00
Logger ( "Replication closed" ) ;
2021-10-14 12:27:08 +02:00
}
async resetDatabase() {
2021-10-19 10:53:54 +02:00
if ( this . changeHandler != null ) {
2021-11-10 11:07:09 +02:00
this . changeHandler . removeAllListeners ( ) ;
2021-10-19 10:53:54 +02:00
this . changeHandler . cancel ( ) ;
}
2021-10-14 12:27:08 +02:00
await this . closeReplication ( ) ;
2021-11-10 11:07:09 +02:00
this . isReady = false ;
2021-10-14 12:27:08 +02:00
await this . localDatabase . destroy ( ) ;
2021-10-15 05:30:06 +02:00
this . localDatabase = null ;
2021-10-14 12:27:08 +02:00
await this . initializeDatabase ( ) ;
2021-10-19 10:53:54 +02:00
this . disposeHashCache ( ) ;
2021-10-21 11:48:42 +02:00
Logger ( "Local Database Reset" , LOG_LEVEL . NOTICE ) ;
2021-10-14 12:27:08 +02:00
}
async tryResetRemoteDatabase ( setting : ObsidianLiveSyncSettings ) {
await this . closeReplication ( ) ;
2021-11-15 05:25:18 +02:00
let uri = setting . couchDB_URI + ( setting . couchDB_DBNAME == "" ? "" : "/" + setting . couchDB_DBNAME ) ;
2021-10-14 12:27:08 +02:00
let auth : Credential = {
username : setting.couchDB_USER ,
password : setting.couchDB_PASSWORD ,
} ;
let con = await connectRemoteCouchDB ( uri , auth ) ;
if ( con === false ) return ;
try {
await con . db . destroy ( ) ;
2021-10-21 11:48:42 +02:00
Logger ( "Remote Database Destroyed" , LOG_LEVEL . NOTICE ) ;
2021-10-14 12:27:08 +02:00
await this . tryCreateRemoteDatabase ( setting ) ;
} catch ( ex ) {
2021-10-21 11:48:42 +02:00
Logger ( "something happend on Remote Database Destory" , LOG_LEVEL . NOTICE ) ;
2021-10-14 12:27:08 +02:00
}
}
async tryCreateRemoteDatabase ( setting : ObsidianLiveSyncSettings ) {
await this . closeReplication ( ) ;
2021-11-15 05:25:18 +02:00
let uri = setting . couchDB_URI + ( setting . couchDB_DBNAME == "" ? "" : "/" + setting . couchDB_DBNAME ) ;
2021-10-14 12:27:08 +02:00
let auth : Credential = {
username : setting.couchDB_USER ,
password : setting.couchDB_PASSWORD ,
} ;
let con2 = await connectRemoteCouchDB ( uri , auth ) ;
if ( con2 === false ) return ;
2021-10-21 11:48:42 +02:00
Logger ( "Remote Database Created or Connected" , LOG_LEVEL . NOTICE ) ;
2021-10-14 12:27:08 +02:00
}
2021-10-26 11:08:01 +02:00
async markRemoteLocked ( setting : ObsidianLiveSyncSettings , locked : boolean ) {
2021-11-15 05:25:18 +02:00
let uri = setting . couchDB_URI + ( setting . couchDB_DBNAME == "" ? "" : "/" + setting . couchDB_DBNAME ) ;
2021-10-26 11:08:01 +02:00
let auth : Credential = {
username : setting.couchDB_USER ,
password : setting.couchDB_PASSWORD ,
} ;
let dbret = await connectRemoteCouchDB ( uri , auth ) ;
if ( dbret === false ) {
Logger ( ` could not connect to ${ uri } ` , LOG_LEVEL . NOTICE ) ;
return ;
}
if ( ! ( await checkRemoteVersion ( dbret . db , this . migrate . bind ( this ) , VER ) ) ) {
2021-11-05 09:38:45 +02:00
Logger ( "Remote database is newer or corrupted, make sure to latest version of self-hosted-livesync installed" , LOG_LEVEL . NOTICE ) ;
2021-10-26 11:08:01 +02:00
return ;
}
let defInitPoint : EntryMilestoneInfo = {
_id : MILSTONE_DOCID ,
type : "milestoneinfo" ,
created : ( new Date ( ) as any ) / 1 ,
locked : locked ,
accepted_nodes : [ this . nodeid ] ,
} ;
let remoteMilestone : EntryMilestoneInfo = await resolveWithIgnoreKnownError ( dbret . db . get ( MILSTONE_DOCID ) , defInitPoint ) ;
remoteMilestone . accepted_nodes = [ this . nodeid ] ;
remoteMilestone . locked = locked ;
if ( locked ) {
Logger ( "Lock remote database to prevent data corruption" , LOG_LEVEL . NOTICE ) ;
} else {
Logger ( "Unlock remote database to prevent data corruption" , LOG_LEVEL . NOTICE ) ;
}
await dbret . db . put ( remoteMilestone ) ;
}
async markRemoteResolved ( setting : ObsidianLiveSyncSettings ) {
2021-11-15 05:25:18 +02:00
let uri = setting . couchDB_URI + ( setting . couchDB_DBNAME == "" ? "" : "/" + setting . couchDB_DBNAME ) ;
2021-10-26 11:08:01 +02:00
let auth : Credential = {
username : setting.couchDB_USER ,
password : setting.couchDB_PASSWORD ,
} ;
let dbret = await connectRemoteCouchDB ( uri , auth ) ;
if ( dbret === false ) {
Logger ( ` could not connect to ${ uri } ` , LOG_LEVEL . NOTICE ) ;
return ;
}
if ( ! ( await checkRemoteVersion ( dbret . db , this . migrate . bind ( this ) , VER ) ) ) {
2021-11-05 09:38:45 +02:00
Logger ( "Remote database is newer or corrupted, make sure to latest version of self-hosted-livesync installed" , LOG_LEVEL . NOTICE ) ;
2021-10-26 11:08:01 +02:00
return ;
}
let defInitPoint : EntryMilestoneInfo = {
_id : MILSTONE_DOCID ,
type : "milestoneinfo" ,
created : ( new Date ( ) as any ) / 1 ,
locked : false ,
accepted_nodes : [ this . nodeid ] ,
} ;
// check local database hash status and remote replicate hash status
let remoteMilestone : EntryMilestoneInfo = await resolveWithIgnoreKnownError ( dbret . db . get ( MILSTONE_DOCID ) , defInitPoint ) ;
// remoteMilestone.locked = false;
remoteMilestone . accepted_nodes = Array . from ( new Set ( [ . . . remoteMilestone . accepted_nodes , this . nodeid ] ) ) ;
// this.remoteLocked = false;
Logger ( "Mark this device as 'resolved'." , LOG_LEVEL . NOTICE ) ;
await dbret . db . put ( remoteMilestone ) ;
}
2021-10-15 10:58:42 +02:00
async garbageCollect() {
// get all documents of NewEntry2
// we don't use queries , just use allDocs();
let c = 0 ;
let readCount = 0 ;
let hashPieces : string [ ] = [ ] ;
let usedPieces : string [ ] = [ ] ;
2021-11-10 11:07:09 +02:00
Logger ( "Collecting Garbage" ) ;
2021-10-15 10:58:42 +02:00
do {
2021-11-10 11:07:09 +02:00
let result = await this . localDatabase . allDocs ( { include_docs : true , skip : c , limit : 500 , conflicts : true } ) ;
2021-10-15 10:58:42 +02:00
readCount = result . rows . length ;
2021-11-10 11:07:09 +02:00
Logger ( "checked:" + readCount ) ;
2021-10-15 10:58:42 +02:00
if ( readCount > 0 ) {
//there are some result
for ( let v of result . rows ) {
let doc = v . doc ;
2021-10-17 15:10:03 +02:00
if ( doc . type == "newnote" || doc . type == "plain" ) {
2021-10-15 10:58:42 +02:00
// used pieces memo.
usedPieces = Array . from ( new Set ( [ . . . usedPieces , . . . doc . children ] ) ) ;
2021-10-18 08:07:44 +02:00
if ( doc . _conflicts ) {
for ( let cid of doc . _conflicts ) {
let p = await this . localDatabase . get < EntryDoc > ( doc . _id , { rev : cid } ) ;
if ( p . type == "newnote" || p . type == "plain" ) {
usedPieces = Array . from ( new Set ( [ . . . usedPieces , . . . p . children ] ) ) ;
}
}
}
2021-10-15 10:58:42 +02:00
}
if ( doc . type == "leaf" ) {
// all pieces.
hashPieces = Array . from ( new Set ( [ . . . hashPieces , doc . _id ] ) ) ;
}
}
}
c += readCount ;
} while ( readCount != 0 ) ;
// items collected.
2021-11-10 11:07:09 +02:00
Logger ( "Finding unused pieces" ) ;
2021-10-15 10:58:42 +02:00
const garbages = hashPieces . filter ( ( e ) = > usedPieces . indexOf ( e ) == - 1 ) ;
let deleteCount = 0 ;
2021-11-10 11:07:09 +02:00
Logger ( "we have to delete:" + garbages . length ) ;
let deleteDoc : EntryDoc [ ] = [ ] ;
2021-10-15 10:58:42 +02:00
for ( let v of garbages ) {
try {
let item = await this . localDatabase . get ( v ) ;
item . _deleted = true ;
2021-11-10 11:07:09 +02:00
deleteDoc . push ( item ) ;
if ( deleteDoc . length > 50 ) {
await this . localDatabase . bulkDocs ( deleteDoc ) ;
deleteDoc = [ ] ;
Logger ( "delete:" + deleteCount ) ;
}
2021-10-15 10:58:42 +02:00
deleteCount ++ ;
} catch ( ex ) {
if ( ex . status && ex . status == 404 ) {
// NO OP. It should be timing problem.
} else {
throw ex ;
}
}
}
2021-11-10 11:07:09 +02:00
if ( deleteDoc . length > 0 ) {
await this . localDatabase . bulkDocs ( deleteDoc ) ;
}
2021-10-21 11:48:42 +02:00
Logger ( ` GC:deleted ${ deleteCount } items. ` ) ;
2021-10-15 10:58:42 +02:00
}
2021-10-14 12:27:08 +02:00
}
2021-10-12 16:50:13 +02:00
export default class ObsidianLiveSyncPlugin extends Plugin {
settings : ObsidianLiveSyncSettings ;
2021-10-14 12:27:08 +02:00
localDatabase : LocalPouchDB ;
2021-10-12 16:50:13 +02:00
logMessage : string [ ] = [ ] ;
2021-10-14 12:27:08 +02:00
statusBar : HTMLElement ;
statusBar2 : HTMLElement ;
async onload() {
2021-10-21 11:48:42 +02:00
Logger = this . addLog . bind ( this ) ; // Logger moved to global.
Logger ( "loading plugin" ) ;
2021-10-17 15:10:03 +02:00
const lsname = "obsidian-live-sync-ver" + this . app . vault . getName ( ) ;
const last_version = localStorage . getItem ( lsname ) ;
2021-10-14 12:27:08 +02:00
await this . loadSettings ( ) ;
2021-10-17 15:10:03 +02:00
if ( ! last_version || Number ( last_version ) < VER ) {
this . settings . liveSync = false ;
this . settings . syncOnSave = false ;
this . settings . syncOnStart = false ;
2021-11-10 11:07:09 +02:00
this . settings . periodicReplication = false ;
2021-10-17 15:10:03 +02:00
this . settings . versionUpFlash = "I changed specifications incompatiblly, so when you enable sync again, be sure to made version up all nother devides." ;
this . saveSettings ( ) ;
}
localStorage . setItem ( lsname , ` ${ VER } ` ) ;
await this . openDatabase ( ) ;
2021-10-14 12:27:08 +02:00
addIcon (
"replicate" ,
` <g transform="matrix(1.15 0 0 1.15 -8.31 -9.52)" fill="currentColor" fill-rule="evenodd">
< path d = "m85 22.2c-0.799-4.74-4.99-8.37-9.88-8.37-0.499 0-1.1 0.101-1.6 0.101-2.4-3.03-6.09-4.94-10.3-4.94-6.09 0-11.2 4.14-12.8 9.79-5.59 1.11-9.78 6.05-9.78 12 0 6.76 5.39 12.2 12 12.2h29.9c5.79 0 10.1-4.74 10.1-10.6 0-4.84-3.29-8.88-7.68-10.2zm-2.99 14.7h-29.5c-2.3-0.202-4.29-1.51-5.29-3.53-0.899-2.12-0.699-4.54 0.698-6.46 1.2-1.61 2.99-2.52 4.89-2.52 0.299 0 0.698 0 0.998 0.101l1.8 0.303v-2.02c0-3.63 2.4-6.76 5.89-7.57 0.599-0.101 1.2-0.202 1.8-0.202 2.89 0 5.49 1.62 6.79 4.24l0.598 1.21 1.3-0.504c0.599-0.202 1.3-0.303 2-0.303 1.3 0 2.5 0.404 3.59 1.11 1.6 1.21 2.6 3.13 2.6 5.15v1.61h2c2.6 0 4.69 2.12 4.69 4.74-0.099 2.52-2.2 4.64-4.79 4.64z" / >
< path d = "m53.2 49.2h-41.6c-1.8 0-3.2 1.4-3.2 3.2v28.6c0 1.8 1.4 3.2 3.2 3.2h15.8v4h-7v6h24v-6h-7v-4h15.8c1.8 0 3.2-1.4 3.2-3.2v-28.6c0-1.8-1.4-3.2-3.2-3.2zm-2.8 29h-36v-23h36z" / >
< path d = "m73 49.2c1.02 1.29 1.53 2.97 1.53 4.56 0 2.97-1.74 5.65-4.39 7.04v-4.06l-7.46 7.33 7.46 7.14v-4.06c7.66-1.98 12.2-9.61 10-17-0.102-0.297-0.205-0.595-0.307-0.892z" / >
< path d = "m24.1 43c-0.817-0.991-1.53-2.97-1.53-4.56 0-2.97 1.74-5.65 4.39-7.04v4.06l7.46-7.33-7.46-7.14v4.06c-7.66 1.98-12.2 9.61-10 17 0.102 0.297 0.205 0.595 0.307 0.892z" / >
< / g > `
) ;
addIcon (
"view-log" ,
` <g transform="matrix(1.28 0 0 1.28 -131 -411)" fill="currentColor" fill-rule="evenodd">
< path d = "m103 330h76v12h-76z" / >
< path d = "m106 346v44h70v-44zm45 16h-20v-8h20z" / >
< / g > `
) ;
this . addRibbonIcon ( "replicate" , "Replicate" , async ( ) = > {
2021-10-15 05:30:06 +02:00
await this . replicate ( true ) ;
2021-10-14 12:27:08 +02:00
} ) ;
2021-10-15 05:30:06 +02:00
let x = this . addRibbonIcon ( "view-log" , "Show log" , ( ) = > {
2021-10-14 12:27:08 +02:00
new LogDisplayModal ( this . app , this ) . open ( ) ;
} ) ;
2021-10-15 05:30:06 +02:00
2021-10-14 12:27:08 +02:00
this . statusBar = this . addStatusBarItem ( ) ;
2021-11-04 12:12:43 +02:00
this . statusBar . addClass ( "syncstatusbar" ) ;
this . refreshStatusText = this . refreshStatusText . bind ( this ) ;
2021-10-14 12:27:08 +02:00
this . statusBar2 = this . addStatusBarItem ( ) ;
2021-10-15 05:30:06 +02:00
let delay = this . settings . savingDelay ;
if ( delay < 200 ) delay = 200 ;
if ( delay > 5000 ) delay = 5000 ;
2021-11-10 03:06:36 +02:00
// this.watchVaultChange = debounce(this.watchVaultChange.bind(this), delay, false);
2021-11-09 08:05:12 +02:00
// this.watchVaultDelete = debounce(this.watchVaultDelete.bind(this), delay, false);
// this.watchVaultRename = debounce(this.watchVaultRename.bind(this), delay, false);
2021-11-10 03:06:36 +02:00
this . watchVaultChange = this . watchVaultChange . bind ( this ) ;
this . watchVaultCreate = this . watchVaultCreate . bind ( this ) ;
2021-11-09 08:05:12 +02:00
this . watchVaultDelete = this . watchVaultDelete . bind ( this ) ;
this . watchVaultRename = this . watchVaultRename . bind ( this ) ;
2021-10-15 05:30:06 +02:00
this . watchWorkspaceOpen = debounce ( this . watchWorkspaceOpen . bind ( this ) , delay , false ) ;
2021-11-09 08:05:12 +02:00
this . watchWindowVisiblity = debounce ( this . watchWindowVisiblity . bind ( this ) , delay , false ) ;
2021-10-18 08:07:44 +02:00
2021-10-14 12:27:08 +02:00
this . parseReplicationResult = this . parseReplicationResult . bind ( this ) ;
2021-11-10 11:07:09 +02:00
this . periodicSync = this . periodicSync . bind ( this ) ;
this . setPeriodicSync = this . setPeriodicSync . bind ( this ) ;
// this.registerWatchEvents();
2021-10-14 12:27:08 +02:00
this . addSettingTab ( new ObsidianLiveSyncSettingTab ( this . app , this ) ) ;
2021-10-17 04:06:25 +02:00
this . app . workspace . onLayoutReady ( async ( ) = > {
2021-11-12 12:11:52 +02:00
try {
await this . initializeDatabase ( ) ;
this . realizeSettingSyncMode ( ) ;
this . registerWatchEvents ( ) ;
} catch ( ex ) {
Logger ( "Error while loading Self-hosted LiveSync" , LOG_LEVEL . NOTICE ) ;
2021-11-15 05:25:18 +02:00
Logger ( ex , LOG_LEVEL . VERBOSE ) ;
2021-11-12 12:11:52 +02:00
}
2021-10-17 04:06:25 +02:00
} ) ;
2021-10-18 08:07:44 +02:00
this . addCommand ( {
id : "livesync-replicate" ,
name : "Replicate now" ,
callback : ( ) = > {
this . replicate ( ) ;
} ,
} ) ;
2021-11-10 11:07:09 +02:00
this . addCommand ( {
id : "livesync-dump" ,
name : "Dump informations of this doc " ,
editorCallback : ( editor : Editor , view : MarkdownView ) = > {
//this.replicate();
this . localDatabase . getDBEntry ( view . file . path , { } , true ) ;
} ,
} ) ;
2021-10-18 08:07:44 +02:00
this . addCommand ( {
id : "livesync-gc" ,
name : "garbage collect now" ,
callback : ( ) = > {
this . garbageCollect ( ) ;
} ,
} ) ;
this . addCommand ( {
id : "livesync-toggle" ,
name : "Toggle LiveSync" ,
callback : ( ) = > {
if ( this . settings . liveSync ) {
this . settings . liveSync = false ;
2021-10-21 11:48:42 +02:00
Logger ( "LiveSync Disabled." , LOG_LEVEL . NOTICE ) ;
2021-10-18 08:07:44 +02:00
} else {
this . settings . liveSync = true ;
2021-10-21 11:48:42 +02:00
Logger ( "LiveSync Enabled." , LOG_LEVEL . NOTICE ) ;
2021-10-18 08:07:44 +02:00
}
this . realizeSettingSyncMode ( ) ;
this . saveSettings ( ) ;
} ,
} ) ;
2021-10-14 12:27:08 +02:00
}
onunload() {
2021-10-15 10:58:42 +02:00
if ( this . gcTimerHandler != null ) {
clearTimeout ( this . gcTimerHandler ) ;
this . gcTimerHandler = null ;
}
2021-11-10 11:07:09 +02:00
this . clearPeriodicSync ( ) ;
2021-10-14 12:27:08 +02:00
this . localDatabase . closeReplication ( ) ;
this . localDatabase . close ( ) ;
2021-10-15 05:30:06 +02:00
window . removeEventListener ( "visibilitychange" , this . watchWindowVisiblity ) ;
2021-10-21 11:48:42 +02:00
Logger ( "unloading plugin" ) ;
2021-10-14 12:27:08 +02:00
}
async openDatabase() {
if ( this . localDatabase != null ) {
this . localDatabase . close ( ) ;
}
let vaultName = this . app . vault . getName ( ) ;
2021-11-10 11:07:09 +02:00
Logger ( "Open Database..." ) ;
2021-10-21 11:48:42 +02:00
this . localDatabase = new LocalPouchDB ( this . settings , vaultName ) ;
2021-11-04 12:12:43 +02:00
this . localDatabase . updateInfo = ( ) = > {
this . refreshStatusText ( ) ;
} ;
2021-10-15 10:58:42 +02:00
await this . localDatabase . initializeDatabase ( ) ;
}
async garbageCollect() {
await this . localDatabase . garbageCollect ( ) ;
2021-10-14 12:27:08 +02:00
}
async loadSettings() {
this . settings = Object . assign ( { } , DEFAULT_SETTINGS , await this . loadData ( ) ) ;
2021-11-12 12:11:52 +02:00
this . settings . workingEncrypt = this . settings . encrypt ;
this . settings . workingPassphrase = this . settings . passphrase ;
2021-10-14 12:27:08 +02:00
}
2021-10-12 16:50:13 +02:00
2021-10-14 12:27:08 +02:00
async saveSettings() {
await this . saveData ( this . settings ) ;
2021-10-21 11:48:42 +02:00
this . localDatabase . settings = this . settings ;
2021-11-10 11:07:09 +02:00
this . realizeSettingSyncMode ( ) ;
2021-10-14 12:27:08 +02:00
}
2021-10-15 10:58:42 +02:00
gcTimerHandler : any = null ;
gcHook() {
if ( this . settings . gcDelay == 0 ) return ;
const GC_DELAY = this . settings . gcDelay * 1000 ; // if leaving opening window, try GC,
if ( this . gcTimerHandler != null ) {
clearTimeout ( this . gcTimerHandler ) ;
this . gcTimerHandler = null ;
}
this . gcTimerHandler = setTimeout ( ( ) = > {
this . gcTimerHandler = null ;
this . garbageCollect ( ) ;
} , GC_DELAY ) ;
}
2021-10-14 12:27:08 +02:00
registerWatchEvents() {
this . registerEvent ( this . app . vault . on ( "modify" , this . watchVaultChange ) ) ;
this . registerEvent ( this . app . vault . on ( "delete" , this . watchVaultDelete ) ) ;
this . registerEvent ( this . app . vault . on ( "rename" , this . watchVaultRename ) ) ;
this . registerEvent ( this . app . vault . on ( "create" , this . watchVaultChange ) ) ;
this . registerEvent ( this . app . workspace . on ( "file-open" , this . watchWorkspaceOpen ) ) ;
2021-11-10 11:07:09 +02:00
window . addEventListener ( "visibilitychange" , this . watchWindowVisiblity ) ;
2021-10-14 12:27:08 +02:00
}
2021-10-15 05:30:06 +02:00
watchWindowVisiblity() {
2021-11-09 08:05:12 +02:00
this . watchWindowVisiblityAsync ( ) ;
}
async watchWindowVisiblityAsync() {
2021-10-19 10:53:54 +02:00
if ( this . settings . suspendFileWatching ) return ;
2021-10-15 05:30:06 +02:00
let isHidden = document . hidden ;
if ( isHidden ) {
this . localDatabase . closeReplication ( ) ;
2021-11-10 11:07:09 +02:00
this . clearPeriodicSync ( ) ;
2021-10-15 05:30:06 +02:00
} else {
if ( this . settings . liveSync ) {
2021-11-09 08:05:12 +02:00
await this . localDatabase . openReplication ( this . settings , true , false , this . parseReplicationResult ) ;
2021-10-15 05:30:06 +02:00
}
if ( this . settings . syncOnStart ) {
2021-11-09 08:05:12 +02:00
await this . localDatabase . openReplication ( this . settings , false , false , this . parseReplicationResult ) ;
2021-10-15 05:30:06 +02:00
}
2021-11-10 11:07:09 +02:00
if ( this . settings . periodicReplication ) {
this . setPeriodicSync ( ) ;
}
2021-10-15 05:30:06 +02:00
}
2021-10-15 10:58:42 +02:00
this . gcHook ( ) ;
2021-10-15 05:30:06 +02:00
}
2021-10-14 12:27:08 +02:00
watchWorkspaceOpen ( file : TFile ) {
2021-10-19 10:53:54 +02:00
if ( this . settings . suspendFileWatching ) return ;
2021-11-09 08:05:12 +02:00
this . watchWorkspaceOpenAsync ( file ) ;
}
async watchWorkspaceOpenAsync ( file : TFile ) {
2021-10-14 12:27:08 +02:00
if ( file == null ) return ;
2021-11-10 11:07:09 +02:00
if ( this . settings . syncOnFileOpen ) {
await this . replicate ( ) ;
}
2021-10-18 08:07:44 +02:00
this . localDatabase . disposeHashCache ( ) ;
2021-11-09 08:05:12 +02:00
await this . showIfConflicted ( file ) ;
2021-10-15 10:58:42 +02:00
this . gcHook ( ) ;
2021-10-14 12:27:08 +02:00
}
2021-11-10 03:06:36 +02:00
watchVaultCreate ( file : TFile , . . . args : any [ ] ) {
if ( this . settings . suspendFileWatching ) return ;
this . watchVaultChangeAsync ( file , . . . args ) ;
}
2021-10-14 12:27:08 +02:00
watchVaultChange ( file : TFile , . . . args : any [ ] ) {
2021-10-19 10:53:54 +02:00
if ( this . settings . suspendFileWatching ) return ;
2021-11-09 08:05:12 +02:00
this . watchVaultChangeAsync ( file , . . . args ) ;
}
batchFileChange : string [ ] = [ ] ;
async watchVaultChangeAsync ( file : TFile , . . . args : any [ ] ) {
2021-11-10 11:07:09 +02:00
if ( file instanceof TFile ) {
await this . updateIntoDB ( file ) ;
this . gcHook ( ) ;
}
2021-10-14 12:27:08 +02:00
}
2021-11-09 08:05:12 +02:00
watchVaultDelete ( file : TFile | TFolder ) {
2021-11-10 11:07:09 +02:00
console . log ( ` ${ file . path } delete ` ) ;
2021-10-19 10:53:54 +02:00
if ( this . settings . suspendFileWatching ) return ;
2021-11-09 08:05:12 +02:00
this . watchVaultDeleteAsync ( file ) ;
}
async watchVaultDeleteAsync ( file : TFile | TFolder ) {
if ( file instanceof TFile ) {
await this . deleteFromDB ( file ) ;
} else if ( file instanceof TFolder ) {
await this . deleteFolderOnDB ( file ) ;
2021-10-14 12:27:08 +02:00
}
2021-10-15 10:58:42 +02:00
this . gcHook ( ) ;
2021-10-14 12:27:08 +02:00
}
2021-11-09 08:05:12 +02:00
GetAllFilesRecursively ( file : TAbstractFile ) : TFile [ ] {
if ( file instanceof TFile ) {
return [ file ] ;
} else if ( file instanceof TFolder ) {
let result : TFile [ ] = [ ] ;
for ( var v of file . children ) {
result . push ( . . . this . GetAllFilesRecursively ( v ) ) ;
}
return result ;
2021-10-14 12:27:08 +02:00
} else {
2021-11-09 08:05:12 +02:00
Logger ( ` Filetype error: ${ file . path } ` , LOG_LEVEL . NOTICE ) ;
throw new Error ( ` Filetype error: ${ file . path } ` ) ;
}
}
watchVaultRename ( file : TFile | TFolder , oldFile : any ) {
if ( this . settings . suspendFileWatching ) return ;
this . watchVaultRenameAsync ( file , oldFile ) ;
}
getFilePath ( file : TAbstractFile ) : string {
if ( file instanceof TFolder ) {
if ( file . isRoot ( ) ) return "" ;
return this . getFilePath ( file . parent ) + "/" + file . name ;
}
if ( file instanceof TFile ) {
return this . getFilePath ( file . parent ) + "/" + file . name ;
}
}
async watchVaultRenameAsync ( file : TFile | TFolder , oldFile : any ) {
Logger ( ` ${ oldFile } renamed to ${ file . path } ` , LOG_LEVEL . VERBOSE ) ;
if ( file instanceof TFolder ) {
const newFiles = this . GetAllFilesRecursively ( file ) ;
// for guard edge cases. this won't happen and each file's event will be raise.
for ( const i of newFiles ) {
let newFilePath = normalizePath ( this . getFilePath ( i ) ) ;
let newFile = this . app . vault . getAbstractFileByPath ( newFilePath ) ;
if ( newFile instanceof TFile ) {
Logger ( ` save ${ newFile . path } into db ` ) ;
await this . updateIntoDB ( newFile ) ;
}
}
Logger ( ` delete below ${ oldFile } from db ` ) ;
await this . deleteFromDBbyPath ( oldFile ) ;
} else if ( file instanceof TFile ) {
Logger ( ` file save ${ file . path } into db ` ) ;
await this . updateIntoDB ( file ) ;
Logger ( ` deleted ${ oldFile } into db ` ) ;
await this . deleteFromDBbyPath ( oldFile ) ;
2021-10-14 12:27:08 +02:00
}
2021-10-15 10:58:42 +02:00
this . gcHook ( ) ;
2021-10-14 12:27:08 +02:00
}
2021-11-10 11:07:09 +02:00
addLogHook : ( ) = > void = null ;
2021-10-14 12:27:08 +02:00
//--> Basic document Functions
2021-10-18 08:07:44 +02:00
async addLog ( message : any , level : LOG_LEVEL = LOG_LEVEL . INFO ) {
if ( level < LOG_LEVEL . INFO && this . settings && this . settings . lessInformationInLog ) {
2021-10-15 05:30:06 +02:00
return ;
}
2021-10-18 08:07:44 +02:00
if ( this . settings && ! this . settings . showVerboseLog && level == LOG_LEVEL . VERBOSE ) {
return ;
}
let valutName = this . app . vault . getName ( ) ;
2021-10-12 16:50:13 +02:00
let timestamp = new Date ( ) . toLocaleString ( ) ;
2021-10-13 14:38:44 +02:00
let messagecontent = typeof message == "string" ? message : JSON.stringify ( message , null , 2 ) ;
let newmessage = timestamp + "->" + messagecontent ;
2021-10-12 16:50:13 +02:00
this . logMessage = [ ] . concat ( this . logMessage ) . concat ( [ newmessage ] ) . slice ( - 100 ) ;
2021-10-18 08:07:44 +02:00
console . log ( valutName + ":" + newmessage ) ;
2021-11-04 12:12:43 +02:00
// if (this.statusBar2 != null) {
// this.statusBar2.setText(newmessage.substring(0, 60));
// }
2021-10-18 08:07:44 +02:00
if ( level >= LOG_LEVEL . NOTICE ) {
2021-10-13 14:38:44 +02:00
new Notice ( messagecontent ) ;
2021-10-12 16:50:13 +02:00
}
2021-11-10 11:07:09 +02:00
if ( this . addLogHook != null ) this . addLogHook ( ) ;
2021-10-12 16:50:13 +02:00
}
async ensureDirectory ( fullpath : string ) {
let pathElements = fullpath . split ( "/" ) ;
pathElements . pop ( ) ;
let c = "" ;
for ( var v of pathElements ) {
c += v ;
try {
await this . app . vault . createFolder ( c ) ;
} catch ( ex ) {
2021-10-13 14:38:44 +02:00
// basically skip exceptions.
if ( ex . message && ex . message == "Folder already exists." ) {
// especialy this message is.
} else {
2021-10-21 11:48:42 +02:00
Logger ( "Folder Create Error" ) ;
Logger ( ex ) ;
2021-10-13 14:38:44 +02:00
}
2021-10-12 16:50:13 +02:00
}
c += "/" ;
}
}
2021-10-19 10:53:54 +02:00
async doc2storage_create ( docEntry : EntryBody , force? : boolean ) {
2021-10-18 08:07:44 +02:00
let doc = await this . localDatabase . getDBEntry ( docEntry . _id , { rev : docEntry._rev } ) ;
2021-10-14 12:27:08 +02:00
if ( doc === false ) return ;
2021-10-17 15:10:03 +02:00
if ( doc . datatype == "newnote" ) {
let bin = base64ToArrayBuffer ( doc . data ) ;
if ( bin != null ) {
2021-10-26 11:08:01 +02:00
if ( ! isValidPath ( doc . _id ) ) {
Logger ( ` The file that having platform dependent name has been arrived. This file has skipped: ${ doc . _id } ` , LOG_LEVEL . NOTICE ) ;
return ;
}
2021-10-17 15:10:03 +02:00
await this . ensureDirectory ( doc . _id ) ;
2021-11-04 12:12:43 +02:00
try {
let newfile = await this . app . vault . createBinary ( normalizePath ( doc . _id ) , bin , { ctime : doc.ctime , mtime : doc.mtime } ) ;
Logger ( "live : write to local (newfile:b) " + doc . _id ) ;
await this . app . vault . trigger ( "create" , newfile ) ;
} catch ( ex ) {
Logger ( "could not write to local (newfile:bin) " + doc . _id , LOG_LEVEL . NOTICE ) ;
2021-11-12 12:11:52 +02:00
Logger ( ex , LOG_LEVEL . VERBOSE ) ;
2021-11-04 12:12:43 +02:00
}
2021-10-17 15:10:03 +02:00
}
} else if ( doc . datatype == "plain" ) {
2021-10-26 11:08:01 +02:00
if ( ! isValidPath ( doc . _id ) ) {
Logger ( ` The file that having platform dependent name has been arrived. This file has skipped: ${ doc . _id } ` , LOG_LEVEL . NOTICE ) ;
return ;
}
2021-10-12 16:50:13 +02:00
await this . ensureDirectory ( doc . _id ) ;
2021-11-04 12:12:43 +02:00
try {
let newfile = await this . app . vault . create ( normalizePath ( doc . _id ) , doc . data , { ctime : doc.ctime , mtime : doc.mtime } ) ;
Logger ( "live : write to local (newfile:p) " + doc . _id ) ;
await this . app . vault . trigger ( "create" , newfile ) ;
} catch ( ex ) {
Logger ( "could not write to local (newfile:plain) " + doc . _id , LOG_LEVEL . NOTICE ) ;
2021-11-12 12:11:52 +02:00
Logger ( ex , LOG_LEVEL . VERBOSE ) ;
2021-11-04 12:12:43 +02:00
}
2021-10-17 15:10:03 +02:00
} else {
2021-10-21 11:48:42 +02:00
Logger ( "live : New data imcoming, but we cound't parse that." + doc . datatype , LOG_LEVEL . NOTICE ) ;
2021-10-12 16:50:13 +02:00
}
}
2021-10-14 12:27:08 +02:00
2021-10-13 14:38:44 +02:00
async deleteVaultItem ( file : TFile | TFolder ) {
let dir = file . parent ;
2021-11-04 12:12:43 +02:00
if ( this . settings . trashInsteadDelete ) {
await this . app . vault . trash ( file , false ) ;
} else {
await this . app . vault . delete ( file ) ;
}
2021-10-21 11:48:42 +02:00
Logger ( ` deleted: ${ file . path } ` ) ;
Logger ( ` other items: ${ dir . children . length } ` ) ;
2021-10-13 14:38:44 +02:00
if ( dir . children . length == 0 ) {
2021-11-15 05:25:18 +02:00
if ( ! this . settings . doNotDeleteFolder ) {
Logger ( ` all files deleted by replication, so delete dir ` ) ;
await this . deleteVaultItem ( dir ) ;
}
2021-10-13 14:38:44 +02:00
}
}
2021-10-19 10:53:54 +02:00
async doc2storate_modify ( docEntry : EntryBody , file : TFile , force? : boolean ) {
2021-10-14 12:27:08 +02:00
if ( docEntry . _deleted ) {
2021-10-12 16:50:13 +02:00
//basically pass.
//but if there're no docs left, delete file.
2021-10-18 08:07:44 +02:00
let lastDocs = await this . localDatabase . getDBEntry ( docEntry . _id ) ;
2021-10-14 12:27:08 +02:00
if ( lastDocs === false ) {
await this . deleteVaultItem ( file ) ;
} else {
2021-10-18 08:07:44 +02:00
// it perhaps delete some revisions.
// may be we have to reload this
await this . pullFile ( docEntry . _id , null , true ) ;
2021-10-21 11:48:42 +02:00
Logger ( ` delete skipped: ${ lastDocs . _id } ` ) ;
2021-10-12 16:50:13 +02:00
}
return ;
}
2021-11-12 12:11:52 +02:00
let localMtime = ~ ~ ( file . stat . mtime / 1000 ) ;
let docMtime = ~ ~ ( docEntry . mtime / 1000 ) ;
if ( localMtime < docMtime || force ) {
2021-10-18 08:07:44 +02:00
let doc = await this . localDatabase . getDBEntry ( docEntry . _id ) ;
let msg = "livesync : newer local files so write to local:" + file . path ;
if ( force ) msg = "livesync : force write to local:" + file . path ;
2021-10-14 12:27:08 +02:00
if ( doc === false ) return ;
2021-10-17 15:10:03 +02:00
if ( doc . datatype == "newnote" ) {
let bin = base64ToArrayBuffer ( doc . data ) ;
if ( bin != null ) {
2021-10-26 11:08:01 +02:00
if ( ! isValidPath ( doc . _id ) ) {
Logger ( ` The file that having platform dependent name has been arrived. This file has skipped: ${ doc . _id } ` , LOG_LEVEL . NOTICE ) ;
return ;
}
2021-10-18 08:07:44 +02:00
await this . ensureDirectory ( doc . _id ) ;
2021-11-04 12:12:43 +02:00
try {
await this . app . vault . modifyBinary ( file , bin , { ctime : doc.ctime , mtime : doc.mtime } ) ;
Logger ( msg ) ;
await this . app . vault . trigger ( "modify" , file ) ;
} catch ( ex ) {
Logger ( "could not write to local (modify:bin) " + doc . _id , LOG_LEVEL . NOTICE ) ;
}
2021-10-17 15:10:03 +02:00
}
2021-11-12 12:11:52 +02:00
} else if ( doc . datatype == "plain" ) {
2021-10-26 11:08:01 +02:00
if ( ! isValidPath ( doc . _id ) ) {
Logger ( ` The file that having platform dependent name has been arrived. This file has skipped: ${ doc . _id } ` , LOG_LEVEL . NOTICE ) ;
return ;
}
2021-10-17 15:10:03 +02:00
await this . ensureDirectory ( doc . _id ) ;
2021-11-04 12:12:43 +02:00
try {
await this . app . vault . modify ( file , doc . data , { ctime : doc.ctime , mtime : doc.mtime } ) ;
Logger ( msg ) ;
await this . app . vault . trigger ( "modify" , file ) ;
} catch ( ex ) {
Logger ( "could not write to local (modify:plain) " + doc . _id , LOG_LEVEL . NOTICE ) ;
}
2021-10-17 15:10:03 +02:00
} else {
2021-10-21 11:48:42 +02:00
Logger ( "live : New data imcoming, but we cound't parse that.:" + doc . datatype + "-" , LOG_LEVEL . NOTICE ) ;
2021-10-12 16:50:13 +02:00
}
2021-11-12 12:11:52 +02:00
} else if ( localMtime > docMtime ) {
2021-10-12 16:50:13 +02:00
// newer local file.
// ?
} else {
//Nothing have to op.
//eq.case
}
}
2021-10-19 10:53:54 +02:00
async handleDBChanged ( change : EntryBody ) {
2021-10-12 16:50:13 +02:00
let allfiles = this . app . vault . getFiles ( ) ;
let targetFiles = allfiles . filter ( ( e ) = > e . path == change . _id ) ;
if ( targetFiles . length == 0 ) {
if ( change . _deleted ) {
return ;
}
let doc = change ;
await this . doc2storage_create ( doc ) ;
}
if ( targetFiles . length == 1 ) {
let doc = change ;
let file = targetFiles [ 0 ] ;
await this . doc2storate_modify ( doc , file ) ;
await this . showIfConflicted ( file ) ;
}
}
2021-11-10 11:07:09 +02:00
periodicSyncHandler : NodeJS.Timer = null ;
2021-10-12 16:50:13 +02:00
//---> Sync
2021-10-19 10:53:54 +02:00
async parseReplicationResult ( docs : Array < PouchDB.Core.ExistingDocument < EntryDoc > > ) : Promise < void > {
2021-11-04 12:12:43 +02:00
this . refreshStatusText ( ) ;
2021-10-14 12:27:08 +02:00
for ( var change of docs ) {
2021-10-17 15:10:03 +02:00
if ( this . localDatabase . isSelfModified ( change . _id , change . _rev ) ) {
return ;
}
2021-10-21 11:48:42 +02:00
Logger ( "replication change arrived" , LOG_LEVEL . VERBOSE ) ;
2021-10-26 11:08:01 +02:00
if ( change . type != "leaf" && change . type != "versioninfo" && change . type != "milestoneinfo" && change . type != "nodeinfo" ) {
2021-10-19 10:53:54 +02:00
await this . handleDBChanged ( change ) ;
}
2021-10-21 11:48:42 +02:00
if ( change . type == "versioninfo" ) {
if ( change . version > VER ) {
this . localDatabase . closeReplication ( ) ;
2021-11-05 09:38:45 +02:00
Logger ( ` Remote database updated to incompatible version. update your self-hosted-livesync plugin. ` , LOG_LEVEL . NOTICE ) ;
2021-10-21 11:48:42 +02:00
}
}
2021-10-17 15:10:03 +02:00
this . gcHook ( ) ;
2021-10-12 16:50:13 +02:00
}
}
2021-11-10 11:07:09 +02:00
clearPeriodicSync() {
if ( this . periodicSyncHandler != null ) {
clearInterval ( this . periodicSyncHandler ) ;
this . periodicSyncHandler = null ;
}
}
setPeriodicSync() {
if ( this . settings . periodicReplication && this . settings . periodicReplicationInterval > 0 ) {
this . clearPeriodicSync ( ) ;
this . periodicSyncHandler = setInterval ( ( ) = > this . periodicSync , Math . max ( this . settings . periodicReplicationInterval , 30 ) * 1000 ) ;
}
}
async periodicSync() {
await this . replicate ( ) ;
}
2021-11-04 12:12:43 +02:00
realizeSettingSyncMode() {
2021-10-21 11:48:42 +02:00
this . localDatabase . closeReplication ( ) ;
2021-10-14 12:27:08 +02:00
if ( this . settings . liveSync ) {
2021-10-15 05:30:06 +02:00
this . localDatabase . openReplication ( this . settings , true , false , this . parseReplicationResult ) ;
2021-10-14 12:27:08 +02:00
this . refreshStatusText ( ) ;
2021-10-12 16:50:13 +02:00
}
2021-11-10 11:07:09 +02:00
this . clearPeriodicSync ( ) ;
this . setPeriodicSync ( ) ;
2021-10-12 16:50:13 +02:00
}
refreshStatusText() {
2021-11-04 12:12:43 +02:00
let sent = this . localDatabase . docSent ;
let arrived = this . localDatabase . docArrived ;
let w = "" ;
switch ( this . localDatabase . syncStatus ) {
case "CLOSED" :
case "COMPLETED" :
case "NOT_CONNECTED" :
w = "⏹" ;
break ;
case "PAUSED" :
w = "💤" ;
break ;
case "CONNECTED" :
w = "⚡" ;
break ;
case "ERRORED" :
w = "⚠" ;
break ;
default :
w = "?" ;
}
2021-11-08 10:52:07 +02:00
this . statusBar . title = this . localDatabase . syncStatus ;
2021-11-04 12:12:43 +02:00
this . statusBar . setText ( ` Sync: ${ w } ↑ ${ sent } ↓ ${ arrived } ` ) ;
2021-10-12 16:50:13 +02:00
}
2021-10-15 05:30:06 +02:00
async replicate ( showMessage? : boolean ) {
2021-10-17 15:10:03 +02:00
if ( this . settings . versionUpFlash != "" ) {
new Notice ( "Open settings and check message, please." ) ;
return ;
}
2021-10-15 05:30:06 +02:00
this . localDatabase . openReplication ( this . settings , false , showMessage , this . parseReplicationResult ) ;
2021-10-12 16:50:13 +02:00
}
2021-11-12 12:11:52 +02:00
async initializeDatabase ( showingNotice? : boolean ) {
2021-10-14 12:27:08 +02:00
await this . openDatabase ( ) ;
2021-11-12 12:11:52 +02:00
await this . syncAllFiles ( showingNotice ) ;
2021-10-12 16:50:13 +02:00
}
2021-11-12 12:11:52 +02:00
async replicateAllToServer ( showingNotice? : boolean ) {
return await this . localDatabase . replicateAllToServer ( this . settings , showingNotice ) ;
2021-10-26 11:08:01 +02:00
}
async markRemoteLocked() {
return await this . localDatabase . markRemoteLocked ( this . settings , true ) ;
}
async markRemoteUnlocked() {
return await this . localDatabase . markRemoteLocked ( this . settings , false ) ;
}
async markRemoteResolved() {
return await this . localDatabase . markRemoteResolved ( this . settings ) ;
}
2021-11-12 12:11:52 +02:00
async syncAllFiles ( showingNotice? : boolean ) {
2021-10-12 16:50:13 +02:00
// synchronize all files between database and storage.
2021-11-12 12:11:52 +02:00
let notice : Notice = null ;
if ( showingNotice ) {
notice = new Notice ( "Initializing" , 0 ) ;
}
2021-10-12 16:50:13 +02:00
const filesStorage = this . app . vault . getFiles ( ) ;
const filesStorageName = filesStorage . map ( ( e ) = > e . path ) ;
2021-10-14 12:27:08 +02:00
const wf = await this . localDatabase . localDatabase . allDocs ( ) ;
2021-11-10 11:07:09 +02:00
const filesDatabase = wf . rows . filter ( ( e ) = > ! e . id . startsWith ( "h:" ) && e . id != "obsydian_livesync_version" ) . map ( ( e ) = > normalizePath ( e . id ) ) ;
2021-10-12 16:50:13 +02:00
const onlyInStorage = filesStorage . filter ( ( e ) = > filesDatabase . indexOf ( e . path ) == - 1 ) ;
const onlyInDatabase = filesDatabase . filter ( ( e ) = > filesStorageName . indexOf ( e ) == - 1 ) ;
2021-10-18 08:07:44 +02:00
2021-10-12 16:50:13 +02:00
const onlyInStorageNames = onlyInStorage . map ( ( e ) = > e . path ) ;
const syncFiles = filesStorage . filter ( ( e ) = > onlyInStorageNames . indexOf ( e . path ) == - 1 ) ;
2021-11-04 12:12:43 +02:00
Logger ( "Initialize and checking database files" ) ;
Logger ( "Updating database by new files" ) ;
this . statusBar . setText ( ` UPDATE DATABASE ` ) ;
2021-11-12 12:11:52 +02:00
async function runAll < T > ( procedurename : string , objects : T [ ] , callback : ( arg : T ) = > Promise < void > ) {
const count = objects . length ;
Logger ( procedurename ) ;
let i = 0 ;
2021-11-15 05:25:18 +02:00
let lastTicks = performance . now ( ) + 2000 ;
2021-11-12 12:11:52 +02:00
let procs = objects . map ( async ( e ) = > {
try {
// debugger;
// Logger("hello?")
await callback ( e ) ;
i ++ ;
2021-11-15 05:25:18 +02:00
if ( lastTicks < performance . now ( ) ) {
2021-11-12 12:11:52 +02:00
const notify = ` ${ procedurename } : ${ i } / ${ count } ` ;
if ( notice != null ) notice . setMessage ( notify ) ;
Logger ( notify ) ;
2021-11-15 05:25:18 +02:00
lastTicks = performance . now ( ) + 2000 ;
2021-11-12 12:11:52 +02:00
// this.statusBar.setText(notify);
}
} catch ( ex ) {
Logger ( ` Error while ${ procedurename } ` , LOG_LEVEL . NOTICE ) ;
Logger ( ex ) ;
}
} ) ;
2021-11-15 05:25:18 +02:00
if ( ! Promise . allSettled ) {
await Promise . all (
procs . map ( ( p ) = >
p
. then ( ( value ) = > ( {
status : "fulfilled" ,
value ,
} ) )
. catch ( ( reason ) = > ( {
status : "rejected" ,
reason ,
} ) )
)
) ;
} else {
await Promise . allSettled ( procs ) ;
}
2021-10-12 16:50:13 +02:00
}
2021-11-12 12:11:52 +02:00
await runAll ( "UPDATE DATABASE" , onlyInStorage , async ( e ) = > {
Logger ( ` Update into ${ e . path } ` ) ;
await this . updateIntoDB ( e ) ;
} ) ;
await runAll ( "UPDATE STORAGE" , onlyInDatabase , async ( e ) = > {
Logger ( ` Pull from db: ${ e } ` ) ;
await this . pullFile ( e , filesStorage ) ;
} ) ;
await runAll ( "CHECK FILE STATUS" , syncFiles , async ( e ) = > {
await this . syncFileBetweenDBandStorage ( e , filesStorage ) ;
} ) ;
2021-11-10 11:07:09 +02:00
this . statusBar . setText ( ` NOW TRACKING! ` ) ;
2021-11-12 12:11:52 +02:00
Logger ( "Initialized,NOW TRACKING!" ) ;
if ( showingNotice ) {
notice . hide ( ) ;
Logger ( "Initialize done!" , LOG_LEVEL . NOTICE ) ;
}
2021-10-12 16:50:13 +02:00
}
2021-10-13 14:38:44 +02:00
async deleteFolderOnDB ( folder : TFolder ) {
2021-10-21 11:48:42 +02:00
Logger ( ` delete folder: ${ folder . path } ` ) ;
2021-10-26 11:08:01 +02:00
await this . localDatabase . deleteDBEntryPrefix ( folder . path + "/" ) ;
2021-10-13 14:38:44 +02:00
for ( var v of folder . children ) {
let entry = v as TFile & TFolder ;
2021-10-21 11:48:42 +02:00
Logger ( ` ->entry: ${ entry . path } ` , LOG_LEVEL . VERBOSE ) ;
2021-10-13 14:38:44 +02:00
if ( entry . children ) {
2021-10-21 11:48:42 +02:00
Logger ( ` ->is dir ` , LOG_LEVEL . VERBOSE ) ;
2021-10-13 14:38:44 +02:00
await this . deleteFolderOnDB ( entry ) ;
2021-10-15 05:30:06 +02:00
try {
2021-11-04 12:12:43 +02:00
if ( this . settings . trashInsteadDelete ) {
await this . app . vault . trash ( entry , false ) ;
} else {
await this . app . vault . delete ( entry ) ;
}
2021-10-15 05:30:06 +02:00
} catch ( ex ) {
if ( ex . code && ex . code == "ENOENT" ) {
//NO OP.
} else {
2021-10-26 11:08:01 +02:00
Logger ( ` error while delete folder: ${ entry . path } ` , LOG_LEVEL . NOTICE ) ;
2021-10-21 11:48:42 +02:00
Logger ( ex ) ;
2021-10-15 05:30:06 +02:00
}
}
2021-10-13 14:38:44 +02:00
} else {
2021-10-21 11:48:42 +02:00
Logger ( ` ->is file ` , LOG_LEVEL . VERBOSE ) ;
2021-10-14 12:27:08 +02:00
await this . deleteFromDB ( entry ) ;
2021-10-13 14:38:44 +02:00
}
}
2021-10-15 05:30:06 +02:00
try {
2021-11-04 12:12:43 +02:00
if ( this . settings . trashInsteadDelete ) {
await this . app . vault . trash ( folder , false ) ;
} else {
await this . app . vault . delete ( folder ) ;
}
2021-10-15 05:30:06 +02:00
} catch ( ex ) {
if ( ex . code && ex . code == "ENOENT" ) {
//NO OP.
} else {
2021-10-21 11:48:42 +02:00
Logger ( ` error while delete filder: ${ folder . path } ` , LOG_LEVEL . NOTICE ) ;
Logger ( ex ) ;
2021-10-15 05:30:06 +02:00
}
}
2021-10-12 16:50:13 +02:00
}
2021-10-14 12:27:08 +02:00
2021-10-13 14:38:44 +02:00
async renameFolder ( folder : TFolder , oldFile : any ) {
for ( var v of folder . children ) {
let entry = v as TFile & TFolder ;
if ( entry . children ) {
await this . deleteFolderOnDB ( entry ) ;
2021-11-04 12:12:43 +02:00
if ( this . settings . trashInsteadDelete ) {
await this . app . vault . trash ( entry , false ) ;
} else {
await this . app . vault . delete ( entry ) ;
}
2021-10-13 14:38:44 +02:00
} else {
2021-10-14 12:27:08 +02:00
await this . deleteFromDB ( entry ) ;
2021-10-13 14:38:44 +02:00
}
}
2021-10-12 16:50:13 +02:00
}
// --> conflict resolving
async getConflictedDoc ( path : string , rev : string ) : Promise < false | diff_result_leaf > {
try {
2021-10-18 08:07:44 +02:00
let doc = await this . localDatabase . getDBEntry ( path , { rev : rev } ) ;
2021-10-14 12:27:08 +02:00
if ( doc === false ) return false ;
2021-10-17 15:10:03 +02:00
let data = doc . data ;
if ( doc . datatype == "newnote" ) {
data = base64ToString ( doc . data ) ;
} else if ( doc . datatype == "plain" ) {
data = doc . data ;
}
2021-10-12 16:50:13 +02:00
return {
ctime : doc.ctime ,
mtime : doc.mtime ,
rev : rev ,
2021-10-17 15:10:03 +02:00
data : data ,
2021-10-12 16:50:13 +02:00
} ;
} catch ( ex ) {
if ( ex . status && ex . status == 404 ) {
return false ;
}
}
return false ;
}
2021-10-18 08:07:44 +02:00
/ * *
* Getting file conflicted status .
* @param path the file location
* @returns true - > resolved , false - > nothing to do , or check result .
* /
2021-10-12 16:50:13 +02:00
async getConflictedStatus ( path : string ) : Promise < diff_check_result > {
2021-10-18 08:07:44 +02:00
let test = await this . localDatabase . getDBEntry ( path , { conflicts : true } ) ;
if ( test === false ) return false ;
2021-10-13 14:38:44 +02:00
if ( test == null ) return false ;
2021-10-12 16:50:13 +02:00
if ( ! test . _conflicts ) return false ;
if ( test . _conflicts . length == 0 ) return false ;
2021-10-18 08:07:44 +02:00
// should be one or more conflicts;
2021-10-12 16:50:13 +02:00
let leftLeaf = await this . getConflictedDoc ( path , test . _rev ) ;
let rightLeaf = await this . getConflictedDoc ( path , test . _conflicts [ 0 ] ) ;
2021-10-18 08:07:44 +02:00
if ( leftLeaf == false ) {
// what's going on..
2021-10-21 11:48:42 +02:00
Logger ( ` could not get current revisions: ${ path } ` , LOG_LEVEL . NOTICE ) ;
2021-10-18 08:07:44 +02:00
return false ;
}
if ( rightLeaf == false ) {
// Conflicted item could not load, delete this.
await this . localDatabase . deleteDBEntry ( path , { rev : test._conflicts [ 0 ] } ) ;
await this . pullFile ( path , null , true ) ;
2021-10-21 11:48:42 +02:00
Logger ( ` could not get old revisions, automaticaly used newer one: ${ path } ` , LOG_LEVEL . NOTICE ) ;
2021-10-18 08:07:44 +02:00
return true ;
}
2021-10-12 16:50:13 +02:00
// first,check for same contents
if ( leftLeaf . data == rightLeaf . data ) {
let leaf = leftLeaf ;
if ( leftLeaf . mtime > rightLeaf . mtime ) {
leaf = rightLeaf ;
}
2021-10-18 08:07:44 +02:00
await this . localDatabase . deleteDBEntry ( path , { rev : leaf.rev } ) ;
2021-10-12 16:50:13 +02:00
await this . pullFile ( path , null , true ) ;
2021-10-21 11:48:42 +02:00
Logger ( ` automaticaly merged: ${ path } ` ) ;
2021-10-12 16:50:13 +02:00
return true ;
}
2021-10-18 08:07:44 +02:00
// make diff.
2021-10-12 16:50:13 +02:00
let dmp = new diff_match_patch ( ) ;
var diff = dmp . diff_main ( leftLeaf . data , rightLeaf . data ) ;
dmp . diff_cleanupSemantic ( diff ) ;
2021-10-21 11:48:42 +02:00
Logger ( ` conflict(s) found: ${ path } ` ) ;
2021-10-12 16:50:13 +02:00
return {
left : leftLeaf ,
right : rightLeaf ,
diff : diff ,
} ;
}
async showIfConflicted ( file : TFile ) {
let conflictCheckResult = await this . getConflictedStatus ( file . path ) ;
if ( conflictCheckResult === false ) return ; //nothign to do.
if ( conflictCheckResult === true ) {
//auto resolved, but need check again;
setTimeout ( ( ) = > {
this . showIfConflicted ( file ) ;
2021-10-18 08:07:44 +02:00
} , 500 ) ;
2021-10-12 16:50:13 +02:00
return ;
}
//there conflicts, and have to resolve ;
let leaf = this . app . workspace . activeLeaf ;
if ( leaf ) {
new ConflictResolveModal ( this . app , conflictCheckResult , async ( selected ) = > {
2021-10-18 08:07:44 +02:00
let testDoc = await this . localDatabase . getDBEntry ( file . path , { conflicts : true } ) ;
if ( testDoc === false ) return ;
if ( ! testDoc . _conflicts ) {
2021-10-21 11:48:42 +02:00
Logger ( "something went wrong on merging." , LOG_LEVEL . NOTICE ) ;
2021-10-18 08:07:44 +02:00
return ;
}
2021-10-12 16:50:13 +02:00
let toDelete = selected ;
2021-10-18 08:07:44 +02:00
if ( toDelete == null ) {
//concat both,
if ( conflictCheckResult !== false && conflictCheckResult !== true ) {
// write data,and delete both old rev.
let p = conflictCheckResult . diff . map ( ( e ) = > e [ 1 ] ) . join ( "" ) ;
await this . app . vault . modify ( file , p ) ;
await this . localDatabase . deleteDBEntry ( file . path , { rev : conflictCheckResult.left.rev } ) ;
await this . localDatabase . deleteDBEntry ( file . path , { rev : conflictCheckResult.right.rev } ) ;
}
return ;
}
2021-10-12 16:50:13 +02:00
if ( toDelete == "" ) {
return ;
}
2021-10-21 11:48:42 +02:00
Logger ( ` resolved conflict: ${ file . path } ` ) ;
2021-10-18 08:07:44 +02:00
await this . localDatabase . deleteDBEntry ( file . path , { rev : toDelete } ) ;
2021-10-12 16:50:13 +02:00
await this . pullFile ( file . path , null , true ) ;
setTimeout ( ( ) = > {
//resolved, check again.
this . showIfConflicted ( file ) ;
2021-10-18 08:07:44 +02:00
} , 500 ) ;
2021-10-12 16:50:13 +02:00
} ) . open ( ) ;
}
}
2021-10-18 08:07:44 +02:00
async pullFile ( filename : string , fileList? : TFile [ ] , force? : boolean , rev? : string ) {
2021-10-12 16:50:13 +02:00
if ( ! fileList ) {
fileList = this . app . vault . getFiles ( ) ;
}
2021-11-04 12:12:43 +02:00
let targetFiles = fileList . filter ( ( e ) = > e . path == normalizePath ( filename ) ) ;
2021-10-12 16:50:13 +02:00
if ( targetFiles . length == 0 ) {
//have to create;
2021-10-18 08:07:44 +02:00
let doc = await this . localDatabase . getDBEntry ( filename , rev ? { rev : rev } : null ) ;
2021-10-14 12:27:08 +02:00
if ( doc === false ) return ;
2021-10-12 16:50:13 +02:00
await this . doc2storage_create ( doc , force ) ;
} else if ( targetFiles . length == 1 ) {
//normal case
let file = targetFiles [ 0 ] ;
2021-10-18 08:07:44 +02:00
let doc = await this . localDatabase . getDBEntry ( filename , rev ? { rev : rev } : null ) ;
2021-10-14 12:27:08 +02:00
if ( doc === false ) return ;
2021-10-12 16:50:13 +02:00
await this . doc2storate_modify ( doc , file , force ) ;
} else {
2021-10-21 11:48:42 +02:00
Logger ( ` target files: ${ filename } is two or more files in your vault ` ) ;
2021-10-12 16:50:13 +02:00
//something went wrong..
}
//when to opened file;
}
async syncFileBetweenDBandStorage ( file : TFile , fileList? : TFile [ ] ) {
2021-11-04 12:12:43 +02:00
let doc = await this . localDatabase . getDBEntryMeta ( file . path ) ;
2021-10-14 12:27:08 +02:00
if ( doc === false ) return ;
2021-11-12 12:11:52 +02:00
let storageMtime = ~ ~ ( file . stat . mtime / 1000 ) ;
let docMtime = ~ ~ ( doc . mtime / 1000 ) ;
if ( storageMtime > docMtime ) {
2021-10-12 16:50:13 +02:00
//newer local file.
2021-11-12 12:11:52 +02:00
Logger ( "DB -> STORAGE :" + file . path ) ;
Logger ( ` ${ storageMtime } > ${ docMtime } ` ) ;
2021-10-14 12:27:08 +02:00
await this . updateIntoDB ( file ) ;
2021-11-12 12:11:52 +02:00
} else if ( storageMtime < docMtime ) {
2021-10-12 16:50:13 +02:00
//newer database file.
2021-11-12 12:11:52 +02:00
Logger ( "STORAGE <- DB :" + file . path ) ;
Logger ( ` ${ storageMtime } < ${ docMtime } ` ) ;
2021-11-04 12:12:43 +02:00
let docx = await this . localDatabase . getDBEntry ( file . path ) ;
if ( docx != false ) {
await this . doc2storate_modify ( docx , file ) ;
}
2021-10-12 16:50:13 +02:00
} else {
2021-11-12 12:11:52 +02:00
// Logger("EVEN :" + file.path, LOG_LEVEL.VERBOSE);
// Logger(`${storageMtime} = ${docMtime}`, LOG_LEVEL.VERBOSE);
2021-10-12 16:50:13 +02:00
//eq.case
}
}
2021-10-14 12:27:08 +02:00
async updateIntoDB ( file : TFile ) {
2021-10-17 15:10:03 +02:00
let content = "" ;
let datatype : "plain" | "newnote" = "newnote" ;
if ( file . extension != "md" ) {
let contentBin = await this . app . vault . readBinary ( file ) ;
2021-11-10 11:07:09 +02:00
content = await arrayBufferToBase64 ( contentBin ) ;
2021-10-17 15:10:03 +02:00
datatype = "newnote" ;
} else {
content = await this . app . vault . read ( file ) ;
datatype = "plain" ;
}
2021-10-12 16:50:13 +02:00
let fullpath = file . path ;
2021-10-14 12:27:08 +02:00
let d : LoadedEntry = {
2021-10-12 16:50:13 +02:00
_id : fullpath ,
data : content ,
ctime : file.stat.ctime ,
mtime : file.stat.mtime ,
size : file.stat.size ,
2021-10-14 12:27:08 +02:00
children : [ ] ,
2021-10-18 08:07:44 +02:00
datatype : datatype ,
2021-10-12 16:50:13 +02:00
} ;
2021-10-14 12:27:08 +02:00
//From here
2021-10-18 08:07:44 +02:00
let old = await this . localDatabase . getDBEntry ( fullpath ) ;
2021-10-14 12:27:08 +02:00
if ( old !== false ) {
2021-10-13 14:38:44 +02:00
let oldData = { data : old.data , deleted : old._deleted } ;
let newData = { data : d.data , deleted : d._deleted } ;
2021-10-12 16:50:13 +02:00
if ( JSON . stringify ( oldData ) == JSON . stringify ( newData ) ) {
2021-10-21 11:48:42 +02:00
Logger ( "not changed:" + fullpath + ( d . _deleted ? " (deleted)" : "" ) , LOG_LEVEL . VERBOSE ) ;
2021-10-12 16:50:13 +02:00
return ;
}
2021-10-14 12:27:08 +02:00
// d._rev = old._rev;
2021-10-12 16:50:13 +02:00
}
2021-10-14 12:27:08 +02:00
let ret = await this . localDatabase . putDBEntry ( d ) ;
2021-10-12 16:50:13 +02:00
2021-10-21 11:48:42 +02:00
Logger ( "put database:" + fullpath + "(" + datatype + ") " ) ;
2021-10-12 16:50:13 +02:00
if ( this . settings . syncOnSave ) {
await this . replicate ( ) ;
}
}
2021-10-14 12:27:08 +02:00
async deleteFromDB ( file : TFile ) {
2021-10-12 16:50:13 +02:00
let fullpath = file . path ;
2021-10-21 11:48:42 +02:00
Logger ( ` deleteDB By path: ${ fullpath } ` ) ;
2021-10-14 12:27:08 +02:00
await this . deleteFromDBbyPath ( fullpath ) ;
2021-10-12 16:50:13 +02:00
if ( this . settings . syncOnSave ) {
await this . replicate ( ) ;
}
}
2021-10-14 12:27:08 +02:00
async deleteFromDBbyPath ( fullpath : string ) {
await this . localDatabase . deleteDBEntry ( fullpath ) ;
2021-10-12 16:50:13 +02:00
if ( this . settings . syncOnSave ) {
await this . replicate ( ) ;
}
}
async resetLocalDatabase() {
2021-10-14 12:27:08 +02:00
await this . localDatabase . resetDatabase ( ) ;
2021-10-12 16:50:13 +02:00
}
2021-10-14 12:27:08 +02:00
async tryResetRemoteDatabase() {
await this . localDatabase . tryResetRemoteDatabase ( this . settings ) ;
2021-10-12 16:50:13 +02:00
}
2021-10-14 12:27:08 +02:00
async tryCreateRemoteDatabase() {
await this . localDatabase . tryCreateRemoteDatabase ( this . settings ) ;
2021-10-12 16:50:13 +02:00
}
}
class LogDisplayModal extends Modal {
2021-10-13 14:38:44 +02:00
plugin : ObsidianLiveSyncPlugin ;
logEl : HTMLDivElement ;
constructor ( app : App , plugin : ObsidianLiveSyncPlugin ) {
2021-10-12 16:50:13 +02:00
super ( app ) ;
2021-10-13 14:38:44 +02:00
this . plugin = plugin ;
}
updateLog() {
let msg = "" ;
2021-10-15 05:30:06 +02:00
for ( var v of this . plugin . logMessage ) {
2021-10-13 14:38:44 +02:00
msg += escapeStringToHTML ( v ) + "<br>" ;
}
this . logEl . innerHTML = msg ;
}
onOpen() {
let { contentEl } = this ;
contentEl . empty ( ) ;
contentEl . createEl ( "h2" , { text : "Sync Status" } ) ;
let div = contentEl . createDiv ( "" ) ;
div . addClass ( "op-scrollable" ) ;
div . addClass ( "op-pre" ) ;
this . logEl = div ;
2021-10-15 05:30:06 +02:00
this . updateLog = this . updateLog . bind ( this ) ;
2021-11-10 11:07:09 +02:00
this . plugin . addLogHook = this . updateLog ;
2021-10-13 14:38:44 +02:00
this . updateLog ( ) ;
}
onClose() {
let { contentEl } = this ;
contentEl . empty ( ) ;
2021-11-10 11:07:09 +02:00
this . plugin . addLogHook = null ;
2021-10-12 16:50:13 +02:00
}
}
class ConflictResolveModal extends Modal {
// result: Array<[number, string]>;
result : diff_result ;
callback : ( remove_rev : string ) = > Promise < void > ;
constructor ( app : App , diff : diff_result , callback : ( remove_rev : string ) = > Promise < void > ) {
super ( app ) ;
this . result = diff ;
this . callback = callback ;
}
onOpen() {
let { contentEl } = this ;
contentEl . empty ( ) ;
contentEl . createEl ( "h2" , { text : "This document has conflicted changes." } ) ;
let div = contentEl . createDiv ( "" ) ;
div . addClass ( "op-scrollable" ) ;
let diff = "" ;
// const showContents = this.result.map((e) => (e[0] == 1 ? "<span class='added'>" + htmlEscape(e[1]) + "</span>" : e[0] == -1 ? "<span class='deleted'>" + htmlEscape(e[1]) + "</span>" : "<span class='normal'>" + htmlEscape(e[1]) + "</span>"));
for ( let v of this . result . diff ) {
let x1 = v [ 0 ] ;
let x2 = v [ 1 ] ;
if ( x1 == DIFF_DELETE ) {
diff += "<span class='deleted'>" + escapeStringToHTML ( x2 ) + "</span>" ;
} else if ( x1 == DIFF_EQUAL ) {
diff += "<span class='normal'>" + escapeStringToHTML ( x2 ) + "</span>" ;
} else if ( x1 == DIFF_INSERT ) {
diff += "<span class='added'>" + escapeStringToHTML ( x2 ) + "</span>" ;
}
}
diff = diff . replace ( /\n/g , "<br>" ) ;
div . innerHTML = diff ;
let div2 = contentEl . createDiv ( "" ) ;
let date1 = new Date ( this . result . left . mtime ) . toLocaleString ( ) ;
let date2 = new Date ( this . result . right . mtime ) . toLocaleString ( ) ;
div2 . innerHTML = `
< span class = 'deleted' > A :$ { date1 } < / span > < br / > < span class = 'added' > B :$ { date2 } < / span > < br >
` ;
contentEl . createEl ( "button" , { text : "Keep A" } , ( e ) = > {
e . addEventListener ( "click" , async ( ) = > {
await this . callback ( this . result . right . rev ) ;
this . close ( ) ;
} ) ;
} ) ;
contentEl . createEl ( "button" , { text : "Keep B" } , ( e ) = > {
e . addEventListener ( "click" , async ( ) = > {
await this . callback ( this . result . left . rev ) ;
this . close ( ) ;
} ) ;
} ) ;
2021-10-18 08:07:44 +02:00
contentEl . createEl ( "button" , { text : "Concat both" } , ( e ) = > {
e . addEventListener ( "click" , async ( ) = > {
await this . callback ( null ) ;
this . close ( ) ;
} ) ;
} ) ;
2021-10-12 16:50:13 +02:00
contentEl . createEl ( "button" , { text : "Not now" } , ( e ) = > {
e . addEventListener ( "click" , async ( ) = > {
this . close ( ) ;
} ) ;
} ) ;
}
onClose() {
let { contentEl } = this ;
contentEl . empty ( ) ;
}
}
class ObsidianLiveSyncSettingTab extends PluginSettingTab {
plugin : ObsidianLiveSyncPlugin ;
constructor ( app : App , plugin : ObsidianLiveSyncPlugin ) {
super ( app , plugin ) ;
this . plugin = plugin ;
}
2021-10-14 12:27:08 +02:00
async testConnection ( ) : Promise < void > {
2021-11-15 05:25:18 +02:00
let db = await connectRemoteCouchDB ( this . plugin . settings . couchDB_URI + ( this . plugin . settings . couchDB_DBNAME == "" ? "" : "/" + this . plugin . settings . couchDB_DBNAME ) , {
2021-10-13 14:38:44 +02:00
username : this.plugin.settings.couchDB_USER ,
password : this.plugin.settings.couchDB_PASSWORD ,
2021-10-12 16:50:13 +02:00
} ) ;
2021-10-13 14:38:44 +02:00
if ( db === false ) {
2021-11-15 05:25:18 +02:00
this . plugin . addLog ( ` could not connect to ${ this . plugin . settings . couchDB_URI } : ${ this . plugin . settings . couchDB_DBNAME } ` , LOG_LEVEL . NOTICE ) ;
2021-10-13 14:38:44 +02:00
return ;
2021-10-12 16:50:13 +02:00
}
2021-10-18 08:07:44 +02:00
this . plugin . addLog ( ` Connected to ${ db . info . db_name } ` , LOG_LEVEL . NOTICE ) ;
2021-10-12 16:50:13 +02:00
}
display ( ) : void {
let { containerEl } = this ;
containerEl . empty ( ) ;
2021-11-05 09:38:45 +02:00
containerEl . createEl ( "h2" , { text : "Settings for Self-hosted LiveSync." } ) ;
2021-10-12 16:50:13 +02:00
2021-11-15 05:25:18 +02:00
containerEl . createEl ( "h3" , { text : "Remote Database configuration" } ) ;
const isAnySyncEnabled = ( ) : boolean = > {
if ( this . plugin . settings . liveSync ) return true ;
if ( this . plugin . settings . periodicReplication ) return true ;
if ( this . plugin . settings . syncOnFileOpen ) return true ;
if ( this . plugin . settings . syncOnSave ) return true ;
if ( this . plugin . settings . syncOnStart ) return true ;
} ;
const applyDisplayEnabled = ( ) = > {
if ( isAnySyncEnabled ( ) ) {
dbsettings . forEach ( ( e ) = > {
e . setDisabled ( true ) . setTooltip ( "When any sync is enabled, It cound't be changed." ) ;
} ) ;
} else {
dbsettings . forEach ( ( e ) = > {
e . setDisabled ( false ) . setTooltip ( "" ) ;
} ) ;
}
if ( this . plugin . settings . liveSync ) {
syncNonLive . forEach ( ( e ) = > {
e . setDisabled ( true ) . setTooltip ( "" ) ;
} ) ;
syncLive . forEach ( ( e ) = > {
e . setDisabled ( false ) . setTooltip ( "" ) ;
} ) ;
} else if ( this . plugin . settings . syncOnFileOpen || this . plugin . settings . syncOnSave || this . plugin . settings . syncOnStart || this . plugin . settings . periodicReplication ) {
syncNonLive . forEach ( ( e ) = > {
e . setDisabled ( false ) . setTooltip ( "" ) ;
} ) ;
syncLive . forEach ( ( e ) = > {
e . setDisabled ( true ) . setTooltip ( "" ) ;
} ) ;
} else {
syncNonLive . forEach ( ( e ) = > {
e . setDisabled ( false ) . setTooltip ( "" ) ;
} ) ;
syncLive . forEach ( ( e ) = > {
e . setDisabled ( false ) . setTooltip ( "" ) ;
} ) ;
}
} ;
let dbsettings : Setting [ ] = [ ] ;
dbsettings . push (
new Setting ( containerEl ) . setName ( "URI" ) . addText ( ( text ) = >
2021-10-12 16:50:13 +02:00
text
2021-11-15 05:25:18 +02:00
. setPlaceholder ( "https://........" )
. setValue ( this . plugin . settings . couchDB_URI )
2021-10-12 16:50:13 +02:00
. onChange ( async ( value ) = > {
2021-11-15 05:25:18 +02:00
this . plugin . settings . couchDB_URI = value ;
2021-10-12 16:50:13 +02:00
await this . plugin . saveSettings ( ) ;
} )
2021-11-15 05:25:18 +02:00
) ,
new Setting ( containerEl )
. setName ( "Username" )
. setDesc ( "username" )
. addText ( ( text ) = >
text
. setPlaceholder ( "" )
. setValue ( this . plugin . settings . couchDB_USER )
. onChange ( async ( value ) = > {
this . plugin . settings . couchDB_USER = value ;
await this . plugin . saveSettings ( ) ;
} )
) ,
new Setting ( containerEl )
. setName ( "Password" )
. setDesc ( "password" )
. addText ( ( text ) = > {
text . setPlaceholder ( "" )
. setValue ( this . plugin . settings . couchDB_PASSWORD )
. onChange ( async ( value ) = > {
this . plugin . settings . couchDB_PASSWORD = value ;
await this . plugin . saveSettings ( ) ;
} ) ;
text . inputEl . setAttribute ( "type" , "password" ) ;
} ) ,
new Setting ( containerEl ) . setName ( "Database name" ) . addText ( ( text ) = >
text
. setPlaceholder ( "" )
. setValue ( this . plugin . settings . couchDB_DBNAME )
2021-10-12 16:50:13 +02:00
. onChange ( async ( value ) = > {
2021-11-15 05:25:18 +02:00
this . plugin . settings . couchDB_DBNAME = value ;
2021-10-12 16:50:13 +02:00
await this . plugin . saveSettings ( ) ;
2021-11-15 05:25:18 +02:00
} )
)
) ;
2021-10-26 11:08:01 +02:00
new Setting ( containerEl )
. setName ( "Test Database Connection" )
. setDesc ( "Open database connection. If the remote database is not found and you have the privilege to create a database, the database will be created." )
. addButton ( ( button ) = >
button
. setButtonText ( "Test" )
. setDisabled ( false )
. onClick ( async ( ) = > {
await this . testConnection ( ) ;
} )
) ;
2021-10-12 16:50:13 +02:00
2021-11-15 05:25:18 +02:00
containerEl . createEl ( "h3" , { text : "Local Database configuration" } ) ;
// new Setting(containerEl)
// .setName("File to Database saving delay")
// .setDesc("ms, between 200 and 5000, restart required.")
// .addText((text) => {
// text.setPlaceholder("")
// .setValue(this.plugin.settings.savingDelay + "")
// .onChange(async (value) => {
// let v = Number(value);
// if (isNaN(v) || v < 200 || v > 5000) {
// return 200;
// //text.inputEl.va;
// }
// this.plugin.settings.savingDelay = v;
// await this.plugin.saveSettings();
// });
// text.inputEl.setAttribute("type", "number");
// });
2021-10-15 10:58:42 +02:00
new Setting ( containerEl )
2021-11-15 05:25:18 +02:00
. setName ( "Auto Garbage Collection delay" )
2021-10-15 10:58:42 +02:00
. setDesc ( "(seconds), if you set zero, you have to run manually." )
. addText ( ( text ) = > {
text . setPlaceholder ( "" )
. setValue ( this . plugin . settings . gcDelay + "" )
. onChange ( async ( value ) = > {
let v = Number ( value ) ;
2021-10-18 08:07:44 +02:00
if ( isNaN ( v ) || v > 5000 ) {
return 0 ;
2021-10-15 10:58:42 +02:00
}
this . plugin . settings . gcDelay = v ;
await this . plugin . saveSettings ( ) ;
} ) ;
text . inputEl . setAttribute ( "type" , "number" ) ;
} ) ;
2021-11-15 05:25:18 +02:00
new Setting ( containerEl ) . setName ( "Manual Garbage Collect" ) . addButton ( ( button ) = >
button
. setButtonText ( "Collect now" )
. setDisabled ( false )
. onClick ( async ( ) = > {
await this . plugin . garbageCollect ( ) ;
2021-10-18 08:07:44 +02:00
} )
2021-11-15 05:25:18 +02:00
) ;
2021-11-12 12:11:52 +02:00
new Setting ( containerEl )
2021-11-15 05:25:18 +02:00
. setName ( "End to End Encryption (beta)" )
2021-11-12 12:11:52 +02:00
. setDesc ( "Encrypting contents on the database." )
. addToggle ( ( toggle ) = >
toggle . setValue ( this . plugin . settings . workingEncrypt ) . onChange ( async ( value ) = > {
this . plugin . settings . workingEncrypt = value ;
2021-11-15 05:25:18 +02:00
phasspharase . setDisabled ( ! value ) ;
2021-11-12 12:11:52 +02:00
await this . plugin . saveSettings ( ) ;
} )
) ;
2021-11-15 05:25:18 +02:00
let phasspharase = new Setting ( containerEl )
2021-11-12 12:11:52 +02:00
. setName ( "Passphrase" )
. setDesc ( "Encrypting passphrase" )
. addText ( ( text ) = > {
text . setPlaceholder ( "" )
. setValue ( this . plugin . settings . workingPassphrase )
. onChange ( async ( value ) = > {
this . plugin . settings . workingPassphrase = value ;
await this . plugin . saveSettings ( ) ;
} ) ;
text . inputEl . setAttribute ( "type" , "password" ) ;
} ) ;
2021-11-15 05:25:18 +02:00
phasspharase . setDisabled ( ! this . plugin . settings . workingEncrypt ) ;
containerEl . createEl ( "div" , {
text : "When you change any encryption enabled or passphrase, you have to reset all databases to make sure that the last password is unused and erase encrypted data from anywhere. This operation will not lost your vault if you are fully synced." ,
} ) ;
2021-11-12 12:11:52 +02:00
const applyEncryption = async ( sendToServer : boolean ) = > {
if ( this . plugin . settings . workingEncrypt && this . plugin . settings . workingPassphrase == "" ) {
Logger ( "If you enable encryption, you have to set the passphrase" , LOG_LEVEL . NOTICE ) ;
return ;
}
if ( this . plugin . settings . workingEncrypt && ! ( await testCrypt ( ) ) ) {
Logger ( "WARNING! Your device would not support encryption." , LOG_LEVEL . NOTICE ) ;
return ;
}
if ( ! this . plugin . settings . workingEncrypt ) {
this . plugin . settings . workingPassphrase = "" ;
}
this . plugin . settings . liveSync = false ;
this . plugin . settings . periodicReplication = false ;
this . plugin . settings . syncOnSave = false ;
this . plugin . settings . syncOnStart = false ;
this . plugin . settings . encrypt = this . plugin . settings . workingEncrypt ;
this . plugin . settings . passphrase = this . plugin . settings . workingPassphrase ;
await this . plugin . saveSettings ( ) ;
await this . plugin . resetLocalDatabase ( ) ;
if ( sendToServer ) {
await this . plugin . initializeDatabase ( true ) ;
await this . plugin . markRemoteLocked ( ) ;
await this . plugin . tryResetRemoteDatabase ( ) ;
await this . plugin . markRemoteLocked ( ) ;
await this . plugin . replicateAllToServer ( true ) ;
} else {
await this . plugin . markRemoteResolved ( ) ;
await this . plugin . replicate ( true ) ;
}
} ;
new Setting ( containerEl )
. setName ( "Apply" )
. setDesc ( "apply encryption settinngs, and re-initialize database" )
. addButton ( ( button ) = >
button
. setButtonText ( "Apply and send" )
. setWarning ( )
. setDisabled ( false )
. onClick ( async ( ) = > {
await applyEncryption ( true ) ;
} )
)
. addButton ( ( button ) = >
button
. setButtonText ( "Apply and receive" )
. setWarning ( )
. setDisabled ( false )
. onClick ( async ( ) = > {
await applyEncryption ( false ) ;
} )
) ;
2021-11-15 05:25:18 +02:00
containerEl . createEl ( "h3" , { text : "General Settings" } ) ;
new Setting ( containerEl )
. setName ( "Do not show low-priority Log" )
. setDesc ( "Reduce log infomations" )
. addToggle ( ( toggle ) = >
toggle . setValue ( this . plugin . settings . lessInformationInLog ) . onChange ( async ( value ) = > {
this . plugin . settings . lessInformationInLog = value ;
await this . plugin . saveSettings ( ) ;
} )
) ;
new Setting ( containerEl )
. setName ( "Verbose Log" )
. setDesc ( "Show verbose log " )
. addToggle ( ( toggle ) = >
toggle . setValue ( this . plugin . settings . showVerboseLog ) . onChange ( async ( value ) = > {
this . plugin . settings . showVerboseLog = value ;
await this . plugin . saveSettings ( ) ;
} )
) ;
2021-10-19 10:53:54 +02:00
containerEl . createEl ( "h3" , { text : "Sync setting" } ) ;
2021-10-17 15:10:03 +02:00
if ( this . plugin . settings . versionUpFlash != "" ) {
let c = containerEl . createEl ( "div" , { text : this.plugin.settings.versionUpFlash } ) ;
c . createEl ( "button" , { text : "I got it and updated." } , ( e ) = > {
e . addEventListener ( "click" , async ( ) = > {
this . plugin . settings . versionUpFlash = "" ;
2021-10-19 10:53:54 +02:00
await this . plugin . saveSettings ( ) ;
2021-11-15 05:25:18 +02:00
applyDisplayEnabled ( ) ;
2021-10-17 15:10:03 +02:00
c . remove ( ) ;
} ) ;
} ) ;
2021-10-18 08:07:44 +02:00
c . addClass ( "op-warn" ) ;
2021-10-17 15:10:03 +02:00
}
2021-10-19 10:53:54 +02:00
2021-11-15 05:25:18 +02:00
let syncLive : Setting [ ] = [ ] ;
let syncNonLive : Setting [ ] = [ ] ;
syncLive . push (
new Setting ( containerEl )
. setName ( "LiveSync" )
. setDesc ( "Sync realtime" )
. addToggle ( ( toggle ) = >
toggle . setValue ( this . plugin . settings . liveSync ) . onChange ( async ( value ) = > {
this . plugin . settings . liveSync = value ;
// ps.setDisabled(value);
await this . plugin . saveSettings ( ) ;
applyDisplayEnabled ( ) ;
this . plugin . realizeSettingSyncMode ( ) ;
} )
)
) ;
2021-11-10 11:07:09 +02:00
2021-11-15 05:25:18 +02:00
syncNonLive . push (
new Setting ( containerEl )
. setName ( "Periodic Sync" )
. setDesc ( "Sync periodically" )
. addToggle ( ( toggle ) = >
toggle . setValue ( this . plugin . settings . periodicReplication ) . onChange ( async ( value ) = > {
this . plugin . settings . periodicReplication = value ;
2021-11-10 11:07:09 +02:00
await this . plugin . saveSettings ( ) ;
2021-11-15 05:25:18 +02:00
applyDisplayEnabled ( ) ;
} )
) ,
new Setting ( containerEl )
. setName ( "Periodic sync intreval" )
. setDesc ( "Interval (sec)" )
. addText ( ( text ) = > {
text . setPlaceholder ( "" )
. setValue ( this . plugin . settings . periodicReplicationInterval + "" )
. onChange ( async ( value ) = > {
let v = Number ( value ) ;
if ( isNaN ( v ) || v > 5000 ) {
return 0 ;
}
this . plugin . settings . periodicReplicationInterval = v ;
await this . plugin . saveSettings ( ) ;
applyDisplayEnabled ( ) ;
} ) ;
text . inputEl . setAttribute ( "type" , "number" ) ;
} ) ,
new Setting ( containerEl )
. setName ( "Sync on Save" )
. setDesc ( "When you save file, sync automatically" )
. addToggle ( ( toggle ) = >
toggle . setValue ( this . plugin . settings . syncOnSave ) . onChange ( async ( value ) = > {
this . plugin . settings . syncOnSave = value ;
await this . plugin . saveSettings ( ) ;
applyDisplayEnabled ( ) ;
} )
) ,
new Setting ( containerEl )
. setName ( "Sync on File Open" )
. setDesc ( "When you open file, sync automatically" )
. addToggle ( ( toggle ) = >
toggle . setValue ( this . plugin . settings . syncOnFileOpen ) . onChange ( async ( value ) = > {
this . plugin . settings . syncOnFileOpen = value ;
await this . plugin . saveSettings ( ) ;
applyDisplayEnabled ( ) ;
} )
) ,
new Setting ( containerEl )
. setName ( "Sync on Start" )
. setDesc ( "Start synchronization on Obsidian started." )
. addToggle ( ( toggle ) = >
toggle . setValue ( this . plugin . settings . syncOnStart ) . onChange ( async ( value ) = > {
this . plugin . settings . syncOnStart = value ;
await this . plugin . saveSettings ( ) ;
applyDisplayEnabled ( ) ;
} )
)
) ;
2021-11-10 11:07:09 +02:00
2021-10-12 16:50:13 +02:00
new Setting ( containerEl )
2021-11-15 05:25:18 +02:00
. setName ( "Use Trash for deleted files" )
. setDesc ( "Do not delete files that deleted in remote, just move to trash." )
2021-10-12 16:50:13 +02:00
. addToggle ( ( toggle ) = >
2021-11-15 05:25:18 +02:00
toggle . setValue ( this . plugin . settings . trashInsteadDelete ) . onChange ( async ( value ) = > {
this . plugin . settings . trashInsteadDelete = value ;
2021-10-12 16:50:13 +02:00
await this . plugin . saveSettings ( ) ;
} )
) ;
2021-10-19 10:53:54 +02:00
2021-11-04 12:12:43 +02:00
new Setting ( containerEl )
2021-11-15 05:25:18 +02:00
. setName ( "Do not delete empty folder" )
. setDesc ( "Normally, folder is deleted When the folder became empty by replication. enable this, leave it as is" )
2021-11-04 12:12:43 +02:00
. addToggle ( ( toggle ) = >
2021-11-15 05:25:18 +02:00
toggle . setValue ( this . plugin . settings . doNotDeleteFolder ) . onChange ( async ( value ) = > {
this . plugin . settings . doNotDeleteFolder = value ;
2021-11-04 12:12:43 +02:00
await this . plugin . saveSettings ( ) ;
} )
) ;
2021-10-17 15:10:03 +02:00
new Setting ( containerEl )
. setName ( "Minimum chunk size" )
. setDesc ( "(letters), minimum chunk size." )
. addText ( ( text ) = > {
text . setPlaceholder ( "" )
. setValue ( this . plugin . settings . minimumChunkSize + "" )
. onChange ( async ( value ) = > {
let v = Number ( value ) ;
if ( isNaN ( v ) || v < 10 || v > 1000 ) {
return 10 ;
}
this . plugin . settings . minimumChunkSize = v ;
await this . plugin . saveSettings ( ) ;
} ) ;
text . inputEl . setAttribute ( "type" , "number" ) ;
} ) ;
2021-10-19 10:53:54 +02:00
2021-10-17 15:10:03 +02:00
new Setting ( containerEl )
. setName ( "LongLine Threshold" )
. setDesc ( "(letters), If the line is longer than this, make the line to chunk" )
. addText ( ( text ) = > {
text . setPlaceholder ( "" )
. setValue ( this . plugin . settings . longLineThreshold + "" )
. onChange ( async ( value ) = > {
let v = Number ( value ) ;
if ( isNaN ( v ) || v < 10 || v > 1000 ) {
return 10 ;
}
this . plugin . settings . longLineThreshold = v ;
await this . plugin . saveSettings ( ) ;
} ) ;
text . inputEl . setAttribute ( "type" , "number" ) ;
} ) ;
2021-10-19 10:53:54 +02:00
containerEl . createEl ( "h3" , { text : "Hatch" } ) ;
2021-10-26 11:08:01 +02:00
if ( this . plugin . localDatabase . remoteLockedAndDeviceNotAccepted ) {
let c = containerEl . createEl ( "div" , {
text : "To prevent unwanted vault corruption, the remote database has been locked for synchronization, and this device was not marked as 'resolved'. it caused by some operations like this. re-initialized. Local database initialization should be required. please back your vault up, reset local database, and press 'Mark this device as resolved'. " ,
} ) ;
c . createEl ( "button" , { text : "I'm ready, mark this device 'resolved'" } , ( e ) = > {
e . addEventListener ( "click" , async ( ) = > {
await this . plugin . markRemoteResolved ( ) ;
c . remove ( ) ;
} ) ;
} ) ;
c . addClass ( "op-warn" ) ;
} else {
if ( this . plugin . localDatabase . remoteLocked ) {
let c = containerEl . createEl ( "div" , {
text : "To prevent unwanted vault corruption, the remote database has been locked for synchronization. (This device is marked 'resolved') When all your devices are marked 'resolved', unlock the database." ,
} ) ;
c . createEl ( "button" , { text : "I'm ready, unlock the database" } , ( e ) = > {
e . addEventListener ( "click" , async ( ) = > {
await this . plugin . markRemoteUnlocked ( ) ;
c . remove ( ) ;
} ) ;
} ) ;
c . addClass ( "op-warn" ) ;
}
}
2021-11-15 05:25:18 +02:00
const dropHistory = async ( sendToServer : boolean ) = > {
this . plugin . settings . liveSync = false ;
this . plugin . settings . periodicReplication = false ;
this . plugin . settings . syncOnSave = false ;
this . plugin . settings . syncOnStart = false ;
2021-10-26 11:08:01 +02:00
2021-11-15 05:25:18 +02:00
await this . plugin . saveSettings ( ) ;
applyDisplayEnabled ( ) ;
await this . plugin . resetLocalDatabase ( ) ;
if ( sendToServer ) {
await this . plugin . initializeDatabase ( true ) ;
await this . plugin . markRemoteLocked ( ) ;
await this . plugin . tryResetRemoteDatabase ( ) ;
await this . plugin . markRemoteLocked ( ) ;
await this . plugin . replicateAllToServer ( true ) ;
} else {
await this . plugin . markRemoteResolved ( ) ;
await this . plugin . replicate ( true ) ;
}
} ;
2021-10-26 11:08:01 +02:00
new Setting ( containerEl )
. setName ( "Drop History" )
2021-11-15 05:25:18 +02:00
. setDesc ( "Initialize local and remote database, and send all or retrieve all again." )
2021-10-26 11:08:01 +02:00
. addButton ( ( button ) = >
button
2021-11-15 05:25:18 +02:00
. setButtonText ( "Drop and send" )
2021-10-26 11:08:01 +02:00
. setWarning ( )
. setDisabled ( false )
. onClick ( async ( ) = > {
2021-11-15 05:25:18 +02:00
await dropHistory ( true ) ;
} )
)
. addButton ( ( button ) = >
button
. setButtonText ( "Drop and receive" )
. setWarning ( )
. setDisabled ( false )
. onClick ( async ( ) = > {
await dropHistory ( false ) ;
2021-10-26 11:08:01 +02:00
} )
) ;
2021-11-15 05:25:18 +02:00
2021-10-26 11:08:01 +02:00
new Setting ( containerEl )
. setName ( "Lock remote database" )
. setDesc ( "Lock remote database for synchronize" )
. addButton ( ( button ) = >
button
. setButtonText ( "Lock" )
. setDisabled ( false )
. onClick ( async ( ) = > {
await this . plugin . markRemoteLocked ( ) ;
} )
) ;
2021-10-19 10:53:54 +02:00
new Setting ( containerEl )
. setName ( "Suspend file watching" )
. setDesc ( "if enables it, all file operations are ignored." )
. addToggle ( ( toggle ) = >
toggle . setValue ( this . plugin . settings . suspendFileWatching ) . onChange ( async ( value ) = > {
this . plugin . settings . suspendFileWatching = value ;
await this . plugin . saveSettings ( ) ;
} )
) ;
2021-10-26 11:08:01 +02:00
new Setting ( containerEl )
. setName ( "Reset remote database" )
. setDesc ( "Reset remote database, this affects only database. If you replicate again, remote database will restored by local database." )
. addButton ( ( button ) = >
button
. setButtonText ( "Reset" )
. setDisabled ( false )
. onClick ( async ( ) = > {
await this . plugin . tryResetRemoteDatabase ( ) ;
} )
) ;
new Setting ( containerEl )
. setName ( "Reset local database" )
. setDesc ( "Reset local database, this affects only database. If you replicate again, local database will restored by remote database." )
. addButton ( ( button ) = >
button
. setButtonText ( "Reset" )
. setDisabled ( false )
. onClick ( async ( ) = > {
await this . plugin . resetLocalDatabase ( ) ;
} )
) ;
new Setting ( containerEl )
. setName ( "Initialize local database again" )
. setDesc ( "WARNING: Reset local database and reconstruct by storage data. It affects local database, but if you replicate remote as is, remote data will be merged or corrupted." )
. addButton ( ( button ) = >
button
. setButtonText ( "INITIALIZE" )
. setWarning ( )
. setDisabled ( false )
. onClick ( async ( ) = > {
await this . plugin . resetLocalDatabase ( ) ;
await this . plugin . initializeDatabase ( ) ;
} )
) ;
2021-10-19 10:53:54 +02:00
containerEl . createEl ( "h3" , { text : "Corrupted data" } ) ;
if ( Object . keys ( this . plugin . localDatabase . corruptedEntries ) . length > 0 ) {
let cx = containerEl . createEl ( "div" , { text : "If you have copy of these items on any device, simply edit once or twice. Or not, delete this. sorry.." } ) ;
for ( let k in this . plugin . localDatabase . corruptedEntries ) {
let xx = cx . createEl ( "div" , { text : ` ${ k } ` } ) ;
let ba = xx . createEl ( "button" , { text : ` Delete this ` } , ( e ) = > {
e . addEventListener ( "click" , async ( ) = > {
await this . plugin . localDatabase . deleteDBEntry ( k ) ;
xx . remove ( ) ;
} ) ;
} ) ;
}
2021-11-10 11:07:09 +02:00
} else {
let cx = containerEl . createEl ( "div" , { text : "There's no collupted data." } ) ;
2021-10-19 10:53:54 +02:00
}
2021-11-15 05:25:18 +02:00
applyDisplayEnabled ( ) ;
2021-10-12 16:50:13 +02:00
}
}