Files
sing-box-for-apple/Library/Database/Profile+Transferable.swift
T
世界 42d1107c53 Fix profile save file not working
SwiftUI only supports one fileExporter modifier per view hierarchy.
When multiple fileExporter modifiers are attached, only the last one
takes effect. Unified ProfileExportDocument and ProfileJSONExportDocument
into a single ProfileAnyExportDocument to use one fileExporter for both
file types.
2026-01-13 17:05:12 +08:00

214 lines
7.6 KiB
Swift

import Foundation
import Libbox
import SwiftUI
import UniformTypeIdentifiers
public extension Profile {
func toContent() throws -> LibboxProfileContent {
let content = LibboxProfileContent()
content.name = name
content.type = Int32(type.rawValue)
content.config = try read()
if type != .local {
content.remotePath = remoteURL!
}
if type == .remote {
content.autoUpdate = autoUpdate
content.autoUpdateInterval = autoUpdateInterval
if let lastUpdated {
content.lastUpdated = Int64(lastUpdated.timeIntervalSince1970 * 1000)
}
}
return content
}
}
public func dateFromTimestamp(_ timestamp: Int64) -> Date {
if timestamp > 100_000_000_000 {
return Date(timeIntervalSince1970: Double(timestamp) / 1000)
} else {
return Date(timeIntervalSince1970: Double(timestamp))
}
}
@available(iOS 16.0, macOS 13.0, *)
extension Profile: Transferable {
public static var transferRepresentation: some TransferRepresentation {
ProxyRepresentation { profile in
try TypedProfile(profile.toContent())
}
}
}
public extension LibboxProfileContent {
static func from(_ data: Data) throws -> LibboxProfileContent {
var error: NSError?
let content = LibboxDecodeProfileContent(data, &error)
if let error {
throw error
}
return content!
}
@discardableResult
func importProfile() async throws -> Profile {
let nextProfileID = try await ProfileManager.nextID()
let profileConfigDirectory = FilePath.sharedDirectory.appendingPathComponent("configs", isDirectory: true)
try FileManager.default.createDirectory(at: profileConfigDirectory, withIntermediateDirectories: true)
let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json")
try config.write(to: profileConfig, atomically: true, encoding: .utf8)
var lastUpdatedAt: Date?
if lastUpdated > 0 {
lastUpdatedAt = dateFromTimestamp(lastUpdated)
}
let uniqueProfileName = try await ProfileManager.uniqueName(name)
let profile = Profile(name: uniqueProfileName, type: ProfileType(rawValue: Int(type))!, path: profileConfig.relativePath, remoteURL: remotePath, autoUpdate: autoUpdate, autoUpdateInterval: autoUpdateInterval, lastUpdated: lastUpdatedAt)
try await ProfileManager.create(profile)
await SharedPreferences.selectedProfileID.set(profile.mustID)
return profile
}
func generateShareFile() throws -> URL {
let shareDirectory = FilePath.cacheDirectory.appendingPathComponent("share", isDirectory: true)
try FileManager.default.createDirectory(at: shareDirectory, withIntermediateDirectories: true)
let shareFile = shareDirectory.appendingPathComponent("\(name).bpf")
try encode()!.write(to: shareFile)
return shareFile
}
}
public extension String {
func generateShareFile(name: String) throws -> URL {
let shareDirectory = FilePath.cacheDirectory.appendingPathComponent("share", isDirectory: true)
try FileManager.default.createDirectory(at: shareDirectory, withIntermediateDirectories: true)
let shareFile = shareDirectory.appendingPathComponent(name)
try write(to: shareFile, atomically: true, encoding: .utf8)
return shareFile
}
}
@available(iOS 16.0, macOS 13.0, *)
public struct TypedProfile: Transferable, Codable {
public let content: LibboxProfileContent
public init(_ content: LibboxProfileContent) {
self.content = content
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let data = try container.decode(Data.self)
try self.init(.from(data))
}
public static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .profile) { typed in
try SentTransferredFile(typed.content.generateShareFile(), allowAccessingOriginalFile: true)
} importing: { received in
try TypedProfile(.from(Data(contentsOf: received.file)))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(content.encode()!)
}
}
public extension UTType {
static let profile = UTType(exportedAs: AppConfiguration.profileUTType)
}
#if !os(tvOS)
// MARK: - FileDocument for Export
public struct ProfileExportDocument: FileDocument {
public static var readableContentTypes: [UTType] { [.profile] }
public let data: Data
public let filename: String
public init(content: LibboxProfileContent) throws {
guard let encoded = content.encode() else {
throw NSError(domain: "ProfileExportDocument", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to encode profile"])
}
data = encoded
filename = "\(content.name).bpf"
}
public init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents else {
throw CocoaError(.fileReadCorruptFile)
}
self.data = data
filename = "profile.bpf"
}
public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper {
FileWrapper(regularFileWithContents: data)
}
}
public struct ProfileJSONExportDocument: FileDocument {
public static var readableContentTypes: [UTType] { [.json] }
public let content: String
public let filename: String
public init(jsonContent: String, name: String) {
content = jsonContent
filename = "\(name).json"
}
public init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let content = String(data: data, encoding: .utf8)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.content = content
filename = "profile.json"
}
public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper {
guard let data = content.data(using: .utf8) else {
throw CocoaError(.fileWriteInapplicableStringEncoding)
}
return FileWrapper(regularFileWithContents: data)
}
}
public struct ProfileAnyExportDocument: FileDocument {
public static var readableContentTypes: [UTType] { [.data, .json] }
public let data: Data
public let filename: String
public let contentType: UTType
public init(profile: ProfileExportDocument) {
data = profile.data
filename = profile.filename
contentType = .data
}
public init(json: ProfileJSONExportDocument) {
data = json.content.data(using: .utf8) ?? Data()
filename = json.filename
contentType = .json
}
public init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents else {
throw CocoaError(.fileReadCorruptFile)
}
self.data = data
filename = "profile"
contentType = .data
}
public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper {
FileWrapper(regularFileWithContents: data)
}
}
#endif