@@ -85,69 +85,104 @@ object Bootstrap {
85
85
}
86
86
87
87
/**
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 .
89
89
*
90
90
* @param args the args, the launcher has been called with
91
91
* @return false, if there is a serious problem
92
92
*/
93
93
def checkLibraries (args : Array [String ]): Boolean = {
94
94
95
- // TODO: Someday in the future, we need incremental library checking to manage updates without full download
96
-
97
95
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()) {
111
110
try {
112
- libFolder.mkdir ()
111
+ libFile.delete ()
113
112
} 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
115
115
}
116
116
}
117
+ }
117
118
118
- // Download all libraries
119
- // TODO: Check validity if everything is downloaded
120
- println(" Downloading libraries..." )
121
- downloadLibraries()
119
+ val dependencies = getDependencies
122
120
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)
127
125
}
128
126
129
127
/**
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 .
131
129
*
132
130
* @return false, if there is a serious problem
133
131
*/
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
+ }
148
151
}
149
152
}
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
+ }
151
186
}
152
187
153
188
/**
@@ -169,7 +204,7 @@ object Bootstrap {
169
204
else {
170
205
// Save file in the lib folder (keeping the name and type)
171
206
try {
172
- url #> new File ( s " $currentFolderPath /lib/ ${ libraryURL.substring(libraryURL.lastIndexOf( " / " ))} " ) !!
207
+ url #> libraryFile( libraryURL) !!
173
208
174
209
true
175
210
} catch {
@@ -187,6 +222,53 @@ object Bootstrap {
187
222
}
188
223
}
189
224
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
+
190
272
/**
191
273
* Checks, if the installation is valid
192
274
*/
0 commit comments