2021-10-18 08:07:44 +02:00
import { App , debounce , Modal , Notice , Plugin , PluginSettingTab , Setting , TFile , addIcon , TFolder } 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-10-19 10:53:54 +02:00
const LEAF_WAIT_TIMEOUT = 30000 ; // 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-10-14 12:27:08 +02:00
liveSync : boolean ;
2021-10-12 16:50:13 +02:00
syncOnSave : boolean ;
syncOnStart : 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-10-12 16:50:13 +02:00
}
const DEFAULT_SETTINGS : ObsidianLiveSyncSettings = {
couchDB_URI : "" ,
couchDB_USER : "" ,
couchDB_PASSWORD : "" ,
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-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.
function arrayBufferToBase64 ( buffer : ArrayBuffer ) {
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 ) ;
}
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 } > = > {
if ( ! isValidRemoteCouchDBURI ( uri ) ) false ;
let db = new PouchDB ( uri , {
auth ,
} ) ;
try {
let info = await db . info ( ) ;
return { db : db , info : info } ;
} catch ( ex ) {
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-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-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-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
this . initializeDatabase ( ) ;
}
close() {
this . localDatabase . close ( ) ;
}
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-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-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 ) ;
}
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 ) ;
} ) ;
this . changeHandler = changes ;
2021-10-18 08:07:44 +02:00
await this . prepareHashFunctions ( ) ;
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 ;
const { h32 , h64 } = await xxhash ( ) ;
this . h32 = h32 ;
this . h64 = h64 ;
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" ) {
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" ) {
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 ;
}
}
}
async getDBEntry ( id : string , opt? : PouchDB.Core.GetOptions , retryCount = 5 ) : 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-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-10-19 10:53:54 +02:00
let childrens ;
try {
childrens = await Promise . all ( obj . children . map ( ( e ) = > this . getDBLeaf ( e ) ) ) ;
} 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-19 10:53:54 +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-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 ) ;
console . log ( "!" + v . id ) ;
} else {
if ( ! v . id . startsWith ( "h:" ) ) {
console . log ( "?" + v . id ) ;
}
}
}
}
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-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-10-17 15:10:03 +02:00
if ( note . _id . endsWith ( ".md" ) ) {
pieceSize = MAX_DOC_SIZE ;
plainSplit = true ;
2021-10-15 10:58:42 +02:00
}
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-21 11:48:42 +02:00
let minimumChunkSize = this . settings . minimumChunkSize ;
2021-10-17 15:10:03 +02:00
if ( minimumChunkSize < 10 ) minimumChunkSize = 10 ;
2021-10-21 11:48:42 +02:00
let longLineThreshold = this . settings . longLineThreshold ;
2021-10-17 15:10:03 +02:00
if ( longLineThreshold < 100 ) longLineThreshold = 100 ;
if ( plainSplit ) {
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
cPieceSize = n1 + 1 ;
} else {
// cPieceSize = Math.min.apply([n2, n3, n4].filter((e) => e > 1));
// ^ heavy.
if ( n2 > 0 && cPieceSize < n2 ) cPieceSize = n2 + 1 ;
if ( n3 > 0 && cPieceSize < n3 ) cPieceSize = n3 + 3 ;
if ( n4 > 0 && cPieceSize < n4 ) cPieceSize = n4 + 0 ;
cPieceSize ++ ;
}
2021-10-18 08:07:44 +02:00
} while ( cPieceSize < minimumChunkSize ) ;
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-10-15 10:58:42 +02:00
// Get has 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 {
hashedPiece = this . h32 ( piece ) ;
leafid = "h:" + hashedPiece ;
do {
let nleafid = leafid ;
try {
nleafid = ` ${ leafid } ${ hashQ } ` ;
let pieceData = await this . localDatabase . get < EntryLeaf > ( nleafid ) ;
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 ;
} else {
needMake = false ;
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
let d : EntryLeaf = {
_id : leafid ,
data : piece ,
type : "leaf" ,
} ;
let result = await this . localDatabase . put ( d ) ;
this . updateRecentModifiedDocs ( result . id , result . rev , d . _deleted ) ;
if ( result . ok ) {
2021-10-21 11:48:42 +02:00
Logger ( ` save ok:id: ${ result . id } rev: ${ result . rev } ` , LOG_LEVEL . VERBOSE ) ;
2021-10-18 08:07:44 +02:00
this . hashCache [ piece ] = leafid ;
this . hashCacheRev [ leafid ] = piece ;
made ++ ;
2021-10-15 10:58:42 +02:00
} else {
2021-10-21 11:48:42 +02:00
Logger ( "save faild" ) ;
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-18 08:07:44 +02:00
2021-10-14 12:27:08 +02:00
savenNotes . push ( leafid ) ;
} while ( leftData != "" ) ;
2021-10-21 11:48:42 +02:00
Logger ( ` note content saven, pieces: ${ processed } new: ${ made } , skip: ${ skiped } , cache: ${ cacheUsed } ` ) ;
2021-10-17 15:10:03 +02:00
let newDoc : PlainEntry | NewEntry = {
2021-10-14 12:27:08 +02:00
NewNote : true ,
children : savenNotes ,
_id : note._id ,
ctime : note.ctime ,
mtime : note.mtime ,
size : note.size ,
2021-10-17 15:10:03 +02:00
type : plainSplit ? "plain" : "newnote" ,
2021-10-14 12:27:08 +02:00
} ;
// Here for upsert logic,
try {
let old = await this . localDatabase . get ( newDoc . _id ) ;
2021-10-17 15:10:03 +02:00
if ( ! old . type || old . type == "notes" || old . type == "newnote" || old . type == "plain" ) {
2021-10-14 12:27:08 +02:00
// simple use rev for new doc
newDoc . _rev = old . _rev ;
}
} catch ( ex ) {
if ( ex . status && ex . status == 404 ) {
// NO OP/
} else {
throw ex ;
}
}
2021-10-17 15:10:03 +02:00
let r = await this . localDatabase . put ( newDoc ) ;
2021-10-18 08:07:44 +02:00
this . updateRecentModifiedDocs ( r . id , r . rev , newDoc . _deleted ) ;
2021-10-19 10:53:54 +02:00
if ( typeof this . corruptedEntries [ note . _id ] != "undefined" ) {
delete this . corruptedEntries [ note . _id ] ;
}
2021-10-21 11:48:42 +02:00
Logger ( ` note saven: ${ newDoc . _id } : ${ r . rev } ` ) ;
2021-10-14 12:27:08 +02:00
}
syncHandler : PouchDB.Replication.Sync < { } > = null ;
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-10-26 11:08:01 +02:00
replicateAllToServer ( setting : ObsidianLiveSyncSettings ) {
return new Promise ( async ( res , rej ) = > {
this . closeReplication ( ) ;
Logger ( "send all data to server" , LOG_LEVEL . NOTICE ) ;
let uri = setting . couchDB_URI ;
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 rej ( ` could not connect to ${ uri } ` ) ;
}
let syncOptionBase : PouchDB.Replication.SyncOptions = {
batch_size : 250 ,
batches_limit : 40 ,
} ;
let db = dbret . db ;
//replicate once
let replicate = this . localDatabase . replicate . to ( db , syncOptionBase ) ;
replicate
. on ( "change" , async ( e ) = > {
// no op.
Logger ( ` sending..: ${ e . docs . length } ` ) ;
} )
. on ( "complete" , async ( info ) = > {
Logger ( "Completed" , LOG_LEVEL . NOTICE ) ;
replicate . cancel ( ) ;
replicate . removeAllListeners ( ) ;
res ( true ) ;
} )
. on ( "error" , ( e ) = > {
Logger ( "Pulling Replication error" , LOG_LEVEL . NOTICE ) ;
Logger ( e ) ;
rej ( e ) ;
} ) ;
} ) ;
}
2021-10-15 05:30:06 +02:00
async openReplication ( setting : ObsidianLiveSyncSettings , keepAlive : boolean , showResult : boolean , callback : ( e : PouchDB.Core.ExistingDocument < { } > [ ] ) = > Promise < void > ) {
2021-10-17 15:10:03 +02:00
if ( setting . versionUpFlash != "" ) {
new Notice ( "Open settings and check message, please." ) ;
return ;
}
2021-10-14 12:27:08 +02:00
let uri = setting . couchDB_URI ;
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 ) ) ) {
Logger ( "Remote database is newer or corrupted, make sure to latest version of obsidian-livesync installed" , LOG_LEVEL . NOTICE ) ;
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-10-14 12:27:08 +02:00
let db = dbret . db ;
//replicate once
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
. 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-10-21 11:48:42 +02:00
Logger ( ` pulled ${ e . docs . length } doc(s) ` ) ;
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-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-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-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-10-15 05:30:06 +02:00
callback ( e . change . docs ) ;
2021-10-21 11:48:42 +02:00
Logger ( ` replicated ${ e . change . docs . length } doc(s) ` ) ;
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-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 ;
} )
. on ( "denied" , ( e ) = > {
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-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-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-10-21 11:48:42 +02:00
Logger ( "Pulling Replication error" , LOG_LEVEL . NOTICE ) ;
Logger ( e ) ;
2021-10-14 12:27:08 +02:00
} ) ;
}
closeReplication() {
if ( this . syncHandler == null ) {
return ;
}
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 ) {
this . changeHandler . cancel ( ) ;
}
2021-10-14 12:27:08 +02:00
await this . closeReplication ( ) ;
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 ( ) ;
await this . closeReplication ( ) ;
let uri = setting . couchDB_URI ;
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 ( ) ;
let uri = setting . couchDB_URI ;
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 ) {
let uri = setting . couchDB_URI ;
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 ) ) ) {
Logger ( "Remote database is newer or corrupted, make sure to latest version of obsidian-livesync installed" , LOG_LEVEL . NOTICE ) ;
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 ) {
let uri = setting . couchDB_URI ;
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 ) ) ) {
Logger ( "Remote database is newer or corrupted, make sure to latest version of obsidian-livesync installed" , LOG_LEVEL . NOTICE ) ;
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 [ ] = [ ] ;
do {
2021-10-18 08:07:44 +02:00
let result = await this . localDatabase . allDocs ( { include_docs : true , skip : c , limit : 100 , conflicts : true } ) ;
2021-10-15 10:58:42 +02:00
readCount = result . rows . length ;
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.
const garbages = hashPieces . filter ( ( e ) = > usedPieces . indexOf ( e ) == - 1 ) ;
let deleteCount = 0 ;
for ( let v of garbages ) {
try {
let item = await this . localDatabase . get ( v ) ;
item . _deleted = true ;
await this . localDatabase . put ( item ) ;
deleteCount ++ ;
} catch ( ex ) {
if ( ex . status && ex . status == 404 ) {
// NO OP. It should be timing problem.
} else {
throw ex ;
}
}
}
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 ;
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 ( ) ;
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 ;
this . watchVaultChange = debounce ( this . watchVaultChange . bind ( this ) , delay , false ) ;
this . watchVaultDelete = debounce ( this . watchVaultDelete . bind ( this ) , delay , false ) ;
this . watchVaultRename = debounce ( this . watchVaultRename . bind ( this ) , delay , false ) ;
this . watchWorkspaceOpen = debounce ( this . watchWorkspaceOpen . bind ( this ) , delay , false ) ;
2021-10-18 08:07:44 +02:00
2021-10-14 12:27:08 +02:00
this . registerWatchEvents ( ) ;
this . parseReplicationResult = this . parseReplicationResult . bind ( this ) ;
this . addSettingTab ( new ObsidianLiveSyncSettingTab ( this . app , this ) ) ;
2021-10-17 04:06:25 +02:00
this . app . workspace . onLayoutReady ( async ( ) = > {
2021-10-14 12:27:08 +02:00
await this . initializeDatabase ( ) ;
this . realizeSettingSyncMode ( ) ;
if ( this . settings . syncOnStart ) {
2021-10-15 05:30:06 +02:00
await this . replicate ( false ) ;
2021-10-14 12:27:08 +02:00
}
2021-10-17 04:06:25 +02:00
} ) ;
2021-10-14 12:27:08 +02:00
// when in mobile, too long suspended , connection won't back if setting retry:true
this . registerInterval (
window . setInterval ( async ( ) = > {
if ( this . settings . liveSync ) {
await this . localDatabase . closeReplication ( ) ;
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
}
}
} , 60 * 1000 )
) ;
2021-10-18 08:07:44 +02:00
this . addCommand ( {
id : "livesync-replicate" ,
name : "Replicate now" ,
callback : ( ) = > {
this . replicate ( ) ;
} ,
} ) ;
2021-10-19 10:53:54 +02:00
// this.addCommand({
// id: "livesync-test",
// name: "test reset db and replicate",
// callback: async () => {
// await this.resetLocalDatabase();
// await this.replicate();
// },
// });
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-15 05:30:06 +02:00
this . watchWindowVisiblity = this . watchWindowVisiblity . bind ( this ) ;
window . addEventListener ( "visibilitychange" , this . watchWindowVisiblity ) ;
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-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-10-21 11:48:42 +02:00
this . localDatabase = new LocalPouchDB ( this . settings , vaultName ) ;
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-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-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-10-15 05:30:06 +02:00
watchWindowVisiblity() {
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 ( ) ;
} else {
if ( this . settings . liveSync ) {
this . localDatabase . openReplication ( this . settings , true , false , this . parseReplicationResult ) ;
}
if ( this . settings . syncOnStart ) {
this . localDatabase . openReplication ( this . settings , false , false , this . parseReplicationResult ) ;
}
}
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-10-14 12:27:08 +02:00
if ( file == null ) return ;
2021-10-18 08:07:44 +02:00
this . localDatabase . disposeHashCache ( ) ;
2021-10-14 12:27:08 +02:00
this . showIfConflicted ( file ) ;
2021-10-15 10:58:42 +02:00
this . gcHook ( ) ;
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-10-14 12:27:08 +02:00
this . updateIntoDB ( file ) ;
2021-10-15 10:58:42 +02:00
this . gcHook ( ) ;
2021-10-14 12:27:08 +02:00
}
watchVaultDelete ( file : TFile & TFolder ) {
2021-10-19 10:53:54 +02:00
if ( this . settings . suspendFileWatching ) return ;
2021-10-14 12:27:08 +02:00
if ( file . children ) {
//folder
this . deleteFolderOnDB ( file ) ;
// this.app.vault.delete(file);
} else {
this . deleteFromDB ( file ) ;
}
2021-10-15 10:58:42 +02:00
this . gcHook ( ) ;
2021-10-14 12:27:08 +02:00
}
watchVaultRename ( file : TFile & TFolder , oldFile : any ) {
2021-10-19 10:53:54 +02:00
if ( this . settings . suspendFileWatching ) return ;
2021-10-14 12:27:08 +02:00
if ( file . children ) {
// this.renameFolder(file,oldFile);
2021-10-21 11:48:42 +02:00
Logger ( ` folder name changed:(this operation is not supported) ${ file . path } ` , LOG_LEVEL . NOTICE ) ;
2021-10-14 12:27:08 +02:00
} else {
this . updateIntoDB ( file ) ;
this . deleteFromDBbyPath ( oldFile ) ;
}
2021-10-15 10:58:42 +02:00
this . gcHook ( ) ;
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 ) {
2021-10-15 05:30:06 +02:00
// debugger;
2021-10-18 08:07:44 +02:00
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-10-12 16:50:13 +02:00
if ( this . statusBar2 != null ) {
2021-10-13 14:38:44 +02:00
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
}
}
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 ) ;
let newfile = await this . app . vault . createBinary ( doc . _id , bin , { ctime : doc.ctime , mtime : doc.mtime } ) ;
2021-10-21 11:48:42 +02:00
Logger ( "live : write to local (newfile:b) " + doc . _id ) ;
2021-10-17 15:10:03 +02:00
await this . app . vault . trigger ( "create" , newfile ) ;
}
} 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-10-17 15:10:03 +02:00
let newfile = await this . app . vault . create ( doc . _id , doc . data , { ctime : doc.ctime , mtime : doc.mtime } ) ;
2021-10-21 11:48:42 +02:00
Logger ( "live : write to local (newfile:p) " + doc . _id ) ;
2021-10-12 16:50:13 +02:00
await this . app . vault . trigger ( "create" , newfile ) ;
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 ;
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-10-21 11:48:42 +02:00
Logger ( ` all files deleted by replication, so delete dir ` ) ;
2021-10-13 14:38:44 +02:00
await this . deleteVaultItem ( dir ) ;
}
}
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-10-14 12:27:08 +02:00
if ( file . stat . mtime < docEntry . mtime || 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-10-17 15:10:03 +02:00
await this . app . vault . modifyBinary ( file , bin , { ctime : doc.ctime , mtime : doc.mtime } ) ;
2021-10-21 11:48:42 +02:00
Logger ( msg ) ;
2021-10-17 15:10:03 +02:00
await this . app . vault . trigger ( "modify" , file ) ;
}
2021-10-18 08:07:44 +02:00
}
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 ) ;
await this . app . vault . modify ( file , doc . data , { ctime : doc.ctime , mtime : doc.mtime } ) ;
2021-10-21 11:48:42 +02:00
Logger ( msg ) ;
2021-10-12 16:50:13 +02:00
await this . app . vault . trigger ( "modify" , file ) ;
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
} else if ( file . stat . mtime > docEntry . mtime ) {
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 ) ;
}
}
//---> Sync
2021-10-19 10:53:54 +02:00
async parseReplicationResult ( docs : Array < PouchDB.Core.ExistingDocument < EntryDoc > > ) : Promise < void > {
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 ( ) ;
Logger ( ` Remote database updated to incompatible version. update your Obsidian-livesync plugin. ` , LOG_LEVEL . NOTICE ) ;
}
}
2021-10-17 15:10:03 +02:00
this . gcHook ( ) ;
2021-10-12 16:50:13 +02:00
}
}
async 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
}
}
refreshStatusText() {
2021-10-14 12:27:08 +02:00
let statusStr = this . localDatabase . status ( ) ;
2021-10-12 16:50:13 +02:00
this . statusBar . setText ( "Sync:" + statusStr ) ;
}
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-10-14 12:27:08 +02:00
async initializeDatabase() {
await this . openDatabase ( ) ;
await this . syncAllFiles ( ) ;
2021-10-12 16:50:13 +02:00
}
2021-10-26 11:08:01 +02:00
async replicateAllToServer() {
return await this . localDatabase . replicateAllToServer ( this . settings ) ;
}
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-10-12 16:50:13 +02:00
async syncAllFiles() {
// synchronize all files between database and storage.
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-10-12 16:50:13 +02:00
const filesDatabase = wf . rows . map ( ( e ) = > e . id ) ;
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-10-18 08:07:44 +02:00
// just write to DB from storage.
2021-10-12 16:50:13 +02:00
for ( let v of onlyInStorage ) {
2021-10-14 12:27:08 +02:00
await this . updateIntoDB ( v ) ;
2021-10-12 16:50:13 +02:00
}
2021-10-18 08:07:44 +02:00
// simply realize it
2021-10-12 16:50:13 +02:00
for ( let v of onlyInDatabase ) {
await this . pullFile ( v , filesStorage ) ;
}
2021-10-18 08:07:44 +02:00
// have to sync below..
2021-10-12 16:50:13 +02:00
for ( let v of syncFiles ) {
await this . syncFileBetweenDBandStorage ( v , filesStorage ) ;
}
}
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 {
await this . app . vault . delete ( entry ) ;
} 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 {
await this . app . vault . delete ( folder ) ;
} 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-10-17 04:06:25 +02:00
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 ( ) ;
}
let targetFiles = fileList . filter ( ( e ) = > e . path == filename ) ;
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-10-18 08:07:44 +02:00
let doc = await this . localDatabase . getDBEntry ( file . path ) ;
2021-10-14 12:27:08 +02:00
if ( doc === false ) return ;
2021-10-12 16:50:13 +02:00
if ( file . stat . mtime > doc . mtime ) {
//newer local file.
2021-10-14 12:27:08 +02:00
await this . updateIntoDB ( file ) ;
2021-10-21 11:48:42 +02:00
Logger ( "sync : older databse files so write to database:" + file . path ) ;
2021-10-12 16:50:13 +02:00
} else if ( file . stat . mtime < doc . mtime ) {
//newer database file.
2021-10-21 11:48:42 +02:00
Logger ( "sync : older storage files so write from database:" + file . path ) ;
2021-10-12 16:50:13 +02:00
await this . doc2storate_modify ( doc , file ) ;
} else {
//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 ) ;
content = arrayBufferToBase64 ( contentBin ) ;
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 ) ;
// this.plugin.onLogChanged = this.updateLog;
2021-10-13 14:38:44 +02:00
this . updateLog ( ) ;
}
onClose() {
let { contentEl } = this ;
contentEl . empty ( ) ;
2021-10-15 05:30:06 +02:00
// this.plugin.onLogChanged = 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-10-13 14:38:44 +02:00
let db = await connectRemoteCouchDB ( this . plugin . settings . couchDB_URI , {
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-10-18 08:07:44 +02:00
this . plugin . addLog ( ` could not connect to ${ this . plugin . settings . couchDB_URI } ` , 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 ( ) ;
containerEl . createEl ( "h2" , { text : "Settings for obsidian-livesync." } ) ;
2021-10-13 14:38:44 +02:00
new Setting ( containerEl ) . setName ( "CouchDB Remote URI" ) . addText ( ( text ) = >
text
. setPlaceholder ( "https://........" )
. setValue ( this . plugin . settings . couchDB_URI )
. onChange ( async ( value ) = > {
this . plugin . settings . couchDB_URI = value ;
await this . plugin . saveSettings ( ) ;
} )
) ;
2021-10-12 16:50:13 +02:00
new Setting ( containerEl )
. setName ( "CouchDB 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 ( "CouchDB 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" ) ;
} ) ;
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-10-19 10:53:54 +02:00
containerEl . createEl ( "h3" , { text : "Database configuration" } ) ;
2021-10-15 05:30:06 +02:00
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 )
. setName ( "Auto GC delay" )
. 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-10-19 10:53:54 +02:00
containerEl . createEl ( "h3" , { text : "Log Setting" } ) ;
2021-10-15 05:30:06 +02:00
new Setting ( containerEl )
2021-10-19 10:53:54 +02:00
. setName ( "Do not show low-priority Log" )
2021-10-15 05:30:06 +02:00
. setDesc ( "Reduce log infomations" )
. addToggle ( ( toggle ) = >
toggle . setValue ( this . plugin . settings . lessInformationInLog ) . onChange ( async ( value ) = > {
this . plugin . settings . lessInformationInLog = value ;
await this . plugin . saveSettings ( ) ;
} )
) ;
2021-10-18 08:07:44 +02:00
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-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-10-12 16:50:13 +02:00
new Setting ( containerEl )
. setName ( "LiveSync" )
. setDesc ( "Sync realtime" )
. addToggle ( ( toggle ) = >
2021-10-14 12:27:08 +02:00
toggle . setValue ( this . plugin . settings . liveSync ) . onChange ( async ( value ) = > {
this . plugin . settings . liveSync = value ;
2021-10-12 16:50:13 +02:00
await this . plugin . saveSettings ( ) ;
this . plugin . realizeSettingSyncMode ( ) ;
} )
) ;
new Setting ( containerEl )
. setName ( "Sync on Save" )
2021-10-19 10:53:54 +02:00
. setDesc ( "When you save file, sync automatically" )
2021-10-12 16:50:13 +02:00
. addToggle ( ( toggle ) = >
toggle . setValue ( this . plugin . settings . syncOnSave ) . onChange ( async ( value ) = > {
this . plugin . settings . syncOnSave = value ;
await this . plugin . saveSettings ( ) ;
} )
) ;
new Setting ( containerEl )
. setName ( "Sync on Start" )
2021-10-19 10:53:54 +02:00
. setDesc ( "Start synchronization on Obsidian started." )
2021-10-12 16:50:13 +02:00
. addToggle ( ( toggle ) = >
toggle . setValue ( this . plugin . settings . syncOnStart ) . onChange ( async ( value ) = > {
this . plugin . settings . syncOnStart = value ;
await this . plugin . saveSettings ( ) ;
} )
) ;
2021-10-19 10:53:54 +02:00
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
2021-10-18 08:07:44 +02:00
new Setting ( containerEl ) . setName ( "Garbage Collect" ) . addButton ( ( button ) = >
button
. setButtonText ( "Garbage Collection" )
. setDisabled ( false )
. onClick ( async ( ) = > {
await this . plugin . garbageCollect ( ) ;
} )
) ;
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" ) ;
}
}
new Setting ( containerEl )
. setName ( "Drop History" )
. setDesc ( "Initialize local and remote database, and create local database from storage and put all into server. And also, lock the database to prevent data corruption." )
. addButton ( ( button ) = >
button
. setButtonText ( "Execute" )
. setWarning ( )
. setDisabled ( false )
. onClick ( async ( ) = > {
await this . plugin . resetLocalDatabase ( ) ;
await this . plugin . initializeDatabase ( ) ;
await this . plugin . tryResetRemoteDatabase ( ) ;
await this . plugin . markRemoteLocked ( ) ;
await this . plugin . replicateAllToServer ( ) ;
} )
) ;
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-10-12 16:50:13 +02:00
}
}