Skip to content

Fix/orphaned crypted file cleanup#15098

Open
akshajrawat wants to merge 8 commits intolaurent22:devfrom
akshajrawat:fix/orphaned-crypted-file-cleanup
Open

Fix/orphaned crypted file cleanup#15098
akshajrawat wants to merge 8 commits intolaurent22:devfrom
akshajrawat:fix/orphaned-crypted-file-cleanup

Conversation

@akshajrawat
Copy link
Copy Markdown
Contributor

@akshajrawat akshajrawat commented Apr 14, 2026

Problem

Fixes #9093

When End-to-End Encryption (E2EE) is enabled, Joplin creates temporary .crypted files during the resource upload process. If an upload is interrupted or the application crashes, these files remain orphaned in the resources directory.

Previously, cleaning these files required scanning the entire resources folder on startup. For users with large libraries (10,000+ items), this approach is inefficient as startup time scales linearly with the number of resources. This results in unnecessary disk clutter and potential performance degradation.

Solution

This PR introduces an $O(1)$ cleanup solution by isolating temporary encryption files.

here is the benchmark testing :
https://discourse.joplinapp.org/t/gsoc-2026-local-note-encryption-draft-proposal-and-poc/49012/33?u=akshajrawat

Key Changes:

  • Isolation: Temporary .crypted files are now redirected to a dedicated subdirectory: [profile]/tmp/temp_cache.

  • Efficient Cleanup: Added a startup hook across Desktop, CLI, and Mobile platforms to wipe and recreate the temp_cache directory upon boot.

  • Redirection: Updated Resource.fullPathForSyncUpload to utilize this new path logic.

  • Maintenance: Standard post-decryption cleanup continues to function in the main directory to ensure no regression in existing cleanup logic.

This change is low-risk as it only affects temporary files used during the sync-upload lifecycle and does not modify primary resource storage logic.

Test Plan

  • Added a unit test in packages/lib/models/Resource.test.ts to verify that emptyTempEncryptionCache() correctly identifies and removes files within the targeted directory.

AI Assistance Disclosure

  • Was AI used to generate this code? Yes, AI was used to assist in identifying the appropriate startup hook locations and to refine the TypeScript implementation for cross-platform compatibility.

  • Was AI used to generate this PR description? Yes, the pr was generated though was completely read through and made detailed changes to before submitting

Note on GSoC Project Alignment:

The implementation of the temp_cache directory aligns with the architectural goals discussed in all the GSoC proposal's resource handling part of the local encryption implementation agreed by one of the GSOC mentor. This isolated cache provides a structured foundation that can be further used for local encryption tasks and optimized resource handling in future project phases.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 92438764-00ff-4d53-b650-f34a5fdb844c

📥 Commits

Reviewing files that changed from the base of the PR and between 591f4ed and 2341bc8.

📒 Files selected for processing (2)
  • packages/lib/models/Resource.test.ts
  • packages/lib/models/Resource.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/lib/models/Resource.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/lib/models/Resource.ts

📝 Walkthrough

Walkthrough

Startup routines added to CLI, desktop and mobile to remove orphaned .crypted files from a temporary encryption cache. Resource gained temp path and cache-emptying methods; decrypt and sync-path logic updated to perform and clean up temporary encrypted files.

Changes

Cohort / File(s) Summary
Core resource encryption and cleanup logic
packages/lib/models/Resource.ts, packages/lib/models/Resource.test.ts
Added tempCryptedPath() and emptyTempEncryptionCache(). decrypt() now awaits move operations and performs best-effort removal of leftover .crypted files with warning logs on failure. fullPathForSyncUpload() uses the temp encryption cache for encrypted output. Added test verifying orphaned .crypted cleanup.
CLI startup cleanup
packages/app-cli/app/app.ts
Calls Resource.emptyTempEncryptionCache() during Application.start() (placed after command service init), wrapped in try/catch and logs warnings without aborting startup.
Desktop startup cleanup
packages/app-desktop/app.ts
Inserted startup task app/deleteOrphanedTempCache after app/updateTray that calls Resource.emptyTempEncryptionCache() with try/catch and warning-level logging.
Mobile startup cleanup
packages/app-mobile/utils/buildStartupTasks.ts
Added startup task buildStartupTasks/empty temp encryption cache invoking Resource.emptyTempEncryptionCache() wrapped in try/catch; logs warnings on failure to avoid failing startup chain.

Sequence Diagram(s)

sequenceDiagram
  participant Startup as Startup Manager
  participant App as Application (CLI/Desktop/Mobile)
  participant Resource as Resource
  participant FS as fsDriver
  participant Logger as Logger

  Startup->>App: begin startup sequence
  App->>Resource: emptyTempEncryptionCache()
  Resource->>FS: stat/remove tempDir/encryptionCache/*.crypted
  FS-->>Resource: success / error
  Resource->>Logger: warn(...) on error
  Resource-->>App: return (no throw)
  App-->>Startup: continue startup (commands/GUI)
Loading

Suggested reviewers

  • mrjo118
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix/orphaned crypted file cleanup' directly describes the main objective of removing orphaned .crypted files, aligning with the core problem and solution presented in the PR.
Description check ✅ Passed The description comprehensively explains the problem, solution, key changes, test plan, and acknowledges AI assistance—all relevant to the changeset's objective of implementing efficient orphaned file cleanup.
Linked Issues check ✅ Passed The PR implements the objective from issue #9093 by automatically deleting .crypted files at application startup across all platforms (Desktop, CLI, Mobile) with an efficient O(1) mechanism.
Out of Scope Changes check ✅ Passed All changes focus on the orphaned .crypted file cleanup objective: temp cache isolation, startup hooks, path redirection, post-decryption cleanup, and verification tests are directly related to the linked issue requirement.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added bug It's a bug mobile All mobile platforms desktop All desktop platforms cli CLI app specific issue performance Performance issues labels Apr 14, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/lib/models/Resource.ts (1)

269-271: Log the error details for debuggability.

The caught error is not included in the log message, making it harder to diagnose cleanup failures. Additionally, using info level for failures is inconsistent with the calling code in mobile/CLI which uses warn.

♻️ Suggested improvement
 		} catch (error) {
-			this.logger().info('Could not clear temporary encryption cache.');
+			this.logger().warn('Could not clear temporary encryption cache:', error);
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/models/Resource.ts` around lines 269 - 271, The catch block in
Resource.ts that currently does this.logger().info('Could not clear temporary
encryption cache.') should be changed to log the caught error details and use a
warning level: replace the info call with this.logger().warn(...) and include
the error object (e.g., this.logger().warn('Could not clear temporary encryption
cache', { error })) so the error message and stack are captured for debugging;
ensure the caught variable (error) is preserved and passed to the logger rather
than discarded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/lib/models/Resource.ts`:
- Around line 269-271: The catch block in Resource.ts that currently does
this.logger().info('Could not clear temporary encryption cache.') should be
changed to log the caught error details and use a warning level: replace the
info call with this.logger().warn(...) and include the error object (e.g.,
this.logger().warn('Could not clear temporary encryption cache', { error })) so
the error message and stack are captured for debugging; ensure the caught
variable (error) is preserved and passed to the logger rather than discarded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4d8dc375-03d1-4886-b623-b663044e24db

📥 Commits

Reviewing files that changed from the base of the PR and between d406e27 and 5cfc88d.

📒 Files selected for processing (5)
  • packages/app-cli/app/app.ts
  • packages/app-desktop/app.ts
  • packages/app-mobile/utils/buildStartupTasks.ts
  • packages/lib/models/Resource.test.ts
  • packages/lib/models/Resource.ts

Comment thread packages/app-desktop/app.ts Outdated

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

addTask('app/deleteOrphanedTempCache', async () => await Resource.emptyTempEncryptionCache());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why there isn't a try catch for this call?

Copy link
Copy Markdown
Contributor Author

@akshajrawat akshajrawat Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for review!
emptyTempEncryptionCache itself handels the error so basically it never will throw an error, though I was particularly not sure about any edge cases arising in mobile or cli so I added the double try catch there.
Now that you've mentioned it, I think it would be better to add a try catch here too to maintain the consistency.
I will just make a quick commit by adding the nitpick and try catch

@mrjo118
Copy link
Copy Markdown
Contributor

mrjo118 commented Apr 15, 2026

Is it possible when starting the app, that the sync could be in progress while the emptying of the temp folder occurs, and this could delete a crypted file while it is uploading?

@akshajrawat
Copy link
Copy Markdown
Contributor Author

akshajrawat commented Apr 16, 2026

Is it possible when starting the app, that the sync could be in progress while the emptying of the temp folder occurs, and this could delete a crypted file while it is uploading?

I wanted to be 100% sure, so I verified this by forcing an artificial delay on the sweep and seeing the startup logs.

There's no risk of a race condition here. The logs confirm that the startup sweep finishes during the initial boot sequence (within the first ~2 seconds), while the background Synchronizer doesn't even wake up to index resources until about 11 seconds later.

Because their lifecycles are completely isolated + Even if an unexpected overlap occurred, the tempCryptedPath function ensures the path is instantly rebuilt in time for the Sync process to use it, acting as an absolute safeguard against collisions.

@mrjo118
Copy link
Copy Markdown
Contributor

mrjo118 commented Apr 16, 2026

Is it possible when starting the app, that the sync could be in progress while the emptying of the temp folder occurs, and this could delete a crypted file while it is uploading?

I wanted to be 100% sure, so I verified this by forcing an artificial delay on the sweep and seeing the startup logs.

There's no risk of a race condition here. The logs confirm that the startup sweep finishes during the initial boot sequence (within the first ~2 seconds), while the background Synchronizer doesn't even wake up to index resources until about 11 seconds later.

Because their lifecycles are completely isolated + Even if an unexpected overlap occurred, the tempCryptedPath function ensures the path is instantly rebuilt in time for the Sync process to use it, acting as an absolute safeguard against collisions.

If you put some random 20gb file in the tempCryptedPath, would it still delete within the first 2 seconds?

@akshajrawat
Copy link
Copy Markdown
Contributor Author

If you put some random 20gb file in the tempCryptedPath, would it still delete within the first 2 seconds?

For a single large file, deletion is usually very fast since the OS primarily removes metadata, so in practice it should still complete within the startup window. That said, I wouldn’t rely on strict timing guarantees.

Even with a larger number of files (e.g., up to ~10k in temp_cache), cleanup remains fast in practice since we’re operating on a small, isolated directory rather than scanning the entire resource folder (as reflected in the benchmarks).

The main safety comes from the blocking startup sequence (cleanup runs before the Synchronizer starts and the app waits for it to complete), so even in unlikely edge cases (e.g., temporary buildup of files), we avoid impacting active uploads.

@laurent22 laurent22 added the v3.7 label Apr 16, 2026
Copy link
Copy Markdown
Owner

@laurent22 laurent22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for looking into this. I think that's a sensible approach and indeed should be much faster than scanning the resource folder.

I looked at the benchmark and maybe I'm lacking context, but was that done on the CLI app? And if so would it be possible to try on the Android app?

We need to think about the worst case - some people import hundreds of thousands of notes and will have as many .crypted files. If you have, say, a million .crypted files in that folder in Android, how does it perform?

Als I'm not a fan of calling this "temp cache". Maybe just "encryptionCache"?

Comment thread packages/lib/models/Resource.ts Outdated

if (await this.fsDriver().exists(tempCacheDir)) {
try {
await this.fsDriver().remove(tempCacheDir);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before proceeding please add a message "Clearing encryption cache..." too

@akshajrawat
Copy link
Copy Markdown
Contributor Author

I looked at the benchmark and maybe I'm lacking context

To give some context on the benchmark, I actually didn't run it in the CLI app. I wrote a quick standalone Node script just to test the raw disk I/O difference between scanning a huge folder vs. dropping an isolated one.

As for the 1 million files on Android scenario: the app shouldn't ever hold that many at once since the sync engine processes uploads in small batches. But even if a massive backlog did pile up over time, dropping the directory natively on Android is definitely the safest bet. It bypasses the React Native JS bridge entirely.

Here is the comparison benchmark for mobile too :

Image

Here is the deletion time for 1 million file :-
Each file are of around 4kb.
Image

@coderabbitai coderabbitai Bot removed bug It's a bug mobile All mobile platforms desktop All desktop platforms cli CLI app specific issue performance Performance issues labels Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Delete .crypted files when the app starts

3 participants