diff --git a/package.json b/package.json index 86d716cf..7e595630 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "runtipi", - "version": "1.6.1", + "version": "2.0.0", "description": "A homeserver for everyone", "scripts": { "knip": "knip", diff --git a/packages/cli/package.json b/packages/cli/package.json index 6963855a..f7d18a95 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@runtipi/cli", - "version": "1.6.0", + "version": "2.0.0", "description": "", "main": "index.js", "bin": "dist/index.js", diff --git a/packages/cli/src/executors/app/app.executors.ts b/packages/cli/src/executors/app/app.executors.ts index 1661cc83..4c08e241 100644 --- a/packages/cli/src/executors/app/app.executors.ts +++ b/packages/cli/src/executors/app/app.executors.ts @@ -158,7 +158,10 @@ export class AppExecutors { this.logger.info(`App ${appId} started`); - await execAsync(`chmod -R a+rwx ${path.join(appDataDirPath)}`).catch(() => {}); + this.logger.info(`Setting permissions for app ${appId}`); + await execAsync(`chmod -R a+rwx ${path.join(appDataDirPath)}`).catch(() => { + this.logger.error(`Error setting permissions for app ${appId}`); + }); return { success: true, message: `App ${appId} started successfully` }; } catch (err) { diff --git a/packages/cli/src/executors/system/system.executors.ts b/packages/cli/src/executors/system/system.executors.ts index b58c67cd..7546c729 100644 --- a/packages/cli/src/executors/system/system.executors.ts +++ b/packages/cli/src/executors/system/system.executors.ts @@ -19,6 +19,7 @@ import { pathExists } from '@/utils/fs-helpers'; import { getEnv } from '@/utils/environment/environment'; import { fileLogger } from '@/utils/logger/file-logger'; import { runPostgresMigrations } from '@/utils/migrations/run-migration'; +import { getUserIds } from '@/utils/environment/user'; const execAsync = promisify(exec); @@ -103,16 +104,15 @@ export class SystemExecutors { path.join(rootFolderHost, 'VERSION'), ]; + const { uid, gid } = getUserIds(); + // Give permission to read and write to all files and folders for the current user await Promise.all( filesAndFolders.map(async (fileOrFolder) => { if (await pathExists(fileOrFolder)) { - await execAsync(`sudo chown -R :tipi ${fileOrFolder}`).catch((e) => { - fileLogger.error(e); - }); - await execAsync(`sudo chmod -R 770 ${fileOrFolder}`).catch((e) => { - fileLogger.error(e); - }); + await execAsync(`sudo chown -R ${uid}:${gid} ${fileOrFolder}`); + + await execAsync(`sudo chmod -R 750 ${fileOrFolder}`); } }), ); @@ -181,11 +181,22 @@ export class SystemExecutors { * This method will start Tipi. * It will copy the system files, generate the system env file, pull the images and start the containers. */ - public start = async (permissions = true) => { + public start = async (sudo = true) => { const spinner = new TerminalSpinner('Starting Tipi...'); try { - if (permissions) { + if (sudo) { await this.ensureFilePermissions(this.rootFolder); + } else { + console.log( + boxen("You are running in sudoless mode. While tipi should work as expected, you'll probably face folder permission issues with the apps you install and you'll need to manually fix them.", { + title: '⛔️ Sudoless mode', + titleAlignment: 'center', + padding: 1, + borderStyle: 'double', + borderColor: 'red', + margin: { top: 1 }, + }), + ); } spinner.start(); @@ -194,7 +205,7 @@ export class SystemExecutors { spinner.done('System files copied'); - if (permissions) { + if (sudo) { await this.ensureFilePermissions(this.rootFolder, false); } @@ -238,8 +249,16 @@ export class SystemExecutors { const out = fs.openSync('./logs/watcher.log', 'a'); const err = fs.openSync('./logs/watcher.log', 'a'); - const subprocess = spawn('./runtipi-cli', [process.argv[1] as string, 'watch'], { cwd: this.rootFolder, detached: true, stdio: ['ignore', out, err] }); - subprocess.unref(); + if (sudo) { + // Dummy sudo call to ask for password + await execAsync('sudo echo "Dummy sudo call"'); + const subprocess = spawn('sudo', ['./runtipi-cli', 'watch'], { cwd: this.rootFolder, stdio: ['inherit', out, err] }); + + subprocess.unref(); + } else { + const subprocess = spawn('./runtipi-cli', [process.argv[1] as string, 'watch'], { cwd: this.rootFolder, detached: true, stdio: ['ignore', out, err] }); + subprocess.unref(); + } spinner.done('Watcher started'); @@ -318,7 +337,7 @@ export class SystemExecutors { * runtipi-cli binary with the new one. * @param {string} target */ - public update = async (target: string) => { + public update = async (target: string, sudo = true) => { const spinner = new TerminalSpinner('Evaluating target version...'); try { spinner.start(); @@ -408,7 +427,15 @@ export class SystemExecutors { // eslint-disable-next-line no-promise-executor-return await new Promise((resolve) => setTimeout(resolve, 3000)); - const childProcess = spawn('./runtipi-cli', [process.argv[1] as string, 'start', '--no-permissions']); + const args = [process.argv[1] as string, 'start']; + + const { isSudo } = getUserIds(); + + if (!sudo && !isSudo) { + args.push('--no-sudo'); + } + + const childProcess = spawn('./runtipi-cli', args); childProcess.stdout.on('data', (data) => { process.stdout.write(data); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 74c8a731..42e9cefd 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -21,9 +21,10 @@ const main = async () => { .command('start') .description('Start tipi') .option('--no-permissions', 'Skip permissions check') + .option('--no-sudo', 'Skip sudo usage') .action(async (options) => { const systemExecutors = new SystemExecutors(); - await systemExecutors.start(options.permissions); + await systemExecutors.start(options.sudo); }); program diff --git a/packages/cli/src/services/watcher/watcher.ts b/packages/cli/src/services/watcher/watcher.ts index e3c50a30..32b1fbc4 100644 --- a/packages/cli/src/services/watcher/watcher.ts +++ b/packages/cli/src/services/watcher/watcher.ts @@ -4,10 +4,14 @@ import { exec } from 'child_process'; import { promisify } from 'util'; import { AppExecutors, RepoExecutors, SystemExecutors } from '@/executors'; import { getEnv } from '@/utils/environment/environment'; +import { getUserIds } from '@/utils/environment/user'; const execAsync = promisify(exec); const runCommand = async (jobData: unknown) => { + const { gid, uid, isSudo } = getUserIds(); + console.log(`Running command with uid ${uid} and gid ${gid}`); + const { installApp, startApp, stopApp, uninstallApp, updateApp, regenerateAppEnv } = new AppExecutors(); const { cloneRepo, pullRepo } = new RepoExecutors(); const { systemInfo, restart, update } = new SystemExecutors(); @@ -65,7 +69,11 @@ const runCommand = async (jobData: unknown) => { } if (data.command === 'update') { - ({ success, message } = await update(data.version)); + if (!isSudo) { + ({ success, message } = await update(data.version, false)); + } else { + ({ success, message } = await update(data.version, true)); + } } } diff --git a/packages/cli/src/utils/environment/user.ts b/packages/cli/src/utils/environment/user.ts new file mode 100644 index 00000000..56ba7771 --- /dev/null +++ b/packages/cli/src/utils/environment/user.ts @@ -0,0 +1,12 @@ +/** + * Returns the user id and group id of the current user + */ +export const getUserIds = () => { + if (process.getgid && process.getuid) { + const isSudo = process.getgid() === 0 && process.getuid() === 0; + + return { uid: process.getuid(), gid: process.getgid(), isSudo }; + } + + return { uid: 1000, gid: 1000, isSudo: false }; +};