Hi everyone! This is my GSoC proposal on supporting multiple profiles. I didn't get selected though, probably because I didn't have enough PR's
Introduction
Project
Synopsis
I am interested in working on the following ideas for my GSoC
project, taken from the ideas page of Joplin
(https://joplinapp.org/gsoc2020/ideas.html): Support for multiple profiles
Benefits
The multiple profiles feature is important for people who use Joplin
heavily and for multiple purposes. While the concept of notebooks allow
grouping of notes, it does not insulate notebooks from each other. For
an example, it is a very common scenario for a user to use Joplin for
both taking notes at home and at work. Having separate work and home
profiles will be advantageous to them, so that the home notebooks don't
show up while working at his/her office. In general, the concept of
profiles allows for blackbox separation between notes.
Deliverables
Assuming that both these projects are assigned to me, users after the
completion of this project will be able to do the following:
The user will be able to create multiple profiles with different
names, apart from the default profile which is automatically created
when the user opens Joplin for the first time. The profiles will be
completely insulated from each other. On switching profiles, the app
may be required to restart (depending on the final implementation).
It may be possible to add support for multiple instances of Joplin
running on different profiles.
My plans are to first implement these features for the desktop version.
If time permits, I will implement it also for the mobile client.
Details
The following details are based off the desktop client. I will work on
the mobile client if time permits and my mentor allows it.
Basic structure and Desktop UI
I will be taking ideas from the Github issue on this
(https://github.com/laurent22/joplin/issues/591). Currently, the files
and directories where the metadata and database is stored are hard-coded
in BaseApplication.js
via a profileDir
variable. I intend to make a
new class called in Profile
in a file Profile.ts
(As instructed in
CONTRIBUTING.md, I will be using Typescript). The class roughly would
look like this:
class Profile {
id: number;
directoryName: string;
parentDirectory: string;
...
}
The BaseApplication
object will contain a Profile
object, which will
store the current Profile which is being used. In addition, since the
app will restart when profiles are switched, the next profile to open
must be stored in the root folder of all profiles (the .joplin-desktop
directory). As mentioned in the Github issue, it is a good idea to have
an additional Profiles.ini
file in the root directory. For handling
that file, a separate class must be made, which I plan to call
ProfilesHandler
. An object of this class will be loaded in app.js
.
When the app restarts, it will look up the profile to load up and pass
the requisite Profile
object to BaseApplication
.
As for the user interface, I intend to add a menu option in the Files
menu, saying 'Switch Profile;, which will open up a screen or a
pop-over. It is easier to make a screen, but a pop-over will look
better. The pop-over will show the existing profiles as well as an
option to create a new one. Overall, this can be implemented in a file
called ProfileScreen.tsx
, with some changes in app.js
.
Where to change the code
Firstly, we need to change how the profile directory is determined, in determineProfileDir(initArgs)
of lib/BaseApplication.js
. Currently, it first checks whether a profile path is set in the command-line args, otherwise defaults to $HOME/.config/<app-name>/
. Now, instead it will use the object of the ProfilesHandler
class (created in the constructor), which will be defined in Profileshandler.ts
. This class will read the profiles.ini
file. This file will be stored in either the directory pointed by the JOPLINAPP_ROOT
environment variable, if available, or else will default to $HOME/.config/<app-name>/
. The profiles.ini
file will point to the new profile to open. However, if the profile to open is passed via command-line args, then it will open that profile. As of now, the --profile
option accepts the profile-dir
but this will have to be changed to profile-name
in handleStartFlags_(arg, setDefault)
.
Apart from this, it is necessary to write to profiles.ini
when the app exits to indicate the profile to open next time. This change will be reflected in BaseApplication.exit()
, using the object of the ProfilesHandler
class to do it.
For handling conflicts between the global custom css file, to be stored in the profiles root directory, and the custom css file in the profile directory, code must be added after line 1291 of ElectronClient/app.js
to load the global css file as well. Then CssUtils.injectCustomStyles()
must be modified to compare the two custom css files and load the one in the profile directory in case of a conflict. I will figure out the details in future. However, a simple hack that works will be to append the css file from the profile directory after appending the css file from the profiles root directory. By the CSS convention if there are conflicting rules, a later rule will override the previous one.
Apart from this, there will of course be code for the ProfileScreen and for creating profiles, but I have described that in my proposal. A lot of the heavy lifitng of the profiles will be done in the ProfilesHandler.tsx
.
A diagramatic view of how Profiles work
The following flowchart describes how the concept of Profiles is to be used in Joplin:
Simultaneously running multiple profiles
Now that there are seperate databases for seperate profiles, multiple instances of Joplin perhaps could be run on different profiles.
Several problems may arise from this however:
-
How does the new instance of Joplin know the current profile(s) running?
This can be fixed however by adding a section in the profiles.ini
file listing the currently active profiles. An additional complication that may arise from this is that the instances may not be closed in the same order as they are opened. So the profiles.ini
file has to be clobbered and rewritten to completely every time some modifications have to be made to it.(For an instance while closing the app or while creating a new profile).
-
When modifications are made to profiles in one instance how do(es) the other instance(s) know that?
A simple fix may be to outlaw making edits to profiles when multiple instances are running. A more complicated solution is to watch for file changes via the fs.watchfile
or fs.watch
module. Even this solution has potential problems. Because of the buffering mechanisms of the IO interface, while writing to the profiles.ini
file, potentially at several intermediate writes, the file will be left in an illegal state yet triggering the watch listener, and this may lead to the app crashing. Perhaps during the write phase, locking the profiles.ini
from both read and write may solve this problem (having a lockfile mechanism, that is). Even so, we have to disallow editing names of profiles which are currently being run in a seperate instance.
-
What happens to the Electron metadata of the multiple instances?
This doesn't seem to be a problem, even in the current master build. The metadata of the last instance of Joplin to be closed will be the one that is used while opening new ones.
Structure of the $JOPLINAPP_ROOT
folder
As I have mentioned above, I intend to introduce a new environment variable called JOPLINAP_ROOT
. The user can set this to modify where the database and other metadata is stored (not the Electron metadata, however). THe structure of this folder will be as follows:
Note that
- Rectangles indicate directories, ellipses files
-
profile0
is the default profile, which is created the first time the app is opened
-
profile<id>*
denotes that there may be zero or more directorires of the name profile<id>
where id is a number in the range 1, 2, 3 ... . The id is a number to denote the profile, the actual profile names will stored in profiles.ini
.
- All the profile directories have a similar structure. I have shown it fully only for the default profile.
Error handling
Malformed/Missing profiles.ini
file
Malforming can occur if the user edits the file by hand, or due to some errors in the OS/harddisk (this is relatively rare on journalled file systems, such as in all relatively modern machines) or a computer virus. There will be an initial checking in the ProfilesHandler.tsx
. Of course if a section is missing, it can be retrieved from the directory structure. However, under the current design, profile names will not be retrievable. Either the user has to re-input it or the profile name must also be stored in the profie directory (redundancy of data to recover from errors). This process extends to the case if the profiles.ini
file is missing all together.
Altered directory structure
If the user modifies by hand contents of a profile directory, the error will be caught by existing code. If a profiles directory is missing, it will be detected when the user switches to that profile. The user will be informed and the profile directory will bw newly formed. Basically, this is the same as creating a new profile of a given name.
Restarting the app on the desktop
The Electron app can be relaunched using app.relaunch(); app.quit();
according to the official docs. A function encapsulating the same should be made in bridge.js
.