Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions app/src/main/kotlin/com/ustadmobile/zim2xapi/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,12 @@ class DownloadTopic : CliktCommand(name = "convert") {

// extract the zim
ExtractZimUseCase(zimDumpProcess).invoke(createdZimFile, extractedZimFolder)

// make sure index.html exists
CreateIndexHtmlUseCase(zimDumpProcess).invoke(createdZimFile, extractedZimFolder)
// fix any exceptions found in the folder
FixExtractZimExceptionsUseCase(zimDumpProcess).invoke(createdZimFile, extractedZimFolder)
FixExtractZimExceptionsUseCase().invoke(extractedZimFolder)

ShrinkXapiUseCase().invoke(extractedZimFolder)

// create the xApi zip file
val xapiFile = CreateXapiFileUseCase(zimDumpProcess, AddxAPIStatementUseCase(), json).invoke(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.ustadmobile.zim2xapi

import com.ustadmobile.zim2xapi.utils.FileConstants
import java.io.File

class CreateIndexHtmlUseCase(private val zimDumpProcess: ProcessBuilderUseCase) {

operator fun invoke(
zimFile: File,
zimFolder: File
) {

val exceptionsFolder = File(zimFolder, FileConstants.EXCEPTIONS_FOLDER)
val indexHtmlFile = File(zimFolder, FileConstants.INDEX_HTML_FILE)

// If index.html already exists, assume the work is done and return
if (indexHtmlFile.exists()) {
return
}

// get the mainpage of the zim and rename it to index.html
val infoOutput = zimDumpProcess.invoke("info ${zimFile.absolutePath}")
val mainPageLine = infoOutput.lines().find { it.trim().startsWith("main page:") }
val mainPage = mainPageLine?.split(":")?.get(1)?.trim()
?: throw Exception("Zim mainPage not provided by zimdump")

// it will be located in either of these folders
val mainPageFile = File(zimFolder, mainPage)
val mainFile = File(exceptionsFolder, mainPage)

val fileToRename = when {
mainPageFile.exists() && mainPageFile.isFile -> mainPageFile
mainFile.exists() && mainFile.isFile -> mainFile
else -> throw Exception("Zim main page not found in extracted folder")
}

val successRename = fileToRename.renameTo(File(zimFolder, FileConstants.INDEX_HTML_FILE))
if (!successRename) {
throw Exception("Failed to rename $mainPage to ${FileConstants.INDEX_HTML_FILE}")
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.ustadmobile.zim2xapi

import com.ustadmobile.zim2xapi.models.ActivityDefinition
import com.ustadmobile.zim2xapi.models.XapiObject
import com.ustadmobile.zim2xapi.utils.FileConstants.INDEX_HTML_FILE
import kotlinx.serialization.json.Json
import org.jsoup.Jsoup
import java.io.File
Expand All @@ -25,7 +26,7 @@ class CreateXapiFileUseCase(
passingGrade: Int
): File {

val indexHtml = File(zimFolder, INDEX_HTML)
val indexHtml = File(zimFolder, INDEX_HTML_FILE)
val doc = Jsoup.parse(indexHtml, "UTF-8")
val title = doc.title()
val description = doc.select("meta[name=description]").attr("content")
Expand All @@ -46,7 +47,7 @@ class CreateXapiFileUseCase(
<activity id="$activityId" type="$ACTIVITY_TYPE">
<name>$title</name>
<description lang="$lang">$description</description>
<launch lang="$lang">$INDEX_HTML</launch>
<launch lang="$lang">$INDEX_HTML_FILE</launch>
</activity>
</activities>
</tincan>
Expand Down Expand Up @@ -90,7 +91,6 @@ class CreateXapiFileUseCase(
companion object {

const val TINCAN_XML = "tincan.xml"
const val INDEX_HTML = "index.html"

const val ACTIVITY_TYPE = "http://adlnet.gov/expapi/activities/module"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,22 @@
package com.ustadmobile.zim2xapi

import com.ustadmobile.zim2xapi.utils.FileConstants
import java.io.File
import java.net.URLDecoder

class FixExtractZimExceptionsUseCase(private val zimDumpProcess: ProcessBuilderUseCase) {
class FixExtractZimExceptionsUseCase {

operator fun invoke(
zimFile: File,
zimFolder: File
) {

val exceptionsFolder = File(zimFolder, EXCEPTIONS_FOLDER_NAME)

// get the mainpage of the zim and rename it to index.html
val infoOutput = zimDumpProcess.invoke("info ${zimFile.absolutePath}")
val mainPageLine = infoOutput.lines().find { it.trim().startsWith("main page:") }
val mainPage = mainPageLine?.split(":")?.get(1)?.trim()
?: throw Exception("Zim mainPage not provided by zimdump")

// it will be located in either of these folders
val mainPageFile = File(zimFolder, mainPage)
val mainFile = File(exceptionsFolder, mainPage)

val fileToRename = when {
mainPageFile.exists() && mainPageFile.isFile -> mainPageFile
mainFile.exists() && mainFile.isFile -> mainFile
else -> throw Exception("Zim main page not found in extracted folder")
}

val successRename = fileToRename.renameTo(File(zimFolder, INDEX_HTML))
if(!successRename){
throw Exception("Failed to rename $mainPage to $INDEX_HTML")
}
val exceptionsFolder = File(zimFolder, FileConstants.EXCEPTIONS_FOLDER)

if (!exceptionsFolder.exists()) {
// no errors found when extracting
return
}


// fix each file found in exceptions folder
exceptionsFolder.walkTopDown().forEach { file ->
if (file.isFile) {
Expand All @@ -57,20 +35,13 @@ class FixExtractZimExceptionsUseCase(private val zimDumpProcess: ProcessBuilderU
}

// check folder is empty
if(exceptionsFolder.listFiles()?.isNotEmpty() == true){
if (exceptionsFolder.listFiles()?.isNotEmpty() == true) {
throw Exception("Cannot proceed: There are unprocessed files in the exceptions folder")
}else{
} else {
if (!exceptionsFolder.delete()) {
throw Exception("Failed to delete the exceptions folder: ${exceptionsFolder.absolutePath}. Check if the folder is empty and accessible.")
}
}
}

companion object {

const val EXCEPTIONS_FOLDER_NAME = "_exceptions"
const val INDEX_HTML = "index.html"

}

}
71 changes: 71 additions & 0 deletions app/src/main/kotlin/com/ustadmobile/zim2xapi/ShrinkXapiUseCase.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.ustadmobile.zim2xapi

import com.ustadmobile.zim2xapi.utils.FileConstants.INDEX_HTML_FILE
import com.ustadmobile.zim2xapi.utils.FileConstants.ASSETS_FOLDER
import org.jsoup.Jsoup
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException

class ShrinkXapiUseCase {

operator fun invoke(
zimFolder: File
) {

val indexHtml = File(zimFolder, INDEX_HTML_FILE)
if (!indexHtml.exists()) throw FileNotFoundException("index html not created")
val assetsFolder = File(zimFolder, ASSETS_FOLDER)
if (!assetsFolder.exists()) throw FileNotFoundException("assets html not found")

val document = Jsoup.parse(indexHtml, "UTF-8")

val referencedPaths =
document.select("[href], [src]").map { it.attr("href") + it.attr("src") }.filter { it.isNotBlank() }
SUBFOLDERS.forEach { subfolder ->
val subfolderFile = File(assetsFolder, subfolder)
val isReferenced = referencedPaths.any { it.contains(subfolder, ignoreCase = true) }

if (!isReferenced) {
// Delete subfolder if not referenced
deleteFolderIfExists(subfolderFile)
}
}

deleteMapFiles(zimFolder)
}

private fun deleteMapFiles(folder: File) {
folder.walkTopDown().forEach {file ->
if (file.isFile && file.extension == ".map".trim('.')) {
file.delete()
}
}
}
private fun deleteFolderIfExists(folder: File) {
if (folder.exists() && folder.isDirectory) {
val foldedDeleted = folder.deleteRecursively()
if (!foldedDeleted) throw IOException("attempt to delete folder ${folder.name} failed")
} else {
throw FileNotFoundException("attempt to delete folder ${folder.name} failed: not found")
}
}

companion object {

const val VIDEOJS_FOLDER = "videojs"
const val PERSEUS_FOLDER = "perseus"
const val OGVJS_FOLDER = "ogvjs"
const val PDFJS_FOLDER = "pdfjs"
const val BOOTSTRAP_FOLDER = "bootstrap"
const val BOOTSTRAP_ICONS_FOLDER = "bootstrap-icons"

val SUBFOLDERS = listOf(
BOOTSTRAP_FOLDER, BOOTSTRAP_ICONS_FOLDER,
OGVJS_FOLDER, PDFJS_FOLDER, PERSEUS_FOLDER, VIDEOJS_FOLDER
)


}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.ustadmobile.zim2xapi.utils

object FileConstants {

const val INDEX_HTML_FILE = "index.html"
const val ASSETS_FOLDER = "assets"
const val EXCEPTIONS_FOLDER = "_exceptions"

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.ustadmobile.zim2xapi

import com.ustadmobile.zim2xapi.utils.FileConstants
import io.mockk.every
import io.mockk.mockk
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.io.File
import java.io.PrintWriter
import java.net.URLEncoder
import kotlin.io.path.createTempDirectory
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class CreateIndexHtmlUseCaseTest {

private val zimDumpProcess = mockk<ProcessBuilderUseCase>(relaxed = true)
private val createIndexHtmlUseCase = CreateIndexHtmlUseCase(zimDumpProcess)

private lateinit var zimFolder: File

@Before
fun setup(){
zimFolder = createTemporaryFolder("zimFolder")
}

@Test
fun `invoke should rename main page to index html`() {
// Arrange
val mainPageName = "main.html"
createFileInFolder(zimFolder, mainPageName)

mockZimDumpProcessMainPage("main page: $mainPageName")

// Act
createIndexHtmlUseCase.invoke(zimFile = zimFolder, zimFolder = zimFolder)

// Assert
val indexHtmlFile = File(zimFolder, "index.html")
assertTrue(indexHtmlFile.exists(), "index.html should be created by renaming the main page")
}

@Test
fun `invoke should throw exception if main page is not found`() {
// Arrange
val zimFolder = createTemporaryFolder("zimFolder")

mockZimDumpProcessMainPage("main page: non_existent.html")

// Act & Assert
assertFailsWith<Exception>("Zim main page not found in extracted folder") {
createIndexHtmlUseCase.invoke(zimFile = zimFolder, zimFolder = zimFolder)
}

}

@Test
fun `invoke should skip renaming if index html already exists`() {
// Arrange
val zimFolder = createTemporaryFolder("zimFolder")
val indexHtmlFile = createFileInFolder(zimFolder, "index.html")

// Mock the main page (though it shouldn't matter because index.html exists)
mockZimDumpProcessMainPage("main page: main.html")

// Act
createIndexHtmlUseCase.invoke(zimFile = zimFolder, zimFolder = zimFolder)

// Assert
assertTrue(indexHtmlFile.exists(), "index.html should still exist")
}

@After
fun cleanUp(){
cleanupTempDirs(zimFolder)
}


private fun createTemporaryFolder(name: String): File {
return createTempDirectory(name).toFile()
}
private fun createFileInFolder(folder: File, fileName: String): File {
val file = File(folder, fileName)
PrintWriter(file).use { writer ->
writer.println("Sample content")
}
return file
}

private fun mockZimDumpProcessMainPage(mainPage: String) {
every { zimDumpProcess.invoke(any()) } returns mainPage
}

private fun cleanupTempDirs(vararg dirs: File) {
dirs.forEach { it.deleteRecursively() }
}

}
Loading