Why does fs.writeFile not save file?

I have this plugin:

import { Plugin } from 'obsidian';
const fs = require('fs');
export default class MyPlugin extends Plugin {
    async onload() {
        var jsonContent = JSON.stringify(app.plugins.plugins.breadcrumbs.mainG.toJSON());
        console.log(jsonContent)
        fs.writeFile("output.json", jsonContent, 'utf8', function (err) {
            if (err) {
                console.log("An error occured while writing JSON Object to File.");
                return console.log(err);
            }
            console.log("JSON file has been saved.");
        });
    }
}

But it doesnā€™t save the file. Iā€™m not sure what went wrong?

Try not using the fs library if possible, this would not work on mobile:
https://github.com/obsidianmd/obsidian-releases/blob/master/plugin-review.md#nodejs-and-electron-api

There are methods on Plugin#.app.vault for that: https://marcus.se.net/obsidian-plugin-docs/api/classes/Vault

You can see what errors are produced in the console:

To check for errors, first open the developer tools by pressing Ctrl-Shift-I on Windows/Linux or Cmd-Opt-I on macOS, and then go to the ā€œConsoleā€ tab.

huh, I thought that with Electron we can use in any device? I even thought that Obsidian itself is an Electron app?

No worry this plugin is only intended to run on desktop.

I see that there is a create method:

create(path: string, data: string, options?: DataWriteOptions): Promise<TFile>;

but Iā€™m not sure if this is the correct one, nor how to use it. I donā€™t know what option I have to put in the DataWriteOptions. My code is now:

export default class MyPlugin extends Plugin {
	async onload() {
		var jsonContent = JSON.stringify(this.app.plugins.plugins.breadcrumbs.mainG.toJSON()); 
		console.log(jsonContent)
		app.vault.create(".", jsonContent);

and it says

Uncaught (in promise) Error: File already exists.

I also try to search for my variable, but there is no app.plugins.plugins.dotmaker.jsonContent in the console.

Only the desktop version of Obsidian uses electron, the mobile version uses capacitor.

this.app.vault.create("output.json", jsonContent);
would be the correct function call
you can omit the options as shown by the ?, they contain stuff that is not necessary here.
https://marcus.se.net/obsidian-plugin-docs/api/interfaces/DataWriteOptions

1 Like

I see. It still doesnā€™t export the file (have check in both the plugin folder and Obsidian folder (C:\Users\ganuo\AppData\Local\Obsidian)).

What is the difference between this.app.class and app.class? It seems that the two can be used interchangeably?

Do you know where I can access the jsonContent string in my console?

It should have create the file in your vaults main directory.

this is used to make sure you are using the correct App instance as you could define another variable with the same name in your code.
https://dmitripavlutin.com/javascript-scope/

your jsonContent should already show up in the console.

You are right. I wonder how I could miss that. I remembered I had checked it as well. I even searched for the whole computer. Anyway, I wonder why it decides that the current location is the main vault, not in the plugin directory, where the js file locates?

I see. So app is also a class as well. Reading the documentation I donā€™t see much explanation of it. Can you explain how it works?

Yes the content of that variable is displayed in the console, but I cannot call it again directly in it. Both jsonContent and this.app.plugins.plugins.dotmaker.jsonContent do not work.

I want to automatically update the output every time the plugin runs. create cannot overwrite the file, so I guess modify is the way to go. But I get:

Uncaught (in promise) TypeError: Cannot create property ā€˜savingā€™ on string ā€˜output.jsonā€™

Do you know how to fix?

Because all operations by the vault class are relative to the vaults main directory.
You can use the following snippet to get the correct directory.

 const configPath = this.app.vault.configDir + "/plugins/.../";

The App class represents the currenlty running instance of obsidian.

This is because your jsonContent variable is only accesible inside your onload() function, to be able to read it you need to change its scope(see the link in my last reply).

The modify function requires the use of a TFile, you can get
this by using the getAbstractFileByPath function and the checking if the returned value is instanceof TFile.
If this is not the case you should create the file.


One thing I do notice is that you do not use TypeScript(which is the default for obsidian) to write your plugin, it should not even allowed you to compile the plugin.
Here is a short tutorial to get you started:
https://phibr0.medium.com/how-to-create-your-own-obsidian-plugin-53f2d5d44046

Thanks. I wasnā€™t aware about onload().

Is this the correct code? It returns null instead of instanceof TFile

const configPath = this.app.vault.configDir + "/plugins/.../";
this.app.vault.getAbstractFileByPath(configPath)
null

One thing I do notice is that you do not use TypeScript(which is the default for obsidian) to write your plugin, it should not even allowed you to compile the plugin.

I donā€™t know. Iā€™m editing the sample plugin, so Iā€™m actually coding with ts right now. npm can still compile it to js

The ā€¦ in the snippet is just a placeholder, you need to put in your actual path.

All of these are null:

this.app.vault.getAbstractFileByPath(".")
this.app.vault.getAbstractFileByPath(".obsidian")
this.app.vault.getAbstractFileByPath(this.app.vault.configDir)
this.app.vault.getAbstractFileByPath("D:/")

However, itā€™s interesting that when I use the path "/", I get a circular object:

JSON.stringify(this.app.vault.getAbstractFileByPath("/")) 

VM314:1 Uncaught TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'e'
    |     property '_' -> object with constructor 'Object'
    |     property 'rename' -> object with constructor 'Array'
    |     index 0 -> object with constructor 'Object'
    --- property 'e' closes the circle
    at JSON.stringify (<anonymous>)
    at <anonymous>:1:6

You need to specify an actual file there, so
const configPath = this.app.vault.configDir + "/plugins/yourplugin/data.json";

itā€™s still null :crying_cat_face:

then that file does not exist and you need to create one first.

Add a console.log('captures/' + getFileName()) just to make sure your file name is correct. When I had this problem it turned out that I had a problem with the file path/name and node just wasnā€™t throwing me an error to explain.

yes there is an existing data.json file in the /plugins/dotmaker/ directory

@jhony Just add console.log('captures/' + getFileName()) into the plugin? There is no such a function.

getAbstractFileByPath always returned null if path included a hidden folder (e.g. : .obsidian). Not sure what I am doing wrong.

So I decided to use app.vault.adapter.exists instead.

To create and overwrite a file, I just use app.vault.adapter.write. To read it if it exists, app.vault.adapter.read.

Not sure if it is the proper way to read/write files inside an Obsidian vault, but it seems to work here (so far Iā€™ve only been able to test on Windows, Android 12 and iPad).

2 Likes

Hmm, I use getAbstractFileByPath for a file that is outside the vault and a file inside that vault, and both are not contained in a hidden folder, and itā€™s still null.

Here is how this works:

Relative path for existing file inside .obsidian

const configPath = this.app.vault.configDir + "/plugins/dotmaker/data.json";
app.vault.adapter.exists(configPath) 
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: true

app.vault.adapter.read(configPath)
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: ""

app.vault.adapter.write(configPath) 
[[PromiseState]]: "rejected"
[[PromiseResult]]: "TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received undefined

Absolute path for existing file inside .obsidian

fileinside = app.vault.adapter.basePath + "\\.obsidian\\plugins\\dotmaker\\data.json";

app.vault.adapter.exists(fileinside) 
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: false

app.vault.adapter.read(fileinside) 
[[PromiseState]]: "rejected"
[[PromiseResult]]: Error: "ENOENT: no such file or directory, open 'D:\Quįŗ£ Cįŗ§u\B Nį»™i dung\Knowledge graphs\CĆ¢y vįŗ„n đį»\D:\Quįŗ£ Cįŗ§u\B Nį»™i dung\Knowledge graphs\CĆ¢y vįŗ„n đį»\.obsidian\plugins\dotmaker\data.json'"

app.vault.adapter.write(fileinside) 
[[PromiseState]]: "rejected"
[[PromiseResult]]: TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received undefined

For the file with absolute path outside .obsidian but inside vault, the error is the same with the file inside .obsidian.

checking the properties of app.vault.adapter I see a writeFile() fsPromise, with the arguments like this:

I try app.vault.adapter.writeFile(configPath, output) and it doesnā€™t work, but the autocomplete suggests me to try app.vault.adapter.write(configPath, output). This time it works. I have no idea how to get to understand solution though

You would be better off reading the docs for the adapter class,
there is a write function that takes 3 parameters, the third being optional:
https://marcus.se.net/obsidian-plugin-docs/api/interfaces/DataAdapter#write

The writeFile() function is not documented and is an internal function, so I would recommend not using that.