Rename to miniSCP; migrate legacy data dirs on boot
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# BunSCP
|
# miniSCP
|
||||||
|
|
||||||
A cross-platform, dual-panel SFTP/local file manager — a small WinSCP-style
|
A cross-platform, dual-panel SFTP/local file manager — a small WinSCP-style
|
||||||
commander built on **Bun**, **Electrobun**, and **Angular**.
|
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
|
## Build a packaged app
|
||||||
|
|
||||||
```bash
|
```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
|
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
|
```ts
|
||||||
export const BRANDING = {
|
export const BRANDING = {
|
||||||
appName: "BunSCP", // window title, bundle name, app-data folder
|
appName: "miniSCP", // window title, bundle name, app-data folder
|
||||||
identifier: "es.teknes.bunscp",
|
identifier: "es.teknes.miniscp",
|
||||||
|
legacyNames: ["BunSCP"], // previous names; their data dirs migrate on boot
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
icon: "assets/icon.iconset",
|
icon: "assets/icon.iconset",
|
||||||
} as const;
|
} 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
|
tray template image, and the tray progress frames from the SVGs
|
||||||
(`@resvg/resvg-js`, dev-only dependency).
|
(`@resvg/resvg-js`, dev-only dependency).
|
||||||
|
|
||||||
> Renaming `appName` changes the app-data directory, so existing saved
|
> Renaming `appName` changes the app-data directory. Add the old name to
|
||||||
> connections won't be found under the new name — copy `connections.json` across
|
> `legacyNames` and the app moves the previous folder (connections, settings,
|
||||||
> if you need them.
|
> edit temp files) into place on the next boot.
|
||||||
|
|
||||||
## Where connections are stored
|
## Where connections are stored
|
||||||
|
|
||||||
Saved connections live in a plain JSON file in the OS app-data directory
|
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.
|
`0600` permissions.
|
||||||
|
|
||||||
> ⚠️ **Passwords are stored in plaintext** in this file (a deliberate v1
|
> ⚠️ **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
|
## Same-file protection
|
||||||
|
|
||||||
Copying a file or folder onto itself would truncate it to zero bytes (the
|
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:
|
this at several layers:
|
||||||
|
|
||||||
- **Same session / same host**: every connection carries a filesystem identity
|
- **Same session / same host**: every connection carries a filesystem identity
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "bunscp",
|
"name": "miniscp",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+5
-1
@@ -7,6 +7,7 @@ import { SessionRegistry } from "./fs/registry";
|
|||||||
import { applyApplicationMenu } from "./menu";
|
import { applyApplicationMenu } from "./menu";
|
||||||
import { createAppRPC } from "./rpc-handlers";
|
import { createAppRPC } from "./rpc-handlers";
|
||||||
import { ConnectionStore } from "./store/connections";
|
import { ConnectionStore } from "./store/connections";
|
||||||
|
import { migrateLegacyDataDir } from "./store/paths";
|
||||||
import { SettingsStore } from "./store/settings";
|
import { SettingsStore } from "./store/settings";
|
||||||
import { AppTray } from "./tray";
|
import { AppTray } from "./tray";
|
||||||
import { TransferEngine } from "./transfers/engine";
|
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 registry = new SessionRegistry();
|
||||||
const engine = new TransferEngine(registry);
|
const engine = new TransferEngine(registry);
|
||||||
const store = new ConnectionStore();
|
const store = new ConnectionStore();
|
||||||
@@ -62,7 +66,7 @@ const win = new BrowserWindow({
|
|||||||
? { titleBarStyle: "hiddenInset" as const, trafficLightOffset: { x: 12, y: 13 } }
|
? { titleBarStyle: "hiddenInset" as const, trafficLightOffset: { x: 12, y: 13 } }
|
||||||
: {}),
|
: {}),
|
||||||
// lets the UI pick the fused layout before first paint
|
// 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,
|
rpc,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+24
-7
@@ -1,18 +1,35 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { BRANDING } from "../../shared/branding";
|
import { BRANDING } from "../../shared/branding";
|
||||||
|
|
||||||
export function appDataDir(): string {
|
function dirFor(appName: string): string {
|
||||||
const home = os.homedir();
|
const home = os.homedir();
|
||||||
switch (process.platform) {
|
switch (process.platform) {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
return path.join(home, "Library", "Application Support", BRANDING.appName);
|
return path.join(home, "Library", "Application Support", appName);
|
||||||
case "win32":
|
case "win32":
|
||||||
return path.join(
|
return path.join(process.env.APPDATA ?? path.join(home, "AppData", "Roaming"), appName);
|
||||||
process.env.APPDATA ?? path.join(home, "AppData", "Roaming"),
|
|
||||||
BRANDING.appName,
|
|
||||||
);
|
|
||||||
default:
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { pipeline } from "node:stream/promises";
|
import { pipeline } from "node:stream/promises";
|
||||||
import type { Readable } from "node:stream";
|
import type { Readable } from "node:stream";
|
||||||
|
import { BRANDING } from "../../shared/branding";
|
||||||
import type { EntryType, TransferSnapshot, TransferState } from "../../shared/types";
|
import type { EntryType, TransferSnapshot, TransferState } from "../../shared/types";
|
||||||
import type { FileSystemProvider } from "../fs/provider";
|
import type { FileSystemProvider } from "../fs/provider";
|
||||||
import type { SessionRegistry } from "../fs/registry";
|
import type { SessionRegistry } from "../fs/registry";
|
||||||
import { walkTree } from "./walk";
|
import { walkTree } from "./walk";
|
||||||
|
|
||||||
const PROGRESS_INTERVAL_MS = 100;
|
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
|
// workers per job; the shared slot budget is the real limit
|
||||||
const MAX_WORKERS_PER_JOB = 8;
|
const MAX_WORKERS_PER_JOB = 8;
|
||||||
|
|
||||||
@@ -359,7 +362,8 @@ export class TransferEngine {
|
|||||||
dir: string,
|
dir: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const token = crypto.randomUUID();
|
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 {
|
try {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const stream = dst.createWriteStream(probePath);
|
const stream = dst.createWriteStream(probePath);
|
||||||
@@ -368,7 +372,7 @@ export class TransferEngine {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const chunks: Buffer[] = [];
|
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);
|
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
||||||
}
|
}
|
||||||
const seen = Buffer.concat(chunks).toString("utf8") === token;
|
const seen = Buffer.concat(chunks).toString("utf8") === token;
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
// The single place to rebrand the app. Everything (window title, bundle name,
|
// The single place to rebrand the app. Everything (window title, bundle name,
|
||||||
// app-data directory, UI header, icon) reads from here.
|
// app-data directory, UI header, icon) reads from here.
|
||||||
export const BRANDING = {
|
export const BRANDING = {
|
||||||
appName: "BunSCP",
|
appName: "miniSCP",
|
||||||
identifier: "es.teknes.bunscp",
|
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",
|
version: "0.1.0",
|
||||||
// macOS build converts this .iconset folder into AppIcon.icns.
|
// macOS build converts this .iconset folder into AppIcon.icns.
|
||||||
// Regenerate from assets/icon.png with scripts/gen-iconset.sh.
|
// Regenerate from assets/icon.png with scripts/gen-iconset.sh.
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
// Injected by the bun process via BrowserWindow preload before any page script.
|
// Injected by the bun process via BrowserWindow preload before any page script.
|
||||||
|
// Brand-neutral name so rebrands never touch this file.
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
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). */
|
/** True when the window uses macOS hiddenInset chrome (tab bar fused with titlebar). */
|
||||||
export const IS_MAC_CHROME = PLATFORM === 'darwin';
|
export const IS_MAC_CHROME = PLATFORM === 'darwin';
|
||||||
|
|||||||
Reference in New Issue
Block a user