From ba0b2434fb5f082c2befa51845873e46ac947e77 Mon Sep 17 00:00:00 2001 From: Jokin Date: Sun, 12 Jul 2026 01:53:26 +0200 Subject: [PATCH] Rename to miniSCP; migrate legacy data dirs on boot Co-Authored-By: Claude Fable 5 --- README.md | 19 ++++++++++--------- package.json | 2 +- src/bun/index.ts | 6 +++++- src/bun/store/paths.ts | 31 ++++++++++++++++++++++++------- src/bun/transfers/engine.ts | 8 ++++++-- src/shared/branding.ts | 7 +++++-- ui/src/app/platform.ts | 5 +++-- 7 files changed, 54 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 5eeb718..926a8ec 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# BunSCP +# miniSCP A cross-platform, dual-panel SFTP/local file manager — a small WinSCP-style commander built on **Bun**, **Electrobun**, and **Angular**. @@ -90,7 +90,7 @@ Bun main process (`src/bun/**`, RPC schema) trigger an app relaunch. ## Build a packaged app ```bash -bun run build # ng build (ui/dist) + electrobun build -> build/*/BunSCP*.app +bun run build # ng build (ui/dist) + electrobun build -> build/*/miniSCP*.app ``` The Angular bundle is copied into the app under `views://app`. The macOS icon is @@ -107,8 +107,9 @@ Everything user-facing reads from **[`src/shared/branding.ts`](src/shared/brandi ```ts export const BRANDING = { - appName: "BunSCP", // window title, bundle name, app-data folder - identifier: "es.teknes.bunscp", + appName: "miniSCP", // window title, bundle name, app-data folder + identifier: "es.teknes.miniscp", + legacyNames: ["BunSCP"], // previous names; their data dirs migrate on boot version: "0.1.0", icon: "assets/icon.iconset", } as const; @@ -120,14 +121,14 @@ To change the icon: edit `assets/logo.svg` (app icon) and tray template image, and the tray progress frames from the SVGs (`@resvg/resvg-js`, dev-only dependency). -> Renaming `appName` changes the app-data directory, so existing saved -> connections won't be found under the new name — copy `connections.json` across -> if you need them. +> Renaming `appName` changes the app-data directory. Add the old name to +> `legacyNames` and the app moves the previous folder (connections, settings, +> edit temp files) into place on the next boot. ## Where connections are stored Saved connections live in a plain JSON file in the OS app-data directory -(macOS: `~/Library/Application Support/BunSCP/connections.json`), written with +(macOS: `~/Library/Application Support/miniSCP/connections.json`), written with `0600` permissions. > ⚠️ **Passwords are stored in plaintext** in this file (a deliberate v1 @@ -148,7 +149,7 @@ Saved connections live in a plain JSON file in the OS app-data directory ## Same-file protection Copying a file or folder onto itself would truncate it to zero bytes (the -destination is opened for writing before the source is read). BunSCP blocks +destination is opened for writing before the source is read). miniSCP blocks this at several layers: - **Same session / same host**: every connection carries a filesystem identity diff --git a/package.json b/package.json index 6926b1b..42dac62 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "bunscp", + "name": "miniscp", "version": "0.1.0", "private": true, "type": "module", diff --git a/src/bun/index.ts b/src/bun/index.ts index d014931..2ec7889 100644 --- a/src/bun/index.ts +++ b/src/bun/index.ts @@ -7,6 +7,7 @@ import { SessionRegistry } from "./fs/registry"; import { applyApplicationMenu } from "./menu"; import { createAppRPC } from "./rpc-handlers"; import { ConnectionStore } from "./store/connections"; +import { migrateLegacyDataDir } from "./store/paths"; import { SettingsStore } from "./store/settings"; import { AppTray } from "./tray"; import { TransferEngine } from "./transfers/engine"; @@ -31,6 +32,9 @@ async function getMainViewUrl(): Promise { } } +// after a rebrand, adopt the previous name's data dir before stores read it +migrateLegacyDataDir(); + const registry = new SessionRegistry(); const engine = new TransferEngine(registry); const store = new ConnectionStore(); @@ -62,7 +66,7 @@ const win = new BrowserWindow({ ? { titleBarStyle: "hiddenInset" as const, trafficLightOffset: { x: 12, y: 13 } } : {}), // lets the UI pick the fused layout before first paint - preload: `window.__BUNSCP_PLATFORM=${JSON.stringify(process.platform)};`, + preload: `window.__APP_PLATFORM=${JSON.stringify(process.platform)};`, rpc, }); diff --git a/src/bun/store/paths.ts b/src/bun/store/paths.ts index ed401d9..15f6840 100644 --- a/src/bun/store/paths.ts +++ b/src/bun/store/paths.ts @@ -1,18 +1,35 @@ +import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { BRANDING } from "../../shared/branding"; -export function appDataDir(): string { +function dirFor(appName: string): string { const home = os.homedir(); switch (process.platform) { case "darwin": - return path.join(home, "Library", "Application Support", BRANDING.appName); + return path.join(home, "Library", "Application Support", appName); case "win32": - return path.join( - process.env.APPDATA ?? path.join(home, "AppData", "Roaming"), - BRANDING.appName, - ); + return path.join(process.env.APPDATA ?? path.join(home, "AppData", "Roaming"), appName); default: - return path.join(process.env.XDG_CONFIG_HOME ?? path.join(home, ".config"), BRANDING.appName); + return path.join(process.env.XDG_CONFIG_HOME ?? path.join(home, ".config"), appName); + } +} + +export function appDataDir(): string { + return dirFor(BRANDING.appName); +} + +/** One-time move of a previous name's data dir (connections, settings, edits) + * after a rebrand. Runs at boot, before anything touches the stores. */ +export function migrateLegacyDataDir(): void { + const current = appDataDir(); + if (fs.existsSync(current)) return; + for (const legacy of BRANDING.legacyNames) { + const old = dirFor(legacy); + if (!fs.existsSync(old)) continue; + fs.mkdirSync(path.dirname(current), { recursive: true }); + fs.renameSync(old, current); + console.log(`[store] migrated data dir ${legacy} -> ${BRANDING.appName}`); + return; } } diff --git a/src/bun/transfers/engine.ts b/src/bun/transfers/engine.ts index d19ae87..6c6cbe7 100644 --- a/src/bun/transfers/engine.ts +++ b/src/bun/transfers/engine.ts @@ -1,11 +1,14 @@ import { pipeline } from "node:stream/promises"; import type { Readable } from "node:stream"; +import { BRANDING } from "../../shared/branding"; import type { EntryType, TransferSnapshot, TransferState } from "../../shared/types"; import type { FileSystemProvider } from "../fs/provider"; import type { SessionRegistry } from "../fs/registry"; import { walkTree } from "./walk"; const PROGRESS_INTERVAL_MS = 100; +// same-fs probe temp files stay attributable to the app that wrote them +const PROBE_PREFIX = `.${BRANDING.appName.toLowerCase()}-probe-`; // workers per job; the shared slot budget is the real limit const MAX_WORKERS_PER_JOB = 8; @@ -359,7 +362,8 @@ export class TransferEngine { dir: string, ): Promise { const token = crypto.randomUUID(); - const probePath = dst.join(dir, `.bunscp-probe-${token.slice(0, 8)}`); + const probeName = `${PROBE_PREFIX}${token.slice(0, 8)}`; + const probePath = dst.join(dir, probeName); try { await new Promise((resolve, reject) => { const stream = dst.createWriteStream(probePath); @@ -368,7 +372,7 @@ export class TransferEngine { }); try { const chunks: Buffer[] = []; - for await (const chunk of src.createReadStream(src.join(dir, `.bunscp-probe-${token.slice(0, 8)}`))) { + for await (const chunk of src.createReadStream(src.join(dir, probeName))) { chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); } const seen = Buffer.concat(chunks).toString("utf8") === token; diff --git a/src/shared/branding.ts b/src/shared/branding.ts index 6777f10..87c8627 100644 --- a/src/shared/branding.ts +++ b/src/shared/branding.ts @@ -1,8 +1,11 @@ // The single place to rebrand the app. Everything (window title, bundle name, // app-data directory, UI header, icon) reads from here. export const BRANDING = { - appName: "BunSCP", - identifier: "es.teknes.bunscp", + appName: "miniSCP", + identifier: "es.teknes.miniscp", + // Previous app names: their app-data folders are migrated on boot + // (see migrateLegacyDataDir in src/bun/store/paths.ts). + legacyNames: ["BunSCP"], version: "0.1.0", // macOS build converts this .iconset folder into AppIcon.icns. // Regenerate from assets/icon.png with scripts/gen-iconset.sh. diff --git a/ui/src/app/platform.ts b/ui/src/app/platform.ts index 20c1b40..a0c2fc8 100644 --- a/ui/src/app/platform.ts +++ b/ui/src/app/platform.ts @@ -1,11 +1,12 @@ // Injected by the bun process via BrowserWindow preload before any page script. +// Brand-neutral name so rebrands never touch this file. declare global { interface Window { - __BUNSCP_PLATFORM?: string; + __APP_PLATFORM?: string; } } -export const PLATFORM = typeof window !== 'undefined' ? (window.__BUNSCP_PLATFORM ?? '') : ''; +export const PLATFORM = typeof window !== 'undefined' ? (window.__APP_PLATFORM ?? '') : ''; /** True when the window uses macOS hiddenInset chrome (tab bar fused with titlebar). */ export const IS_MAC_CHROME = PLATFORM === 'darwin';