Skip to content

Commit 578c4da

Browse files
committed
chore(desktop): fix s3 folders dialog and add manual check-updates button
1 parent ada294c commit 578c4da

8 files changed

Lines changed: 510 additions & 70 deletions

File tree

apps/desktop/src/main/index.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,21 @@
11
import { app, shell, BrowserWindow, ipcMain } from 'electron'
22
import { join } from 'path'
33
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
4-
import { autoUpdater } from 'electron-updater'
54
import icon from '../../build/icon.png?asset'
65
import { registerS3IpcHandlers } from './s3-ipc'
76
import { registerDynamoDBIpcHandlers } from './dynamodb-ipc'
87
import { registerSQSIpcHandlers } from './sqs-ipc'
98
import { registerSNSIpcHandlers } from './sns-ipc'
109
import { registerMildStackIpcHandlers } from './mildstack-ipc'
1110
import { setupCliInstaller } from './setup-cli'
11+
import { checkForUpdatesInBackground, registerUpdaterIpcHandlers } from './updater-ipc'
1212

1313
// Set app name for macOS Dock and Menu Bar as early as possible
1414
if (process.platform === 'darwin') {
1515
app.name = 'MildStack Desktop'
1616
app.setName('MildStack Desktop')
1717
}
1818

19-
function checkForUpdates(): void {
20-
if (is.dev) return
21-
22-
autoUpdater.on('error', (error) => {
23-
console.error('Auto updater error:', error)
24-
})
25-
26-
autoUpdater.checkForUpdatesAndNotify().catch((error) => {
27-
console.error('Auto updater check failed:', error)
28-
})
29-
}
30-
3119
function createWindow(): void {
3220
// Create the browser window.
3321
const mainWindow = new BrowserWindow({
@@ -88,9 +76,10 @@ app.whenReady().then(() => {
8876
registerSQSIpcHandlers()
8977
registerSNSIpcHandlers()
9078
registerMildStackIpcHandlers()
79+
registerUpdaterIpcHandlers()
9180

9281
createWindow()
93-
checkForUpdates()
82+
checkForUpdatesInBackground()
9483
setupCliInstaller()
9584

9685
app.on('activate', function () {

apps/desktop/src/main/s3-ipc.ts

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -123,32 +123,45 @@ export function registerS3IpcHandlers(): void {
123123
})
124124
)
125125

126-
const folders = (response.CommonPrefixes ?? []).flatMap((prefix) => {
127-
if (!prefix.Prefix) return []
126+
const folders = new Map<string, { Key: string; prefix: string; isFolder: true }>()
127+
128+
for (const prefix of response.CommonPrefixes ?? []) {
129+
if (!prefix.Prefix) continue
130+
folders.set(prefix.Prefix, {
131+
Key: prefix.Prefix,
132+
prefix: prefix.Prefix,
133+
isFolder: true
134+
})
135+
}
136+
137+
const files = (response.Contents ?? []).flatMap((object) => {
138+
if (!object.Key || object.Key === args.prefix) return []
139+
140+
if (object.Key.endsWith('/')) {
141+
if (!folders.has(object.Key)) {
142+
folders.set(object.Key, {
143+
Key: object.Key,
144+
prefix: object.Key,
145+
isFolder: true
146+
})
147+
}
148+
return []
149+
}
150+
128151
return [
129152
{
130-
Key: prefix.Prefix,
131-
prefix: prefix.Prefix,
132-
isFolder: true
153+
Key: object.Key,
154+
LastModified: object.LastModified?.toISOString(),
155+
ETag: object.ETag,
156+
Size: object.Size,
157+
StorageClass: object.StorageClass,
158+
isFolder: false
133159
}
134160
]
135161
})
136162

137-
const folderKeys = new Set(folders.map((f) => f.Key))
138-
139-
const files = (response.Contents ?? [])
140-
.filter((object) => object.Key && object.Key !== args.prefix && !folderKeys.has(object.Key))
141-
.map((object) => ({
142-
Key: object.Key!,
143-
LastModified: object.LastModified?.toISOString(),
144-
ETag: object.ETag,
145-
Size: object.Size,
146-
StorageClass: object.StorageClass,
147-
isFolder: false
148-
}))
149-
150163
return {
151-
objects: [...folders, ...files],
164+
objects: [...folders.values(), ...files],
152165
hasMore: Boolean(response.IsTruncated),
153166
continuationToken: response.NextContinuationToken
154167
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import { app, ipcMain } from 'electron'
2+
import { is } from '@electron-toolkit/utils'
3+
import { autoUpdater } from 'electron-updater'
4+
5+
type AppUpdateState =
6+
| 'idle'
7+
| 'checking'
8+
| 'available'
9+
| 'not-available'
10+
| 'downloading'
11+
| 'downloaded'
12+
| 'unsupported'
13+
| 'error'
14+
15+
export interface AppUpdateStatus {
16+
currentVersion: string
17+
state: AppUpdateState
18+
availableVersion?: string
19+
lastCheckedAt?: string
20+
error?: string
21+
}
22+
23+
let handlersRegistered = false
24+
let listenersRegistered = false
25+
26+
let updateStatus: AppUpdateStatus = {
27+
currentVersion: app.getVersion(),
28+
state: is.dev ? 'unsupported' : 'idle'
29+
}
30+
31+
function getStatus(): AppUpdateStatus {
32+
return {
33+
...updateStatus,
34+
currentVersion: app.getVersion()
35+
}
36+
}
37+
38+
function setStatus(next: Partial<AppUpdateStatus>): void {
39+
updateStatus = {
40+
...updateStatus,
41+
...next,
42+
currentVersion: app.getVersion()
43+
}
44+
}
45+
46+
function parseUpdaterError(error: unknown): string {
47+
if (error instanceof Error) return error.message
48+
return String(error)
49+
}
50+
51+
function updatesSupported(): boolean {
52+
return app.isPackaged && !is.dev
53+
}
54+
55+
function ensureUpdaterListeners(): void {
56+
if (listenersRegistered) return
57+
listenersRegistered = true
58+
59+
autoUpdater.autoDownload = false
60+
autoUpdater.autoInstallOnAppQuit = false
61+
62+
autoUpdater.on('checking-for-update', () => {
63+
setStatus({
64+
state: 'checking',
65+
error: undefined
66+
})
67+
})
68+
69+
autoUpdater.on('update-available', (info) => {
70+
setStatus({
71+
state: 'available',
72+
availableVersion: info.version,
73+
error: undefined
74+
})
75+
})
76+
77+
autoUpdater.on('update-not-available', () => {
78+
setStatus({
79+
state: 'not-available',
80+
availableVersion: undefined,
81+
error: undefined
82+
})
83+
})
84+
85+
autoUpdater.on('download-progress', () => {
86+
setStatus({
87+
state: 'downloading',
88+
error: undefined
89+
})
90+
})
91+
92+
autoUpdater.on('update-downloaded', (info) => {
93+
setStatus({
94+
state: 'downloaded',
95+
availableVersion: info.version,
96+
error: undefined
97+
})
98+
})
99+
100+
autoUpdater.on('error', (error) => {
101+
setStatus({
102+
state: 'error',
103+
error: parseUpdaterError(error)
104+
})
105+
console.error('Auto updater error:', error)
106+
})
107+
}
108+
109+
async function handleCheckForUpdates(): Promise<AppUpdateStatus> {
110+
if (!updatesSupported()) {
111+
setStatus({
112+
state: 'unsupported',
113+
availableVersion: undefined,
114+
error: 'Update checks are only available in packaged builds.',
115+
lastCheckedAt: new Date().toISOString()
116+
})
117+
return getStatus()
118+
}
119+
120+
ensureUpdaterListeners()
121+
122+
try {
123+
setStatus({
124+
state: 'checking',
125+
availableVersion: undefined,
126+
error: undefined,
127+
lastCheckedAt: new Date().toISOString()
128+
})
129+
130+
const result = await autoUpdater.checkForUpdates()
131+
setStatus({ lastCheckedAt: new Date().toISOString() })
132+
133+
if (!result) {
134+
setStatus({
135+
state: 'unsupported',
136+
error: 'Updater is not active in this build.'
137+
})
138+
return getStatus()
139+
}
140+
141+
if (result.isUpdateAvailable) {
142+
setStatus({
143+
state: 'available',
144+
availableVersion: result.updateInfo.version,
145+
error: undefined
146+
})
147+
return getStatus()
148+
}
149+
150+
setStatus({
151+
state: 'not-available',
152+
availableVersion: undefined,
153+
error: undefined
154+
})
155+
return getStatus()
156+
} catch (error) {
157+
setStatus({
158+
state: 'error',
159+
error: parseUpdaterError(error)
160+
})
161+
return getStatus()
162+
}
163+
}
164+
165+
async function handleInstallUpdate(): Promise<AppUpdateStatus> {
166+
if (!updatesSupported()) {
167+
setStatus({
168+
state: 'unsupported',
169+
availableVersion: undefined,
170+
error: 'Updates are only available in packaged builds.'
171+
})
172+
return getStatus()
173+
}
174+
175+
ensureUpdaterListeners()
176+
177+
if (updateStatus.state === 'downloaded') {
178+
autoUpdater.quitAndInstall()
179+
return getStatus()
180+
}
181+
182+
if (updateStatus.state !== 'available') {
183+
setStatus({
184+
state: 'error',
185+
error: 'No update ready to install. Check for updates first.'
186+
})
187+
return getStatus()
188+
}
189+
190+
try {
191+
setStatus({
192+
state: 'downloading',
193+
error: undefined
194+
})
195+
196+
await autoUpdater.downloadUpdate()
197+
198+
setStatus({
199+
state: 'downloaded',
200+
error: undefined
201+
})
202+
203+
autoUpdater.quitAndInstall()
204+
return getStatus()
205+
} catch (error) {
206+
setStatus({
207+
state: 'error',
208+
error: parseUpdaterError(error)
209+
})
210+
return getStatus()
211+
}
212+
}
213+
214+
export function registerUpdaterIpcHandlers(): void {
215+
if (handlersRegistered) return
216+
handlersRegistered = true
217+
218+
ensureUpdaterListeners()
219+
220+
ipcMain.handle('app:update:status', async () => getStatus())
221+
ipcMain.handle('app:update:check', async () => handleCheckForUpdates())
222+
ipcMain.handle('app:update:install', async () => handleInstallUpdate())
223+
}
224+
225+
export function checkForUpdatesInBackground(): void {
226+
if (!updatesSupported()) return
227+
228+
ensureUpdaterListeners()
229+
230+
autoUpdater.checkForUpdates().catch((error) => {
231+
setStatus({
232+
state: 'error',
233+
error: parseUpdaterError(error)
234+
})
235+
console.error('Auto updater check failed:', error)
236+
})
237+
}

apps/desktop/src/preload/index.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,24 @@ interface MildStackInstancesResponse {
190190
ports: number[] | null
191191
}
192192

193+
type AppUpdateState =
194+
| 'idle'
195+
| 'checking'
196+
| 'available'
197+
| 'not-available'
198+
| 'downloading'
199+
| 'downloaded'
200+
| 'unsupported'
201+
| 'error'
202+
203+
interface AppUpdateStatus {
204+
currentVersion: string
205+
state: AppUpdateState
206+
availableVersion?: string
207+
lastCheckedAt?: string
208+
error?: string
209+
}
210+
193211
interface MildStackApi {
194212
instances(): Promise<MildStackInstancesResponse>
195213
start(port: number): Promise<{ success: boolean; error?: string }>
@@ -200,6 +218,9 @@ interface MildStackApi {
200218
setCliPath(cliPath: string): Promise<{ cliPath: string }>
201219
resetCliPath(): Promise<{ cliPath: string }>
202220
testCliPath(): Promise<{ valid: boolean; error?: string }>
221+
getAppUpdateStatus(): Promise<AppUpdateStatus>
222+
checkAppUpdates(): Promise<AppUpdateStatus>
223+
installAppUpdate(): Promise<AppUpdateStatus>
203224
}
204225

205226
declare global {

0 commit comments

Comments
 (0)