diff --git a/.mtn-ignore b/.mtn-ignore index d1ca4ab93..599088618 100644 --- a/.mtn-ignore +++ b/.mtn-ignore @@ -57,6 +57,8 @@ launchers/macosx/project/target launchers/browserbundle/project/target launchers/target launchers/project/target +launchers/common/target +launchers/output # Reporting sloccount.sc diff --git a/history.txt b/history.txt index 0a3037c8f..7f679bd28 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,9 @@ +2018-05-01 meeh + * launchers: + - Added deployment profile for Mac OS X launcher. + - Sourced out common code to a common library SBT project. + - Decided ~/Library/I2P should be base path on Mac OS X when using regular bundle (Non-BB). + 2018-04-29 zzz * Console: Fix NPE on /configsidebar (ticket #2220) diff --git a/launchers/browserbundle/src/main/scala/net/i2p/RouterLauncherApp.scala b/launchers/browserbundle/src/main/scala/net/i2p/RouterLauncherApp.scala index 4f5e7a1b6..ff1f6a53f 100644 --- a/launchers/browserbundle/src/main/scala/net/i2p/RouterLauncherApp.scala +++ b/launchers/browserbundle/src/main/scala/net/i2p/RouterLauncherApp.scala @@ -2,6 +2,7 @@ package net.i2p import java.io.{File, InputStream} +import net.i2p.launchers.DeployProfile import net.i2p.router.Router import org.json4s._ import org.json4s.native.JsonMethods._ diff --git a/launchers/build.sbt b/launchers/build.sbt index dd1ddedd6..d0853139a 100644 --- a/launchers/build.sbt +++ b/launchers/build.sbt @@ -20,6 +20,11 @@ lazy val commonSettings = Seq( packageDescription := "Blabla" ) +lazy val common = (project in file("common")) + .settings( + commonSettings, + name := "LauncherCommon" + ) lazy val browserbundle = (project in file("browserbundle")) .settings( @@ -27,7 +32,7 @@ lazy val browserbundle = (project in file("browserbundle")) name := "RouterLaunchApp", assemblyJarName in assembly := s"${name.value}-${version.value}.jar", mainClass in assembly := Some("net.i2p.RouterLauncherApp") - ) + ).dependsOn(common) lazy val macosx = (project in file("macosx")) .settings( @@ -35,11 +40,11 @@ lazy val macosx = (project in file("macosx")) name := "MacI2PLauncher", assemblyJarName in assembly := s"${name.value}-${version.value}.jar", mainClass in assembly := Some("net.i2p.MacOSXRouterLauncherApp") - ) + ).dependsOn(common) lazy val root = (project in file(".")) - .aggregate(browserbundle, macosx) + .aggregate(common, browserbundle, macosx) scalacOptions in Compile := Seq("-deprecated") diff --git a/launchers/common/build.sbt b/launchers/common/build.sbt new file mode 100644 index 000000000..e69de29bb diff --git a/launchers/common/src/main/scala/net/i2p/launchers/CaseClasses.scala b/launchers/common/src/main/scala/net/i2p/launchers/CaseClasses.scala new file mode 100644 index 000000000..31a522fa1 --- /dev/null +++ b/launchers/common/src/main/scala/net/i2p/launchers/CaseClasses.scala @@ -0,0 +1,45 @@ +package net.i2p.launchers + +import java.io.{File, InputStream} + +/** + * This represent a file or a directory. A filesystem resource. + * It's used to handle a correct deployment of base files, even + * installer resources got kind of a flat list. (i.e geoip files + * is placed under a dir geoip) + * + * With this class we can now have a list containing both directories + * and files in a type safe list. + * + * @param path + * @param content + * @param files + * @since 0.9.35 + */ +class FDObject(path: String, content: Option[InputStream], files: Option[List[File]] = None, subDirectories: Boolean = false) { + def isFile = files.isEmpty + + def getPath = path + + def filesIsDirectories = subDirectories + + def getContent: Either[InputStream, List[File]] = { + if (files.isEmpty) return Left(content.get) + Right(files.get.map { f => new File(DeployProfile.pathJoin(path, f.getName)) }) + } +} + + +class FDObjFile(file: String) extends FDObject(new File(file).getPath, Some(getClass.getResourceAsStream("/".concat(file))) ) +class FDObjDir(name: String, files: List[String],subDirectories:Boolean=false) extends FDObject(name, None, Some(files.map { s => new File(s) }),subDirectories) + +/** + * + * This is a case class, it's a Scala thing. Think of it as a type. + * If you're familiar with C, it's like a "typedef" even iirc Scala + * has a own function for that as well. + * + * + */ + + diff --git a/launchers/browserbundle/src/main/scala/net/i2p/DeployProfile.scala b/launchers/common/src/main/scala/net/i2p/launchers/DeployProfile.scala similarity index 77% rename from launchers/browserbundle/src/main/scala/net/i2p/DeployProfile.scala rename to launchers/common/src/main/scala/net/i2p/launchers/DeployProfile.scala index 7a73d9002..e64f5686b 100644 --- a/launchers/browserbundle/src/main/scala/net/i2p/DeployProfile.scala +++ b/launchers/common/src/main/scala/net/i2p/launchers/DeployProfile.scala @@ -1,20 +1,10 @@ -package net.i2p +package net.i2p.launchers import java.io.{File, InputStream} -/** - * - * The purpose of this class is to copy files from the i2p "default config" directory - * and to a "current config" directory relative to the browser bundle. - * - * @author Meeh - * @version 0.0.1 - * @since 0.9.35 - */ -class DeployProfile(confDir: String, baseDir: String) { - import java.nio.file.{Paths, Files} - import java.nio.charset.StandardCharsets +object DeployProfile { + /** * This joins two paths in a cross platform way. Meaning it takes care of either we use * \\ or / as directory separator. It returns the resulting path in a string. @@ -25,6 +15,21 @@ class DeployProfile(confDir: String, baseDir: String) { * @return String */ def pathJoin(parent:String,child:String): String = new File(new File(parent), child).getPath +} + +/** + * + * The purpose of this class is to copy files from the i2p "default config" directory + * and to a "current config" directory relative to the browser bundle - but this class is + * also used by the OSX launcher since it shares common properties like that the bundle has + * to be read only. + * + * @author Meeh + * @version 0.0.1 + * @since 0.9.35 + */ +class DeployProfile(val confDir: String, val baseDir: String) { + import java.nio.file.{Files, Paths} /** * This function copies resources from the fatjar to the config directory of i2p. @@ -35,7 +40,21 @@ class DeployProfile(confDir: String, baseDir: String) { */ def copyFileResToDisk(fStr: String) = Files.copy( getClass.getResource("/".concat(fStr)).getContent.asInstanceOf[InputStream], - Paths.get(pathJoin(confDir, fStr)).normalize() + Paths.get(DeployProfile.pathJoin(confDir, fStr)).normalize() + ) + + + /** + * This function copies resources from the fatjar to the config directory of i2p. + * + * @since 0.9.35 + * @param path + * @param is + * @return Unit + */ + def copyBaseFileResToDisk(path: String, is: InputStream) = Files.copy( + is, + Paths.get(DeployProfile.pathJoin(baseDir, path)).normalize() ) /** diff --git a/launchers/common/src/main/scala/net/i2p/launchers/OSXDefaults.scala b/launchers/common/src/main/scala/net/i2p/launchers/OSXDefaults.scala new file mode 100644 index 000000000..42457cab1 --- /dev/null +++ b/launchers/common/src/main/scala/net/i2p/launchers/OSXDefaults.scala @@ -0,0 +1,18 @@ +package net.i2p.launchers + +import java.io.File + +/** + * Defaults object for Mac OS X + * + * + * @author Meeh + * @since 0.9.35 + */ +object OSXDefaults { + + def getOSXConfigDirectory = new File(DeployProfile.pathJoin(System.getProperty("user.home"), "Library/Application Support/i2p")) + + def getOSXBaseDirectory = new File(DeployProfile.pathJoin(System.getProperty("user.home"),"Library/I2P")) + +} diff --git a/launchers/common/src/main/scala/net/i2p/launchers/OSXDeployment.scala b/launchers/common/src/main/scala/net/i2p/launchers/OSXDeployment.scala new file mode 100644 index 000000000..50c1113e9 --- /dev/null +++ b/launchers/common/src/main/scala/net/i2p/launchers/OSXDeployment.scala @@ -0,0 +1,190 @@ +package net.i2p.launchers + +import java.io.{File, IOException} +import java.util.zip.ZipFile +import collection.JavaConverters._ + +/** + * + * OSXDeployment + * + * This class can be a bit new for java developers. In Scala, when inherit other classes, + * you would need to define their arguments if the super class only has constructors taking arguments. + * + * This child class don't take arguments, but rather get them from the signleton OSXDefaults. + * + * This class should be able to copy recursive resources into correct position for a normal deployment. + * + * + * This class might look like black magic for some. But what it does is to deploy a structure like for example: + * tree ~/Library/I2P + * /Users/mikalv/Library/I2P + * ├── blocklist.txt + * ├── certificates + * │   ├── family + * │   │   ├── gostcoin.crt + * │   │   ├── i2p-dev.crt + * │   │   ├── i2pd-dev.crt + * │   │   └── volatile.crt + * │   ├── i2ptunnel + * │   ├── news + * │   │   ├── ampernand_at_gmail.com.crt + * │   │   ├── echelon_at_mail.i2p.crt + * │   │   ├── str4d_at_mail.i2p.crt + * │   │   └── zzz_at_mail.i2p.crt + * │   ├── plugin + * │   │   ├── cacapo_at_mail.i2p.crt + * │   │   ├── str4d_at_mail.i2p.crt + * │   │   └── zzz-plugin_at_mail.i2p.crt + * │   ├── reseed + * │   │   ├── atomike_at_mail.i2p.crt + * │   │   ├── backup_at_mail.i2p.crt + * │   │   ├── bugme_at_mail.i2p.crt + * │   │   ├── creativecowpat_at_mail.i2p.crt + * │   │   ├── echelon_at_mail.i2p.crt + * │   │   ├── hottuna_at_mail.i2p.crt + * │   │   ├── igor_at_novg.net.crt + * │   │   ├── lazygravy_at_mail.i2p.crt + * │   │   ├── meeh_at_mail.i2p.crt + * │   │   └── zmx_at_mail.i2p.crt + * │   ├── revocations + * │   ├── router + * │   │   ├── echelon_at_mail.i2p.crt + * │   │   ├── str4d_at_mail.i2p.crt + * │   │   └── zzz_at_mail.i2p.crt + * │   └── ssl + * │   ├── echelon.reseed2017.crt + * │   ├── i2p.mooo.com.crt + * │   ├── i2pseed.creativecowpat.net.crt + * │   ├── isrgrootx1.crt + * │   └── reseed.onion.im.crt + * ├── clients.config + * ├── geoip + * │   ├── continents.txt + * │   ├── countries.txt + * │   ├── geoip.txt + * │   └── geoipv6.dat.gz + * ├── hosts.txt + * └── i2ptunnel.config + * + * @author Meeh + * @since 0.9.35 + */ +class OSXDeployment extends + DeployProfile( + OSXDefaults.getOSXConfigDirectory.getAbsolutePath, + OSXDefaults.getOSXBaseDirectory.getAbsolutePath + ) { + + /** + * This function will find the executing jar. "myself" + * @return + */ + def executingJarFile = getClass().getProtectionDomain().getCodeSource().getLocation() + + + /** + * This list is a micro DSL for how files should + * be deployed to the filesystem in the base + * directory. + */ + val staticFilesFromResources = List( + new FDObjFile("blocklist.txt"), + new FDObjFile("clients.config"), + new FDObjFile("hosts.txt"), + new FDObjDir("geoip", files = List( + "continents.txt", + "countries.txt", + "geoip.txt", + "geoipv6.dat.gz")), + new FDObjFile("i2ptunnel.config"), + new FDObjDir("certificates", List( + "family", + "i2ptunnel", + "news", + "plugin", + "reseed", + "revocations", + "router", + "ssl" + ),subDirectories=true) + ) + + /** + * This function copies an directory of files from the jar + * to the base directory defined in the launcher. + * @param dir + * @return + */ + def copyDirFromRes(dir: File) = { + // A small hack + val zipFile = new ZipFile(executingJarFile.getFile) + zipFile.entries().asScala.toList.filter(_.toString.startsWith(dir.getPath)).filter(!_.isDirectory).map { entry => + copyBaseFileResToDisk(entry.getName, getClass.getResourceAsStream("/".concat(entry.getName))) + } + } + + /** + * This function will depending on directory or not copy either the file + * or create the directory and copy directory content if any. + * + * @param file + * @param isDir + * @return + */ + def createFileOrDirectory(file: File, isDir: Boolean = false) = { + if (file != null) { + println(s"createFileOrDirectory(${file},${isDir})") + try { + if (!new File(DeployProfile.pathJoin(baseDir,file.getPath)).exists()) { + if (isDir) { + // Handle dir + new File(DeployProfile.pathJoin(baseDir,file.getPath)).mkdirs() + copyDirFromRes(file) + } else { + // Handle file + copyBaseFileResToDisk(file.getPath, getClass.getResourceAsStream("/".concat(file.getName))) + } + } + } catch { + case ex:IOException => println(s"Error! Exception ${ex}") + } + } + } + + if (!new File(baseDir).exists()) { + new File(baseDir).mkdirs() + } + + /** + * a map function work as a loop with some built in security + * for "null" objects. + * What happens here is "for each staticFilesFromResources" do => + * + * Then, based upon if it's a file or a directory, different actions take place. + * + * the match case is controlling the flow based upon which type of object it is. + */ + staticFilesFromResources.map { fd => fd.getContent match { + case Left(is) => { + // Write file + if (!new File(DeployProfile.pathJoin(baseDir, fd.getPath)).exists()) { + println(s"copyBaseFileResToDisk(${fd.getPath})") + try { + copyBaseFileResToDisk(fd.getPath, is) + } catch { + case ex:IOException => println(s"Error! Exception ${ex}") + } + } + } + case Right(dir) => { + // Ensure directory + println(s"Directory(${fd.getPath})") + if (!new File(DeployProfile.pathJoin(baseDir,fd.getPath)).exists()) { + new File(DeployProfile.pathJoin(baseDir,fd.getPath)).mkdirs() + } + dir.map { f => createFileOrDirectory(f,fd.filesIsDirectories) } + } + } } + +} diff --git a/launchers/macosx/build.sbt b/launchers/macosx/build.sbt index eedcd9c25..a4925009f 100644 --- a/launchers/macosx/build.sbt +++ b/launchers/macosx/build.sbt @@ -9,6 +9,16 @@ lazy val i2pVersion = "0.9.34" lazy val buildAppBundleTask = taskKey[Unit](s"Build an Mac OS X bundle for I2P ${i2pVersion}.") lazy val bundleBuildPath = file("./output") +lazy val staticFiles = List( + "blocklist.txt", + "clients.config", + "continents.txt", + "countries.txt", + "hosts.txt", + "geoip.txt", + "router.config", + "webapps.config" +) // Pointing the resources directory to the "installer" directory resourceDirectory in Compile := baseDirectory.value / ".." / ".." / "installer" / "resources" @@ -33,6 +43,19 @@ buildAppBundleTask := { ) paths.map { case (s,p) => p.mkdirs() } val dirsToCopy = List("certificates","locale","man") + + /** + * + * First of, if "map" is unknown for you - shame on you :p + * + * It's a loop basically where it loops through a list/array + * with the current indexed item as subject. + * + * The code bellow takes the different lists and + * copy all the directories or files from the i2p.i2p build dir, + * and into the bundle so the launcher will know where to find i2p. + * + */ dirsToCopy.map { d => IO.copyDirectory( new File(resDir, d), new File(paths.get("i2pbaseBunldePath").get, d) ) } warsForCopy.map { w => IO.copyFile( new File(i2pBuildDir, w), new File(paths.get("webappsBunldePath").get, w) ) } warsForCopy.map { j => IO.copyFile( new File(i2pBuildDir, j), new File(paths.get("i2pJarsBunldePath").get, j) ) } @@ -53,6 +76,6 @@ libraryDependencies ++= Seq( ) -assemblyOption in assembly := (assemblyOption in assembly).value.copy(prependShellScript = Some(defaultShellScript)) +//assemblyOption in assembly := (assemblyOption in assembly).value.copy(prependShellScript = Some(defaultShellScript)) assemblyJarName in assembly := s"${name.value}-${version.value}" diff --git a/launchers/macosx/project/plugins.sbt b/launchers/macosx/project/plugins.sbt new file mode 100644 index 000000000..36cfe7660 --- /dev/null +++ b/launchers/macosx/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6") +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.4") diff --git a/launchers/macosx/src/main/scala/net/i2p/MacOSXRouterLauncherApp.scala b/launchers/macosx/src/main/scala/net/i2p/MacOSXRouterLauncherApp.scala index a454b0d0a..b1ec05b4d 100644 --- a/launchers/macosx/src/main/scala/net/i2p/MacOSXRouterLauncherApp.scala +++ b/launchers/macosx/src/main/scala/net/i2p/MacOSXRouterLauncherApp.scala @@ -1,6 +1,7 @@ package net.i2p import net.i2p.router.Router +import net.i2p.launchers.OSXDeployment import java.io.File /** @@ -35,5 +36,7 @@ object MacOSXRouterLauncherApp extends App { val i2pBaseBundleDir = new File(new File("."), "../Resources/i2pbase") + new OSXDeployment() + Router.main(args) }