@@ -85,69 +85,104 @@ object Bootstrap {
8585 }
8686
8787 /**
88- * Checks if the library folder exists or the reload-flag is set. Triggers the download-process.
88+ * Checks if the library folder exists or the reload-flag is set. Triggers the download-process if libraries are missing .
8989 *
9090 * @param args the args, the launcher has been called with
9191 * @return false, if there is a serious problem
9292 */
9393 def checkLibraries (args : Array [String ]): Boolean = {
9494
95- // TODO: Someday in the future, we need incremental library checking to manage updates without full download
96-
9795 val libFolder = new File (s " $currentFolderPath/lib " )
98- // Args contains --reload or lib folder is non existent?
99- if ((args.length > 0 && args.head == " --reload" ) || ! libFolder.exists()) {
100-
101- // Create or clean directory
102- if (libFolder.exists()) {
103- for (libFile <- libFolder.listFiles()) {
104- try {
105- libFile.delete()
106- } catch {
107- case e : Exception => println(s " Unable to delete file ' ${libFile.getName}'. Message: ${e.getMessage}" )
108- }
109- }
110- } else {
96+
97+ // Create folder for libs if missing
98+ if (! libFolder.exists()) {
99+ try {
100+ libFolder.mkdir()
101+ } catch {
102+ case e : Exception => println(s " Unable to create library directory. Message: ${e.getMessage}" )
103+ return false
104+ }
105+ }
106+
107+ // --reload flags instructs to delete all downloaded libraries and to re-download them
108+ if (args.contains(" --reload" )) {
109+ for (libFile <- libFolder.listFiles()) {
111110 try {
112- libFolder.mkdir ()
111+ libFile.delete ()
113112 } catch {
114- case e : Exception => println(s " Unable to create library directory. Message: ${e.getMessage}" )
113+ case e : Exception => println(s " Unable to delete file ' ${libFile.getName}'. Message: ${e.getMessage}" )
114+ return false
115115 }
116116 }
117+ }
117118
118- // Download all libraries
119- // TODO: Check validity if everything is downloaded
120- println(" Downloading libraries..." )
121- downloadLibraries()
119+ val dependencies = getDependencies
122120
123- } else {
124- println( " Found libraries folder. Assuming all dependencies are available properly. " )
125- true
126- }
121+ // Download all libraries
122+ // TODO: Check validity if everything is downloaded
123+ // try downloading libs and only if it succeeded (returned true) then try to delete older libs
124+ downloadMissingLibraries(dependencies) && deleteUndesiredLibraries(dependencies)
127125 }
128126
129127 /**
130- * Reads the dependency xml file and tries to download every library.
128+ * Reads the dependency xml file and tries to download every library that is missing .
131129 *
132130 * @return false, if there is a serious problem
133131 */
134- def downloadLibraries (): Boolean = {
135-
136- // Get dependency xml and read dependencies with their download URL
137- val dependencyStream = getClass.getResourceAsStream(" /dependencies.xml" )
138- val dependencyXML = xml.XML .load(dependencyStream)
139- val dependencies = for (dependency <- dependencyXML \\ " dependency" )
140- yield ((dependency \ " name" ).text.trim, (dependency \ " url" ).text.trim)
141-
142- for (i <- dependencies.indices) {
143- val dependency = dependencies(i)
144- println(s " [ ${i + 1 }/ ${dependencies.length}] ${dependency._1} ( ${dependency._2}) " )
145- if (! downloadLibrary(dependency._1, dependency._2)) {
146- // Second try, just in case
147- downloadLibrary(dependency._1, dependency._2)
132+ private def downloadMissingLibraries (dependencies : List [(String , String )]): Boolean = {
133+ // using par here to make multiple http requests in parallel, otherwise its awfully slow on internet connections with high RTTs
134+ val missing = dependencies.par.filterNot(dep => isLibraryDownloaded(dep._2)).toList
135+
136+ if (missing.isEmpty) {
137+ println(" All required libraries are already downloaded." )
138+ } else {
139+ println(s " Downloading ${missing.length} missing libraries... " )
140+
141+ for (i <- missing.indices) {
142+ val (name, url) = missing(i)
143+
144+ println(s " [ ${i + 1 }/ ${missing.length}] $name ( $url) " )
145+ if (! downloadLibrary(name, url)) {
146+ // Second try, just in case
147+ if (! downloadLibrary(name, url)) {
148+ return false // error has been displayed, stop bootstrapper from starting with missing lib
149+ }
150+ }
148151 }
149152 }
150- true
153+ true // everything went fine
154+ }
155+
156+ /**
157+ * Deletes all undesired libraries. Currently these are all libs that aren't on the list of dependencies.
158+ * The main responsibility is to delete old libs that got updated or libs that aren't required anymore by Chat Overflow.
159+ *
160+ * @param dependencies the libs that should be kept
161+ * @return false, if a file couldn't be deleted
162+ */
163+ private def deleteUndesiredLibraries (dependencies : List [(String , String )]): Boolean = {
164+ val libDir = new File (s " $currentFolderPath/lib " )
165+ if (libDir.exists() && libDir.isDirectory) {
166+ // Desired filenames
167+ val libraryFilenames = dependencies.map(d => libraryFile(d._2).getName)
168+
169+ val undesiredFiles = libDir.listFiles().filterNot(file => libraryFilenames.contains(file.getName)) // filter out libs on the dependency list
170+
171+ // Count errors while trying to remove undesired files
172+ val errorCount = undesiredFiles.count(file => {
173+ println(s " Deleting old or unnecessary library at $file" )
174+ if (file.delete()) {
175+ false // no error
176+ } else {
177+ println(s " Error: Couldn't delete file $file. " )
178+ true // error
179+ }
180+ })
181+ errorCount == 0 // return false if at least one error occurred
182+ } else {
183+ // Shouldn't be possible, because this is called from checkLibraries, which creates this directory.
184+ true
185+ }
151186 }
152187
153188 /**
@@ -169,7 +204,7 @@ object Bootstrap {
169204 else {
170205 // Save file in the lib folder (keeping the name and type)
171206 try {
172- url #> new File ( s " $currentFolderPath /lib/ ${ libraryURL.substring(libraryURL.lastIndexOf( " / " ))} " ) !!
207+ url #> libraryFile( libraryURL) !!
173208
174209 true
175210 } catch {
@@ -187,6 +222,53 @@ object Bootstrap {
187222 }
188223 }
189224
225+ /**
226+ * Gets all required dependencies from the dependencies.xml in the jar file
227+ *
228+ * @return a list of tuples that contain the name (e.g. log4j) without org or version and the url.
229+ */
230+ private def getDependencies : List [(String , String )] = {
231+ val stream = getClass.getResourceAsStream(" /dependencies.xml" )
232+ val depXml = xml.XML .load(stream)
233+ val dependencies = depXml \\ " dependency"
234+ val dependencyTuples = dependencies.map(dep => {
235+ val name = (dep \ " name" ).text.trim
236+ val url = (dep \ " url" ).text.trim
237+ (name, url)
238+ })
239+
240+ dependencyTuples.toList
241+ }
242+
243+ /**
244+ * Checks whether this library is fully downloaded
245+ *
246+ * @param libraryURL the url of the library
247+ * @return true if it is completely downloaded, false if only partially downloaded or not downloaded at all
248+ */
249+ private def isLibraryDownloaded (libraryURL : String ): Boolean = {
250+ val f = libraryFile(libraryURL)
251+
252+ if (! f.exists()) {
253+ false
254+ } else {
255+ try {
256+ // We assume here that the libs don't change at the repo.
257+ // While this is true for Maven Central, which is immutable once a file has been uploaded, its not for JCenter.
258+ // Updating a released artifact generally isn't valued among developers
259+ // and the odds of the updated artifact having the same size is very unlikely.
260+ val url = new URL (libraryURL)
261+ url.openConnection().getContentLengthLong == f.length()
262+ } catch {
263+ case _ : Exception => false
264+ }
265+ }
266+ }
267+
268+ private def libraryFile (libraryURL : String ): File = {
269+ new File (s " $currentFolderPath/lib/ ${libraryURL.substring(libraryURL.lastIndexOf(" /" ))}" )
270+ }
271+
190272 /**
191273 * Checks, if the installation is valid
192274 */
0 commit comments