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

feat: allow customizing browser data location #33554

Merged
merged 7 commits into from May 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 14 additions & 4 deletions docs/api/app.md
Expand Up @@ -635,8 +635,18 @@ Returns `string` - The current application directory.
* `%APPDATA%` on Windows
* `$XDG_CONFIG_HOME` or `~/.config` on Linux
* `~/Library/Application Support` on macOS
* `userData` The directory for storing your app's configuration files, which by
default it is the `appData` directory appended with your app's name.
* `userData` The directory for storing your app's configuration files, which
by default is the `appData` directory appended with your app's name. By
convention files storing user data should be written to this directory, and
it is not recommended to write large files here because some environments
may backup this directory to cloud storage.
Comment on lines +641 to +642
Copy link
Member

Choose a reason for hiding this comment

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

Does Chromium have this problem? I would be surprised if Chrome's caches are uploaded to cloud storage on any platform...

Copy link
Member Author

Choose a reason for hiding this comment

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

Chrome stores all data under %LOCALAPPDATA%/Google/Chrome, unlike Electron apps it writes nothing to %APPDATA%.

Also Chrome writes browsing data (i.e. sessionData) to a subdirectory (%LOCALAPPDATA%/Google/Chrome/User Data/Default) instead of the root %LOCALAPPDATA%/Google/Chrome, which is also different from Electron where we mix browsing data and other types of data under the same directory.

We really made some mistakes when choosing where to store data in Electron.

I'm going to expose a few more paths in app.getPath and write a guide on how to setup the directories to correct locations after this PR.

Copy link
Member

Choose a reason for hiding this comment

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

Hang on—so Chrome can choose the session data directory per-profile, then! Chromium will put things under userData/Profile 1, userData/Profile 2 and so on. Shouldn't we offer the same capability?

Copy link
Member Author

Choose a reason for hiding this comment

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

We do the same thing in Electron, sessions created by session.fromPartition write data to $sessionData/Partitions/$partitionName subdirectory instead of the root directory.

The difference is Electron writes data of the default session into the root $sessionData directory, while Chrome browser writes into the User Data/Default subdirectory.

Copy link
Member

Choose a reason for hiding this comment

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

Hm, Chrome writes to User Data/$partitionName, right? Default is a name of a partition.

Copy link
Member Author

@zcbenz zcbenz May 4, 2022

Choose a reason for hiding this comment

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

Yes, Chrome treats the default profile the same with other profiles and writes data to User Data/$name.

Electron treats the default session specially and writes its data to $sessionData, the data of other sessions are written into $sessionData/Partitions/$partitionName instead.

This was because we did not consider the support of multiple sessions in the early days of Electron.

Copy link
Member

Choose a reason for hiding this comment

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

ahhhh, right, got it. Thanks, that makes sense!

* `sessionData` The directory for storing data generated by `Session`, such
as localStorage, cookies, disk cache, downloaded dictionaries, network
state, devtools files. By default this points to `userData`. Chromium may
write very large disk cache here, so if your app does not rely on browser
storage like localStorage or cookies to save user data, it is recommended
to set this directory to other locations to avoid polluting the `userData`
Copy link
Member

Choose a reason for hiding this comment

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

other locations

I think it would be a good idea to recommend a particular location here!

Copy link
Member Author

Choose a reason for hiding this comment

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

It is hard to recommend a particular location here within a short paragraph, I would like to introduce a new path that users can easily use in their app app.setPath('sessionData', app.getPath('betterLocation'), with a guide on where apps should store their data. But I would like to do it in a new pull request, since it is going to be a bunch of new changes that need new reviews.

directory.
* `temp` Temporary directory.
* `exe` The current executable file.
* `module` The `libchromiumcontent` library.
Expand Down Expand Up @@ -686,9 +696,9 @@ In that case, the directory should be created with `fs.mkdirSync` or similar.

You can only override paths of a `name` defined in `app.getPath`.

By default, web pages' cookies and caches will be stored under the `userData`
By default, web pages' cookies and caches will be stored under the `sessionData`
directory. If you want to change this location, you have to override the
`userData` path before the `ready` event of the `app` module is emitted.
`sessionData` path before the `ready` event of the `app` module is emitted.

### `app.getVersion()`

Expand Down
5 changes: 4 additions & 1 deletion shell/app/electron_main_delegate.cc
Expand Up @@ -134,11 +134,14 @@ bool ElectronPathProvider(int key, base::FilePath* result) {
break;
case chrome::DIR_APP_DICTIONARIES:
// TODO(nornagon): can we just default to using Chrome's logic here?
if (!base::PathService::Get(chrome::DIR_USER_DATA, &cur))
if (!base::PathService::Get(DIR_SESSION_DATA, &cur))
return false;
cur = cur.Append(base::FilePath::FromUTF8Unsafe("Dictionaries"));
create_dir = true;
break;
case DIR_SESSION_DATA:
// By default and for backward, equivalent to DIR_USER_DATA.
return base::PathService::Get(chrome::DIR_USER_DATA, result);
case DIR_USER_CACHE: {
#if BUILDFLAG(IS_POSIX)
int parent_key = base::DIR_CACHE;
Expand Down
2 changes: 2 additions & 0 deletions shell/browser/api/electron_api_app.cc
Expand Up @@ -473,6 +473,8 @@ IconLoader::IconSize GetIconSizeByString(const std::string& size) {
int GetPathConstant(const std::string& name) {
if (name == "appData")
return DIR_APP_DATA;
else if (name == "sessionData")
return DIR_SESSION_DATA;
else if (name == "userData")
return chrome::DIR_USER_DATA;
else if (name == "cache")
Expand Down
2 changes: 1 addition & 1 deletion shell/browser/browser_process_impl.cc
Expand Up @@ -110,7 +110,7 @@ void BrowserProcessImpl::PostEarlyInitialization() {
// Only use a persistent prefs store when cookie encryption is enabled as that
// is the only key that needs it
base::FilePath prefs_path;
CHECK(base::PathService::Get(chrome::DIR_USER_DATA, &prefs_path));
CHECK(base::PathService::Get(electron::DIR_SESSION_DATA, &prefs_path));
prefs_path = prefs_path.Append(FILE_PATH_LITERAL("Local State"));
base::ThreadRestrictions::ScopedAllowIO allow_io;
scoped_refptr<JsonPrefStore> user_pref_store =
Expand Down
8 changes: 4 additions & 4 deletions shell/browser/electron_browser_client.cc
Expand Up @@ -1139,11 +1139,11 @@ void ElectronBrowserClient::OnNetworkServiceCreated(

std::vector<base::FilePath>
ElectronBrowserClient::GetNetworkContextsParentDirectory() {
base::FilePath user_data_dir;
base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
DCHECK(!user_data_dir.empty());
base::FilePath session_data;
base::PathService::Get(DIR_SESSION_DATA, &session_data);
DCHECK(!session_data.empty());

return {user_data_dir};
return {session_data};
}

std::string ElectronBrowserClient::GetProduct() {
Expand Down
3 changes: 1 addition & 2 deletions shell/browser/electron_browser_context.cc
Expand Up @@ -120,8 +120,7 @@ ElectronBrowserContext::ElectronBrowserContext(const std::string& partition,
base::StringToInt(command_line->GetSwitchValueASCII(switches::kDiskCacheSize),
&max_cache_size_);

CHECK(base::PathService::Get(chrome::DIR_USER_DATA, &path_));

base::PathService::Get(DIR_SESSION_DATA, &path_);
if (!in_memory && !partition.empty())
path_ = path_.Append(FILE_PATH_LITERAL("Partitions"))
.Append(base::FilePath::FromUTF8Unsafe(
Expand Down
2 changes: 1 addition & 1 deletion shell/browser/electron_browser_main_parts.cc
Expand Up @@ -495,7 +495,7 @@ void ElectronBrowserMainParts::PostCreateMainMessageLoop() {
// https://source.chromium.org/chromium/chromium/src/+/master:chrome/common/chrome_switches.cc;l=689;drc=9d82515060b9b75fa941986f5db7390299669ef1
config->should_use_preference =
command_line.HasSwitch(::switches::kEnableEncryptionSelection);
base::PathService::Get(chrome::DIR_USER_DATA, &config->user_data_path);
base::PathService::Get(DIR_SESSION_DATA, &config->user_data_path);
OSCrypt::SetConfig(std::move(config));
#endif
#if BUILDFLAG(IS_POSIX)
Expand Down
6 changes: 3 additions & 3 deletions shell/browser/ui/devtools_manager_delegate.cc
Expand Up @@ -91,10 +91,10 @@ const char kBrowserCloseMethod[] = "Browser.close";

// static
void DevToolsManagerDelegate::StartHttpHandler() {
base::FilePath user_dir;
base::PathService::Get(chrome::DIR_USER_DATA, &user_dir);
base::FilePath session_data;
base::PathService::Get(DIR_SESSION_DATA, &session_data);
content::DevToolsAgentHost::StartRemoteDebuggingServer(
CreateSocketFactory(), user_dir, base::FilePath());
CreateSocketFactory(), session_data, base::FilePath());
}

DevToolsManagerDelegate::DevToolsManagerDelegate() = default;
Expand Down
3 changes: 2 additions & 1 deletion shell/common/electron_paths.h
Expand Up @@ -23,7 +23,8 @@ enum {
PATH_START = 11000,

DIR_USER_CACHE = PATH_START, // Directory where user cache can be written.
DIR_APP_LOGS, // Directory where app logs live
DIR_APP_LOGS, // Directory where app logs live.
DIR_SESSION_DATA, // Where cookies, localStorage are stored.

#if BUILDFLAG(IS_WIN)
DIR_RECENT, // Directory where recent files live
Expand Down
50 changes: 49 additions & 1 deletion spec-main/api-app-spec.ts
Expand Up @@ -3,7 +3,7 @@ import * as cp from 'child_process';
import * as https from 'https';
import * as http from 'http';
import * as net from 'net';
import * as fs from 'fs';
import * as fs from 'fs-extra';
import * as path from 'path';
import { promisify } from 'util';
import { app, BrowserWindow, Menu, session, net as electronNet } from 'electron/main';
Expand Down Expand Up @@ -1078,6 +1078,54 @@ describe('app module', () => {

expect(() => { app.getPath(badPath as any); }).to.throw();
});

describe('sessionData', () => {
const appPath = path.join(__dirname, 'fixtures', 'apps', 'set-path');
const appName = fs.readJsonSync(path.join(appPath, 'package.json')).name;
const userDataPath = path.join(app.getPath('appData'), appName);
const tempBrowserDataPath = path.join(app.getPath('temp'), appName);

const sessionFiles = [
'Preferences',
'Code Cache',
'Local Storage',
'IndexedDB',
'Service Worker'
];
const hasSessionFiles = (dir: string) => {
for (const file of sessionFiles) {
if (!fs.existsSync(path.join(dir, file))) {
return false;
}
}
return true;
};

beforeEach(() => {
fs.removeSync(userDataPath);
fs.removeSync(tempBrowserDataPath);
});

it('writes to userData by default', () => {
expect(hasSessionFiles(userDataPath)).to.equal(false);
cp.spawnSync(process.execPath, [appPath]);
expect(hasSessionFiles(userDataPath)).to.equal(true);
});

it('can be changed', () => {
expect(hasSessionFiles(userDataPath)).to.equal(false);
cp.spawnSync(process.execPath, [appPath, 'sessionData', tempBrowserDataPath]);
expect(hasSessionFiles(userDataPath)).to.equal(false);
expect(hasSessionFiles(tempBrowserDataPath)).to.equal(true);
});

it('changing userData affects default sessionData', () => {
expect(hasSessionFiles(userDataPath)).to.equal(false);
cp.spawnSync(process.execPath, [appPath, 'userData', tempBrowserDataPath]);
expect(hasSessionFiles(userDataPath)).to.equal(false);
expect(hasSessionFiles(tempBrowserDataPath)).to.equal(true);
});
});
});

describe('setAppLogsPath(path)', () => {
Expand Down
43 changes: 43 additions & 0 deletions spec-main/fixtures/apps/set-path/main.js
@@ -0,0 +1,43 @@
const http = require('http');
const { app, ipcMain, BrowserWindow } = require('electron');

if (process.argv.length > 3) {
app.setPath(process.argv[2], process.argv[3]);
}

const html = `
<script>
async function main() {
localStorage.setItem('myCat', 'Tom')
const db = indexedDB.open('db-name', 1)
await new Promise(resolve => db.onsuccess = resolve)
await navigator.serviceWorker.register('sw.js', {scope: './'})
}

main().then(() => {
require('electron').ipcRenderer.send('success')
})
</script>
`;

const js = 'console.log("From service worker")';

app.once('ready', () => {
ipcMain.on('success', () => {
app.quit();
});

const server = http.createServer((request, response) => {
if (request.url === '/') {
response.writeHead(200, { 'Content-Type': 'text/html' });
response.end(html);
} else if (request.url === '/sw.js') {
response.writeHead(200, { 'Content-Type': 'text/javascript' });
response.end(js);
}
}).listen(0, '127.0.0.1', () => {
const serverUrl = 'http://127.0.0.1:' + server.address().port;
const mainWindow = new BrowserWindow({ show: false, webPreferences: { webSecurity: true, nodeIntegration: true, contextIsolation: false } });
mainWindow.loadURL(serverUrl);
});
});
4 changes: 4 additions & 0 deletions spec-main/fixtures/apps/set-path/package.json
@@ -0,0 +1,4 @@
{
"name": "electron-test-set-path",
"main": "main.js"
}