Skip to content

Add new setting for storing user data inside app folder for windows #7513

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

Open
wants to merge 4 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// IPC Channels

const IpcChannels = {
ENABLE_PROXY: 'enable-proxy',
DISABLE_PROXY: 'disable-proxy',
Expand Down Expand Up @@ -45,6 +46,10 @@ const IpcChannels = {
GET_SCREENSHOT_FALLBACK_FOLDER: 'get-screenshot-fallback-folder',
CHOOSE_DEFAULT_FOLDER: 'choose-default-folder',
WRITE_TO_DEFAULT_FOLDER: 'write-to-default-folder',

GET_STORE_USER_DATA_IN_APP_FOLDER_ALLOWED: 'get-store-user-data-in-app-folder-allowed',
GET_STORE_USER_DATA_IN_APP_FOLDER_ENABLED: 'get-store-user-data-in-app-folder-enabled',
TOGGLE_STORE_USER_DATA_IN_APP_FOLDER: 'toggle-store-user-data-in-app-folder',
}

const DBActions = {
Expand Down Expand Up @@ -222,6 +227,9 @@ const MIXED_SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT = 4
// Displayed on the about page and used in the main.js file to only allow bitcoin URLs with this wallet address to be opened
const ABOUT_BITCOIN_ADDRESS = '1Lih7Ho5gnxb1CwPD4o59ss78pwo2T91eS'

// Filename for enabling user data storage in app folder (Windows)
const STORE_USER_DATA_IN_APP_FOLDER_SWITCH_FILENAME = 'store-user-data-in-app-folder'

export {
IpcChannels,
DBActions,
Expand All @@ -235,4 +243,5 @@ export {
SEARCH_RESULTS_DISPLAY_LIMIT,
MIXED_SEARCH_HISTORY_ENTRIES_DISPLAY_LIMIT,
ABOUT_BITCOIN_ADDRESS,
STORE_USER_DATA_IN_APP_FOLDER_SWITCH_FILENAME,
}
6 changes: 3 additions & 3 deletions src/datastores/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import Datastore from '@seald-io/nedb'
let dbPath = null

if (process.env.IS_ELECTRON_MAIN) {
const { app } = require('electron')
const { join } = require('path')
// this code only runs in the electron main process, so hopefully using sync fs code here should be fine 😬
const { statSync, realpathSync } = require('fs')
const userDataPath = app.getPath('userData') // This is based on the user's OS

const { USER_DATA_PATH } = require('../main/userDataFolder')
dbPath = (dbName) => {
let path = join(userDataPath, `${dbName}.db`)
let path = join(USER_DATA_PATH, `${dbName}.db`)

// returns undefined if the path doesn't exist
if (statSync(path, { throwIfNoEntry: false })?.isSymbolicLink) {
Expand Down
27 changes: 23 additions & 4 deletions src/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ import contextMenu from 'electron-context-menu'

import packageDetails from '../../package.json'
import { generatePoToken } from './poTokenGenerator'
import {
STORE_USER_DATA_IN_APP_FOLDER_ALLOWED,
STORE_USER_DATA_IN_APP_FOLDER_ENABLED,
USER_DATA_PATH,
toggleStoreUserDataInAppFolderEnabledAndMigrateFiles,
} from './userDataFolder'

const brotliDecompressAsync = promisify(brotliDecompress)

Expand Down Expand Up @@ -75,6 +81,7 @@ function runApp() {
}

const ROOT_APP_URL = process.env.NODE_ENV === 'development' ? 'http://localhost:9080' : 'app://bundle/index.html'
const APP_FOLDER_PATH = path.dirname(process.execPath)

contextMenu({
showSearchWithGoogle: false,
Expand Down Expand Up @@ -273,12 +280,10 @@ function runApp() {
let mainWindow
let startupUrl

const userDataPath = app.getPath('userData')

// command line switches need to be added before the app ready event first
// that means we can't use the normal settings system as that is asynchronous,
// doing it synchronously ensures that we add it before the event fires
const REPLACE_HTTP_CACHE_PATH = `${userDataPath}/experiment-replace-http-cache`
const REPLACE_HTTP_CACHE_PATH = `${USER_DATA_PATH}/experiment-replace-http-cache`
const replaceHttpCache = existsSync(REPLACE_HTTP_CACHE_PATH)
if (replaceHttpCache) {
// the http cache causes excessive disk usage during video playback
Expand All @@ -287,7 +292,7 @@ function runApp() {
app.commandLine.appendSwitch('disable-http-cache')
}

const PLAYER_CACHE_PATH = `${userDataPath}/player_cache`
const PLAYER_CACHE_PATH = `${APP_FOLDER_PATH}/player_cache`

// See: https://stackoverflow.com/questions/45570589/electron-protocol-handler-not-working-on-windows
// remove so we can register each time as we run the app.
Expand Down Expand Up @@ -1256,6 +1261,20 @@ function runApp() {
relaunch()
})

ipcMain.handle(IpcChannels.GET_STORE_USER_DATA_IN_APP_FOLDER_ALLOWED, () => {
return STORE_USER_DATA_IN_APP_FOLDER_ALLOWED
})

ipcMain.handle(IpcChannels.GET_STORE_USER_DATA_IN_APP_FOLDER_ENABLED, () => {
return STORE_USER_DATA_IN_APP_FOLDER_ENABLED
})

ipcMain.once(IpcChannels.TOGGLE_STORE_USER_DATA_IN_APP_FOLDER, async () => {
await toggleStoreUserDataInAppFolderEnabledAndMigrateFiles()

relaunch()
})

function playerCachePathForKey(key) {
// Remove path separators and period characters,
// to prevent any files outside of the player_cache directory,
Expand Down
64 changes: 64 additions & 0 deletions src/main/userDataFolder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { app } from 'electron'
import { existsSync } from 'fs'
import path from 'path'
import asyncFs from 'fs/promises'
import {
STORE_USER_DATA_IN_APP_FOLDER_SWITCH_FILENAME,
} from '../constants'

const DEFAULT_USER_DATA_PATH = app.getPath('userData')
// For portable version (single executable) the config should be stored next to that executable file
// Otherwise use the folder of many files (with `FreeTube.exe` in it)
const APP_FOLDER_PATH = process.env.PORTABLE_EXECUTABLE_DIR ?? path.dirname(process.execPath)
const APP_FOLDER_USER_DATA_PATH = path.join(APP_FOLDER_PATH, 'userData')
const USER_DATA_IN_APP_FOLDER_SWITCH_FILE_PATH = path.join(APP_FOLDER_PATH, STORE_USER_DATA_IN_APP_FOLDER_SWITCH_FILENAME)
const TO_BE_MIGRATED_FILES = [
// All files in src/datastores/index.js & flag file for experimental setting
'settings.db',
'profiles.db',
'playlists.db',
'history.db',
'search-history.db',
'subscription-cache.db',
'experiment-replace-http-cache',
]
// Windows & NOT installer version & flag file exists
export const STORE_USER_DATA_IN_APP_FOLDER_ALLOWED =
process.platform === 'win32' &&
!existsSync(path.join(path.dirname(process.execPath), 'Uninstall FreeTube.exe'))
export const STORE_USER_DATA_IN_APP_FOLDER_ENABLED =
STORE_USER_DATA_IN_APP_FOLDER_ALLOWED &&
existsSync(USER_DATA_IN_APP_FOLDER_SWITCH_FILE_PATH)
export const USER_DATA_PATH = STORE_USER_DATA_IN_APP_FOLDER_ENABLED ? APP_FOLDER_USER_DATA_PATH : DEFAULT_USER_DATA_PATH

export async function toggleStoreUserDataInAppFolderEnabledAndMigrateFiles() {
// Migrate files first, only toggle setting when migration successful
if (STORE_USER_DATA_IN_APP_FOLDER_ENABLED) {
await migrateFilesFromHereToThere(APP_FOLDER_USER_DATA_PATH, DEFAULT_USER_DATA_PATH)
await asyncFs.rm(USER_DATA_IN_APP_FOLDER_SWITCH_FILE_PATH)
} else {
await migrateFilesFromHereToThere(DEFAULT_USER_DATA_PATH, APP_FOLDER_USER_DATA_PATH)
// create an empty file
const handle = await asyncFs.open(USER_DATA_IN_APP_FOLDER_SWITCH_FILE_PATH, 'w')
await handle.close()
}
}

async function migrateFilesFromHereToThere(herePath, therePath) {
await asyncFs.mkdir(therePath, { recursive: true })

return Promise.all(
TO_BE_MIGRATED_FILES.map(async (filename) => {
const sourceFilepath = path.join(herePath, filename)
const destFilepath = path.join(therePath, filename)
if (!existsSync(sourceFilepath)) {
if (existsSync(destFilepath)) {
return asyncFs.rm(destFilepath)
}
return
}

return asyncFs.copyFile(sourceFilepath, destFilepath)
})
)
}
18 changes: 18 additions & 0 deletions src/preload/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,24 @@ export default {
ipcRenderer.send(IpcChannels.TOGGLE_REPLACE_HTTP_CACHE)
},

/**
* @returns {Promise<boolean>}
*/
getStoreUserDataInAppFolderAllowed: () => {
return ipcRenderer.invoke(IpcChannels.GET_STORE_USER_DATA_IN_APP_FOLDER_ALLOWED)
},

/**
* @returns {Promise<boolean>}
*/
getStoreUserDataInAppFolderEnabled: () => {
return ipcRenderer.invoke(IpcChannels.GET_STORE_USER_DATA_IN_APP_FOLDER_ENABLED)
},

toggleStoreUserDataInAppFolder: () => {
ipcRenderer.send(IpcChannels.TOGGLE_STORE_USER_DATA_IN_APP_FOLDER)
},

// Allows programmatic toggling of picture-in-picture mode without accompanying user interaction.
// See: https://developer.mozilla.org/en-US/docs/Web/Security/User_activation#transient_activation
requestPiP: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,28 @@
:label="$t('Settings.Experimental Settings.Replace HTTP Cache')"
compact
:default-value="replaceHttpCache"
:disabled="replaceHttpCacheLoading"
:disabled="settingValuesLoading"
:tooltip="$t('Tooltips.Experimental Settings.Replace HTTP Cache')"
@change="handleRestartPrompt"
@change="handleReplaceHttpCacheChange"
/>
</FtFlexBox>
<FtFlexBox v-if="storeUserDataInAppFolderAllowed">
<FtToggleSwitch
tooltip-position="top"
:label="$t('Settings.Experimental Settings.Store User Data In App Folder.Label')"
compact
:default-value="storeUserDataInAppFolderEnabled"
:disabled="settingValuesLoading"
:tooltip="$t('Settings.Experimental Settings.Store User Data In App Folder.Tooltip')"
@change="handleStoreUserDataInAppFolderChange"
/>
</FtFlexBox>
<FtPrompt
v-if="showRestartPrompt"
:label="$t('Settings[\'The app needs to restart for changes to take effect. Restart and apply change?\']')"
:option-names="[$t('Yes, Restart'), $t('Cancel')]"
:option-values="['restart', 'cancel']"
@click="handleReplaceHttpCache"
@click="handleRestartPromptClick"
/>
</FtSettingsSection>
</template>
Expand All @@ -34,39 +45,84 @@ import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
import FtPrompt from '../FtPrompt/FtPrompt.vue'

const replaceHttpCacheLoading = ref(true)
const settingValuesLoading = ref(true)
const replaceHttpCache = ref(false)
const storeUserDataInAppFolderEnabled = ref(false)
const showRestartPrompt = ref(false)

const storeUserDataInAppFolderAllowed = ref(false)

const NextActions = {
// Simply use 1-N unique values
NOTHING: 0,
TOGGLE_REPLACE_HTTP_CACHE: 1,
TOGGLE_STORE_USER_DATA_IN_APP_FOLDER: 2,
}

let nextAction = NextActions.NOTHING

onMounted(async () => {
if (process.env.IS_ELECTRON) {
replaceHttpCache.value = await window.ftElectron.getReplaceHttpCache()
storeUserDataInAppFolderAllowed.value = await window.ftElectron.getStoreUserDataInAppFolderAllowed()
if (storeUserDataInAppFolderAllowed.value) {
storeUserDataInAppFolderEnabled.value = await window.ftElectron.getStoreUserDataInAppFolderEnabled()
}
}

replaceHttpCacheLoading.value = false
settingValuesLoading.value = false
})

/**
* @param {boolean} value
*/
function handleRestartPrompt(value) {
function handleReplaceHttpCacheChange(value) {
replaceHttpCache.value = value
nextAction = NextActions.TOGGLE_REPLACE_HTTP_CACHE
showRestartPrompt.value = true
}

/**
* @param {boolean} value
*/
function handleStoreUserDataInAppFolderChange(value) {
storeUserDataInAppFolderEnabled.value = value
nextAction = NextActions.TOGGLE_STORE_USER_DATA_IN_APP_FOLDER
showRestartPrompt.value = true
}

/**
* @param {'restart' | 'cancel' | null} value
*/
function handleReplaceHttpCache(value) {
function handleRestartPromptClick(value) {
showRestartPrompt.value = false

if (value === null || value === 'cancel') {
replaceHttpCache.value = !replaceHttpCache.value
return
}
switch (nextAction) {
case NextActions.TOGGLE_REPLACE_HTTP_CACHE: {
if (value === null || value === 'cancel') {
replaceHttpCache.value = !replaceHttpCache.value
return
}

if (process.env.IS_ELECTRON) {
window.ftElectron.toggleReplaceHttpCache()
if (process.env.IS_ELECTRON) {
window.ftElectron.toggleReplaceHttpCache()
}
break
}
case NextActions.TOGGLE_STORE_USER_DATA_IN_APP_FOLDER: {
if (value === null || value === 'cancel') {
storeUserDataInAppFolderEnabled.value = !storeUserDataInAppFolderEnabled.value
return
}

if (process.env.IS_ELECTRON) {
window.ftElectron.toggleStoreUserDataInAppFolder()
}
break
}
default: {
console.error('[Internal] nextAction has unexpected value', nextAction)
}
}
}
</script>
Expand Down
3 changes: 3 additions & 0 deletions static/locales/en-US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,9 @@ Settings:
Experimental Settings: Experimental
Warning: These settings are experimental, they may cause crashes while enabled. Making backups is highly recommended. Use at your own risk!
Replace HTTP Cache: Replace HTTP Cache
Store User Data In App Folder:
Label: Store User Data In App Folder
Tooltip: User data files will be stored inside the folder where the executable resides. Making backups before enabling is highly recommended. Use at your own risk!
Password Dialog:
Password: Password
Enter Password To Unlock: Enter password to unlock settings
Expand Down