Skip to content

Commit a545784

Browse files
committed
Add setting for compatibility mode (readBinary)
1 parent 6fdfd16 commit a545784

File tree

3 files changed

+81
-0
lines changed

3 files changed

+81
-0
lines changed

src/main.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@ export default class GpgPlugin extends Plugin {
4242
private passphraseRequestPromise: Promise<string | null> | null = null;
4343

4444
private originalAdapterReadFunction: (normalizedPath: string) => Promise<string>;
45+
private originalAdapterReadBinaryFunction: (normalizedPath: string) => Promise<ArrayBuffer>;
4546
private originalAdapterWriteFunction: (normalizedPath: string, data: string, options?: DataWriteOptions) => Promise<void>
4647
private originalAdapterProcessFunction: (normalizedPath: string, fn: (data: string) => string, options?: DataWriteOptions) => Promise<string>;
4748
private hookedAdapterReadRef: (normalizedPath: string) => Promise<string>;
49+
private hookedAdapterReadBinaryRef: (normalizedPath: string) => Promise<ArrayBuffer>;
4850
private hookedAdapterWriteRef: (normalizedPath: string, data: string, options?: DataWriteOptions) => Promise<void>;
4951
private hookedAdapterProcessRef: (normalizedPath: string, fn: (data: string) => string, options?: DataWriteOptions) => Promise<string>;
5052

@@ -126,6 +128,7 @@ export default class GpgPlugin extends Plugin {
126128
// save the original Obsidiane functions to call them
127129
// and in case of plugin unload, we restore them
128130
this.originalAdapterReadFunction = this.app.vault.adapter.read;
131+
this.originalAdapterReadBinaryFunction = this.app.vault.adapter.readBinary;
129132
this.originalAdapterWriteFunction = this.app.vault.adapter.write;
130133
this.originalAdapterProcessFunction = this.app.vault.adapter.process;
131134
this.originalVaultCachedReadFunction = this.app.vault.cachedRead;
@@ -141,12 +144,14 @@ export default class GpgPlugin extends Plugin {
141144
// by third-party plugins as this could lead to data-loss when
142145
// gpgCrypt plugin gets unloaded.
143146
this.hookedAdapterReadRef = this.hookedAdapterRead.bind(this);
147+
this.hookedAdapterReadBinaryRef = this.hookedAdapterReadBinary.bind(this);
144148
this.hookedAdapterWriteRef = this.hookedAdapterWrite.bind(this);
145149
this.hookedAdapterProcessRef = this.hookedAdapterProcess.bind(this);
146150
this.hookedVaultCachedReadRef = this.hookedVaultCachedRead.bind(this);
147151
this.hookedFileRecoveryOnFileChangeRef = this.hookedFileRecoveryOnFileChange.bind(this);
148152
this.hookedFileRecoveryForceAddRef = this.hookedFileRecoveryForceAdd.bind(this);
149153
this.app.vault.adapter.read = this.hookedAdapterReadRef;
154+
this.app.vault.adapter.readBinary = this.hookedAdapterReadBinaryRef;
150155
this.app.vault.adapter.write = this.hookedAdapterWriteRef;
151156
this.app.vault.adapter.process = this.hookedAdapterProcessRef;
152157
this.app.vault.cachedRead = this.hookedVaultCachedReadRef;
@@ -213,6 +218,7 @@ export default class GpgPlugin extends Plugin {
213218
//@ts-ignore
214219
if (
215220
this.app.vault.adapter.read != this.hookedAdapterReadRef ||
221+
this.app.vault.adapter.readBinary != this.hookedAdapterReadBinaryRef ||
216222
this.app.vault.adapter.write != this.hookedAdapterWriteRef ||
217223
this.app.vault.adapter.process != this.hookedAdapterProcessRef ||
218224
this.app.vault.cachedRead != this.hookedVaultCachedReadRef ||
@@ -230,6 +236,7 @@ export default class GpgPlugin extends Plugin {
230236

231237
// restore original Obsidian read/write functions
232238
this.app.vault.adapter.read = this.originalAdapterReadFunction;
239+
this.app.vault.adapter.readBinary = this.originalAdapterReadBinaryFunction;
233240
this.app.vault.adapter.write = this.originalAdapterWriteFunction;
234241
this.app.vault.adapter.process = this.originalAdapterProcessFunction;
235242
this.app.vault.cachedRead = this.originalVaultCachedReadFunction;
@@ -302,6 +309,58 @@ export default class GpgPlugin extends Plugin {
302309
return this.decryptionCache.get(content)!;
303310
}
304311

312+
// Gets executed when Obsidian reads a binary file
313+
// TODO: deduplicate cpde with hookedAdapterRead function
314+
private async hookedAdapterReadBinary(normalizedPath: string): Promise<ArrayBuffer> {
315+
_log(`Hooked Adapter - readBinary (${normalizedPath})`);
316+
317+
const contentArray = await this.originalReadBinary(normalizedPath);
318+
const content = new TextDecoder().decode(new Uint8Array(contentArray));
319+
const isEncrypted = await this.gpgNative.isEncrypted(content);
320+
321+
// in case the file status is already marked as encrypted, we don't set it to plaintext
322+
// so we get a warning in case of the next write
323+
if (!this.encryptedFileStatus.has(normalizedPath) || this.encryptedFileStatus.get(normalizedPath) !== true) {
324+
this.encryptedFileStatus.set(normalizedPath, isEncrypted);
325+
}
326+
327+
if (!isEncrypted || !this.settings.compatibilityMode) {
328+
return contentArray;
329+
}
330+
331+
// As Obsidian is doing multiple read calls for one note opening, it doesnt directly output
332+
// any exceptions anymore to the user. With this, gpgCrypt has to output any errors to the
333+
// user over Notices. To avoid duplicated Notices for the same error, the complete
334+
// note decryption part is now taking place in two promises: one external promise which is getting
335+
// cached for multiple calls for the same note and to throw an error on rejection
336+
// and an internal promise, to show the Notice only once.
337+
338+
// If we already have a request in flight (with the same encrypted text), share it
339+
if (this.decryptionCache.has(content)) {
340+
return new TextEncoder().encode(await this.decryptionCache.get(content)!);
341+
}
342+
343+
this.decryptionCache.set(content, new Promise(async (resolve, reject) => {
344+
let errorOccurred = false;
345+
try {
346+
//await new Promise(res => setTimeout(res, 10000))
347+
resolve(await this.decrypt(normalizedPath, content));
348+
} catch (error) {
349+
errorOccurred = true;
350+
reject(error)
351+
} finally {
352+
// Reset for the next time we need an external decrypt.
353+
// If cache option is set and no error occured, the entry is kept for faster note reopening
354+
if (errorOccurred || !this.settings.backendWrapper.cache) {
355+
// In some cases, the promise is executed too fast so it is getting cached for at least 500ms.
356+
setTimeout(() => { _log(`Delete decryption cache for ${normalizedPath}`); this.decryptionCache.delete(content); }, 500);
357+
}
358+
}
359+
}));
360+
361+
return new TextEncoder().encode(await this.decryptionCache.get(content)!).buffer;
362+
}
363+
305364
// Gets executed when Obsidian writes a file
306365
private async hookedAdapterWrite(normalizedPath: string, data: string, options?: DataWriteOptions | undefined): Promise<void> {
307366
_log(`Hooked Adapter - write (${normalizedPath})`);
@@ -447,6 +506,10 @@ export default class GpgPlugin extends Plugin {
447506
return this.originalAdapterReadFunction.call(this.app.vault.adapter, normalizedPath);
448507
}
449508

509+
async originalReadBinary(normalizedPath: string) {
510+
return this.originalAdapterReadBinaryFunction.call(this.app.vault.adapter, normalizedPath);
511+
}
512+
450513
async originalWrite(normalizedPath: string, data: string, options?: DataWriteOptions | undefined) {
451514
return this.originalAdapterWriteFunction.call(this.app.vault.adapter, normalizedPath, data, options);
452515
}
@@ -806,6 +869,8 @@ export default class GpgPlugin extends Plugin {
806869

807870
fileRecovery: FileRecovery.ENCRYPTED,
808871

872+
compatibilityMode: false,
873+
809874
backend: Backend.NATIVE,
810875

811876
backendNative: {

src/settings/Settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface Settings {
1818
encryptAll: boolean,
1919
renameToGpg: boolean,
2020
fileRecovery: string,
21+
compatibilityMode: boolean;
2122
backend: string;
2223

2324
backendNative : BackendNativeSettings;

src/settings/SettingsTab.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,21 @@ export class SettingsTab extends PluginSettingTab {
8585
});
8686
});
8787

88+
const compatibilityModeSetting = new Setting(this.containerEl)
89+
.setName("Compatibility mode")
90+
.setDesc("Enable this setting to use Obsidian's native metadata generation needed for features like link blocks and certain plugins (e.g., Excalidraw). ")
91+
.addToggle(toggle => {
92+
toggle.setTooltip("Enable this setting to use Obsidian's native metadata generation needed for features like link blocks and certain plugins (e.g., Excalidraw). Warning: this exposes plaintext headings and file structure on disk.")
93+
.setValue(this.settings.compatibilityMode)
94+
.onChange(async (value) => {
95+
this.settings.compatibilityMode = value;
96+
await this.plugin.saveSettings();
97+
});
98+
});
99+
100+
const warningEl = compatibilityModeSetting.descEl.createSpan({ cls: 'mod-warning' });
101+
warningEl.innerText = "Warning: this exposes plaintext headings and file structure on disk.";
102+
88103

89104
new Setting(this.containerEl)
90105
.setHeading()

0 commit comments

Comments
 (0)