Skip to content
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ winget install KnifeLemon.Filee
- Drag gestures need X11.
On Wayland, use the drop zone and the file manager's right-click menu.

Portable releases keep their data beside the app; see [portable mode](docs/PORTABLE.md).

Filee shows new releases in its menu and updates itself, keeping your settings and engines.
After an update, a few slides show what's new.

Expand Down
10 changes: 8 additions & 2 deletions build/build-installer.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

Setup itself (src/Filee.Setup) is published as one self-contained, trimmed program (no .NET needed on the PC);
build/tools/make-setup.cs appends the publish folder to it as an LZMA payload with a SHA-256 manifest, and the
engines Setup offers from EngineDownloads. The portable zip is the publish folder as it is: without Setup's
uninstall.exe next to Filee.exe, Filee knows it is a portable copy.
engines Setup offers from EngineDownloads. The portable zip adds filee.portable beside Filee.exe; the installer
payload does not include this marker.

.PARAMETER Runtime
win-x64 (default) or win-arm64. The ARM64 build (Filee-<version>-win-arm64-Setup.exe and -Portable.zip) has an ARM64
Expand Down Expand Up @@ -195,6 +195,12 @@ if ($Portable) {
Write-Host "zip $zip"
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::CreateFromDirectory($PublishDir, $zip, [System.IO.Compression.CompressionLevel]::Optimal, $false)
# Add only to the archive: the publish folder also supplies Setup's installed payload.
$archive = [System.IO.Compression.ZipFile]::Open($zip, [System.IO.Compression.ZipArchiveMode]::Update)
try {
if (-not $archive.GetEntry('filee.portable')) { $null = $archive.CreateEntry('filee.portable') }
}
finally { $archive.Dispose() }
$files += $zip
}

Expand Down
2 changes: 2 additions & 0 deletions build/build-portable.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ else {
Copy-Item -LiteralPath (Join-Path $root 'docs/GNOME-WAYLAND.md') -Destination $package
}
Invoke-Checked 'Installer permissions' { chmod +x (Join-Path $package 'install.sh') (Join-Path $package 'uninstall.sh') }
# Keep the marker outside the signed macOS bundle. The install scripts keep installed copies separate.
New-Item -ItemType File -Path (Join-Path $package 'filee.portable') | Out-Null
$temporaryArchive = "$archive.$([Guid]::NewGuid().ToString('N')).partial"
Invoke-Checked 'Create tar.gz' { tar -czf $temporaryArchive -C $stage $packageName }
Move-Item -LiteralPath $temporaryArchive -Destination $archive
Expand Down
3 changes: 3 additions & 0 deletions build/smoke-portable.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ New-Item -ItemType Directory -Path $WorkDir | Out-Null
$smoke = (Resolve-Path -LiteralPath $WorkDir).Path
Invoke-Checked 'Archive extraction' { tar -xzf $archive.FullName -C $smoke }
$package = Join-Path $smoke ($archive.Name.Replace('.tar.gz', ''))
if (-not (Test-Path -LiteralPath (Join-Path $package 'filee.portable') -PathType Leaf)) {
throw 'Portable archive is missing filee.portable.'
}
if ($IsMacOS) {
Invoke-Checked 'Extracted app signature' { codesign --verify --deep --strict (Join-Path $package 'Filee.app') }
}
Expand Down
14 changes: 14 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,20 @@ hidden rows and manual breaks, then see the PDF that those settings will produce

## Settings and data

`PortableMode.Initialize` runs in both entry points before logging, settings or engine discovery. A
`filee.portable` marker selects the adjacent `data` directory through `FILEE_DATA_DIR`; an explicit override
keeps precedence. macOS stores the marker/data outside `.app` to preserve the signed bundle. Startup validates
write/delete access and reports failure without loading the normal user profile. Portable settings turn off
login and file-manager integration; the existing separate-data guard prevents registration and unregistration.
The General page explains this and disables those controls.

Conversion work folders, temporary files and engine caches keep their existing behavior and locations.
Portable mode does not change the environment passed to external engines.
Portable release archives include the marker by default. Installer payloads keep normal profile storage;
the desktop app offers a one-time copy of an existing profile into unused portable data before loading settings.
Import stages copies first and preserves the source. Windows integration cleanup checks the exact executable
target and asks independently of import. See [portable mode](PORTABLE.md) for migration and quarantine details.

`UserDataStore` owns `settings.json`, `presets.json`, `profiles.json` and `history.json`
(System.Text.Json source generation, atomic writes, broken files are kept as `*.broken`).
Bump `AppSettings.CurrentSchemaVersion` and add a step to `SettingsMigrations` when the format changes.
Expand Down
45 changes: 45 additions & 0 deletions docs/PORTABLE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
### True portable mode

Portable release archives include an empty `filee.portable` beside `Filee.exe` (or the `Filee` executable on Linux).
For other copies, create this marker before starting Filee to enable portable mode.
On macOS, put the marker beside `Filee.app`, outside the bundle. Settings, downloaded engines and logs then live
in a `data` folder beside the marker. Both the app and CLI use it. Login startup and file-manager integration
are disabled, leaving an installed copy's entries untouched. The data folder must be writable; Filee reports
an error instead of falling back to the user profile.

Conversion work folders, temporary files and engine caches keep their existing behavior and locations.
An explicit `FILEE_DATA_DIR` still takes precedence.
Installer packages keep using the normal user profile.
Update a marked copy by replacing its application files while keeping `filee.portable` and `data`.

### Bringing existing data along

Close the previous Filee copy before starting the portable app. If portable data has no settings, library,
history or downloaded engines and the normal profile contains `settings.json`, the app offers a one-time import.
It copies settings, presets, profiles, history and downloaded engines, with percentage progress. Originals stay
untouched. Settings, presets, profiles and history each have their own checkbox; downloaded engines are one
additional group, including shared dependencies. Available choices are selected by default. The engines choice
shows their size when it can be measured. Custom profiles may need their matching presets; import both to keep
custom actions. History can be excluded independently. Unselected engines can be installed later; without imported
settings, normal setup opens.
Choose **Import existing data** to copy the selected categories, or **Start fresh** to use defaults and
remember that choice. This appears before Home and engine setup. A failed copy can be retried;
existing portable data is never overwritten. Start the desktop app before using the CLI if you want to import.
Custom engine paths remain external; downloaded copies travel with the portable folder.

If this same executable previously registered startup or file-manager integration on Windows, the app asks before
removing those entries. Entries targeting another executable are preserved. Cleanup is offered independently of
import, so declining import does not hide obsolete integration. If cleanup fails or is cancelled, it is offered
again next time while owned entries remain. `--uninstall-cleanup` also removes only this copy's entries.

### macOS quarantine

Extract the tar.gz into a writable folder and keep the marker beside `Filee.app`. Finder can launch a quarantined
app from an App Translocation path, where the marker is not visible. After verifying the download's source and
checksum, remove quarantine from the extracted app before launching:

```sh
xattr -dr com.apple.quarantine /path/to/Filee.app
```

The .dmg is an installation image, not a writable portable folder. Installation copies only the app bundle.
4 changes: 4 additions & 0 deletions installer/linux/INSTALL.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
Run ./Filee to test the extracted app, or ./filee-cli help for the command line.
The included filee.portable marker keeps settings, downloaded engines and logs
in data beside the executable. On first start the app offers to copy existing
profile data and downloaded engines into unused portable data, leaving originals intact.
Installing with install.sh uses the normal user profile instead.

Run ./install.sh to install for the current user. It requires Python 3, creates
an application launcher and ~/.local/bin/filee, and does not require root.
Expand Down
26 changes: 24 additions & 2 deletions installer/linux/install.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
#!/bin/sh
set -eu

# Copy application files only: a package may already have been run in portable mode.
copy_application() {
python3 - "$source_dir" "$1" <<'PY'
import os, shutil, sys
source, target = sys.argv[1:]
os.makedirs(target, exist_ok=True)
for name in os.listdir(source):
if name in ('data', 'filee.portable'):
continue
src, dst = os.path.join(source, name), os.path.join(target, name)
if os.path.islink(src):
os.symlink(os.readlink(src), dst)
elif os.path.isdir(src):
shutil.copytree(src, dst, symlinks=True)
else:
shutil.copy2(src, dst)
PY
}

# Filee Linux standalone installer.
# Supports first-time installation and in-place upgrade (--upgrade).
# Preserves downloaded conversion engines (~/.local/share/Filee/engines) and user settings (~/.config/Filee).
Expand Down Expand Up @@ -134,7 +153,9 @@ if [ "$upgrade" -eq 0 ]; then
fi

mkdir -p "$data_dir/filee" "$bin_dir" "$data_dir/applications"
cp -R "$source_dir" "$install_dir"
copy_application "$install_dir"
# The archive is portable; an installed copy uses the ordinary user profile.
rm -f "$install_dir/filee.portable"
printf '%s\n' 'com.filee.app' > "$install_dir/.filee-package"
rm -f "$install_dir/.filee-upgrade-token"
ln -s "$install_dir/filee-cli" "$bin_dir/filee"
Expand Down Expand Up @@ -185,7 +206,8 @@ else
mkdir "$staging_dir"

# Copy incoming payload into staging (preserves source_dir without moving/deleting it).
cp -R "$source_dir/." "$staging_dir/"
copy_application "$staging_dir"
rm -f "$staging_dir/filee.portable"
printf '%s\n' 'com.filee.app' > "$staging_dir/.filee-package"
printf '%s\n' "$upgrade_token" > "$staging_dir/.filee-upgrade-token"
chmod +x "$staging_dir/Filee" "$staging_dir/filee-cli"
Expand Down
10 changes: 10 additions & 0 deletions installer/macos/INSTALL.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
This package requires macOS 13 or later and contains a separate build for
Apple Silicon or Intel Macs. The .NET runtime is included.
The tar.gz includes filee.portable beside Filee.app: running there keeps settings,
downloaded engines and logs in an adjacent data folder. Existing profile data is
offered for import on first start when portable data is unused. Originals are kept.
Installation and the .dmg use the normal user profile.
For portable use, move the extracted folder to a writable location. Finder may
launch a quarantined app from a temporary App Translocation path, hiding the marker.
After verifying the download's source and checksum, remove quarantine from the
extracted app in Terminal before launching it:
xattr -dr com.apple.quarantine /path/to/Filee.app
Keep filee.portable beside Filee.app. Do not run portable mode from the read-only .dmg.
Run ./install.sh to copy Filee.app to ~/Applications and add ~/.local/bin/filee.
From the .dmg, drag Filee.app onto Applications instead (no filee command then).
You can also move Filee.app into Applications manually. Keep its location stable
Expand Down
11 changes: 10 additions & 1 deletion src/Filee.App/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,19 @@ public override void Initialize()
SingleSelectionGuard.Register();
}

public override void OnFrameworkInitializationCompleted()
public override async void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
try { await PortableFirstStart.RunAsync(); }
catch (Exception ex) when (ex is not OutOfMemoryException)
{
Program.Log.LogError(ex, "Portable first-start preparation failed");
desktop.Shutdown(1);
return;
}
// macOS preloads settings to configure its Dock; reload after a possible import.
if (PortableMode.IsEnabled) Program.PreloadedStore?.Load();
AppHost.Build(preloadedStore: Program.PreloadedStore);
Start(desktop);
}
Expand Down
15 changes: 15 additions & 0 deletions src/Filee.App/Assets/i18n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,21 @@
"format.category.Data": "بيانات",
"format.category.Model3D": "نموذج ثلاثي الأبعاد",
"format.category.Geo": "بيانات جغرافية",
"portable.import_existing": "استيراد البيانات الحالية",
"portable.start_fresh": "البدء من جديد",
"portable.import_summary": "انسخ الإعدادات والإعدادات المسبقة والسجل والمحركات المنزّلة إلى هذا المجلد المحمول. لن تتغير الملفات الأصلية أو تُحذف.",
"portable.import_next": "يتابع Filee بالبيانات المحددة. إذا لم تحدد الإعدادات، يبدأ الإعداد الأولي بالقيم الافتراضية. ستظهر موافقة منفصلة لحذف إدخالات بدء التشغيل أو المستكشف القديمة الخاصة بهذا البرنامج. لن تتغير التثبيتات الأخرى.",
"portable.fresh_next": "البدء من جديد يتخطى النسخ ويفتح الإعدادات الافتراضية. تبقى بياناتك الحالية في مكانها.",
"portable.choose_settings": "الإعدادات",
"portable.choose_presets": "الإعدادات المسبقة",
"portable.choose_profiles": "الملفات الشخصية",
"portable.choose_history": "السجل",
"portable.profile_dependencies": "قد تحتاج الملفات الشخصية المخصصة إلى إعداداتها المسبقة. استورد الاثنين للحفاظ على الإجراءات. تبقى المحركات مع تبعياتها.",
"portable.choose_engines": "المحركات المنزّلة",
"portable.import_prompt": "هل تريد نسخ الإعدادات والإعدادات المسبقة والسجل والمحركات المنزّلة من {0} إلى {1}؟ ستبقى النسخ الأصلية دون تغيير. البدء من جديد يبدأ ملفًا محمولًا جديدًا.",
"portable.cleanup_prompt": "هل تريد إزالة إدخالات بدء التشغيل ومدير الملفات السابقة الخاصة بهذا الملف التنفيذي؟ ستبقى إدخالات التثبيتات الأخرى دون تغيير.",
"startup.portable_unwritable": "لا يستطيع Filee الكتابة في مجلد البيانات المحمول:\n{0}\n\nانقل Filee إلى مجلد قابل للكتابة أو اسمح بالكتابة في هذا المجلد، ثم حاول مجددًا.",
"general.portable_mode": "الوضع المحمول: تُحفظ الإعدادات والمحركات المنزلة والسجلات في مجلد البيانات المحمول. بدء التشغيل التلقائي وتكامل مدير الملفات معطلان.",
"general.title": "عام",
"general.language": "اللغة",
"general.language_auto": "لغة النظام",
Expand Down
15 changes: 15 additions & 0 deletions src/Filee.App/Assets/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,21 @@
"format.category.Data": "Daten",
"format.category.Model3D": "3D-Modell",
"format.category.Geo": "Geodaten",
"portable.import_existing": "Vorhandene Daten importieren",
"portable.start_fresh": "Neu beginnen",
"portable.import_summary": "Einstellungen, Vorgaben, Verlauf und heruntergeladene Engines in diesen portablen Ordner kopieren. Die Originale werden weder geändert noch gelöscht.",
"portable.import_next": "Danach verwendet Filee die ausgewählten Daten. Ohne Einstellungen öffnet sich die normale Einrichtung mit Standardwerten. Alte Autostart- oder Explorer-Einträge dieser Programmdatei werden nur nach separater Bestätigung entfernt. Andere Installationen bleiben unverändert.",
"portable.fresh_next": "Neu beginnen überspringt das Kopieren und öffnet die Einrichtung mit Standardwerten. Vorhandene Daten bleiben erhalten.",
"portable.choose_settings": "Einstellungen",
"portable.choose_presets": "Vorgaben",
"portable.choose_profiles": "Profile",
"portable.choose_history": "Verlauf",
"portable.profile_dependencies": "Eigene Profile benötigen eventuell ihre Vorgaben. Importieren Sie beide, um eigene Aktionen zu behalten. Engines werden mit ihren Abhängigkeiten zusammengehalten.",
"portable.choose_engines": "Heruntergeladene Engines",
"portable.import_prompt": "Vorhandene Einstellungen, Vorgaben, Verlauf und heruntergeladene Engines von {0} nach {1} kopieren? Die Originale bleiben unverändert. Neu beginnen startet ein neues portables Profil.",
"portable.cleanup_prompt": "Frühere Autostart- und Dateimanager-Einträge dieser Programmdatei entfernen? Einträge anderer Installationen bleiben unverändert.",
"startup.portable_unwritable": "Filee kann nicht in seinen portablen Datenordner schreiben:\n{0}\n\nVerschieben Sie Filee in einen beschreibbaren Ordner oder erlauben Sie das Schreiben in diesen Ordner und versuchen Sie es erneut.",
"general.portable_mode": "Portabler Modus: Einstellungen, heruntergeladene Engines und Protokolle bleiben im portablen Datenordner. Autostart und Dateimanager-Integration sind deaktiviert.",
"general.title": "Allgemein",
"general.language": "Sprache",
"general.language_auto": "Systemstandard",
Expand Down
15 changes: 15 additions & 0 deletions src/Filee.App/Assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,21 @@
"format.category.Model3D": "3D model",
"format.category.Geo": "Geospatial",

"portable.import_existing": "Import existing data",
"portable.start_fresh": "Start fresh",
"portable.import_summary": "Copy your settings, presets, history and downloaded engines into this portable folder. The original files will not be changed or deleted.",
"portable.import_next": "After copying, Filee continues with your selected data. If settings are not selected, normal setup opens with defaults. If this executable has old startup or Explorer menu entries, a separate confirmation will ask whether to remove them. Other installations stay unchanged.",
"portable.fresh_next": "Start fresh skips copying and opens normal setup with default settings. Your existing data stays where it is.",
"portable.choose_settings": "Settings",
"portable.choose_presets": "Presets",
"portable.choose_profiles": "Profiles",
"portable.choose_history": "History",
"portable.profile_dependencies": "Custom profiles may need their matching presets. Import both to keep custom actions. Downloaded engines are kept together with their dependencies.",
"portable.choose_engines": "Downloaded engines",
"portable.import_prompt": "Copy existing settings, presets, history and downloaded engines from {0} into {1}? The originals will stay unchanged. Choose Start fresh to use default settings.",
"portable.cleanup_prompt": "Remove the previous startup and file-manager entries belonging to this executable? Entries for other installations will stay unchanged.",
"startup.portable_unwritable": "Filee cannot write to its portable data folder:\n{0}\n\nMove Filee to a writable folder or make this folder writable, then try again.",
"general.portable_mode": "Portable mode: settings, downloaded engines and logs stay in the portable data folder. Startup and file-manager integration are disabled.",
"general.title": "General",
"general.language": "Language",
"general.language_auto": "System default",
Expand Down
Loading
Loading