Skip to content

Commit 5197b0e

Browse files
authored
fix(import): detect foreign apps via LaunchServices (#1305) (#1318)
* fix(import): detect foreign apps via LaunchServices instead of data files (#1305) * refactor(import): address review feedback — unify app URL discovery and scan workspaces - Add installedAppURL() to the importer protocol with a default impl that uses LaunchServices. DBeaverImporter overrides to try all four product IDs (Community/Enterprise/Ultimate/Lite) in order. - ImportFromAppSourcePicker now derives the app icon from importer.installedAppURL(). DBeaver Enterprise/Ultimate/Lite users get the correct icon instead of falling back to the SF Symbol bird. - DBeaverImporter scans ~/Library/DBeaverData/workspace* for the latest workspace dir instead of hardcoding workspace6, so workspace7 (and any future bump) works without code changes.
1 parent ebf96d4 commit 5197b0e

6 files changed

Lines changed: 67 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3333

3434
### Fixed
3535

36+
- Import from other apps now detects TablePlus, Sequel Ace, and DBeaver via LaunchServices instead of probing for data files, so newly installed apps are picked up even before they have been opened (#1305)
3637
- ClickHouse (and any HTTP-based driver) can now connect to plain-HTTP servers addressed by DNS hostname; previously App Transport Security blocked the request because only IP-addressed plain HTTP is exempt by default (#1316)
3738
- Import from TablePlus now reads passwords from the keychain correctly; previously it queried the wrong service name and silently returned empty passwords without prompting
3839
- Port numbers in the import preview, welcome screen linked-file list, and plugin details no longer render with a thousand separator (e.g. 3306 instead of 3.306) under Vietnamese, German, and other locales that use a dot as a digit separator

TablePro/Core/Services/Export/ForeignApp/DBeaverImporter.swift

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// TablePro
44
//
55

6+
import AppKit
67
import CommonCrypto
78
import Foundation
89
import os
@@ -16,11 +17,29 @@ struct DBeaverImporter: ForeignAppImporter {
1617
let symbolName = "bird"
1718
let appBundleIdentifier = "org.jkiss.dbeaver.core.product"
1819

19-
var workspaceBaseURL: URL = FileManager.default.homeDirectoryForCurrentUser
20-
.appendingPathComponent("Library/DBeaverData/workspace6")
20+
/// All known DBeaver Eclipse product identifiers. Community, Enterprise,
21+
/// Ultimate, and Lite variants each register a different bundle ID, but
22+
/// they all write to the same `~/Library/DBeaverData/workspace*`.
23+
private static let knownBundleIdentifiers = [
24+
"org.jkiss.dbeaver.core.product",
25+
"org.jkiss.dbeaver.ee.core.product",
26+
"org.jkiss.dbeaver.ue.product",
27+
"org.jkiss.dbeaver.lite.product"
28+
]
29+
30+
/// Root directory containing DBeaver workspace folders. The actual
31+
/// workspace path is discovered by scanning for `workspace*` subdirs so
32+
/// future versions (workspace7, etc.) keep working without code changes.
33+
var dbeaverDataRoot: URL = FileManager.default.homeDirectoryForCurrentUser
34+
.appendingPathComponent("Library/DBeaverData")
2135

22-
func isAvailable() -> Bool {
23-
findDataSourcesFile() != nil
36+
func installedAppURL() -> URL? {
37+
for bundleId in Self.knownBundleIdentifiers {
38+
if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId) {
39+
return url
40+
}
41+
}
42+
return nil
2443
}
2544

2645
func connectionCount() -> Int {
@@ -100,18 +119,30 @@ struct DBeaverImporter: ForeignAppImporter {
100119

101120
// MARK: - File Discovery
102121

122+
/// Scans `~/Library/DBeaverData/workspace*` for a project folder that
123+
/// contains `.dbeaver/data-sources.json`. Supports any workspace version
124+
/// (workspace6, workspace7, ...) by enumeration rather than hardcoding.
103125
private func findDataSourcesFile() -> URL? {
104126
let fm = FileManager.default
105-
let basePath = workspaceBaseURL.path
106-
guard fm.fileExists(atPath: basePath),
107-
let contents = try? fm.contentsOfDirectory(atPath: basePath) else { return nil }
108-
109-
for dirName in contents {
110-
let candidate = workspaceBaseURL
111-
.appendingPathComponent(dirName)
112-
.appendingPathComponent(".dbeaver/data-sources.json")
113-
if fm.fileExists(atPath: candidate.path) {
114-
return candidate
127+
guard let workspaceDirs = try? fm.contentsOfDirectory(
128+
at: dbeaverDataRoot,
129+
includingPropertiesForKeys: [.isDirectoryKey],
130+
options: [.skipsHiddenFiles]
131+
) else { return nil }
132+
133+
let workspaces = workspaceDirs
134+
.filter { $0.lastPathComponent.hasPrefix("workspace") }
135+
.sorted { $0.lastPathComponent > $1.lastPathComponent }
136+
137+
for workspace in workspaces {
138+
guard let projects = try? fm.contentsOfDirectory(atPath: workspace.path) else { continue }
139+
for projectName in projects {
140+
let candidate = workspace
141+
.appendingPathComponent(projectName)
142+
.appendingPathComponent(".dbeaver/data-sources.json")
143+
if fm.fileExists(atPath: candidate.path) {
144+
return candidate
145+
}
115146
}
116147
}
117148
return nil

TablePro/Core/Services/Export/ForeignApp/ForeignAppImporter.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// TablePro
44
//
55

6+
import AppKit
67
import Foundation
78
import os
89
import Security
@@ -13,12 +14,29 @@ protocol ForeignAppImporter {
1314
var id: String { get }
1415
var displayName: String { get }
1516
var symbolName: String { get }
17+
/// Canonical bundle identifier of the source app. Importers whose source
18+
/// app ships in multiple editions (e.g. DBeaver Community / Enterprise)
19+
/// should override `installedAppURL()` to look those up as well.
1620
var appBundleIdentifier: String { get }
17-
func isAvailable() -> Bool
21+
func installedAppURL() -> URL?
1822
func connectionCount() -> Int
1923
func importConnections(includePasswords: Bool) throws -> ForeignAppImportResult
2024
}
2125

26+
extension ForeignAppImporter {
27+
/// LaunchServices lookup for the source app. Returns the URL on disk if
28+
/// the app is registered with macOS, regardless of whether the user has
29+
/// opened it or created any data. Override to consider multiple editions.
30+
func installedAppURL() -> URL? {
31+
NSWorkspace.shared.urlForApplication(withBundleIdentifier: appBundleIdentifier)
32+
}
33+
34+
/// Convenience: true when the source app is installed.
35+
func isAvailable() -> Bool {
36+
installedAppURL() != nil
37+
}
38+
}
39+
2240
// MARK: - Result
2341

2442
struct ForeignAppImportResult {

TablePro/Core/Services/Export/ForeignApp/SequelAceImporter.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ struct SequelAceImporter: ForeignAppImporter {
2020
+ "Sequel Ace/Data/Favorites.plist"
2121
)
2222

23-
func isAvailable() -> Bool {
24-
FileManager.default.fileExists(atPath: favoritesFileURL.path)
25-
}
26-
2723
func connectionCount() -> Int {
2824
guard let root = loadRootDict() else { return 0 }
2925
guard let favoritesRoot = root["Favorites Root"] as? [String: Any],

TablePro/Core/Services/Export/ForeignApp/TablePlusImporter.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@ struct TablePlusImporter: ForeignAppImporter {
2121
var groupsFileURL: URL = FileManager.default.homeDirectoryForCurrentUser
2222
.appendingPathComponent("Library/Application Support/com.tinyapp.TablePlus/Data/ConnectionGroups.plist")
2323

24-
func isAvailable() -> Bool {
25-
FileManager.default.fileExists(atPath: connectionsFileURL.path)
26-
}
27-
2824
func connectionCount() -> Int {
2925
guard let data = try? Data(contentsOf: connectionsFileURL),
3026
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil),

TablePro/Views/Connection/ImportFromApp/ImportFromAppSourcePicker.swift

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,8 @@ struct ImportFromAppSourcePicker: View {
9696

9797
@ViewBuilder
9898
private func appIcon(for importer: any ForeignAppImporter) -> some View {
99-
let icon = resolveAppIcon(bundleId: importer.appBundleIdentifier)
100-
if let icon {
101-
Image(nsImage: icon)
99+
if let appURL = importer.installedAppURL() {
100+
Image(nsImage: NSWorkspace.shared.icon(forFile: appURL.path))
102101
.resizable()
103102
.aspectRatio(contentMode: .fit)
104103
} else {
@@ -173,8 +172,4 @@ struct ImportFromAppSourcePicker: View {
173172
onSelect(state.importer, includePasswords)
174173
}
175174

176-
private func resolveAppIcon(bundleId: String) -> NSImage? {
177-
guard let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId) else { return nil }
178-
return NSWorkspace.shared.icon(forFile: appURL.path)
179-
}
180175
}

0 commit comments

Comments
 (0)