Rename to miniSCP; migrate legacy data dirs on boot

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jokin
2026-07-12 01:53:26 +02:00
co-authored by Claude Fable 5
parent 320080de6d
commit ba0b2434fb
7 changed files with 54 additions and 24 deletions
+10 -9
View File
@@ -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
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "bunscp",
"name": "miniscp",
"version": "0.1.0",
"private": true,
"type": "module",
+5 -1
View File
@@ -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<string> {
}
}
// 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,
});
+24 -7
View File
@@ -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;
}
}
+6 -2
View File
@@ -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<boolean> {
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<void>((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;
+5 -2
View File
@@ -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.
+3 -2
View File
@@ -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';