Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/app-cli/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { FolderEntity, NoteEntity } from '@joplin/lib/services/database/types';

type FolderOrNoteType = ModelType.Note | ModelType.Folder | 'folderOrNote';
import initializeCommandService from './utils/initializeCommandService';
import Resource from '@joplin/lib/models/Resource';
const { cliUtils } = require('./cli-utils.js');
const Cache = require('@joplin/lib/Cache');

Expand Down Expand Up @@ -407,6 +408,13 @@ class Application extends BaseApplication {

initializeCommandService(this.store(), Setting.value('env') === Env.Dev);

// remove temp cache file
try {
await Resource.emptyTempEncryptionCache();
} catch (error) {
this.logger().warn('Failed to empty temp encryption cache during startup:', error);
}

// If we have some arguments left at this point, it's a command
// so execute it.
if (argv.length) {
Expand Down
8 changes: 8 additions & 0 deletions packages/app-desktop/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,14 @@ class Application extends BaseApplication {

addTask('app/updateTray', () => this.updateTray());

addTask('app/deleteOrphanedTempCache', async () =>{
try {
await Resource.emptyTempEncryptionCache();
} catch (error) {
this.logger().warn('Failed to empty temp encryption cache during startup:', error);
}
});

addTask('app/set main window state', () => {
if (Setting.value('startMinimized') && Setting.value('showTrayIcon')) {
bridge().mainWindow().hide();
Expand Down
9 changes: 9 additions & 0 deletions packages/app-mobile/utils/buildStartupTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,15 @@ const buildStartupTasks = (
}
});

// remove temp cache file
addTask('buildStartupTasks/empty temp encryption cache', async () => {
try {
await Resource.emptyTempEncryptionCache();
} catch (error) {
logger.warn('Failed to empty temp encryption cache during startup:', error);
}
});

return startupTasks;
};

Expand Down
17 changes: 17 additions & 0 deletions packages/lib/models/Resource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,23 @@ describe('models/Resource', () => {
}
});

it('should clean up orphaned .crypted files in the encryptionCache directory', async () => {
// Get the path using the model's own method
const fakeOrphanPath = await Resource.tempCryptedPath('test_orphan');

// tempCryptedPath already ensures the dir exists, but we write the file
await shim.fsDriver().writeFile(fakeOrphanPath, 'fake encrypted garbage', 'utf8');

// Verify it was created successfully
expect(await pathExists(fakeOrphanPath)).toBe(true);

// Call for new sweep function
await Resource.emptyTempEncryptionCache();

// Prove the file was deleted
expect(await pathExists(fakeOrphanPath)).toBe(false);
});

test.each([
['empty search should return all resources sorted by title asc', { searchQuery: '', sortField: 'title', sortDirection: 'asc', limit: 10, offset: 0 }, 'title', ['alpha', 'Bravo', 'delta', 'Zulu'], false],
['pagination should report hasMore when limit is lower than total rows', { sortField: 'title', sortDirection: 'asc', limit: 2, offset: 0 }, 'title', ['alpha', 'Bravo'], true],
Expand Down
51 changes: 48 additions & 3 deletions packages/lib/models/Resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,16 +224,55 @@ export default class Resource extends BaseItem {
// at all. It can happen for example when there's a crash between the moment the data
// is decrypted and the resource item is updated.
this.logger().warn(`Found a resource that was most likely already decrypted but was marked as encrypted. Marked it as decrypted: ${item.id}`);
this.fsDriver().move(encryptedPath, plainTextPath);
await this.fsDriver().move(encryptedPath, plainTextPath);
} else {
throw error;
}
}

// We do this outside the main decrypt Try/Catch block
// If this fails it does NOT throw an error and only logs a warning, letting the db transaction occur below
try {
if (await this.fsDriver().exists(encryptedPath)) {
// The file was successfully decrypted into plaintext.
// We must delete the leftover .crypted file from the main resource directory immediately.
await this.fsDriver().remove(encryptedPath);
}
} catch (cleanupError) {
this.logger().warn(`Could not remove leftover .crypted file ${encryptedPath}:`, cleanupError);
}

decryptedItem.encryption_blob_encrypted = 0;
return super.save(decryptedItem, { autoTimestamp: false });
}

public static async tempCryptedPath(resourceId: string): Promise<string> {
const tempDir = Setting.value('tempDir');
const encryptionCache = `${tempDir}/encryptionCache`;

// quick check to ensure our folder exists
if (!(await this.fsDriver().exists(encryptionCache))) {
await this.fsDriver().mkdir(encryptionCache);
}

return `${encryptionCache}/${resourceId}.crypted`;
}

public static async emptyTempEncryptionCache() {
const tempDir = Setting.value('tempDir');
const encryptionCache = `${tempDir}/encryptionCache`;

if (await this.fsDriver().exists(encryptionCache)) {
try {
this.logger().info('Clearing encryption cache...');
await this.fsDriver().remove(encryptionCache);
this.logger().info('Cleared temporary encryption cache.');
} catch (error) {
this.logger().warn('Could not clear temporary encryption cache:', error);
}
}
}

// Prepare the resource by encrypting it if needed.
// The call returns the path to the physical file AND a representation of the resource object
// as it should be uploaded to the sync target. Note that this may be different from what is stored
Expand All @@ -251,8 +290,14 @@ export default class Resource extends BaseItem {
return { path: plainTextPath, resource: resource };
}

const encryptedPath = this.fullPath(resource, true);
if (resource.encryption_blob_encrypted) return { path: encryptedPath, resource: resource };
// if the resource is already encrypted return the current path of the resource
if (resource.encryption_blob_encrypted) {
const encryptedPath = this.fullPath(resource, true);
return { path: encryptedPath, resource: resource };
}

// updated the dir path so that the resource goes to the new temp directory
const encryptedPath = await this.tempCryptedPath(resource.id);

try {
await this.encryptionService().encryptFile(plainTextPath, encryptedPath, {
Expand Down
Loading