Skip to content
Merged
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
20 changes: 6 additions & 14 deletions apps/desktop/electron/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
TransferRequestSchema,
type IpcResult,
} from '../shared/contract.js'
import { requireConnection } from './services/connections.js'
import { browserFor, dropSession, sessionFor } from './services/sessions.js'
import { store } from './services/store.js'
import { cancelTransfer, previewTransfer, startTransfer } from './services/transfers.js'
Expand Down Expand Up @@ -71,8 +72,7 @@ export function registerIpc(): void {

handle(IPC.connectionsTest, z.object({ id: z.string().min(1) }), async ({ id }) => {
const db = await store()
const connection = await db.findConnection(id)
if (!connection) throw new Error('That connection no longer exists.')
const connection = await requireConnection(id)

const session = await sessionFor(connection)
const report = await probeConnection(session, connection.rsyncPath)
Expand Down Expand Up @@ -154,9 +154,7 @@ export function registerIpc(): void {
})

handle(IPC.fsListRemote, RemotePathRequestSchema, async ({ connectionId, path }) => {
const db = await store()
const connection = await db.findConnection(connectionId)
if (!connection) throw new Error('That connection no longer exists.')
const connection = await requireConnection(connectionId)

const browser = await browserFor(connection)
try {
Expand All @@ -167,9 +165,7 @@ export function registerIpc(): void {
})

handle(IPC.fsMkdirRemote, RemotePathRequestSchema, async ({ connectionId, path }) => {
const db = await store()
const connection = await db.findConnection(connectionId)
if (!connection) throw new Error('That connection no longer exists.')
const connection = await requireConnection(connectionId)
const browser = await browserFor(connection)
try {
await browser.mkdir(path)
Expand All @@ -180,9 +176,7 @@ export function registerIpc(): void {
})

handle(IPC.fsRenameRemote, RenameRequestSchema, async ({ connectionId, from, to }) => {
const db = await store()
const connection = await db.findConnection(connectionId)
if (!connection) throw new Error('That connection no longer exists.')
const connection = await requireConnection(connectionId)
const browser = await browserFor(connection)
try {
await browser.rename(from, to)
Expand All @@ -196,9 +190,7 @@ export function registerIpc(): void {
IPC.fsDeleteRemote,
z.object({ connectionId: z.string().min(1), path: PathSchema, isDirectory: z.boolean() }),
async ({ connectionId, path, isDirectory }) => {
const db = await store()
const connection = await db.findConnection(connectionId)
if (!connection) throw new Error('That connection no longer exists.')
const connection = await requireConnection(connectionId)
const browser = await browserFor(connection)
try {
if (isDirectory) await browser.rmdir(path)
Expand Down
54 changes: 54 additions & 0 deletions apps/desktop/electron/main/services/connections.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { Connection } from '@diskpush/schemas'
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'

const saved = new Map<string, Connection>()

vi.mock('./store.js', () => ({
store: async () => ({ findConnection: async (id: string) => saved.get(id) ?? null }),
}))

const { requireConnection, resolveConnection } = await import('./connections.js')

beforeAll(() => {
const directory = mkdtempSync(join(tmpdir(), 'diskpush-ssh-config-'))
const path = join(directory, 'config')
writeFileSync(path, 'Host seed1\n HostName seed1.example.com\n User deploy\n Port 2222\n')
process.env.DISKPUSH_SSH_CONFIG = path
})

afterEach(() => saved.clear())

describe('resolveConnection', () => {
// The bug this exists to prevent: the picker offers ~/.ssh/config hosts but
// never saves them, so a database-only lookup made every one of them fail
// with "That connection no longer exists" the moment it was selected — and
// on a machine with no saved connections, that was every server in the list.
it('resolves a host that only exists in ~/.ssh/config', async () => {
const connection = await resolveConnection('ssh-config:seed1')
expect(connection?.host).toBe('seed1.example.com')
expect(connection?.username).toBe('deploy')
expect(connection?.port).toBe(2222)
})

it('prefers the saved row when an imported host was edited afterwards', async () => {
saved.set('ssh-config:seed1', { id: 'ssh-config:seed1', host: 'edited.example.com' } as Connection)
expect((await resolveConnection('ssh-config:seed1'))?.host).toBe('edited.example.com')
})

it('resolves a saved connection by its own id', async () => {
saved.set('abc-123', { id: 'abc-123', host: 'saved.example.com' } as Connection)
expect((await resolveConnection('abc-123'))?.host).toBe('saved.example.com')
})

it('does not read ssh_config for an id that never came from it', async () => {
expect(await resolveConnection('deleted-uuid')).toBeNull()
})

it('still reports a host that has since left ssh_config', async () => {
expect(await resolveConnection('ssh-config:gone')).toBeNull()
await expect(requireConnection('ssh-config:gone')).rejects.toThrow('That connection no longer exists.')
})
})
29 changes: 29 additions & 0 deletions apps/desktop/electron/main/services/connections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { sshConfigConnections } from '@diskpush/ssh-core'
import type { Connection } from '@diskpush/schemas'
import { store } from './store.js'

/** The id prefix `sshConfigConnections()` gives hosts it reads from ~/.ssh/config. */
const SSH_CONFIG_PREFIX = 'ssh-config:'

/**
* Resolves a connection id the renderer sent back to us.
*
* The picker offers saved connections *and* hosts from ~/.ssh/config, and the
* latter are deliberately never persisted — so looking only in the database
* makes every ssh_config host unusable the moment it is selected. Saved rows
* still win: importing a host and then editing it must not be undone by the
* file it came from.
*/
export async function resolveConnection(id: string): Promise<Connection | null> {
const saved = await (await store()).findConnection(id)
if (saved) return saved
if (!id.startsWith(SSH_CONFIG_PREFIX)) return null
return sshConfigConnections().find((connection) => connection.id === id) ?? null
}

/** As `resolveConnection`, for the callers that cannot proceed without one. */
export async function requireConnection(id: string): Promise<Connection> {
const connection = await resolveConnection(id)
if (!connection) throw new Error('That connection no longer exists.')
return connection
}
11 changes: 4 additions & 7 deletions apps/desktop/electron/main/services/transfers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { defaultRsyncOptions, summarizeChanges, type Change, type Endpoint, type
import { execFile } from 'node:child_process'
import { promisify } from 'node:util'
import { IPC, type EndpointRef, type TransferOptions, type TransferRequest } from '../../shared/contract.js'
import { requireConnection, resolveConnection } from './connections.js'
import { store } from './store.js'

const execFileAsync = promisify(execFile)
Expand All @@ -30,9 +31,7 @@ const running = new Map<string, RunningJob>()
async function resolveEndpoint(ref: EndpointRef): Promise<{ endpoint: Endpoint; connectionId: string | null }> {
if (ref.type === 'local') return { endpoint: { type: 'local', path: ref.path }, connectionId: null }

const db = await store()
const connection = await db.findConnection(ref.connectionId)
if (!connection) throw new Error('That connection no longer exists.')
const connection = await requireConnection(ref.connectionId)

return {
endpoint: {
Expand All @@ -49,8 +48,7 @@ async function resolveEndpoint(ref: EndpointRef): Promise<{ endpoint: Endpoint;

async function shellOptionsFor(connectionId: string | null) {
if (!connectionId) return {}
const db = await store()
const connection = await db.findConnection(connectionId)
const connection = await resolveConnection(connectionId)
if (!connection) return {}
return {
keyPath: connection.authType === 'key' || connection.authType === 'key-passphrase' ? connection.keyPath : null,
Expand Down Expand Up @@ -111,8 +109,7 @@ async function buildPlan(request: TransferRequest, overrides: Partial<RsyncOptio
const options = { ...optionsFrom(request.options), ...overrides }

const isServerToServer = source.endpoint.type === 'ssh' && destination.endpoint.type === 'ssh'
const db = await store()
const sourceConnection = source.connectionId ? await db.findConnection(source.connectionId) : null
const sourceConnection = source.connectionId ? await resolveConnection(source.connectionId) : null

return planTransfer({
source: source.endpoint,
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export default function Workspace() {

return (
<div className="flex h-full flex-col">
<header className="flex h-[52px] shrink-0 items-center gap-3.5 border-b border-line bg-chrome px-4">
<header className="flex h-[72px] shrink-0 items-center gap-3.5 border-b border-line bg-chrome px-4">
{/*
Two files, not one. logo.dark.png is the lockup FOR a dark background:
its "Disk" is white, so on the light theme it vanished and the header
Expand All @@ -248,15 +248,15 @@ export default function Workspace() {
alt="DiskPush"
width={2172}
height={724}
className="hidden h-[22px] w-auto dark:block"
className="hidden h-auto w-[150px] dark:block"
priority
/>
<Image
src="/logo.png"
alt="DiskPush"
width={2172}
height={724}
className="block h-[22px] w-auto dark:hidden"
className="block h-auto w-[150px] dark:hidden"
priority
/>
<div className="h-5 w-px bg-line" />
Expand Down
4 changes: 2 additions & 2 deletions apps/web/components/chrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ function Logo() {
alt="DiskPush"
width={2172}
height={724}
className="logo-dark w-60 h-auto"
className="logo-dark w-[150px] h-auto"
/>
<img src="/logo.png" alt="DiskPush" width={2172} height={724} className="logo-light w-60 h-auto" />
<img src="/logo.png" alt="DiskPush" width={2172} height={724} className="logo-light w-[150px] h-auto" />
</>
)
}
Expand Down
Loading