From 4f843dda0f8afbc37688d7753bc93790c7fa9841 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 25 Jun 2022 11:34:22 +1000 Subject: [PATCH] Enable backend media handling; add media check --- .../scopedstorage/MigrateEssentialFiles.kt | 7 +- .../java/com/ichi2/async/CollectionTask.kt | 10 ++- .../java/com/ichi2/libanki/BackendMedia.kt | 71 +++++++++++++++++++ .../main/java/com/ichi2/libanki/Collection.kt | 12 +++- .../java/com/ichi2/libanki/CollectionV16.kt | 17 +++++ .../main/java/com/ichi2/libanki/Media.java | 4 +- .../MigrateEssentialFilesTest.kt | 4 +- .../java/com/ichi2/libanki/CheckMediaTest.kt | 5 ++ 8 files changed, 119 insertions(+), 11 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/libanki/BackendMedia.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/scopedstorage/MigrateEssentialFiles.kt b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/scopedstorage/MigrateEssentialFiles.kt index 3eaa3fb004ce..4f8cbdc05bfb 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/scopedstorage/MigrateEssentialFiles.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/scopedstorage/MigrateEssentialFiles.kt @@ -487,7 +487,12 @@ internal constructor( */ val PRIORITY_FILES = listOf( SQLiteDBFiles("collection.anki2"), // Anki collection - SQLiteDBFiles("collection.media.ad.db2"), // media database + journal + if (BackendFactory.defaultLegacySchema) { + SQLiteDBFiles("collection.media.ad.db2") + } else { + // this is created on demand in the new backend + OptionalFile("collection.media.db") + }, // media database + journal OptionalFile(".nomedia"), // written immediately OptionalFile("collection.log") // written immediately and conflicts ) diff --git a/AnkiDroid/src/main/java/com/ichi2/async/CollectionTask.kt b/AnkiDroid/src/main/java/com/ichi2/async/CollectionTask.kt index 1efba75eba8f..d5ee3ed1ebac 100644 --- a/AnkiDroid/src/main/java/com/ichi2/async/CollectionTask.kt +++ b/AnkiDroid/src/main/java/com/ichi2/async/CollectionTask.kt @@ -40,6 +40,7 @@ import com.ichi2.libanki.sched.DeckTreeNode import com.ichi2.libanki.sched.TreeNode import com.ichi2.utils.* import com.ichi2.utils.SyncStatus.Companion.ignoreDatabaseModification +import net.ankiweb.rsdroid.BackendFactory import net.ankiweb.rsdroid.RustCleanup import org.apache.commons.compress.archivers.zip.ZipFile import timber.log.Timber @@ -1078,8 +1079,13 @@ open class CollectionTask(val task: TaskDelegateBase) : TaskDelegate() { override fun task(col: Collection, collectionTask: ProgressSenderAndCancelListener): Int { val m = col.media - for (fname in unused) { - m.removeFile(fname) + if (!BackendFactory.defaultLegacySchema) { + // FIXME: this provides progress info that is not currently used + col.newMedia.removeFiles(unused) + } else { + for (fname in unused) { + m.removeFile(fname) + } } return unused.size } diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/BackendMedia.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/BackendMedia.kt new file mode 100644 index 000000000000..9a0d09f9155d --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/BackendMedia.kt @@ -0,0 +1,71 @@ +/*************************************************************************************** + * Copyright (c) 2012 Ankitects Pty Ltd * + * * + * This program is free software; you can redistribute it and/or modify it under * + * the terms of the GNU General Public License as published by the Free Software * + * Foundation; either version 3 of the License, or (at your option) any later * + * version. * + * * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY * + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * + * PARTICULAR PURPOSE. See the GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License along with * + * this program. If not, see . * + ****************************************************************************************/ + +package com.ichi2.libanki + +class BackendMedia(val col: CollectionV16, server: Boolean) : Media(col, server) { + override fun connect() { + // no-op + } + + override fun close() { + // no-op + } + + override fun rebuildIfInvalid() { + // no-op + } + + override fun findChanges(force: Boolean) { + // no-op + } + + override fun markFileAdd(fname: String) { + // no-op; will no longer be called when migrating to new import code. + } + + override fun forceResync() { + col.backend.removeMediaDb(col.path) + } + + override fun removeFile(fname: String) { + removeFiles(listOf(fname)) + } + + // markFileAdd + + // FIXME: this also provides trash count, but UI can not handle it yet + override fun check(): List> { + val out = col.backend.checkMedia() + return listOf(out.missingList, out.unusedList, listOf()) + } + + // FIXME: this currently removes files immediately, as the UI does not expose a way + // to empty the trash or restore media files yet + fun removeFiles(files: Iterable) { + col.backend.trashMediaFiles(files) + emptyTrash() + } + + private fun emptyTrash() { + col.backend.emptyTrash() + } + + @Suppress("UNUSED") + private fun restoreTrash() { + col.backend.restoreTrash() + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.kt index 585048ba6e97..2c0c248d7479 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.kt +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.kt @@ -100,6 +100,8 @@ open class Collection constructor( open val newBackend: CollectionV16 get() = throw Exception("invalid call to newBackend on old backend") + open val newMedia: BackendMedia + get() = throw Exception("invalid call to newMedia on old backend") @VisibleForTesting(otherwise = VisibleForTesting.NONE) fun debugEnsureNoOpenPointers() { @@ -144,8 +146,8 @@ open class Collection constructor( private var mStartReps: Int // BEGIN: SQL table columns - var crt: Long = 0 - var mod: Long = 0 + open var crt: Long = 0 + open var mod: Long = 0 var scm: Long = 0 var dirty: Boolean = false private var mUsn = 0 @@ -163,7 +165,7 @@ open class Collection constructor( private var mLogHnd: PrintWriter? = null init { - media = Media(this, server) + media = initMedia() val created = reopen() log(path, VersionUtils.pkgVersionName) // mLastSave = getTime().now(); // assigned but never accessed - only leaving in for upstream comparison @@ -186,6 +188,10 @@ open class Collection constructor( } } + protected open fun initMedia(): Media { + return Media(this, server) + } + @KotlinCleanup("remove :DeckManager, remove ? on return value") protected open fun initDecks(deckConf: String?): DeckManager? { val deckManager: DeckManager = Decks(this) diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/CollectionV16.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/CollectionV16.kt index 01e591c90cdc..97dd0cb31051 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/CollectionV16.kt +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/CollectionV16.kt @@ -48,9 +48,26 @@ class CollectionV16( return ConfigV16(RustConfigBackend(backend)) } + override fun initMedia(): BackendMedia { + return BackendMedia(this, server) + } + override val newBackend: CollectionV16 get() = this + override val newMedia: BackendMedia + get() = this.media as BackendMedia + + override fun flush(mod: Long) { + // no-op + } + + override var mod: Long = 0 + get() = db.queryLongScalar("select mod from col") + + override var crt: Long = 0 + get() = db.queryLongScalar("select crt from col") + /** col.conf is now unused, handled by [ConfigV16] which has a separate table */ override fun flushConf(): Boolean = false diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/Media.java b/AnkiDroid/src/main/java/com/ichi2/libanki/Media.java index 01cfd9f16557..38e6ee8f6711 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/Media.java +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/Media.java @@ -141,8 +141,6 @@ public Media(Collection col, boolean server) { Timber.e("Cannot create media directory: %s", mDir); } } - // change database -// connect(); } @@ -182,7 +180,7 @@ public void _initDB() { } - public void maybeUpgrade() { + private void maybeUpgrade() { String oldPath = dir() + ".db"; File oldDbFile = new File(oldPath); if (oldDbFile.exists()) { diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/servicelayer/scopedstorage/MigrateEssentialFilesTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/servicelayer/scopedstorage/MigrateEssentialFilesTest.kt index 07ab8607eecc..965372619fef 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/servicelayer/scopedstorage/MigrateEssentialFilesTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/servicelayer/scopedstorage/MigrateEssentialFilesTest.kt @@ -215,7 +215,7 @@ class MigrateEssentialFilesTest : RobolectricTest() { col.close() // required for Windows, can't delete if locked. - CompatHelper.compat.deleteFile(File(defaultCollectionSourcePath, "collection.media.ad.db2")) + CompatHelper.compat.deleteFile(File(defaultCollectionSourcePath, "collection.anki2")) val ex = assertThrows { executeAlgorithmSuccessfully(defaultCollectionSourcePath) { @@ -223,7 +223,7 @@ class MigrateEssentialFilesTest : RobolectricTest() { } } - assertThat(ex.file.name, equalTo("collection.media.ad.db2")) + assertThat(ex.file.name, equalTo("collection.anki2")) } /** diff --git a/AnkiDroid/src/test/java/com/ichi2/libanki/CheckMediaTest.kt b/AnkiDroid/src/test/java/com/ichi2/libanki/CheckMediaTest.kt index b2aace7af642..34231472ed0b 100644 --- a/AnkiDroid/src/test/java/com/ichi2/libanki/CheckMediaTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/libanki/CheckMediaTest.kt @@ -21,6 +21,7 @@ import com.ichi2.anki.RunInBackground import com.ichi2.async.CollectionTask import com.ichi2.async.CollectionTask.CheckMedia import com.ichi2.async.TaskManager +import net.ankiweb.rsdroid.BackendFactory import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat import org.junit.Test @@ -38,6 +39,10 @@ class CheckMediaTest : RobolectricTest() { @Suppress("deprecation") @Throws(ExecutionException::class, InterruptedException::class) fun checkMediaWorksAfterMissingMetaTable() { + if (!BackendFactory.defaultLegacySchema) { + // this should not happen on the backend, as it creates the tables in a transaction + return + } // 7421 col.media.db.database.execSQL("drop table meta") assertThat(