diff --git a/app/build.gradle b/app/build.gradle index fd4ef939f..3c4ae917f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { minSdkVersion 21 targetSdkVersion 30 versionCode gitCommits - versionName '0.5.2' + versionName '0.5.3' kapt { arguments { diff --git a/app/src/main/java/org/koitharu/kotatsu/core/local/WritableCbzFile.kt b/app/src/main/java/org/koitharu/kotatsu/core/local/WritableCbzFile.kt new file mode 100644 index 000000000..8ddbf6631 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/local/WritableCbzFile.kt @@ -0,0 +1,93 @@ +package org.koitharu.kotatsu.core.local + +import androidx.annotation.CheckResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +class WritableCbzFile(private val file: File) { + + private val dir = File(file.parentFile, file.nameWithoutExtension) + + @Suppress("BlockingMethodInNonBlockingContext") + suspend fun prepare() = withContext(Dispatchers.IO) { + check(dir.list().isNullOrEmpty()) { + "Dir ${dir.name} is not empty" + } + if (!dir.exists()) { + dir.mkdir() + } + ZipInputStream(FileInputStream(file)).use { zip -> + var entry = zip.nextEntry + while (entry != null) { + val target = File(dir.path + File.separator + entry.name) + target.parentFile?.mkdirs() + target.outputStream().use { out -> + zip.copyTo(out) + } + zip.closeEntry() + entry = zip.nextEntry + } + } + } + + suspend fun cleanup() = withContext(Dispatchers.IO) { + dir.deleteRecursively() + } + + @CheckResult + @Suppress("BlockingMethodInNonBlockingContext") + suspend fun flush() = withContext(Dispatchers.IO) { + val tempFile = File(file.path + ".tmp") + if (tempFile.exists()) { + tempFile.delete() + } + try { + ZipOutputStream(FileOutputStream(tempFile)).use { zip -> + dir.listFiles()?.forEach { + zipFile(it, it.name, zip) + } + zip.flush() + } + tempFile.renameTo(file) + } finally { + if (tempFile.exists()) { + tempFile.delete() + } + } + } + + operator fun get(name: String) = File(dir, name) + + operator fun set(name: String, file: File) { + file.copyTo(this[name], overwrite = true) + } + + companion object { + + private fun zipFile(fileToZip: File, fileName: String, zipOut: ZipOutputStream) { + if (fileToZip.isDirectory) { + if (fileName.endsWith("/")) { + zipOut.putNextEntry(ZipEntry(fileName)) + } else { + zipOut.putNextEntry(ZipEntry("$fileName/")) + } + zipOut.closeEntry() + fileToZip.listFiles()?.forEach { childFile -> + zipFile(childFile, "$fileName/${childFile.name}", zipOut) + } + } else { + FileInputStream(fileToZip).use { fis -> + val zipEntry = ZipEntry(fileName) + zipOut.putNextEntry(zipEntry) + fis.copyTo(zipOut) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/domain/local/MangaZip.kt b/app/src/main/java/org/koitharu/kotatsu/domain/local/MangaZip.kt index 8a48d40d9..b6672aa31 100644 --- a/app/src/main/java/org/koitharu/kotatsu/domain/local/MangaZip.kt +++ b/app/src/main/java/org/koitharu/kotatsu/domain/local/MangaZip.kt @@ -1,63 +1,35 @@ package org.koitharu.kotatsu.domain.local +import androidx.annotation.CheckResult import androidx.annotation.WorkerThread +import org.koitharu.kotatsu.core.local.WritableCbzFile import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaChapter -import org.koitharu.kotatsu.utils.ext.sub import org.koitharu.kotatsu.utils.ext.takeIfReadable import org.koitharu.kotatsu.utils.ext.toFileNameSafe import java.io.File -import java.util.zip.ZipEntry -import java.util.zip.ZipInputStream -import java.util.zip.ZipOutputStream @WorkerThread class MangaZip(val file: File) { - private val dir = file.parentFile?.sub(file.name + ".tmp")?.takeIf { it.mkdir() } - ?: throw RuntimeException("Cannot create temporary directory") + private val writableCbz = WritableCbzFile(file) private var index = MangaIndex(null) - fun prepare(manga: Manga) { - extract() - index = MangaIndex(dir.sub(INDEX_ENTRY).takeIfReadable()?.readText()) + suspend fun prepare(manga: Manga) { + writableCbz.prepare() + index = MangaIndex(writableCbz[INDEX_ENTRY].takeIfReadable()?.readText()) index.setMangaInfo(manga, append = true) } - fun cleanup() { - dir.deleteRecursively() + suspend fun cleanup() { + writableCbz.cleanup() } - fun compress() { - dir.sub(INDEX_ENTRY).writeText(index.toString()) - ZipOutputStream(file.outputStream()).use { out -> - for (file in dir.listFiles().orEmpty()) { - val entry = ZipEntry(file.name) - out.putNextEntry(entry) - file.inputStream().use { stream -> - stream.copyTo(out) - } - out.closeEntry() - } - } - } - - private fun extract() { - if (!file.exists()) { - return - } - ZipInputStream(file.inputStream()).use { input -> - while (true) { - val entry = input.nextEntry ?: return - if (!entry.isDirectory) { - dir.sub(entry.name).outputStream().use { out -> - input.copyTo(out) - } - } - input.closeEntry() - } - } + @CheckResult + suspend fun compress(): Boolean { + writableCbz[INDEX_ENTRY].writeText(index.toString()) + return writableCbz.flush() } fun addCover(file: File, ext: String) { @@ -68,7 +40,7 @@ class MangaZip(val file: File) { append(ext) } } - file.copyTo(dir.sub(name), overwrite = true) + writableCbz[name] = file index.setCoverEntry(name) } @@ -80,7 +52,7 @@ class MangaZip(val file: File) { append(ext) } } - file.copyTo(dir.sub(name), overwrite = true) + writableCbz[name] = file index.addChapter(chapter) } diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt index 9e596ad8b..776af0ee0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt @@ -142,7 +142,9 @@ class DownloadService : BaseService() { notification.setCancelId(0) notification.setPostProcessing() notification.update() - output.compress() + if (!output.compress()) { + throw RuntimeException("Cannot create target file") + } val result = MangaProviderFactory.createLocal().getFromFile(output.file) notification.setDone(result) notification.dismiss()