main
swift 210 lines 7.36 KB
Raw
1 import Foundation
2
3 enum OndeEnvironmentBootstrap {
4 struct Paths {
5 let baseURL: URL
6 let hfHomeURL: URL
7 let hfHubCacheURL: URL
8 let temporaryURL: URL
9 }
10
11 private static var didConfigure = false
12
13 static var estimatedDownloadDescription: String {
14 #if os(tvOS)
15 "About 380 MB"
16 #else
17 "About 941 MB"
18 #endif
19 }
20
21 static func configureIfNeeded() {
22 guard !didConfigure else { return }
23
24 guard let paths = paths() else {
25 NSLog(
26 "[OndeEnvironmentBootstrap] ERROR: could not find any writable directory for HF cache"
27 )
28 return
29 }
30
31 NSLog("[OndeEnvironmentBootstrap] Using base directory: %@", paths.baseURL.path)
32
33 setenv("HF_HOME", paths.hfHomeURL.path, 1)
34 setenv("HF_HUB_CACHE", paths.hfHubCacheURL.path, 1)
35 setenv("HUGGINGFACE_HUB_CACHE", paths.hfHubCacheURL.path, 1)
36 setenv("TMPDIR", paths.temporaryURL.path, 1)
37
38 NSLog("[OndeEnvironmentBootstrap] HF_HOME=%@", paths.hfHomeURL.path)
39 NSLog("[OndeEnvironmentBootstrap] HF_HUB_CACHE=%@", paths.hfHubCacheURL.path)
40
41 didConfigure = true
42 }
43
44 static func storageUsageDescription() -> String {
45 guard let paths = paths() else { return "Not available" }
46 let byteCount = directorySize(at: paths.baseURL)
47 guard byteCount > 0 else { return "Not downloaded yet" }
48 return ByteCountFormatter.string(fromByteCount: byteCount, countStyle: .file)
49 }
50
51 static func clearPrivateGuideFiles() {
52 guard let paths = paths() else { return }
53 let fileManager = FileManager.default
54 try? fileManager.removeItem(at: paths.baseURL)
55 try? fileManager.removeItem(at: paths.temporaryURL)
56 didConfigure = false
57 configureIfNeeded()
58 }
59
60 // MARK: - Path resolution
61
62 /// Resolve the best writable base directory for model storage.
63 ///
64 /// Priority order:
65 /// 1. App Group container — persistent, shared across apps in the group.
66 /// This is the preferred location matching splitfire/pepakbasajawa.
67 /// 2. Library/Caches (tvOS only) — writable but can be purged by the system.
68 /// 3. Library/Application Support — persistent, app-private.
69 /// 4. Temporary directory — always writable, last resort.
70 ///
71 /// Instead of a separate writability probe, we try to create the actual
72 /// subdirectories we need (models/hub/, tmp/). If that succeeds, the path
73 /// is usable.
74 static func paths() -> Paths? {
75 let fileManager = FileManager.default
76
77 // 1. App Group container (highest priority — persistent + shared)
78 if let appGroupURL = fileManager.containerURL(
79 forSecurityApplicationGroupIdentifier: "group.com.ondeinference.apps"
80 ) {
81 NSLog("[OndeEnvironmentBootstrap] App Group URL returned: %@", appGroupURL.path)
82
83 let paths = makePaths(from: appGroupURL)
84 if tryCreateDirectories(paths, label: "AppGroup") {
85 return paths
86 }
87 } else {
88 NSLog("[OndeEnvironmentBootstrap] App Group container returned nil")
89 }
90
91 // 2. Library/Application Support/OndeCache (persistent, survives app updates)
92 if let appSupportURL = fileManager.urls(
93 for: .applicationSupportDirectory, in: .userDomainMask
94 ).first {
95 let supportBase = appSupportURL.appendingPathComponent(
96 "OndeCache", isDirectory: true)
97 let paths = makePaths(from: supportBase)
98 if tryCreateDirectories(paths, label: "ApplicationSupport") {
99 return paths
100 }
101 }
102
103 // 3. Library/Caches/OndeCache (writable, but purgeable — wiped on reinstall)
104 #if os(tvOS)
105 if let cachesURL = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first {
106 let cacheBase = cachesURL.appendingPathComponent("OndeCache", isDirectory: true)
107 let paths = makePaths(from: cacheBase)
108 if tryCreateDirectories(paths, label: "Caches") {
109 return paths
110 }
111 }
112 #endif
113
114 // 4. Hard fallback: temporary directory
115 let fallback = fileManager.temporaryDirectory
116 .appendingPathComponent("OndeCache", isDirectory: true)
117 let paths = makePaths(from: fallback)
118 if tryCreateDirectories(paths, label: "TempDir") {
119 NSLog("[OndeEnvironmentBootstrap] Using temp fallback: %@", fallback.path)
120 return paths
121 }
122
123 return nil
124 }
125
126 private static func makePaths(from baseURL: URL) -> Paths {
127 let hfHomeURL = baseURL.appendingPathComponent("models", isDirectory: true)
128 let hfHubCacheURL = hfHomeURL.appendingPathComponent("hub", isDirectory: true)
129 let temporaryURL = baseURL.appendingPathComponent("tmp", isDirectory: true)
130
131 return Paths(
132 baseURL: baseURL,
133 hfHomeURL: hfHomeURL,
134 hfHubCacheURL: hfHubCacheURL,
135 temporaryURL: temporaryURL
136 )
137 }
138
139 /// Try to create the actual directory tree we need.
140 /// Returns true if all directories were created (or already exist).
141 private static func tryCreateDirectories(_ paths: Paths, label: String) -> Bool {
142 let fileManager = FileManager.default
143 let dirs = [paths.hfHubCacheURL, paths.temporaryURL]
144
145 for dir in dirs {
146 do {
147 try fileManager.createDirectory(
148 at: dir,
149 withIntermediateDirectories: true,
150 attributes: nil
151 )
152 } catch {
153 NSLog(
154 "[OndeEnvironmentBootstrap] %@ not usable — failed to create %@: %@",
155 label, dir.path, error.localizedDescription
156 )
157 return false
158 }
159 }
160
161 // Verify we can actually write a file (not just create dirs)
162 let probeFile = paths.temporaryURL.appendingPathComponent("probe")
163 do {
164 try Data([0x42]).write(to: probeFile)
165 try fileManager.removeItem(at: probeFile)
166 } catch {
167 NSLog(
168 "[OndeEnvironmentBootstrap] %@ not usable — write probe failed: %@",
169 label, error.localizedDescription
170 )
171 return false
172 }
173
174 NSLog("[OndeEnvironmentBootstrap] %@ is writable: %@", label, paths.baseURL.path)
175 return true
176 }
177
178 // MARK: - Helpers
179
180 private static func directorySize(at url: URL) -> Int64 {
181 let fileManager = FileManager.default
182 guard
183 let enumerator = fileManager.enumerator(
184 at: url,
185 includingPropertiesForKeys: [.isRegularFileKey, .fileSizeKey],
186 options: [.skipsHiddenFiles]
187 )
188 else {
189 return 0
190 }
191
192 var totalSize: Int64 = 0
193
194 for case let fileURL as URL in enumerator {
195 guard
196 let resourceValues = try? fileURL.resourceValues(forKeys: [
197 .isRegularFileKey, .fileSizeKey,
198 ]),
199 resourceValues.isRegularFile == true,
200 let fileSize = resourceValues.fileSize
201 else {
202 continue
203 }
204
205 totalSize += Int64(fileSize)
206 }
207
208 return totalSize
209 }
210 }