Skip to content
This repository was archived by the owner on Aug 18, 2020. It is now read-only.

Commit ef541f0

Browse files
authored
Merge pull request #13 from daniel0611/bootstrap-incremental-libs
Implement incremental library downloading/updating in Bootstrap Launcher
2 parents ac09a8c + aef6ac0 commit ef541f0

File tree

1 file changed

+125
-43
lines changed

1 file changed

+125
-43
lines changed

bootstrap/src/main/scala/Bootstrap.scala

Lines changed: 125 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)