@@ -31,7 +31,7 @@ struct SampleDependency: Decodable {
3131}
3232
3333/// A Portal Item and its data URL.
34- struct PortalItem {
34+ struct PortalItem : Hashable {
3535 static let arcGISOnlinePortalURL = URL ( string: " https://www.arcgis.com " ) !
3636
3737 /// The identifier of the item.
@@ -125,61 +125,50 @@ func uncompressArchive(at sourceURL: URL, to destinationURL: URL) throws {
125125 process. waitUntilExit ( )
126126}
127127
128- /// Downloads file from portal and write the file(s) to appropriate path(s) .
128+ /// Downloads a file from a given portal and writes it to a given path.
129129/// - Parameters:
130130/// - sourceURL: The portal URL to the resource.
131- /// - downloadDirectory: The directory that stores downloaded data.
132- /// - completion: A closure to handle the results.
133- func downloadFile( at sourceURL: URL , to downloadDirectory: URL , completion: @escaping ( Result < URL , Error > ) -> Void ) {
134- let downloadTaskCompleted = { ( temporaryURL: URL ? , response: URLResponse ? , error: Error ? ) in
135- if let temporaryURL = temporaryURL,
136- let response = response,
137- let suggestedFilename = response. suggestedFilename {
138- do {
139- let downloadName : String
140- let isArchive = ( suggestedFilename as NSString ) . pathExtension == " zip "
141- // If the downloaded file is an archive and contains
142- // - 1 file, use the name of that file.
143- // - multiple files, use the suggested filename (*.zip).
144- // If it is not an archive, use the server suggested filename.
145- if isArchive {
146- let count = try count ( ofFilesInArchiveAt: temporaryURL)
147- if count == 1 {
148- downloadName = try name ( ofFileInArchiveAt: temporaryURL)
149- } else {
150- downloadName = suggestedFilename
151- }
152- } else {
153- downloadName = suggestedFilename
154- }
155-
156- let downloadURL = downloadDirectory. appendingPathComponent ( downloadName, isDirectory: false )
157-
158- if FileManager . default. fileExists ( atPath: downloadURL. path) {
159- try FileManager . default. removeItem ( at: downloadURL)
160- }
161-
162- if isArchive {
163- let extractURL = downloadURL. pathExtension == " zip "
164- // Uncompresses to directory named after archive.
165- ? downloadURL. deletingPathExtension ( )
166- // Uncompresses to appropriate subdirectory.
167- : downloadURL. deletingLastPathComponent ( )
168- try uncompressArchive ( at: temporaryURL, to: extractURL)
169- } else {
170- try FileManager . default. moveItem ( at: temporaryURL, to: downloadURL)
171- }
172-
173- completion ( . success( downloadURL) )
174- } catch {
175- completion ( . failure( error) )
176- }
177- } else if let error = error {
178- completion ( . failure( error) )
131+ /// - downloadDirectory: The directory to store the downloaded data in.
132+ /// - Throws: Exceptions when downloading and moving the file.
133+ /// - Returns: The name of the downloaded file.
134+ func downloadFile( from sourceURL: URL , to downloadDirectory: URL ) async throws -> String {
135+ let portalURLRequest = URLRequest ( url: sourceURL)
136+ let ( temporaryURL, response) = try await URLSession . shared. download ( for: portalURLRequest)
137+
138+ guard let suggestedFilename = response. suggestedFilename else { return " " }
139+ let isArchive = NSString ( string: suggestedFilename) . pathExtension == " zip "
140+
141+ let downloadName : String = try {
142+ // If the downloaded file is an archive and contains
143+ // - 1 file, use the name of that file.
144+ // - multiple files, use the suggested filename (*.zip).
145+ // If it is not an archive, use the server suggested filename.
146+ if isArchive,
147+ try count ( ofFilesInArchiveAt: temporaryURL) == 1 {
148+ return try name ( ofFileInArchiveAt: temporaryURL)
149+ } else {
150+ return suggestedFilename
179151 }
152+ } ( )
153+ let downloadURL = downloadDirectory. appendingPathComponent ( downloadName, isDirectory: false )
154+
155+ if FileManager . default. fileExists ( atPath: downloadURL. path) {
156+ try FileManager . default. removeItem ( at: downloadURL)
180157 }
181- let downloadTask = URLSession . shared. downloadTask ( with: sourceURL, completionHandler: downloadTaskCompleted)
182- downloadTask. resume ( )
158+
159+ if isArchive {
160+ let extractURL = downloadURL. pathExtension == " zip "
161+ // Uncompresses to directory named after archive.
162+ ? downloadURL. deletingPathExtension ( )
163+ // Uncompresses to appropriate subdirectory.
164+ : downloadURL. deletingLastPathComponent ( )
165+
166+ try uncompressArchive ( at: temporaryURL, to: extractURL)
167+ } else {
168+ try FileManager . default. moveItem ( at: temporaryURL, to: downloadURL)
169+ }
170+
171+ return downloadName
183172}
184173
185174// MARK: Script Entry
@@ -207,7 +196,7 @@ if !FileManager.default.fileExists(atPath: downloadDirectoryURL.path) {
207196}
208197
209198/// Portal Items created from iterating through all metadata's "offline\_data".
210- let portalItems : [ PortalItem ] = {
199+ let portalItems : Set < PortalItem > = {
211200 do {
212201 // Finds all subdirectories under the root Samples directory.
213202 let sampleSubDirectories = try FileManager . default
@@ -218,7 +207,7 @@ let portalItems: [PortalItem] = {
218207 // Omit the decoding errors from samples that don't have dependencies.
219208 let sampleDependencies = sampleJSONs
220209 . compactMap { try ? parseJSON ( at: $0) }
221- return sampleDependencies. flatMap ( \. offlineData)
210+ return Set ( sampleDependencies. flatMap ( \. offlineData) )
222211 } catch {
223212 print ( " error: Error decoding Samples dependencies: \( error. localizedDescription) " )
224213 exit ( 1 )
@@ -241,39 +230,56 @@ let previousDownloadedItems: DownloadedItems = {
241230} ( )
242231var downloadedItems = previousDownloadedItems
243232
244- // Asynchronously downloads portal items.
245- let dispatchGroup = DispatchGroup ( )
246-
247- portalItems. forEach { portalItem in
248- let destinationURL = downloadDirectoryURL. appendingPathComponent ( portalItem. identifier, isDirectory: true )
249- // Checks if a directory exists or not, to see if an item is already downloaded.
250- if FileManager . default. fileExists ( atPath: destinationURL. path) {
251- print ( " info: Item \( portalItem. identifier) has already been downloaded. " )
252- } else {
233+ await withTaskGroup ( of: Void . self) { group in
234+ for portalItem in portalItems {
235+ // Checks to see if an item is already downloaded.
236+ guard downloadedItems [ portalItem. identifier] == nil else {
237+ print ( " note: Item already downloaded: \( portalItem. identifier) " )
238+ continue
239+ }
240+
241+ let destinationURL = downloadDirectoryURL. appendingPathComponent ( portalItem. identifier, isDirectory: true )
242+
243+ // Deletes the directory if it already exists.
244+ // This happens when the item is not in the plist and needs to be redownloaded.
245+ if FileManager . default. fileExists ( atPath: destinationURL. path) {
246+ do {
247+ print ( " note: Deleting directory: \( portalItem. identifier) " )
248+ try FileManager . default. removeItem ( at: destinationURL)
249+ } catch {
250+ print ( " error: Error deleting downloaded directory: \( error. localizedDescription) " )
251+ exit ( 1 )
252+ }
253+ }
254+
253255 do {
254256 // Creates an enclosing directory with portal item ID as its name.
255257 try FileManager . default. createDirectory ( at: destinationURL, withIntermediateDirectories: false )
256258 } catch {
257- print ( " error: Error creating download directory: \( error. localizedDescription) . " )
259+ print ( " error: Error creating download directory: \( error. localizedDescription) " )
258260 exit ( 1 )
259261 }
260- print ( " info: Downloading item \( portalItem. identifier) " )
262+
263+ print ( " note: Downloading item: \( portalItem. identifier) " )
261264 fflush ( stdout)
262- dispatchGroup. enter ( )
263- downloadFile ( at: portalItem. dataURL, to: destinationURL) { result in
264- switch result {
265- case . success( let url) :
266- downloadedItems [ portalItem. identifier] = url. lastPathComponent
267- dispatchGroup. leave ( )
268- case . failure( let error) :
265+
266+ group. addTask {
267+ do {
268+ let downloadName = try await downloadFile ( from: portalItem. dataURL, to: destinationURL)
269+ print ( " note: Downloaded item: \( portalItem. identifier) " )
270+ fflush ( stdout)
271+
272+ await MainActor . run {
273+ downloadedItems [ portalItem. identifier] = downloadName
274+ }
275+ } catch {
269276 print ( " error: Error downloading item \( portalItem. identifier) : \( error. localizedDescription) " )
270277 URLSession . shared. invalidateAndCancel ( )
271278 exit ( 1 )
272279 }
273280 }
274281 }
275282}
276- dispatchGroup. wait ( )
277283
278284// Updates the downloaded items property list record if needed.
279285if downloadedItems != previousDownloadedItems {
0 commit comments