Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ module.exports = {
},
],
rules: {
'no-await-in-loop': 'warn',
'@typescript-eslint/no-use-before-define': ['warn', { functions: false, classes: true, variables: true }],
'array-callback-return': 'warn',
'max-len': [
Expand Down
64 changes: 64 additions & 0 deletions .github/workflows/file-manager-extension-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: File Manager Extension Smoke

on:
pull_request:
types: [opened, synchronize, reopened]

jobs:
smoke-file-manager-extension:
name: "🧪 Smoke ${{ matrix.fileManager }} extension"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
fileManager: [nautilus, nemo, dolphin]

container:
image: ubuntu:22.04

steps:
- name: Install base dependencies
run: |
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl git python3 unzip xdg-utils

- name: Check out Git repository
uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm

- name: Install file manager for matrix target
run: |
case "${{ matrix.fileManager }}" in
nautilus)
DEBIAN_FRONTEND=noninteractive apt-get install -y nautilus
;;
nemo)
DEBIAN_FRONTEND=noninteractive apt-get install -y nemo
;;
dolphin)
DEBIAN_FRONTEND=noninteractive apt-get install -y dolphin
;;
*)
echo "Unsupported file manager: ${{ matrix.fileManager }}"
exit 1
;;
esac

- name: Install dependencies
run: npm ci --ignore-scripts

- name: Ensure Electron install
run: node ./.erb/scripts/ensure-electron-install.cjs

- name: Run extension smoke test
env:
EXPECTED_FILE_MANAGER: ${{ matrix.fileManager }}
HOME: ${{ github.workspace }}/.tmp/internxt-home
run: |
mkdir -p "$HOME"
npm run smoke:file-manager-extension
66 changes: 64 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

## Compatibility

As of right now, Internxt Drive Desktop for Linux is only compatible with Ubuntu and Debian with the File explorer **Nautilus** (The default file explorer for Gnome).
Internxt Drive Desktop for Linux is currently tested and supported on Ubuntu and Debian with the file managers **Nautilus**, **Nemo**, and **Dolphin**.

We cannot guarantee that the app will work properly on other Linux distributions or with other file explorers as our development and testing efforts are focused on ensuring the best experience for Ubuntu and Debian users.
The application is also available through the **.deb** and **.rpm** packages for these distributions.

We cannot guarantee full compatibility on other Linux distributions or with unsupported file managers, although the app may still work in some environments.

## Installation

Expand All @@ -20,6 +22,66 @@ Download and install the `.deb` package for full compatibility:
sudo dpkg -i internxt_2.6.0_amd64.deb
```

## Prerequisites for KDE based distros

### KDE Wallet Configuration Guide

Our application requires the KDE key manager to be properly configured. Depending on your security needs, you can choose between two methods:

* **Method 1 (Recommended / Easy):** Uses standard symmetric encryption with a master password. It is fast, requires no additional software, and supports **automatic unlocking when you log in**.
* **Method 2 (Advanced / GPG):** Uses an OpenPGP key pair via Kleopatra for higher security, though it requires manual entry of your passphrase or PIN upon logging in.

> **Why Kleopatra?** It is the official KDE key manager, offering native integration with KDE Wallet, fewer permission conflicts, and a user-friendly setup wizard compared to generic GPG tools.
>
> For reference, Electron's secure storage API is documented here: [safe-storage](https://www.electronjs.org/docs/latest/api/safe-storage).

---

### Method 1: Standard Setup (Easy & Recommended)

This is the simplest way to set up KDE Wallet and allows seamless automatic unlocking upon system login.

#### Step 1: Open KDE Wallet Settings
1. Open **System Settings**.
2. Navigate to **KDE Wallet** (or search for *Wallet* in the search bar).
3. Ensure **Enable the KDE wallet subsystem** is checked.

#### Step 2: Create a New Wallet
1. Under **Automatic Wallet Selection**, click **Create New Wallet...**
2. Enter a name for your wallet (e.g., `kdewallet`).
3. Select **Blowfish encryption** (standard password) and click **Next**.
4. Enter and confirm your **Master Password**.
> **Note:** If you set this password to match your Linux user login password, the wallet will unlock automatically when you sign in!
5. Click **Finish**.

---

### Method 2: GPG Key Setup (Advanced)

Use this method if you prefer asymmetric GPG encryption managed via external key managers.

#### Step 1: Install Kleopatra
Install **Kleopatra**, which will be used to generate your GPG encryption key:

```bash
sudo apt update && sudo apt install kleopatra
```

#### Step 2: Generate a GPG Key Pair
1. Open **Kleopatra** and click **New Key Pair** (or **File > New Key Pair**).
2. Select **Create a personal OpenPGP key pair**.
3. Enter your **Name** and **Email Address**.
4. Click **Create** (or **Finish**) to complete the setup.

### Step 3: Configure KDE Wallet for GPG
1. Open **System Settings** and search for **KDE Wallet**.
2. Under **Automatic Wallet Selection**, click **Create New Wallet...**
3. Select **Use GPG encryption for added security** and click **Next**.
4. Choose the GPG key you created earlier in Kleopatra and click **Finish**.

---


### AppImage

Alternatively, you can use the AppImage format:
Expand Down
94 changes: 94 additions & 0 deletions assets/dolphin/internxt-dolphin-actions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env bash

set -u

BASE_URL="http://localhost:4567/hydration"
ROOT_FOLDER="$HOME/Internxt Drive"

function encode_relative_path() {
python3 - "$1" "$ROOT_FOLDER" <<'PY'
import base64
import os
import sys

file_path = os.path.realpath(sys.argv[1])
root_folder = os.path.realpath(sys.argv[2])

if not file_path.startswith(root_folder):
print("")
sys.exit(0)

relative_path = file_path[len(root_folder):]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The functionality to generate the share link is still present in the app's code and can be accessed through the hydration API. This made it easy for us to create this script, which calls the corresponding endpoint and saves the link to the clipboard.

Since the extension's settings are managed directly through the system settings, it was necessary for the actions to be executed through this script.

if relative_path == "":
relative_path = "/"

print(base64.b64encode(relative_path.encode("utf-8")).decode("utf-8"))
PY
}

function copy_to_clipboard() {
local value="$1"

if command -v wl-copy >/dev/null 2>&1; then
printf '%s' "$value" | wl-copy
return
fi

if command -v xclip >/dev/null 2>&1; then
printf '%s' "$value" | xclip -selection clipboard
return
fi

if command -v xsel >/dev/null 2>&1; then
printf '%s' "$value" | xsel --clipboard --input
return
fi
}

function copy_link() {
local file_path="$1"
local encoded
encoded="$(encode_relative_path "$file_path")"

if [ -z "$encoded" ]; then
exit 0
fi

local response
response="$(curl -sS -X POST "$BASE_URL/copy-link/$encoded" 2>/dev/null || true)"
if [ -z "$response" ]; then
exit 0
fi

local link
link="$(python3 - "$response" <<'PY'
import json
import sys

response = sys.argv[1]
try:
data = json.loads(response)
except json.JSONDecodeError:
print("")
sys.exit(0)

print(data.get("link", ""))
PY
)"

if [ -n "$link" ]; then
copy_to_clipboard "$link"
fi
}

if [ "$#" -lt 2 ]; then
exit 0
fi

action="$1"
shift

if [ "$action" = "copy-link" ]; then
copy_link "$1"
exit 0
fi
15 changes: 15 additions & 0 deletions assets/dolphin/internxt-virtual-drive.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[Desktop Entry]
Type=Service
Name=Internxt Drive Actions
MimeType=all/allfiles;inode/directory;
ServiceTypes=KonqPopupMenu/Plugin
X-KDE-ServiceTypes=KonqPopupMenu/Plugin
Actions=InternxtCopyLink;
X-KDE-Submenu=Internxt Drive
X-KDE-Priority=TopLevel

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

For Dolphin, new options must be configured using a new .desktop file that directly modifies the system settings.

X-KDE-StartupNotify=false

[Desktop Action InternxtCopyLink]
Name=Copy Internxt Link
Icon=insert-link
Exec=/usr/bin/env bash {{HOME}}/.local/share/internxt-dolphin-extension/internxt-dolphin-actions.sh copy-link %f
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"test:renderer": "vitest --config vitest.config.renderer.ts",
"test:renderer:coverage": "vitest --config vitest.config.renderer.ts --coverage",
"test:coverage": "concurrently \"npm:test:main:coverage\" \"npm:test:renderer:coverage\"",
"smoke:file-manager-extension": "NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true ts-node -r ./tests/smoke/file-manager-extension/smoke-electron-mock.cjs src/backend/features/file-manager-extension/file-manager-extension-smoke.ts",
"coverage:merge": "lcov-result-merger 'coverage/*/lcov.info' 'coverage/lcov.info'",
"type-check": "./scripts/tsc-max-errors.sh",
"prepare": "husky install",
Expand Down Expand Up @@ -75,7 +76,7 @@
"category": "Development"
},
"deb": {
"depends": [
"recommends": [

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The installation of this add-on has been changed to “recommended” because in distributions such as Kubuntu that use KDE, this library is not available by default and prevents the application from being installed.

"python3-nautilus"
]
},
Expand Down Expand Up @@ -253,4 +254,4 @@
"node": ">=24.0.0 <25.0.0",
"npm": ">=10.0.0 <11.0.0"
}
}
}
11 changes: 11 additions & 0 deletions src/backend/features/file-manager-extension/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const supportedFileManagers = ['nautilus', 'nemo', 'dolphin', null] as const;
export type SupportedFileManager = (typeof supportedFileManagers)[number];

export const NAUTILUS_EXTENSION_FILENAME = 'internxt-virtual-drive.py';
export const NEMO_EXTENSION_FILENAME = 'internxt-virtual-drive.py';
export const DOLPHIN_MENU_FILENAME = 'internxt-virtual-drive.desktop';
export const DOLPHIN_HELPER_FILENAME = 'internxt-dolphin-actions.sh';

export function isSupportedFileManager(value: SupportedFileManager): value is SupportedFileManager {
return supportedFileManagers.includes(value);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { detectAvailableFileManager, isNautilusAvailable, isNemoAvailable } from './detect-available';
import { detectAvailableFileManager, isNautilusAvailable } from './detect-available';

const { execAsyncMock } = vi.hoisted(() => ({
execAsyncMock: vi.fn(),
Expand All @@ -21,9 +21,10 @@ type Props = {
desktopEntry?: string;
hasNautilus?: boolean;
hasNemo?: boolean;
hasDolphin?: boolean;
};

function mockExecWith({ desktopEntry, hasNautilus = false, hasNemo = false }: Props) {
function mockExecWith({ desktopEntry, hasNautilus = false, hasNemo = false, hasDolphin = false }: Props) {
execAsyncMock.mockImplementation(async (command: string) => {
if (command === 'xdg-mime query default inode/directory') {
if (!desktopEntry) throw new Error('not found');
Expand Down Expand Up @@ -55,6 +56,17 @@ function mockExecWith({ desktopEntry, hasNautilus = false, hasNemo = false }: Pr
}
}

if (command === 'command -v dolphin') {
if (hasDolphin) {
return {
stdout: '/usr/bin/dolphin\n',
stderr: '',
} as ExecAsyncResult;
} else {
throw new Error('dolphin not found');
}
}

throw new Error(`Unexpected command: ${command}`);
});
}
Expand Down Expand Up @@ -85,6 +97,16 @@ describe('detect-available', () => {
expect(result).toBe('nemo');
});

it('should detect dolphin when it is the default directory manager', async () => {
mockExecWith({
desktopEntry: 'org.kde.dolphin.desktop',
hasDolphin: true,
});

const result = await detectAvailableFileManager();
expect(result).toBe('dolphin');
});

it('should fallback to nemo if only nemo binary is available', async () => {
mockExecWith({
hasNemo: true,
Expand All @@ -94,6 +116,15 @@ describe('detect-available', () => {
expect(result).toBe('nemo');
});

it('should fallback to dolphin if only dolphin binary is available', async () => {
mockExecWith({
hasDolphin: true,
});

const result = await detectAvailableFileManager();
expect(result).toBe('dolphin');
});

it('should return null when no file manager is available', async () => {
mockExecWith({});

Expand All @@ -120,23 +151,4 @@ describe('detect-available', () => {
expect(result).toBe(false);
});
});

describe('isNemoAvailable', () => {
it('should return true when nemo is available', async () => {
mockExecWith({
desktopEntry: 'nemo.desktop',
hasNemo: true,
});

const result = await isNemoAvailable();
expect(result).toBe(true);
});

it('should return false when nemo is not available', async () => {
mockExecWith({});

const result = await isNemoAvailable();
expect(result).toBe(false);
});
});
});
Loading
Loading