Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data loss reported #22

Closed
lishid opened this issue Feb 25, 2021 · 19 comments
Closed

Data loss reported #22

lishid opened this issue Feb 25, 2021 · 19 comments

Comments

@lishid
Copy link

@lishid lishid commented Feb 25, 2021

https://forum.obsidian.md/t/data-loss-0-11-3-zero-byte-md-file-in-vault/13538/10

This is caused by the code at:

const fileContents = (file as any).cachedData

cachedData is undefined when the file has not been read this session. Don't use private APIs like this.

Recommended fix: Use vault.read if you're going to be making changes and writing it back to the file.

@lishid lishid changed the title Data loss reported. Data loss reported Feb 25, 2021
@liamcain
Copy link

@liamcain liamcain commented Feb 25, 2021

If you're writing back to disk, you should always use vault.read to avoid data loss.

@lishid
Copy link
Author

@lishid lishid commented Feb 25, 2021

I have temporarily delisted this plugin and issued a deprecation flag for all available versions.

Please let me know when it's resolved so we can add it back.

@delashum
Copy link
Owner

@delashum delashum commented Feb 25, 2021

I'm really sorry to hear that happened. Looking into a fix right now.

What is the difference between vault.read and vault.cachedRead?

@delashum
Copy link
Owner

@delashum delashum commented Feb 25, 2021

My understanding is that the main issue here was using (file as any).cachedData as a source of truth for what was in the file as it can come back as undefined if that file isn't loaded into the cache. I am now loading all files using vault.cachedRead, and I use the results from that to write updates. Is that sufficient? Or do I need to use vault.read instead?

@delashum delashum reopened this Feb 25, 2021
@lishid
Copy link
Author

@lishid lishid commented Feb 25, 2021

Always use vault.read if you intend to write the file back to disk. vault.cachedRead may return stale data, for example, if the user modified the file on disk, but it's a network drive so we didn't get any notification that the file has changed.

@delashum
Copy link
Owner

@delashum delashum commented Feb 25, 2021

Do those two commits seem sufficient? Tested locally and works fine

@lishid
Copy link
Author

@lishid lishid commented Feb 25, 2021

Let me do a quick review here.

const file = app.vault.getFiles().find((f) => f.path === item.filePath)

Here you should probably use the proper API to get the file - there's one for getting the abstract file by path, just need to check if the result is instanceof TFile.

Actually, why aren't you reading the file contents when toggling the item? Seems like you're working off a full copy of the file contents that you've saved sometimes earlier (which may be stale).

I recommend reading the file right before writing it, and processing that version. If you think the file may/will have changed between then two, then depending on what you'd want to do, you can either work off the new version, or maybe just don't touch the file.

You've set your plugin to isDesktopOnly = false, but you're using the NodeJS os package:

import * as os from 'os'

Should probably use a user-agent test for isMacOS instead.

const app: App = (window as any).app

Don't use this. (You're using it more than once) It's not exposed for a good reason. Pass it in from your plugin/view. You can access it when you inherit ItemView using this.app.


More of a long term/bigger improvement down the line:

export const parseTodos = async (

Consider avoiding re-parse every single file on every rerender? Our API can give specific file changes and you could use to incrementally update your parsed files (delete/rename from vault, new/change from metadataCache).

@lishid
Copy link
Author

@lishid lishid commented Feb 25, 2021

So the specific case of data loss I believe is caused by a race-condition that you aren't testing for generally: when a file is changed locally, its cachedData is wiped when Obsidian receives the OS notification. Between then and the metadata cache updates with the new file contents, you'll be left with a stale copy of the old data. Before your patch, this will wipe the file as cachedData was emptied.

@liamcain
Copy link

@liamcain liamcain commented Feb 25, 2021

Also, just to add on: when you're parsing the tasks to display them in the view, you're better off keeping it as vault.cachedRead since it will be notably faster. So I don't think you want the change in a247a5c.

BUT as @lishid mentioned, treat that fileInfo.content as stale and re-read the particular file when toggling the task.

@delashum
Copy link
Owner

@delashum delashum commented Feb 25, 2021

The reason I decided to not re-read the file before writing is that I am tracking each checklist-item by what line it's on. And if I read a file right before writing and the todo is now on a different line than what's in my cache the update won't be possible. I will think about another way to approach this though. There is definitely a race condition since there is a delay between updates and the this.app.metadataCache.on("resolve",...) firing.

Since there isn't really any documentation I have little insight into what the different between TFile and TAbstractFile. I am only using app.vault.getFiles().find((f) => f.path === item.filePath) because app.vaule.getAbstractFile() didn't seem to provide what I was looking for. I would definitely love to hear how I can get a specific file though.

I am curious what's wrong with (window as any).app . Seems like it's pointing to the exact same object as this.app coming from the View. Btw, I got my inspiration to pull app from window from your repo @liamcain obsidian-calendar-plugin.

Good suggestion on using user-agent. I'll get that fixed

To your last suggestion, I would love to only re-parse updated files! It is very unclear to me due to the lack of documentation on how to listen for those sort of changes, so i opted to just re-parse everything each time.

@lishid
Copy link
Author

@lishid lishid commented Feb 25, 2021

The reason I decided to not re-read the file before writing is that I am tracking each checklist-item by what line it's on. And if I read a file right before writing and the todo is now on a different line than what's in my cache the update won't be possible. I will think about another way to approach this though. There is definitely a race condition since there is a delay between updates and the this.app.metadataCache.on("resolve",...) firing.

Yeah I think for this, you should probably compare the read file with the one you have cached, and bail with a new Notice saying the underlying file has changed, try again in a few seconds. If you have a way to know what the user did and map it to the new file (probably not for your case I'm guessing?) you can attempt to still perform the action on the new file contents.

Since there isn't really any documentation I have little insight into what the different between TFile and TAbstractFile. I am only using app.vault.getFiles().find((f) => f.path === item.filePath) because app.vaule.getAbstractFile() didn't seem to provide what I was looking for. I would definitely love to hear how I can get a specific file though.

Do this:

let file = vault.getAbstractFileByPath(path);
if (file instanceof TFile) {
  //... your code here ...
}

I am curious what's wrong with (window as any).app . Seems like it's pointing to the exact same object as this.app coming from the View. Btw, I got my inspiration to pull app from window from your repo @liamcain obsidian-calendar-plugin.

😠 @liamcain

window.app is used as a debugging pointer for testing things from console. It may be renamed or removed in the future.

To your last suggestion, I would love to only re-parse updated files! It is very unclear to me due to the lack of documentation on how to listen for those sort of changes, so i opted to just re-parse everything each time.

The ones we're internally using for incrementally generating search results and backlinks, is something like the following:

vault.on('delete') => remove the file
vault.on('rename') => move the results for the old file to the new file, knowing the contents probably didn't change
metadataCache.on('changed') (or 'resolve' if you need the cache) => parse/update the file. This replaces the new and changed triggers from vault.

@delashum
Copy link
Owner

@delashum delashum commented Feb 25, 2021

Awesome, thanks for the helpful responses. This gives me a lot more information to work with. I apologize again for losing one of your users data, that sucks.

I should be able to find some time this week to make all these changes.

Also, I would be happy to help write some documentation on the Plug-in API if that's something you are interested in happening. There are quite a few things that were really unclear to me that I think could help prevent this sort of thing going forward.

@lishid
Copy link
Author

@lishid lishid commented Feb 25, 2021

No worries! Awesome work and appreciate the quick response. In the end it's not your fault, we should have more actively pushed our users to backup their data. Which we will!

Liam is working on some FAQ on the docs, which might be of interest to you. Would be great if you can participate once you have more time.

@dolphinpoint
Copy link

@dolphinpoint dolphinpoint commented Feb 27, 2021

Really hope you can get this working @delashum. This plugin is one of my favorites! 🤙

delashum added a commit that referenced this issue Feb 28, 2021
@delashum
Copy link
Owner

@delashum delashum commented Feb 28, 2021

@lishid what needs to happen to get this back on the store? I just released 1.0.12 which includes all the changes we discussed in this thread

@lishid
Copy link
Author

@lishid lishid commented Feb 28, 2021

I can add it back to the plugin list.

@lishid
Copy link
Author

@lishid lishid commented Feb 28, 2021

@delashum delashum closed this Feb 28, 2021
@dolphinpoint
Copy link

@dolphinpoint dolphinpoint commented Feb 28, 2021

🙌🙌🙌

@luckman212
Copy link

@luckman212 luckman212 commented Feb 28, 2021

@brandonxoliver I think you meant 💎🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
5 participants