Files
bambuddy/frontend/src/utils/slicer.ts
T
maziggy 76b997fc8a fix(slicer): encode file URL in protocol-handler scheme on Windows/Linux
"Open in Slicer" emitted `orcaslicer://open?file=<URL>` and
  `bambustudio://open?file=<URL>` by plain string concatenation, relying
  on a stale comment that claimed the browser preserves URLs in the query
  string. That ignores the slicer's own `url_decode()` on the received
  query (BS post_init → url_decode + split_str; OrcaSlicer Downloader
  regex + url_decode), so any already-percent-encoded character — most
  commonly `%20` from filenames with spaces — decoded to a literal space
  and the slicer's subsequent HTTP GET returned 0 bytes or 404.

  All three URL forms now use `encodeURIComponent()` (matching what the
  macOS `bambustudioopen://` branch was already doing, which is why the
  bug didn't surface on macOS). Corrected the file-level comment to
  document the actual invariant.

  Regression test in slicer.test.ts feeds the exact issue reproduction
  URL and asserts `%2520` appears in the generated href.
2026-04-23 10:03:44 +02:00

101 lines
3.6 KiB
TypeScript

/**
* Utility for opening files in slicer applications
*
* Protocol handler URL formats (from BambuStudio/OrcaSlicer source code):
*
* Bambu Studio has TWO separate URL handlers:
* 1. post_init() [Windows/Linux CLI args]: bambustudio://open?file=<URL>
* - Checks: starts_with("bambustudio://open")
* - Calls url_decode(), then split_str(url, "file=")
* 2. MacOpenURL() [macOS Apple Events]: bambustudioopen://<encoded-URL>
* - Checks: starts_with("bambustudioopen://")
* - Strips prefix, then url_decode()
*
* OrcaSlicer Downloader accepts both formats via regex:
* - (orcaslicer|bambustudio|...)://open?file=<URL>
* - bambustudioopen://<URL>
*
* Key insight: every form needs encodeURIComponent on the file URL, because
* the slicer calls url_decode() on the received query (post_init calls
* url_decode then split_str; MacOpenURL strips the prefix then url_decode;
* OrcaSlicer's Downloader regex-extracts then url_decode). Without encoding,
* any already-percent-encoded character in the download URL (most commonly
* %20 in filenames with spaces) decodes to a literal space and the slicer's
* subsequent HTTP fetch fails with a 0-byte body or 404. See issue #1059.
*/
export type SlicerType = 'bambu_studio' | 'orcaslicer';
type Platform = 'windows' | 'macos' | 'linux' | 'unknown';
/**
* Detect the user's operating system
*/
export function detectPlatform(): Platform {
const userAgent = navigator.userAgent.toLowerCase();
const platform = navigator.platform?.toLowerCase() || '';
if (userAgent.includes('win') || platform.includes('win')) {
return 'windows';
}
if (userAgent.includes('mac') || platform.includes('mac')) {
return 'macos';
}
if (userAgent.includes('linux') || platform.includes('linux')) {
return 'linux';
}
return 'unknown';
}
/**
* Open a URL in the specified slicer application.
* @param downloadUrl - The URL to the file to open
* @param slicer - Which slicer to use (defaults to bambu_studio)
*/
export function openInSlicer(downloadUrl: string, slicer: SlicerType = 'bambu_studio'): void {
let url: string;
const encoded = encodeURIComponent(downloadUrl);
if (slicer === 'orcaslicer') {
url = `orcaslicer://open?file=${encoded}`;
} else {
const platform = detectPlatform();
if (platform === 'macos') {
// macOS only: bambustudioopen scheme via MacOpenURL() callback.
url = `bambustudioopen://${encoded}`;
} else {
// Windows/Linux: bambustudio://open?file= via post_init() CLI args.
// IMPORTANT: On Linux, BS only handles "bambustudio://open" prefix —
// it does NOT process "bambustudioopen://" (that's macOS-only).
url = `bambustudio://open?file=${encoded}`;
}
}
// Use a temporary <a> element to trigger the protocol handler.
// This avoids navigating away from the page (unlike window.location.href).
const link = document.createElement('a');
link.href = url;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
/**
* Build a full download URL for a file
* @param path - The API path (e.g., from api.getArchiveForSlicer())
*/
export function buildDownloadUrl(path: string): string {
return `${window.location.origin}${path}`;
}
/**
* Convenience function to open an archive in the slicer
* @param path - The API path to the archive
* @param slicer - Which slicer to use (defaults to bambu_studio)
*/
export function openArchiveInSlicer(path: string, slicer: SlicerType = 'bambu_studio'): void {
const downloadUrl = buildDownloadUrl(path);
openInSlicer(downloadUrl, slicer);
}