@@ -42,11 +42,14 @@ actor EditorService {
4242 self . siteURL = siteURL
4343 self . urlSession = urlSession
4444
45- self . storeURL = URL . applicationDirectory
46- . appendingPathComponent ( " GutenbergKit " , isDirectory: true )
45+ self . storeURL = EditorService . rootURL
4746 . appendingPathComponent ( siteURL. sha1, isDirectory: true )
4847 }
4948
49+ private static var rootURL : URL {
50+ URL . applicationDirectory. appendingPathComponent ( " GutenbergKit " , isDirectory: true )
51+ }
52+
5053 /// Set up the editor for the given site.
5154 ///
5255 /// - warning: The request make take a significant amount of time the first
@@ -138,7 +141,7 @@ actor EditorService {
138141
139142 // Only write manifest to disk after all assets are successfully fetched
140143 do {
141- createStoreDirectoryIfNeeded ( )
144+ FileManager . default . createDirectoryIfNeeded ( at : storeURL )
142145 try manifestData. write ( to: manifestFileURL)
143146 log ( . info, " Manifest saved to disk " )
144147 } catch {
@@ -169,15 +172,15 @@ actor EditorService {
169172 private func fetchEditorSettings( baseURL: URL , authHeader: String ) async throws -> Data {
170173 let data = try await fetchData ( for: baseURL. appendingPathComponent ( " /wp-block-editor/v1/settings " ) , authHeader: authHeader)
171174 do {
172- createStoreDirectoryIfNeeded ( )
175+ FileManager . default . createDirectoryIfNeeded ( at : storeURL )
173176 try data. write ( to: editorSettingsFileURL)
174177 } catch {
175178 assertionFailure ( " Failed to save settings: \( error) " )
176179 }
177180 return data
178181 }
179182
180- // MARK: - Manifest
183+ // MARK: - Assets Manifest
181184
182185 /// Fetches the editor assets manifest from the WordPress REST API
183186 /// Does not write to disk - use this to get manifest data without persisting it
@@ -194,43 +197,13 @@ actor EditorService {
194197 /// Returns the editor assets manifest as a JSON string, with JavaScript and stylesheet links
195198 /// modified so that their content can be cached and reused by the editor.
196199 ///
197- /// Verifies that all required assets are cached before returning the manifest.
198- ///
199200 /// - Parameter siteURL: The site URL to extract the scheme for scheme-less links
200201 /// - Returns: JSON string of the processed manifest
201- /// - Throws: `EditorServiceError` if assets are missing or manifest processing fails
202202 private func getManifestForEditor( siteURL: String ) throws -> String {
203203 // For scheme-less links (i.e. '//stats.wp.com/w.js'), use the scheme in `siteURL`.
204204 let siteURLScheme = URL ( string: siteURL) ? . scheme
205205 let data = try Data ( contentsOf: manifestFileURL)
206206 let manifest = try JSONDecoder ( ) . decode ( EditorAssetsManifest . self, from: data)
207- let assetLinks = try manifest. parseAssetLinks ( )
208-
209- // Verify all assets are cached
210- let fileManager = FileManager . default
211- var missingAssets : [ String ] = [ ]
212-
213- for urlString in assetLinks {
214- let filename = cachedFilename ( for: urlString)
215- let localURL = assetsDirectoryURL. appendingPathComponent ( filename)
216-
217- if !fileManager. fileExists ( atPath: localURL. path) {
218- missingAssets. append ( urlString)
219- }
220- }
221-
222- if !missingAssets. isEmpty {
223- log ( . error, " Missing \( missingAssets. count) asset(s) from cache " )
224- for (index, asset) in missingAssets. prefix ( 5 ) . enumerated ( ) {
225- log ( . error, " [ \( index + 1 ) ] \( asset) " )
226- }
227- if missingAssets. count > 5 {
228- log ( . error, " ... and \( missingAssets. count - 5 ) more " )
229- }
230- throw URLError ( . resourceUnavailable)
231- }
232-
233- log ( . info, " All \( assetLinks. count) manifest assets verified in cache " )
234207
235208 // Process manifest for editor
236209 let processedData = try manifest. renderForEditor ( defaultScheme: siteURLScheme)
@@ -244,85 +217,93 @@ actor EditorService {
244217
245218 /// Fetches all assets from the manifest and stores them on the device
246219 private func fetchAssets( manifestData: Data ) async throws {
220+ let startTime = CFAbsoluteTimeGetCurrent ( )
247221 let manifest = try JSONDecoder ( ) . decode ( EditorAssetsManifest . self, from: manifestData)
248222 let assetLinks = try manifest. parseAssetLinks ( )
223+ . filter { isSupportedAsset ( $0) }
249224
250225 log ( . info, " Found \( assetLinks. count) assets to fetch " )
251226
252- // Create assets directory if needed
253- let fileManager = FileManager . default
254- if !fileManager. fileExists ( atPath: assetsDirectoryURL. path) {
255- try fileManager. createDirectory ( at: assetsDirectoryURL, withIntermediateDirectories: true )
256- }
227+ FileManager . default. createDirectoryIfNeeded ( at: assetsDirectoryURL)
257228
258229 // Track statistics
259230 var fetchedCount = 0
260231 var cachedCount = 0
261- var totalSize : Int64 = 0
232+ var assetURLs : [ URL ] = [ ]
262233
263234 // Fetch all assets in parallel
264- try await withThrowingTaskGroup ( of: ( Bool, Int64 ) . self) { group in
235+ await withTaskGroup ( of: Result < ( Bool , URL ) , Error > . self) { group in
265236 for link in assetLinks {
266237 group. addTask {
267- try await self . fetchAsset ( from: link)
238+ await Result { try await self . fetchAsset ( from: link) }
268239 }
269240 }
270241
271- for try await (wasCached, size) in group {
272- if wasCached {
273- cachedCount += 1
274- } else {
275- fetchedCount += 1
242+ for await result in group {
243+ switch result {
244+ case . success( let ( wasCached, url) ) :
245+ if wasCached {
246+ cachedCount += 1
247+ } else {
248+ fetchedCount += 1
249+ }
250+ assetURLs. append ( url)
251+ case . failure( let error) :
252+ log ( . error, " Failed to fetch asset: \( error) " )
276253 }
277- totalSize += size
278254 }
279255 }
280256
281- let totalSizeMB = Double ( totalSize ) / ( 1024 * 1024 )
282- log ( . info, " Assets loaded: \( fetchedCount) fetched, \( cachedCount) cached, total size: \( String ( format: " %.2f " , totalSizeMB ) ) MB " )
257+ let totalTime = CFAbsoluteTimeGetCurrent ( ) - startTime
258+ log ( . info, " Assets loaded: \( fetchedCount) fetched, \( cachedCount) cached, total size: \( assetURLs . reduce ( 0 ) { $0 + $1 . fileSize } . formatted ) in \( String ( format: " %.2f " , totalTime ) ) s " )
283259 }
284260
285- /// Fetches a single asset and stores it on disk
286- /// - Returns: A tuple indicating (wasCached, fileSize)
287- private func fetchAsset( from urlString: String ) async throws -> ( Bool , Int64 ) {
261+ /// Checks if an asset URL is supported
262+ private func isSupportedAsset( _ urlString: String ) -> Bool {
288263 guard let url = URL ( string: urlString) else {
289264 log ( . warn, " Malformed asset link: \( urlString) " )
290- return ( false , 0 )
265+ return false
291266 }
292267
293268 guard url. scheme == " http " || url. scheme == " https " else {
294269 log ( . warn, " Unexpected asset link: \( urlString) " )
295- return ( false , 0 )
270+ return false
296271 }
297272
298273 let supportedResourceSuffixes = [ " .js " , " .css " , " .js.map " ]
299274 guard supportedResourceSuffixes. contains ( where: { url. lastPathComponent. hasSuffix ( $0) } ) else {
300275 log ( . warn, " Unsupported asset URL: \( url) " )
301- return ( false , 0 )
276+ return false
277+ }
278+
279+ return true
280+ }
281+
282+ /// Fetches a single asset and stores it on disk
283+ /// - Returns: A tuple indicating (wasCached, fileURL)
284+ private func fetchAsset( from urlString: String ) async throws -> ( Bool , URL ) {
285+ guard let url = URL ( string: urlString) else {
286+ throw URLError ( . badURL)
302287 }
303288
304289 let localURL = assetsDirectoryURL. appendingPathComponent ( cachedFilename ( for: urlString) )
305290
306- // Check if already cached
307291 if FileManager . default. fileExists ( atPath: localURL. path) {
308- let size = try ? FileManager . default. attributesOfItem ( atPath: localURL. path) [ . size] as? Int64 ?? 0
309- return ( true , size ?? 0 )
292+ return ( true , localURL)
310293 }
311294
295+ let startTime = CFAbsoluteTimeGetCurrent ( )
312296 let ( downloadedURL, response) = try await urlSession. download ( from: url)
313- if let status = ( response as? HTTPURLResponse ) ? . statusCode, ( 200 ..< 300 ) . contains ( status) {
314- let size = try ? FileManager . default. attributesOfItem ( atPath: downloadedURL. path) [ . size] as? Int64 ?? 0
315- do {
316- try FileManager . default. moveItem ( at: downloadedURL, to: localURL)
317- } catch {
318- log ( . error, " Failed to move downloaded assets \( downloadedURL) \( localURL) " )
319- }
320- log ( . debug, " Downloaded asset: \( url. lastPathComponent) ( \( size ?? 0 ) bytes) " )
321- return ( false , size ?? 0 )
322- } else {
323- log ( . error, " Received unexpected HTTP response for URL: \( url) " )
324- return ( false , 0 )
297+ let downloadTime = CFAbsoluteTimeGetCurrent ( ) - startTime
298+
299+ guard let status = ( response as? HTTPURLResponse ) ? . statusCode, ( 200 ..< 300 ) . contains ( status) else {
300+ throw URLError ( . badServerResponse)
325301 }
302+
303+ try FileManager . default. moveItem ( at: downloadedURL, to: localURL)
304+
305+ log ( . debug, " Downloaded asset: \( url. lastPathComponent) ( \( localURL. fileSize. formatted) ) in \( String ( format: " %.2f " , downloadTime) ) s " )
306+ return ( false , localURL)
326307 }
327308
328309 /// Loads a cached asset from disk
@@ -347,11 +328,9 @@ actor EditorService {
347328
348329 /// Deletes all cached editor data for all sites
349330 static func deleteAllData( ) throws {
350- let rootURL = URL . documentsDirectory. appendingPathComponent ( " GutenbergKit " , isDirectory: true )
351- guard FileManager . default. fileExists ( atPath: rootURL. path) else {
352- return
331+ if FileManager . default. fileExists ( atPath: EditorService . rootURL. path ( ) ) {
332+ try FileManager . default. removeItem ( at: EditorService . rootURL)
353333 }
354- try FileManager . default. removeItem ( at: rootURL)
355334 }
356335
357336 /// Generates a cached filename from an asset URL using SHA256 hash
@@ -365,12 +344,6 @@ actor EditorService {
365344 return hash
366345 }
367346
368- private func createStoreDirectoryIfNeeded( ) {
369- if !FileManager. default. fileExists ( atPath: storeURL. path) {
370- try ? FileManager . default. createDirectory ( at: storeURL, withIntermediateDirectories: true )
371- }
372- }
373-
374347 private func fetchData( for requestURL: URL , authHeader: String ) async throws -> Data {
375348 var request = URLRequest ( url: requestURL)
376349 request. setValue ( authHeader, forHTTPHeaderField: " Authorization " )
@@ -393,3 +366,23 @@ private extension Result {
393366 }
394367 }
395368}
369+
370+ private extension URL {
371+ var fileSize : Int64 {
372+ ( try ? FileManager . default. attributesOfItem ( atPath: path ( ) ) [ . size] as? Int64 ) ?? 0
373+ }
374+ }
375+
376+ private extension Int64 {
377+ var formatted : String {
378+ ByteCountFormatter . string ( fromByteCount: self , countStyle: . file)
379+ }
380+ }
381+
382+ private extension FileManager {
383+ func createDirectoryIfNeeded( at url: URL ) {
384+ if !fileExists( atPath: url. path) {
385+ try ? createDirectory ( at: url, withIntermediateDirectories: true )
386+ }
387+ }
388+ }
0 commit comments